constellation/keyservice/kms/gcp/gcp.go

315 lines
11 KiB
Go
Raw Normal View History

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package gcp
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"errors"
"fmt"
"io"
"strings"
"time"
kms "cloud.google.com/go/kms/apiv1"
"cloud.google.com/go/kms/apiv1/kmspb"
"github.com/edgelesssys/constellation/v2/keyservice/internal/config"
"github.com/edgelesssys/constellation/v2/keyservice/internal/storage"
kmsInterface "github.com/edgelesssys/constellation/v2/keyservice/kms"
"github.com/edgelesssys/constellation/v2/keyservice/kms/util"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/option"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type clientAPI interface {
io.Closer
CreateCryptoKey(context.Context, *kmspb.CreateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error)
CreateImportJob(context.Context, *kmspb.CreateImportJobRequest, ...gax.CallOption) (*kmspb.ImportJob, error)
Decrypt(context.Context, *kmspb.DecryptRequest, ...gax.CallOption) (*kmspb.DecryptResponse, error)
Encrypt(context.Context, *kmspb.EncryptRequest, ...gax.CallOption) (*kmspb.EncryptResponse, error)
GetKeyRing(context.Context, *kmspb.GetKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error)
ImportCryptoKeyVersion(context.Context, *kmspb.ImportCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error)
UpdateCryptoKeyPrimaryVersion(context.Context, *kmspb.UpdateCryptoKeyPrimaryVersionRequest, ...gax.CallOption) (*kmspb.CryptoKey, error)
GetImportJob(context.Context, *kmspb.GetImportJobRequest, ...gax.CallOption) (*kmspb.ImportJob, error)
}
// KMSClient implements the CloudKMS interface for Google Cloud Platform.
type KMSClient struct {
projectID string
locationID string
keyRingID string
newClient func(ctx context.Context, opts ...option.ClientOption) (clientAPI, error)
waitBackoffLimit int
storage kmsInterface.Storage
protectionLevel kmspb.ProtectionLevel
opts []gax.CallOption
}
// New initializes a KMS client for Google Cloud Platform.
func New(ctx context.Context, projectID, locationID, keyRingID string, store kmsInterface.Storage, protectionLvl kmspb.ProtectionLevel, opts ...gax.CallOption) (*KMSClient, error) {
if store == nil {
store = storage.NewMemMapStorage()
}
if protectionLvl != kmspb.ProtectionLevel_SOFTWARE && protectionLvl != kmspb.ProtectionLevel_HSM {
protectionLvl = kmspb.ProtectionLevel_SOFTWARE
}
c := &KMSClient{
projectID: projectID,
locationID: locationID,
keyRingID: keyRingID,
newClient: keyManagementClientFactory,
waitBackoffLimit: 10,
storage: store,
protectionLevel: protectionLvl,
opts: opts,
}
// test if the KMS can be reached with the given configuration
if err := c.testConnection(ctx); err != nil {
return nil, fmt.Errorf("testing connection to GCP KMS: %w", err)
}
return c, nil
}
// CreateKEK creates a new Key Encryption Key using Google Key Management System.
//
// If no key material is provided, a new key is generated by Google's KMS, otherwise the key material is used to import the key.
func (c *KMSClient) CreateKEK(ctx context.Context, keyID string, key []byte) error {
client, err := c.newClient(ctx)
if err != nil {
return err
}
defer client.Close()
if len(key) == 0 {
_, err := c.createNewKEK(ctx, keyID, client, false)
if err != nil {
return fmt.Errorf("creating new KEK in Google KMS: %w", err)
}
return nil
}
_, err = c.importKEK(ctx, keyID, key, client)
if err != nil {
return fmt.Errorf("importing KEK to Google KMS: %w", err)
}
return nil
}
// GetDEK fetches an encrypted Data Encryption Key from storage and decrypts it using a KEK stored in Google's KMS.
func (c *KMSClient) GetDEK(ctx context.Context, kekID, keyID string, dekSize int) ([]byte, error) {
client, err := c.newClient(ctx)
if err != nil {
return nil, err
}
defer client.Close()
encryptedDEK, err := c.storage.Get(ctx, keyID)
if err != nil {
if !errors.Is(err, storage.ErrDEKUnset) {
return nil, fmt.Errorf("loading encrypted DEK from storage: %w", err)
}
// If the DEK does not exist we generate a new random DEK and save it to storage
newDEK, err := util.GetRandomKey(dekSize)
if err != nil {
return nil, fmt.Errorf("key generation: %w", err)
}
return newDEK, c.putDEK(ctx, client, kekID, keyID, newDEK)
}
request := &kmspb.DecryptRequest{
Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", c.projectID, c.locationID, c.keyRingID, kekID),
Ciphertext: encryptedDEK,
}
res, err := client.Decrypt(ctx, request, c.opts...)
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return nil, kmsInterface.ErrKEKUnknown
}
return nil, fmt.Errorf("decrypting DEK: %w", err)
}
return res.GetPlaintext(), nil
}
// putDEK encrypts a Data Encryption Key using a KEK stored in Google's KMS and saves it to storage.
func (c *KMSClient) putDEK(ctx context.Context, client clientAPI, kekID, keyID string, plainDEK []byte) error {
request := &kmspb.EncryptRequest{
Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", c.projectID, c.locationID, c.keyRingID, kekID),
Plaintext: plainDEK,
}
res, err := client.Encrypt(ctx, request, c.opts...)
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return kmsInterface.ErrKEKUnknown
}
return fmt.Errorf("encrypting DEK: %w", err)
}
return c.storage.Put(ctx, keyID, res.Ciphertext)
}
// createNewKEK creates a new symmetric Crypto Key in Google's KMS.
func (c *KMSClient) createNewKEK(ctx context.Context, keyID string, client clientAPI, importOnly bool) (*kmspb.CryptoKey, error) {
request := &kmspb.CreateCryptoKeyRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", c.projectID, c.locationID, c.keyRingID),
CryptoKeyId: keyID,
CryptoKey: &kmspb.CryptoKey{
Purpose: kmspb.CryptoKey_ENCRYPT_DECRYPT,
Labels: map[string]string{
"created-by": "constellation-kms-client",
"component": "constellation-kek",
},
VersionTemplate: &kmspb.CryptoKeyVersionTemplate{
ProtectionLevel: c.protectionLevel,
Algorithm: kmspb.CryptoKeyVersion_GOOGLE_SYMMETRIC_ENCRYPTION,
},
ImportOnly: importOnly,
},
SkipInitialVersionCreation: importOnly,
}
return client.CreateCryptoKey(ctx, request, c.opts...)
}
// importKEK imports a symmetric Crypto Key to Google's KMS-
//
// Keys in the Google KMS can not be removed, only disabled and/or key material destroyed.
// Since we create the initial key with `SkipInitialVersionCreation=true`, no key material is created and we do not perform any cleanup on failure.
func (c *KMSClient) importKEK(ctx context.Context, keyID string, key []byte, client clientAPI) (*kmspb.CryptoKey, error) {
// we need an empty crypto key to import into
parentKey, err := c.createNewKEK(ctx, keyID, client, true)
if err != nil {
return nil, err
}
// Create import job
jobName := fmt.Sprintf("import-job-%s", keyID)
request := &kmspb.CreateImportJobRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", c.projectID, c.locationID, c.keyRingID),
ImportJobId: jobName,
ImportJob: &kmspb.ImportJob{
ImportMethod: kmspb.ImportJob_RSA_OAEP_4096_SHA1_AES_256,
ProtectionLevel: c.protectionLevel,
},
}
_, err = client.CreateImportJob(ctx, request, c.opts...)
if err != nil {
return nil, err
}
impRes, ok := c.waitBackoff(ctx, jobName, client)
if !ok {
return nil, fmt.Errorf("import job was not active after %d tries, giving up", c.waitBackoffLimit)
}
// Wrap the to be imported key using a public RSA key from the created import job and an ephemeral AES as specified here: https://cloud.google.com/kms/docs/wrapping-a-key
wrappingPublicKey, err := util.ParsePEMtoPublicKeyRSA([]byte(impRes.PublicKey.GetPem()))
if err != nil {
return nil, err
}
wrappedKey, err := wrapCryptoKey(key, wrappingPublicKey)
if err != nil {
return nil, fmt.Errorf("wrapping public key: %w", err)
}
// Perform the actual key import
importReq := &kmspb.ImportCryptoKeyVersionRequest{
Parent: parentKey.GetName(),
Algorithm: kmspb.CryptoKeyVersion_GOOGLE_SYMMETRIC_ENCRYPTION,
ImportJob: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/importJobs/%s", c.projectID, c.locationID, c.keyRingID, jobName),
WrappedKeyMaterial: &kmspb.ImportCryptoKeyVersionRequest_RsaAesWrappedKey{
RsaAesWrappedKey: wrappedKey,
},
}
res, err := client.ImportCryptoKeyVersion(ctx, importReq, c.opts...)
if err != nil {
return nil, err
}
// Set the imported key as the primary key version
newVersion := strings.Split(res.GetName(), "/")
updateRequest := &kmspb.UpdateCryptoKeyPrimaryVersionRequest{
Name: parentKey.GetName(),
CryptoKeyVersionId: newVersion[len(newVersion)-1], // We only need the Version ID of the imported key, not the full resource name
}
return client.UpdateCryptoKeyPrimaryVersion(ctx, updateRequest, c.opts...)
}
// waitBackoff is a utility function to wait for the creation of an import job.
func (c *KMSClient) waitBackoff(ctx context.Context, jobName string, client clientAPI) (*kmspb.ImportJob, bool) {
for i := 0; i < c.waitBackoffLimit; i++ {
res, err := client.GetImportJob(ctx, &kmspb.GetImportJobRequest{
Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/importJobs/%s", c.projectID, c.locationID, c.keyRingID, jobName),
}, c.opts...)
if (err == nil) && (res.State == kmspb.ImportJob_ACTIVE) {
return res, true
}
// wait for increasingly longer time until we either reach the preset limit or get an active job
time.Sleep(time.Second * 5 * time.Duration(i))
}
return nil, false
}
// testConnection checks if the KMS is reachable with the given configuration.
func (c *KMSClient) testConnection(ctx context.Context) error {
client, err := c.newClient(ctx)
if err != nil {
return err
}
defer client.Close()
if _, err := client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{
Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", c.projectID, c.locationID, c.keyRingID),
}); err != nil {
return fmt.Errorf("GCP KMS not reachable: %w", err)
}
return nil
}
func keyManagementClientFactory(ctx context.Context, opts ...option.ClientOption) (clientAPI, error) {
return kms.NewKeyManagementClient(ctx, opts...)
}
// wrapCryptoKey wraps a key for import using a public RSA key, see: https://cloud.google.com/kms/docs/wrapping-a-key
func wrapCryptoKey(key []byte, wrapKeyRSA *rsa.PublicKey) ([]byte, error) {
// Enforce 256bit key length
if len(key) != config.SymmetricKeyLength {
return nil, fmt.Errorf("invalid key size: want [%d], got [%d]", config.SymmetricKeyLength, len(key))
}
// create random 256bit AES wrapping key
wrapKeyAES := make([]byte, config.SymmetricKeyLength)
if _, err := rand.Read(wrapKeyAES); err != nil {
return nil, err
}
// Perform CKM_AES_KEY_WRAP_PAD to wrap the key
wrappedKey, err := util.WrapAES(key, wrapKeyAES)
if err != nil {
return nil, err
}
// Encrypt the ephemeral AES key with the KMS provided wrapping key
// Google KMS requires RSAES-OAEP with SHA-1 and an empty label
encWrapKeyAES, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, wrapKeyRSA, wrapKeyAES, nil)
if err != nil {
return nil, err
}
return append(encWrapKeyAES, wrappedKey...), nil
}