From 42c7dc71708e1d41b877ffa4bb3761d8aa66a63a Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 23 Apr 2022 18:34:51 +0200 Subject: [PATCH] Improve config handling and test coverage --- pkg/config/amd64.go | 2 +- pkg/config/arm64.go | 2 +- pkg/config/armhf.go | 2 +- pkg/config/config.go | 138 +++++-- pkg/config/config_test.go | 833 ++++++++++++++++++++++++++++++++++---- pkg/hsm/hsm.go | 5 +- 6 files changed, 876 insertions(+), 106 deletions(-) diff --git a/pkg/config/amd64.go b/pkg/config/amd64.go index 7e76301..8e5103c 100644 --- a/pkg/config/amd64.go +++ b/pkg/config/amd64.go @@ -2,4 +2,4 @@ package config -const softHsmModule = "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so" +const SoftHsmModule = "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so" diff --git a/pkg/config/arm64.go b/pkg/config/arm64.go index b2c7386..11f1ad6 100644 --- a/pkg/config/arm64.go +++ b/pkg/config/arm64.go @@ -2,4 +2,4 @@ package config -const softHsmModule = "/usr/lib/aarch64-linux-gnu/softhsm/libsofthsm2.so" +const SoftHsmModule = "/usr/lib/aarch64-linux-gnu/softhsm/libsofthsm2.so" diff --git a/pkg/config/armhf.go b/pkg/config/armhf.go index 98266bf..9a6fbe1 100644 --- a/pkg/config/armhf.go +++ b/pkg/config/armhf.go @@ -2,4 +2,4 @@ package config -const softHsmModule = "/usr/lib/arm-linux-gnueabihf/softhsm/libsofthsm2.so" +const SoftHsmModule = "/usr/lib/arm-linux-gnueabihf/softhsm/libsofthsm2.so" diff --git a/pkg/config/config.go b/pkg/config/config.go index 65eff96..97a8ab7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,22 +15,96 @@ import ( ) type Settings struct { - Organization struct { - Country []string `yaml:"country"` - Organization []string `yaml:"organization"` - Locality []string `yaml:"locality"` - StreetAddress []string `yaml:"street-address"` - PostalCode []string `yaml:"postal-code"` - } `yaml:"organization"` + Organization *pkix.Name ValidityYears struct { - Root int `yaml:"root"` - Intermediary int `yaml:"intermediary"` - } `yaml:"validity-years"` + Root, Intermediary int + } URLPatterns struct { - Ocsp string `yaml:"ocsp"` - CRL string `yaml:"crl"` - Issuer string `yaml:"issuer"` - } `yaml:"url-patterns"` + Ocsp, CRL, Issuer string + } +} + +type SettingsError struct { + msg string +} + +func (se SettingsError) Error() string { + return fmt.Sprintf("invalid Settings %s", se.msg) +} + +func (s *Settings) UnmarshalYAML(n *yaml.Node) error { + data := struct { + Organization struct { + Country []string `yaml:"country"` + Organization []string `yaml:"organization"` + Locality []string `yaml:"locality"` + StreetAddress []string `yaml:"street-address"` + PostalCode []string `yaml:"postal-code"` + } `yaml:"organization"` + ValidityYears struct { + Root int `yaml:"root"` + Intermediary int `yaml:"intermediary"` + } `yaml:"validity-years"` + URLPatterns struct { + Ocsp string `yaml:"ocsp"` + CRL string `yaml:"crl"` + Issuer string `yaml:"issuer"` + } `yaml:"url-patterns"` + }{} + + err := n.Decode(&data) + if err != nil { + return fmt.Errorf("could not decode YAML: %w", err) + } + + if data.Organization.Organization == nil { + return SettingsError{"you need to specify 'organization'"} + } + + if data.ValidityYears.Root == 0 || data.ValidityYears.Intermediary == 0 { + return SettingsError{"you must specify validity years for 'root' and 'intermediary'"} + } + + if data.ValidityYears.Root < data.ValidityYears.Intermediary { + return SettingsError{"validity of root CA certificates must be equal or greater than those if intermediary CA certificates"} + } + + if data.URLPatterns.Ocsp == "" { + return SettingsError{"you must specify an 'ocsp' URL pattern"} + } + if strings.Count(data.URLPatterns.Ocsp, "%s") > 1 { + return SettingsError{"url-pattern 'ocsp' must contain zero or one '%s' placeholder"} + } + + if data.URLPatterns.CRL == "" { + return SettingsError{"you must specify an 'crl' URL pattern"} + } + if strings.Count(data.URLPatterns.CRL, "%s") != 1 { + return SettingsError{"url-pattern 'crl' must contain one '%s' placeholder"} + } + + if data.URLPatterns.Issuer == "" { + return SettingsError{"you must specify an 'issuer' URL pattern"} + } + if strings.Count(data.URLPatterns.Issuer, "%s") != 1 { + return SettingsError{"url-pattern 'issuer' must contain one '%s' placeholder"} + } + + s.Organization = &pkix.Name{} + s.Organization.Organization = data.Organization.Organization + s.Organization.Country = data.Organization.Country + s.Organization.Locality = data.Organization.Locality + s.Organization.StreetAddress = data.Organization.StreetAddress + s.Organization.PostalCode = data.Organization.PostalCode + + s.ValidityYears.Root = data.ValidityYears.Root + s.ValidityYears.Intermediary = data.ValidityYears.Intermediary + + s.URLPatterns.Ocsp = data.URLPatterns.Ocsp + s.URLPatterns.CRL = data.URLPatterns.CRL + s.URLPatterns.Issuer = data.URLPatterns.Issuer + + return nil } type KeyStorage struct { @@ -52,7 +126,7 @@ func (k *KeyStorage) UnmarshalYAML(n *yaml.Node) error { switch ks.TokenType { case "softhsm": - k.Module = softHsmModule + k.Module = SoftHsmModule case "p11module": if ks.Module == "" { return errors.New("specify a 'module' field when using the 'p11module' type") @@ -62,6 +136,9 @@ func (k *KeyStorage) UnmarshalYAML(n *yaml.Node) error { return fmt.Errorf("unsupported KeyStorage type '%s'", ks.TokenType) } + if ks.Label == "" { + return errors.New("element 'label' must be specified") + } k.Label = ks.Label return nil @@ -81,9 +158,9 @@ func (c *SignerConfig) GetCADefinition(label string) (*CaCertificateEntry, error return entry, nil } -func (c *SignerConfig) CalculateValidity(cert *CaCertificateEntry) (time.Time, time.Time) { +func (c *SignerConfig) CalculateValidity(cert *CaCertificateEntry, relativeTo time.Time) (time.Time, time.Time) { var notBefore, notAfter time.Time - notBefore = time.Now() + notBefore = relativeTo if cert.IsRoot() { notAfter = notBefore.AddDate(c.global.ValidityYears.Root, 0, 0) @@ -110,19 +187,19 @@ func (c *SignerConfig) CertificateFileName(label string) string { } func (c *SignerConfig) BuildIssuerURL(cert *CaCertificateEntry) string { - return fmt.Sprintf(c.global.URLPatterns.Issuer, cert.parent) + return fmt.Sprintf(c.global.URLPatterns.Issuer, cert.Parent) } func (c *SignerConfig) BuildOCSPURL(cert *CaCertificateEntry) string { // in case the configuration specifies a placeholder if strings.Count(c.global.URLPatterns.Ocsp, "%s") == 1 { - return fmt.Sprintf(c.global.URLPatterns.Ocsp, cert.parent) + return fmt.Sprintf(c.global.URLPatterns.Ocsp, cert.Parent) } return c.global.URLPatterns.Ocsp } func (c *SignerConfig) BuildCRLUrl(cert *CaCertificateEntry) string { - return fmt.Sprintf(c.global.URLPatterns.CRL, cert.parent) + return fmt.Sprintf(c.global.URLPatterns.CRL, cert.Parent) } func (c *SignerConfig) GetParentCA(label string) (*CaCertificateEntry, error) { @@ -135,7 +212,7 @@ func (c *SignerConfig) GetParentCA(label string) (*CaCertificateEntry, error) { return nil, fmt.Errorf("CA %s is a root CA and has no parent", label) } - return c.caMap[entry.parent], nil + return c.caMap[entry.Parent], nil } // RootCAs returns the labels of all configured root CAs @@ -302,7 +379,7 @@ type CaCertificateEntry struct { ExtKeyUsage []x509.ExtKeyUsage Certificate *x509.Certificate KeyPair crypto.Signer - parent string + Parent string Storage string } @@ -334,27 +411,30 @@ func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error { c.CommonName = m.CommonName c.MaxPathLen = m.MaxPathLen + if m.Parent == "" && m.ExtKeyUsage != nil { + return errors.New("a root CA must not specify 'ext-key-usages'") + } + + c.Parent = m.Parent + if m.ExtKeyUsage != nil { c.ExtKeyUsage, err = mapExtKeyUsageNames(m.ExtKeyUsage) + if err != nil { + return err + } } - c.parent = m.Parent - if m.Storage != "" { c.Storage = m.Storage } else { c.Storage = "default" } - if err != nil { - return err - } - return nil } func (c *CaCertificateEntry) IsRoot() bool { - return c.parent == "" + return c.Parent == "" } func mapExtKeyUsageNames(usages []string) ([]x509.ExtKeyUsage, error) { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 7e5b553..ec96083 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,47 +1,124 @@ -package config +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 *PrivateKeyInfo - expected string + name string + pkInfo *config.PrivateKeyInfo + expected string + expectErr bool }{ { - "RSA", - &PrivateKeyInfo{ + name: "RSA", + pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.RSA, RSABits: 3072, }, - `algorithm: RSA + expected: `algorithm: RSA rsa-bits: 3072 `, }, { - "ECDSA", - &PrivateKeyInfo{ + name: "ECDSA P-224", + pkInfo: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P224(), }, - `algorithm: EC + 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)) @@ -53,56 +130,99 @@ func TestPrivateKeyInfo_UnmarshalYAML(t *testing.T) { testData := []struct { name string yaml string - expected *PrivateKeyInfo + expected *config.PrivateKeyInfo expectErr bool }{ { - "RSA", - `label: "mykey" + name: "malformed", + yaml: `noyaml`, + expectErr: true, + }, + { + name: "RSA", + yaml: `label: "mykey" algorithm: "RSA" rsa-bits: 2048`, - &PrivateKeyInfo{ + expected: &config.PrivateKeyInfo{ Algorithm: x509.RSA, RSABits: 2048, }, - false, }, { - "ECDSA", - `label: "mykey" + 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"`, - &PrivateKeyInfo{ + expected: &config.PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P521(), }, - false, }, { - "no-algorithm", - `label: "mykey"`, - nil, - true, + name: "no-algorithm", + yaml: `label: "mykey"`, + expectErr: true, }, { - "RSA-no-rsa-bits", - `label: "mykey" + name: "RSA-no-rsa-bits", + yaml: `label: "mykey" algorithm: "RSA"`, - nil, - true, + expectErr: true, }, { - "ECDSA-no-curve", - `label: "mykey" + name: "ECDSA-no-curve", + yaml: `label: "mykey" algorithm: "EC"`, - nil, - true, + 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 := &PrivateKeyInfo{} + pkInfo := &config.PrivateKeyInfo{} err := yaml.Unmarshal([]byte(item.yaml), pkInfo) if err != nil && !item.expectErr { require.NoError(t, err) @@ -120,47 +240,172 @@ algorithm: "EC"`, } func TestCaCertificateEntry_UnmarshalYAML(t *testing.T) { - data := `{ - "key-info": { - "algorithm":"EC", - "ecc-curve":"P-521" - }, - "certificate-file":"test.crt", - "common-name":"My Little Test Root CA" -}` - - entry := CaCertificateEntry{} - - err := yaml.Unmarshal([]byte(data), &entry) - require.NoError(t, err) - - assert.Equal(t, CaCertificateEntry{ - KeyInfo: &PrivateKeyInfo{ - Algorithm: x509.ECDSA, - EccCurve: elliptic.P521(), + 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, }, - CommonName: "My Little Test Root CA", - Storage: "default", - }, entry) + } + + 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 := []struct { - name, yaml string - err bool + testData := map[string]struct { + yaml string + errMsg string }{ - { - name: "Good", + "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" @@ -180,22 +425,75 @@ CAs: ecc-curve: P-256 parent: root `, - err: false, }, - { - name: "Bad", - yaml: `noyamlforyou: ]`, - err: true, + "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 _, item := range testData { - t.Run(item.name, func(t *testing.T) { + for name, item := range testData { + t.Run(name, func(t *testing.T) { r := strings.NewReader(item.yaml) - sc, err := LoadConfiguration(r) + sc, err := config.LoadConfiguration(r) - if item.err { - assert.Error(t, err) + if item.errMsg != "" { + assert.ErrorContains(t, err, item.errMsg) assert.Nil(t, sc) } else { assert.NoError(t, err) @@ -205,17 +503,21 @@ CAs: } } -func TestSignerConfig_RootCAs(t *testing.T) { - yamlData := `--- +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" @@ -235,12 +537,399 @@ CAs: ecc-curve: P-256 parent: root ` - r := strings.NewReader(yamlData) - sc, err := LoadConfiguration(r) + +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) + }) + } +} diff --git a/pkg/hsm/hsm.go b/pkg/hsm/hsm.go index cc589d0..57fc7c8 100644 --- a/pkg/hsm/hsm.go +++ b/pkg/hsm/hsm.go @@ -15,6 +15,7 @@ import ( "math/big" "os" "syscall" + "time" "github.com/ThalesIgnite/crypto11" @@ -74,7 +75,7 @@ func GetRootCACertificate(ctx context.Context, label string) (*x509.Certificate, return nil, errCertificateGenerationRefused } - notBefore, notAfter := sc.CalculateValidity(caCert) + notBefore, notAfter := sc.CalculateValidity(caCert, time.Now()) subject := sc.CalculateSubject(caCert) certificate, err = generateRootCACertificate( @@ -153,7 +154,7 @@ func GetIntermediaryCACertificate(ctx context.Context, certLabel string) (*x509. return nil, errCertificateGenerationRefused } - notBefore, notAfter := sc.CalculateValidity(caCert) + notBefore, notAfter := sc.CalculateValidity(caCert, time.Now()) subject := sc.CalculateSubject(caCert) certificate, err = generateIntermediaryCACertificate(