/* Copyright 2022 CAcert Inc. SPDX-License-Identifier: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config_test import ( "crypto/elliptic" "crypto/x509" "crypto/x509/pkix" "math/big" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" "git.cacert.org/cacert-gosigner/pkg/config" ) type TestCurve struct { } func (t TestCurve) Params() *elliptic.CurveParams { panic("not needed") } func (t TestCurve) IsOnCurve(_, _ *big.Int) bool { panic("not needed") } func (t TestCurve) Add(_, _, _, _ *big.Int) (x, y *big.Int) { panic("not needed") } func (t TestCurve) Double(_, _ *big.Int) (x, y *big.Int) { panic("not needed") } func (t TestCurve) ScalarMult(_, _ *big.Int, _ []byte) (x, y *big.Int) { panic("not needed") } func (t TestCurve) ScalarBaseMult(_ []byte) (x, y *big.Int) { panic("not needed") } func TestPrivateKeyInfo_MarshalYAML(t *testing.T) { testCurve := TestCurve{} testData := []struct { name string pkInfo *config.PrivateKeyInfo expected string expectErr bool }{ { name: "RSA", pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.RSA, RSABits: 3072, }, expected: `algorithm: RSA rsa-bits: 3072 `, }, { name: "ECDSA P-224", pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P224(), }, expected: `algorithm: EC ecc-curve: P-224 `, }, { name: "ECDSA P-256", pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P256(), }, expected: `algorithm: EC ecc-curve: P-256 `, }, { name: "ECDSA P-384", pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P384(), }, expected: `algorithm: EC ecc-curve: P-384 `, }, { name: "ECDSA P-521", pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P521(), }, expected: `algorithm: EC ecc-curve: P-521 `, }, { name: "ECDSA unsupported curve", pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: testCurve, }, expectErr: true, }, } for _, item := range testData { t.Run(item.name, func(t *testing.T) { data, err := yaml.Marshal(item.pkInfo) if item.expectErr { require.Error(t, err) return } require.NoError(t, err) assert.YAMLEq(t, item.expected, string(data)) }) } } func TestPrivateKeyInfo_UnmarshalYAML(t *testing.T) { testData := []struct { name string yaml string expected *config.PrivateKeyInfo expectErr bool }{ { name: "malformed", yaml: `noyaml`, expectErr: true, }, { name: "RSA", yaml: `label: "mykey" algorithm: "RSA" rsa-bits: 2048`, expected: &config.PrivateKeyInfo{ Algorithm: x509.RSA, RSABits: 2048, }, }, { name: "ECDSA P-224", yaml: `label: "mykey" algorithm: "EC" ecc-curve: "P-224"`, expected: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P224(), }, }, { name: "ECDSA P-256", yaml: `label: "mykey" algorithm: "EC" ecc-curve: "P-256"`, expected: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P256(), }, }, { name: "ECDSA P-384", yaml: `label: "mykey" algorithm: "EC" ecc-curve: "P-384"`, expected: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P384(), }, }, { name: "ECDSA P-521", yaml: `label: "mykey" algorithm: "EC" ecc-curve: "P-521"`, expected: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P521(), }, }, { name: "no-algorithm", yaml: `label: "mykey"`, expectErr: true, }, { name: "RSA-no-rsa-bits", yaml: `label: "mykey" algorithm: "RSA"`, expectErr: true, }, { name: "ECDSA-no-curve", yaml: `label: "mykey" algorithm: "EC"`, expectErr: true, }, { name: "ECDSA-invalid-curve", yaml: `label: "mykey" algorithm: "EC" ecc-curve: "fancy-but-wrong"`, expectErr: true, }, { name: "wrong-key-algorithm", yaml: `label: "mykey" algorithm: "ChaCha"`, expectErr: true, }, } for _, item := range testData { t.Run(item.name, func(t *testing.T) { pkInfo := &config.PrivateKeyInfo{} err := yaml.Unmarshal([]byte(item.yaml), pkInfo) if err != nil && !item.expectErr { require.NoError(t, err) } if item.expectErr { assert.Error(t, err) } if !item.expectErr { assert.Equal(t, item.expected, pkInfo) } }) } } func TestCaCertificateEntry_UnmarshalYAML(t *testing.T) { testData := []struct { name, yaml string expected config.CaCertificateEntry expectErr bool }{ { name: "no yaml", yaml: "no yaml", expectErr: true, }, { name: "no common name", yaml: ` key-info: algorithm: EC ecc-curve: P-521 `, expectErr: true, }, { name: "no key info", yaml: ` common-name: My Little Test Root CA `, expectErr: true, }, { name: "EC P-521", yaml: ` key-info: algorithm: EC ecc-curve: P-521 common-name: My Little Test Root CA ext-key-usages: - client - server `, expectErr: true, }, { name: "EC root P-521", yaml: ` key-info: algorithm: EC ecc-curve: P-521 common-name: My Little Test Root CA storage: root `, expected: config.CaCertificateEntry{ KeyInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P521(), }, CommonName: "My Little Test Root CA", Storage: "root", }, }, { name: "EC sub P-256 client server", yaml: ` key-info: algorithm: EC ecc-curve: P-256 common-name: My Little Test Sub CA parent: root ext-key-usages: - client - server `, expected: config.CaCertificateEntry{ KeyInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P256(), }, Parent: "root", CommonName: "My Little Test Sub CA", Storage: "default", ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, }, }, }, { name: "EC sub P-256 person", yaml: ` key-info: algorithm: EC ecc-curve: P-256 common-name: My Little Test Sub CA parent: root ext-key-usages: - client - code - email - ocsp `, expected: config.CaCertificateEntry{ KeyInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P256(), }, CommonName: "My Little Test Sub CA", Storage: "default", Parent: "root", ExtKeyUsage: []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageEmailProtection, x509.ExtKeyUsageOCSPSigning, }, }, }, { name: "invalid ext-key-usages", yaml: ` key-info: algorithm: EC ecc-curve: P-256 common-name: My Little Test Sub CA parent: root ext-key-usages: - wrong_one `, expectErr: true, }, } for _, item := range testData { t.Run(item.name, func(t *testing.T) { entry := config.CaCertificateEntry{} err := yaml.Unmarshal([]byte(item.yaml), &entry) if item.expectErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, item.expected, entry) }) } } func TestLoadConfiguration(t *testing.T) { testData := map[string]struct { yaml string errMsg string }{ "Good": { yaml: `--- Settings: organization: organization: ["Acme CAs Ltd."] validity-years: root: 20 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s issuer: http://%s.cas.example.org/ KeyStorage: default: type: softhsm label: ca-storage CAs: root: common-name: "Root CA" key-info: algorithm: EC ecc-curve: P-384 sub1: common-name: "Sub CA 1" key-info: algorithm: EC ecc-curve: P-256 parent: root sub2: common-name: "Sub CA 2" key-info: algorithm: EC ecc-curve: P-256 parent: root `, }, "Bad": { yaml: `no yaml for you: ]`, errMsg: "could not parse YAML configuration", }, "NoGlobal": { yaml: `--- KeyStorage: default: label: default type: softhsm CAs: root: common-name: "Root CA" key-info: algorithm: EC ecc-curve: P-384 `, errMsg: "configuration entry 'Settings' is missing or empty", }, "NoKeyStorage": { yaml: ` Settings: organization: organization: ["Acme CAs Ltd."] validity-years: root: 20 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ CAs: root: common-name: "Root CA" key-info: algorithm: EC ecc-curve: P-384 `, errMsg: "configuration entry 'KeyStorage' is missing or empty", }, "NoCAs": { yaml: ` Settings: organization: organization: ["Acme CAs Ltd."] validity-years: root: 20 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ KeyStorage: default: label: default type: softhsm `, errMsg: "configuration entry 'CAs' is missing or empty", }, } for name, item := range testData { t.Run(name, func(t *testing.T) { r := strings.NewReader(item.yaml) sc, err := config.LoadConfiguration(r) if item.errMsg != "" { assert.ErrorContains(t, err, item.errMsg) assert.Nil(t, sc) } else { assert.NoError(t, err) assert.NotNil(t, sc) } }) } } const sampleConfig = `--- Settings: organization: organization: ["Acme CAs Ltd."] validity-years: root: 20 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ KeyStorage: default: type: softhsm label: ca-storage CAs: root: common-name: "Root CA" key-info: algorithm: EC ecc-curve: P-384 sub1: common-name: "Sub CA 1" key-info: algorithm: EC ecc-curve: P-256 parent: root sub2: common-name: "Sub CA 2" key-info: algorithm: EC ecc-curve: P-256 parent: root ` func loadSignerConfig(t *testing.T) *config.SignerConfig { r := strings.NewReader(sampleConfig) sc, err := config.LoadConfiguration(r) require.NoError(t, err) require.NotNil(t, sc) return sc } func TestSignerConfig_RootCAs(t *testing.T) { sc := loadSignerConfig(t) roots := sc.RootCAs() assert.Equal(t, roots, []string{"root"}) } func TestSignerConfig_IntermediaryCAs(t *testing.T) { sc := loadSignerConfig(t) cAs := sc.IntermediaryCAs() assert.ElementsMatch(t, cAs, []string{"sub1", "sub2"}) } func TestSignerConfig_GetParentCA(t *testing.T) { testData := map[string]struct{ errMsg string }{ "root": { errMsg: "CA root is a root CA and has no parent", }, "unknown": { errMsg: "no CA definition for unknown", }, "sub1": {}, } sc := loadSignerConfig(t) for name, item := range testData { t.Run(name, func(t *testing.T) { ca, err := sc.GetParentCA(name) if item.errMsg != "" { assert.ErrorContains(t, err, item.errMsg) } else { require.NoError(t, err) assert.Equal(t, "Root CA", ca.CommonName) } }) } } func TestSignerConfig_CertificateFileName(t *testing.T) { sc := loadSignerConfig(t) assert.Equal(t, "root.crt", sc.CertificateFileName("root")) } func TestSignerConfig_BuildCRLUrl(t *testing.T) { sc := loadSignerConfig(t) ca, err := sc.GetCADefinition("sub1") require.NoError(t, err) url := sc.BuildCRLUrl(ca) assert.Equal(t, "http://crl.example.org/root.crl", url) } func TestSignerConfig_BuildIssuerUrl(t *testing.T) { sc := loadSignerConfig(t) ca, err := sc.GetCADefinition("sub1") require.NoError(t, err) url := sc.BuildIssuerURL(ca) assert.Equal(t, "http://root.cas.example.org/", url) } func TestSignerConfig_BuildOCSPURL(t *testing.T) { sc := loadSignerConfig(t) ca, err := sc.GetCADefinition("sub1") require.NoError(t, err) url := sc.BuildOCSPURL(ca) assert.Equal(t, "http://ocsp.example.org/", url) } func TestSignerConfig_BuildOCSPURL2(t *testing.T) { r := strings.NewReader(`--- Settings: organization: organization: ["Acme CAs Ltd."] validity-years: root: 20 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/%s crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ KeyStorage: default: type: softhsm label: ca-storage CAs: root: common-name: "Root CA" key-info: algorithm: EC ecc-curve: P-384 sub1: common-name: "Sub CA 1" key-info: algorithm: EC ecc-curve: P-256 parent: root sub2: common-name: "Sub CA 2" key-info: algorithm: EC ecc-curve: P-256 parent: root `) sc, err := config.LoadConfiguration(r) require.NoError(t, err) require.NotNil(t, sc) ca, err := sc.GetCADefinition("sub1") require.NoError(t, err) url := sc.BuildOCSPURL(ca) assert.Equal(t, "http://ocsp.example.org/root", url) } func TestSettings_UnmarshalYAML(t *testing.T) { testData := map[string]struct { yaml string expected config.Settings errMsg string }{ "no yaml": { yaml: "no yaml", errMsg: "could not decode YAML", }, "missing organization": { yaml: ` validity-years: root: 10 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://issuer.example.org/%s.crt `, errMsg: "invalid Settings you need to specify 'organization'", }, "missing validity-years": { yaml: ` organization: organization: ["Acme CAs Ltd."] url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://issuer.example.org/%s.crt `, errMsg: "invalid Settings you must specify validity years for 'root' and 'intermediary'", }, "missing url-patterns": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 10 intermediary: 5 `, errMsg: "invalid Settings", }, "invalid validity-years": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 5 intermediary: 10 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://issuer.example.org/%s.crt `, errMsg: "invalid Settings validity of root CA certificates must be equal or greater than " + "those of intermediary CA certificates", }, "no OCSP pattern": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 10 intermediary: 5 url-patterns: crl: http://crl.example.org/%s.crl issuer: http://issuer.example.org/%s.crt `, errMsg: "invalid Settings you must specify an 'ocsp' URL pattern", }, "invalid OCSP pattern": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 10 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/%s_%s crl: http://crl.example.org/%s.crl issuer: http://issuer.example.org/%s.crt `, errMsg: "invalid Settings url-pattern 'ocsp' must contain zero or one '%s' placeholder", }, "no CRL pattern": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 10 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ issuer: http://issuer.example.org/%s.crt `, errMsg: "invalid Settings you must specify an 'crl' URL pattern", }, "invalid CRL pattern": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 10 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/ issuer: http://issuer.example.org/%s.crt `, errMsg: "invalid Settings url-pattern 'crl' must contain one '%s' placeholder", }, "no issuer pattern": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 10 intermediary: 5 url-patterns: crl: http://crl.example.org/%s.crl ocsp: http://ocsp.example.org/ `, errMsg: "invalid Settings you must specify an 'issuer' URL pattern", }, "invalid issuer pattern": { yaml: ` organization: organization: ["Acme CAs Ltd."] validity-years: root: 10 intermediary: 5 url-patterns: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://issuer.example.org/ `, errMsg: "invalid Settings url-pattern 'issuer' must contain one '%s' placeholder", }, } for name, item := range testData { t.Run(name, func(t *testing.T) { var s config.Settings err := yaml.Unmarshal([]byte(item.yaml), &s) if item.errMsg != "" { assert.ErrorContains(t, err, item.errMsg) } else { require.NoError(t, err) assert.NotNil(t, s) assert.Equal(t, item.expected, s) } }) } } func TestKeyStorage_UnmarshalYAML(t *testing.T) { testData := map[string]struct { yaml string expected config.KeyStorage errMsg string }{ "no yaml": { yaml: `no yaml`, errMsg: "could not decode YAML:", }, "softhsm": { yaml: ` label: softhsm type: softhsm`, expected: config.KeyStorage{ Label: "softhsm", Module: config.SoftHsmModule, }, }, "custom": { yaml: ` label: custom type: p11module module: /usr/lib/mytokenp11.so`, expected: config.KeyStorage{ Label: "custom", Module: "/usr/lib/mytokenp11.so", }, }, "missing module": { yaml: ` label: custom type: p11module`, errMsg: "specify a 'module' field when using the 'p11module' type", }, "unsupported type": { yaml: ` label: something type: something`, errMsg: "unsupported KeyStorage type 'something'", }, "missing label": { yaml: ` type: softhsm`, errMsg: "element 'label' must be specified", }, } for name, item := range testData { t.Run(name, func(t *testing.T) { var ks config.KeyStorage err := yaml.Unmarshal([]byte(item.yaml), &ks) if item.errMsg != "" { assert.ErrorContains(t, err, item.errMsg) } else { assert.NoError(t, err) assert.Equal(t, item.expected, ks) } }) } } func TestSignerConfig_GetCADefinition(t *testing.T) { sc := loadSignerConfig(t) def, err := sc.GetCADefinition("unknown") assert.ErrorContains(t, err, "no CA definition found for label unknown") assert.Nil(t, def) } func TestSignerConfig_CalculateValidity(t *testing.T) { sc := loadSignerConfig(t) for _, ca := range []string{"root", "sub1"} { t.Run(ca, func(t *testing.T) { def, err := sc.GetCADefinition(ca) require.NoError(t, err) now := time.Now() notBefore, notAfter := sc.CalculateValidity(def, now) assert.True(t, now.Equal(notBefore)) assert.True(t, now.Before(notAfter)) }) } } func TestSignerConfig_CalculateSubject(t *testing.T) { testData := map[string]struct { cn string }{ "root": {cn: "Root CA"}, "sub1": {cn: "Sub CA 1"}, } sc := loadSignerConfig(t) for name, item := range testData { t.Run(name, func(t *testing.T) { def, err := sc.GetCADefinition(name) require.NoError(t, err) subject := sc.CalculateSubject(def) assert.Equal(t, pkix.Name{Organization: []string{"Acme CAs Ltd."}, CommonName: item.cn}, subject) }) } }