certificates/kms/cloudkms/cloudkms.go
2020-01-09 18:41:13 -08:00

172 lines
6.1 KiB
Go

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