diff --git a/cmd/ceremony/README.md b/cmd/ceremony/README.md new file mode 100644 index 000000000..18211d600 --- /dev/null +++ b/cmd/ceremony/README.md @@ -0,0 +1,182 @@ +# `ceremony` + +``` +ceremony --config path/to/config.yml +``` + +`ceremony` is a tool designed for Certificate Authority specific key and certificate ceremonies. The main design principle is that unlike most ceremony tooling there is a single user input, a configuration file, which is required to complete a root, intermediate, or key ceremony. The goal is to make ceremonies as simple as possible and allow for simple verification of a single file, instead of verification of a large number of independent commands. + +`ceremony` operates in one of three modes +* `root` - generates a signing key on HSM and creates a self-signed root certificate that uses the generated key, outputting a PEM public key, and a PEM certificate +* `intermediate` - creates a intermediate certificate and signs it using a signing key already on a HSM, outputting a PEM certificate +* `key` - generates a signing key on HSM, outputting a PEM public key + +These modes are set in the `ceremony-type` field of the configuration file. + +## Configuration format + +`ceremony` uses YAML for its configuration file, mainly as it allows for commenting. Each ceremony type has a different set of configuration fields. + +### Root ceremony + +- `ceremony-type`: string describing the ceremony type, `root`. +- `pkcs11`: object containing PKCS#11 related fields. + | Field | Description | + | --- | --- | + | `module` | Path to the PKCS#11 module to use to communicate with a HSM. | + | `pin` | Specifies the login PIN, should only be provided if the HSM device requires one to interact with the slot. | + | `store-key-in-slot` | Specifies which HSM object slot the generated signing key should be stored in. | + | `store-key-with-label` | Specifies the HSM object label for the generated signing key. | +- `key`: object containing key generation related fields. + | Field | Description | + | --- | --- | + | `key-type` | Specifies the type of key to be generated, either `rsa` or `ecdsa`. If `rsa` the generated key will have an exponent of 65537 and a modulus length specified by `rsa-mod-length`. If `ecdsa` the curve is specified by `ecdsa-curve`. | + | `ecdsa-curve` | Specifies the ECDSA curve to use when generating key, either `P-224`, `P-256`, `P-384`, or `P-521`. | + | `rsa-mod-length` | Specifies the length of the RSA modulus, either `2048` or `4096`. +- `outputs`: object containing paths to write outputs. + | Field | Description | + | --- | --- | + | `public-key-path` | Path to store generated PEM public key. | + | `certificate-path` | Path to store signed PEM certificate. | +- `certificate-profile`: object containing profile for certificate to generate. Fields are documented [below](#Certificate-profile-format). + +Example: + +```yaml +ceremony-type: root +pkcs11: + module: /usr/lib/opensc-pkcs11.so + store-key-in-slot: 0 + store-key-with-label: root signing key +key: + type: ecdsa + ecdsa-curve: P-384 +outputs: + public-key-path: /home/user/root-signing-pub.pem + certificate-path: /home/user/root-cert.pem +certificate-profile: + signature-algorithm: ECDSAWithSHA384 + common-name: CA intermediate + organization: good guys + country: US + not-before: 2020-01-01 12:00:00 + not-after: 2040-01-01 12:00:00 + key-usages: + - Cert Sign + - CRL Sign +``` + +This config generates a ECDSA P-384 key in the HSM with the object label `root signing key` and uses this key to sign a self-signed certificate. The public key for the key generated is written to `/home/user/root-signing-pub.pem` and the certificate is written to `/home/user/root-cert.pem`. + +### Intermediate ceremony + +- `ceremony-type`: string describing the ceremony type, `intermediate`. +- `pkcs11`: object containing PKCS#11 related fields. + | Field | Description | + | --- | --- | + | `module` | Path to the PKCS#11 module to use to communicate with a HSM. | + | `pin` | Specifies the login PIN, should only be provided if the HSM device requires one to interact with the slot. | + | `signing-key-slot` | Specifies which HSM object slot the signing key is in. | + | `signing-key-label` | Specifies the HSM object label for the signing key. | + | `signing-key-id` | Specifies the HSM object ID for the signing key. | +- `inputs`: object containing paths for inputs + | Field | Description | + | --- | --- | + | `public-key-path` | Path to PEM subject public key for certificate. | + | `issuer-path` | Path to PEM issuer certificate. | +- `outputs`: object containing paths to write outputs. + | Field | Description | + | --- | --- | + | `certificate-path` | Path to store signed PEM certificate. | +- `certificate-profile`: object containing profile for certificate to generate. Fields are documented [below](#Certificate-profile-format). + +Example: + +```yaml +ceremony-type: intermediate +pkcs11: + module: /usr/lib/opensc-pkcs11.so + signing-key-slot: 0 + signing-key-label: root signing key + signing-key-id: ffff +inputs: + public-key-path: /home/user/intermediate-signing-pub.pem + issuer-path: /home/user/root-cert.pem +outputs: + certificate-path: /home/user/intermediate-cert.pem +certificate-profile: + signature-algorithm: ECDSAWithSHA384 + common-name: CA root + organization: good guys + country: US + not-before: 2020-01-01 12:00:00 + not-after: 2040-01-01 12:00:00 + ocsp-url: http://good-guys.com/ocsp + crl-url: http://good-guys.com/crl + issuer-url: http://good-guys.com/root + policy-oids: + - 1.2.3 + - 5.4.3.2.1 + key-usages: + - Digital Signature + - Cert Sign + - CRL Sign +``` + +This config generates an intermediate certificate signed by a key in the HSM, identified by the object label `root signing key` and the object ID `ffff`. The subject key used is taken from `/home/user/intermediate-signing-pub.pem` and the issuer is `/home/user/root-cert.pem`, the resulting certificate is written to `/home/user/intermediate-cert.pem`. + +### Key ceremony + +- `ceremony-type`: string describing the ceremony type, `key`. +- `pkcs11`: object containing PKCS#11 related fields. + | Field | Description | + | --- | --- | + | `module` | Path to the PKCS#11 module to use to communicate with a HSM. | + | `pin` | Specifies the login PIN, should only be provided if the HSM device requires one to interact with the slot. | + | `store-key-in-slot` | Specifies which HSM object slot the generated signing key should be stored in. | + | `store-key-with-label` | Specifies the HSM object label for the generated signing key. | +- `key`: object containing key generation related fields. + | Field | Description | + | --- | --- | + | `key-type` | Specifies the type of key to be generated, either `rsa` or `ecdsa`. If `rsa` the generated key will have an exponent of 65537 and a modulus length specified by `rsa-mod-length`. If `ecdsa` the curve is specified by `ecdsa-curve`. | + | `ecdsa-curve` | Specifies the ECDSA curve to use when generating key, either `P-224`, `P-256`, `P-384`, or `P-521`. | + | `rsa-mod-length` | Specifies the length of the RSA modulus, either `2048` or `4096`. +- `outputs`: object containing paths to write outputs. + | Field | Description | + | --- | --- | + | `public-key-path` | Path to store generated PEM public key. | + +Example: + +```yaml +ceremony-type: key +pkcs11: + module: /usr/lib/opensc-pkcs11.so + store-key-in-slot: 0 + store-key-with-label: intermediate signing key +key: + key-type: ecdsa + ecdsa-curve: P-384 +outputs: + public-key-path: /home/user/intermediate-signing-pub.pem +``` + +This config generates an ECDSA P-384 key in the HSM with the object label `intermediate signing key`. The public key is written to `/home/user/intermediate-signing-pub.pem`. + +### Certificate profile format + +The certificate profile defines a restricted set of fields that are used to generate root and intermediate certificates. + +| Field | Description | +| --- | --- | +| `signature-algorithm` | Specifies the signing algorithm to use, one of `SHA256WithRSA`, `SHA384WithRSA`, `SHA512WithRSA`, `ECDSAWithSHA256`, `ECDSAWithSHA384`, `ECDSAWithSHA512` | +| `common-name` | Specifies the subject commonName | +| `organization` | Specifies the subject organization | +| `country` | Specifies the subject country | +| `not-before` | Specifies the certificate notBefore date, in the format `2006-01-02 15:04:05`. The time will be interpreted as UTC. | +| `not-after` | Specifies the certificate notAfter date, in the format `2006-01-02 15:04:05`. The time will be interpreted as UTC. | +| `ocsp-url` | Specifies the AIA OCSP responder URL | +| `crl-url` | Specifies the cRLDistributionPoints URL | +| `issuer-url` | Specifies the AIA caIssuer URL | +| `policy-oids` | Specifies the contents of the certificatePolicies extension | +| `key-usages` | Specifies list of key usage bits should be set, list can contain `Digital Signature`, `CRL Sign`, and `Cert Sign` | diff --git a/cmd/gen-ca/main.go b/cmd/ceremony/cert.go similarity index 51% rename from cmd/gen-ca/main.go rename to cmd/ceremony/cert.go index 8148fb4c3..1e806501c 100644 --- a/cmd/gen-ca/main.go +++ b/cmd/ceremony/cert.go @@ -7,26 +7,213 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" - "encoding/hex" - "encoding/json" - "encoding/pem" "errors" - "flag" "fmt" "io" - "io/ioutil" - "log" "math/big" - "os" "strconv" "strings" "time" - "github.com/miekg/pkcs11" - "github.com/letsencrypt/boulder/pkcs11helpers" + "github.com/miekg/pkcs11" ) +// certProfile contains the information required to generate a certificate +type certProfile struct { + // SignatureAlgorithm should contain one of the allowed signature algorithms + // in AllowedSigAlgs + SignatureAlgorithm string `yaml:"signature-algorithm"` + + // CommonName should contain the requested subject common name + CommonName string `yaml:"common-name"` + // Organization should contain the requested subject organization + Organization string `yaml:"organization"` + // Country should contain the requested subject country code + Country string `yaml:"country"` + + // NotBefore should contain the requested NotBefore date for the + // certificate in the format "2006-01-02 15:04:05". Dates will + // always be UTC. + NotBefore string `yaml:"not-before"` + // NotAfter should contain the requested NotAfter date for the + // certificate in the format "2006-01-02 15:04:05". Dates will + // always be UTC. + NotAfter string `yaml:"not-after"` + + // OCSPURL should contain the URL at which a OCSP responder that + // can respond to OCSP requests for this certificate operates + OCSPURL string `yaml:"ocsp-url"` + // CRLURL should contain the URL at which CRLs for this certificate + // can be found + CRLURL string `yaml:"crl-url"` + // IssuerURL should contain the URL at which the issuing certificate + // can be found, this is only required if generating an intermediate + // certificate + IssuerURL string `yaml:"issuer-url"` + + // PolicyOIDs should contain any OIDs to be inserted in a certificate + // policies extension. These should be formatted in the standard OID + // string format (i.e. "1.2.3") + PolicyOIDs []string `yaml:"policy-oids"` + + // KeyUsages should contain the set of key usage bits to set + KeyUsages []string `yaml:"key-usages"` +} + +// AllowedSigAlgs contains the allowed signature algorithms +var AllowedSigAlgs = map[string]x509.SignatureAlgorithm{ + "SHA256WithRSA": x509.SHA256WithRSA, + "SHA384WithRSA": x509.SHA384WithRSA, + "SHA512WithRSA": x509.SHA512WithRSA, + "ECDSAWithSHA256": x509.ECDSAWithSHA256, + "ECDSAWithSHA384": x509.ECDSAWithSHA384, + "ECDSAWithSHA512": x509.ECDSAWithSHA512, +} + +func (profile *certProfile) verifyProfile(root bool) error { + if profile.NotBefore == "" { + return errors.New("not-before is required") + } + if profile.NotAfter == "" { + return errors.New("not-after is required") + } + if profile.SignatureAlgorithm == "" { + return errors.New("signature-algorithm is required") + } + if profile.CommonName == "" { + return errors.New("common-name is required") + } + if profile.Organization == "" { + return errors.New("organization is required") + } + if profile.Country == "" { + return errors.New("country is required") + } + if !root && profile.OCSPURL == "" { + return errors.New("ocsp-url is required for intermediates") + } + if !root && profile.CRLURL == "" { + return errors.New("crl-url is required for intermediates") + } + if !root && profile.IssuerURL == "" { + return errors.New("issuer-url is required for intermediates") + } + return nil +} + +func parseOID(oidStr string) (asn1.ObjectIdentifier, error) { + var oid asn1.ObjectIdentifier + for _, a := range strings.Split(oidStr, ".") { + i, err := strconv.Atoi(a) + if err != nil { + return nil, err + } + oid = append(oid, i) + } + return oid, nil +} + +var stringToKeyUsage = map[string]x509.KeyUsage{ + "Digital Signature": x509.KeyUsageDigitalSignature, + "CRL Sign": x509.KeyUsageCRLSign, + "Cert Sign": x509.KeyUsageCertSign, +} + +// makeTemplate generates the certificate template for use in x509.CreateCertificate +func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte) (*x509.Certificate, error) { + dateLayout := "2006-01-02 15:04:05" + notBefore, err := time.Parse(dateLayout, profile.NotBefore) + if err != nil { + return nil, err + } + notAfter, err := time.Parse(dateLayout, profile.NotAfter) + if err != nil { + return nil, err + } + + var ocspServer []string + if profile.OCSPURL != "" { + ocspServer = []string{profile.OCSPURL} + } + var crlDistributionPoints []string + if profile.CRLURL != "" { + crlDistributionPoints = []string{profile.CRLURL} + } + var issuingCertificateURL []string + if profile.IssuerURL != "" { + issuingCertificateURL = []string{profile.IssuerURL} + } + + var policyOIDs []asn1.ObjectIdentifier + for _, oidStr := range profile.PolicyOIDs { + oid, err := parseOID(oidStr) + if err != nil { + return nil, err + } + policyOIDs = append(policyOIDs, oid) + } + + sigAlg, ok := AllowedSigAlgs[profile.SignatureAlgorithm] + if !ok { + return nil, fmt.Errorf("unsupported signature algorithm %q", profile.SignatureAlgorithm) + } + + subjectKeyID := sha256.Sum256(pubKey) + + serial := make([]byte, 16) + _, err = randReader.Read(serial) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %s", err) + } + + var ku x509.KeyUsage + if len(profile.KeyUsages) == 0 { + return nil, errors.New("key usages must be set") + } + for _, kuStr := range profile.KeyUsages { + kuBit, ok := stringToKeyUsage[kuStr] + if !ok { + return nil, fmt.Errorf("unknown key usage %q", kuStr) + } + ku |= kuBit + } + + cert := &x509.Certificate{ + SignatureAlgorithm: sigAlg, + SerialNumber: big.NewInt(0).SetBytes(serial), + BasicConstraintsValid: true, + IsCA: true, + Subject: pkix.Name{ + CommonName: profile.CommonName, + Organization: []string{profile.Organization}, + Country: []string{profile.Country}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + OCSPServer: ocspServer, + CRLDistributionPoints: crlDistributionPoints, + IssuingCertificateURL: issuingCertificateURL, + PolicyIdentifiers: policyOIDs, + KeyUsage: ku, + SubjectKeyId: subjectKeyID[:], + } + + return cert, nil +} + +// failReader exists to be passed to x509.CreateCertificate which requires +// a source of randomness for signing methods that require a source of +// randomness. Since HSM based signing will generate its own randomness +// we don't need a real reader. Instead of passing a nil reader we use one +// that always returns errors in case the internal usage of this reader +// changes. +type failReader struct{} + +func (fr *failReader) Read([]byte) (int, error) { + return 0, errors.New("Empty reader used by x509.CreateCertificate") +} + // x509Signer is a convenience wrapper used for converting between the // PKCS#11 ECDSA signature format and the RFC 5480 one which is required // for X.509 certificates @@ -68,40 +255,15 @@ func (p *x509Signer) Public() crypto.PublicKey { return p.pub } -// findObject looks up a PKCS#11 object handle based on the provided template. -// In the case where zero or more than one objects are found to match the -// template an error is returned. -func findObject(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) { - if err := ctx.FindObjectsInit(session, tmpl); err != nil { - return 0, err - } - handles, more, err := ctx.FindObjects(session, 1) - if err != nil { - return 0, err - } - if len(handles) == 0 { - return 0, errors.New("no objects found matching provided template") - } - if more { - return 0, errors.New("more than one object matches provided template") - } - if err := ctx.FindObjectsFinal(session); err != nil { - return 0, err - } - return handles[0], nil -} - -// getKey constructs a x509Signer for the private key object associated with the -// given label and ID -func getKey(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label string, idStr string) (*x509Signer, error) { - id, err := hex.DecodeString(idStr) - if err != nil { - return nil, err - } - +// newSigner constructs a x509Signer for the private key object associated with the +// given label and ID. Unlike letsencrypt/pkcs11key this method doesn't rely on +// having the actual public key object in order to retrieve the private key +// handle. This is because we already have the key pair object ID, and as such +// do not need to query the HSM to retrieve it. +func newSigner(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label string, id []byte) (crypto.Signer, error) { // Retrieve the private key handle that will later be used for the certificate // signing operation - privateHandle, err := findObject(ctx, session, []*pkcs11.Attribute{ + privateHandle, err := pkcs11helpers.FindObject(ctx, session, []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), pkcs11.NewAttribute(pkcs11.CKA_LABEL, label), pkcs11.NewAttribute(pkcs11.CKA_ID, id), @@ -121,7 +283,7 @@ func getKey(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label string, // Retrieve the public key handle with the same CKA_ID as the private key // and construct a {rsa,ecdsa}.PublicKey for use in x509.CreateCertificate - pubHandle, err := findObject(ctx, session, []*pkcs11.Attribute{ + pubHandle, err := pkcs11helpers.FindObject(ctx, session, []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), pkcs11.NewAttribute(pkcs11.CKA_LABEL, label), pkcs11.NewAttribute(pkcs11.CKA_ID, id), @@ -159,302 +321,3 @@ func getKey(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label string, pub: pub, }, nil } - -// AllowedSigAlgs contains the allowed signature algorithms -var AllowedSigAlgs = map[string]x509.SignatureAlgorithm{ - "SHA256WithRSA": x509.SHA256WithRSA, - "SHA384WithRSA": x509.SHA384WithRSA, - "SHA512WithRSA": x509.SHA512WithRSA, - "ECDSAWithSHA256": x509.ECDSAWithSHA256, - "ECDSAWithSHA384": x509.ECDSAWithSHA384, - "ECDSAWithSHA512": x509.ECDSAWithSHA512, -} - -// CertProfile contains the information required to generate a certificate -// for signing -type CertProfile struct { - // SignatureAlgorithm should contain one of the allowed signature algorithms - // in AllowedSigAlgs - SignatureAlgorithm string - - // CommonName should contain the requested subject common name - CommonName string - // Organization should contain the requested subject organization - Organization string - // Country should contain the requested subject country code - Country string - - // NotBefore should contain the requested NotBefore date for the - // certificate in the format "2006-01-02 15:04:05". Dates will - // always be UTC. - NotBefore string - // NotAfter should contain the requested NotAfter date for the - // certificate in the format "2006-01-02 15:04:05". Dates will - // always be UTC. - NotAfter string - - // OCSPURL should contain the URL at which a OCSP responder that - // can respond to OCSP requests for this certificate operates - OCSPURL string - // CRLURL should contain the URL at which CRLs for this certificate - // can be found - CRLURL string - // IssuerURL should contain the URL at which the issuing certificate - // can be found, this is only required if generating an intermediate - // certificate - IssuerURL string - - // PolicyOIDs should contain any OIDs to be inserted in a certificate - // policies extension. These should be formatted in the standard OID - // string format (i.e. "1.2.3") - PolicyOIDs []string -} - -func parseOID(oidStr string) (asn1.ObjectIdentifier, error) { - var oid asn1.ObjectIdentifier - for _, a := range strings.Split(oidStr, ".") { - i, err := strconv.Atoi(a) - if err != nil { - return nil, err - } - oid = append(oid, i) - } - return oid, nil -} - -func verifyProfile(profile CertProfile, root bool) error { - if profile.NotBefore == "" { - return errors.New("NotBefore in profile is required") - } - if profile.NotAfter == "" { - return errors.New("NotAfter in profile is required") - } - if profile.SignatureAlgorithm == "" { - return errors.New("SignatureAlgorithm in profile is required") - } - if profile.CommonName == "" { - return errors.New("CommonName in profile is required") - } - if profile.Organization == "" { - return errors.New("Organization in profile is required") - } - if profile.Country == "" { - return errors.New("Country in profile is required") - } - if !root && profile.OCSPURL == "" { - return errors.New("OCSPURL in profile is required for intermediates") - } - if !root && profile.CRLURL == "" { - return errors.New("CRLURL in profile is required for intermediates") - } - if !root && profile.IssuerURL == "" { - return errors.New("IssuerURL in profile is required for intermediates") - } - return nil -} - -// makeTemplate generates the certificate template for use in x509.CreateCertificate -func makeTemplate(ctx pkcs11helpers.PKCtx, profile *CertProfile, pubKey []byte, session pkcs11.SessionHandle) (*x509.Certificate, error) { - dateLayout := "2006-01-02 15:04:05" - notBefore, err := time.Parse(dateLayout, profile.NotBefore) - if err != nil { - return nil, err - } - notAfter, err := time.Parse(dateLayout, profile.NotAfter) - if err != nil { - return nil, err - } - - var ocspServer []string - if profile.OCSPURL != "" { - ocspServer = []string{profile.OCSPURL} - } - var crlDistributionPoints []string - if profile.CRLURL != "" { - crlDistributionPoints = []string{profile.CRLURL} - } - var issuingCertificateURL []string - if profile.IssuerURL != "" { - issuingCertificateURL = []string{profile.IssuerURL} - } - - var policyOIDs []asn1.ObjectIdentifier - for _, oidStr := range profile.PolicyOIDs { - oid, err := parseOID(oidStr) - if err != nil { - return nil, err - } - policyOIDs = append(policyOIDs, oid) - } - - sigAlg, ok := AllowedSigAlgs[profile.SignatureAlgorithm] - if !ok { - return nil, fmt.Errorf("unsupported signature algorithm %q", profile.SignatureAlgorithm) - } - - subjectKeyID := sha256.Sum256(pubKey) - - serial, err := ctx.GenerateRandom(session, 16) - if err != nil { - return nil, fmt.Errorf("failed to generate serial number: %s", err) - } - - cert := &x509.Certificate{ - SignatureAlgorithm: sigAlg, - SerialNumber: big.NewInt(0).SetBytes(serial), - BasicConstraintsValid: true, - IsCA: true, - Subject: pkix.Name{ - CommonName: profile.CommonName, - Organization: []string{profile.Organization}, - Country: []string{profile.Country}, - }, - NotBefore: notBefore, - NotAfter: notAfter, - OCSPServer: ocspServer, - CRLDistributionPoints: crlDistributionPoints, - IssuingCertificateURL: issuingCertificateURL, - PolicyIdentifiers: policyOIDs, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - SubjectKeyId: subjectKeyID[:], - } - - return cert, nil -} - -type failReader struct{} - -func (fr *failReader) Read([]byte) (int, error) { - return 0, errors.New("Empty reader used by x509.CreateCertificate") -} - -func main() { - module := flag.String("module", "", "PKCS#11 module to use") - slot := flag.Uint("slot", 0, "ID of PKCS#11 slot containing token with signing key.") - pin := flag.String("pin", "", "PKCS#11 token PIN. If empty, will assume PED based login.") - label := flag.String("label", "", "PKCS#11 key label") - id := flag.String("id", "", "PKCS#11 hex key ID (simplified format, i.e. ffff") - profilePath := flag.String("profile", "", "Path to file containing certificate profile in JSON format. See https://godoc.org/github.com/letsencrypt/boulder/cmd/gen-ca#CertProfile for details.") - pubKeyPath := flag.String("publicKey", "", "Path to file containing the subject public key in PEM format") - issuerPath := flag.String("issuer", "", "Path to issuer cert if generating an intermediate") - outputPath := flag.String("output", "", "Path to store generated PEM certificate") - flag.Parse() - - if *module == "" { - log.Fatal("--module is required") - } - if *label == "" { - log.Fatal("--label is required") - } - if *id == "" { - log.Fatal("--id is required") - } - if *profilePath == "" { - log.Fatal("--profile is required") - } - if *pubKeyPath == "" { - log.Fatal("--publicKey is required") - } - if *outputPath == "" { - log.Fatal("--output is required") - } - - ctx, session, err := pkcs11helpers.Initialize(*module, *slot, *pin) - if err != nil { - log.Fatalf("Failed to setup session and PKCS#11 context: %s", err) - } - log.Println("Opened PKCS#11 session") - - privKey, err := getKey(ctx, session, *label, *id) - if err != nil { - log.Fatalf("Failed to retrieve private key handle: %s", err) - } - log.Println("Retrieved private key handle") - - profileBytes, err := ioutil.ReadFile(*profilePath) - if err != nil { - log.Fatalf("Failed to read certificate profile %q: %s", *profilePath, err) - } - var profile CertProfile - err = json.Unmarshal(profileBytes, &profile) - if err != nil { - log.Fatalf("Failed to parse certificate profile: %s", err) - } - if err = verifyProfile(profile, *issuerPath == ""); err != nil { - log.Fatalf("Invalid certificate profile: %s", err) - } - - pubPEMBytes, err := ioutil.ReadFile(*pubKeyPath) - if err != nil { - log.Fatalf("Failed to read public key %q: %s", *pubKeyPath, err) - } - pubPEM, _ := pem.Decode(pubPEMBytes) - if pubPEM == nil { - log.Fatal("Failed to parse public key") - } - pub, err := x509.ParsePKIXPublicKey(pubPEM.Bytes) - if err != nil { - log.Fatalf("Failed to parse public key: %s", err) - } - - certTemplate, err := makeTemplate(ctx, &profile, pubPEM.Bytes, session) - if err != nil { - log.Fatalf("Failed to construct certificate template from profile: %s", err) - } - log.Println("Generated certificate template from profile") - var issuer *x509.Certificate - if *issuerPath != "" { - // If generating an intermediate, load the parent issuer and - // set the Authority Key Identifier in the template. - issuerPEMBytes, err := ioutil.ReadFile(*issuerPath) - if err != nil { - log.Fatalf("Failed to read issuer certificate %q: %s", *issuerPath, err) - } - issuerPEM, _ := pem.Decode(issuerPEMBytes) - if issuerPEM == nil { - log.Fatal("Failed to parse issuer certificate PEM") - } - issuer, err = x509.ParseCertificate(issuerPEM.Bytes) - if err != nil { - log.Fatalf("Failed to parse issuer certificate: %s", err) - } - certTemplate.AuthorityKeyId = issuer.SubjectKeyId - } else { - issuer = certTemplate - } - - // x509.CreateCertificate uses a io.Reader here for signing methods that require - // a source of randomness. Since PKCS#11 based signing generates needed randomness - // at the HSM we don't need to pass a real reader. Instead of passing a nil reader - // we use one that always returns errors in case the internal usage of this reader - // changes. - certBytes, err := x509.CreateCertificate(&failReader{}, certTemplate, issuer, pub, privKey) - if err != nil { - log.Fatalf("Failed to create certificate: %s", err) - } - log.Printf("Signed certificate: %x\n", certBytes) - cert, err := x509.ParseCertificate(certBytes) - if err != nil { - log.Fatalf("Failed to parse signed certificate: %s", err) - } - - // If generating a root then the signing key is the public key - // in the cert itself, so set the parent to itself - var parent *x509.Certificate - if *issuerPath == "" { - parent = cert - } else { - parent = issuer - } - if err := cert.CheckSignatureFrom(parent); err != nil { - log.Fatalf("Failed to verify certificate signature: %s", err) - } - log.Println("Verified certificate signature") - - pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) - log.Printf("Certificate PEM:\n%s", pemBytes) - if err := ioutil.WriteFile(*outputPath, pemBytes, os.ModePerm); err != nil { - log.Fatalf("Failed to write certificate to %q: %s", *outputPath, err) - } - log.Printf("Certificate written to %q\n", *outputPath) -} diff --git a/cmd/gen-ca/main_test.go b/cmd/ceremony/cert_test.go similarity index 65% rename from cmd/gen-ca/main_test.go rename to cmd/ceremony/cert_test.go index ba0bdf443..25de08cae 100644 --- a/cmd/gen-ca/main_test.go +++ b/cmd/ceremony/cert_test.go @@ -7,6 +7,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/sha256" + "crypto/x509" "encoding/asn1" "errors" "math/big" @@ -65,74 +66,212 @@ func TestX509Signer(t *testing.T) { test.AssertEquals(t, signer.Public(), tk.Public()) } -func TestFindObject(t *testing.T) { +func TestParseOID(t *testing.T) { + _, err := parseOID("") + test.AssertError(t, err, "parseOID accepted an empty OID") + _, err = parseOID("a.b.c") + test.AssertError(t, err, "parseOID accepted an OID containing non-ints") + oid, err := parseOID("1.2.3") + test.AssertNotError(t, err, "parseOID failed with a valid OID") + test.Assert(t, oid.Equal(asn1.ObjectIdentifier{1, 2, 3}), "parseOID returned incorrect OID") +} + +func TestMakeTemplate(t *testing.T) { ctx := pkcs11helpers.MockCtx{} + profile := &certProfile{} + randReader := newRandReader(&ctx, 0) - // test findObject fails when FindObjectsInit fails - ctx.FindObjectsInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Attribute) error { - return errors.New("broken") - } - _, err := findObject(ctx, 0, nil) - test.AssertError(t, err, "findObject didn't fail when FindObjectsInit failed") + profile.NotBefore = "1234" + _, err := makeTemplate(randReader, profile, nil) + test.AssertError(t, err, "makeTemplate didn't fail with invalid not before") - // test findObject fails when FindObjects fails - ctx.FindObjectsInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Attribute) error { - return nil - } - ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { - return nil, false, errors.New("broken") - } - _, err = findObject(ctx, 0, nil) - test.AssertError(t, err, "findObject didn't fail when FindObjects failed") + profile.NotBefore = "2018-05-18 11:31:00" + profile.NotAfter = "1234" + _, err = makeTemplate(randReader, profile, nil) + test.AssertError(t, err, "makeTemplate didn't fail with invalid not after") - // test findObject fails when no handles are returned - ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { - return []pkcs11.ObjectHandle{}, false, nil - } - _, err = findObject(ctx, 0, nil) - test.AssertError(t, err, "findObject didn't fail when FindObjects returns no handles") + profile.NotAfter = "2018-05-18 11:31:00" + profile.PolicyOIDs = []string{""} + _, err = makeTemplate(randReader, profile, nil) + test.AssertError(t, err, "makeTemplate didn't fail with invalid policy OID") - // test findObject fails when multiple handles are returned - ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { - return []pkcs11.ObjectHandle{1}, true, nil - } - _, err = findObject(ctx, 0, nil) - test.AssertError(t, err, "findObject didn't fail when FindObjects returns multiple handles") + profile.PolicyOIDs = []string{"1.2.3"} + profile.SignatureAlgorithm = "nope" + _, err = makeTemplate(randReader, profile, nil) + test.AssertError(t, err, "makeTemplate didn't fail with invalid signature algorithm") - // test findObject fails when FindObjectsFinal fails - ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { - return []pkcs11.ObjectHandle{1}, false, nil + profile.SignatureAlgorithm = "SHA256WithRSA" + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return nil, errors.New("bad") } - ctx.FindObjectsFinalFunc = func(pkcs11.SessionHandle) error { - return errors.New("broken") - } - _, err = findObject(ctx, 0, nil) - test.AssertError(t, err, "findObject didn't fail when FindObjectsFinal fails") + _, err = makeTemplate(randReader, profile, nil) + test.AssertError(t, err, "makeTemplate didn't fail when GenerateRandom failed") - // test findObject works - ctx.FindObjectsFinalFunc = func(pkcs11.SessionHandle) error { - return nil + ctx.GenerateRandomFunc = func(_ pkcs11.SessionHandle, length int) ([]byte, error) { + r := make([]byte, length) + _, err := rand.Read(r) + return r, err + } + + _, err = makeTemplate(randReader, profile, nil) + test.AssertError(t, err, "makeTemplate didn't fail with empty key usages") + + profile.KeyUsages = []string{"asd"} + _, err = makeTemplate(randReader, profile, nil) + test.AssertError(t, err, "makeTemplate didn't fail with invalid key usages") + + profile.KeyUsages = []string{"Digital Signature", "CRL Sign"} + profile.CommonName = "common name" + profile.Organization = "organization" + profile.Country = "country" + profile.OCSPURL = "ocsp" + profile.CRLURL = "crl" + profile.IssuerURL = "issuer" + cert, err := makeTemplate(randReader, profile, nil) + test.AssertNotError(t, err, "makeTemplate failed when everything worked as expected") + test.AssertEquals(t, cert.Subject.CommonName, profile.CommonName) + test.AssertEquals(t, len(cert.Subject.Organization), 1) + test.AssertEquals(t, cert.Subject.Organization[0], profile.Organization) + test.AssertEquals(t, len(cert.Subject.Country), 1) + test.AssertEquals(t, cert.Subject.Country[0], profile.Country) + test.AssertEquals(t, len(cert.OCSPServer), 1) + test.AssertEquals(t, cert.OCSPServer[0], profile.OCSPURL) + test.AssertEquals(t, len(cert.CRLDistributionPoints), 1) + test.AssertEquals(t, cert.CRLDistributionPoints[0], profile.CRLURL) + test.AssertEquals(t, len(cert.IssuingCertificateURL), 1) + test.AssertEquals(t, cert.IssuingCertificateURL[0], profile.IssuerURL) + test.AssertEquals(t, cert.KeyUsage, x509.KeyUsageDigitalSignature|x509.KeyUsageCRLSign) +} + +func TestVerifyProfile(t *testing.T) { + for _, tc := range []struct { + profile certProfile + root bool + expectedErr string + }{ + { + profile: certProfile{}, + root: false, + expectedErr: "not-before is required", + }, + { + profile: certProfile{ + NotBefore: "a", + }, + root: false, + expectedErr: "not-after is required", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + }, + root: false, + expectedErr: "signature-algorithm is required", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + }, + root: false, + expectedErr: "common-name is required", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + }, + root: false, + expectedErr: "organization is required", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + }, + root: false, + expectedErr: "country is required", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + }, + root: false, + expectedErr: "ocsp-url is required for intermediates", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + OCSPURL: "g", + }, + root: false, + expectedErr: "crl-url is required for intermediates", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + OCSPURL: "g", + CRLURL: "h", + }, + root: false, + expectedErr: "issuer-url is required for intermediates", + }, + { + profile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + }, + root: true, + }, + } { + err := tc.profile.verifyProfile(tc.root) + if err != nil { + if tc.expectedErr != err.Error() { + t.Fatalf("Expected %q, got %q", tc.expectedErr, err.Error()) + } + } else if tc.expectedErr != "" { + t.Fatalf("verifyProfile didn't fail, expected %q", tc.expectedErr) + } } - handle, err := findObject(ctx, 0, nil) - test.AssertNotError(t, err, "findObject failed when everything worked as expected") - test.AssertEquals(t, handle, pkcs11.ObjectHandle(1)) } func TestGetKey(t *testing.T) { ctx := pkcs11helpers.MockCtx{} - // test getKey fails with invalid key ID - _, err := getKey(ctx, 0, "label", "not hex") - test.AssertError(t, err, "getKey didn't fail with invalid key ID") - - // test getKey fails when findObject for private key handle fails + // test newSigner fails when pkcs11helpers.FindObject for private key handle fails ctx.FindObjectsInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Attribute) error { return errors.New("broken") } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertError(t, err, "getKey didn't fail when findObject for private key handle failed") + _, err := newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertError(t, err, "newSigner didn't fail when pkcs11helpers.FindObject for private key handle failed") - // test getKey fails when GetAttributeValue fails + // test newSigner fails when GetAttributeValue fails ctx.FindObjectsInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Attribute) error { return nil } @@ -145,17 +284,17 @@ func TestGetKey(t *testing.T) { ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return nil, errors.New("broken") } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertError(t, err, "getKey didn't fail when GetAttributeValue for private key type failed") + _, err = newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertError(t, err, "newSigner didn't fail when GetAttributeValue for private key type failed") - // test getKey fails when GetAttributeValue returns no attributes + // test newSigner fails when GetAttributeValue returns no attributes ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return nil, nil } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertError(t, err, "getKey didn't fail when GetAttributeValue for private key type returned no attributes") + _, err = newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertError(t, err, "newSigner didn't fail when GetAttributeValue for private key type returned no attributes") - // test getKey fails when findObject for public key handle fails + // test newSigner fails when pkcs11helpers.FindObject for public key handle fails ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return []*pkcs11.Attribute{pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC)}, nil } @@ -165,34 +304,34 @@ func TestGetKey(t *testing.T) { } return nil } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertError(t, err, "getKey didn't fail when findObject for public key handle failed") + _, err = newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertError(t, err, "newSigner didn't fail when pkcs11helpers.FindObject for public key handle failed") - // test getKey fails when findObject for private key returns unknown CKA_KEY_TYPE + // test newSigner fails when pkcs11helpers.FindObject for private key returns unknown CKA_KEY_TYPE ctx.FindObjectsInitFunc = func(_ pkcs11.SessionHandle, tmpl []*pkcs11.Attribute) error { return nil } ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return []*pkcs11.Attribute{pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, []byte{2, 0, 0, 0, 0, 0, 0, 0})}, nil } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertError(t, err, "getKey didn't fail when GetAttributeValue for private key returned unknown key type") + _, err = newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertError(t, err, "newSigner didn't fail when GetAttributeValue for private key returned unknown key type") - // test getKey fails when GetRSAPublicKey fails + // test newSigner fails when GetRSAPublicKey fails ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return []*pkcs11.Attribute{pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, []byte{0, 0, 0, 0, 0, 0, 0, 0})}, nil } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertError(t, err, "getKey didn't fail when GetRSAPublicKey fails") + _, err = newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertError(t, err, "newSigner didn't fail when GetRSAPublicKey fails") - // test getKey fails when GetECDSAPublicKey fails + // test newSigner fails when GetECDSAPublicKey fails ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return []*pkcs11.Attribute{pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, []byte{3, 0, 0, 0, 0, 0, 0, 0})}, nil } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertError(t, err, "getKey didn't fail when GetECDSAPublicKey fails") + _, err = newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertError(t, err, "newSigner didn't fail when GetECDSAPublicKey fails") - // test getKey works when everything... works + // test newSigner works when everything... works ctx.GetAttributeValueFunc = func(_ pkcs11.SessionHandle, _ pkcs11.ObjectHandle, attrs []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { var returns []*pkcs11.Attribute for _, attr := range attrs { @@ -209,190 +348,6 @@ func TestGetKey(t *testing.T) { } return returns, nil } - _, err = getKey(ctx, 0, "label", "ffff") - test.AssertNotError(t, err, "getKey failed when everything worked properly") -} - -func TestParseOID(t *testing.T) { - _, err := parseOID("") - test.AssertError(t, err, "parseOID accepted an empty OID") - _, err = parseOID("a.b.c") - test.AssertError(t, err, "parseOID accepted an OID containing non-ints") - oid, err := parseOID("1.2.3") - test.AssertNotError(t, err, "parseOID failed with a valid OID") - test.Assert(t, oid.Equal(asn1.ObjectIdentifier{1, 2, 3}), "parseOID returned incorrect OID") -} - -func TestMakeTemplate(t *testing.T) { - ctx := pkcs11helpers.MockCtx{} - profile := &CertProfile{} - - profile.NotBefore = "1234" - _, err := makeTemplate(ctx, profile, nil, 0) - test.AssertError(t, err, "makeTemplate didn't fail with invalid not before") - - profile.NotBefore = "2018-05-18 11:31:00" - profile.NotAfter = "1234" - _, err = makeTemplate(ctx, profile, nil, 0) - test.AssertError(t, err, "makeTemplate didn't fail with invalid not after") - - profile.NotAfter = "2018-05-18 11:31:00" - profile.PolicyOIDs = []string{""} - _, err = makeTemplate(ctx, profile, nil, 0) - test.AssertError(t, err, "makeTemplate didn't fail with invalid policy OID") - - profile.PolicyOIDs = []string{"1.2.3"} - profile.SignatureAlgorithm = "nope" - _, err = makeTemplate(ctx, profile, nil, 0) - test.AssertError(t, err, "makeTemplate didn't fail with invalid signature algorithm") - - profile.SignatureAlgorithm = "SHA256WithRSA" - ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { - return nil, errors.New("bad") - } - _, err = makeTemplate(ctx, profile, nil, 0) - test.AssertError(t, err, "makeTemplate didn't fail when GenerateRandom failed") - - ctx.GenerateRandomFunc = func(_ pkcs11.SessionHandle, length int) ([]byte, error) { - r := make([]byte, length) - _, err := rand.Read(r) - return r, err - } - profile.CommonName = "common name" - profile.Organization = "organization" - profile.Country = "country" - profile.OCSPURL = "ocsp" - profile.CRLURL = "crl" - profile.IssuerURL = "issuer" - cert, err := makeTemplate(ctx, profile, nil, 0) - test.AssertNotError(t, err, "makeTemplate failed when everything worked as expected") - test.AssertEquals(t, cert.Subject.CommonName, profile.CommonName) - test.AssertEquals(t, len(cert.Subject.Organization), 1) - test.AssertEquals(t, cert.Subject.Organization[0], profile.Organization) - test.AssertEquals(t, len(cert.Subject.Country), 1) - test.AssertEquals(t, cert.Subject.Country[0], profile.Country) - test.AssertEquals(t, len(cert.OCSPServer), 1) - test.AssertEquals(t, cert.OCSPServer[0], profile.OCSPURL) - test.AssertEquals(t, len(cert.CRLDistributionPoints), 1) - test.AssertEquals(t, cert.CRLDistributionPoints[0], profile.CRLURL) - test.AssertEquals(t, len(cert.IssuingCertificateURL), 1) - test.AssertEquals(t, cert.IssuingCertificateURL[0], profile.IssuerURL) -} - -func TestVerifyProfile(t *testing.T) { - for _, tc := range []struct { - profile CertProfile - root bool - expectedErr string - }{ - { - profile: CertProfile{}, - root: false, - expectedErr: "NotBefore in profile is required", - }, - { - profile: CertProfile{ - NotBefore: "a", - }, - root: false, - expectedErr: "NotAfter in profile is required", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - }, - root: false, - expectedErr: "SignatureAlgorithm in profile is required", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - SignatureAlgorithm: "c", - }, - root: false, - expectedErr: "CommonName in profile is required", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - SignatureAlgorithm: "c", - CommonName: "d", - }, - root: false, - expectedErr: "Organization in profile is required", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - SignatureAlgorithm: "c", - CommonName: "d", - Organization: "e", - }, - root: false, - expectedErr: "Country in profile is required", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - SignatureAlgorithm: "c", - CommonName: "d", - Organization: "e", - Country: "f", - }, - root: false, - expectedErr: "OCSPURL in profile is required for intermediates", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - SignatureAlgorithm: "c", - CommonName: "d", - Organization: "e", - Country: "f", - OCSPURL: "g", - }, - root: false, - expectedErr: "CRLURL in profile is required for intermediates", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - SignatureAlgorithm: "c", - CommonName: "d", - Organization: "e", - Country: "f", - OCSPURL: "g", - CRLURL: "h", - }, - root: false, - expectedErr: "IssuerURL in profile is required for intermediates", - }, - { - profile: CertProfile{ - NotBefore: "a", - NotAfter: "b", - SignatureAlgorithm: "c", - CommonName: "d", - Organization: "e", - Country: "f", - }, - root: true, - }, - } { - err := verifyProfile(tc.profile, tc.root) - if err != nil { - if tc.expectedErr != err.Error() { - t.Fatalf("Expected %q, got %q", tc.expectedErr, err.Error()) - } - } else if tc.expectedErr != "" { - t.Fatalf("verifyProfile didn't fail, expected %q", tc.expectedErr) - } - } + _, err = newSigner(ctx, 0, "label", []byte{255, 255}) + test.AssertNotError(t, err, "newSigner failed when everything worked properly") } diff --git a/cmd/gen-key/ecdsa.go b/cmd/ceremony/ecdsa.go similarity index 74% rename from cmd/gen-key/ecdsa.go rename to cmd/ceremony/ecdsa.go index a248c0d49..13ee7df5f 100644 --- a/cmd/gen-key/ecdsa.go +++ b/cmd/ceremony/ecdsa.go @@ -4,7 +4,6 @@ import ( "crypto" "crypto/ecdsa" "crypto/elliptic" - "crypto/rand" "errors" "fmt" "log" @@ -14,11 +13,11 @@ import ( "github.com/miekg/pkcs11" ) -var stringToCurve = map[string]*elliptic.CurveParams{ - elliptic.P224().Params().Name: elliptic.P224().Params(), - elliptic.P256().Params().Name: elliptic.P256().Params(), - elliptic.P384().Params().Name: elliptic.P384().Params(), - elliptic.P521().Params().Name: elliptic.P521().Params(), +var stringToCurve = map[string]elliptic.Curve{ + elliptic.P224().Params().Name: elliptic.P224(), + elliptic.P256().Params().Name: elliptic.P256(), + elliptic.P384().Params().Name: elliptic.P384(), + elliptic.P521().Params().Name: elliptic.P521(), } // curveToOIDDER maps the name of the curves to their DER encoded OIDs @@ -29,20 +28,11 @@ var curveToOIDDER = map[string][]byte{ elliptic.P521().Params().Name: []byte{6, 5, 43, 129, 4, 0, 35}, } -// oidDERToCurve maps the hex of the DER encoding of the various curve OIDs to -// the relevant curve parameters -var oidDERToCurve = map[string]*elliptic.CurveParams{ - "06052B81040021": elliptic.P224().Params(), - "06082A8648CE3D030107": elliptic.P256().Params(), - "06052B81040022": elliptic.P384().Params(), - "06052B81040023": elliptic.P521().Params(), -} - -var curveToHash = map[*elliptic.CurveParams]crypto.Hash{ - elliptic.P224().Params(): crypto.SHA256, - elliptic.P256().Params(): crypto.SHA256, - elliptic.P384().Params(): crypto.SHA384, - elliptic.P521().Params(): crypto.SHA512, +var curveToHash = map[elliptic.Curve]crypto.Hash{ + elliptic.P224(): crypto.SHA256, + elliptic.P256(): crypto.SHA256, + elliptic.P384(): crypto.SHA384, + elliptic.P521(): crypto.SHA512, } var hashToString = map[crypto.Hash]string{ @@ -54,8 +44,8 @@ var hashToString = map[crypto.Hash]string{ // ecArgs constructs the private and public key template attributes sent to the // device and specifies which mechanism should be used. curve determines which // type of key should be generated. -func ecArgs(label string, curve *elliptic.CurveParams, keyID []byte) generateArgs { - encodedCurve := curveToOIDDER[curve.Name] +func ecArgs(label string, curve elliptic.Curve, keyID []byte) generateArgs { + encodedCurve := curveToOIDDER[curve.Params().Name] log.Printf("\tEncoded curve parameters for %s: %X\n", curve.Params().Name, encodedCurve) return generateArgs{ mechanism: []*pkcs11.Mechanism{ @@ -89,7 +79,7 @@ func ecPub( ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, object pkcs11.ObjectHandle, - expectedCurve *elliptic.CurveParams, + expectedCurve elliptic.Curve, ) (*ecdsa.PublicKey, error) { pubKey, err := pkcs11helpers.GetECDSAPublicKey(ctx, session, object) if err != nil { @@ -108,16 +98,17 @@ func ecPub( // a nonce generated on the device and verifying the returned signature using the // public key. func ecVerify(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, object pkcs11.ObjectHandle, pub *ecdsa.PublicKey) error { - nonce, err := getRandomBytes(ctx, session) + nonce := make([]byte, 4) + _, err := newRandReader(ctx, session).Read(nonce) if err != nil { return fmt.Errorf("failed to construct nonce: %s", err) } log.Printf("\tConstructed nonce: %d (%X)\n", big.NewInt(0).SetBytes(nonce), nonce) - hashFunc := curveToHash[pub.Curve.Params()].New() + hashFunc := curveToHash[pub.Curve].New() hashFunc.Write(nonce) digest := hashFunc.Sum(nil) - log.Printf("\tMessage %s hash: %X\n", hashToString[curveToHash[pub.Curve.Params()]], digest) - signature, err := pkcs11helpers.Sign(ctx, session, object, pkcs11helpers.ECDSAKey, digest, curveToHash[pub.Curve.Params()]) + log.Printf("\tMessage %s hash: %X\n", hashToString[curveToHash[pub.Curve]], digest) + signature, err := pkcs11helpers.Sign(ctx, session, object, pkcs11helpers.ECDSAKey, digest, curveToHash[pub.Curve]) if err != nil { return err } @@ -133,35 +124,36 @@ func ecVerify(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, object pkcs // ecGenerate is used to generate and verify a ECDSA key pair of the type // specified by curveStr and with the provided label. It returns the public -// part of the generated key pair as a ecdsa.PublicKey. -func ecGenerate(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label, curveStr string) (*ecdsa.PublicKey, error) { +// part of the generated key pair as a ecdsa.PublicKey and the random key ID +// that the HSM uses to identify the key pair. +func ecGenerate(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label, curveStr string) (*ecdsa.PublicKey, []byte, error) { curve, present := stringToCurve[curveStr] if !present { - return nil, fmt.Errorf("curve %q not supported", curveStr) + return nil, nil, fmt.Errorf("curve %q not supported", curveStr) } keyID := make([]byte, 4) - _, err := rand.Read(keyID) + _, err := newRandReader(ctx, session).Read(keyID) if err != nil { - return nil, err + return nil, nil, err } log.Printf("Generating ECDSA key with curve %s and ID %x\n", curveStr, keyID) args := ecArgs(label, curve, keyID) pub, priv, err := ctx.GenerateKeyPair(session, args.mechanism, args.publicAttrs, args.privateAttrs) if err != nil { - return nil, err + return nil, nil, err } log.Println("Key generated") log.Println("Extracting public key") pk, err := ecPub(ctx, session, pub, curve) if err != nil { - return nil, err + return nil, nil, err } log.Println("Extracted public key") log.Println("Verifying public key") err = ecVerify(ctx, session, priv, pk) if err != nil { - return nil, err + return nil, nil, err } log.Println("Key verified") - return pk, nil + return pk, keyID, nil } diff --git a/cmd/ceremony/ecdsa_test.go b/cmd/ceremony/ecdsa_test.go new file mode 100644 index 000000000..bb125154e --- /dev/null +++ b/cmd/ceremony/ecdsa_test.go @@ -0,0 +1,160 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "testing" + + "github.com/letsencrypt/boulder/pkcs11helpers" + "github.com/letsencrypt/boulder/test" + "github.com/miekg/pkcs11" +) + +func TestECPub(t *testing.T) { + ctx := pkcs11helpers.MockCtx{} + + // test we fail when pkcs11helpers.GetECDSAPublicKey fails + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return nil, errors.New("bad!") + } + _, err := ecPub(ctx, 0, 0, elliptic.P256()) + test.AssertError(t, err, "ecPub didn't fail with non-matching curve") + test.AssertEquals(t, err.Error(), "Failed to retrieve key attributes: bad!") + + // test we fail to construct key with non-matching curve + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 5, 43, 129, 4, 0, 33}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 217, 225, 246, 210, 153, 134, 246, 104, 95, 79, 122, 206, 135, 241, 37, 114, 199, 87, 56, 167, 83, 56, 136, 174, 6, 145, 97, 239, 221, 49, 67, 148, 13, 126, 65, 90, 208, 195, 193, 171, 105, 40, 98, 132, 124, 30, 189, 215, 197, 178, 226, 166, 238, 240, 57, 215}), + }, nil + } + _, err = ecPub(ctx, 0, 0, elliptic.P256()) + test.AssertError(t, err, "ecPub didn't fail with non-matching curve") +} + +func TestECVerify(t *testing.T) { + ctx := pkcs11helpers.MockCtx{} + + // test GenerateRandom failing + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return nil, errors.New("yup") + } + err := ecVerify(ctx, 0, 0, nil) + test.AssertError(t, err, "ecVerify didn't fail on GenerateRandom error") + + // test SignInit failing + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return []byte{1, 2, 3, 4}, nil + } + ctx.SignInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle) error { + return errors.New("yup") + } + err = ecVerify(ctx, 0, 0, &ecdsa.PublicKey{Curve: elliptic.P256()}) + test.AssertError(t, err, "ecVerify didn't fail on SignInit error") + + // test Sign failing + ctx.SignInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle) error { + return nil + } + ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { + return nil, errors.New("yup") + } + err = ecVerify(ctx, 0, 0, &ecdsa.PublicKey{Curve: elliptic.P256()}) + test.AssertError(t, err, "ecVerify didn't fail on Sign error") + + // test signature verification failing + ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { + return []byte{1, 2, 3}, nil + } + tk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "ecdsa.GenerateKey failed") + err = ecVerify(ctx, 0, 0, &tk.PublicKey) + test.AssertError(t, err, "ecVerify didn't fail on signature verification error") + + // test we don't fail with valid signature + ctx.SignFunc = func(_ pkcs11.SessionHandle, msg []byte) ([]byte, error) { + return ecPKCS11Sign(tk, msg) + } + err = ecVerify(ctx, 0, 0, &tk.PublicKey) + test.AssertNotError(t, err, "ecVerify failed with a valid signature") +} + +func TestECGenerate(t *testing.T) { + ctx := pkcs11helpers.MockCtx{} + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return []byte{1, 2, 3}, nil + } + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "Failed to generate a ECDSA test key") + + // Test ecGenerate fails with unknown curve + _, _, err = ecGenerate(ctx, 0, "", "bad-curve") + test.AssertError(t, err, "ecGenerate accepted unknown curve") + + // Test ecGenerate fails when GenerateKeyPair fails + ctx.GenerateKeyPairFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute, []*pkcs11.Attribute) (pkcs11.ObjectHandle, pkcs11.ObjectHandle, error) { + return 0, 0, errors.New("bad") + } + _, _, err = ecGenerate(ctx, 0, "", "P-256") + test.AssertError(t, err, "ecGenerate didn't fail on GenerateKeyPair error") + + // Test ecGenerate fails when ecPub fails + ctx.GenerateKeyPairFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute, []*pkcs11.Attribute) (pkcs11.ObjectHandle, pkcs11.ObjectHandle, error) { + return 0, 0, nil + } + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return nil, errors.New("bad") + } + _, _, err = ecGenerate(ctx, 0, "", "P-256") + test.AssertError(t, err, "ecGenerate didn't fail on ecPub error") + + // Test ecGenerate fails when ecVerify fails + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, elliptic.Marshal(elliptic.P256(), priv.X, priv.Y)), + }, nil + } + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return nil, errors.New("yup") + } + _, _, err = ecGenerate(ctx, 0, "", "P-256") + test.AssertError(t, err, "ecGenerate didn't fail on ecVerify error") + + // Test ecGenerate doesn't fail when everything works + ctx.SignInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle) error { + return nil + } + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return []byte{1, 2, 3}, nil + } + ctx.SignFunc = func(_ pkcs11.SessionHandle, msg []byte) ([]byte, error) { + return ecPKCS11Sign(priv, msg) + } + _, _, err = ecGenerate(ctx, 0, "", "P-256") + test.AssertNotError(t, err, "ecGenerate didn't succeed when everything worked as expected") +} + +func ecPKCS11Sign(priv *ecdsa.PrivateKey, msg []byte) ([]byte, error) { + r, s, err := ecdsa.Sign(rand.Reader, priv, msg[:]) + if err != nil { + return nil, err + } + rBytes := r.Bytes() + sBytes := s.Bytes() + // http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/os/pkcs11-curr-v2.40-os.html + // Section 2.3.1: EC Signatures + // "If r and s have different octet length, the shorter of both must be padded with + // leading zero octets such that both have the same octet length." + switch { + case len(rBytes) < len(sBytes): + padding := make([]byte, len(sBytes)-len(rBytes)) + rBytes = append(padding, rBytes...) + case len(rBytes) > len(sBytes): + padding := make([]byte, len(rBytes)-len(sBytes)) + sBytes = append(padding, sBytes...) + } + return append(rBytes, sBytes...), nil +} diff --git a/cmd/ceremony/key.go b/cmd/ceremony/key.go new file mode 100644 index 000000000..f2703bb64 --- /dev/null +++ b/cmd/ceremony/key.go @@ -0,0 +1,85 @@ +package main + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + + "github.com/letsencrypt/boulder/pkcs11helpers" + "github.com/miekg/pkcs11" +) + +type hsmRandReader struct { + ctx pkcs11helpers.PKCtx + session pkcs11.SessionHandle +} + +func newRandReader(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle) *hsmRandReader { + return &hsmRandReader{ + ctx: ctx, + session: session, + } +} + +func (hrr hsmRandReader) Read(p []byte) (n int, err error) { + r, err := hrr.ctx.GenerateRandom(hrr.session, len(p)) + if err != nil { + return 0, err + } + copy(p[:], r) + return len(r), nil +} + +type generateArgs struct { + mechanism []*pkcs11.Mechanism + privateAttrs []*pkcs11.Attribute + publicAttrs []*pkcs11.Attribute +} + +const ( + rsaExp = 65537 +) + +// keyInfo is a struct used to pass around information about the public key +// associated with the generated private key. der contains the DER encoding +// of the SubjectPublicKeyInfo structure for the public key. id contains the +// HSM key pair object ID. +type keyInfo struct { + key crypto.PublicKey + der []byte + id []byte +} + +func generateKey(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label string, outputPath string, config keyGenConfig) (*keyInfo, error) { + var pubKey crypto.PublicKey + var keyID []byte + var err error + switch config.Type { + case "rsa": + pubKey, keyID, err = rsaGenerate(ctx, session, label, config.RSAModLength, rsaExp) + if err != nil { + return nil, fmt.Errorf("failed to generate RSA key pair: %s", err) + } + case "ecdsa": + pubKey, keyID, err = ecGenerate(ctx, session, label, config.ECDSACurve) + if err != nil { + return nil, fmt.Errorf("failed to generate ECDSA key pair: %s", err) + } + } + + der, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return nil, fmt.Errorf("Failed to marshal public key: %s", err) + } + + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der}) + log.Printf("Public key PEM:\n%s\n", pemBytes) + if err := ioutil.WriteFile(outputPath, pemBytes, 0644); err != nil { + return nil, fmt.Errorf("Failed to write public key to %q: %s", outputPath, err) + } + log.Printf("Public key written to %q\n", outputPath) + return &keyInfo{key: pubKey, der: der, id: keyID}, nil +} diff --git a/cmd/ceremony/key_test.go b/cmd/ceremony/key_test.go new file mode 100644 index 000000000..19d5d7472 --- /dev/null +++ b/cmd/ceremony/key_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "math/big" + "os" + "path" + "testing" + + "github.com/letsencrypt/boulder/pkcs11helpers" + "github.com/letsencrypt/boulder/test" + "github.com/miekg/pkcs11" +) + +func TestGenerateKey(t *testing.T) { + tmp, err := ioutil.TempDir("", "ceremony-testing") + test.AssertNotError(t, err, "Failed to create temporary directory") + defer os.RemoveAll(tmp) + + // RSA + ctx := pkcs11helpers.MockCtx{} + rsaPriv, err := rsa.GenerateKey(rand.Reader, 1024) + test.AssertNotError(t, err, "Failed to generate a test RSA key") + ctx.GenerateKeyPairFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute, []*pkcs11.Attribute) (pkcs11.ObjectHandle, pkcs11.ObjectHandle, error) { + return 0, 0, nil + } + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, big.NewInt(int64(rsaPriv.E)).Bytes()), + pkcs11.NewAttribute(pkcs11.CKA_MODULUS, rsaPriv.N.Bytes()), + }, nil + } + ctx.SignInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle) error { + return nil + } + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return []byte{1, 2, 3}, nil + } + ctx.SignFunc = func(_ pkcs11.SessionHandle, msg []byte) ([]byte, error) { + // Chop of the hash identifier and feed back into rsa.SignPKCS1v15 + return rsa.SignPKCS1v15(rand.Reader, rsaPriv, crypto.SHA256, msg[19:]) + } + keyPath := path.Join(tmp, "test-rsa-key.pem") + keyInfo, err := generateKey(ctx, 0, "", keyPath, keyGenConfig{ + Type: "rsa", + RSAModLength: 1024, + }) + test.AssertNotError(t, err, "Failed to generate RSA key") + diskKeyBytes, err := ioutil.ReadFile(keyPath) + test.AssertNotError(t, err, "Failed to load key from disk") + block, _ := pem.Decode(diskKeyBytes) + diskKey, err := x509.ParsePKIXPublicKey(block.Bytes) + test.AssertNotError(t, err, "Failed to parse disk key") + test.AssertDeepEquals(t, diskKey, keyInfo.key) + + // EC + ecPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "Failed to generate a ECDSA test key") + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, elliptic.Marshal(elliptic.P256(), ecPriv.X, ecPriv.Y)), + }, nil + } + ctx.SignFunc = func(_ pkcs11.SessionHandle, msg []byte) ([]byte, error) { + return ecPKCS11Sign(ecPriv, msg) + } + keyPath = path.Join(tmp, "test-ecdsa-key.pem") + keyInfo, err = generateKey(ctx, 0, "", keyPath, keyGenConfig{ + Type: "ecdsa", + ECDSACurve: "P-256", + }) + test.AssertNotError(t, err, "Failed to generate ECDSA key") + diskKeyBytes, err = ioutil.ReadFile(keyPath) + test.AssertNotError(t, err, "Failed to load key from disk") + block, _ = pem.Decode(diskKeyBytes) + diskKey, err = x509.ParsePKIXPublicKey(block.Bytes) + test.AssertNotError(t, err, "Failed to parse disk key") + test.AssertDeepEquals(t, diskKey, keyInfo.key) +} diff --git a/cmd/ceremony/main.go b/cmd/ceremony/main.go new file mode 100644 index 000000000..0457c9707 --- /dev/null +++ b/cmd/ceremony/main.go @@ -0,0 +1,370 @@ +package main + +import ( + "crypto" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + + "github.com/letsencrypt/boulder/pkcs11helpers" + "gopkg.in/yaml.v2" +) + +type keyGenConfig struct { + Type string `yaml:"type"` + RSAModLength uint `yaml:"rsa-mod-length"` + ECDSACurve string `yaml:"ecdsa-curve"` +} + +var allowedCurves = map[string]bool{ + "P-224": true, + "P-256": true, + "P-384": true, + "P-521": true, +} + +func (kgc keyGenConfig) validate() error { + if kgc.Type == "" { + return errors.New("key.type is required") + } + if kgc.Type != "rsa" && kgc.Type != "ecdsa" { + return errors.New("key.type can only be 'rsa' or 'ecdsa'") + } + if kgc.Type == "rsa" && (kgc.RSAModLength != 2048 && kgc.RSAModLength != 4096) { + return errors.New("key.rsa-mod-length can only be 2048 or 4096") + } + if kgc.Type == "rsa" && kgc.ECDSACurve != "" { + return errors.New("if key.type = 'rsa' then key.ecdsa-curve is not used") + } + if kgc.Type == "ecdsa" && !allowedCurves[kgc.ECDSACurve] { + return errors.New("key.ecdsa-curve can only be 'P-224', 'P-256', 'P-384', or 'P-521'") + } + if kgc.Type == "ecdsa" && kgc.RSAModLength != 0 { + return errors.New("if key.type = 'ecdsa' then key.rsa-mod-length is not used") + } + + return nil +} + +type rootConfig struct { + CeremonyType string `yaml:"ceremony-type"` + PKCS11 struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + } `yaml:"pkcs11"` + Key keyGenConfig `yaml:"key"` + Outputs struct { + PublicKeyPath string `yaml:"public-key-path"` + CertificatePath string `yaml:"certificate-path"` + } `yaml:"outputs"` + CertProfile certProfile `yaml:"certificate-profile"` +} + +func (rc rootConfig) validate() error { + // PKCS11 fields + if rc.PKCS11.Module == "" { + return errors.New("pkcs11.module is required") + } + // key-slot cannot be tested because 0 is a valid slot + if rc.PKCS11.StoreLabel == "" { + return errors.New("pkcs11.store-key-with-label is required") + } + + // Key gen fields + if err := rc.Key.validate(); err != nil { + return err + } + + // Output fields + if rc.Outputs.PublicKeyPath == "" { + return errors.New("outputs.public-key-path is required") + } + if rc.Outputs.CertificatePath == "" { + return errors.New("outputs.certificate-path is required") + } + + // Certificate profile + if err := rc.CertProfile.verifyProfile(true); err != nil { + return err + } + + return nil +} + +type intermediateConfig struct { + CeremonyType string `yaml:"ceremony-type"` + PKCS11 struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + } `yaml:"pkcs11"` + Inputs struct { + PublicKeyPath string `yaml:"public-key-path"` + IssuerCertificatePath string `yaml:"issuer-certificate-path"` + } `yaml:"inputs"` + Outputs struct { + CertificatePath string `yaml:"certificate-path"` + } `yaml:"outputs"` + CertProfile certProfile `yaml:"certificate-profile"` +} + +func (ic intermediateConfig) validate() error { + // PKCS11 fields + if ic.PKCS11.Module == "" { + return errors.New("pkcs11.module is required") + } + // key-slot cannot be tested because 0 is a valid slot + if ic.PKCS11.SigningLabel == "" { + return errors.New("pkcs11.signing-key-label is required") + } + if ic.PKCS11.SigningKeyID == "" { + return errors.New("pkcs11.signing-key-id is required") + } + + // Input fields + if ic.Inputs.PublicKeyPath == "" { + return errors.New("inputs.public-key-path is required") + } + if ic.Inputs.IssuerCertificatePath == "" { + return errors.New("inputs.issuer-certificate is required") + } + + // Output fields + if ic.Outputs.CertificatePath == "" { + return errors.New("outputs.certificate-path is required") + } + + // Certificate profile + if err := ic.CertProfile.verifyProfile(false); err != nil { + return err + } + + return nil +} + +type keyConfig struct { + CeremonyType string `yaml:"ceremony-type"` + PKCS11 struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + } `yaml:"pkcs11"` + Key keyGenConfig `yaml:"key"` + Outputs struct { + PublicKeyPath string `yaml:"public-key-path"` + } `yaml:"outputs"` +} + +func (kc keyConfig) validate() error { + // PKCS11 fields + if kc.PKCS11.Module == "" { + return errors.New("pkcs11.module is required") + } + // key-slot cannot be tested because 0 is a valid slot + if kc.PKCS11.StoreLabel == "" { + return errors.New("pkcs11.store-key-with-label is required") + } + + // Key gen fields + if err := kc.Key.validate(); err != nil { + return err + } + + // Output fields + if kc.Outputs.PublicKeyPath == "" { + return errors.New("outputs.public-key-path is required") + } + + return nil +} + +func signAndWriteCert(tbs, issuer *x509.Certificate, subjectPubKey crypto.PublicKey, signer crypto.Signer, certPath string) error { + // x509.CreateCertificate uses a io.Reader here for signing methods that require + // a source of randomness. Since PKCS#11 based signing generates needed randomness + // at the HSM we don't need to pass a real reader. Instead of passing a nil reader + // we use one that always returns errors in case the internal usage of this reader + // changes. + certBytes, err := x509.CreateCertificate(&failReader{}, tbs, issuer, subjectPubKey, signer) + if err != nil { + return fmt.Errorf("failed to create certificate: %s", err) + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) + log.Printf("Signed certificate PEM:\n%s", pemBytes) + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return fmt.Errorf("failed to parse signed certificate: %s", err) + } + if tbs == issuer { + // If cert is self-signed we need to populate the issuer subject key to + // verify the signature + issuer.PublicKey = cert.PublicKey + issuer.PublicKeyAlgorithm = cert.PublicKeyAlgorithm + } + + if err := cert.CheckSignatureFrom(issuer); err != nil { + return fmt.Errorf("failed to verify certificate signature: %s", err) + } + log.Printf("Certificate PEM:\n%s", pemBytes) + if err := ioutil.WriteFile(certPath, pemBytes, 0644); err != nil { + return fmt.Errorf("failed to write certificate to %q: %s", certPath, err) + } + log.Printf("Certificate written to %q\n", certPath) + return nil +} + +func rootCeremony(configBytes []byte) error { + var config rootConfig + err := yaml.UnmarshalStrict(configBytes, &config) + if err != nil { + return fmt.Errorf("failed to parse config: %s", err) + } + ctx, session, err := pkcs11helpers.Initialize(config.PKCS11.Module, config.PKCS11.StoreSlot, config.PKCS11.PIN) + if err != nil { + return fmt.Errorf("failed to setup session and PKCS#11 context for slot %d: %s", config.PKCS11.StoreSlot, err) + } + log.Printf("Opened PKCS#11 session for slot %d\n", config.PKCS11.StoreSlot) + keyInfo, err := generateKey(ctx, session, config.PKCS11.StoreLabel, config.Outputs.PublicKeyPath, config.Key) + if err != nil { + return err + } + signer, err := newSigner(ctx, session, config.PKCS11.StoreLabel, keyInfo.id) + if err != nil { + return fmt.Errorf("failed to retrieve signer: %s", err) + } + template, err := makeTemplate(newRandReader(ctx, session), &config.CertProfile, keyInfo.der) + if err != nil { + return fmt.Errorf("failed to create certificate profile: %s", err) + } + + err = signAndWriteCert(template, template, keyInfo.key, signer, config.Outputs.CertificatePath) + if err != nil { + return err + } + + return nil +} + +func intermediateCeremony(configBytes []byte) error { + var config intermediateConfig + err := yaml.UnmarshalStrict(configBytes, &config) + if err != nil { + return fmt.Errorf("failed to parse config: %s", err) + } + ctx, session, err := pkcs11helpers.Initialize(config.PKCS11.Module, config.PKCS11.SigningSlot, config.PKCS11.PIN) + if err != nil { + return fmt.Errorf("failed to setup session and PKCS#11 context for slot %d: %s", config.PKCS11.SigningSlot, err) + } + log.Printf("Opened PKCS#11 session for slot %d\n", config.PKCS11.SigningSlot) + keyID, err := hex.DecodeString(config.PKCS11.SigningKeyID) + if err != nil { + return fmt.Errorf("failed to decode key-id: %s", err) + } + signer, err := newSigner(ctx, session, config.PKCS11.SigningLabel, keyID) + if err != nil { + return fmt.Errorf("failed to retrieve private key handle: %s", err) + } + log.Println("Retrieved private key handle") + + pubPEMBytes, err := ioutil.ReadFile(config.Inputs.PublicKeyPath) + if err != nil { + return fmt.Errorf("failed to read public key %q: %s", config.Inputs.PublicKeyPath, err) + } + pubPEM, _ := pem.Decode(pubPEMBytes) + if pubPEM == nil { + return fmt.Errorf("failed to parse public key") + } + pub, err := x509.ParsePKIXPublicKey(pubPEM.Bytes) + if err != nil { + return fmt.Errorf("failed to parse public key: %s", err) + } + issuerPEMBytes, err := ioutil.ReadFile(config.Inputs.IssuerCertificatePath) + if err != nil { + return fmt.Errorf("failed to read issuer certificate %q: %s", config.Inputs.IssuerCertificatePath, err) + } + issuerPEM, _ := pem.Decode(issuerPEMBytes) + if issuerPEM == nil { + return fmt.Errorf("failed to parse issuer certificate PEM") + } + issuer, err := x509.ParseCertificate(issuerPEM.Bytes) + if err != nil { + return fmt.Errorf("failed to parse issuer certificate: %s", err) + } + template, err := makeTemplate(newRandReader(ctx, session), &config.CertProfile, pubPEM.Bytes) + if err != nil { + return fmt.Errorf("failed to create certificate profile: %s", err) + } + template.AuthorityKeyId = issuer.SubjectKeyId + + err = signAndWriteCert(template, issuer, pub, signer, config.Outputs.CertificatePath) + if err != nil { + return err + } + + return nil +} + +func keyCeremony(configBytes []byte) error { + var config keyConfig + err := yaml.UnmarshalStrict(configBytes, &config) + if err != nil { + return fmt.Errorf("failed to parse config: %s", err) + } + ctx, session, err := pkcs11helpers.Initialize(config.PKCS11.Module, config.PKCS11.StoreSlot, config.PKCS11.PIN) + if err != nil { + return fmt.Errorf("failed to setup session and PKCS#11 context for slot %d: %s", config.PKCS11.StoreSlot, err) + } + log.Printf("Opened PKCS#11 session for slot %d\n", config.PKCS11.StoreSlot) + if _, err = generateKey(ctx, session, config.PKCS11.StoreLabel, config.Outputs.PublicKeyPath, config.Key); err != nil { + return err + } + + return nil +} + +func main() { + configPath := flag.String("config", "", "Path to ceremony configuration file") + flag.Parse() + + if *configPath == "" { + log.Fatal("--config is required") + } + configBytes, err := ioutil.ReadFile(*configPath) + if err != nil { + log.Fatalf("Failed to read config file: %s", err) + } + var ct struct { + CeremonyType string `yaml:"ceremony-type"` + } + err = yaml.Unmarshal(configBytes, &ct) + if err != nil { + log.Fatalf("Failed to parse config: %s", err) + } + + switch ct.CeremonyType { + case "root": + err = rootCeremony(configBytes) + if err != nil { + log.Fatalf("root ceremony failed: %s", err) + } + case "intermediate": + err = intermediateCeremony(configBytes) + if err != nil { + log.Fatalf("intermediate ceremony failed: %s", err) + } + case "key": + err = keyCeremony(configBytes) + if err != nil { + log.Fatalf("key ceremony failed: %s", err) + } + } +} diff --git a/cmd/ceremony/main_test.go b/cmd/ceremony/main_test.go new file mode 100644 index 000000000..1da198c6a --- /dev/null +++ b/cmd/ceremony/main_test.go @@ -0,0 +1,520 @@ +package main + +import "testing" + +func TestKeyGenConfigValidate(t *testing.T) { + cases := []struct { + name string + config keyGenConfig + expectedError string + }{ + { + name: "no key.type", + config: keyGenConfig{}, + expectedError: "key.type is required", + }, + { + name: "bad key.type", + config: keyGenConfig{ + Type: "doop", + }, + expectedError: "key.type can only be 'rsa' or 'ecdsa'", + }, + { + name: "bad key.rsa-mod-length", + config: keyGenConfig{ + Type: "rsa", + RSAModLength: 1337, + }, + expectedError: "key.rsa-mod-length can only be 2048 or 4096", + }, + { + name: "key.type is rsa but key.ecdsa-curve is present", + config: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + ECDSACurve: "bad", + }, + expectedError: "if key.type = 'rsa' then key.ecdsa-curve is not used", + }, + { + name: "bad key.ecdsa-curve", + config: keyGenConfig{ + Type: "ecdsa", + ECDSACurve: "bad", + }, + expectedError: "key.ecdsa-curve can only be 'P-224', 'P-256', 'P-384', or 'P-521'", + }, + { + name: "key.type is ecdsa but key.rsa-mod-length is present", + config: keyGenConfig{ + Type: "ecdsa", + RSAModLength: 2048, + ECDSACurve: "P-256", + }, + expectedError: "if key.type = 'ecdsa' then key.rsa-mod-length is not used", + }, + { + name: "good rsa config", + config: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + }, + }, + { + name: "good ecdsa config", + config: keyGenConfig{ + Type: "ecdsa", + ECDSACurve: "P-256", + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.config.validate() + if err != nil && err.Error() != tc.expectedError { + t.Fatalf("Unexpected error, wanted: %q, got: %q", tc.expectedError, err) + } else if err == nil && tc.expectedError != "" { + t.Fatalf("validate didn't fail, wanted: %q", err) + } + }) + } +} + +func TestRootConfigValidate(t *testing.T) { + cases := []struct { + name string + config rootConfig + expectedError string + }{ + { + name: "no pkcs11.module", + config: rootConfig{}, + expectedError: "pkcs11.module is required", + }, + { + name: "no pkcs11.store-key-with-label", + config: rootConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + }, + }, + expectedError: "pkcs11.store-key-with-label is required", + }, + { + name: "bad key fields", + config: rootConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + }, + expectedError: "key.type is required", + }, + { + name: "no outputs.public-key-path", + config: rootConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + Key: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + }, + }, + expectedError: "outputs.public-key-path is required", + }, + { + name: "no outputs.certificate-path", + config: rootConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + Key: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + }, + Outputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + CertificatePath string `yaml:"certificate-path"` + }{ + PublicKeyPath: "path", + }, + }, + expectedError: "outputs.certificate-path is required", + }, + { + name: "bad certificate-profile", + config: rootConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + Key: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + }, + Outputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + CertificatePath string `yaml:"certificate-path"` + }{ + PublicKeyPath: "path", + CertificatePath: "path", + }, + }, + expectedError: "not-before is required", + }, + { + name: "good config", + config: rootConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + Key: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + }, + Outputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + CertificatePath string `yaml:"certificate-path"` + }{ + PublicKeyPath: "path", + CertificatePath: "path", + }, + CertProfile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.config.validate() + if err != nil && err.Error() != tc.expectedError { + t.Fatalf("Unexpected error, wanted: %q, got: %q", tc.expectedError, err) + } else if err == nil && tc.expectedError != "" { + t.Fatalf("validate didn't fail, wanted: %q", err) + } + }) + } +} + +func TestIntermediateConfigValidate(t *testing.T) { + cases := []struct { + name string + config intermediateConfig + expectedError string + }{ + { + name: "no pkcs11.module", + config: intermediateConfig{}, + expectedError: "pkcs11.module is required", + }, + { + name: "no pkcs11.signing-key-label", + config: intermediateConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + }{ + Module: "module", + }, + }, + expectedError: "pkcs11.signing-key-label is required", + }, + { + name: "no pkcs11.key-id", + config: intermediateConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + }{ + Module: "module", + SigningLabel: "label", + }, + }, + expectedError: "pkcs11.signing-key-id is required", + }, + { + name: "no inputs.public-key-path", + config: intermediateConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + }{ + Module: "module", + SigningLabel: "label", + SigningKeyID: "id", + }, + }, + expectedError: "inputs.public-key-path is required", + }, + { + name: "no inputs.issuer-certificate-path", + config: intermediateConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + }{ + Module: "module", + SigningLabel: "label", + SigningKeyID: "id", + }, + Inputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + IssuerCertificatePath string `yaml:"issuer-certificate-path"` + }{ + PublicKeyPath: "path", + }, + }, + expectedError: "inputs.issuer-certificate is required", + }, + { + name: "no outputs.certificate-path", + config: intermediateConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + }{ + Module: "module", + SigningLabel: "label", + SigningKeyID: "id", + }, + Inputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + IssuerCertificatePath string `yaml:"issuer-certificate-path"` + }{ + PublicKeyPath: "path", + IssuerCertificatePath: "path", + }, + }, + expectedError: "outputs.certificate-path is required", + }, + { + name: "bad certificate-profile", + config: intermediateConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + }{ + Module: "module", + SigningLabel: "label", + SigningKeyID: "id", + }, + Inputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + IssuerCertificatePath string `yaml:"issuer-certificate-path"` + }{ + PublicKeyPath: "path", + IssuerCertificatePath: "path", + }, + Outputs: struct { + CertificatePath string `yaml:"certificate-path"` + }{ + CertificatePath: "path", + }, + }, + expectedError: "not-before is required", + }, + { + name: "good config", + config: intermediateConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + SigningSlot uint `yaml:"signing-key-slot"` + SigningLabel string `yaml:"signing-key-label"` + SigningKeyID string `yaml:"signing-key-id"` + }{ + Module: "module", + SigningLabel: "label", + SigningKeyID: "id", + }, + Inputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + IssuerCertificatePath string `yaml:"issuer-certificate-path"` + }{ + PublicKeyPath: "path", + IssuerCertificatePath: "path", + }, + Outputs: struct { + CertificatePath string `yaml:"certificate-path"` + }{ + CertificatePath: "path", + }, + CertProfile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + OCSPURL: "g", + CRLURL: "h", + IssuerURL: "i", + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.config.validate() + if err != nil && err.Error() != tc.expectedError { + t.Fatalf("Unexpected error, wanted: %q, got: %q", tc.expectedError, err) + } else if err == nil && tc.expectedError != "" { + t.Fatalf("validate didn't fail, wanted: %q", err) + } + }) + } +} + +func TestKeyConfigValidate(t *testing.T) { + cases := []struct { + name string + config keyConfig + expectedError string + }{ + { + name: "no pkcs11.module", + config: keyConfig{}, + expectedError: "pkcs11.module is required", + }, + { + name: "no pkcs11.store-key-with-label", + config: keyConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + }, + }, + expectedError: "pkcs11.store-key-with-label is required", + }, + { + name: "bad key fields", + config: keyConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + }, + expectedError: "key.type is required", + }, + { + name: "no outputs.public-key-path", + config: keyConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + Key: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + }, + }, + expectedError: "outputs.public-key-path is required", + }, + { + name: "good config", + config: keyConfig{ + PKCS11: struct { + Module string `yaml:"module"` + PIN string `yaml:"pin"` + StoreSlot uint `yaml:"store-key-in-slot"` + StoreLabel string `yaml:"store-key-with-label"` + }{ + Module: "module", + StoreLabel: "label", + }, + Key: keyGenConfig{ + Type: "rsa", + RSAModLength: 2048, + }, + Outputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + }{ + PublicKeyPath: "path", + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.config.validate() + if err != nil && err.Error() != tc.expectedError { + t.Fatalf("Unexpected error, wanted: %q, got: %q", tc.expectedError, err) + } else if err == nil && tc.expectedError != "" { + t.Fatalf("validate didn't fail, wanted: %q", err) + } + }) + } +} diff --git a/cmd/gen-key/rsa.go b/cmd/ceremony/rsa.go similarity index 93% rename from cmd/gen-key/rsa.go rename to cmd/ceremony/rsa.go index f78339d67..4baa8c8df 100644 --- a/cmd/gen-key/rsa.go +++ b/cmd/ceremony/rsa.go @@ -2,7 +2,6 @@ package main import ( "crypto" - "crypto/rand" "crypto/rsa" "crypto/sha256" "errors" @@ -76,7 +75,8 @@ func rsaPub(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, object pkcs11 // a nonce generated on the device and verifying the returned signature using the // public key. func rsaVerify(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, object pkcs11.ObjectHandle, pub *rsa.PublicKey) error { - nonce, err := getRandomBytes(ctx, session) + nonce := make([]byte, 4) + _, err := newRandReader(ctx, session).Read(nonce) if err != nil { return fmt.Errorf("Failed to retrieve nonce: %s", err) } @@ -98,30 +98,31 @@ func rsaVerify(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, object pkc // rsaGenerate is used to generate and verify a RSA key pair of the size // specified by modulusLen and with the exponent specified by pubExponent. -// It returns the public part of the generated key pair as a rsa.PublicKey. -func rsaGenerate(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label string, modulusLen, pubExponent uint) (*rsa.PublicKey, error) { +// It returns the public part of the generated key pair as a rsa.PublicKey +// and the random key ID that the HSM uses to identify the key pair. +func rsaGenerate(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle, label string, modulusLen, pubExponent uint) (*rsa.PublicKey, []byte, error) { keyID := make([]byte, 4) - _, err := rand.Read(keyID) + _, err := newRandReader(ctx, session).Read(keyID) if err != nil { - return nil, err + return nil, nil, err } log.Printf("Generating RSA key with %d bit modulus and public exponent %d and ID %x\n", modulusLen, pubExponent, keyID) args := rsaArgs(label, modulusLen, pubExponent, keyID) pub, priv, err := ctx.GenerateKeyPair(session, args.mechanism, args.publicAttrs, args.privateAttrs) if err != nil { - return nil, err + return nil, nil, err } log.Println("Key generated") log.Println("Extracting public key") pk, err := rsaPub(ctx, session, pub, modulusLen, pubExponent) if err != nil { - return nil, err + return nil, nil, err } log.Println("Extracted public key") log.Println("Verifying public key") err = rsaVerify(ctx, session, priv, pk) if err != nil { - return nil, err + return nil, nil, err } - return pk, nil + return pk, keyID, nil } diff --git a/cmd/gen-key/rsa_test.go b/cmd/ceremony/rsa_test.go similarity index 65% rename from cmd/gen-key/rsa_test.go rename to cmd/ceremony/rsa_test.go index 009128eeb..179b31c92 100644 --- a/cmd/gen-key/rsa_test.go +++ b/cmd/ceremony/rsa_test.go @@ -4,8 +4,8 @@ import ( "crypto" "crypto/rand" "crypto/rsa" - "crypto/sha256" "errors" + "math/big" "testing" "github.com/letsencrypt/boulder/pkcs11helpers" @@ -16,30 +16,17 @@ import ( func TestRSAPub(t *testing.T) { ctx := pkcs11helpers.MockCtx{} - // test attribute retrieval failing - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return nil, errors.New("yup") - } - _, err := rsaPub(ctx, 0, 0, 0, 0) - test.AssertError(t, err, "rsaPub didn't fail on GetAttributeValue error") - - // test we fail to construct key with missing modulus and exp - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{}, nil - } - _, err = rsaPub(ctx, 0, 0, 0, 0) - test.AssertError(t, err, "rsaPub didn't fail with empty attribute list") - // test we fail to construct key with non-matching exp ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, []byte{1, 0, 1}), + pkcs11.NewAttribute(pkcs11.CKA_MODULUS, []byte{255}), }, nil } - _, err = rsaPub(ctx, 0, 0, 0, 0) + _, err := rsaPub(ctx, 0, 0, 0, 255) test.AssertError(t, err, "rsaPub didn't fail with non-matching exp") - // test we fail to construct key with non-matching exp + // test we fail to construct key with non-matching modulus ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return []*pkcs11.Attribute{ pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, []byte{1, 0, 1}), @@ -85,7 +72,7 @@ func TestRSAVerify(t *testing.T) { return nil } ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { - return []byte{1, 2, 3}, nil + return []byte{1, 2, 3, 4}, nil } ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { return nil, errors.New("yup") @@ -103,9 +90,9 @@ func TestRSAVerify(t *testing.T) { test.AssertError(t, err, "rsaVerify didn't fail on signature verification error") // test we don't fail with valid signature - ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { - hash := sha256.Sum256([]byte{1, 2, 3}) - return rsa.SignPKCS1v15(rand.Reader, tk, crypto.SHA256, hash[:]) + ctx.SignFunc = func(_ pkcs11.SessionHandle, msg []byte) ([]byte, error) { + // Chop of the hash identifier and feed back into rsa.SignPKCS1v15 + return rsa.SignPKCS1v15(rand.Reader, tk, crypto.SHA256, msg[19:]) } err = rsaVerify(ctx, 0, 0, &tk.PublicKey) test.AssertNotError(t, err, "rsaVerify failed with a valid signature") @@ -113,12 +100,18 @@ func TestRSAVerify(t *testing.T) { func TestRSAGenerate(t *testing.T) { ctx := pkcs11helpers.MockCtx{} + ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { + return []byte{1, 2, 3}, nil + } + + priv, err := rsa.GenerateKey(rand.Reader, 1024) + test.AssertNotError(t, err, "Failed to generate a RSA test key") // Test rsaGenerate fails when GenerateKeyPair fails ctx.GenerateKeyPairFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute, []*pkcs11.Attribute) (pkcs11.ObjectHandle, pkcs11.ObjectHandle, error) { return 0, 0, errors.New("bad") } - _, err := rsaGenerate(ctx, 0, "", 1024, 65537) + _, _, err = rsaGenerate(ctx, 0, "", 1024, 65537) test.AssertError(t, err, "rsaGenerate didn't fail on GenerateKeyPair error") // Test rsaGenerate fails when rsaPub fails @@ -128,20 +121,20 @@ func TestRSAGenerate(t *testing.T) { ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return nil, errors.New("bad") } - _, err = rsaGenerate(ctx, 0, "", 1024, 65537) + _, _, err = rsaGenerate(ctx, 0, "", 1024, 65537) test.AssertError(t, err, "rsaGenerate didn't fail on rsaPub error") // Test rsaGenerate fails when rsaVerify fails ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, []byte{1, 0, 1}), - pkcs11.NewAttribute(pkcs11.CKA_MODULUS, []byte{217, 226, 207, 73, 127, 217, 136, 48, 203, 2, 12, 223, 251, 130, 143, 118, 13, 186, 82, 183, 220, 178, 158, 204, 19, 255, 121, 75, 243, 84, 118, 40, 128, 29, 11, 245, 43, 246, 217, 244, 166, 208, 36, 59, 69, 34, 142, 40, 22, 230, 195, 193, 111, 202, 186, 174, 233, 175, 140, 74, 19, 135, 191, 82, 27, 41, 123, 157, 174, 219, 38, 71, 19, 138, 28, 41, 48, 52, 142, 234, 196, 242, 51, 90, 204, 10, 235, 88, 150, 156, 89, 156, 199, 152, 173, 251, 88, 67, 138, 147, 86, 190, 236, 107, 190, 169, 53, 160, 219, 71, 147, 247, 230, 24, 188, 44, 61, 92, 106, 254, 125, 145, 233, 211, 76, 13, 159, 167}), + pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, big.NewInt(int64(priv.E)).Bytes()), + pkcs11.NewAttribute(pkcs11.CKA_MODULUS, priv.N.Bytes()), }, nil } ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { return nil, errors.New("yup") } - _, err = rsaGenerate(ctx, 0, "", 1024, 65537) + _, _, err = rsaGenerate(ctx, 0, "", 1024, 65537) test.AssertError(t, err, "rsaGenerate didn't fail on rsaVerify error") // Test rsaGenerate doesn't fail when everything works @@ -151,9 +144,10 @@ func TestRSAGenerate(t *testing.T) { ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { return []byte{1, 2, 3}, nil } - ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { - return []byte{182, 42, 17, 237, 215, 151, 23, 254, 234, 219, 10, 119, 178, 76, 204, 254, 235, 67, 135, 83, 97, 134, 117, 38, 68, 115, 190, 250, 69, 200, 138, 225, 5, 188, 175, 45, 32, 179, 239, 145, 13, 168, 119, 75, 11, 171, 161, 220, 39, 185, 249, 87, 226, 132, 237, 82, 246, 187, 26, 232, 69, 86, 29, 12, 233, 8, 252, 59, 24, 194, 173, 74, 191, 101, 249, 108, 195, 240, 100, 28, 241, 70, 78, 236, 9, 136, 130, 218, 245, 195, 128, 80, 253, 42, 82, 99, 200, 115, 14, 75, 218, 176, 94, 98, 7, 226, 110, 24, 187, 108, 42, 144, 238, 244, 114, 153, 125, 3, 248, 129, 159, 51, 91, 26, 177, 118, 250, 79}, nil + ctx.SignFunc = func(_ pkcs11.SessionHandle, msg []byte) ([]byte, error) { + // Chop of the hash identifier and feed back into rsa.SignPKCS1v15 + return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, msg[19:]) } - _, err = rsaGenerate(ctx, 0, "", 1024, 65537) + _, _, err = rsaGenerate(ctx, 0, "", 1024, 65537) test.AssertNotError(t, err, "rsaGenerate didn't succeed when everything worked as expected") } diff --git a/cmd/gen-key/ecdsa_test.go b/cmd/gen-key/ecdsa_test.go deleted file mode 100644 index aacef74b4..000000000 --- a/cmd/gen-key/ecdsa_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package main - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "errors" - "testing" - - "github.com/letsencrypt/boulder/pkcs11helpers" - "github.com/letsencrypt/boulder/test" - "github.com/miekg/pkcs11" -) - -func TestECPub(t *testing.T) { - ctx := pkcs11helpers.MockCtx{} - - // test attribute retrieval failing - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return nil, errors.New("yup") - } - _, err := ecPub(ctx, 0, 0, nil) - test.AssertError(t, err, "ecPub didn't fail on GetAttributeValue error") - - // test we fail to construct key with missing params and point - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{}, nil - } - _, err = ecPub(ctx, 0, 0, nil) - test.AssertError(t, err, "ecPub didn't fail with empty attribute list") - - // test we fail to construct key with unknown curve - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{1, 2, 3}), - }, nil - } - _, err = ecPub(ctx, 0, 0, elliptic.P224().Params()) - test.AssertError(t, err, "ecPub didn't fail with unknown curve") - - // test we fail to construct key with non-matching curve - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), - }, nil - } - _, err = ecPub(ctx, 0, 0, elliptic.P224().Params()) - test.AssertError(t, err, "ecPub didn't fail with non-matching curve") - - // test we fail to construct key with invalid EC point (invalid encoding) - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{255}), - }, nil - } - _, err = ecPub(ctx, 0, 0, elliptic.P256().Params()) - test.AssertError(t, err, "ecPub didn't fail with invalid EC point (invalid encoding)") - - // test we fail to construct key with invalid EC point (empty octet string) - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 0}), - }, nil - } - _, err = ecPub(ctx, 0, 0, elliptic.P256().Params()) - test.AssertError(t, err, "ecPub didn't fail with invalid EC point (octet string, invalid contents)") - // test we fail to construct key with invalid EC point (empty octet string) - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 4, 4, 1, 2, 3}), - }, nil - } - _, err = ecPub(ctx, 0, 0, elliptic.P256().Params()) - test.AssertError(t, err, "ecPub didn't fail with invalid EC point (empty octet string)") - - // test we don't fail with the correct attributes (traditional encoding) - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 5, 43, 129, 4, 0, 33}), - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 217, 225, 246, 210, 153, 134, 246, 104, 95, 79, 122, 206, 135, 241, 37, 114, 199, 87, 56, 167, 83, 56, 136, 174, 6, 145, 97, 239, 221, 49, 67, 148, 13, 126, 65, 90, 208, 195, 193, 171, 105, 40, 98, 132, 124, 30, 189, 215, 197, 178, 226, 166, 238, 240, 57, 215}), - }, nil - } - _, err = ecPub(ctx, 0, 0, elliptic.P224().Params()) - test.AssertNotError(t, err, "ecPub failed with valid attributes (traditional encoding)") - - // test we don't fail with the correct attributes (non-traditional encoding) - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 5, 43, 129, 4, 0, 33}), - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 57, 4, 217, 225, 246, 210, 153, 134, 246, 104, 95, 79, 122, 206, 135, 241, 37, 114, 199, 87, 56, 167, 83, 56, 136, 174, 6, 145, 97, 239, 221, 49, 67, 148, 13, 126, 65, 90, 208, 195, 193, 171, 105, 40, 98, 132, 124, 30, 189, 215, 197, 178, 226, 166, 238, 240, 57, 215}), - }, nil - } - _, err = ecPub(ctx, 0, 0, elliptic.P224().Params()) - test.AssertNotError(t, err, "ecPub failed with valid attributes (non-traditional encoding)") -} - -func TestECVerify(t *testing.T) { - ctx := pkcs11helpers.MockCtx{} - - // test GenerateRandom failing - ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { - return nil, errors.New("yup") - } - err := ecVerify(ctx, 0, 0, nil) - test.AssertError(t, err, "ecVerify didn't fail on GenerateRandom error") - - // test SignInit failing - ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { - return []byte{1, 2, 3}, nil - } - ctx.SignInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle) error { - return errors.New("yup") - } - err = ecVerify(ctx, 0, 0, &ecdsa.PublicKey{Curve: elliptic.P256().Params()}) - test.AssertError(t, err, "ecVerify didn't fail on SignInit error") - - // test Sign failing - ctx.SignInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle) error { - return nil - } - ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { - return nil, errors.New("yup") - } - err = ecVerify(ctx, 0, 0, &ecdsa.PublicKey{Curve: elliptic.P256().Params()}) - test.AssertError(t, err, "ecVerify didn't fail on Sign error") - - // test signature verification failing - ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { - return []byte{1, 2, 3}, nil - } - tk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - test.AssertNotError(t, err, "ecdsa.GenerateKey failed") - err = ecVerify(ctx, 0, 0, &tk.PublicKey) - test.AssertError(t, err, "ecVerify didn't fail on signature verification error") - - // test we don't fail with valid signature - ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { - hash := sha256.Sum256([]byte{1, 2, 3}) - r, s, err := ecdsa.Sign(rand.Reader, tk, hash[:]) - if err != nil { - return nil, err - } - rBytes := r.Bytes() - sBytes := s.Bytes() - // http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/os/pkcs11-curr-v2.40-os.html - // Section 2.3.1: EC Signatures - // "If r and s have different octet length, the shorter of both must be padded with - // leading zero octets such that both have the same octet length." - switch { - case len(rBytes) < len(sBytes): - padding := make([]byte, len(sBytes)-len(rBytes)) - rBytes = append(padding, rBytes...) - case len(rBytes) > len(sBytes): - padding := make([]byte, len(rBytes)-len(sBytes)) - sBytes = append(padding, sBytes...) - } - return append(rBytes, sBytes...), nil - } - err = ecVerify(ctx, 0, 0, &tk.PublicKey) - test.AssertNotError(t, err, "ecVerify failed with a valid signature") -} - -func TestECGenerate(t *testing.T) { - ctx := pkcs11helpers.MockCtx{} - - // Test ecGenerate fails with unknown curve - _, err := ecGenerate(ctx, 0, "", "bad-curve") - test.AssertError(t, err, "ecGenerate accepted unknown curve") - - // Test ecGenerate fails when GenerateKeyPair fails - ctx.GenerateKeyPairFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute, []*pkcs11.Attribute) (pkcs11.ObjectHandle, pkcs11.ObjectHandle, error) { - return 0, 0, errors.New("bad") - } - _, err = ecGenerate(ctx, 0, "", "P-256") - test.AssertError(t, err, "ecGenerate didn't fail on GenerateKeyPair error") - - // Test ecGenerate fails when ecPub fails - ctx.GenerateKeyPairFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute, []*pkcs11.Attribute) (pkcs11.ObjectHandle, pkcs11.ObjectHandle, error) { - return 0, 0, nil - } - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return nil, errors.New("bad") - } - _, err = ecGenerate(ctx, 0, "", "P-256") - test.AssertError(t, err, "ecGenerate didn't fail on ecPub error") - - // Test ecGenerate fails when ecVerify fails - ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { - return []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 71, 137, 101, 56, 44, 59, 172, 148, 152, 118, 61, 183, 215, 242, 168, 62, 77, 94, 246, 212, 164, 96, 210, 134, 87, 169, 142, 226, 189, 118, 137, 203, 117, 55, 2, 215, 177, 159, 42, 196, 33, 91, 92, 251, 98, 53, 137, 221, 167, 148, 25, 209, 1, 5, 90, 52, 43, 18, 7, 30, 33, 142, 228, 235}), - }, nil - } - ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { - return nil, errors.New("yup") - } - _, err = ecGenerate(ctx, 0, "", "P-256") - test.AssertError(t, err, "ecGenerate didn't fail on ecVerify error") - - // Test ecGenerate doesn't fail when everything works - ctx.SignInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle) error { - return nil - } - ctx.GenerateRandomFunc = func(pkcs11.SessionHandle, int) ([]byte, error) { - return []byte{1, 2, 3}, nil - } - ctx.SignFunc = func(pkcs11.SessionHandle, []byte) ([]byte, error) { - return []byte{82, 33, 179, 118, 118, 141, 38, 154, 5, 20, 207, 140, 127, 221, 237, 139, 222, 74, 189, 107, 84, 133, 127, 80, 226, 169, 25, 110, 141, 226, 196, 69, 202, 51, 204, 77, 22, 198, 104, 91, 74, 120, 221, 156, 122, 11, 43, 54, 106, 10, 165, 202, 229, 71, 44, 18, 113, 236, 213, 47, 208, 239, 198, 33}, nil - } - _, err = ecGenerate(ctx, 0, "", "P-256") - test.AssertNotError(t, err, "ecGenerate didn't succeed when everything worked as expected") -} diff --git a/cmd/gen-key/main.go b/cmd/gen-key/main.go deleted file mode 100644 index 19013d851..000000000 --- a/cmd/gen-key/main.go +++ /dev/null @@ -1,114 +0,0 @@ -// gen-key is a tool for generating RSA or ECDSA keys on a HSM using PKCS#11. -// After generating the key pair it attempts to extract and construct the public -// key and verifies a test message that was signed using the generated private -// key. Any action it takes should be thoroughly logged and documented. -// -// When generating a key this tool follows the following steps: -// 1. Constructs templates for the private and public keys consisting -// of the appropriate PKCS#11 attributes. -// 2. Executes a PKCS#11 GenerateKeyPair operation with the constructed -// templates and either CKM_RSA_PKCS_KEY_PAIR_GEN or CKM_EC_KEY_PAIR_GEN. -// 3. Extracts the public key components from the returned public key object -// handle and construct a Golang public key object from them. -// 4. Generates 4 bytes of random data from the HSM using a PKCS#11 GenerateRandom -// operation. -// 5. Signs the random data with the private key object handle using a PKCS#11 -// SignInit/Sign operation. -// 6. Verifies the returned signature of the random data with the constructed -// public key. -// 7. Marshals the public key into a PEM public key object and print it to STDOUT. -// -package main - -import ( - "crypto/x509" - "encoding/pem" - "flag" - "io/ioutil" - "log" - "os" - - "github.com/letsencrypt/boulder/pkcs11helpers" - "github.com/miekg/pkcs11" -) - -type generateArgs struct { - mechanism []*pkcs11.Mechanism - privateAttrs []*pkcs11.Attribute - publicAttrs []*pkcs11.Attribute -} - -func getRandomBytes(ctx pkcs11helpers.PKCtx, session pkcs11.SessionHandle) ([]byte, error) { - r, err := ctx.GenerateRandom(session, 4) - if err != nil { - return nil, err - } - return r, nil -} - -func main() { - module := flag.String("module", "", "PKCS#11 module to use") - keyType := flag.String("type", "", "Type of key to generate (RSA or ECDSA)") - slot := flag.Uint("slot", 0, "Slot to generate key in") - pin := flag.String("pin", "", "PIN for slot if not using PED to login") - label := flag.String("label", "", "Key label") - rsaModLen := flag.Uint("modulus-bits", 0, "Size of RSA modulus in bits. Only used if --type=RSA") - rsaExp := flag.Uint("public-exponent", 65537, "Public RSA exponent. Only used if --type=RSA") - ecdsaCurve := flag.String("curve", "", "Type of ECDSA curve to use (P-224, P-256, P-384, P-521). Only used if --type=ECDSA") - outputPath := flag.String("output", "", "Path to store generated PEM public key") - flag.Parse() - - if *module == "" { - log.Fatal("--module is required") - } - if *keyType == "" { - log.Fatal("--type is required") - } - if *keyType != "RSA" && *keyType != "ECDSA" { - log.Fatal("--type may only be RSA or ECDSA") - } - if *label == "" { - log.Fatal("--label is required") - } - if *outputPath == "" { - log.Fatal("--output is required") - } - - ctx, session, err := pkcs11helpers.Initialize(*module, *slot, *pin) - if err != nil { - log.Fatalf("Failed to setup session and PKCS#11 context: %s", err) - } - log.Println("Opened PKCS#11 session") - - var pubKey interface{} - switch *keyType { - case "RSA": - if *rsaModLen == 0 { - log.Fatal("--modulus-bits is required") - } - pubKey, err = rsaGenerate(ctx, session, *label, *rsaModLen, *rsaExp) - if err != nil { - log.Fatalf("Failed to generate RSA key pair: %s", err) - } - case "ECDSA": - if *ecdsaCurve == "" { - log.Fatal("--ecdsaCurve is required") - } - pubKey, err = ecGenerate(ctx, session, *label, *ecdsaCurve) - if err != nil { - log.Fatalf("Failed to generate ECDSA key pair: %s", err) - } - } - - der, err := x509.MarshalPKIXPublicKey(pubKey) - if err != nil { - log.Fatalf("Failed to marshal public key: %s", err) - } - - pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der}) - log.Printf("Public key PEM:\n%s\n", pemBytes) - if err := ioutil.WriteFile(*outputPath, pemBytes, os.ModePerm); err != nil { - log.Fatalf("Failed to write public key to %q: %s", *outputPath, err) - } - log.Printf("Public key written to %q\n", *outputPath) -} diff --git a/pkcs11helpers/helpers.go b/pkcs11helpers/helpers.go index 0fe4f6f6d..0be0b2c40 100644 --- a/pkcs11helpers/helpers.go +++ b/pkcs11helpers/helpers.go @@ -79,11 +79,11 @@ func GetRSAPublicKey(ctx PKCtx, session pkcs11.SessionHandle, object pkcs11.Obje // oidDERToCurve maps the hex of the DER encoding of the various curve OIDs to // the relevant curve parameters -var oidDERToCurve = map[string]*elliptic.CurveParams{ - "06052B81040021": elliptic.P224().Params(), - "06082A8648CE3D030107": elliptic.P256().Params(), - "06052B81040022": elliptic.P384().Params(), - "06052B81040023": elliptic.P521().Params(), +var oidDERToCurve = map[string]elliptic.Curve{ + "06052B81040021": elliptic.P224(), + "06082A8648CE3D030107": elliptic.P256(), + "06052B81040022": elliptic.P384(), + "06052B81040023": elliptic.P521(), } func GetECDSAPublicKey(ctx PKCtx, session pkcs11.SessionHandle, object pkcs11.ObjectHandle) (*ecdsa.PublicKey, error) { @@ -182,6 +182,29 @@ func Sign(ctx PKCtx, session pkcs11.SessionHandle, object pkcs11.ObjectHandle, k return signature, nil } +// FindObject looks up a PKCS#11 object handle based on the provided template. +// In the case where zero or more than one objects are found to match the +// template an error is returned. +func FindObject(ctx PKCtx, session pkcs11.SessionHandle, tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) { + if err := ctx.FindObjectsInit(session, tmpl); err != nil { + return 0, err + } + handles, more, err := ctx.FindObjects(session, 1) + if err != nil { + return 0, err + } + if len(handles) == 0 { + return 0, errors.New("no objects found matching provided template") + } + if more { + return 0, errors.New("more than one object matches provided template") + } + if err := ctx.FindObjectsFinal(session); err != nil { + return 0, err + } + return handles[0], nil +} + type MockCtx struct { GenerateKeyPairFunc func(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute, []*pkcs11.Attribute) (pkcs11.ObjectHandle, pkcs11.ObjectHandle, error) GetAttributeValueFunc func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) diff --git a/pkcs11helpers/helpers_test.go b/pkcs11helpers/helpers_test.go new file mode 100644 index 000000000..3b87e06aa --- /dev/null +++ b/pkcs11helpers/helpers_test.go @@ -0,0 +1,167 @@ +package pkcs11helpers + +import ( + "errors" + "testing" + + "github.com/letsencrypt/boulder/test" + "github.com/miekg/pkcs11" +) + +func TestGetECDSAPublicKey(t *testing.T) { + ctx := MockCtx{} + + // test attribute retrieval failing + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return nil, errors.New("yup") + } + _, err := GetECDSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "ecPub didn't fail on GetAttributeValue error") + + // test we fail to construct key with missing params and point + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{}, nil + } + _, err = GetECDSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "ecPub didn't fail with empty attribute list") + + // test we fail to construct key with unknown curve + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{1, 2, 3}), + }, nil + } + _, err = GetECDSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "ecPub didn't fail with unknown curve") + + // test we fail to construct key with invalid EC point (invalid encoding) + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{255}), + }, nil + } + _, err = GetECDSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "ecPub didn't fail with invalid EC point (invalid encoding)") + + // test we fail to construct key with invalid EC point (empty octet string) + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 0}), + }, nil + } + _, err = GetECDSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "ecPub didn't fail with invalid EC point (empty octet string)") + + // test we fail to construct key with invalid EC point (octet string, invalid contents) + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 8, 42, 134, 72, 206, 61, 3, 1, 7}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 4, 4, 1, 2, 3}), + }, nil + } + _, err = GetECDSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "ecPub didn't fail with invalid EC point (octet string, invalid contents)") + + // test we don't fail with the correct attributes (traditional encoding) + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 5, 43, 129, 4, 0, 33}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 217, 225, 246, 210, 153, 134, 246, 104, 95, 79, 122, 206, 135, 241, 37, 114, 199, 87, 56, 167, 83, 56, 136, 174, 6, 145, 97, 239, 221, 49, 67, 148, 13, 126, 65, 90, 208, 195, 193, 171, 105, 40, 98, 132, 124, 30, 189, 215, 197, 178, 226, 166, 238, 240, 57, 215}), + }, nil + } + _, err = GetECDSAPublicKey(ctx, 0, 0) + test.AssertNotError(t, err, "ecPub failed with valid attributes (traditional encoding)") + + // test we don't fail with the correct attributes (non-traditional encoding) + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, []byte{6, 5, 43, 129, 4, 0, 33}), + pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, []byte{4, 57, 4, 217, 225, 246, 210, 153, 134, 246, 104, 95, 79, 122, 206, 135, 241, 37, 114, 199, 87, 56, 167, 83, 56, 136, 174, 6, 145, 97, 239, 221, 49, 67, 148, 13, 126, 65, 90, 208, 195, 193, 171, 105, 40, 98, 132, 124, 30, 189, 215, 197, 178, 226, 166, 238, 240, 57, 215}), + }, nil + } + _, err = GetECDSAPublicKey(ctx, 0, 0) + test.AssertNotError(t, err, "ecPub failed with valid attributes (non-traditional encoding)") +} + +func TestRSAPublicKey(t *testing.T) { + ctx := MockCtx{} + + // test attribute retrieval failing + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return nil, errors.New("yup") + } + _, err := GetRSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "rsaPub didn't fail on GetAttributeValue error") + + // test we fail to construct key with missing modulus and exp + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{}, nil + } + _, err = GetRSAPublicKey(ctx, 0, 0) + test.AssertError(t, err, "rsaPub didn't fail with empty attribute list") + + // test we don't fail with the correct attributes + ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) { + return []*pkcs11.Attribute{ + pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, []byte{1, 0, 1}), + pkcs11.NewAttribute(pkcs11.CKA_MODULUS, []byte{255}), + }, nil + } + _, err = GetRSAPublicKey(ctx, 0, 0) + test.AssertNotError(t, err, "rsaPub failed with valid attributes") +} + +func TestFindObject(t *testing.T) { + ctx := MockCtx{} + + // test FindObject fails when FindObjectsInit fails + ctx.FindObjectsInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Attribute) error { + return errors.New("broken") + } + _, err := FindObject(ctx, 0, nil) + test.AssertError(t, err, "FindObject didn't fail when FindObjectsInit failed") + + // test FindObject fails when FindObjects fails + ctx.FindObjectsInitFunc = func(pkcs11.SessionHandle, []*pkcs11.Attribute) error { + return nil + } + ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { + return nil, false, errors.New("broken") + } + _, err = FindObject(ctx, 0, nil) + test.AssertError(t, err, "FindObject didn't fail when FindObjects failed") + + // test FindObject fails when no handles are returned + ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { + return []pkcs11.ObjectHandle{}, false, nil + } + _, err = FindObject(ctx, 0, nil) + test.AssertError(t, err, "FindObject didn't fail when FindObjects returns no handles") + + // test FindObject fails when multiple handles are returned + ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { + return []pkcs11.ObjectHandle{1}, true, nil + } + _, err = FindObject(ctx, 0, nil) + test.AssertError(t, err, "FindObject didn't fail when FindObjects returns multiple handles") + + // test FindObject fails when FindObjectsFinal fails + ctx.FindObjectsFunc = func(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error) { + return []pkcs11.ObjectHandle{1}, false, nil + } + ctx.FindObjectsFinalFunc = func(pkcs11.SessionHandle) error { + return errors.New("broken") + } + _, err = FindObject(ctx, 0, nil) + test.AssertError(t, err, "FindObject didn't fail when FindObjectsFinal fails") + + // test FindObject works + ctx.FindObjectsFinalFunc = func(pkcs11.SessionHandle) error { + return nil + } + handle, err := FindObject(ctx, 0, nil) + test.AssertNotError(t, err, "FindObject failed when everything worked as expected") + test.AssertEquals(t, handle, pkcs11.ObjectHandle(1)) +}