Jan Dittberner
de997913cf
This commit implements a mechanism to load CA configuration dynamically from JSON files. Missing keys and certificates can be generated in a PKCS#11 HSM or Smartcard. Certificates are stored as PEM encoded .crt files in the filesystem. The default PKCS#11 module (softhsm2) is now loaded from a platform specific path using go:build comments.
240 lines
6 KiB
Go
240 lines
6 KiB
Go
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
|
|
}
|