package config import ( "crypto" "crypto/elliptic" "crypto/x509" "crypto/x509/pkix" "errors" "fmt" "io" "strings" "time" "gopkg.in/yaml.v3" ) 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"` 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"` } 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) } 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) (time.Time, time.Time) { var notBefore, notAfter time.Time notBefore = time.Now() 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.ExtKeyUsage != nil { c.ExtKeyUsage, err = mapExtKeyUsageNames(m.ExtKeyUsage) } 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 == "" } 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 }