Add support for cloudkms and softkms.

This commit is contained in:
Mariano Cano 2020-01-09 18:41:13 -08:00
parent 8a10c5032f
commit d13754166a
6 changed files with 594 additions and 0 deletions

59
kms/apiv1/options.go Normal file
View file

@ -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
}

136
kms/apiv1/requests.go Normal file
View file

@ -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
}

172
kms/cloudkms/cloudkms.go Normal file
View file

@ -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)
}

80
kms/cloudkms/signer.go Normal file
View file

@ -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
}

35
kms/kms.go Normal file
View file

@ -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)
}
}

112
kms/softkms/softkms.go Normal file
View file

@ -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
}