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 if 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) }) } }