feat:(kms) encrypt data with DEK using AES-GCM instead of AES-CBC

Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
Anish Ramasekar 2022-07-13 17:14:50 +00:00
parent 12004ea53d
commit d54631a41a
No known key found for this signature in database
GPG Key ID: F1F7F3518F1ECB0C
3 changed files with 57 additions and 57 deletions

View File

@ -369,8 +369,10 @@ func secretboxPrefixTransformer(config *apiserverconfig.SecretboxConfiguration)
func envelopePrefixTransformer(config *apiserverconfig.KMSConfiguration, envelopeService envelope.Service, prefix string) (value.PrefixTransformer, error) { func envelopePrefixTransformer(config *apiserverconfig.KMSConfiguration, envelopeService envelope.Service, prefix string) (value.PrefixTransformer, error) {
baseTransformerFunc := func(block cipher.Block) value.Transformer { baseTransformerFunc := func(block cipher.Block) value.Transformer {
// v1.24: write using AES-CBC only but support reads via AES-CBC and AES-GCM (so we can move to AES-GCM) // v1.24: write using AES-CBC only but support reads via AES-CBC and AES-GCM (so we can move to AES-GCM)
// TODO(aramase): swap this ordering in v1.25 // v1.25: write using AES-GCM only but support reads via AES-GCM and fallback to AES-CBC for backwards compatibility
return unionTransformers{aestransformer.NewCBCTransformer(block), aestransformer.NewGCMTransformer(block)} // TODO(aramase): Post v1.25: We cannot drop CBC read support until we automate storage migration.
// We could have a release note that hard requires users to perform storage migration.
return unionTransformers{aestransformer.NewGCMTransformer(block), aestransformer.NewCBCTransformer(block)}
} }
envelopeTransformer, err := envelope.NewEnvelopeTransformer(envelopeService, int(*config.CacheSize), baseTransformerFunc) envelopeTransformer, err := envelope.NewEnvelopeTransformer(envelopeService, int(*config.CacheSize), baseTransformerFunc)

View File

@ -25,7 +25,6 @@ import (
"crypto/aes" "crypto/aes"
"encoding/base64" "encoding/base64"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@ -87,10 +86,10 @@ func (r envelope) plainTextPayload(secretETCDPath string) ([]byte, error) {
// etcd path of the key is used as the authenticated context - need to pass it to decrypt // etcd path of the key is used as the authenticated context - need to pass it to decrypt
ctx := context.Background() ctx := context.Background()
dataCtx := value.DefaultContext([]byte(secretETCDPath)) dataCtx := value.DefaultContext([]byte(secretETCDPath))
aescbcTransformer := aestransformer.NewCBCTransformer(block) aesgcmTransformer := aestransformer.NewGCMTransformer(block)
plainSecret, _, err := aescbcTransformer.TransformFromStorage(ctx, r.cipherTextPayload(), dataCtx) plainSecret, _, err := aesgcmTransformer.TransformFromStorage(ctx, r.cipherTextPayload(), dataCtx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to transform from storage via AESCBC, err: %w", err) return nil, fmt.Errorf("failed to transform from storage via AESGCM, err: %w", err)
} }
return plainSecret, nil return plainSecret, nil
@ -103,10 +102,10 @@ func (r envelope) plainTextPayload(secretETCDPath string) ([]byte, error) {
// 3. KMS gRPC Plugin should encrypt the DEK with a Key Encryption Key (KEK) and pass it back to envelopeTransformer // 3. KMS gRPC Plugin should encrypt the DEK with a Key Encryption Key (KEK) and pass it back to envelopeTransformer
// 4. The cipherTextPayload (ex. Secret) should be encrypted via AES CBC transform // 4. The cipherTextPayload (ex. Secret) should be encrypted via AES CBC transform
// 5. Prefix-EncryptedDEK-EncryptedPayload structure should be deposited to ETCD // 5. Prefix-EncryptedDEK-EncryptedPayload structure should be deposited to ETCD
// 6. Direct AES CBC decryption of the cipherTextPayload written with AES GCM transform does not work // 6. Direct AES GCM decryption of the cipherTextPayload written with AES CBC transform does not work
// 7. AES GCM secrets should be un-enveloped on direct reads from Kube API Server // 7. Existing AES CBC secrets should be un-enveloped on direct reads from Kube API Server
// 8. No-op updates to the secret should cause new AES CBC key to be used // 8. No-op updates to the secret should cause new AES GCM key to be used
// 9. Direct AES CBC decryption works after the new AES CBC key is used // 9. Direct AES GCM decryption works after the new AES GCM key is used
func TestKMSProvider(t *testing.T) { func TestKMSProvider(t *testing.T) {
encryptionConfig := ` encryptionConfig := `
kind: EncryptionConfiguration kind: EncryptionConfiguration
@ -194,90 +193,89 @@ resources:
t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey])) t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
} }
// write data using AES GCM to simulate a downgrade // write data using AES CBC to simulate a downgrade
futureSecretBytes, err := base64.StdEncoding.DecodeString(futureSecret) oldSecretBytes, err := base64.StdEncoding.DecodeString(oldSecret)
if err != nil { if err != nil {
t.Fatalf("failed to base64 decode future secret, err: %v", err) t.Fatalf("failed to base64 decode old secret, err: %v", err)
} }
futureKeyBytes, err := base64.StdEncoding.DecodeString(futureAESGCMKey) oldKeyBytes, err := base64.StdEncoding.DecodeString(oldAESCBCKey)
if err != nil { if err != nil {
t.Fatalf("failed to base64 decode future key, err: %v", err) t.Fatalf("failed to base64 decode old key, err: %v", err)
} }
block, err := aes.NewCipher(futureKeyBytes) block, err := aes.NewCipher(oldKeyBytes)
if err != nil { if err != nil {
t.Fatalf("invalid key, err: %v", err) t.Fatalf("invalid key, err: %v", err)
} }
// we cannot precompute this because the authenticated data changes per run oldEncryptedSecretBytes, err := aestransformer.NewCBCTransformer(block).TransformToStorage(ctx, oldSecretBytes, value.DefaultContext(secretETCDPath))
futureEncryptedSecretBytes, err := aestransformer.NewGCMTransformer(block).TransformToStorage(ctx, futureSecretBytes, value.DefaultContext(secretETCDPath))
if err != nil { if err != nil {
t.Fatalf("failed to encrypt future secret, err: %v", err) t.Fatalf("failed to encrypt old secret, err: %v", err)
} }
futureEncryptedSecretBuf := cryptobyte.NewBuilder(nil) oldEncryptedSecretBuf := cryptobyte.NewBuilder(nil)
futureEncryptedSecretBuf.AddBytes([]byte(wantPrefix)) oldEncryptedSecretBuf.AddBytes([]byte(wantPrefix))
futureEncryptedSecretBuf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { oldEncryptedSecretBuf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes([]byte(futureAESGCMKey)) b.AddBytes([]byte(oldAESCBCKey))
}) })
futureEncryptedSecretBuf.AddBytes(futureEncryptedSecretBytes) oldEncryptedSecretBuf.AddBytes(oldEncryptedSecretBytes)
_, err = test.writeRawRecordToETCD(secretETCDPath, futureEncryptedSecretBuf.BytesOrPanic()) _, err = test.writeRawRecordToETCD(secretETCDPath, oldEncryptedSecretBuf.BytesOrPanic())
if err != nil { if err != nil {
t.Fatalf("failed to write future encrypted secret, err: %v", err) t.Fatalf("failed to write old encrypted secret, err: %v", err)
} }
// confirm that direct AES CBC decryption does not work // confirm that direct AES GCM decryption does not work
failingRawEnvelope, err := test.getRawSecretFromETCD() failingRawEnvelope, err := test.getRawSecretFromETCD()
if err != nil { if err != nil {
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
} }
failingFutureEnvelope := envelope{ failingOldEnvelope := envelope{
providerName: providerName, providerName: providerName,
rawEnvelope: failingRawEnvelope, rawEnvelope: failingRawEnvelope,
plainTextDEK: futureKeyBytes, plainTextDEK: oldKeyBytes,
} }
failingFuturePlainSecret, err := failingFutureEnvelope.plainTextPayload(secretETCDPath) failingOldPlainSecret, err := failingOldEnvelope.plainTextPayload(secretETCDPath)
if err == nil || !errors.Is(err, aestransformer.ErrInvalidBlockSize) { if err == nil {
t.Fatalf("AESCBC decryption failure not seen, err: %v, data: %s", err, string(failingFuturePlainSecret)) t.Fatalf("AESGCM decryption failure not seen, data: %s", string(failingOldPlainSecret))
} }
// AES GCM secrets should be un-enveloped on direct reads from Kube API Server. // Existing AES CBC secrets should be un-enveloped on direct reads from Kube API Server.
futureSecretObj, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{}) oldSecretObj, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{})
if err != nil { if err != nil {
t.Fatalf("failed to read future secret via Kube API, err: %v", err) t.Fatalf("failed to read old secret via Kube API, err: %v", err)
} }
if futureSecretVal != string(futureSecretObj.Data[secretKey]) { if oldSecretVal != string(oldSecretObj.Data[secretKey]) {
t.Fatalf("expected %s from KubeAPI, but got %s", futureSecretVal, string(futureSecretObj.Data[secretKey])) t.Fatalf("expected %s from KubeAPI, but got %s", oldSecretVal, string(oldSecretObj.Data[secretKey]))
} }
// no-op update should cause new AES CBC key to be used // no-op update should cause new AES GCM key to be used
futureSecretUpdated, err := secretClient.Update(ctx, futureSecretObj, metav1.UpdateOptions{}) oldSecretUpdated, err := secretClient.Update(ctx, oldSecretObj, metav1.UpdateOptions{})
if err != nil { if err != nil {
t.Fatalf("failed to update future secret via Kube API, err: %v", err) t.Fatalf("failed to update old secret via Kube API, err: %v", err)
} }
if futureSecretObj.ResourceVersion == futureSecretUpdated.ResourceVersion { if oldSecretObj.ResourceVersion == oldSecretUpdated.ResourceVersion {
t.Fatalf("future secret not updated on no-op write: %s", futureSecretObj.ResourceVersion) t.Fatalf("old secret not updated on no-op write: %s", oldSecretObj.ResourceVersion)
} }
// confirm that direct AES CBC decryption works // confirm that direct AES GCM decryption works
futureRawEnvelope, err := test.getRawSecretFromETCD() oldRawEnvelope, err := test.getRawSecretFromETCD()
if err != nil { if err != nil {
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
} }
futureEnvelope := envelope{ oldEnvelope := envelope{
providerName: providerName, providerName: providerName,
rawEnvelope: futureRawEnvelope, rawEnvelope: oldRawEnvelope,
plainTextDEK: pluginMock.LastEncryptRequest(), plainTextDEK: pluginMock.LastEncryptRequest(),
} }
if !bytes.HasPrefix(futureRawEnvelope, []byte(wantPrefix)) { if !bytes.HasPrefix(oldRawEnvelope, []byte(wantPrefix)) {
t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, futureRawEnvelope) t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, oldRawEnvelope)
} }
futurePlainSecret, err := futureEnvelope.plainTextPayload(secretETCDPath) oldPlainSecret, err := oldEnvelope.plainTextPayload(secretETCDPath)
if err != nil { if err != nil {
t.Fatalf("failed to transform from storage via AESCBC, err: %v", err) t.Fatalf("failed to transform from storage via AESGCM, err: %v", err)
} }
if !strings.Contains(string(futurePlainSecret), futureSecretVal) { if !strings.Contains(string(oldPlainSecret), oldSecretVal) {
t.Fatalf("expected %q after decryption, but got %q", futureSecretVal, string(futurePlainSecret)) t.Fatalf("expected %q after decryption, but got %q", oldSecretVal, string(oldPlainSecret))
} }
} }

View File

@ -51,11 +51,11 @@ const (
testSecret = "test-secret" testSecret = "test-secret"
metricsPrefix = "apiserver_storage_" metricsPrefix = "apiserver_storage_"
// precomputed key and secret for use with AES GCM // precomputed key and secret for use with AES CBC
// this looks exactly the same as the AES CBC secret but with a different value // this looks exactly the same as the AES GCM secret but with a different value
futureAESGCMKey = "e0/+tts8FS254BZimFZWtUsOCOUDSkvzB72PyimMlkY=" oldAESCBCKey = "e0/+tts8FS254BZimFZWtUsOCOUDSkvzB72PyimMlkY="
futureSecret = "azhzAAoMCgJ2MRIGU2VjcmV0En4KXwoLdGVzdC1zZWNyZXQSABoWc2VjcmV0LWVuY3J5cHRpb24tdGVzdCIAKiQ3MmRmZTVjNC0xNDU2LTQyMzktYjFlZC1hZGZmYTJmMWY3YmEyADgAQggI5Jy/7wUQAHoAEhMKB2FwaV9rZXkSCPCfpJfwn5C8GgZPcGFxdWUaACIA" oldSecret = "azhzAAoMCgJ2MRIGU2VjcmV0En4KXwoLdGVzdC1zZWNyZXQSABoWc2VjcmV0LWVuY3J5cHRpb24tdGVzdCIAKiQ3MmRmZTVjNC0xNDU2LTQyMzktYjFlZC1hZGZmYTJmMWY3YmEyADgAQggI5Jy/7wUQAHoAEhMKB2FwaV9rZXkSCPCfpJfwn5C8GgZPcGFxdWUaACIA"
futureSecretVal = "\xf0\x9f\xa4\x97\xf0\x9f\x90\xbc" oldSecretVal = "\xf0\x9f\xa4\x97\xf0\x9f\x90\xbc"
) )
type unSealSecret func(ctx context.Context, cipherText []byte, dataCtx value.Context, config apiserverconfigv1.ProviderConfiguration) ([]byte, error) type unSealSecret func(ctx context.Context, cipherText []byte, dataCtx value.Context, config apiserverconfigv1.ProviderConfiguration) ([]byte, error)