constellation/keyservice/kms/aws/aws_test.go
Otto Bittner 90b88e1cf9 kms: rename kms to keyservice
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.
2023-01-16 11:56:34 +01:00

520 lines
18 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package aws
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strconv"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"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/stretchr/testify/assert"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m,
// https://github.com/census-instrumentation/opencensus-go/issues/1262
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
)
}
const (
kekLen = 32
importedPublicKeyLen = 2048
)
// fakeAWSClient mocks the AWS KMS client.
type fakeAWSClient struct {
kekPool map[string][]byte
keyIDCount int
keyPairStruct *rsa.PrivateKey
}
// GenerateDataKey creates a new DEK.
func (m *fakeAWSClient) GenerateDataKey(ctx context.Context, params *kms.GenerateDataKeyInput, optFns ...func(*kms.Options)) (*kms.GenerateDataKeyOutput, error) {
if params.KeyId == nil {
return nil, fmt.Errorf("Missing paramerter KeyId")
}
kekID := *params.KeyId
if _, ok := m.kekPool[kekID]; !ok {
return nil, kmsInterface.ErrKEKUnknown
}
var keyLen int32
if params.NumberOfBytes != nil {
keyLen = *params.NumberOfBytes
} else {
switch params.KeySpec {
case types.DataKeySpecAes128:
keyLen = 128
case types.DataKeySpecAes256:
keyLen = 256
}
}
dek := make([]byte, keyLen)
// should not be random, but dependent on the context and the kekId
contextKeyID := params.EncryptionContext[DEKContext]
i := 0
for i < kekLen/2 {
dek[i] = contextKeyID[i%len(contextKeyID)]
i++
}
for i < kekLen {
dek[i] = []byte(*params.KeyId)[i%len(*params.KeyId)]
i++
}
output := &kms.GenerateDataKeyOutput{
CiphertextBlob: dek,
KeyId: &kekID,
Plaintext: dek,
}
return output, nil
}
// Listed in order called by CreateKEK
// DescribeKey takes an alias and searches in the kekPool whether this alias exists.
// If the alias exists, return the keyId the alias refers to (identifiable via the same key value). Otherwise, indicate that the key was not found.
func (m *fakeAWSClient) DescribeKey(ctx context.Context, params *kms.DescribeKeyInput, optFns ...func(*kms.Options)) (*kms.DescribeKeyOutput, error) {
if aliasKeyValue, ok := m.kekPool[*params.KeyId]; ok {
awsGeneratedKeyID := m.getIDPartnerByKeyValue(*params.KeyId, aliasKeyValue)
if awsGeneratedKeyID == "" {
return nil, errors.New("could not determine id partner")
}
describeKeyOutput := &kms.DescribeKeyOutput{
KeyMetadata: &types.KeyMetadata{
KeyId: &awsGeneratedKeyID,
},
}
return describeKeyOutput, nil
}
return nil, &types.NotFoundException{Message: aws.String("not found exception error")}
}
// CreateKey stores a key in fakeKMSStore.
func (m *fakeAWSClient) CreateKey(ctx context.Context, params *kms.CreateKeyInput, optFns ...func(*kms.Options)) (*kms.CreateKeyOutput, error) {
m.keyIDCount++
kekID := strconv.Itoa(m.keyIDCount)
if _, ok := m.kekPool[kekID]; ok {
return nil, errors.New("Key with this id already exists")
}
// Use m.keyIdCount to make the key values unique for the search in describeKey
kekValue := make([]byte, kekLen)
for i := 0; i < kekLen; i++ {
kekValue[i] = byte(m.keyIDCount)
}
m.kekPool[kekID] = kekValue
keyOutput := &kms.CreateKeyOutput{
KeyMetadata: &types.KeyMetadata{
KeyId: &kekID,
},
}
return keyOutput, nil
}
// CreateAlias changes keyId value to alias.
func (m *fakeAWSClient) CreateAlias(ctx context.Context, params *kms.CreateAliasInput, optFns ...func(*kms.Options)) (*kms.CreateAliasOutput, error) {
kekValue, ok := m.kekPool[*params.TargetKeyId]
if !ok {
return nil, kmsInterface.ErrKEKUnknown
}
// Copy the key bytes of the KEK to a new entry using the alias as keyId.
// As with the KMS, each key can be addressed using either its alias or the incremented keyId generated during creation
m.kekPool[*params.AliasName] = kekValue
return &kms.CreateAliasOutput{}, nil
}
// GetParametersForImport returns an empty public key and empty import token.
func (m *fakeAWSClient) GetParametersForImport(ctx context.Context, params *kms.GetParametersForImportInput, optFns ...func(*kms.Options)) (*kms.GetParametersForImportOutput, error) {
var err error
m.keyPairStruct, err = rsa.GenerateKey(rand.Reader, importedPublicKeyLen)
if err != nil {
return nil, errors.New("Error generating Key Pair")
}
var pki any = &m.keyPairStruct.PublicKey
publicKeyBytes, err := x509.MarshalPKIXPublicKey(pki)
if err != nil {
return nil, err
}
importToken := make([]byte, 1)
getParametersForImportOutput := &kms.GetParametersForImportOutput{
PublicKey: publicKeyBytes,
ImportToken: importToken,
}
return getParametersForImportOutput, nil
}
// ImportKeyMaterial stores the encrypted KEK in the KMS.
func (m *fakeAWSClient) ImportKeyMaterial(ctx context.Context, params *kms.ImportKeyMaterialInput, optFns ...func(*kms.Options)) (*kms.ImportKeyMaterialOutput, error) {
if _, ok := m.kekPool[*params.KeyId]; !ok {
return nil, kmsInterface.ErrKEKUnknown
}
// decrypt the encryptedKeyMaterial generated using RSAES_OAEP_SHA_256 to get the imported key bytes
kekValue, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, m.keyPairStruct, params.EncryptedKeyMaterial, nil)
if err != nil {
return nil, errors.New("Error decrypting the wrapped key")
}
// Set imported key bytes for keyId and alias
currentKeyValue := m.kekPool[*params.KeyId]
aliasID := m.getIDPartnerByKeyValue(*params.KeyId, currentKeyValue)
if aliasID == "" {
return nil, errors.New("could not determine id partner")
}
m.kekPool[aliasID] = kekValue
m.kekPool[*params.KeyId] = kekValue
return &kms.ImportKeyMaterialOutput{}, nil
}
// PutKeyPolicy sets the policy for an existing KEK in the kms.
func (m *fakeAWSClient) PutKeyPolicy(ctx context.Context, params *kms.PutKeyPolicyInput, optFns ...func(*kms.Options)) (*kms.PutKeyPolicyOutput, error) {
return &kms.PutKeyPolicyOutput{}, nil
}
// Decrypt performs decryption.
// Since the keys are saved in plain text during testing, decrypt returns the saved value.
func (m *fakeAWSClient) Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) {
decryptOutput := &kms.DecryptOutput{
Plaintext: params.CiphertextBlob,
}
return decryptOutput, nil
}
// getIDPartnerByKeyValue returns a the alternative name of a key, if it exists.
// For every key there should exist an alias after successful execution of CreateKEK.
// Both key and its alias are stored with the same key value.
// For consistency, they should be updated simultaneously, since they must store the same key value.
func (m *fakeAWSClient) getIDPartnerByKeyValue(keyID1 string, keyValue1 []byte) string {
// Search for the keyId with the same keyValue the input key has
for keyID2, keyValue2 := range m.kekPool {
if bytes.Equal(keyValue2, keyValue1) && keyID2 != keyID1 {
return keyID2
}
}
return ""
}
// DeleteAlias is a stub.
func (m *fakeAWSClient) DeleteAlias(ctx context.Context, params *kms.DeleteAliasInput, optFns ...func(*kms.Options)) (*kms.DeleteAliasOutput, error) {
return &kms.DeleteAliasOutput{}, nil
}
// ScheduleKeyDeletion is a stub.
func (m *fakeAWSClient) ScheduleKeyDeletion(ctx context.Context, params *kms.ScheduleKeyDeletionInput, optFns ...func(*kms.Options)) (*kms.ScheduleKeyDeletionOutput, error) {
return &kms.ScheduleKeyDeletionOutput{}, nil
}
// GenerateDataKeyWithoutPlaintext is a stub.
func (m *fakeAWSClient) GenerateDataKeyWithoutPlaintext(ctx context.Context, params *kms.GenerateDataKeyWithoutPlaintextInput, optFns ...func(*kms.Options)) (*kms.GenerateDataKeyWithoutPlaintextOutput, error) {
return nil, errors.New("Not implemented")
}
// Encrypt is a stub.
func (m *fakeAWSClient) Encrypt(ctx context.Context, params *kms.EncryptInput, optFns ...func(*kms.Options)) (*kms.EncryptOutput, error) {
return nil, errors.New("Not implemented")
}
type stubKeyPolicyProducer struct {
createKeyPolicyErr error
}
// CreateKeyPolicy creates a key policy.
func (m *stubKeyPolicyProducer) CreateKeyPolicy(keyID string) (string, error) {
return "", m.createKeyPolicyErr
}
func TestAWSKMSClient(t *testing.T) {
assert := assert.New(t)
awsClient := &fakeAWSClient{kekPool: make(map[string][]byte, 2)}
client := &KMSClient{
awsClient: awsClient,
policyProducer: &stubKeyPolicyProducer{},
storage: storage.NewMemMapStorage(),
}
awsClient.keyIDCount = -1
testKEK1ID := "testKEK1"
testKEK1 := []byte("test KEK")
testKEK2ID := "testKEK2"
testKEK2 := []byte("more test KEK")
ctx := context.Background()
// try to get a DEK before setting the KEK
_, err := client.GetDEK(ctx, testKEK1ID, "volume01", config.SymmetricKeyLength)
assert.Error(err)
assert.ErrorIs(err, kmsInterface.ErrKEKUnknown)
// test CreateKEK method
assert.NoError(client.CreateKEK(ctx, testKEK1ID, testKEK1))
assert.Equal(testKEK1, awsClient.kekPool[strconv.Itoa(awsClient.keyIDCount)])
// make sure that CreateKEK is idempotent
assert.NoError(client.CreateKEK(ctx, testKEK1ID, testKEK1))
assert.Equal(testKEK1, awsClient.kekPool[strconv.Itoa(awsClient.keyIDCount)])
// test setting a second KEK
assert.NoError(client.CreateKEK(ctx, testKEK2ID, testKEK2))
assert.Equal(testKEK2, awsClient.kekPool[strconv.Itoa(awsClient.keyIDCount)])
// test GetDEK method
dek1, err := client.GetDEK(ctx, testKEK1ID, "volume01", config.SymmetricKeyLength)
assert.NoError(err)
dek2, err := client.GetDEK(ctx, testKEK2ID, "volume02", config.SymmetricKeyLength)
assert.NoError(err)
// make sure that GetDEK is idempotent
dek1Copy, err := client.GetDEK(ctx, testKEK1ID, "volume01", config.SymmetricKeyLength)
assert.NoError(err)
assert.Equal(dek1, dek1Copy)
dek2Copy, err := client.GetDEK(ctx, testKEK2ID, "volume02", config.SymmetricKeyLength)
assert.NoError(err)
assert.Equal(dek2, dek2Copy)
}
func TestCreateKEK(t *testing.T) {
someErr := errors.New("error")
importKey := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
importPubKey, _ := pem.Decode([]byte(`-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu+OepfHCTiTi27nkTGke
dn+AIkiM1AIWWDwqfqG85aNulcj60mGQGXIYV8LoEVkyKOhYBIUmJUaVczB4ltqq
ZhR7l46RQw2vnv+XiUmfK555d4ZDInyjTusO69hE6tkuYKdXLlG1HzcrhJ254LE2
wXtE1Yf9DygOsWet+S32gmpfH2whUY1mRTdwW4zoY4c3qtmmWImhVVNr6qR8Z95X
Y49EteCoNIomQNEZH7EnMlBsh34L7doOsckh1aTvQcrJorQSrBkWKbdV6kvuBKZp
fLK0DZiOh9BwZCZANtOqgH3V+AuNk338iON8eKCFRjoiQ40YGM6xKH3E6PHVnuKt
uIO0MPvE0qdV8Lvs+nCCrvwP5sJKZuciM40ioEO1pV1y3491xIxYhx3OfN4gg2h8
cgdKob/R8qwxqTrfceO36FBFb1vXCUApsm5oy6WxmUtIUgoYhK+6JYpVWDyOJYwP
iMJhdJA65n2ZliN8NxEhsaFoMgw76BOiD0wkt/CKPmNbOm5MGS3/fiZCt6A6u3cn
Ubhn4tvjy/q5XzVqZtBeoseW2TyyrsAN53LBkSqag5tG/264CQDigQ6Y/OADOE2x
n08MyrFHIL/wFMscOvJo7c2Eo4EW1yXkEkAy5tF5PZgnfRObakj4gdqPeq18FNzc
Y+t5OxL3kL15VzY1Ob0d5cMCAwEAAQ==
-----END PUBLIC KEY-----`))
testCases := map[string]struct {
client *stubAWSClient
policyProducer KeyPolicyProducer
importKey []byte
cleanupRequired bool
wantErr bool
}{
"create new kek successful": {
client: &stubAWSClient{createKeyID: "key-id"},
policyProducer: &stubKeyPolicyProducer{},
},
"CreateKeyPolicy fails on existing": {
client: &stubAWSClient{},
policyProducer: &stubKeyPolicyProducer{createKeyPolicyErr: someErr},
wantErr: true,
},
"CreateKeyPolicy fails on new": {
client: &stubAWSClient{describeKeyErr: &types.NotFoundException{}},
policyProducer: &stubKeyPolicyProducer{createKeyPolicyErr: someErr},
cleanupRequired: true,
wantErr: true,
},
"PutKeyPolicy fails on new": {
client: &stubAWSClient{
describeKeyErr: &types.NotFoundException{},
putKeyPolicyErr: someErr,
createKeyID: "key-id",
},
policyProducer: &stubKeyPolicyProducer{},
cleanupRequired: true,
wantErr: true,
},
"CreateAlias fails on new": {
client: &stubAWSClient{
describeKeyErr: &types.NotFoundException{},
createAliasErr: someErr,
createKeyID: "key-id",
},
policyProducer: &stubKeyPolicyProducer{},
cleanupRequired: true,
wantErr: true,
},
"CreateKey fails on new": {
client: &stubAWSClient{describeKeyErr: &types.NotFoundException{}, createKeyErr: someErr},
policyProducer: &stubKeyPolicyProducer{},
wantErr: true,
},
"DescribeKey fails": {
client: &stubAWSClient{describeKeyErr: someErr},
policyProducer: &stubKeyPolicyProducer{},
wantErr: true,
},
"DescribeKey fails with not found error": {
client: &stubAWSClient{describeKeyErr: &types.NotFoundException{}},
policyProducer: &stubKeyPolicyProducer{},
},
"import kek successful": {
client: &stubAWSClient{getParametersForImportPubKey: importPubKey.Bytes},
policyProducer: &stubKeyPolicyProducer{},
importKey: importKey,
},
"GetParametersForImport fails on new": {
client: &stubAWSClient{
describeKeyErr: &types.NotFoundException{},
getParametersForImportErr: someErr,
createKeyID: "key-id",
},
policyProducer: &stubKeyPolicyProducer{},
importKey: importKey,
cleanupRequired: true,
wantErr: true,
},
"ImportKeyMaterial fails on new": {
client: &stubAWSClient{
describeKeyErr: &types.NotFoundException{},
importKeyMaterialErr: someErr,
createKeyID: "key-id",
},
policyProducer: &stubKeyPolicyProducer{},
importKey: importKey,
cleanupRequired: true,
wantErr: true,
},
"GetParametersForImport fails on existing": {
client: &stubAWSClient{getParametersForImportErr: someErr},
policyProducer: &stubKeyPolicyProducer{},
importKey: importKey,
wantErr: true,
},
"ImportKeyMaterial fails on existing": {
client: &stubAWSClient{importKeyMaterialErr: someErr},
policyProducer: &stubKeyPolicyProducer{},
importKey: importKey,
wantErr: true,
},
"errors during cleanup don't stop execution": {
client: &stubAWSClient{
describeKeyErr: &types.NotFoundException{},
deleteAliasErr: someErr,
createKeyID: "key-id",
},
policyProducer: &stubKeyPolicyProducer{createKeyPolicyErr: someErr},
cleanupRequired: true,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
client := KMSClient{
awsClient: tc.client,
storage: storage.NewMemMapStorage(),
policyProducer: tc.policyProducer,
}
err := client.CreateKEK(context.Background(), "test-key", tc.importKey)
if tc.wantErr {
assert.Error(err)
if tc.cleanupRequired {
assert.True(tc.client.cleanUpCalled, "failed to clean up")
} else {
assert.False(tc.client.cleanUpCalled, "cleaned up when not necessary")
}
} else {
assert.NoError(err)
}
})
}
}
type stubAWSClient struct {
cleanUpCalled bool
createAliasErr error
createKeyErr error
createKeyID string
decryptErr error
deleteAliasErr error
describeKeyErr error
encryptErr error
generateDataKeyErr error
generateDataKeyWithoutPlaintextErr error
getParametersForImportErr error
getParametersForImportPubKey []byte
importKeyMaterialErr error
putKeyPolicyErr error
scheduleKeyDeletionErr error
}
func (s *stubAWSClient) CreateAlias(ctx context.Context, params *kms.CreateAliasInput, optFns ...func(*kms.Options)) (*kms.CreateAliasOutput, error) {
return &kms.CreateAliasOutput{}, s.createAliasErr
}
func (s *stubAWSClient) CreateKey(ctx context.Context, params *kms.CreateKeyInput, optFns ...func(*kms.Options)) (*kms.CreateKeyOutput, error) {
return &kms.CreateKeyOutput{KeyMetadata: &types.KeyMetadata{KeyId: aws.String(s.createKeyID)}}, s.createKeyErr
}
func (s *stubAWSClient) Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) {
return &kms.DecryptOutput{}, s.decryptErr
}
func (s *stubAWSClient) DeleteAlias(ctx context.Context, params *kms.DeleteAliasInput, optFns ...func(*kms.Options)) (*kms.DeleteAliasOutput, error) {
return &kms.DeleteAliasOutput{}, s.deleteAliasErr
}
func (s *stubAWSClient) DescribeKey(ctx context.Context, params *kms.DescribeKeyInput, optFns ...func(*kms.Options)) (*kms.DescribeKeyOutput, error) {
return &kms.DescribeKeyOutput{KeyMetadata: &types.KeyMetadata{KeyId: params.KeyId}}, s.describeKeyErr
}
func (s *stubAWSClient) Encrypt(ctx context.Context, params *kms.EncryptInput, optFns ...func(*kms.Options)) (*kms.EncryptOutput, error) {
return &kms.EncryptOutput{}, s.encryptErr
}
func (s *stubAWSClient) GenerateDataKey(ctx context.Context, params *kms.GenerateDataKeyInput, optFns ...func(*kms.Options)) (*kms.GenerateDataKeyOutput, error) {
return &kms.GenerateDataKeyOutput{}, s.generateDataKeyErr
}
func (s *stubAWSClient) GenerateDataKeyWithoutPlaintext(ctx context.Context, params *kms.GenerateDataKeyWithoutPlaintextInput, optFns ...func(*kms.Options)) (*kms.GenerateDataKeyWithoutPlaintextOutput, error) {
return &kms.GenerateDataKeyWithoutPlaintextOutput{}, s.generateDataKeyWithoutPlaintextErr
}
func (s *stubAWSClient) GetParametersForImport(ctx context.Context, params *kms.GetParametersForImportInput, optFns ...func(*kms.Options)) (*kms.GetParametersForImportOutput, error) {
return &kms.GetParametersForImportOutput{
PublicKey: s.getParametersForImportPubKey,
}, s.getParametersForImportErr
}
func (s *stubAWSClient) ImportKeyMaterial(ctx context.Context, params *kms.ImportKeyMaterialInput, optFns ...func(*kms.Options)) (*kms.ImportKeyMaterialOutput, error) {
return &kms.ImportKeyMaterialOutput{}, s.importKeyMaterialErr
}
func (s *stubAWSClient) PutKeyPolicy(ctx context.Context, params *kms.PutKeyPolicyInput, optFns ...func(*kms.Options)) (*kms.PutKeyPolicyOutput, error) {
return &kms.PutKeyPolicyOutput{}, s.putKeyPolicyErr
}
func (s *stubAWSClient) ScheduleKeyDeletion(ctx context.Context, params *kms.ScheduleKeyDeletionInput, optFns ...func(*kms.Options)) (*kms.ScheduleKeyDeletionOutput, error) {
s.cleanUpCalled = true
return &kms.ScheduleKeyDeletionOutput{}, s.scheduleKeyDeletionErr
}