package config import ( "crypto" "crypto/elliptic" "crypto/x509" "crypto/x509/pkix" "errors" "fmt" "io" "strings" "time" "gopkg.in/yaml.v3" ) type Settings struct { Organization *pkix.Name ValidityYears struct { Root, Intermediary int } URLPatterns struct { 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 { Label string Module string } func (k *KeyStorage) UnmarshalYAML(n *yaml.Node) error { ks := struct { TokenType string `yaml:"type"` Label string `yaml:"label"` Module string `yaml:"module"` }{} err := n.Decode(&ks) if err != nil { return fmt.Errorf("could not decode YAML: %w", err) } switch ks.TokenType { case "softhsm": k.Module = SoftHsmModule case "p11module": if ks.Module == "" { return errors.New("specify a 'module' field when using the 'p11module' type") } k.Module = ks.Module default: 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 } type SignerConfig struct { global *Settings `yaml:"Settings"` caMap map[string]*CaCertificateEntry `yaml:"CAs"` KeyStorage map[string]*KeyStorage `yaml:"KeyStorage"` } func (c *SignerConfig) GetCADefinition(label string) (*CaCertificateEntry, error) { entry, ok := c.caMap[label] if !ok { return nil, fmt.Errorf("no CA definition found for label %s", label) } return entry, nil } func (c *SignerConfig) CalculateValidity(cert *CaCertificateEntry, relativeTo time.Time) (time.Time, time.Time) { var notBefore, notAfter time.Time notBefore = relativeTo if cert.IsRoot() { notAfter = notBefore.AddDate(c.global.ValidityYears.Root, 0, 0) } else { notAfter = notBefore.AddDate(c.global.ValidityYears.Intermediary, 0, 0) } return notBefore, notAfter } func (c *SignerConfig) CalculateSubject(cert *CaCertificateEntry) pkix.Name { subject := pkix.Name{ Country: c.global.Organization.Country, Organization: c.global.Organization.Organization, Locality: c.global.Organization.Locality, StreetAddress: c.global.Organization.StreetAddress, PostalCode: c.global.Organization.PostalCode, } subject.CommonName = cert.CommonName return subject } func (c *SignerConfig) CertificateFileName(label string) string { return fmt.Sprintf("%s.crt", label) } func (c *SignerConfig) BuildIssuerURL(cert *CaCertificateEntry) string { 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 c.global.URLPatterns.Ocsp } func (c *SignerConfig) BuildCRLUrl(cert *CaCertificateEntry) string { return fmt.Sprintf(c.global.URLPatterns.CRL, cert.Parent) } func (c *SignerConfig) GetParentCA(label string) (*CaCertificateEntry, error) { entry, ok := c.caMap[label] if !ok { return nil, fmt.Errorf("no CA definition for %s", label) } if entry.IsRoot() { return nil, fmt.Errorf("CA %s is a root CA and has no parent", label) } return c.caMap[entry.Parent], nil } // RootCAs returns the labels of all configured root CAs func (c *SignerConfig) RootCAs() []string { roots := make([]string, 0) for label, entry := range c.caMap { if entry.IsRoot() { roots = append(roots, label) } } return roots } // IntermediaryCAs returns the labels of all configured intermediary CAs func (c *SignerConfig) IntermediaryCAs() []string { intermediaries := make([]string, 0) for label, entry := range c.caMap { if !entry.IsRoot() { intermediaries = append(intermediaries, label) } } return intermediaries } // LoadConfiguration reads YAML configuration from the given reader as a SignerConfig structure func LoadConfiguration(r io.Reader) (*SignerConfig, error) { config := struct { Global *Settings `yaml:"Settings"` CAs map[string]*CaCertificateEntry `yaml:"CAs"` KeyStorage map[string]*KeyStorage `yaml:"KeyStorage"` }{} decoder := yaml.NewDecoder(r) err := decoder.Decode(&config) if err != nil { return nil, fmt.Errorf("could not parse YAML configuration: %w", err) } if config.Global == nil { return nil, errors.New("configuration entry 'Settings' is missing or empty") } if config.CAs == nil { return nil, errors.New("configuration entry 'CAs' is missing or empty") } if config.KeyStorage == nil { return nil, errors.New("configuration entry 'KeyStorage' is missing or empty") } return &SignerConfig{ global: config.Global, caMap: config.CAs, KeyStorage: config.KeyStorage, }, nil } type PrivateKeyInfo struct { Algorithm x509.PublicKeyAlgorithm EccCurve elliptic.Curve RSABits int } func (p *PrivateKeyInfo) UnmarshalYAML(value *yaml.Node) error { internalStructure := struct { Algorithm string `yaml:"algorithm"` EccCurve string `yaml:"ecc-curve,omitempty"` RSABits *int `yaml:"rsa-bits,omitempty"` }{} err := value.Decode(&internalStructure) if err != nil { return fmt.Errorf("could not unmarshal private key info: %w", err) } switch internalStructure.Algorithm { case "RSA": p.Algorithm = x509.RSA if internalStructure.RSABits == nil { return errors.New("element 'rsa-bits' with RSA key length required for algorithm RSA") } p.RSABits = *internalStructure.RSABits case "EC": p.Algorithm = x509.ECDSA if internalStructure.EccCurve == "" { return errors.New("element 'ecc-curve' required for algorithm EC") } p.EccCurve, err = nameToCurve(internalStructure.EccCurve) if err != nil { return err } case "": return errors.New("element 'algorithm' must be specified as 'EC' or 'RSA'") default: return fmt.Errorf( "unsupported key algorithm %s, use either 'EC' or 'RSA'", internalStructure.Algorithm, ) } return nil } func (p *PrivateKeyInfo) MarshalYAML() (interface{}, error) { internalStructure := struct { Algorithm string `yaml:"algorithm"` EccCurve string `yaml:"ecc-curve,omitempty"` RSABits *int `yaml:"rsa-bits,omitempty"` }{} switch p.Algorithm { case x509.RSA: internalStructure.Algorithm = "RSA" internalStructure.RSABits = &p.RSABits case x509.ECDSA: internalStructure.Algorithm = "EC" curveName, err := curveToName(p.EccCurve) if err != nil { return nil, err } internalStructure.EccCurve = curveName } return internalStructure, nil } func curveToName(curve elliptic.Curve) (string, error) { switch curve { case elliptic.P224(): return "P-224", nil case elliptic.P256(): return "P-256", nil case elliptic.P384(): return "P-384", nil case elliptic.P521(): return "P-521", nil default: return "", fmt.Errorf("unsupported EC curve %s", curve) } } // nameToCurve maps a curve name to an elliptic curve func nameToCurve(name string) (elliptic.Curve, error) { switch name { case "P-224", "secp224r1": return elliptic.P224(), nil case "P-256", "secp256r1": return elliptic.P256(), nil case "P-384", "secp384r1": return elliptic.P384(), nil case "P-521", "secp521r1": return elliptic.P521(), nil default: return nil, fmt.Errorf("unsupported EC curve %s", name) } } type CaCertificateEntry struct { KeyInfo *PrivateKeyInfo CommonName string MaxPathLen int // maximum path length should be 0 for CAs that issue end entity certificates ExtKeyUsage []x509.ExtKeyUsage Certificate *x509.Certificate KeyPair crypto.Signer Parent string Storage string } func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error { var m struct { KeyInfo *PrivateKeyInfo `yaml:"key-info"` CommonName string `yaml:"common-name"` MaxPathLen int `yaml:"max-path-len,omitempty"` // maximum path length should be 0 for CAs that issue end entity certificates ExtKeyUsage []string `yaml:"ext-key-usages,omitempty"` Parent string `yaml:"parent"` Storage string `yaml:"storage"` } err := value.Decode(&m) if err != nil { return err } if m.KeyInfo == nil { return errors.New("element 'key-info' must be set") } c.KeyInfo = m.KeyInfo if m.CommonName == "" { return errors.New("element 'common-name' must be set") } 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 } } if m.Storage != "" { c.Storage = m.Storage } else { c.Storage = "default" } return nil } func (c *CaCertificateEntry) IsRoot() bool { return c.Parent == "" } func mapExtKeyUsageNames(usages []string) ([]x509.ExtKeyUsage, error) { extKeyUsages := make([]x509.ExtKeyUsage, len(usages)) for idx, usage := range usages { switch usage { case "client": extKeyUsages[idx] = x509.ExtKeyUsageClientAuth case "code": extKeyUsages[idx] = x509.ExtKeyUsageCodeSigning case "email": extKeyUsages[idx] = x509.ExtKeyUsageEmailProtection case "server": extKeyUsages[idx] = x509.ExtKeyUsageServerAuth case "ocsp": extKeyUsages[idx] = x509.ExtKeyUsageOCSPSigning default: return nil, fmt.Errorf("unsupported extended key usage %s", usage) } } return extKeyUsages, nil }