/* 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 import ( "crypto" "crypto/elliptic" "crypto/x509" "crypto/x509/pkix" "errors" "fmt" "io" "path" "strings" "time" "gopkg.in/yaml.v3" "git.cacert.org/cacert-gosigner/internal/x509/openssl" "git.cacert.org/cacert-gosigner/internal/x509/revoking" "git.cacert.org/cacert-gosigner/internal/x509/signing" ) const minRSABits = 2048 type Serial struct { Device string Baud int Timeout time.Duration } type Settings struct { Organization *pkix.Name ValidityYears struct { Root, Subordinate int } URLPatterns struct { Ocsp, CRL, Issuer string } Serial *Serial } 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"` Subordinate int `yaml:"subordinate"` } `yaml:"validity-years"` URLPatterns struct { Ocsp string `yaml:"ocsp"` CRL string `yaml:"crl"` Issuer string `yaml:"issuer"` } `yaml:"url-patterns"` Serial struct { Device string `yaml:"device"` Baud int `yaml:"baud"` TimeoutMillis int `yaml:"timeout-millis"` } `yaml:"serial"` }{} 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.Subordinate == 0 { return SettingsError{"you must specify validity years for 'root' and 'subordinate'"} } if data.ValidityYears.Root < data.ValidityYears.Subordinate { return SettingsError{"validity of root CA certificates must be equal or greater than those of" + " subordinate 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"} } if data.Serial.Device == "" { return SettingsError{"you must specify a serial 'device'"} } if data.Serial.Baud == 0 { data.Serial.Baud = 115200 } if data.Serial.TimeoutMillis == 0 { data.Serial.TimeoutMillis = 5000 } 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.Subordinate = data.ValidityYears.Subordinate s.URLPatterns.Ocsp = data.URLPatterns.Ocsp s.URLPatterns.CRL = data.URLPatterns.CRL s.URLPatterns.Issuer = data.URLPatterns.Issuer s.Serial = &Serial{ Device: data.Serial.Device, Baud: data.Serial.Baud, Timeout: time.Duration(data.Serial.TimeoutMillis) * time.Millisecond, } 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 CARepository interface { revoking.Repository signing.Repository } type SignerConfig struct { global *Settings `yaml:"Settings"` caMap map[string]*CaCertificateEntry `yaml:"CAs"` keyStorage map[string]*KeyStorage `yaml:"KeyStorage"` repositories map[string]CARepository } 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.Subordinate, 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(label string) string { return fmt.Sprintf(c.global.URLPatterns.CRL, label) } 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) } if entry.Parent == "" { return nil, fmt.Errorf("parent for %s is empty", label) } parent, ok := c.caMap[entry.Parent] if !ok { return nil, fmt.Errorf("parent %s for %s not found in signer config", entry.Parent, label) } return 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 } // SubordinateCAs returns the labels of all configured subordinate CAs func (c *SignerConfig) SubordinateCAs() []string { subordinates := make([]string, 0) for label, entry := range c.caMap { if !entry.IsRoot() { subordinates = append(subordinates, label) } } return subordinates } func (c *SignerConfig) GetKeyStorage(label string) (*KeyStorage, error) { keyStorage, ok := c.keyStorage[label] if !ok { return nil, fmt.Errorf("could not find storage definition with label %s", label) } return keyStorage, nil } func (c *SignerConfig) GetSerial() *Serial { return c.global.Serial } func (c *SignerConfig) Repository(name string) (CARepository, error) { var ( repo CARepository ok bool err error ) repo, ok = c.repositories[name] if !ok { repo, err = openssl.NewFileRepository(path.Join("repos", name)) if err != nil { return nil, fmt.Errorf("could not create repository for %s: %w", name, err) } c.repositories[name] = repo } return repo, nil } // 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, repositories: make(map[string]CARepository), }, nil } type PrivateKeyInfo struct { Algorithm x509.PublicKeyAlgorithm EccCurve elliptic.Curve RSABits int CRLSignatureAlgorithm x509.SignatureAlgorithm } 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"` CRLSignatureAlgorithm string `yaml:"crl-signature-algorithm,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 if p.RSABits < minRSABits { return fmt.Errorf("RSA keys must have a length of at least %d bits", minRSABits) } p.CRLSignatureAlgorithm = determineRSASignatureAlgorithm(internalStructure.CRLSignatureAlgorithm) 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 } p.CRLSignatureAlgorithm = determineECDSASignatureAlgorithm(internalStructure.CRLSignatureAlgorithm) 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 determineRSASignatureAlgorithm(algorithm string) x509.SignatureAlgorithm { switch strings.ToLower(algorithm) { case "sha1withrsa", "sha1": return x509.SHA1WithRSA case "sha384withrsa", "sha384": return x509.SHA384WithRSA case "sha512withrsa", "sha512": return x509.SHA512WithRSA default: return x509.SHA256WithRSA } } func determineECDSASignatureAlgorithm(algorithm string) x509.SignatureAlgorithm { switch strings.ToLower(algorithm) { case "sha1withecdsa", "sha1": return x509.ECDSAWithSHA1 case "sha384withecdsa", "sha384": return x509.ECDSAWithSHA384 case "sha512withecdsa", "sha512": return x509.ECDSAWithSHA512 default: return x509.ECDSAWithSHA256 } } 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) } } var validProfileUsages = map[string]signing.ProfileUsage{ "ocsp": signing.UsageOCSP, "client": signing.UsageClient, "code": signing.UsageCode, "person": signing.UsagePerson, "server": signing.UsageServer, "server_client": signing.UsageServerClient, "org_client": signing.UsageOrgClient, "org_code": signing.UsageOrgCode, "org_email": signing.UsageOrgEmail, "org_person": signing.UsageOrgPerson, "org_server": signing.UsageOrgServer, "org_server_client": signing.UsageOrgServerClient, } func ParseUsage(u string) (signing.ProfileUsage, error) { usage, ok := validProfileUsages[u] if !ok { return signing.UsageInvalid, fmt.Errorf("unsupported profile usage: %s", u) } return usage, nil } type Profile struct { Name string UseFor signing.ProfileUsage Years int Months int Days int } 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 Profiles []Profile } func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) error { var m struct { KeyInfo *PrivateKeyInfo `yaml:"key-info"` CommonName string `yaml:"common-name"` // maximum path length should be 0 for CAs that issue end entity certificates MaxPathLen int `yaml:"max-path-len,omitempty"` ExtKeyUsage []string `yaml:"ext-key-usages,omitempty"` Parent string `yaml:"parent"` Storage string `yaml:"storage"` Profiles []struct { Name string `yaml:"name"` UseFor string `yaml:"use-for"` Validity struct { Years int `yaml:"years"` Months int `yaml:"months"` Days int `yaml:"days"` } `yaml:"validity"` } `yaml:"profiles"` } err := value.Decode(&m) if err != nil { return fmt.Errorf("could not unmarshal CA certificate entry: %w", 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" } if m.Profiles != nil { c.Profiles = make([]Profile, len(m.Profiles)) for i, prof := range m.Profiles { usage, err := ParseUsage(prof.UseFor) if err != nil { return fmt.Errorf("config error: %w", err) } if prof.Validity.Years <= 0 && prof.Validity.Months <= 0 && prof.Validity.Days <= 0 { prof.Validity.Years, prof.Validity.Months, prof.Validity.Days = defaultValidity(usage) } c.Profiles[i] = Profile{ Name: prof.Name, UseFor: usage, Years: prof.Validity.Years, Months: prof.Validity.Months, Days: prof.Validity.Days, } } } return nil } func defaultValidity(useFor signing.ProfileUsage) (years int, months int, days int) { switch useFor { case signing.UsagePerson, signing.UsageOrgPerson, signing.UsageOrgEmail: return 2, 0, 0 default: return 1, 0, 0 } } 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 }