173 lines
6.1 KiB
Go
173 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)
|
||
|
}
|