mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-31 09:43:23 -05:00
90b88e1cf9
In the light of extending our eKMS support it will be helpful to have a tighter use of the word "KMS". KMS should refer to the actual component that manages keys. The keyservice, also called KMS in the constellation code, does not manage keys itself. It talks to a KMS backend, which in turn does the actual key management.
277 lines
11 KiB
Go
277 lines
11 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package aws
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"errors"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/service/kms"
|
|
"github.com/aws/aws-sdk-go-v2/service/kms/types"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
// DEKContext is used as the encryption context in AWS KMS.
|
|
DEKContext = "aws:ebs:id"
|
|
)
|
|
|
|
// ClientAPI satisfies the Amazons KMS client's methods we need.
|
|
// This allows us to mock the actual client, see https://aws.github.io/aws-sdk-go-v2/docs/unit-testing/
|
|
type ClientAPI interface {
|
|
CreateAlias(ctx context.Context, params *kms.CreateAliasInput, optFns ...func(*kms.Options)) (*kms.CreateAliasOutput, error)
|
|
CreateKey(ctx context.Context, params *kms.CreateKeyInput, optFns ...func(*kms.Options)) (*kms.CreateKeyOutput, error)
|
|
Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error)
|
|
DeleteAlias(ctx context.Context, params *kms.DeleteAliasInput, optFns ...func(*kms.Options)) (*kms.DeleteAliasOutput, error)
|
|
DescribeKey(ctx context.Context, params *kms.DescribeKeyInput, optFns ...func(*kms.Options)) (*kms.DescribeKeyOutput, error)
|
|
Encrypt(ctx context.Context, params *kms.EncryptInput, optFns ...func(*kms.Options)) (*kms.EncryptOutput, error)
|
|
GenerateDataKey(ctx context.Context, params *kms.GenerateDataKeyInput, optFns ...func(*kms.Options)) (*kms.GenerateDataKeyOutput, error)
|
|
GenerateDataKeyWithoutPlaintext(ctx context.Context, params *kms.GenerateDataKeyWithoutPlaintextInput, optFns ...func(*kms.Options)) (*kms.GenerateDataKeyWithoutPlaintextOutput, error)
|
|
GetParametersForImport(ctx context.Context, params *kms.GetParametersForImportInput, optFns ...func(*kms.Options)) (*kms.GetParametersForImportOutput, error)
|
|
ImportKeyMaterial(ctx context.Context, params *kms.ImportKeyMaterialInput, optFns ...func(*kms.Options)) (*kms.ImportKeyMaterialOutput, error)
|
|
PutKeyPolicy(ctx context.Context, params *kms.PutKeyPolicyInput, optFns ...func(*kms.Options)) (*kms.PutKeyPolicyOutput, error)
|
|
ScheduleKeyDeletion(ctx context.Context, params *kms.ScheduleKeyDeletionInput, optFns ...func(*kms.Options)) (*kms.ScheduleKeyDeletionOutput, error)
|
|
}
|
|
|
|
// KeyPolicyProducer allows to have callbacks for generating key policies at runtime.
|
|
type KeyPolicyProducer interface {
|
|
// CreateKeyPolicy returns a key policy for a given key ID.
|
|
CreateKeyPolicy(keyID string) (string, error)
|
|
}
|
|
|
|
// KMSClient implements the CloudKMS interface for AWS.
|
|
type KMSClient struct {
|
|
awsClient ClientAPI
|
|
policyProducer KeyPolicyProducer
|
|
storage kmsInterface.Storage
|
|
}
|
|
|
|
// New creates and initializes a new KMSClient for AWS.
|
|
//
|
|
// The parameter client needs to be initialized with valid AWS credentials (https://aws.github.io/aws-sdk-go-v2/docs/getting-started).
|
|
// If storage is nil, the default MemMapStorage is used.
|
|
func New(ctx context.Context, policyProducer KeyPolicyProducer, store kmsInterface.Storage, optFns ...func(*awsconfig.LoadOptions) error) (*KMSClient, error) {
|
|
if store == nil {
|
|
store = storage.NewMemMapStorage()
|
|
}
|
|
|
|
cfg, err := awsconfig.LoadDefaultConfig(ctx, optFns...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client := kms.NewFromConfig(cfg)
|
|
|
|
return &KMSClient{
|
|
awsClient: client,
|
|
policyProducer: policyProducer,
|
|
storage: store,
|
|
}, nil
|
|
}
|
|
|
|
// CreateKEK creates a new KEK with the given key material and policy. If successful, the key can be referenced by keyID in the KMS in accordance to the policy.
|
|
// https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys.html
|
|
func (c *KMSClient) CreateKEK(ctx context.Context, keyID string, key []byte) error {
|
|
alias := "alias/" + keyID
|
|
|
|
// Check whether key with keyID already exists
|
|
describeKeyInput := &kms.DescribeKeyInput{
|
|
KeyId: aws.String(alias),
|
|
}
|
|
// If the keyID parameter is used for a key in the AWS KMS, the response includes the keyID generated by AWS at creation
|
|
var awsGeneratedKeyID string
|
|
newKeyCreationNeeded := true
|
|
var nfe *types.NotFoundException
|
|
describeKeyOutput, err := c.awsClient.DescribeKey(ctx, describeKeyInput)
|
|
if err == nil {
|
|
// The request is valid and a key with the keyID exists
|
|
awsGeneratedKeyID = *describeKeyOutput.KeyMetadata.KeyId
|
|
newKeyCreationNeeded = false
|
|
} else if !errors.As(err, &nfe) {
|
|
return err
|
|
}
|
|
|
|
// If it is not needed to create a new key, the steps to create the key and the alias can be skipped
|
|
if newKeyCreationNeeded {
|
|
// specifies that the key should be empty at creation and the material will be imported afterward
|
|
origin := types.OriginTypeExternal
|
|
if len(key) == 0 {
|
|
origin = types.OriginTypeAwsKms
|
|
}
|
|
// Creates new AWS KMS key with empty key material
|
|
var tags []types.Tag
|
|
for tagKey, tagValue := range config.KmsTags {
|
|
tags = append(tags, types.Tag{
|
|
TagKey: aws.String(tagKey),
|
|
TagValue: aws.String(tagValue),
|
|
})
|
|
}
|
|
createKeyInput := &kms.CreateKeyInput{
|
|
Description: aws.String("Constellation Key Encryption Key"),
|
|
Origin: origin,
|
|
Tags: tags,
|
|
}
|
|
kekMetadata, err := c.awsClient.CreateKey(ctx, createKeyInput)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Use the keyId of the created key in the following
|
|
awsGeneratedKeyID = *kekMetadata.KeyMetadata.KeyId
|
|
|
|
// Creates Alias for the KEK, so the key can be accessed by specifying the keyID
|
|
createAliasInput := &kms.CreateAliasInput{
|
|
AliasName: aws.String(alias),
|
|
TargetKeyId: &awsGeneratedKeyID,
|
|
}
|
|
if _, err = c.awsClient.CreateAlias(ctx, createAliasInput); err != nil {
|
|
c.tryCleanUpResources(ctx, newKeyCreationNeeded, awsGeneratedKeyID, alias)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Only import key if the key is not empty
|
|
if len(key) != 0 {
|
|
// Retrieves token and public AWS key to encrypt the KEK for transmitting to AWS KMS
|
|
getImportParameterInput := &kms.GetParametersForImportInput{
|
|
KeyId: &awsGeneratedKeyID,
|
|
// if supported, it is recommended to use 'RSAES_OAEP_SHA_256': https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-get-public-key-and-token.html
|
|
WrappingAlgorithm: types.AlgorithmSpecRsaesOaepSha256,
|
|
WrappingKeySpec: types.WrappingKeySpecRsa2048,
|
|
}
|
|
getParametersForImportOutput, err := c.awsClient.GetParametersForImport(ctx, getImportParameterInput)
|
|
if err != nil {
|
|
c.tryCleanUpResources(ctx, newKeyCreationNeeded, awsGeneratedKeyID, alias)
|
|
return err
|
|
}
|
|
|
|
// Encrypt the private key with the public key provided by AWS KMS
|
|
// From the AWS KMS get-public-key documentation:
|
|
// The value is a DER-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI), as defined in RFC 5280.
|
|
// When you use the HTTP API or the Amazon Web Services CLI, the value is Base64-encoded. Otherwise, it is not Base64-encoded.
|
|
publicKey, err := util.ParseDERtoPublicKeyRSA(getParametersForImportOutput.PublicKey)
|
|
if err != nil {
|
|
c.tryCleanUpResources(ctx, newKeyCreationNeeded, awsGeneratedKeyID, alias)
|
|
return err
|
|
}
|
|
encryptedKEK, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, key, nil)
|
|
if err != nil {
|
|
c.tryCleanUpResources(ctx, newKeyCreationNeeded, awsGeneratedKeyID, alias)
|
|
return err
|
|
}
|
|
|
|
// Pushes the key material for the created KEK to AWS KMS
|
|
// In case the key has already key material, the importKeyMaterial operation only succeeds if the newly imported key material matches the previous imported one
|
|
// Otherwise it responds with a IncorrectKeyMaterialException
|
|
importKeyMaterialInput := &kms.ImportKeyMaterialInput{
|
|
EncryptedKeyMaterial: encryptedKEK,
|
|
ImportToken: getParametersForImportOutput.ImportToken,
|
|
KeyId: &awsGeneratedKeyID,
|
|
ExpirationModel: types.ExpirationModelTypeKeyMaterialDoesNotExpire,
|
|
}
|
|
if _, err = c.awsClient.ImportKeyMaterial(ctx, importKeyMaterialInput); err != nil {
|
|
c.tryCleanUpResources(ctx, newKeyCreationNeeded, awsGeneratedKeyID, alias)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Pushes key policy for KEK
|
|
// Since the default policy of the KEK does not allow decryption for an IAM role, one has to include that in the key policy when importing the KEK.
|
|
// Decryption is needed for retrieving the DEKs from the storage.
|
|
policy, err := c.policyProducer.CreateKeyPolicy(awsGeneratedKeyID)
|
|
if err != nil {
|
|
c.tryCleanUpResources(ctx, newKeyCreationNeeded, awsGeneratedKeyID, alias)
|
|
return err
|
|
}
|
|
putKeyPolicyInput := &kms.PutKeyPolicyInput{
|
|
KeyId: &awsGeneratedKeyID,
|
|
Policy: &policy,
|
|
PolicyName: aws.String("default"),
|
|
}
|
|
if _, err = c.awsClient.PutKeyPolicy(ctx, putKeyPolicyInput); err != nil {
|
|
c.tryCleanUpResources(ctx, newKeyCreationNeeded, awsGeneratedKeyID, alias)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetDEK returns the DEK for dekID and kekID from the KMS.
|
|
func (c *KMSClient) GetDEK(ctx context.Context, kekID, keyID string, dekSize int) ([]byte, error) {
|
|
// The KEK should be identified by its alias. The alias always has the same scheme: 'alias/<kekId>'
|
|
kekID = "alias/" + kekID
|
|
|
|
// If a key for keyID exists in the storage, decrypt the key using the KEK.
|
|
dek, err := c.decryptDEKFromStorage(ctx, kekID, keyID)
|
|
if err == nil {
|
|
return dek, nil
|
|
}
|
|
if !errors.Is(err, storage.ErrDEKUnset) {
|
|
return nil, err
|
|
}
|
|
return c.putNewDEKToStorage(ctx, kekID, keyID, dekSize)
|
|
}
|
|
|
|
func (c *KMSClient) decryptDEKFromStorage(ctx context.Context, kekID, keyID string) ([]byte, error) {
|
|
encryptedKey, err := c.storage.Get(ctx, keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
decryptInput := &kms.DecryptInput{
|
|
CiphertextBlob: encryptedKey,
|
|
EncryptionContext: map[string]string{DEKContext: keyID},
|
|
KeyId: &kekID,
|
|
}
|
|
decryptOutput, err := c.awsClient.Decrypt(ctx, decryptInput)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return decryptOutput.Plaintext, nil
|
|
}
|
|
|
|
func (c *KMSClient) putNewDEKToStorage(ctx context.Context, kekID, keyID string, dekSize int) ([]byte, error) {
|
|
// GenerateDataKey always generates a new unique key, even if the input stays the same.
|
|
input := &kms.GenerateDataKeyInput{
|
|
KeyId: &kekID,
|
|
// The encryption context is used for encryption. It must be the same when decrypting the ciphertext output
|
|
EncryptionContext: map[string]string{DEKContext: keyID}, // https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context
|
|
NumberOfBytes: aws.Int32(int32(dekSize)),
|
|
}
|
|
output, err := c.awsClient.GenerateDataKey(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// store encrypted key in storage
|
|
if err := c.storage.Put(ctx, keyID, output.CiphertextBlob); err != nil {
|
|
return nil, err
|
|
}
|
|
return output.Plaintext, nil
|
|
}
|
|
|
|
func (c *KMSClient) tryCleanUpResources(ctx context.Context, generatedNewKey bool, awsGeneratedKeyID, alias string) {
|
|
if !generatedNewKey {
|
|
return
|
|
}
|
|
// Delete Alias
|
|
deleteAliasInput := &kms.DeleteAliasInput{
|
|
AliasName: &alias,
|
|
}
|
|
_, _ = c.awsClient.DeleteAlias(ctx, deleteAliasInput) // Might fail, ignoring the error.
|
|
|
|
// Delete Key
|
|
scheduleKeyDeletionInput := &kms.ScheduleKeyDeletionInput{
|
|
KeyId: &awsGeneratedKeyID,
|
|
PendingWindowInDays: aws.Int32(7),
|
|
}
|
|
_, _ = c.awsClient.ScheduleKeyDeletion(ctx, scheduleKeyDeletionInput) // Might fail, ignoring the error.
|
|
}
|