diff --git a/CHANGELOG.md b/CHANGELOG.md index 70635fb6d..4de80b221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,10 +37,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Detect OIDC tokens issued by Kubernetes (smallstep/cli#953). - Add support for Smallstep Managed Endpoint X509 extension (smallstep/cli#989). +- Support signing a certificate for a private key that can only be used for encryption with the `--skip-csr-signature` flag in `step certificate create`. Some KMSs restrict key usage to a single type of cryptographic operation. This blocks RSA decryption keys from being used to sign a CSR for their public key. Using the `--skip-csr-signature` flag, the public key is used directly with a certificate template, removing the need for the CSR signature. ### Changed - Increase PBKDF2 iterations to 600k (smallstep/cli#949). +- `--kms` flag is no longer used for the CA (signing) key for `step certificate create`. It was replaced by the `--ca-kms` flag (smallstep/cli#942). ### Fixed diff --git a/command/certificate/create.go b/command/certificate/create.go index a216deb65..a975b6525 100644 --- a/command/certificate/create.go +++ b/command/certificate/create.go @@ -44,11 +44,11 @@ func createCommand() cli.Command { [**--kms**=] [**--csr**] [**--profile**=] [**--template**=] [**--set**=] [**--set-file**=] [**--not-before**=] [**--not-after**=] -[**--password-file**=] [**--ca**=] +[**--password-file**=] [**--ca**=] [**--ca-key**=] [**--ca-password-file**=] -[**--san**=] [**--bundle**] [**--key**=] +[**--ca-kms**=] [**--san**=] [**--bundle**] [**--key**=] [**--kty**=] [**--curve**=] [**--size**=] -[**--no-password**] [**--insecure**]`, +[**--skip-csr-signature**] [**--no-password**] [**--insecure**]`, Description: `**step certificate create** generates a certificate or a certificate signing request (CSR) that can be signed later using 'step certificate sign' (or some other tool) to produce a certificate. @@ -345,11 +345,34 @@ $ step kms create \ 'pkcs11:id=4001;object=intermediate-key' $ step certificate create \ --profile intermediate-ca \ - --kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \ + --ca-kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' --ca root_ca.crt --ca-key 'pkcs11:id=4000' \ + --kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \ --key 'pkcs11:id=4001' \ 'My KMS Intermediate' intermediate_ca.crt ''' + +Create an intermediate certificate for an RSA decryption key in Google Cloud KMS, signed by a root stored on disk, using : +''' +$ step certificate create \ + --profile intermediate-ca \ + --ca root_ca.crt --ca-key root_ca_key \ + --kms cloudkms: \ + --key 'projects/myProjectID/locations/global/keyRings/myKeyRing/cryptoKeys/myKey/cryptoKeyVersions/1' \ + --skip-csr-signature \ + 'My RSA Intermediate' intermediate_rsa_ca.crt +''' + +Create an intermediate certificate for an RSA signing key in Google Cloud KMS, signed by a root stored in an HSM, using : +''' +$ step certificate create \ + --profile intermediate-ca \ + --ca-kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \ + --ca root_ca.crt --ca-key 'pkcs11:id=4000' \ + --kms cloudkms: \ + --key 'projects/myProjectID/locations/global/keyRings/myKeyRing/cryptoKeys/myKey/cryptoKeyVersions/1' \ + 'My RSA Intermediate' intermediate_rsa_ca.crt +''' `, Flags: []cli.Flag{ flags.KMSUri, @@ -446,6 +469,14 @@ the **--ca** flag.`, Name: "insecure", Hidden: true, }, + cli.StringFlag{ + Name: "ca-kms", + Usage: "The to configure the KMS used for signing the certificate", + }, + cli.BoolFlag{ + Name: "skip-csr-signature", + Usage: "Skip creating and signing a CSR", + }, }, } } @@ -485,17 +516,22 @@ func createAction(ctx *cli.Context) error { } var ( - sans = ctx.StringSlice("san") - profile = ctx.String("profile") - templateFile = ctx.String("template") - bundle = ctx.Bool("bundle") - subtle = ctx.Bool("subtle") + sans = ctx.StringSlice("san") + profile = ctx.String("profile") + templateFile = ctx.String("template") + bundle = ctx.Bool("bundle") + subtle = ctx.Bool("subtle") + skipCSRSignature = ctx.Bool("skip-csr-signature") ) if ctx.IsSet("profile") && templateFile != "" { return errs.IncompatibleFlagWithFlag(ctx, "profile", "template") } + if ctx.Bool("csr") && skipCSRSignature { + return errs.IncompatibleFlagWithFlag(ctx, "csr", "skip-csr-signature") + } + // Read template if passed var template string if templateFile != "" { @@ -631,20 +667,31 @@ func createAction(ctx *cli.Context) error { defaultValidity = defaultTemplatevalidity } - // Create X.509 certificate used as base for the certificate - cr, err := x509util.CreateCertificateRequest(subject, sans, signer) - if err != nil { - return err - } - // Create X.509 certificate templateData := x509util.CreateTemplateData(subject, sans) templateData.SetUserData(userData) - certificate, err := x509util.NewCertificate(cr, x509util.WithTemplate(template, templateData)) - if err != nil { - return err + + var certTemplate = &x509.Certificate{} + if skipCSRSignature { + certTemplate.PublicKey = pub + certificate, err := x509util.NewCertificateFromX509(certTemplate, x509util.WithTemplate(template, templateData)) + if err != nil { + return err + } + certTemplate = certificate.GetCertificate() + } else { + // Create X.509 certificate used as base for the certificate + cr, err := x509util.CreateCertificateRequest(subject, sans, priv) + if err != nil { + return err + } + certificate, err := x509util.NewCertificate(cr, x509util.WithTemplate(template, templateData)) + if err != nil { + return err + } + certTemplate = certificate.GetCertificate() } - certTemplate := certificate.GetCertificate() + if parent == nil { parent = certTemplate } @@ -766,9 +813,9 @@ func parseSigner(ctx *cli.Context, defaultSigner crypto.Signer) (*x509.Certifica var ( caCert = ctx.String("ca") caKey = ctx.String("ca-key") + caKMS = ctx.String("ca-kms") profile = ctx.String("profile") template = ctx.String("template") - kms = ctx.String("kms") ) // Check required flags when profile is used. @@ -819,7 +866,7 @@ func parseSigner(ctx *cli.Context, defaultSigner crypto.Signer) (*x509.Certifica opts = append(opts, pemutil.WithPasswordFile(passFile)) } - signer, err := cryptoutil.CreateSigner(kms, caKey, opts...) + signer, err := cryptoutil.CreateSigner(caKMS, caKey, opts...) if err != nil { return nil, nil, err } diff --git a/internal/cryptoutil/cryptoutil.go b/internal/cryptoutil/cryptoutil.go index 02965bd39..0204cf934 100644 --- a/internal/cryptoutil/cryptoutil.go +++ b/internal/cryptoutil/cryptoutil.go @@ -29,7 +29,7 @@ type Attestor interface { // CreateSigner reads a key from a file with a given name or creates a signer // with the given kms and name uri. func CreateSigner(kms, name string, opts ...pemutil.Options) (crypto.Signer, error) { - if kms == "" { + if kms == "" || isSoftKMS(kms) { s, err := pemutil.Read(name, opts...) if err != nil { return nil, err @@ -43,6 +43,10 @@ func CreateSigner(kms, name string, opts ...pemutil.Options) (crypto.Signer, err return newKMSSigner(kms, name) } +func isSoftKMS(kms string) bool { + return strings.HasPrefix(strings.ToLower(strings.TrimSpace(kms)), "softkms") +} + // LoadCertificate returns a x509.Certificate from a kms or file func LoadCertificate(kms, certPath string) ([]*x509.Certificate, error) { if kms == "" { @@ -138,9 +142,13 @@ func IsKMSSigner(signer crypto.Signer) (ok bool) { } // IsX509Signer returns true if the given signer is supported by Go's -// crypto/x509 package to sign sign X509 certificates. This methods returns true +// crypto/x509 package to sign X509 certificates. This methods returns true // for ECDSA, RSA and Ed25519 keys, but if the kms is `sshagentkms:` it will // only return true for Ed25519 keys. +// TODO(hs): introspect the KMS key to verify that it can actually be +// used for signing? E.g. for Google Cloud KMS RSA keys can be used for +// signing or decryption, but only one of those at a time. Trying to use +// a signing key to decrypt data will result in an error from Cloud KMS. func IsX509Signer(signer crypto.Signer) bool { pub := signer.Public() if ks, ok := signer.(*kmsSigner); ok {