package config import ( "crypto" "crypto/elliptic" "crypto/x509" "crypto/x509/pkix" "encoding/json" "errors" "fmt" "io" "time" ) type Settings struct { Organization pkix.Name `json:"organization"` RootYears int `json:"root-years"` IntermediaryYears int `json:"intermediary-years"` } func (s *Settings) CalculateValidity(cert *CaCertificateEntry) (time.Time, time.Time) { var notBefore, notAfter time.Time notBefore = time.Now() if cert.Parent == nil { notAfter = notBefore.AddDate(s.RootYears, 0, 0) } else { notAfter = notBefore.AddDate(s.IntermediaryYears, 0, 0) } return notBefore, notAfter } func (s *Settings) CalculateSubject(cert *CaCertificateEntry) pkix.Name { subject := s.Organization subject.CommonName = cert.CommonName return subject } func (s *Settings) BuildIssuerURL(parentCA *CaCertificateEntry) string { return fmt.Sprintf("http://www.example.org/%s.crt", parentCA.Label) } func (s *Settings) BuildOCSPURL(_ *CaCertificateEntry) string { return "http://ocsp.example.org/" } func (s *Settings) BuildCRLUrl(parentCA *CaCertificateEntry) string { return fmt.Sprintf("http://crl.cacert.org/%s.crl", parentCA.Label) } type SignerConfig struct { Global *Settings `json:"Settings"` CAs []*CaCertificateEntry `json:"CAs"` } // LoadConfiguration reads JSON configuration from the given reader as a SignerConfig structure func LoadConfiguration(r io.Reader) (*SignerConfig, error) { data, err := io.ReadAll(r) if err != nil { return nil, fmt.Errorf("could not load configuration: %w", err) } config := &SignerConfig{} err = json.Unmarshal(data, config) if err != nil { return nil, fmt.Errorf("could not parse JSON configuration: %w", err) } return config, nil } type PrivateKeyInfo struct { Algorithm x509.PublicKeyAlgorithm EccCurve elliptic.Curve RSABits int } func (p *PrivateKeyInfo) UnmarshalJSON(data []byte) error { internalStructure := struct { Label string `json:"label"` Algorithm string `json:"algorithm"` EccCurve string `json:"ecc-curve,omitempty"` RSABits *int `json:"rsa-bits,omitempty"` }{} err := json.Unmarshal(data, &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("RSA key length not specified") } p.RSABits = *internalStructure.RSABits case "EC": p.Algorithm = x509.ECDSA p.EccCurve, err = nameToCurve(internalStructure.EccCurve) if err != nil { return err } default: return fmt.Errorf("unsupported key algorithm %s", internalStructure.Algorithm) } return nil } func (p *PrivateKeyInfo) MarshalJSON() ([]byte, error) { internalStructure := struct { Algorithm string `json:"algorithm"` EccCurve string `json:"ecc-curve,omitempty"` RSABits *int `json:"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 } data, err := json.Marshal(internalStructure) if err != nil { return nil, fmt.Errorf("could not marshal private key info: %w", err) } return data, 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 { Label string KeyInfo *PrivateKeyInfo CommonName string SubCAs []*CaCertificateEntry MaxPathLen int `json:"max-path-len"` // maximum path length should be 0 for CAs that issue end entity certificates ExtKeyUsage []x509.ExtKeyUsage `json:"ext-key-usage"` Certificate *x509.Certificate KeyPair crypto.Signer Parent *CaCertificateEntry } func (c *CaCertificateEntry) CertificateFileName() string { return c.Label + ".crt" } func (c *CaCertificateEntry) UnmarshalJSON(data []byte) error { var m struct { Label string KeyInfo *PrivateKeyInfo `json:"key-info"` CommonName string `json:"common-name"` SubCAs []*CaCertificateEntry `json:"sub-cas,omitempty"` MaxPathLen int `json:"max-path-len,omitempty"` // maximum path length should be 0 for CAs that issue end entity certificates ExtKeyUsage []string `json:"ext-key-usage,omitempty"` } err := json.Unmarshal(data, &m) if err != nil { return err } c.Label = m.Label c.KeyInfo = m.KeyInfo c.CommonName = m.CommonName c.SubCAs = m.SubCAs c.MaxPathLen = m.MaxPathLen if m.ExtKeyUsage != nil { c.ExtKeyUsage, err = mapExtKeyUsageNames(m.ExtKeyUsage) } if err != nil { return err } return nil } 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 }