From de997913cf1b0369b0a709589f3643cfab71c0ff Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sat, 16 Apr 2022 22:24:32 +0200 Subject: [PATCH] Implement configuration and CA hierarchy setup 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. --- .gitignore | 2 + cmd/signer/amd64.go | 5 + cmd/signer/arm64.go | 5 + cmd/signer/armhf.go | 5 + cmd/signer/main.go | 95 ++++++++++ cmd/signer/main_test.go | 96 ---------- go.mod | 2 + go.sum | 4 + pkg/config/config.go | 240 +++++++++++++++++++++++++ pkg/config/config_test.go | 136 ++++++++++++++ pkg/hsm/hsm.go | 367 ++++++++++++++++++++++++++++++++++++++ pkg/hsm/setup.go | 62 +++++++ 12 files changed, 923 insertions(+), 96 deletions(-) create mode 100644 cmd/signer/amd64.go create mode 100644 cmd/signer/arm64.go create mode 100644 cmd/signer/armhf.go delete mode 100644 cmd/signer/main_test.go create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/hsm/hsm.go create mode 100644 pkg/hsm/setup.go diff --git a/.gitignore b/.gitignore index c4fadf7..056169c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +*.crt *.pem *.pub .idea/ +ca-hierarchy.json dist/ \ No newline at end of file diff --git a/cmd/signer/amd64.go b/cmd/signer/amd64.go new file mode 100644 index 0000000..30dff02 --- /dev/null +++ b/cmd/signer/amd64.go @@ -0,0 +1,5 @@ +//go:build linux && amd64 + +package main + +const defaultPkcs11Module = "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so" diff --git a/cmd/signer/arm64.go b/cmd/signer/arm64.go new file mode 100644 index 0000000..bb2fc1a --- /dev/null +++ b/cmd/signer/arm64.go @@ -0,0 +1,5 @@ +//go:build linux && arm64 + +package main + +const defaultPkcs11Module = "/usr/lib/aarch64-linux-gnu/softhsm/libsofthsm2.so" diff --git a/cmd/signer/armhf.go b/cmd/signer/armhf.go new file mode 100644 index 0000000..b999ba0 --- /dev/null +++ b/cmd/signer/armhf.go @@ -0,0 +1,5 @@ +//go:build linux && arm + +package main + +const defaultPkcs11Module = "/usr/lib/arm-linux-gnueabihf/softhsm/libsofthsm2.so" diff --git a/cmd/signer/main.go b/cmd/signer/main.go index da29a2c..ece7c47 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -1,4 +1,99 @@ package main +import ( + "flag" + "fmt" + "log" + "os" + "strings" + "syscall" + + "git.cacert.org/cacert-gosigner/pkg/config" + "github.com/ThalesIgnite/crypto11" + "golang.org/x/term" + + "git.cacert.org/cacert-gosigner/pkg/hsm" +) + +var ( + commit string + date string + version string +) + +const ( + defaultTokenLabel = "localhsm" + defaultSignerConfigFile = "ca-hierarchy.json" +) + func main() { + p11Config := &crypto11.Config{} + var ( + showVersion bool + signerConfigFile string + ) + + log.Printf("cacert-gosigner %s (%s) - built %s\n", version, commit, date) + + flag.StringVar(&p11Config.Path, "module", defaultPkcs11Module, "PKCS#11 module") + flag.StringVar(&p11Config.TokenLabel, "token", defaultTokenLabel, "PKCS#11 token label") + flag.StringVar(&signerConfigFile, "caconfig", defaultSignerConfigFile, "signer configuration file") + flag.BoolVar(&showVersion, "version", false, "show version") + + flag.Parse() + + if showVersion { + return + } + + log.Printf("using PKCS#11 module %s", p11Config.Path) + log.Printf("looking for token with label %s", p11Config.TokenLabel) + + configFile, err := os.Open(signerConfigFile) + if err != nil { + log.Fatalf("could not open singer configuration file %s: %v", signerConfigFile, err) + } + + caConfig, err := config.LoadConfiguration(configFile) + if err != nil { + log.Fatalf("could not load CA hierarchy: %v", err) + } + + getPin(p11Config) + + p11Context, err := crypto11.Configure(p11Config) + if err != nil { + log.Fatalf("could not configure PKCS#11 library: %v", err) + } + + defer func(p11Context *crypto11.Context) { + err := p11Context.Close() + if err != nil { + log.Printf("could not close PKCS#11 library context: %v", err) + } + }(p11Context) + + err = hsm.EnsureCAKeysAndCertificates(p11Context, caConfig) + if err != nil { + log.Fatalf("could not ensure CA keys and certificates exist: %v", err) + } +} + +func getPin(p11Config *crypto11.Config) { + pin, found := os.LookupEnv("TOKEN_PIN") + if !found { + log.Printf("environment variable TOKEN_PIN has not been set") + if !term.IsTerminal(syscall.Stdin) { + log.Fatal("stdin is not a terminal") + } + fmt.Print("Enter PIN: ") + bytePin, err := term.ReadPassword(syscall.Stdin) + if err != nil { + log.Fatalf("could not read PIN") + } + fmt.Println() + + pin = string(bytePin) + } + p11Config.Pin = strings.TrimSpace(pin) } diff --git a/cmd/signer/main_test.go b/cmd/signer/main_test.go deleted file mode 100644 index fe0b62e..0000000 --- a/cmd/signer/main_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "math/big" - "os" - "testing" - "time" - - "github.com/ThalesIgnite/crypto11" -) - -const defaultPkcs11Module = "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so" - -func TestStart(t *testing.T) { - pkcs11Module, found := os.LookupEnv("PKCS11_LIB") - if !found { - pkcs11Module = defaultPkcs11Module - } - p11Context, err := crypto11.Configure(&crypto11.Config{ - Path: pkcs11Module, - TokenLabel: "localhsm", - Pin: "123456", - }) - if err != nil { - t.Fatalf("could not configure PKCS#11 library: %v", err) - } - - defer func(p11Context *crypto11.Context) { - err := p11Context.Close() - if err != nil { - t.Errorf("could not close PKCS#11 library context: %v", err) - } - }(p11Context) - - pair, err := p11Context.FindKeyPair(nil, []byte("rootkey2022")) - if err != nil { - t.Fatalf("could not find requested key pair: %v", err) - } - - serial, err := randomSerialNumber() - if err != nil { - t.Fatal(err) - } - - notBefore := time.Now() - notAfter := notBefore.AddDate(20, 0, 0) - - certTemplate := &x509.Certificate{ - SerialNumber: serial, - Subject: pkix.Name{ - Country: []string{"CH"}, - Organization: []string{"CAcert Inc."}, - Locality: []string{"Genève"}, - StreetAddress: []string{"Clos Belmont 2"}, - PostalCode: []string{"1208"}, - CommonName: "CAcert ECC Root 2022", - }, - NotBefore: notBefore, - NotAfter: notAfter, - MaxPathLen: 0, - MaxPathLenZero: true, - BasicConstraintsValid: true, - KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign, - IsCA: true, - SignatureAlgorithm: x509.ECDSAWithSHA256, - } - - certificate, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, pair.Public(), pair) - if err != nil { - t.Fatalf("could not create root certificate: %v", err) - } - - certBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: certificate, - } - - err = os.WriteFile("/tmp/test.pem", pem.EncodeToMemory(certBlock), 0o600) - if err != nil { - t.Errorf("could not write certificate: %v", err) - } -} - -func randomSerialNumber() (*big.Int, error) { - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return nil, fmt.Errorf("could not generate serial number: %w", err) - } - return serialNumber, nil -} diff --git a/go.mod b/go.mod index 7744625..5cf72ca 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/ThalesIgnite/crypto11 v1.2.5 github.com/stretchr/testify v1.7.1 + golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 ) require ( @@ -13,5 +14,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/thales-e-security/pool v0.0.2 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 5010593..20201a2 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,10 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..1f536aa --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,240 @@ +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 +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..f53c39f --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,136 @@ +package config + +import ( + "crypto/elliptic" + "crypto/x509" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPrivateKeyInfo_MarshalJSON(t *testing.T) { + testData := []struct { + name string + pkInfo *PrivateKeyInfo + expected string + }{ + { + "RSA", + &PrivateKeyInfo{ + Algorithm: x509.RSA, + RSABits: 3072, + }, + `{"algorithm":"RSA","rsa-bits":3072}`, + }, + { + "ECDSA", + &PrivateKeyInfo{ + Algorithm: x509.ECDSA, + EccCurve: elliptic.P224(), + }, + `{"algorithm":"EC","ecc-curve":"P-224"}`, + }, + } + + for _, item := range testData { + t.Run(item.name, func(t *testing.T) { + data, err := json.Marshal(item.pkInfo) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, item.expected, string(data)) + }) + } +} + +func TestPrivateKeyInfo_UnmarshalJSON(t *testing.T) { + testData := []struct { + name string + json string + expected *PrivateKeyInfo + expectErr bool + }{ + { + "RSA", + `{"label":"mykey","algorithm":"RSA","rsa-bits":2048}`, + &PrivateKeyInfo{ + Algorithm: x509.RSA, + RSABits: 2048, + }, + false, + }, + { + "ECDSA", + `{"label":"mykey","algorithm":"EC","ecc-curve":"P-521"}`, + &PrivateKeyInfo{ + Algorithm: x509.ECDSA, + EccCurve: elliptic.P521(), + }, + false, + }, + { + "no-algorithm", + `{"label":"mykey"}`, + nil, + true, + }, + { + "RSA-no-rsa-bits", + `{"label":"mykey","algorithm":"RSA"}`, + nil, + true, + }, + { + "ECDSA-no-curve", + `{"label":"mykey","algorithm":"EC"}`, + nil, + true, + }, + } + + for _, item := range testData { + t.Run(item.name, func(t *testing.T) { + pkInfo := &PrivateKeyInfo{} + err := json.Unmarshal([]byte(item.json), pkInfo) + if err != nil { + if !item.expectErr { + t.Fatal(err) + } + } + + if !item.expectErr { + assert.Equal(t, item.expected, pkInfo) + } + }) + } +} + +func TestCaCertificateEntry_UnmarshalJSON(t *testing.T) { + data := `{ + "label":"root", + "key-info": { + "algorithm":"EC", + "ecc-curve":"P-521" + }, + "certificate-file":"test.crt", + "common-name":"My Little Test Root CA" +}` + + entry := CaCertificateEntry{} + + err := json.Unmarshal([]byte(data), &entry) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, CaCertificateEntry{ + Label: "root", + KeyInfo: &PrivateKeyInfo{ + Algorithm: x509.ECDSA, + EccCurve: elliptic.P521(), + }, + CommonName: "My Little Test Root CA", + }, entry) +} diff --git a/pkg/hsm/hsm.go b/pkg/hsm/hsm.go new file mode 100644 index 0000000..7ec99ba --- /dev/null +++ b/pkg/hsm/hsm.go @@ -0,0 +1,367 @@ +package hsm + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "log" + "math/big" + "os" + "syscall" + "time" + + "git.cacert.org/cacert-gosigner/pkg/config" + "github.com/ThalesIgnite/crypto11" +) + +func GetRootCACertificate(p11Context *crypto11.Context, settings *config.Settings, caCert *config.CaCertificateEntry) (*x509.Certificate, crypto.Signer, error) { + keyPair, err := getKeyPair(p11Context, caCert.Label, caCert.KeyInfo) + if err != nil { + return nil, nil, err + } + + certFile := caCert.CertificateFileName() + + certificate, err := loadCertificate(certFile) + if err != nil { + return nil, nil, err + } + + if certificate != nil && certificateMatches(certificate, keyPair) { + return certificate, keyPair, nil + } + + notBefore := time.Now() + notAfter := notBefore.AddDate(settings.RootYears, 0, 0) + + subject := settings.Organization + subject.CommonName = caCert.CommonName + + certificate, err = generateRootCACertificate( + certFile, + keyPair, + &x509.Certificate{ + Subject: subject, + NotBefore: notBefore, + NotAfter: notAfter, + MaxPathLen: 0, + MaxPathLenZero: false, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign, + IsCA: true, + }, + ) + + if err != nil { + return nil, nil, err + } + + err = addCertificate(p11Context, caCert.Label, certificate) + if err != nil { + return nil, nil, err + } + + return certificate, keyPair, nil +} + +func GetIntermediaryCACertificate( + p11Context *crypto11.Context, + settings *config.Settings, + caCert *config.CaCertificateEntry, +) (*x509.Certificate, crypto.Signer, error) { + keyPair, err := getKeyPair(p11Context, caCert.Label, caCert.KeyInfo) + if err != nil { + return nil, nil, err + } + + certFile := caCert.CertificateFileName() + + certificate, err := loadCertificate(certFile) + if err != nil { + return nil, nil, err + } + + if certificate != nil && certificateMatches(certificate, keyPair) { + return certificate, keyPair, nil + } + + notBefore, notAfter := settings.CalculateValidity(caCert) + subject := settings.CalculateSubject(caCert) + + certificate, err = generateIntermediaryCACertificate( + caCert, + keyPair.Public(), + &x509.Certificate{ + Subject: subject, + NotBefore: notBefore, + NotAfter: notAfter, + MaxPathLen: caCert.MaxPathLen, + MaxPathLenZero: true, + BasicConstraintsValid: true, + IsCA: true, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: caCert.ExtKeyUsage, + IssuingCertificateURL: []string{settings.BuildIssuerURL(caCert.Parent)}, + OCSPServer: []string{settings.BuildOCSPURL(caCert.Parent)}, + CRLDistributionPoints: []string{settings.BuildCRLUrl(caCert.Parent)}, + PolicyIdentifiers: []asn1.ObjectIdentifier{ + // use policy identifiers from http://wiki.cacert.org/OidAllocation + {1, 3, 6, 1, 4, 1, 18506, 2, 3, 1}, // 1.3.6.1.4.1.18506.2.3.1 Class3 Policy Version 1 + }, + }, + ) + + if err != nil { + return nil, nil, err + } + + err = addCertificate(p11Context, caCert.Label, certificate) + if err != nil { + return nil, nil, err + } + + return certificate, keyPair, nil +} + +func generateIntermediaryCACertificate(caCert *config.CaCertificateEntry, publicKey crypto.PublicKey, template *x509.Certificate) (*x509.Certificate, error) { + serial, err := randomSerialNumber() + if err != nil { + return nil, err + } + + template.SerialNumber = serial + template.SignatureAlgorithm, err = determineSignatureAlgorithm(caCert.Parent.KeyPair) + + if err != nil { + return nil, err + } + + certBytes, err := x509.CreateCertificate( + rand.Reader, + template, + caCert.Parent.Certificate, + publicKey, + caCert.Parent.KeyPair, + ) + if err != nil { + return nil, fmt.Errorf("could not create intermediary CA certificate: %w", err) + } + + certBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + } + + certFile := caCert.CertificateFileName() + + err = os.WriteFile(certFile, pem.EncodeToMemory(certBlock), 0o600) + if err != nil { + return nil, fmt.Errorf("could not write certificate to %s: %w", certFile, err) + } + + certificate, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, fmt.Errorf("could not parse generated certificate: %w", err) + } + + return certificate, nil +} + +func addCertificate(p11Context *crypto11.Context, label string, certificate *x509.Certificate) error { + objectId, err := randomObjectId() + if err != nil { + return err + } + + err = p11Context.ImportCertificateWithLabel(objectId, []byte(label), certificate) + if err != nil { + return fmt.Errorf("could not import certificate into token: %w", err) + } + + return nil +} + +func getKeyPair(p11Context *crypto11.Context, label string, keyInfo *config.PrivateKeyInfo) (crypto.Signer, error) { + keyPair, err := p11Context.FindKeyPair(nil, []byte(label)) + if err != nil { + return nil, fmt.Errorf("could not find requested key pair: %w", err) + } + + if keyPair != nil { + return keyPair, nil + } + + switch keyInfo.Algorithm { + case x509.RSA: + keyPair, err = generateRSAKeyPair(p11Context, label, keyInfo) + if err != nil { + return nil, fmt.Errorf("could not generate RSA key pair: %w", err) + } + case x509.ECDSA: + keyPair, err = generateECDSAKeyPair(p11Context, label, keyInfo) + if err != nil { + return nil, fmt.Errorf("could not generate ECDSA key pair: %w", err) + } + default: + return nil, fmt.Errorf("could not generate private key with label %s with unsupported key algorithm %s", label, keyInfo.Algorithm) + } + + return keyPair, nil +} + +func generateECDSAKeyPair(p11Context *crypto11.Context, label string, keyInfo *config.PrivateKeyInfo) (crypto11.Signer, error) { + newObjectId, err := randomObjectId() + if err != nil { + return nil, err + } + + return p11Context.GenerateECDSAKeyPairWithLabel(newObjectId, []byte(label), keyInfo.EccCurve) +} + +func generateRSAKeyPair(p11Context *crypto11.Context, label string, keyInfo *config.PrivateKeyInfo) (crypto11.Signer, error) { + newObjectId, err := randomObjectId() + if err != nil { + return nil, err + } + + return p11Context.GenerateRSAKeyPairWithLabel(newObjectId, []byte(label), keyInfo.RSABits) +} + +func randomObjectId() ([]byte, error) { + result := make([]byte, 20) + _, err := rand.Read(result) + if err != nil { + return nil, fmt.Errorf("could not create new random object id: %w", err) + } + return result, nil +} + +func generateRootCACertificate(certFile string, keyPair crypto.Signer, template *x509.Certificate) (*x509.Certificate, error) { + serial, err := randomSerialNumber() + if err != nil { + return nil, err + } + + template.SerialNumber = serial + template.SignatureAlgorithm, err = determineSignatureAlgorithm(keyPair) + + if err != nil { + return nil, err + } + + certBytes, err := x509.CreateCertificate( + rand.Reader, + template, + template, + keyPair.Public(), + keyPair, + ) + if err != nil { + return nil, fmt.Errorf("could not create root certificate: %w", err) + } + + certBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + } + + err = os.WriteFile(certFile, pem.EncodeToMemory(certBlock), 0o600) + if err != nil { + return nil, fmt.Errorf("could not write certificate to %s: %w", certFile, err) + } + + certificate, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, fmt.Errorf("could not parse generated certificate: %w", err) + } + + return certificate, nil +} + +func determineSignatureAlgorithm(keyPair crypto.Signer) (x509.SignatureAlgorithm, error) { + switch keyPair.Public().(type) { + case *ecdsa.PublicKey: + return x509.ECDSAWithSHA256, nil + case *rsa.PublicKey: + return x509.SHA256WithRSA, nil + default: + return x509.UnknownSignatureAlgorithm, + fmt.Errorf("could not determine signature algorithm for key of type %T", keyPair) + } +} + +func certificateMatches(certificate *x509.Certificate, key crypto.Signer) bool { + switch v := certificate.PublicKey.(type) { + case *ecdsa.PublicKey: + if pub, ok := key.Public().(*ecdsa.PublicKey); ok { + if v.Equal(pub) { + return true + } + } + case *rsa.PublicKey: + if pub, ok := key.Public().(*rsa.PublicKey); ok { + if v.Equal(pub) { + return true + } + } + default: + log.Printf("unsupported public key %v", v) + } + + log.Printf( + "public key from certificate does not match private key: %s != %s", + certificate.PublicKey, + key.Public(), + ) + return false +} + +func loadCertificate(certFile string) (*x509.Certificate, error) { + certFileInfo, err := os.Stat(certFile) + if err != nil { + if errors.Is(err, syscall.ENOENT) { + return nil, nil + } + return nil, fmt.Errorf("could not get info for %s: %w", certFile, err) + } + + if !certFileInfo.Mode().IsRegular() { + return nil, fmt.Errorf("certificate file %s is not a regular file", certFile) + } + + certData, err := os.ReadFile(certFile) + if err != nil { + return nil, fmt.Errorf("could not read %s: %w", certFile, err) + } + + pemData, _ := pem.Decode(certData) + if pemData == nil { + return nil, fmt.Errorf("no PEM data in %s", certFile) + } + + if pemData.Type != "CERTIFICATE" { + return nil, fmt.Errorf("no certificate found in %s", certFile) + } + + certificate, err := x509.ParseCertificate(pemData.Bytes) + if err != nil { + return nil, fmt.Errorf("could not parse certificate from %s: %w", certFile, err) + } + + return certificate, nil +} + +func randomSerialNumber() (*big.Int, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("could not generate serial number: %w", err) + } + return serialNumber, nil +} diff --git a/pkg/hsm/setup.go b/pkg/hsm/setup.go new file mode 100644 index 0000000..3d8eca7 --- /dev/null +++ b/pkg/hsm/setup.go @@ -0,0 +1,62 @@ +package hsm + +import ( + "log" + + "git.cacert.org/cacert-gosigner/pkg/config" + "github.com/ThalesIgnite/crypto11" +) + +func EnsureCAKeysAndCertificates(p11Context *crypto11.Context, conf *config.SignerConfig) error { + var err error + + for _, root := range conf.CAs { + root.Certificate, root.KeyPair, err = GetRootCACertificate(p11Context, conf.Global, root) + if err != nil { + return err + } + + log.Printf("got root CA certificate:\n Subject %s\n Issuer %s\n Valid from %s until %s\n Serial %s", + root.Certificate.Subject, + root.Certificate.Issuer, + root.Certificate.NotBefore, + root.Certificate.NotAfter, + root.Certificate.SerialNumber) + + for _, intermediary := range root.SubCAs { + err := setupIntermediaries(p11Context, conf.Global, intermediary, root) + if err != nil { + return err + } + } + } + + return nil +} + +func setupIntermediaries(p11Context *crypto11.Context, settings *config.Settings, intermediary, parent *config.CaCertificateEntry) error { + var err error + + intermediary.Parent = parent + + intermediary.Certificate, intermediary.KeyPair, err = GetIntermediaryCACertificate(p11Context, settings, intermediary) + if err != nil { + return err + } + + log.Printf("got intermediary CA certificate:\n Subject %s\n Issuer %s\n Valid from %s until %s\n Serial %s", + intermediary.Certificate.Subject, + intermediary.Certificate.Issuer, + intermediary.Certificate.NotBefore, + intermediary.Certificate.NotAfter, + intermediary.Certificate.SerialNumber) + + for _, sub := range intermediary.SubCAs { + err := setupIntermediaries(p11Context, settings, sub, intermediary) + if err != nil { + return err + } + } + + return nil +}