From d13754166a16ce0d37a98f5f535933ab48a9b027 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 9 Jan 2020 18:41:13 -0800 Subject: [PATCH] Add support for cloudkms and softkms. --- kms/apiv1/options.go | 59 ++++++++++++++ kms/apiv1/requests.go | 136 +++++++++++++++++++++++++++++++ kms/cloudkms/cloudkms.go | 172 +++++++++++++++++++++++++++++++++++++++ kms/cloudkms/signer.go | 80 ++++++++++++++++++ kms/kms.go | 35 ++++++++ kms/softkms/softkms.go | 112 +++++++++++++++++++++++++ 6 files changed, 594 insertions(+) create mode 100644 kms/apiv1/options.go create mode 100644 kms/apiv1/requests.go create mode 100644 kms/cloudkms/cloudkms.go create mode 100644 kms/cloudkms/signer.go create mode 100644 kms/kms.go create mode 100644 kms/softkms/softkms.go diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go new file mode 100644 index 00000000..a6672087 --- /dev/null +++ b/kms/apiv1/options.go @@ -0,0 +1,59 @@ +package apiv1 + +import ( + "strings" + + "github.com/pkg/errors" +) + +// ErrNotImplemented +type ErrNotImplemented struct { + msg string +} + +func (e ErrNotImplemented) Error() string { + if e.msg != "" { + return e.msg + } + return "not implemented" +} + +// Type represents the KMS type used. +type Type string + +const ( + // DefaultKMS is a KMS implementation using software. + DefaultKMS Type = "" + // SoftKMS is a KMS implementation using software. + SoftKMS = "softkms" + // CloudKMS is a KMS implementation using Google's Cloud KMS. + CloudKMS = "cloudkms" + // AmazonKMS is a KMS implementation using Amazon AWS KMS. + AmazonKMS = "awskms" + // PKCS11 is a KMS implementation using the PKCS11 standard. + PKCS11 = "pkcs11" +) + +type Options struct { + Type string `json:"type"` + CredentialsFile string `json:"credentialsFile"` +} + +// Validate checks the fields in Options. +func (o *Options) Validate() error { + if o == nil { + return nil + } + + switch Type(strings.ToLower(o.Type)) { + case DefaultKMS, SoftKMS, CloudKMS: + case AmazonKMS: + return ErrNotImplemented{"support for AmazonKMS is not yet implemented"} + case PKCS11: + return ErrNotImplemented{"support for PKCS11 is not yet implemented"} + default: + return errors.Errorf("unsupported kms type %s", o.Type) + } + + return nil +} diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go new file mode 100644 index 00000000..d079f6c1 --- /dev/null +++ b/kms/apiv1/requests.go @@ -0,0 +1,136 @@ +package apiv1 + +import ( + "crypto" + "fmt" +) + +type KeyType int + +const ( + // nolint:camelcase + RSA_2048 KeyType = iota + RSA_3072 + RSA_4096 + EC_P256 + EC_P384 + EC_P512 +) + +// ProtectionLevel specifies on some KMS how cryptographic operations are +// performed. +type ProtectionLevel int + +const ( + // Protection level not specified. + UnspecifiedProtectionLevel ProtectionLevel = iota + // Crypto operations are performed in software. + Software + // Crypto operations are performed in a Hardware Security Module. + HSM +) + +// String returns a string representation of p. +func (p ProtectionLevel) String() string { + switch p { + case UnspecifiedProtectionLevel: + return "unspecified" + case Software: + return "software" + case HSM: + return "hsm" + default: + return fmt.Sprintf("unknown(%d)", p) + } +} + +// SignatureAlgorithm used for cryptographic signing. +type SignatureAlgorithm int + +const ( + // Not specified. + UnspecifiedSignAlgorithm SignatureAlgorithm = iota + // RSASSA-PKCS1-v1_5 key and a SHA256 digest. + SHA256WithRSA + // RSASSA-PKCS1-v1_5 key and a SHA384 digest. + SHA384WithRSA + // RSASSA-PKCS1-v1_5 key and a SHA512 digest. + SHA512WithRSA + // RSASSA-PSS key with a SHA256 digest. + SHA256WithRSAPSS + // RSASSA-PSS key with a SHA384 digest. + SHA384WithRSAPSS + // RSASSA-PSS key with a SHA512 digest. + SHA512WithRSAPSS + // ECDSA on the NIST P-256 curve with a SHA256 digest. + ECDSAWithSHA256 + // ECDSA on the NIST P-384 curve with a SHA384 digest. + ECDSAWithSHA384 + // ECDSA on the NIST P-521 curve with a SHA512 digest. + ECDSAWithSHA512 + // EdDSA on Curve25519 with a SHA512 digest. + PureEd25519 +) + +// String returns a string representation of s. +func (s SignatureAlgorithm) String() string { + switch s { + case UnspecifiedSignAlgorithm: + return "unspecified" + case SHA256WithRSA: + return "SHA256-RSA" + case SHA384WithRSA: + return "SHA384-RSA" + case SHA512WithRSA: + return "SHA512-RSA" + case SHA256WithRSAPSS: + return "SHA256-RSAPSS" + case SHA384WithRSAPSS: + return "SHA384-RSAPSS" + case SHA512WithRSAPSS: + return "SHA512-RSAPSS" + case ECDSAWithSHA256: + return "ECDSA-SHA256" + case ECDSAWithSHA384: + return "ECDSA-SHA384" + case ECDSAWithSHA512: + return "ECDSA-SHA512" + case PureEd25519: + return "Ed25519" + default: + return fmt.Sprintf("unknown(%d)", s) + } +} + +type GetPublicKeyRequest struct { + Name string +} + +type GetPublicKeyResponse struct { + Name string + PublicKey crypto.PublicKey +} + +type CreateKeyRequest struct { + Parent string + Name string + Type KeyType + Bits int + SignatureAlgorithm SignatureAlgorithm + + // ProtectionLevel specifies how cryptographic operations are performed. + // Used by: cloudkms + ProtectionLevel ProtectionLevel +} + +type CreateKeyResponse struct { + Name string + PublicKey crypto.PublicKey + PrivateKey crypto.PrivateKey +} + +type CreateSignerRequest struct { + SigningKey string + SigningKeyPEM []byte + Password string +} diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go new file mode 100644 index 00000000..f122bdeb --- /dev/null +++ b/kms/cloudkms/cloudkms.go @@ -0,0 +1,172 @@ +package cloudkms + +import ( + "context" + "crypto" + "time" + + cloudkms "cloud.google.com/go/kms/apiv1" + gax "github.com/googleapis/gax-go/v2" + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/pemutil" + "google.golang.org/api/option" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +// protectionLevelMapping maps step protection levels with cloud kms ones. +var protectionLevelMapping = map[apiv1.ProtectionLevel]kmspb.ProtectionLevel{ + apiv1.UnspecifiedProtectionLevel: kmspb.ProtectionLevel_PROTECTION_LEVEL_UNSPECIFIED, + apiv1.Software: kmspb.ProtectionLevel_SOFTWARE, + apiv1.HSM: kmspb.ProtectionLevel_HSM, +} + +// signatureAlgorithmMapping is a mapping between the step signature algorithm, +// and bits for RSA keys, with cloud kms one. +// +// Cloud KMS does not support SHA384WithRSA, SHA384WithRSAPSS, SHA384WithRSAPSS, +// ECDSAWithSHA512, and PureEd25519. +var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ + apiv1.UnspecifiedSignAlgorithm: kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED, + apiv1.SHA256WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256, + 2048: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256, + 3072: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + }, + apiv1.SHA512WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + }, + apiv1.SHA256WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256, + 2048: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256, + 3072: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256, + }, + apiv1.SHA512WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512, + }, + apiv1.ECDSAWithSHA256: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, + apiv1.ECDSAWithSHA384: kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, +} + +type keyManagementClient interface { + GetPublicKey(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error) + AsymmetricSign(context.Context, *kmspb.AsymmetricSignRequest, ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) + CreateCryptoKey(context.Context, *kmspb.CreateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error) +} + +// CloudKMS implements a KMS using Google's Cloud apiv1. +type CloudKMS struct { + client keyManagementClient +} + +func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) { + var cloudOpts []option.ClientOption + if opts.CredentialsFile != "" { + cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile)) + } + + client, err := cloudkms.NewKeyManagementClient(ctx, cloudOpts...) + if err != nil { + return nil, err + } + + return &CloudKMS{ + client: client, + }, nil +} + +// CreateSigner returns a new cloudkms signer configured with the given signing +// key name. +func (k *CloudKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + if req.SigningKey == "" { + return nil, errors.New("signing key cannot be empty") + } + + return newSigner(k.client, req.SigningKey), nil +} + +// CreateKey creates in Google's Cloud KMS a new asymmetric key for signing. +func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + switch { + case req.Name == "": + return nil, errors.New("createKeyRequest 'name' cannot be empty") + case req.Parent == "": + return nil, errors.New("createKeyRequest 'parent' cannot be empty") + } + + protectionLevel, ok := protectionLevelMapping[req.ProtectionLevel] + if !ok { + return nil, errors.Errorf("cloudKMS does not support protection level '%s'", req.ProtectionLevel) + } + + var signatureAlgorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm + v, ok := signatureAlgorithmMapping[req.SignatureAlgorithm] + if !ok { + return nil, errors.Errorf("cloudKMS does not support signature algorithm '%s'", req.SignatureAlgorithm) + } + switch v := v.(type) { + case kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm: + signatureAlgorithm = v + case map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm: + if signatureAlgorithm, ok = v[req.Bits]; !ok { + return nil, errors.Errorf("cloudKMS does not support signature algorithm '%s' with '%d' bits", req.SignatureAlgorithm, req.Bits) + } + default: + return nil, errors.Errorf("unexpected error: this should not happen") + } + + ctx, cancel := defaultContext() + defer cancel() + + response, err := k.client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{ + Parent: req.Parent, + CryptoKeyId: req.Name, + CryptoKey: &kmspb.CryptoKey{ + Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ + ProtectionLevel: protectionLevel, + Algorithm: signatureAlgorithm, + }, + }, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS CreateCryptoKey failed") + } + + return &apiv1.CreateKeyResponse{ + Name: response.Name, + }, nil +} + +// GetPublicKey gets from Google's Cloud KMS a public key by name. Key names +// follow the pattern: +// projects/([^/]+)/locations/([a-zA-Z0-9_-]{1,63})/keyRings/([a-zA-Z0-9_-]{1,63})/cryptoKeys/([a-zA-Z0-9_-]{1,63})/cryptoKeyVersions/([a-zA-Z0-9_-]{1,63}) +func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPublicKeyResponse, error) { + ctx, cancel := defaultContext() + defer cancel() + + response, err := k.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ + Name: req.Name, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS GetPublicKey failed") + } + + pk, err := pemutil.ParseKey([]byte(response.Pem)) + if err != nil { + return nil, err + } + + return &apiv1.GetPublicKeyResponse{ + Name: req.Name, + PublicKey: pk, + }, nil +} + +func defaultContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 15*time.Second) +} diff --git a/kms/cloudkms/signer.go b/kms/cloudkms/signer.go new file mode 100644 index 00000000..a28fd9d8 --- /dev/null +++ b/kms/cloudkms/signer.go @@ -0,0 +1,80 @@ +package cloudkms + +import ( + "crypto" + "io" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/pemutil" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +// signer implements a crypto.Signer using Google's Cloud KMS. +type signer struct { + client keyManagementClient + signingKey string +} + +func newSigner(c keyManagementClient, signingKey string) *signer { + return &signer{ + client: c, + signingKey: signingKey, + } +} + +// Public returns the public key of this signer or an error. +func (s *signer) Public() crypto.PublicKey { + ctx, cancel := defaultContext() + defer cancel() + + response, err := s.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ + Name: s.signingKey, + }) + if err != nil { + println(1, err.Error()) + return errors.Wrap(err, "cloudKMS GetPublicKey failed") + } + + pk, err := pemutil.ParseKey([]byte(response.Pem)) + if err != nil { + println(2, err.Error()) + return err + } + + return pk +} + +// Sign signs digest with the private key stored in Google's Cloud KMS. +func (s *signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + req := &kmspb.AsymmetricSignRequest{ + Name: s.signingKey, + Digest: &kmspb.Digest{}, + } + + switch h := opts.HashFunc(); h { + case crypto.SHA256: + req.Digest.Digest = &kmspb.Digest_Sha256{ + Sha256: digest, + } + case crypto.SHA384: + req.Digest.Digest = &kmspb.Digest_Sha384{ + Sha384: digest, + } + case crypto.SHA512: + req.Digest.Digest = &kmspb.Digest_Sha512{ + Sha512: digest, + } + default: + return nil, errors.Errorf("unsupported hash function %v", h) + } + + ctx, cancel := defaultContext() + defer cancel() + + response, err := s.client.AsymmetricSign(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "cloudKMS AsymmetricSign failed") + } + + return response.Signature, nil +} diff --git a/kms/kms.go b/kms/kms.go new file mode 100644 index 00000000..5e54451d --- /dev/null +++ b/kms/kms.go @@ -0,0 +1,35 @@ +package kms + +import ( + "context" + "crypto" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/cloudkms" + "github.com/smallstep/certificates/kms/softkms" +) + +// KeyManager is the interface implemented by all the KMS. +type KeyManager interface { + GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPublicKeyResponse, error) + CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) + CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) +} + +// New initializes a new KMS from the given type. +func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + + switch apiv1.Type(strings.ToLower(opts.Type)) { + case apiv1.DefaultKMS, apiv1.SoftKMS: + return softkms.New(ctx, opts) + case apiv1.CloudKMS: + return cloudkms.New(ctx, opts) + default: + return nil, errors.Errorf("unsupported kms type '%s'", opts.Type) + } +} diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go new file mode 100644 index 00000000..adb8483e --- /dev/null +++ b/kms/softkms/softkms.go @@ -0,0 +1,112 @@ +package softkms + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/crypto/pemutil" +) + +type algorithmAttributes struct { + Type string + Curve string +} + +var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes{ + apiv1.UnspecifiedSignAlgorithm: algorithmAttributes{"EC", "P-256"}, + apiv1.SHA256WithRSA: algorithmAttributes{"RSA", ""}, + apiv1.SHA384WithRSA: algorithmAttributes{"RSA", ""}, + apiv1.SHA512WithRSA: algorithmAttributes{"RSA", ""}, + apiv1.SHA256WithRSAPSS: algorithmAttributes{"RSA", ""}, + apiv1.SHA384WithRSAPSS: algorithmAttributes{"RSA", ""}, + apiv1.SHA512WithRSAPSS: algorithmAttributes{"RSA", ""}, + apiv1.ECDSAWithSHA256: algorithmAttributes{"EC", "P-256"}, + apiv1.ECDSAWithSHA384: algorithmAttributes{"EC", "P-384"}, + apiv1.ECDSAWithSHA512: algorithmAttributes{"EC", "P-521"}, + apiv1.PureEd25519: algorithmAttributes{"OKP", "Ed25519"}, +} + +// SoftKSM is a key manager that uses keys stored in disk. +type SoftKMS struct{} + +// New returns a new SoftKSM. +func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) { + return &SoftKMS{}, nil +} + +// CreateSigner returns a new signer configured with the given signing key. +func (k *SoftKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + var opts []pemutil.Options + if req.Password != "" { + opts = append(opts, pemutil.WithPassword([]byte(req.Password))) + } + + switch { + case len(req.SigningKeyPEM) != 0: + v, err := pemutil.ParseKey(req.SigningKeyPEM, opts...) + if err != nil { + return nil, err + } + sig, ok := v.(crypto.Signer) + if !ok { + return nil, errors.New("signingKeyPEM is not a crypto.Signer") + } + return sig, nil + case req.SigningKey != "": + v, err := pemutil.Read(req.SigningKey, opts...) + if err != nil { + return nil, err + } + sig, ok := v.(crypto.Signer) + if !ok { + return nil, errors.New("signingKey is not a crypto.Signer") + } + return sig, nil + default: + return nil, errors.New("failed to load softKMS: please define signingKeyPEM or signingKey") + } +} + +func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + v, ok := signatureAlgorithmMapping[req.SignatureAlgorithm] + if !ok { + return nil, errors.Errorf("softKMS does not support signature algorithm '%s'", req.SignatureAlgorithm) + } + + pub, priv, err := keys.GenerateKeyPair(v.Type, v.Curve, req.Bits) + if err != nil { + return nil, err + } + + return &apiv1.CreateKeyResponse{ + Name: req.Name, + PublicKey: pub, + PrivateKey: priv, + }, nil +} + +func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (*apiv1.GetPublicKeyResponse, error) { + v, err := pemutil.Read(req.Name) + if err != nil { + return nil, err + } + + switch v.(type) { + case *x509.Certificate: + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + default: + return nil, errors.Errorf("unsupported public key type %T", v) + } + + return &apiv1.GetPublicKeyResponse{ + Name: req.Name, + PublicKey: v, + }, nil +}