diff --git a/.gitignore b/.gitignore index 056169c..b623959 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.pub .idea/ ca-hierarchy.json +config.yaml dist/ \ No newline at end of file diff --git a/cmd/signer/amd64.go b/cmd/signer/amd64.go deleted file mode 100644 index 30dff02..0000000 --- a/cmd/signer/amd64.go +++ /dev/null @@ -1,5 +0,0 @@ -//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 deleted file mode 100644 index bb2fc1a..0000000 --- a/cmd/signer/arm64.go +++ /dev/null @@ -1,5 +0,0 @@ -//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 deleted file mode 100644 index b999ba0..0000000 --- a/cmd/signer/armhf.go +++ /dev/null @@ -1,5 +0,0 @@ -//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 ece7c47..beca33c 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -2,16 +2,10 @@ 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" ) @@ -22,23 +16,23 @@ var ( ) const ( - defaultTokenLabel = "localhsm" - defaultSignerConfigFile = "ca-hierarchy.json" + defaultSignerConfigFile = "config.yaml" ) func main() { - p11Config := &crypto11.Config{} var ( showVersion bool signerConfigFile string + setupMode bool ) + log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile | log.LUTC) + 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.BoolVar(&setupMode, "setup", false, "setup mode") flag.Parse() @@ -46,9 +40,6 @@ func main() { 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) @@ -59,41 +50,20 @@ func main() { 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) + if setupMode { + log.Print("running in setup mode") } - defer func(p11Context *crypto11.Context) { - err := p11Context.Close() - if err != nil { - log.Printf("could not close PKCS#11 library context: %v", err) - } - }(p11Context) + ctx := hsm.SetupContext(caConfig, setupMode) - err = hsm.EnsureCAKeysAndCertificates(p11Context, caConfig) + err = hsm.EnsureCAKeysAndCertificates(ctx) 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) + if setupMode { + return } - p11Config.Pin = strings.TrimSpace(pin) + + log.Print("setup complete, starting signer operation") } diff --git a/docs/config.sample.yaml b/docs/config.sample.yaml new file mode 100644 index 0000000..80616f8 --- /dev/null +++ b/docs/config.sample.yaml @@ -0,0 +1,131 @@ +--- +# configuration example for the CAcert signer software +# +# each directive is described at its first occurrence. If a description is +# missing please file a bug report. + +# Settings defines global settings for the signer +Settings: + # define address information for the CA operator, this will be used for + # the SubjectDN of CA certificates + organization: + country: [ "CH" ] + organization: [ "CAcert Inc." ] + locality: [ "Genève" ] + street-address: [ "Clos Belmont 2" ] + postal-code: [ "1208" ] + # define how long CA certificates should be valid + validity-years: + root: 20 + intermediary: 5 + # URL patterns used for certificate fields. The first %s is replaced with + # the identifier of a CA certificate + url-patterns: + ocsp: "http://ocsp.cacert.org/" + crl: "http://crl.cacert.org/%s.crl" + issuer: "http://www.cacert.org/certs/%s.crt" + +# KeyStorage defines PKCS#11 tokens, a token named 'default' must be present +KeyStorage: + default: + # HSM type, softhsm support is builtin + type: softhsm + # token label of the PKCS#11 token + label: localhsm + offline: + # HSM type, p11module requires a module parameter + type: p11module + # path to a PKCS#11 shared library module + module: /usr/lib/x86_64-linux-gnu/pkcs11/onepin-opensc-pkcs11.so + label: smartcard + +# CAs defines the CA hierarchy of root and intermediary CA certificates +CAs: + # a root CA, the map key will be used as a label for PKCS11 and URLs + ecc_root_2022: + # information about the private key + key-info: + # key algorithm EC (for elliptic curve) or RSA are supported + algorithm: "EC" + # elliptic curve name P-224, P-256, P-384 and P-521 are supported + ecc-curve: "P-521" + # common name for the SubjectDN of the CA certificate + common-name: "CAcert ECC Root CA 2022" + # storage can be any label from the KeyStorage configuration + storage: offline + rsa_root_2022: + key-info: + algorithm: "RSA" + # RSA key length in bits (> 3072 is recommended by NIST, BSI and others) + rsa-bits: 4096 + common-name: "CAcert RSA Root CA 2022" + storage: offline + ecc_person_2022: + key-info: + algorithm: "EC" + ecc-curve: "P-384" + # parent CA, must be any of the other defined CAs + parent: "ecc_root_2022" + common-name: "CAcert ECC Person CA 2022" + # extended key usage, only makes sense for non root CAs, supported values are + # + # - client for client authentication + # - code for code signing + # - email for email protection (aka S/MIME) + # - ocsp for OCSP signing + # - server for server authentication + # + # CAs should only sign endpoint certificates for one of the extended key usages + # defined in the CA certificate itself + ext-key-usages: + - client + - code + - email + - ocsp + rsa_person_2022: + key-info: + algorithm: "RSA" + rsa-bits: 3072 + parent: "rsa_root_2022" + common-name: "CAcert RSA Person CA 2022" + ext-key-usages: + - client + - code + - email + - ocsp + ecc_client_2022: + key-info: + algorithm: "EC" + ecc-curve: "P-384" + parent: "ecc_root_2022" + common-name: "CAcert ECC Client CA 2022" + ext-key-usages: + - client + - ocsp + rsa_client_2022: + key-info: + algorithm: "RSA" + rsa-bits: 3072 + parent: "rsa_root_2022" + common-name: "CAcert RSA Client CA 2022" + ext-key-usages: + - client + - ocsp + ecc_server_2022: + key-info: + algorithm: "EC" + ecc-curve: "P-384" + parent: "ecc_root_2022" + common-name: "CAcert ECC Server CA 2022" + ext-key-usages: + - server + - ocsp + rsa_server_2022: + key-info: + algorithm: "RSA" + rsa-bits: 3072 + parent: "rsa_root_2022" + common-name: "CAcert RSA Server CA 2022" + ext-key-usages: + - server + - ocsp diff --git a/go.mod b/go.mod index 5cf72ca..8ac9014 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/ThalesIgnite/crypto11 v1.2.5 github.com/stretchr/testify v1.7.1 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) require ( @@ -15,5 +16,4 @@ require ( 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/pkg/config/amd64.go b/pkg/config/amd64.go new file mode 100644 index 0000000..7e76301 --- /dev/null +++ b/pkg/config/amd64.go @@ -0,0 +1,5 @@ +//go:build linux && amd64 + +package config + +const softHsmModule = "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so" diff --git a/pkg/config/arm64.go b/pkg/config/arm64.go new file mode 100644 index 0000000..b2c7386 --- /dev/null +++ b/pkg/config/arm64.go @@ -0,0 +1,5 @@ +//go:build linux && arm64 + +package config + +const softHsmModule = "/usr/lib/aarch64-linux-gnu/softhsm/libsofthsm2.so" diff --git a/pkg/config/armhf.go b/pkg/config/armhf.go new file mode 100644 index 0000000..98266bf --- /dev/null +++ b/pkg/config/armhf.go @@ -0,0 +1,5 @@ +//go:build linux && arm + +package config + +const softHsmModule = "/usr/lib/arm-linux-gnueabihf/softhsm/libsofthsm2.so" diff --git a/pkg/config/config.go b/pkg/config/config.go index 1f536aa..25b1fdb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,71 +5,183 @@ import ( "crypto/elliptic" "crypto/x509" "crypto/x509/pkix" - "encoding/json" "errors" "fmt" "io" + "strings" "time" + + "gopkg.in/yaml.v3" ) type Settings struct { - Organization pkix.Name `json:"organization"` - RootYears int `json:"root-years"` - IntermediaryYears int `json:"intermediary-years"` + 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"` } -func (s *Settings) CalculateValidity(cert *CaCertificateEntry) (time.Time, time.Time) { - var notBefore, notAfter time.Time - notBefore = time.Now() +type KeyStorage struct { + Label string + Module string +} - if cert.Parent == nil { - notAfter = notBefore.AddDate(s.RootYears, 0, 0) - } else { - notAfter = notBefore.AddDate(s.IntermediaryYears, 0, 0) +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) } - return notBefore, notAfter -} + 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) + } -func (s *Settings) CalculateSubject(cert *CaCertificateEntry) pkix.Name { - subject := s.Organization - subject.CommonName = cert.CommonName + k.Label = ks.Label - 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) + return nil } type SignerConfig struct { - Global *Settings `json:"Settings"` - CAs []*CaCertificateEntry `json:"CAs"` + global *Settings `yaml:"Settings"` + caMap map[string]*CaCertificateEntry `yaml:"CAs"` + KeyStorage map[string]*KeyStorage `yaml:"KeyStorage"` } -// LoadConfiguration reads JSON configuration from the given reader as a SignerConfig structure +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) { - data, err := io.ReadAll(r) + 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 load configuration: %w", err) + return nil, fmt.Errorf("could not parse YAML 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 + return &SignerConfig{ + global: config.Global, + caMap: config.CAs, + KeyStorage: config.KeyStorage, + }, nil } type PrivateKeyInfo struct { @@ -78,15 +190,15 @@ type PrivateKeyInfo struct { RSABits int } -func (p *PrivateKeyInfo) UnmarshalJSON(data []byte) error { +func (p *PrivateKeyInfo) UnmarshalYAML(value *yaml.Node) error { internalStructure := struct { - Label string `json:"label"` - Algorithm string `json:"algorithm"` - EccCurve string `json:"ecc-curve,omitempty"` - RSABits *int `json:"rsa-bits,omitempty"` + Label string `yaml:"label"` + Algorithm string `yaml:"algorithm"` + EccCurve string `yaml:"ecc-curve,omitempty"` + RSABits *int `yaml:"rsa-bits,omitempty"` }{} - err := json.Unmarshal(data, &internalStructure) + err := value.Decode(&internalStructure) if err != nil { return fmt.Errorf("could not unmarshal private key info: %w", err) } @@ -111,11 +223,11 @@ func (p *PrivateKeyInfo) UnmarshalJSON(data []byte) error { return nil } -func (p *PrivateKeyInfo) MarshalJSON() ([]byte, error) { +func (p *PrivateKeyInfo) MarshalYAML() (interface{}, error) { internalStructure := struct { - Algorithm string `json:"algorithm"` - EccCurve string `json:"ecc-curve,omitempty"` - RSABits *int `json:"rsa-bits,omitempty"` + Algorithm string `yaml:"algorithm"` + EccCurve string `yaml:"ecc-curve,omitempty"` + RSABits *int `yaml:"rsa-bits,omitempty"` }{} switch p.Algorithm { case x509.RSA: @@ -130,12 +242,7 @@ func (p *PrivateKeyInfo) MarshalJSON() ([]byte, error) { 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 + return internalStructure, nil } func curveToName(curve elliptic.Curve) (string, error) { @@ -170,45 +277,47 @@ func nameToCurve(name string) (elliptic.Curve, error) { } 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"` + 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 *CaCertificateEntry + parent string + Storage string } -func (c *CaCertificateEntry) CertificateFileName() string { - return c.Label + ".crt" -} - -func (c *CaCertificateEntry) UnmarshalJSON(data []byte) error { +func (c *CaCertificateEntry) UnmarshalYAML(value *yaml.Node) 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"` + 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 := json.Unmarshal(data, &m) + err := value.Decode(&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) } + c.parent = m.Parent + + if m.Storage != "" { + c.Storage = m.Storage + } else { + c.Storage = "default" + } + if err != nil { return err } @@ -216,6 +325,10 @@ func (c *CaCertificateEntry) UnmarshalJSON(data []byte) error { return nil } +func (c *CaCertificateEntry) IsRoot() bool { + return c.parent == "" +} + func mapExtKeyUsageNames(usages []string) ([]x509.ExtKeyUsage, error) { extKeyUsages := make([]x509.ExtKeyUsage, len(usages)) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f53c39f..6353e59 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -3,13 +3,13 @@ package config import ( "crypto/elliptic" "crypto/x509" - "encoding/json" "testing" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) -func TestPrivateKeyInfo_MarshalJSON(t *testing.T) { +func TestPrivateKeyInfo_MarshalYAML(t *testing.T) { testData := []struct { name string pkInfo *PrivateKeyInfo @@ -21,7 +21,9 @@ func TestPrivateKeyInfo_MarshalJSON(t *testing.T) { Algorithm: x509.RSA, RSABits: 3072, }, - `{"algorithm":"RSA","rsa-bits":3072}`, + `algorithm: RSA +rsa-bits: 3072 +`, }, { "ECDSA", @@ -29,13 +31,15 @@ func TestPrivateKeyInfo_MarshalJSON(t *testing.T) { Algorithm: x509.ECDSA, EccCurve: elliptic.P224(), }, - `{"algorithm":"EC","ecc-curve":"P-224"}`, + `algorithm: EC +ecc-curve: P-224 +`, }, } for _, item := range testData { t.Run(item.name, func(t *testing.T) { - data, err := json.Marshal(item.pkInfo) + data, err := yaml.Marshal(item.pkInfo) if err != nil { t.Fatal(err) } @@ -45,16 +49,18 @@ func TestPrivateKeyInfo_MarshalJSON(t *testing.T) { } } -func TestPrivateKeyInfo_UnmarshalJSON(t *testing.T) { +func TestPrivateKeyInfo_UnmarshalYAML(t *testing.T) { testData := []struct { name string - json string + yaml string expected *PrivateKeyInfo expectErr bool }{ { "RSA", - `{"label":"mykey","algorithm":"RSA","rsa-bits":2048}`, + `label: "mykey" +algorithm: "RSA" +rsa-bits: 2048`, &PrivateKeyInfo{ Algorithm: x509.RSA, RSABits: 2048, @@ -63,7 +69,9 @@ func TestPrivateKeyInfo_UnmarshalJSON(t *testing.T) { }, { "ECDSA", - `{"label":"mykey","algorithm":"EC","ecc-curve":"P-521"}`, + `label: "mykey" +algorithm: "EC" +ecc-curve: "P-521"`, &PrivateKeyInfo{ Algorithm: x509.ECDSA, EccCurve: elliptic.P521(), @@ -72,19 +80,21 @@ func TestPrivateKeyInfo_UnmarshalJSON(t *testing.T) { }, { "no-algorithm", - `{"label":"mykey"}`, + `label: "mykey"`, nil, true, }, { "RSA-no-rsa-bits", - `{"label":"mykey","algorithm":"RSA"}`, + `label: "mykey" +algorithm: "RSA"`, nil, true, }, { "ECDSA-no-curve", - `{"label":"mykey","algorithm":"EC"}`, + `label: "mykey" +algorithm: "EC"`, nil, true, }, @@ -93,7 +103,7 @@ func TestPrivateKeyInfo_UnmarshalJSON(t *testing.T) { for _, item := range testData { t.Run(item.name, func(t *testing.T) { pkInfo := &PrivateKeyInfo{} - err := json.Unmarshal([]byte(item.json), pkInfo) + err := yaml.Unmarshal([]byte(item.yaml), pkInfo) if err != nil { if !item.expectErr { t.Fatal(err) @@ -107,7 +117,7 @@ func TestPrivateKeyInfo_UnmarshalJSON(t *testing.T) { } } -func TestCaCertificateEntry_UnmarshalJSON(t *testing.T) { +func TestCaCertificateEntry_UnmarshalYAML(t *testing.T) { data := `{ "label":"root", "key-info": { @@ -120,7 +130,7 @@ func TestCaCertificateEntry_UnmarshalJSON(t *testing.T) { entry := CaCertificateEntry{} - err := json.Unmarshal([]byte(data), &entry) + err := yaml.Unmarshal([]byte(data), &entry) if err != nil { t.Fatal(err) } diff --git a/pkg/hsm/context.go b/pkg/hsm/context.go new file mode 100644 index 0000000..03107bd --- /dev/null +++ b/pkg/hsm/context.go @@ -0,0 +1,56 @@ +package hsm + +import ( + "context" + + "github.com/ThalesIgnite/crypto11" + + "git.cacert.org/cacert-gosigner/pkg/config" +) + +type ctxKey int + +const ( + ctxP11Contexts ctxKey = iota + ctxSetupMode + ctxSignerConfig +) + +// SetupContext sets global context for HSM operations. +func SetupContext( + signerConfig *config.SignerConfig, + setupMode bool, +) context.Context { + ctx := context.Background() + + ctx = context.WithValue(ctx, ctxP11Contexts, make(map[string]*crypto11.Context)) + ctx = context.WithValue(ctx, ctxSignerConfig, signerConfig) + ctx = context.WithValue(ctx, ctxSetupMode, setupMode) + + return ctx +} + +func GetSignerConfig(ctx context.Context) *config.SignerConfig { + return ctx.Value(ctxSignerConfig).(*config.SignerConfig) +} + +func IsSetupMode(ctx context.Context) bool { + return ctx.Value(ctxSetupMode).(bool) +} + +func GetP11Context(ctx context.Context, entry *config.CaCertificateEntry) (*crypto11.Context, error) { + contexts := ctx.Value(ctxP11Contexts).(map[string]*crypto11.Context) + + if p11Context, ok := contexts[entry.Storage]; ok { + return p11Context, nil + } + + p11Context, err := prepareCrypto11Context(ctx, entry.Storage) + if err != nil { + return nil, err + } + + contexts[entry.Storage] = p11Context + + return p11Context, nil +} diff --git a/pkg/hsm/hsm.go b/pkg/hsm/hsm.go index 1780956..cc589d0 100644 --- a/pkg/hsm/hsm.go +++ b/pkg/hsm/hsm.go @@ -1,6 +1,7 @@ package hsm import ( + "context" "crypto" "crypto/ecdsa" "crypto/rand" @@ -14,39 +15,67 @@ import ( "math/big" "os" "syscall" - "time" + + "github.com/ThalesIgnite/crypto11" "git.cacert.org/cacert-gosigner/pkg/config" - "github.com/ThalesIgnite/crypto11" ) var ( // 1.3.6.1.4.1.18506.2.3.1 Class3 Policy Version 1 oidCAcertClass3PolicyV1 = []int{1, 3, 6, 1, 4, 1, 18506, 2, 3, 1} + + errKeyGenerationRefused = errors.New("not in setup mode, refusing key generation") + errCertificateGenerationRefused = errors.New("not in setup mode, refusing certificate generation") ) -func GetRootCACertificate(p11Context *crypto11.Context, settings *config.Settings, caCert *config.CaCertificateEntry) (*x509.Certificate, crypto.Signer, error) { - keyPair, err := getKeyPair(p11Context, caCert.Label, caCert.KeyInfo) +func GetRootCACertificate(ctx context.Context, label string) (*x509.Certificate, error) { + var ( + certificate *x509.Certificate + keyPair crypto.Signer + ) + + sc := GetSignerConfig(ctx) + + caCert, err := sc.GetCADefinition(label) if err != nil { - return nil, nil, err + return nil, err } - certFile := caCert.CertificateFileName() + if !caCert.IsRoot() { + return nil, fmt.Errorf("CA definition %s is not a root CA definition", label) + } - certificate, err := loadCertificate(certFile) + certFile := sc.CertificateFileName(label) + + certificate, err = loadCertificate(certFile) if err != nil { - return nil, nil, err + return nil, err + } + + if certificate != nil && !IsSetupMode(ctx) { + caCert.Certificate = certificate + + return certificate, nil + } + + keyPair, err = getKeyPair(ctx, label, caCert.KeyInfo) + if err != nil { + return nil, err } if certificate != nil && certificateMatches(certificate, keyPair) { - return certificate, keyPair, nil + caCert.Certificate, caCert.KeyPair = certificate, keyPair + + return certificate, nil } - notBefore := time.Now() - notAfter := notBefore.AddDate(settings.RootYears, 0, 0) + if !IsSetupMode(ctx) { + return nil, errCertificateGenerationRefused + } - subject := settings.Organization - subject.CommonName = caCert.CommonName + notBefore, notAfter := sc.CalculateValidity(caCert) + subject := sc.CalculateSubject(caCert) certificate, err = generateRootCACertificate( certFile, @@ -64,43 +93,72 @@ func GetRootCACertificate(p11Context *crypto11.Context, settings *config.Setting ) if err != nil { - return nil, nil, err + return nil, err } - err = addCertificate(p11Context, caCert.Label, certificate) + p11Context, err := GetP11Context(ctx, caCert) if err != nil { - return nil, nil, err + return nil, err } - return certificate, keyPair, nil + err = addCertificate(p11Context, label, certificate) + if err != nil { + return nil, err + } + + caCert.Certificate, caCert.KeyPair = certificate, keyPair + + return certificate, 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) +func GetIntermediaryCACertificate(ctx context.Context, certLabel string) (*x509.Certificate, error) { + var ( + certificate *x509.Certificate + keyPair crypto.Signer + ) + + sc := GetSignerConfig(ctx) + + caCert, err := sc.GetCADefinition(certLabel) if err != nil { - return nil, nil, err + return nil, err } - certFile := caCert.CertificateFileName() + if caCert.IsRoot() { + return nil, fmt.Errorf( + "CA definition %s is a root CA definition, intermediary expected", + certLabel, + ) + } - certificate, err := loadCertificate(certFile) + keyPair, err = getKeyPair(ctx, certLabel, caCert.KeyInfo) if err != nil { - return nil, nil, err + return nil, err + } + + certFile := sc.CertificateFileName(certLabel) + + certificate, err = loadCertificate(certFile) + if err != nil { + return nil, err } if certificate != nil && certificateMatches(certificate, keyPair) { - return certificate, keyPair, nil + caCert.Certificate, caCert.KeyPair = certificate, keyPair + + return certificate, nil } - notBefore, notAfter := settings.CalculateValidity(caCert) - subject := settings.CalculateSubject(caCert) + if !IsSetupMode(ctx) { + return nil, errCertificateGenerationRefused + } + + notBefore, notAfter := sc.CalculateValidity(caCert) + subject := sc.CalculateSubject(caCert) certificate, err = generateIntermediaryCACertificate( - caCert, + sc, + certLabel, keyPair.Public(), &x509.Certificate{ Subject: subject, @@ -112,9 +170,9 @@ func GetIntermediaryCACertificate( 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)}, + IssuingCertificateURL: []string{sc.BuildIssuerURL(caCert)}, + OCSPServer: []string{sc.BuildOCSPURL(caCert)}, + CRLDistributionPoints: []string{sc.BuildCRLUrl(caCert)}, PolicyIdentifiers: []asn1.ObjectIdentifier{ // use policy identifiers from http://wiki.cacert.org/OidAllocation oidCAcertClass3PolicyV1, @@ -123,25 +181,37 @@ func GetIntermediaryCACertificate( ) if err != nil { - return nil, nil, err + return nil, err } - err = addCertificate(p11Context, caCert.Label, certificate) + p11Context, err := GetP11Context(ctx, caCert) if err != nil { - return nil, nil, err + return nil, err } - return certificate, keyPair, nil + err = addCertificate(p11Context, certLabel, certificate) + if err != nil { + return nil, err + } + + caCert.Certificate, caCert.KeyPair = certificate, keyPair + + return certificate, nil } -func generateIntermediaryCACertificate(caCert *config.CaCertificateEntry, publicKey crypto.PublicKey, template *x509.Certificate) (*x509.Certificate, error) { +func generateIntermediaryCACertificate(config *config.SignerConfig, certLabel string, publicKey crypto.PublicKey, template *x509.Certificate) (*x509.Certificate, error) { + parent, err := config.GetParentCA(certLabel) + if err != nil { + return nil, err + } + serial, err := randomSerialNumber() if err != nil { return nil, err } template.SerialNumber = serial - template.SignatureAlgorithm, err = determineSignatureAlgorithm(caCert.Parent.KeyPair) + template.SignatureAlgorithm, err = determineSignatureAlgorithm(parent.KeyPair) if err != nil { return nil, err @@ -150,9 +220,9 @@ func generateIntermediaryCACertificate(caCert *config.CaCertificateEntry, public certBytes, err := x509.CreateCertificate( rand.Reader, template, - caCert.Parent.Certificate, + parent.Certificate, publicKey, - caCert.Parent.KeyPair, + parent.KeyPair, ) if err != nil { return nil, fmt.Errorf("could not create intermediary CA certificate: %w", err) @@ -163,7 +233,7 @@ func generateIntermediaryCACertificate(caCert *config.CaCertificateEntry, public Bytes: certBytes, } - certFile := caCert.CertificateFileName() + certFile := config.CertificateFileName(certLabel) err = os.WriteFile(certFile, pem.EncodeToMemory(certBlock), 0o600) if err != nil { @@ -192,7 +262,22 @@ func addCertificate(p11Context *crypto11.Context, label string, certificate *x50 return nil } -func getKeyPair(p11Context *crypto11.Context, label string, keyInfo *config.PrivateKeyInfo) (crypto.Signer, error) { +func getKeyPair(ctx context.Context, label string, keyInfo *config.PrivateKeyInfo) (crypto.Signer, error) { + sc := GetSignerConfig(ctx) + cert, err := sc.GetCADefinition(label) + if err != nil { + return nil, err + } + + if cert.KeyPair != nil { + return cert.KeyPair, nil + } + + p11Context, err := GetP11Context(ctx, cert) + if err != nil { + return nil, err + } + keyPair, err := p11Context.FindKeyPair(nil, []byte(label)) if err != nil { return nil, fmt.Errorf("could not find requested key pair: %w", err) @@ -202,6 +287,10 @@ func getKeyPair(p11Context *crypto11.Context, label string, keyInfo *config.Priv return keyPair, nil } + if !IsSetupMode(ctx) { + return nil, errKeyGenerationRefused + } + switch keyInfo.Algorithm { case x509.RSA: keyPair, err = generateRSAKeyPair(p11Context, label, keyInfo) diff --git a/pkg/hsm/setup.go b/pkg/hsm/setup.go index 3d8eca7..c5ecec5 100644 --- a/pkg/hsm/setup.go +++ b/pkg/hsm/setup.go @@ -1,61 +1,41 @@ package hsm import ( + "context" "log" - - "git.cacert.org/cacert-gosigner/pkg/config" - "github.com/ThalesIgnite/crypto11" ) -func EnsureCAKeysAndCertificates(p11Context *crypto11.Context, conf *config.SignerConfig) error { - var err error +func EnsureCAKeysAndCertificates(ctx context.Context) error { + var label string - for _, root := range conf.CAs { - root.Certificate, root.KeyPair, err = GetRootCACertificate(p11Context, conf.Global, root) + conf := GetSignerConfig(ctx) + + for _, label := range conf.RootCAs() { + crt, err := GetRootCACertificate(ctx, label) 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 - } - } + crt.Subject, + crt.Issuer, + crt.NotBefore, + crt.NotAfter, + crt.SerialNumber) } - 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) + for _, label = range conf.IntermediaryCAs() { + crt, err := GetIntermediaryCACertificate(ctx, label) 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", + crt.Subject, + crt.Issuer, + crt.NotBefore, + crt.NotAfter, + crt.SerialNumber) } return nil diff --git a/pkg/hsm/storage.go b/pkg/hsm/storage.go new file mode 100644 index 0000000..fb10b12 --- /dev/null +++ b/pkg/hsm/storage.go @@ -0,0 +1,71 @@ +package hsm + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "strings" + "syscall" + + "github.com/ThalesIgnite/crypto11" + "golang.org/x/term" +) + +func prepareCrypto11Context(ctx context.Context, label string) (*crypto11.Context, error) { + storage, ok := GetSignerConfig(ctx).KeyStorage[label] + if !ok { + return nil, fmt.Errorf("could not find storage definition with label %s", label) + } + + var ( + err error + p11Context *crypto11.Context + ) + + p11Config := &crypto11.Config{ + Path: storage.Module, + TokenLabel: storage.Label, + } + + log.Printf("using PKCS#11 module %s", p11Config.Path) + log.Printf("looking for token with label %s", p11Config.TokenLabel) + + p11Config.Pin, err = getPin(p11Config) + if err != nil { + return nil, err + } + + p11Context, err = crypto11.Configure(p11Config) + if err != nil { + return nil, fmt.Errorf("could not configure PKCS#11 library: %v", err) + } + + return p11Context, nil +} + +func getPin(p11Config *crypto11.Config) (string, error) { + tokenPinEnv := fmt.Sprintf("TOKEN_PIN_%s", strings.ToUpper(p11Config.TokenLabel)) + pin, found := os.LookupEnv(tokenPinEnv) + if !found { + log.Printf("environment variable %s has not been set", tokenPinEnv) + + if !term.IsTerminal(syscall.Stdin) { + return "", errors.New("stdin is not a terminal") + } + + fmt.Printf("Enter PIN for token %s: ", p11Config.TokenLabel) + + bytePin, err := term.ReadPassword(syscall.Stdin) + if err != nil { + return "", errors.New("could not read PIN") + } + + fmt.Println() + + pin = string(bytePin) + } + + return strings.TrimSpace(pin), nil +}