kmsv2: KDF based nonce extension

Signed-off-by: Monis Khan <mok@microsoft.com>

Kubernetes-commit: bf49c727ba10881d5378e9242f31dc00dede51be
This commit is contained in:
Monis Khan 2023-03-25 14:41:04 -04:00 committed by Kubernetes Publisher
parent cf66e8fde8
commit 8e93c650b5
15 changed files with 1069 additions and 303 deletions

View File

@ -125,6 +125,13 @@ const (
// Enables KMS v2 API for encryption at rest. // Enables KMS v2 API for encryption at rest.
KMSv2 featuregate.Feature = "KMSv2" KMSv2 featuregate.Feature = "KMSv2"
// owner: @enj
// kep: https://kep.k8s.io/3299
// beta: v1.28
//
// Enables the use of derived encryption keys with KMS v2.
KMSv2KDF featuregate.Feature = "KMSv2KDF"
// owner: @jiahuif // owner: @jiahuif
// kep: https://kep.k8s.io/2887 // kep: https://kep.k8s.io/2887
// alpha: v1.23 // alpha: v1.23
@ -251,6 +258,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
KMSv2: {Default: true, PreRelease: featuregate.Beta}, KMSv2: {Default: true, PreRelease: featuregate.Beta},
KMSv2KDF: {Default: false, PreRelease: featuregate.Beta}, // default and lock to true in 1.29, remove in 1.31
OpenAPIEnums: {Default: true, PreRelease: featuregate.Beta}, OpenAPIEnums: {Default: true, PreRelease: featuregate.Beta},
OpenAPIV3: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.29 OpenAPIV3: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.29

View File

@ -47,6 +47,7 @@ import (
aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes" aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope" "k8s.io/apiserver/pkg/storage/value/encrypt/envelope"
envelopekmsv2 "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2" envelopekmsv2 "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2"
kmstypes "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2/v2"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope/metrics" "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/metrics"
"k8s.io/apiserver/pkg/storage/value/encrypt/identity" "k8s.io/apiserver/pkg/storage/value/encrypt/identity"
"k8s.io/apiserver/pkg/storage/value/encrypt/secretbox" "k8s.io/apiserver/pkg/storage/value/encrypt/secretbox"
@ -63,13 +64,13 @@ const (
kmsTransformerPrefixV2 = "k8s:enc:kms:v2:" kmsTransformerPrefixV2 = "k8s:enc:kms:v2:"
// these constants relate to how the KMS v2 plugin status poll logic // these constants relate to how the KMS v2 plugin status poll logic
// and the DEK generation logic behave. In particular, the positive // and the DEK/seed generation logic behave. In particular, the positive
// interval and max TTL are closely related as the difference between // interval and max TTL are closely related as the difference between
// these values defines the worst case window in which the write DEK // these values defines the worst case window in which the write DEK/seed
// could expire due to the plugin going into an error state. The // could expire due to the plugin going into an error state. The
// worst case window divided by the negative interval defines the // worst case window divided by the negative interval defines the
// minimum amount of times the server will attempt to return to a // minimum amount of times the server will attempt to return to a
// healthy state before the DEK expires and writes begin to fail. // healthy state before the DEK/seed expires and writes begin to fail.
// //
// For now, these values are kept small and hardcoded to support being // For now, these values are kept small and hardcoded to support being
// able to perform a "passive" storage migration while tolerating some // able to perform a "passive" storage migration while tolerating some
@ -82,13 +83,13 @@ const (
// At that point, they are guaranteed to either migrate to the new key // At that point, they are guaranteed to either migrate to the new key
// or get errors during the migration. // or get errors during the migration.
// //
// If the API server coasted forever on the last DEK, they would need // If the API server coasted forever on the last DEK/seed, they would need
// to actively check if it had observed the new key ID before starting // to actively check if it had observed the new key ID before starting
// a migration - otherwise it could keep using the old DEK and their // a migration - otherwise it could keep using the old DEK/seed and their
// storage migration would not do what they thought it did. // storage migration would not do what they thought it did.
kmsv2PluginHealthzPositiveInterval = 1 * time.Minute kmsv2PluginHealthzPositiveInterval = 1 * time.Minute
kmsv2PluginHealthzNegativeInterval = 10 * time.Second kmsv2PluginHealthzNegativeInterval = 10 * time.Second
kmsv2PluginWriteDEKMaxTTL = 3 * time.Minute kmsv2PluginWriteDEKSourceMaxTTL = 3 * time.Minute
kmsPluginHealthzNegativeTTL = 3 * time.Second kmsPluginHealthzNegativeTTL = 3 * time.Second
kmsPluginHealthzPositiveTTL = 20 * time.Second kmsPluginHealthzPositiveTTL = 20 * time.Second
@ -332,8 +333,8 @@ func (h *kmsv2PluginProbe) check(ctx context.Context) error {
return nil return nil
} }
// rotateDEKOnKeyIDChange tries to rotate to a new DEK if the key ID returned by Status does not match the // rotateDEKOnKeyIDChange tries to rotate to a new DEK/seed if the key ID returned by Status does not match the
// current state. If a successful rotation is performed, the new DEK and keyID overwrite the existing state. // current state. If a successful rotation is performed, the new DEK/seed and keyID overwrite the existing state.
// On any failure during rotation (including mismatch between status and encrypt calls), the current state is // On any failure during rotation (including mismatch between status and encrypt calls), the current state is
// preserved and will remain valid to use for encryption until its expiration (the system attempts to coast). // preserved and will remain valid to use for encryption until its expiration (the system attempts to coast).
// If the key ID returned by Status matches the current state, the expiration of the current state is extended // If the key ID returned by Status matches the current state, the expiration of the current state is extended
@ -346,32 +347,38 @@ func (h *kmsv2PluginProbe) rotateDEKOnKeyIDChange(ctx context.Context, statusKey
// allow reads indefinitely in all cases // allow reads indefinitely in all cases
// allow writes indefinitely as long as there is no error // allow writes indefinitely as long as there is no error
// allow writes for only up to kmsv2PluginWriteDEKMaxTTL from now when there are errors // allow writes for only up to kmsv2PluginWriteDEKSourceMaxTTL from now when there are errors
// we start the timer before we make the network call because kmsv2PluginWriteDEKMaxTTL is meant to be the upper bound // we start the timer before we make the network call because kmsv2PluginWriteDEKSourceMaxTTL is meant to be the upper bound
expirationTimestamp := envelopekmsv2.NowFunc().Add(kmsv2PluginWriteDEKMaxTTL) expirationTimestamp := envelopekmsv2.NowFunc().Add(kmsv2PluginWriteDEKSourceMaxTTL)
// state is valid and status keyID is unchanged from when we generated this DEK so there is no need to rotate it // dynamically check if we want to use KDF seed to derive DEKs or just a single DEK
// this gate can only change during tests, but the check is cheap enough to always make
// this allows us to easily exercise both modes without restarting the API server
// TODO integration test that this dynamically takes effect
useSeed := utilfeature.DefaultFeatureGate.Enabled(features.KMSv2KDF)
stateUseSeed := state.EncryptedObject.EncryptedDEKSourceType == kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED
// state is valid and status keyID is unchanged from when we generated this DEK/seed so there is no need to rotate it
// just move the expiration of the current state forward by the reuse interval // just move the expiration of the current state forward by the reuse interval
if errState == nil && state.KeyID == statusKeyID { // useSeed can only change at runtime during tests, so we check it here to allow us to easily exercise both modes
if errState == nil && state.EncryptedObject.KeyID == statusKeyID && stateUseSeed == useSeed {
state.ExpirationTimestamp = expirationTimestamp state.ExpirationTimestamp = expirationTimestamp
h.state.Store(&state) h.state.Store(&state)
return nil return nil
} }
transformer, resp, cacheKey, errGen := envelopekmsv2.GenerateTransformer(ctx, uid, h.service) transformer, encObject, cacheKey, errGen := envelopekmsv2.GenerateTransformer(ctx, uid, h.service, useSeed)
if resp == nil { if encObject == nil {
resp = &kmsservice.EncryptResponse{} // avoid nil panics encObject = &kmstypes.EncryptedObject{} // avoid nil panics
} }
// happy path, should be the common case // happy path, should be the common case
// TODO maybe add success metrics? // TODO maybe add success metrics?
if errGen == nil && resp.KeyID == statusKeyID { if errGen == nil && encObject.KeyID == statusKeyID {
h.state.Store(&envelopekmsv2.State{ h.state.Store(&envelopekmsv2.State{
Transformer: transformer, Transformer: transformer,
EncryptedDEK: resp.Ciphertext, EncryptedObject: *encObject,
KeyID: resp.KeyID,
Annotations: resp.Annotations,
UID: uid, UID: uid,
ExpirationTimestamp: expirationTimestamp, ExpirationTimestamp: expirationTimestamp,
CacheKey: cacheKey, CacheKey: cacheKey,
@ -384,8 +391,9 @@ func (h *kmsv2PluginProbe) rotateDEKOnKeyIDChange(ctx context.Context, statusKey
if klogV6.Enabled() { if klogV6.Enabled() {
klogV6.InfoS("successfully rotated DEK", klogV6.InfoS("successfully rotated DEK",
"uid", uid, "uid", uid,
"newKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(resp.KeyID), "useSeed", useSeed,
"oldKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(state.KeyID), "newKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(encObject.KeyID),
"oldKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(state.EncryptedObject.KeyID),
"expirationTimestamp", expirationTimestamp.Format(time.RFC3339), "expirationTimestamp", expirationTimestamp.Format(time.RFC3339),
) )
} }
@ -393,8 +401,8 @@ func (h *kmsv2PluginProbe) rotateDEKOnKeyIDChange(ctx context.Context, statusKey
} }
} }
return fmt.Errorf("failed to rotate DEK uid=%q, errState=%v, errGen=%v, statusKeyIDHash=%q, encryptKeyIDHash=%q, stateKeyIDHash=%q, expirationTimestamp=%s", return fmt.Errorf("failed to rotate DEK uid=%q, useSeed=%v, errState=%v, errGen=%v, statusKeyIDHash=%q, encryptKeyIDHash=%q, stateKeyIDHash=%q, expirationTimestamp=%s",
uid, errState, errGen, envelopekmsv2.GetHashIfNotEmpty(statusKeyID), envelopekmsv2.GetHashIfNotEmpty(resp.KeyID), envelopekmsv2.GetHashIfNotEmpty(state.KeyID), state.ExpirationTimestamp.Format(time.RFC3339)) uid, useSeed, errState, errGen, envelopekmsv2.GetHashIfNotEmpty(statusKeyID), envelopekmsv2.GetHashIfNotEmpty(encObject.KeyID), envelopekmsv2.GetHashIfNotEmpty(state.EncryptedObject.KeyID), state.ExpirationTimestamp.Format(time.RFC3339))
} }
// getCurrentState returns the latest state from the last status and encrypt calls. // getCurrentState returns the latest state from the last status and encrypt calls.
@ -407,12 +415,13 @@ func (h *kmsv2PluginProbe) getCurrentState() (envelopekmsv2.State, error) {
return envelopekmsv2.State{}, fmt.Errorf("got unexpected nil transformer") return envelopekmsv2.State{}, fmt.Errorf("got unexpected nil transformer")
} }
if len(state.EncryptedDEK) == 0 { encryptedObjectCopy := state.EncryptedObject
return envelopekmsv2.State{}, fmt.Errorf("got unexpected empty EncryptedDEK") if len(encryptedObjectCopy.EncryptedData) != 0 {
return envelopekmsv2.State{}, fmt.Errorf("got unexpected non-empty EncryptedData")
} }
encryptedObjectCopy.EncryptedData = []byte{0} // any non-empty value to pass validation
if len(state.KeyID) == 0 { if err := envelopekmsv2.ValidateEncryptedObject(&encryptedObjectCopy); err != nil {
return envelopekmsv2.State{}, fmt.Errorf("got unexpected empty keyID") return envelopekmsv2.State{}, fmt.Errorf("got invalid EncryptedObject: %w", err)
} }
if state.ExpirationTimestamp.IsZero() { if state.ExpirationTimestamp.IsZero() {
@ -772,7 +781,7 @@ func primeAndProbeKMSv2(ctx context.Context, probe *kmsv2PluginProbe, kmsName st
// make sure that the plugin's key ID is reasonably up-to-date // make sure that the plugin's key ID is reasonably up-to-date
// also, make sure that our DEK is up-to-date to with said key ID (if it expires the server will fail all writes) // also, make sure that our DEK is up-to-date to with said key ID (if it expires the server will fail all writes)
// if this background loop ever stops running, the server will become unfunctional after kmsv2PluginWriteDEKMaxTTL // if this background loop ever stops running, the server will become unfunctional after kmsv2PluginWriteDEKSourceMaxTTL
go wait.PollUntilWithContext( go wait.PollUntilWithContext(
ctx, ctx,
kmsv2PluginHealthzPositiveInterval, kmsv2PluginHealthzPositiveInterval,

View File

@ -39,6 +39,7 @@ import (
"k8s.io/apiserver/pkg/storage/value" "k8s.io/apiserver/pkg/storage/value"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope" "k8s.io/apiserver/pkg/storage/value/encrypt/envelope"
envelopekmsv2 "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2" envelopekmsv2 "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2"
kmstypes "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2/v2"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope/metrics" "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/metrics"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
@ -606,7 +607,7 @@ func TestKMSPluginHealthz(t *testing.T) {
ttl: 3 * time.Second, ttl: 3 * time.Second,
} }
keyID := "1" keyID := "1"
kmsv2Probe.state.Store(&envelopekmsv2.State{KeyID: keyID}) kmsv2Probe.state.Store(&envelopekmsv2.State{EncryptedObject: kmstypes.EncryptedObject{KeyID: keyID}})
testCases := []struct { testCases := []struct {
desc string desc string
@ -1711,6 +1712,7 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
name string name string
service *testKMSv2EnvelopeService service *testKMSv2EnvelopeService
state envelopekmsv2.State state envelopekmsv2.State
useSeed bool
statusKeyID string statusKeyID string
wantState envelopekmsv2.State wantState envelopekmsv2.State
wantEncryptCalls int wantEncryptCalls int
@ -1723,13 +1725,13 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: envelopekmsv2.State{}, state: envelopekmsv2.State{},
statusKeyID: "1", statusKeyID: "1",
wantState: envelopekmsv2.State{ wantState: envelopekmsv2.State{
KeyID: "1", EncryptedObject: kmstypes.EncryptedObject{KeyID: "1"},
ExpirationTimestamp: now.Add(3 * time.Minute), ExpirationTimestamp: now.Add(3 * time.Minute),
}, },
wantEncryptCalls: 1, wantEncryptCalls: 1,
wantLogs: []string{ wantLogs: []string{
`"encrypting content using envelope service" uid="panda"`, `"encrypting content using envelope service" uid="panda"`,
fmt.Sprintf(`"successfully rotated DEK" uid="panda" newKeyIDHash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" oldKeyIDHash="" expirationTimestamp="%s"`, fmt.Sprintf(`"successfully rotated DEK" uid="panda" useSeed=false newKeyIDHash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" oldKeyIDHash="" expirationTimestamp="%s"`,
now.Add(3*time.Minute).Format(time.RFC3339)), now.Add(3*time.Minute).Format(time.RFC3339)),
}, },
wantErr: "", wantErr: "",
@ -1740,20 +1742,38 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "2", now), state: validState(t, "2", now),
statusKeyID: "2", statusKeyID: "2",
wantState: envelopekmsv2.State{ wantState: envelopekmsv2.State{
KeyID: "2", EncryptedObject: kmstypes.EncryptedObject{KeyID: "2"},
ExpirationTimestamp: now.Add(3 * time.Minute), ExpirationTimestamp: now.Add(3 * time.Minute),
}, },
wantEncryptCalls: 0, wantEncryptCalls: 0,
wantLogs: nil, wantLogs: nil,
wantErr: "", wantErr: "",
}, },
{
name: "happy path, with previous state, useSeed=true",
service: &testKMSv2EnvelopeService{keyID: "2"},
state: validState(t, "2", now),
useSeed: true,
statusKeyID: "2",
wantState: envelopekmsv2.State{
EncryptedObject: kmstypes.EncryptedObject{KeyID: "2", EncryptedDEKSourceType: kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED},
ExpirationTimestamp: now.Add(3 * time.Minute),
},
wantEncryptCalls: 1,
wantLogs: []string{
`"encrypting content using envelope service" uid="panda"`,
fmt.Sprintf(`"successfully rotated DEK" uid="panda" useSeed=true newKeyIDHash="sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35" oldKeyIDHash="sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35" expirationTimestamp="%s"`,
now.Add(3*time.Minute).Format(time.RFC3339)),
},
wantErr: "",
},
{ {
name: "previous state expired but key ID matches", name: "previous state expired but key ID matches",
service: &testKMSv2EnvelopeService{err: fmt.Errorf("broken")}, // not called service: &testKMSv2EnvelopeService{err: fmt.Errorf("broken")}, // not called
state: validState(t, "3", now.Add(-time.Hour)), state: validState(t, "3", now.Add(-time.Hour)),
statusKeyID: "3", statusKeyID: "3",
wantState: envelopekmsv2.State{ wantState: envelopekmsv2.State{
KeyID: "3", EncryptedObject: kmstypes.EncryptedObject{KeyID: "3"},
ExpirationTimestamp: now.Add(3 * time.Minute), ExpirationTimestamp: now.Add(3 * time.Minute),
}, },
wantEncryptCalls: 0, wantEncryptCalls: 0,
@ -1766,13 +1786,13 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "3", now.Add(-time.Hour)), state: validState(t, "3", now.Add(-time.Hour)),
statusKeyID: "4", statusKeyID: "4",
wantState: envelopekmsv2.State{ wantState: envelopekmsv2.State{
KeyID: "4", EncryptedObject: kmstypes.EncryptedObject{KeyID: "4"},
ExpirationTimestamp: now.Add(3 * time.Minute), ExpirationTimestamp: now.Add(3 * time.Minute),
}, },
wantEncryptCalls: 1, wantEncryptCalls: 1,
wantLogs: []string{ wantLogs: []string{
`"encrypting content using envelope service" uid="panda"`, `"encrypting content using envelope service" uid="panda"`,
fmt.Sprintf(`"successfully rotated DEK" uid="panda" newKeyIDHash="sha256:4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a" oldKeyIDHash="sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" expirationTimestamp="%s"`, fmt.Sprintf(`"successfully rotated DEK" uid="panda" useSeed=false newKeyIDHash="sha256:4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a" oldKeyIDHash="sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" expirationTimestamp="%s"`,
now.Add(3*time.Minute).Format(time.RFC3339)), now.Add(3*time.Minute).Format(time.RFC3339)),
}, },
wantErr: "", wantErr: "",
@ -1783,14 +1803,14 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "4", now.Add(7*time.Minute)), state: validState(t, "4", now.Add(7*time.Minute)),
statusKeyID: "5", statusKeyID: "5",
wantState: envelopekmsv2.State{ wantState: envelopekmsv2.State{
KeyID: "4", EncryptedObject: kmstypes.EncryptedObject{KeyID: "4"},
ExpirationTimestamp: now.Add(7 * time.Minute), ExpirationTimestamp: now.Add(7 * time.Minute),
}, },
wantEncryptCalls: 1, wantEncryptCalls: 1,
wantLogs: []string{ wantLogs: []string{
`"encrypting content using envelope service" uid="panda"`, `"encrypting content using envelope service" uid="panda"`,
}, },
wantErr: `failed to rotate DEK uid="panda", ` + wantErr: `failed to rotate DEK uid="panda", useSeed=false, ` +
`errState=<nil>, errGen=failed to encrypt DEK, error: broken, statusKeyIDHash="sha256:ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d", ` + `errState=<nil>, errGen=failed to encrypt DEK, error: broken, statusKeyIDHash="sha256:ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d", ` +
`encryptKeyIDHash="", stateKeyIDHash="sha256:4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a", expirationTimestamp=` + now.Add(7*time.Minute).Format(time.RFC3339), `encryptKeyIDHash="", stateKeyIDHash="sha256:4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a", expirationTimestamp=` + now.Add(7*time.Minute).Format(time.RFC3339),
}, },
@ -1804,7 +1824,7 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
wantLogs: []string{ wantLogs: []string{
`"encrypting content using envelope service" uid="panda"`, `"encrypting content using envelope service" uid="panda"`,
}, },
wantErr: `failed to rotate DEK uid="panda", ` + wantErr: `failed to rotate DEK uid="panda", useSeed=false, ` +
`errState=got unexpected nil transformer, errGen=failed to validate annotations: annotations: Invalid value: "panda": ` + `errState=got unexpected nil transformer, errGen=failed to validate annotations: annotations: Invalid value: "panda": ` +
`should be a domain with at least two segments separated by dots, statusKeyIDHash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b", ` + `should be a domain with at least two segments separated by dots, statusKeyIDHash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b", ` +
`encryptKeyIDHash="", stateKeyIDHash="", expirationTimestamp=` + (time.Time{}).Format(time.RFC3339), `encryptKeyIDHash="", stateKeyIDHash="", expirationTimestamp=` + (time.Time{}).Format(time.RFC3339),
@ -1815,14 +1835,14 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "2", now), state: validState(t, "2", now),
statusKeyID: "3", statusKeyID: "3",
wantState: envelopekmsv2.State{ wantState: envelopekmsv2.State{
KeyID: "2", EncryptedObject: kmstypes.EncryptedObject{KeyID: "2"},
ExpirationTimestamp: now, ExpirationTimestamp: now,
}, },
wantEncryptCalls: 1, wantEncryptCalls: 1,
wantLogs: []string{ wantLogs: []string{
`"encrypting content using envelope service" uid="panda"`, `"encrypting content using envelope service" uid="panda"`,
}, },
wantErr: `failed to rotate DEK uid="panda", ` + wantErr: `failed to rotate DEK uid="panda", useSeed=false, ` +
`errState=<nil>, errGen=failed to validate annotations: annotations: Invalid value: "panda": ` + `errState=<nil>, errGen=failed to validate annotations: annotations: Invalid value: "panda": ` +
`should be a domain with at least two segments separated by dots, statusKeyIDHash="sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce", ` + `should be a domain with at least two segments separated by dots, statusKeyIDHash="sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce", ` +
`encryptKeyIDHash="", stateKeyIDHash="sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", expirationTimestamp=` + now.Format(time.RFC3339), `encryptKeyIDHash="", stateKeyIDHash="sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", expirationTimestamp=` + now.Format(time.RFC3339),
@ -1830,6 +1850,8 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv2KDF, tt.useSeed)()
var buf bytes.Buffer var buf bytes.Buffer
klog.SetOutput(&buf) klog.SetOutput(&buf)
@ -1850,14 +1872,29 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
t.Errorf("log mismatch (-want +got):\n%s", diff) t.Errorf("log mismatch (-want +got):\n%s", diff)
} }
ignoredFields := sets.NewString("Transformer", "EncryptedDEK", "UID", "CacheKey") ignoredFields := sets.NewString("Transformer", "EncryptedObject.EncryptedDEKSource", "UID", "CacheKey")
if diff := cmp.Diff(tt.wantState, *h.state.Load(), gotState := *h.state.Load()
if diff := cmp.Diff(tt.wantState, gotState,
cmp.FilterPath(func(path cmp.Path) bool { return ignoredFields.Has(path.String()) }, cmp.Ignore()), cmp.FilterPath(func(path cmp.Path) bool { return ignoredFields.Has(path.String()) }, cmp.Ignore()),
); len(diff) > 0 { ); len(diff) > 0 {
t.Errorf("state mismatch (-want +got):\n%s", diff) t.Errorf("state mismatch (-want +got):\n%s", diff)
} }
if len(cmp.Diff(tt.wantState, gotState)) > 0 { // we only need to run this check when the state changes
validCiphertext := len(gotState.EncryptedObject.EncryptedDEKSource) > 0
if tt.useSeed {
validCiphertext = validCiphertext && gotState.EncryptedObject.EncryptedDEKSourceType == kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED
} else {
validCiphertext = validCiphertext && gotState.EncryptedObject.EncryptedDEKSourceType == kmstypes.EncryptedDEKSourceType_AES_GCM_KEY
}
if !validCiphertext {
t.Errorf("invalid ciphertext with useSeed=%v, encryptedDEKSourceLen=%d, encryptedDEKSourceType=%d", tt.useSeed,
len(gotState.EncryptedObject.EncryptedDEKSource), gotState.EncryptedObject.EncryptedDEKSourceType)
}
}
if tt.wantEncryptCalls != tt.service.encryptCalls { if tt.wantEncryptCalls != tt.service.encryptCalls {
t.Errorf("want %d encryptCalls, got %d", tt.wantEncryptCalls, tt.service.encryptCalls) t.Errorf("want %d encryptCalls, got %d", tt.wantEncryptCalls, tt.service.encryptCalls)
} }
@ -1900,15 +1937,15 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
func validState(t *testing.T, keyID string, exp time.Time) envelopekmsv2.State { func validState(t *testing.T, keyID string, exp time.Time) envelopekmsv2.State {
t.Helper() t.Helper()
transformer, resp, cacheKey, err := envelopekmsv2.GenerateTransformer(testContext(t), "", &testKMSv2EnvelopeService{keyID: keyID}) useSeed := utilfeature.DefaultFeatureGate.Enabled(features.KMSv2KDF) // match the current default behavior
transformer, encObject, cacheKey, err := envelopekmsv2.GenerateTransformer(testContext(t), "", &testKMSv2EnvelopeService{keyID: keyID}, useSeed)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return envelopekmsv2.State{ return envelopekmsv2.State{
Transformer: transformer, Transformer: transformer,
EncryptedDEK: resp.Ciphertext, EncryptedObject: *encObject,
KeyID: resp.KeyID,
Annotations: resp.Annotations,
ExpirationTimestamp: exp, ExpirationTimestamp: exp,
CacheKey: cacheKey, CacheKey: cacheKey,
} }

View File

@ -34,33 +34,11 @@ import (
"k8s.io/klog/v2" "k8s.io/klog/v2"
) )
type gcm struct { // commonSize is the length of various security sensitive byte slices such as encryption keys.
aead cipher.AEAD // Do not change this value. It would be a backward incompatible change.
nonceFunc func([]byte) error const commonSize = 32
}
// NewGCMTransformer takes the given block cipher and performs encryption and decryption on the given data. const keySizeCounterNonceGCM = commonSize
// It implements AEAD encryption of the provided values given a cipher.Block algorithm.
// The authenticated data provided as part of the value.Context method must match when the same
// value is set to and loaded from storage. In order to ensure that values cannot be copied by
// an attacker from a location under their control, use characteristics of the storage location
// (such as the etcd key) as part of the authenticated data.
//
// Because this mode requires a generated IV and IV reuse is a known weakness of AES-GCM, keys
// must be rotated before a birthday attack becomes feasible. NIST SP 800-38D
// (http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf) recommends using the same
// key with random 96-bit nonces (the default nonce length) no more than 2^32 times, and
// therefore transformers using this implementation *must* ensure they allow for frequent key
// rotation. Future work should include investigation of AES-GCM-SIV as an alternative to
// random nonces.
func NewGCMTransformer(block cipher.Block) (value.Transformer, error) {
aead, err := newGCM(block)
if err != nil {
return nil, err
}
return &gcm{aead: aead, nonceFunc: randomNonce}, nil
}
// NewGCMTransformerWithUniqueKeyUnsafe is the same as NewGCMTransformer but is unsafe for general // NewGCMTransformerWithUniqueKeyUnsafe is the same as NewGCMTransformer but is unsafe for general
// use because it makes assumptions about the key underlying the block cipher. Specifically, // use because it makes assumptions about the key underlying the block cipher. Specifically,
@ -78,7 +56,7 @@ func NewGCMTransformer(block cipher.Block) (value.Transformer, error) {
// it can be passed to NewGCMTransformer(aes.NewCipher(key)) to construct a transformer capable // it can be passed to NewGCMTransformer(aes.NewCipher(key)) to construct a transformer capable
// of decrypting values encrypted by this transformer (that transformer must not be used for encryption). // of decrypting values encrypted by this transformer (that transformer must not be used for encryption).
func NewGCMTransformerWithUniqueKeyUnsafe() (value.Transformer, []byte, error) { func NewGCMTransformerWithUniqueKeyUnsafe() (value.Transformer, []byte, error) {
key, err := generateKey(32) key, err := GenerateKey(keySizeCounterNonceGCM)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -126,17 +104,6 @@ func newGCMTransformerWithUniqueKeyUnsafe(block cipher.Block, nonceGen *nonceGen
return &gcm{aead: aead, nonceFunc: nonceFunc}, nil return &gcm{aead: aead, nonceFunc: nonceFunc}, nil
} }
func newGCM(block cipher.Block) (cipher.AEAD, error) {
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if nonceSize := aead.NonceSize(); nonceSize != 12 { // all data in etcd will be broken if this ever changes
return nil, fmt.Errorf("crypto/cipher.NewGCM returned unexpected nonce size: %d", nonceSize)
}
return aead, nil
}
func randomNonce(b []byte) error { func randomNonce(b []byte) error {
_, err := rand.Read(b) _, err := rand.Read(b)
return err return err
@ -164,8 +131,8 @@ func die(msg string) {
klog.FatalDepth(1, msg) klog.FatalDepth(1, msg)
} }
// generateKey generates a random key using system randomness. // GenerateKey generates a random key using system randomness.
func generateKey(length int) (key []byte, err error) { func GenerateKey(length int) (key []byte, err error) {
defer func(start time.Time) { defer func(start time.Time) {
value.RecordDataKeyGeneration(start, err) value.RecordDataKeyGeneration(start, err)
}(time.Now()) }(time.Now())
@ -177,6 +144,45 @@ func generateKey(length int) (key []byte, err error) {
return key, nil return key, nil
} }
// NewGCMTransformer takes the given block cipher and performs encryption and decryption on the given data.
// It implements AEAD encryption of the provided values given a cipher.Block algorithm.
// The authenticated data provided as part of the value.Context method must match when the same
// value is set to and loaded from storage. In order to ensure that values cannot be copied by
// an attacker from a location under their control, use characteristics of the storage location
// (such as the etcd key) as part of the authenticated data.
//
// Because this mode requires a generated IV and IV reuse is a known weakness of AES-GCM, keys
// must be rotated before a birthday attack becomes feasible. NIST SP 800-38D
// (http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf) recommends using the same
// key with random 96-bit nonces (the default nonce length) no more than 2^32 times, and
// therefore transformers using this implementation *must* ensure they allow for frequent key
// rotation. Future work should include investigation of AES-GCM-SIV as an alternative to
// random nonces.
func NewGCMTransformer(block cipher.Block) (value.Transformer, error) {
aead, err := newGCM(block)
if err != nil {
return nil, err
}
return &gcm{aead: aead, nonceFunc: randomNonce}, nil
}
func newGCM(block cipher.Block) (cipher.AEAD, error) {
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if nonceSize := aead.NonceSize(); nonceSize != 12 { // all data in etcd will be broken if this ever changes
return nil, fmt.Errorf("crypto/cipher.NewGCM returned unexpected nonce size: %d", nonceSize)
}
return aead, nil
}
type gcm struct {
aead cipher.AEAD
nonceFunc func([]byte) error
}
func (t *gcm) TransformFromStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, bool, error) { func (t *gcm) TransformFromStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, bool, error) {
nonceSize := t.aead.NonceSize() nonceSize := t.aead.NonceSize()
if len(data) < nonceSize { if len(data) < nonceSize {

View File

@ -0,0 +1,186 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aes
import (
"bytes"
"context"
"crypto/aes"
"crypto/sha256"
"errors"
"fmt"
"io"
"time"
"golang.org/x/crypto/hkdf"
"k8s.io/apiserver/pkg/storage/value"
"k8s.io/utils/clock"
)
const (
// cacheTTL is the TTL of KDF cache entries. We assume that the value.Context.AuthenticatedData
// for every call is the etcd storage path of the associated resource, and use that as the primary
// cache key (with a secondary check that confirms that the info matches). Thus if a client
// is constantly creating resources with new names (and thus new paths), they will keep adding new
// entries to the cache for up to this TTL before the GC logic starts deleting old entries. Each
// entry is ~300 bytes in size, so even a malicious client will be bounded in the overall memory
// it can consume.
cacheTTL = 10 * time.Minute
derivedKeySizeExtendedNonceGCM = commonSize
infoSizeExtendedNonceGCM
MinSeedSizeExtendedNonceGCM
)
// NewHKDFExtendedNonceGCMTransformer is the same as NewGCMTransformer but trades storage,
// memory and CPU to work around the limitations of AES-GCM's 12 byte nonce size. The input seed
// is assumed to be a cryptographically strong slice of MinSeedSizeExtendedNonceGCM+ random bytes.
// Unlike NewGCMTransformer, this function is immune to the birthday attack because a new key is generated
// per encryption via a key derivation function: KDF(seed, random_bytes) -> key. The derived key is
// only used once as an AES-GCM key with a random 12 byte nonce. This avoids any concerns around
// cryptographic wear out (by either number of encryptions or the amount of data being encrypted).
// Speaking on the cryptographic safety, the limit on the number of operations that can be preformed
// with a single seed with derived keys and randomly generated nonces is not practically reachable.
// Thus, the scheme does not impose any specific requirements on the seed rotation schedule.
// Reusing the same seed is safe to do over time and across process restarts. Whenever a new
// seed is needed, the caller should generate it via GenerateKey(MinSeedSizeExtendedNonceGCM).
// In regard to KMSv2, organization standards or compliance policies around rotation may require
// that the seed be rotated at some interval. This can be implemented externally by rotating
// the key encryption key via a key ID change.
func NewHKDFExtendedNonceGCMTransformer(seed []byte) (value.Transformer, error) {
if seedLen := len(seed); seedLen < MinSeedSizeExtendedNonceGCM {
return nil, fmt.Errorf("invalid seed length %d used for key generation", seedLen)
}
return &extendedNonceGCM{
seed: seed,
cache: newSimpleCache(clock.RealClock{}, cacheTTL),
}, nil
}
type extendedNonceGCM struct {
seed []byte
cache *simpleCache
}
func (e *extendedNonceGCM) TransformFromStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, bool, error) {
if len(data) < infoSizeExtendedNonceGCM {
return nil, false, errors.New("the stored data was shorter than the required size")
}
info := data[:infoSizeExtendedNonceGCM]
transformer, err := e.derivedKeyTransformer(info, dataCtx, false)
if err != nil {
return nil, false, fmt.Errorf("failed to derive read key from KDF: %w", err)
}
return transformer.TransformFromStorage(ctx, data, dataCtx)
}
func (e *extendedNonceGCM) TransformToStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, error) {
info := make([]byte, infoSizeExtendedNonceGCM)
if err := randomNonce(info); err != nil {
return nil, fmt.Errorf("failed to generate info for KDF: %w", err)
}
transformer, err := e.derivedKeyTransformer(info, dataCtx, true)
if err != nil {
return nil, fmt.Errorf("failed to derive write key from KDF: %w", err)
}
return transformer.TransformToStorage(ctx, data, dataCtx)
}
func (e *extendedNonceGCM) derivedKeyTransformer(info []byte, dataCtx value.Context, write bool) (value.Transformer, error) {
if !write { // no need to check cache on write since we always generate a new transformer
if transformer := e.cache.get(info, dataCtx); transformer != nil {
return transformer, nil
}
// on read, this is a subslice of a much larger slice and we do not want to hold onto that larger slice
info = bytes.Clone(info)
}
key, err := e.sha256KDFExpandOnly(info)
if err != nil {
return nil, fmt.Errorf("failed to KDF expand seed with info: %w", err)
}
transformer, err := newGCMTransformerWithInfo(key, info)
if err != nil {
return nil, fmt.Errorf("failed to build transformer with KDF derived key: %w", err)
}
e.cache.set(dataCtx, transformer)
return transformer, nil
}
func (e *extendedNonceGCM) sha256KDFExpandOnly(info []byte) ([]byte, error) {
kdf := hkdf.Expand(sha256.New, e.seed, info)
derivedKey := make([]byte, derivedKeySizeExtendedNonceGCM)
if _, err := io.ReadFull(kdf, derivedKey); err != nil {
return nil, fmt.Errorf("failed to read a derived key from KDF: %w", err)
}
return derivedKey, nil
}
func newGCMTransformerWithInfo(key, info []byte) (*transformerWithInfo, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
transformer, err := NewGCMTransformer(block)
if err != nil {
return nil, err
}
return &transformerWithInfo{transformer: transformer, info: info}, nil
}
type transformerWithInfo struct {
transformer value.Transformer
// info are extra opaque bytes prepended to the writes from transformer and stripped from reads.
// currently info is used to generate a key via KDF(seed, info) -> key
// and transformer is the output of NewGCMTransformer(aes.NewCipher(key))
info []byte
}
func (t *transformerWithInfo) TransformFromStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, bool, error) {
if !bytes.HasPrefix(data, t.info) {
return nil, false, errors.New("the stored data is missing the required info prefix")
}
return t.transformer.TransformFromStorage(ctx, data[len(t.info):], dataCtx)
}
func (t *transformerWithInfo) TransformToStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, error) {
out, err := t.transformer.TransformToStorage(ctx, data, dataCtx)
if err != nil {
return nil, err
}
outWithInfo := make([]byte, 0, len(out)+len(t.info))
outWithInfo = append(outWithInfo, t.info...)
outWithInfo = append(outWithInfo, out...)
return outWithInfo, nil
}

View File

@ -152,7 +152,7 @@ func TestGCMUnsafeCompatibility(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
transformerDecrypt := newGCMTransformer(t, block) transformerDecrypt := newGCMTransformer(t, block, nil)
ctx := context.Background() ctx := context.Background()
dataCtx := value.DefaultContext("authenticated_data") dataCtx := value.DefaultContext("authenticated_data")
@ -184,7 +184,7 @@ func TestGCMLegacyDataCompatibility(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
transformerDecrypt := newGCMTransformer(t, block) transformerDecrypt := newGCMTransformer(t, block, nil)
// recorded output from NewGCMTransformer at commit 3b1fc60d8010dd8b53e97ba80e4710dbb430beee // recorded output from NewGCMTransformer at commit 3b1fc60d8010dd8b53e97ba80e4710dbb430beee
const legacyCiphertext = "\x9f'\xc8\xfc\xea\x8aX\xc4g\xd8\xe47\xdb\xf2\xd8YU\xf9\xb4\xbd\x91/N\xf9g\u05c8\xa0\xcb\ay}\xac\n?\n\bE`\\\xa8Z\xc8V+J\xe1" const legacyCiphertext = "\x9f'\xc8\xfc\xea\x8aX\xc4g\xd8\xe47\xdb\xf2\xd8YU\xf9\xb4\xbd\x91/N\xf9g\u05c8\xa0\xcb\ay}\xac\n?\n\bE`\\\xa8Z\xc8V+J\xe1"
@ -204,12 +204,36 @@ func TestGCMLegacyDataCompatibility(t *testing.T) {
} }
} }
func TestExtendedNonceGCMLegacyDataCompatibility(t *testing.T) {
// recorded output from NewKDFExtendedNonceGCMTransformerWithUniqueSeed from https://github.com/kubernetes/kubernetes/pull/118828
const (
legacyKey = "]@2:\x82\x0f\xf9Uag^;\x95\xe8\x18g\xc5\xfd\xd5a\xd3Z\x88\xa2Ћ\b\xaa\x9dO\xcf\\"
legacyCiphertext = "$Bu\x9e3\x94_\xba\xd7\t\xdbWz\x0f\x03\x7fا\t\xfcv\x97\x9b\x89B \x9d\xeb\xce˝W\xef\xe3\xd6\xffj\x1e\xf6\xee\x9aP\x03\xb9\x83;0C\xce\xc1\xe4{5\x17[\x15\x11\a\xa8\xd2Ak\x0e)k\xbff\xb5\xd1\x02\xfc\xefߚx\xf2\x93\xd2q"
)
transformerDecrypt := newHKDFExtendedNonceGCMTransformerTest(t, nil, []byte(legacyKey))
ctx := context.Background()
dataCtx := value.DefaultContext("bamboo")
plaintext := []byte("pandas are the best")
plaintextAgain, _, err := transformerDecrypt.TransformFromStorage(ctx, []byte(legacyCiphertext), dataCtx)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, plaintextAgain) {
t.Errorf("expected original plaintext %q, got %q", string(plaintext), string(plaintextAgain))
}
}
func TestGCMUnsafeNonceGen(t *testing.T) { func TestGCMUnsafeNonceGen(t *testing.T) {
block, err := aes.NewCipher([]byte("abcdefghijklmnop")) block, err := aes.NewCipher([]byte("abcdefghijklmnop"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
transformer := newGCMTransformerWithUniqueKeyUnsafeTest(t, block) transformer := newGCMTransformerWithUniqueKeyUnsafeTest(t, block, nil)
ctx := context.Background() ctx := context.Background()
dataCtx := value.DefaultContext("authenticated_data") dataCtx := value.DefaultContext("authenticated_data")
@ -270,7 +294,7 @@ func TestGCMUnsafeNonceGen(t *testing.T) {
func TestGCMNonce(t *testing.T) { func TestGCMNonce(t *testing.T) {
t.Run("gcm", func(t *testing.T) { t.Run("gcm", func(t *testing.T) {
testGCMNonce(t, newGCMTransformer, func(_ int, nonce []byte) { testGCMNonce(t, newGCMTransformer, 0, func(_ int, nonce []byte) {
if bytes.Equal(nonce, make([]byte, len(nonce))) { if bytes.Equal(nonce, make([]byte, len(nonce))) {
t.Error("got all zeros for nonce") t.Error("got all zeros for nonce")
} }
@ -278,21 +302,30 @@ func TestGCMNonce(t *testing.T) {
}) })
t.Run("gcm unsafe", func(t *testing.T) { t.Run("gcm unsafe", func(t *testing.T) {
testGCMNonce(t, newGCMTransformerWithUniqueKeyUnsafeTest, func(i int, nonce []byte) { testGCMNonce(t, newGCMTransformerWithUniqueKeyUnsafeTest, 0, func(i int, nonce []byte) {
counter := binary.LittleEndian.Uint64(nonce) counter := binary.LittleEndian.Uint64(nonce)
if uint64(i+1) != counter { // add one because the counter starts at 1, not 0 if uint64(i+1) != counter { // add one because the counter starts at 1, not 0
t.Errorf("counter nonce is invalid: want %d, got %d", i+1, counter) t.Errorf("counter nonce is invalid: want %d, got %d", i+1, counter)
} }
}) })
}) })
t.Run("gcm extended nonce", func(t *testing.T) {
testGCMNonce(t, newHKDFExtendedNonceGCMTransformerTest, infoSizeExtendedNonceGCM, func(_ int, nonce []byte) {
if bytes.Equal(nonce, make([]byte, len(nonce))) {
t.Error("got all zeros for nonce")
}
})
})
} }
func testGCMNonce(t *testing.T, f func(t testingT, block cipher.Block) value.Transformer, check func(int, []byte)) { func testGCMNonce(t *testing.T, f transformerFunc, infoLen int, check func(int, []byte)) {
block, err := aes.NewCipher([]byte("abcdefghijklmnop")) key := []byte("abcdefghijklmnopabcdefghijklmnop")
block, err := aes.NewCipher(key)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
transformer := f(t, block) transformer := f(t, block, key)
ctx := context.Background() ctx := context.Background()
dataCtx := value.DefaultContext("authenticated_data") dataCtx := value.DefaultContext("authenticated_data")
@ -307,13 +340,20 @@ func testGCMNonce(t *testing.T, f func(t testingT, block cipher.Block) value.Tra
t.Fatal(err) t.Fatal(err)
} }
nonce := out[:12] info := out[:infoLen]
nonce := out[infoLen : 12+infoLen]
randomN := nonce[:4] randomN := nonce[:4]
if bytes.Equal(randomN, make([]byte, len(randomN))) { if bytes.Equal(randomN, make([]byte, len(randomN))) {
t.Error("got all zeros for first four bytes") t.Error("got all zeros for first four bytes")
} }
if infoLen != 0 {
if bytes.Equal(info, make([]byte, infoLen)) {
t.Error("got all zeros for info")
}
}
check(i, nonce[4:]) check(i, nonce[4:])
} }
} }
@ -326,15 +366,22 @@ func TestGCMKeyRotation(t *testing.T) {
t.Run("gcm unsafe", func(t *testing.T) { t.Run("gcm unsafe", func(t *testing.T) {
testGCMKeyRotation(t, newGCMTransformerWithUniqueKeyUnsafeTest) testGCMKeyRotation(t, newGCMTransformerWithUniqueKeyUnsafeTest)
}) })
t.Run("gcm extended", func(t *testing.T) {
testGCMKeyRotation(t, newHKDFExtendedNonceGCMTransformerTest)
})
} }
func testGCMKeyRotation(t *testing.T, f func(t testingT, block cipher.Block) value.Transformer) { func testGCMKeyRotation(t *testing.T, f transformerFunc) {
key1 := []byte("abcdefghijklmnopabcdefghijklmnop")
key2 := []byte("0123456789abcdef0123456789abcdef")
testErr := fmt.Errorf("test error") testErr := fmt.Errorf("test error")
block1, err := aes.NewCipher([]byte("abcdefghijklmnop")) block1, err := aes.NewCipher(key1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
block2, err := aes.NewCipher([]byte("0123456789abcdef")) block2, err := aes.NewCipher(key2)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -343,8 +390,8 @@ func testGCMKeyRotation(t *testing.T, f func(t testingT, block cipher.Block) val
dataCtx := value.DefaultContext("authenticated_data") dataCtx := value.DefaultContext("authenticated_data")
p := value.NewPrefixTransformers(testErr, p := value.NewPrefixTransformers(testErr,
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1)}, value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1, key1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2)}, value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2, key2)},
) )
out, err := p.TransformToStorage(ctx, []byte("firstvalue"), dataCtx) out, err := p.TransformToStorage(ctx, []byte("firstvalue"), dataCtx)
if err != nil { if err != nil {
@ -369,8 +416,8 @@ func testGCMKeyRotation(t *testing.T, f func(t testingT, block cipher.Block) val
// reverse the order, use the second key // reverse the order, use the second key
p = value.NewPrefixTransformers(testErr, p = value.NewPrefixTransformers(testErr,
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2)}, value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2, key2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1)}, value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1, key1)},
) )
from, stale, err = p.TransformFromStorage(ctx, out, dataCtx) from, stale, err = p.TransformFromStorage(ctx, out, dataCtx)
if err != nil { if err != nil {
@ -434,6 +481,12 @@ func TestCBCKeyRotation(t *testing.T) {
} }
} }
var gcmBenchmarks = []namedTransformerFunc{
{name: "gcm-random-nonce", f: newGCMTransformer},
{name: "gcm-counter-nonce", f: newGCMTransformerWithUniqueKeyUnsafeTest},
{name: "gcm-extended-nonce", f: newHKDFExtendedNonceGCMTransformerTest},
}
func BenchmarkGCMRead(b *testing.B) { func BenchmarkGCMRead(b *testing.B) {
tests := []struct { tests := []struct {
keyLength int keyLength int
@ -448,7 +501,16 @@ func BenchmarkGCMRead(b *testing.B) {
for _, t := range tests { for _, t := range tests {
name := fmt.Sprintf("%vKeyLength/%vValueLength/%vExpectStale", t.keyLength, t.valueLength, t.expectStale) name := fmt.Sprintf("%vKeyLength/%vValueLength/%vExpectStale", t.keyLength, t.valueLength, t.expectStale)
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
benchmarkGCMRead(b, t.keyLength, t.valueLength, t.expectStale) for _, n := range gcmBenchmarks {
n := n
if t.keyLength == 16 && n.name == "gcm-extended-nonce" {
continue // gcm-extended-nonce requires 32 byte keys
}
b.Run(n.name, func(b *testing.B) {
b.ReportAllocs()
benchmarkGCMRead(b, n.f, t.keyLength, t.valueLength, t.expectStale)
})
}
}) })
} }
} }
@ -465,23 +527,35 @@ func BenchmarkGCMWrite(b *testing.B) {
for _, t := range tests { for _, t := range tests {
name := fmt.Sprintf("%vKeyLength/%vValueLength", t.keyLength, t.valueLength) name := fmt.Sprintf("%vKeyLength/%vValueLength", t.keyLength, t.valueLength)
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
benchmarkGCMWrite(b, t.keyLength, t.valueLength) for _, n := range gcmBenchmarks {
n := n
if t.keyLength == 16 && n.name == "gcm-extended-nonce" {
continue // gcm-extended-nonce requires 32 byte keys
}
b.Run(n.name, func(b *testing.B) {
b.ReportAllocs()
benchmarkGCMWrite(b, n.f, t.keyLength, t.valueLength)
})
}
}) })
} }
} }
func benchmarkGCMRead(b *testing.B, keyLength int, valueLength int, expectStale bool) { func benchmarkGCMRead(b *testing.B, f transformerFunc, keyLength int, valueLength int, expectStale bool) {
block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength)) key1 := bytes.Repeat([]byte("a"), keyLength)
key2 := bytes.Repeat([]byte("b"), keyLength)
block1, err := aes.NewCipher(key1)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength)) block2, err := aes.NewCipher(key2)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
p := value.NewPrefixTransformers(nil, p := value.NewPrefixTransformers(nil,
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: newGCMTransformer(b, block1)}, value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(b, block1, key1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: newGCMTransformer(b, block2)}, value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(b, block2, key2)},
) )
ctx := context.Background() ctx := context.Background()
@ -495,8 +569,8 @@ func benchmarkGCMRead(b *testing.B, keyLength int, valueLength int, expectStale
// reverse the key order if expecting stale // reverse the key order if expecting stale
if expectStale { if expectStale {
p = value.NewPrefixTransformers(nil, p = value.NewPrefixTransformers(nil,
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: newGCMTransformer(b, block2)}, value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(b, block2, key2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: newGCMTransformer(b, block1)}, value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(b, block1, key1)},
) )
} }
@ -513,18 +587,21 @@ func benchmarkGCMRead(b *testing.B, keyLength int, valueLength int, expectStale
b.StopTimer() b.StopTimer()
} }
func benchmarkGCMWrite(b *testing.B, keyLength int, valueLength int) { func benchmarkGCMWrite(b *testing.B, f transformerFunc, keyLength int, valueLength int) {
block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength)) key1 := bytes.Repeat([]byte("a"), keyLength)
key2 := bytes.Repeat([]byte("b"), keyLength)
block1, err := aes.NewCipher(key1)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength)) block2, err := aes.NewCipher(key2)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
p := value.NewPrefixTransformers(nil, p := value.NewPrefixTransformers(nil,
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: newGCMTransformer(b, block1)}, value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(b, block1, key1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: newGCMTransformer(b, block2)}, value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(b, block2, key2)},
) )
ctx := context.Background() ctx := context.Background()
@ -657,31 +734,29 @@ func TestRoundTrip(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
aes32block, err := aes.NewCipher(bytes.Repeat([]byte("c"), 32)) key32 := bytes.Repeat([]byte("c"), 32)
aes32block, err := aes.NewCipher(key32)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
ctx := context.Background() ctx := context.Background()
tests := []struct { tests := []struct {
name string name string
dataCtx value.Context t value.Transformer
t value.Transformer
}{ }{
{name: "GCM 16 byte key", t: newGCMTransformer(t, aes16block)}, {name: "GCM 16 byte key", t: newGCMTransformer(t, aes16block, nil)},
{name: "GCM 24 byte key", t: newGCMTransformer(t, aes24block)}, {name: "GCM 24 byte key", t: newGCMTransformer(t, aes24block, nil)},
{name: "GCM 32 byte key", t: newGCMTransformer(t, aes32block)}, {name: "GCM 32 byte key", t: newGCMTransformer(t, aes32block, nil)},
{name: "GCM 16 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes16block)}, {name: "GCM 16 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes16block, nil)},
{name: "GCM 24 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes24block)}, {name: "GCM 24 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes24block, nil)},
{name: "GCM 32 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes32block)}, {name: "GCM 32 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes32block, nil)},
{name: "GCM 32 byte seed", t: newHKDFExtendedNonceGCMTransformerTest(t, nil, key32)},
{name: "CBC 32 byte key", t: NewCBCTransformer(aes32block)}, {name: "CBC 32 byte key", t: NewCBCTransformer(aes32block)},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
dataCtx := tt.dataCtx dataCtx := value.DefaultContext("/foo/bar")
if dataCtx == nil {
dataCtx = value.DefaultContext("")
}
for _, l := range lengths { for _, l := range lengths {
data := make([]byte, l) data := make([]byte, l)
if _, err := io.ReadFull(rand.Reader, data); err != nil { if _, err := io.ReadFull(rand.Reader, data); err != nil {
@ -718,12 +793,14 @@ func TestRoundTrip(t *testing.T) {
} }
} }
type testingT interface { type namedTransformerFunc struct {
Helper() name string
Fatal(...any) f transformerFunc
} }
func newGCMTransformer(t testingT, block cipher.Block) value.Transformer { type transformerFunc func(t testing.TB, block cipher.Block, key []byte) value.Transformer
func newGCMTransformer(t testing.TB, block cipher.Block, _ []byte) value.Transformer {
t.Helper() t.Helper()
transformer, err := NewGCMTransformer(block) transformer, err := NewGCMTransformer(block)
@ -734,7 +811,7 @@ func newGCMTransformer(t testingT, block cipher.Block) value.Transformer {
return transformer return transformer
} }
func newGCMTransformerWithUniqueKeyUnsafeTest(t testingT, block cipher.Block) value.Transformer { func newGCMTransformerWithUniqueKeyUnsafeTest(t testing.TB, block cipher.Block, _ []byte) value.Transformer {
t.Helper() t.Helper()
nonceGen := &nonceGenerator{fatal: die} nonceGen := &nonceGenerator{fatal: die}
@ -745,3 +822,14 @@ func newGCMTransformerWithUniqueKeyUnsafeTest(t testingT, block cipher.Block) va
return transformer return transformer
} }
func newHKDFExtendedNonceGCMTransformerTest(t testing.TB, _ cipher.Block, key []byte) value.Transformer {
t.Helper()
transformer, err := NewHKDFExtendedNonceGCMTransformer(key)
if err != nil {
t.Fatal(err)
}
return transformer
}

View File

@ -0,0 +1,91 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aes
import (
"bytes"
"time"
"unsafe"
utilcache "k8s.io/apimachinery/pkg/util/cache"
"k8s.io/apiserver/pkg/storage/value"
"k8s.io/utils/clock"
)
type simpleCache struct {
cache *utilcache.Expiring
ttl time.Duration
}
func newSimpleCache(clock clock.Clock, ttl time.Duration) *simpleCache {
cache := utilcache.NewExpiringWithClock(clock)
// "Stale" entries are always valid for us because the TTL is just used to prevent
// unbounded growth on the cache - for a given info the transformer is always the same.
// The key always corresponds to the exact same value, with the caveat that
// since we use the value.Context.AuthenticatedData to overwrite old keys,
// we always have to check that the info matches (to validate the transformer is correct).
cache.AllowExpiredGet = true
return &simpleCache{
cache: cache,
ttl: ttl,
}
}
// given a key, return the transformer, or nil if it does not exist in the cache
func (c *simpleCache) get(info []byte, dataCtx value.Context) *transformerWithInfo {
val, ok := c.cache.Get(keyFunc(dataCtx))
if !ok {
return nil
}
transformer := val.(*transformerWithInfo)
if !bytes.Equal(transformer.info, info) {
return nil
}
return transformer
}
// set caches the record for the key
func (c *simpleCache) set(dataCtx value.Context, transformer *transformerWithInfo) {
if dataCtx == nil || len(dataCtx.AuthenticatedData()) == 0 {
panic("authenticated data must not be empty")
}
if transformer == nil {
panic("transformer must not be nil")
}
if len(transformer.info) == 0 {
panic("info must not be empty")
}
c.cache.Set(keyFunc(dataCtx), transformer, c.ttl)
}
func keyFunc(dataCtx value.Context) string {
return toString(dataCtx.AuthenticatedData())
}
// toString performs unholy acts to avoid allocations
func toString(b []byte) string {
// unsafe.SliceData relies on cap whereas we want to rely on len
if len(b) == 0 {
return ""
}
// Copied from go 1.20.1 strings.Builder.String
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/strings/builder.go#L48
return unsafe.String(unsafe.SliceData(b), len(b))
}

View File

@ -0,0 +1,137 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aes
import (
"testing"
"time"
clocktesting "k8s.io/utils/clock/testing"
)
type dataString string
func (d dataString) AuthenticatedData() []byte { return []byte(d) }
func Test_simpleCache(t *testing.T) {
info1 := []byte{1}
info2 := []byte{2}
key1 := dataString("1")
key2 := dataString("2")
twi1 := &transformerWithInfo{info: info1}
twi2 := &transformerWithInfo{info: info2}
tests := []struct {
name string
test func(*testing.T, *simpleCache, *clocktesting.FakeClock)
}{
{
name: "get from empty",
test: func(t *testing.T, cache *simpleCache, clock *clocktesting.FakeClock) {
got := cache.get(info1, key1)
twiPtrEquals(t, nil, got)
cacheLenEquals(t, cache, 0)
},
},
{
name: "get after set",
test: func(t *testing.T, cache *simpleCache, clock *clocktesting.FakeClock) {
cache.set(key1, twi1)
got := cache.get(info1, key1)
twiPtrEquals(t, twi1, got)
cacheLenEquals(t, cache, 1)
},
},
{
name: "get after set but with different info",
test: func(t *testing.T, cache *simpleCache, clock *clocktesting.FakeClock) {
cache.set(key1, twi1)
got := cache.get(info2, key1)
twiPtrEquals(t, nil, got)
cacheLenEquals(t, cache, 1)
},
},
{
name: "expired get after set",
test: func(t *testing.T, cache *simpleCache, clock *clocktesting.FakeClock) {
cache.set(key1, twi1)
clock.Step(time.Hour)
got := cache.get(info1, key1)
twiPtrEquals(t, twi1, got)
cacheLenEquals(t, cache, 1)
},
},
{
name: "expired get after GC",
test: func(t *testing.T, cache *simpleCache, clock *clocktesting.FakeClock) {
cache.set(key1, twi1)
clock.Step(time.Hour)
cacheLenEquals(t, cache, 1)
cache.set(key2, twi2) // unrelated set to make GC run
got := cache.get(info1, key1)
twiPtrEquals(t, nil, got)
cacheLenEquals(t, cache, 1)
},
},
{
name: "multiple sets for same key",
test: func(t *testing.T, cache *simpleCache, clock *clocktesting.FakeClock) {
cache.set(key1, twi1)
cacheLenEquals(t, cache, 1)
cache.set(key1, twi2)
cacheLenEquals(t, cache, 1)
got11 := cache.get(info1, key1)
twiPtrEquals(t, nil, got11)
got21 := cache.get(info2, key1)
twiPtrEquals(t, twi2, got21)
got12 := cache.get(info1, key2)
twiPtrEquals(t, nil, got12)
got22 := cache.get(info2, key2)
twiPtrEquals(t, nil, got22)
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
clock := clocktesting.NewFakeClock(time.Now())
cache := newSimpleCache(clock, 10*time.Second)
tt.test(t, cache, clock)
})
}
}
func twiPtrEquals(t *testing.T, want, got *transformerWithInfo) {
t.Helper()
if want != got {
t.Errorf("transformerWithInfo structs are not pointer equivalent")
}
}
func cacheLenEquals(t *testing.T, cache *simpleCache, want int) {
t.Helper()
if got := cache.cache.Len(); want != got {
t.Errorf("unexpected cache len: want %d, got %d", want, got)
}
}

View File

@ -67,7 +67,7 @@ func TestKeyFunc(t *testing.T) {
cache := newSimpleCache(fakeClock, time.Second) cache := newSimpleCache(fakeClock, time.Second)
t.Run("AllocsPerRun test", func(t *testing.T) { t.Run("AllocsPerRun test", func(t *testing.T) {
key, err := generateKey(encryptedDEKMaxSize) // simulate worst case EDEK key, err := generateKey(encryptedDEKSourceMaxSize) // simulate worst case EDEK
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -20,6 +20,7 @@ package kmsv2
import ( import (
"context" "context"
"crypto/aes" "crypto/aes"
"crypto/cipher"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"sort" "sort"
@ -43,6 +44,8 @@ import (
"k8s.io/utils/clock" "k8s.io/utils/clock"
) )
// TODO integration test with old AES GCM data recorded and new KDF data recorded
func init() { func init() {
value.RegisterMetrics() value.RegisterMetrics()
metrics.RegisterMetrics() metrics.RegisterMetrics()
@ -55,22 +58,22 @@ const (
annotationsMaxSize = 32 * 1024 // 32 kB annotationsMaxSize = 32 * 1024 // 32 kB
// KeyIDMaxSize is the maximum size of the keyID. // KeyIDMaxSize is the maximum size of the keyID.
KeyIDMaxSize = 1 * 1024 // 1 kB KeyIDMaxSize = 1 * 1024 // 1 kB
// encryptedDEKMaxSize is the maximum size of the encrypted DEK. // encryptedDEKSourceMaxSize is the maximum size of the encrypted DEK source.
encryptedDEKMaxSize = 1 * 1024 // 1 kB encryptedDEKSourceMaxSize = 1 * 1024 // 1 kB
// cacheTTL is the default time-to-live for the cache entry. // cacheTTL is the default time-to-live for the cache entry.
// this allows the cache to grow to an infinite size for up to a day. // this allows the cache to grow to an infinite size for up to a day.
// this is meant as a temporary solution until the cache is re-written to not have a TTL.
// there is unlikely to be any meaningful memory impact on the server // there is unlikely to be any meaningful memory impact on the server
// because the cache will likely never have more than a few thousand entries // because the cache will likely never have more than a few thousand entries.
// and each entry is roughly ~200 bytes in size. with DEK reuse // each entry can be large due to an internal cache that maps the DEK seed to individual
// and no storage migration, the number of entries in this cache // DEK entries, but that cache has an aggressive TTL to keep the size under control.
// with DEK/seed reuse and no storage migration, the number of entries in this cache
// would be approximated by unique key IDs used by the KMS plugin // would be approximated by unique key IDs used by the KMS plugin
// combined with the number of server restarts. If storage migration // combined with the number of server restarts. If storage migration
// is performed after key ID changes, and the number of restarts // is performed after key ID changes, and the number of restarts
// is limited, this cache size may be as small as the number of API // is limited, this cache size may be as small as the number of API
// servers in use (once old entries expire out from the TTL). // servers in use (once old entries expire out from the TTL).
cacheTTL = 24 * time.Hour cacheTTL = 24 * time.Hour
// error code // key ID related error codes for metrics
errKeyIDOKCode ErrCodeKeyID = "ok" errKeyIDOKCode ErrCodeKeyID = "ok"
errKeyIDEmptyCode ErrCodeKeyID = "empty" errKeyIDEmptyCode ErrCodeKeyID = "empty"
errKeyIDTooLongCode ErrCodeKeyID = "too_long" errKeyIDTooLongCode ErrCodeKeyID = "too_long"
@ -83,23 +86,22 @@ type StateFunc func() (State, error)
type ErrCodeKeyID string type ErrCodeKeyID string
type State struct { type State struct {
Transformer value.Transformer Transformer value.Transformer
EncryptedDEK []byte
KeyID string EncryptedObject kmstypes.EncryptedObject
Annotations map[string][]byte
UID string UID string
ExpirationTimestamp time.Time ExpirationTimestamp time.Time
// CacheKey is the key used to cache the DEK in transformer.cache. // CacheKey is the key used to cache the DEK/seed in envelopeTransformer.cache.
CacheKey []byte CacheKey []byte
} }
func (s *State) ValidateEncryptCapability() error { func (s *State) ValidateEncryptCapability() error {
if now := NowFunc(); now.After(s.ExpirationTimestamp) { if now := NowFunc(); now.After(s.ExpirationTimestamp) {
return fmt.Errorf("EDEK with keyID hash %q expired at %s (current time is %s)", return fmt.Errorf("encryptedDEKSource with keyID hash %q expired at %s (current time is %s)",
GetHashIfNotEmpty(s.KeyID), s.ExpirationTimestamp.Format(time.RFC3339), now.Format(time.RFC3339)) GetHashIfNotEmpty(s.EncryptedObject.KeyID), s.ExpirationTimestamp.Format(time.RFC3339), now.Format(time.RFC3339))
} }
return nil return nil
} }
@ -137,6 +139,8 @@ func (t *envelopeTransformer) TransformFromStorage(ctx context.Context, data []b
return nil, false, err return nil, false, err
} }
useSeed := encryptedObject.EncryptedDEKSourceType == kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED
// TODO: consider marking state.EncryptedDEK != encryptedObject.EncryptedDEK as a stale read to support DEK defragmentation // TODO: consider marking state.EncryptedDEK != encryptedObject.EncryptedDEK as a stale read to support DEK defragmentation
// at a minimum we should have a metric that helps the user understand if DEK fragmentation is high // at a minimum we should have a metric that helps the user understand if DEK fragmentation is high
state, err := t.stateFunc() // no need to call state.ValidateEncryptCapability on reads state, err := t.stateFunc() // no need to call state.ValidateEncryptCapability on reads
@ -144,7 +148,7 @@ func (t *envelopeTransformer) TransformFromStorage(ctx context.Context, data []b
return nil, false, err return nil, false, err
} }
encryptedObjectCacheKey, err := generateCacheKey(encryptedObject.EncryptedDEK, encryptedObject.KeyID, encryptedObject.Annotations) encryptedObjectCacheKey, err := generateCacheKey(encryptedObject.EncryptedDEKSourceType, encryptedObject.EncryptedDEKSource, encryptedObject.KeyID, encryptedObject.Annotations)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@ -163,7 +167,7 @@ func (t *envelopeTransformer) TransformFromStorage(ctx context.Context, data []b
"verb", requestInfo.Verb, "namespace", requestInfo.Namespace, "name", requestInfo.Name) "verb", requestInfo.Verb, "namespace", requestInfo.Namespace, "name", requestInfo.Name)
key, err := t.envelopeService.Decrypt(ctx, uid, &kmsservice.DecryptRequest{ key, err := t.envelopeService.Decrypt(ctx, uid, &kmsservice.DecryptRequest{
Ciphertext: encryptedObject.EncryptedDEK, Ciphertext: encryptedObject.EncryptedDEKSource,
KeyID: encryptedObject.KeyID, KeyID: encryptedObject.KeyID,
Annotations: encryptedObject.Annotations, Annotations: encryptedObject.Annotations,
}) })
@ -171,7 +175,7 @@ func (t *envelopeTransformer) TransformFromStorage(ctx context.Context, data []b
return nil, false, fmt.Errorf("failed to decrypt DEK, error: %w", err) return nil, false, fmt.Errorf("failed to decrypt DEK, error: %w", err)
} }
transformer, err = t.addTransformerForDecryption(encryptedObjectCacheKey, key) transformer, err = t.addTransformerForDecryption(encryptedObjectCacheKey, key, useSeed)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@ -184,8 +188,11 @@ func (t *envelopeTransformer) TransformFromStorage(ctx context.Context, data []b
} }
// data is considered stale if the key ID does not match our current write transformer // data is considered stale if the key ID does not match our current write transformer
return out, stale || encryptedObject.KeyID != state.KeyID, nil return out,
stale ||
encryptedObject.KeyID != state.EncryptedObject.KeyID ||
encryptedObject.EncryptedDEKSourceType != state.EncryptedObject.EncryptedDEKSourceType,
nil
} }
// TransformToStorage encrypts data to be written to disk using envelope encryption. // TransformToStorage encrypts data to be written to disk using envelope encryption.
@ -201,7 +208,7 @@ func (t *envelopeTransformer) TransformToStorage(ctx context.Context, data []byt
// this prevents a cache miss every time the DEK rotates // this prevents a cache miss every time the DEK rotates
// this has the side benefit of causing the cache to perform a GC // this has the side benefit of causing the cache to perform a GC
// TODO see if we can do this inside the stateFunc control loop // TODO see if we can do this inside the stateFunc control loop
// TODO(aramase): Add metrics for cache fill percentage with custom cache implementation. // TODO(aramase): Add metrics for cache size.
t.cache.set(state.CacheKey, state.Transformer) t.cache.set(state.CacheKey, state.Transformer)
requestInfo := getRequestInfoFromContext(ctx) requestInfo := getRequestInfoFromContext(ctx)
@ -214,39 +221,43 @@ func (t *envelopeTransformer) TransformToStorage(ctx context.Context, data []byt
return nil, err return nil, err
} }
metrics.RecordKeyID(metrics.ToStorageLabel, t.providerName, state.KeyID) metrics.RecordKeyID(metrics.ToStorageLabel, t.providerName, state.EncryptedObject.KeyID)
encObject := &kmstypes.EncryptedObject{ encObjectCopy := state.EncryptedObject
KeyID: state.KeyID, encObjectCopy.EncryptedData = result
EncryptedDEK: state.EncryptedDEK,
EncryptedData: result,
Annotations: state.Annotations,
}
// Serialize the EncryptedObject to a byte array. // Serialize the EncryptedObject to a byte array.
return t.doEncode(encObject) return t.doEncode(&encObjectCopy)
} }
// addTransformerForDecryption inserts a new transformer to the Envelope cache of DEKs for future reads. // addTransformerForDecryption inserts a new transformer to the Envelope cache of DEKs for future reads.
func (t *envelopeTransformer) addTransformerForDecryption(cacheKey []byte, key []byte) (value.Read, error) { func (t *envelopeTransformer) addTransformerForDecryption(cacheKey []byte, key []byte, useSeed bool) (value.Read, error) {
block, err := aes.NewCipher(key) var transformer value.Read
var err error
if useSeed {
// the input key is considered safe to use here because it is coming from the KMS plugin / etcd
transformer, err = aestransformer.NewHKDFExtendedNonceGCMTransformer(key)
} else {
var block cipher.Block
block, err = aes.NewCipher(key)
if err != nil {
return nil, err
}
// this is compatible with NewGCMTransformerWithUniqueKeyUnsafe for decryption
// it would use random nonces for encryption but we never do that
transformer, err = aestransformer.NewGCMTransformer(block)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
// this is compatible with NewGCMTransformerWithUniqueKeyUnsafe for decryption // TODO(aramase): Add metrics for cache size.
// it would use random nonces for encryption but we never do that
transformer, err := aestransformer.NewGCMTransformer(block)
if err != nil {
return nil, err
}
// TODO(aramase): Add metrics for cache fill percentage with custom cache implementation.
t.cache.set(cacheKey, transformer) t.cache.set(cacheKey, transformer)
return transformer, nil return transformer, nil
} }
// doEncode encodes the EncryptedObject to a byte array. // doEncode encodes the EncryptedObject to a byte array.
func (t *envelopeTransformer) doEncode(request *kmstypes.EncryptedObject) ([]byte, error) { func (t *envelopeTransformer) doEncode(request *kmstypes.EncryptedObject) ([]byte, error) {
if err := validateEncryptedObject(request); err != nil { if err := ValidateEncryptedObject(request); err != nil {
return nil, err return nil, err
} }
return proto.Marshal(request) return proto.Marshal(request)
@ -258,18 +269,31 @@ func (t *envelopeTransformer) doDecode(originalData []byte) (*kmstypes.Encrypted
if err := proto.Unmarshal(originalData, o); err != nil { if err := proto.Unmarshal(originalData, o); err != nil {
return nil, err return nil, err
} }
// validate the EncryptedObject if err := ValidateEncryptedObject(o); err != nil {
if err := validateEncryptedObject(o); err != nil {
return nil, err return nil, err
} }
return o, nil return o, nil
} }
// GenerateTransformer generates a new transformer and encrypts the DEK using the envelope service. // GenerateTransformer generates a new transformer and encrypts the DEK/seed using the envelope service.
// It returns the transformer, the encrypted DEK, cache key and error. // It returns the transformer, the encrypted DEK/seed, cache key and error.
func GenerateTransformer(ctx context.Context, uid string, envelopeService kmsservice.Service) (value.Transformer, *kmsservice.EncryptResponse, []byte, error) { func GenerateTransformer(ctx context.Context, uid string, envelopeService kmsservice.Service, useSeed bool) (value.Transformer, *kmstypes.EncryptedObject, []byte, error) {
transformer, newKey, err := aestransformer.NewGCMTransformerWithUniqueKeyUnsafe() newTransformerFunc := func() (value.Transformer, []byte, error) {
seed, err := aestransformer.GenerateKey(aestransformer.MinSeedSizeExtendedNonceGCM)
if err != nil {
return nil, nil, err
}
transformer, err := aestransformer.NewHKDFExtendedNonceGCMTransformer(seed)
if err != nil {
return nil, nil, err
}
return transformer, seed, nil
}
if !useSeed {
newTransformerFunc = aestransformer.NewGCMTransformerWithUniqueKeyUnsafe
}
transformer, newKey, err := newTransformerFunc()
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@ -281,32 +305,48 @@ func GenerateTransformer(ctx context.Context, uid string, envelopeService kmsser
return nil, nil, nil, fmt.Errorf("failed to encrypt DEK, error: %w", err) return nil, nil, nil, fmt.Errorf("failed to encrypt DEK, error: %w", err)
} }
if err := validateEncryptedObject(&kmstypes.EncryptedObject{ o := &kmstypes.EncryptedObject{
KeyID: resp.KeyID, KeyID: resp.KeyID,
EncryptedDEK: resp.Ciphertext, EncryptedDEKSource: resp.Ciphertext,
EncryptedData: []byte{0}, // any non-empty value to pass validation EncryptedData: []byte{0}, // any non-empty value to pass validation
Annotations: resp.Annotations, Annotations: resp.Annotations,
}); err != nil { }
if useSeed {
o.EncryptedDEKSourceType = kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED
} else {
o.EncryptedDEKSourceType = kmstypes.EncryptedDEKSourceType_AES_GCM_KEY
}
if err := ValidateEncryptedObject(o); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
cacheKey, err := generateCacheKey(resp.Ciphertext, resp.KeyID, resp.Annotations) cacheKey, err := generateCacheKey(o.EncryptedDEKSourceType, resp.Ciphertext, resp.KeyID, resp.Annotations)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
return transformer, resp, cacheKey, nil o.EncryptedData = nil // make sure that later code that uses this encrypted object sets this field
return transformer, o, cacheKey, nil
} }
func validateEncryptedObject(o *kmstypes.EncryptedObject) error { func ValidateEncryptedObject(o *kmstypes.EncryptedObject) error {
if o == nil { if o == nil {
return fmt.Errorf("encrypted object is nil") return fmt.Errorf("encrypted object is nil")
} }
switch t := o.EncryptedDEKSourceType; t {
case kmstypes.EncryptedDEKSourceType_AES_GCM_KEY:
case kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED:
default:
return fmt.Errorf("unknown encryptedDEKSourceType: %d", t)
}
if len(o.EncryptedData) == 0 { if len(o.EncryptedData) == 0 {
return fmt.Errorf("encrypted data is empty") return fmt.Errorf("encrypted data is empty")
} }
if err := validateEncryptedDEK(o.EncryptedDEK); err != nil { if err := validateEncryptedDEKSource(o.EncryptedDEKSource); err != nil {
return fmt.Errorf("failed to validate encrypted DEK: %w", err) return fmt.Errorf("failed to validate encrypted DEK source: %w", err)
} }
if _, err := ValidateKeyID(o.KeyID); err != nil { if _, err := ValidateKeyID(o.KeyID); err != nil {
return fmt.Errorf("failed to validate key id: %w", err) return fmt.Errorf("failed to validate key id: %w", err)
@ -317,15 +357,15 @@ func validateEncryptedObject(o *kmstypes.EncryptedObject) error {
return nil return nil
} }
// validateEncryptedDEK tests the following: // validateEncryptedDEKSource tests the following:
// 1. The encrypted DEK is not empty. // 1. The encrypted DEK source is not empty.
// 2. The size of encrypted DEK is less than 1 kB. // 2. The size of encrypted DEK source is less than 1 kB.
func validateEncryptedDEK(encryptedDEK []byte) error { func validateEncryptedDEKSource(encryptedDEKSource []byte) error {
if len(encryptedDEK) == 0 { if len(encryptedDEKSource) == 0 {
return fmt.Errorf("encrypted DEK is empty") return fmt.Errorf("encrypted DEK source is empty")
} }
if len(encryptedDEK) > encryptedDEKMaxSize { if len(encryptedDEKSource) > encryptedDEKSourceMaxSize {
return fmt.Errorf("encrypted DEK is %d bytes, which exceeds the max size of %d", len(encryptedDEK), encryptedDEKMaxSize) return fmt.Errorf("encrypted DEK source is %d bytes, which exceeds the max size of %d", len(encryptedDEKSource), encryptedDEKSourceMaxSize)
} }
return nil return nil
} }
@ -370,17 +410,19 @@ func getRequestInfoFromContext(ctx context.Context) *genericapirequest.RequestIn
// generateCacheKey returns a key for the cache. // generateCacheKey returns a key for the cache.
// The key is a concatenation of: // The key is a concatenation of:
// 1. encryptedDEK // 0. encryptedDEKSourceType
// 1. encryptedDEKSource
// 2. keyID // 2. keyID
// 3. length of annotations // 3. length of annotations
// 4. annotations (sorted by key) - each annotation is a concatenation of: // 4. annotations (sorted by key) - each annotation is a concatenation of:
// a. annotation key // a. annotation key
// b. annotation value // b. annotation value
func generateCacheKey(encryptedDEK []byte, keyID string, annotations map[string][]byte) ([]byte, error) { func generateCacheKey(encryptedDEKSourceType kmstypes.EncryptedDEKSourceType, encryptedDEKSource []byte, keyID string, annotations map[string][]byte) ([]byte, error) {
// TODO(aramase): use sync pool buffer to avoid allocations // TODO(aramase): use sync pool buffer to avoid allocations
b := cryptobyte.NewBuilder(nil) b := cryptobyte.NewBuilder(nil)
b.AddUint32(uint32(encryptedDEKSourceType))
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(encryptedDEK) b.AddBytes(encryptedDEKSource)
}) })
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(toBytes(keyID)) b.AddBytes(toBytes(keyID))

View File

@ -27,11 +27,13 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
"github.com/gogo/protobuf/proto" "github.com/gogo/protobuf/proto"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/uuid"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage/value" "k8s.io/apiserver/pkg/storage/value"
@ -167,7 +169,9 @@ func TestEnvelopeCaching(t *testing.T) {
envelopeService := newTestEnvelopeService() envelopeService := newTestEnvelopeService()
fakeClock := testingclock.NewFakeClock(time.Now()) fakeClock := testingclock.NewFakeClock(time.Now())
state, err := testStateFunc(ctx, envelopeService, fakeClock)() useSeed := randomBool()
state, err := testStateFunc(ctx, envelopeService, fakeClock, useSeed)()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -196,7 +200,7 @@ func TestEnvelopeCaching(t *testing.T) {
// force GC to run by performing a write // force GC to run by performing a write
transformer.(*envelopeTransformer).cache.set([]byte("some-other-unrelated-key"), &envelopeTransformer{}) transformer.(*envelopeTransformer).cache.set([]byte("some-other-unrelated-key"), &envelopeTransformer{})
state, err = testStateFunc(ctx, envelopeService, fakeClock)() state, err = testStateFunc(ctx, envelopeService, fakeClock, useSeed)()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -228,17 +232,15 @@ func TestEnvelopeCaching(t *testing.T) {
} }
} }
func testStateFunc(ctx context.Context, envelopeService kmsservice.Service, clock clock.Clock) func() (State, error) { func testStateFunc(ctx context.Context, envelopeService kmsservice.Service, clock clock.Clock, useSeed bool) func() (State, error) {
return func() (State, error) { return func() (State, error) {
transformer, resp, cacheKey, errGen := GenerateTransformer(ctx, string(uuid.NewUUID()), envelopeService) transformer, encObject, cacheKey, errGen := GenerateTransformer(ctx, string(uuid.NewUUID()), envelopeService, useSeed)
if errGen != nil { if errGen != nil {
return State{}, errGen return State{}, errGen
} }
return State{ return State{
Transformer: transformer, Transformer: transformer,
EncryptedDEK: resp.Ciphertext, EncryptedObject: *encObject,
KeyID: resp.KeyID,
Annotations: resp.Annotations,
UID: "panda", UID: "panda",
ExpirationTimestamp: clock.Now().Add(time.Hour), ExpirationTimestamp: clock.Now().Add(time.Hour),
CacheKey: cacheKey, CacheKey: cacheKey,
@ -254,6 +256,8 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
expectedStale bool expectedStale bool
testErr error testErr error
testKeyID string testKeyID string
useSeedWrite bool
useSeedRead bool
}{ }{
{ {
desc: "stateFunc returns err", desc: "stateFunc returns err",
@ -262,11 +266,35 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
testKeyID: "", testKeyID: "",
}, },
{ {
desc: "stateFunc returns same keyID", desc: "stateFunc returns same keyID, not using seed",
expectedStale: false, expectedStale: false,
testErr: nil, testErr: nil,
testKeyID: testKeyVersion, testKeyID: testKeyVersion,
}, },
{
desc: "stateFunc returns same keyID, using seed",
expectedStale: false,
testErr: nil,
testKeyID: testKeyVersion,
useSeedWrite: true,
useSeedRead: true,
},
{
desc: "stateFunc returns same keyID, migrating away from seed",
expectedStale: true,
testErr: nil,
testKeyID: testKeyVersion,
useSeedWrite: true,
useSeedRead: false,
},
{
desc: "stateFunc returns same keyID, migrating to seed",
expectedStale: true,
testErr: nil,
testKeyID: testKeyVersion,
useSeedWrite: false,
useSeedRead: true,
},
{ {
desc: "stateFunc returns different keyID", desc: "stateFunc returns different keyID",
expectedStale: true, expectedStale: true,
@ -283,7 +311,7 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
ctx := testContext(t) ctx := testContext(t)
envelopeService := newTestEnvelopeService() envelopeService := newTestEnvelopeService()
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{})() state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, tt.useSeedWrite)()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -302,7 +330,12 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
} }
// inject test data before performing a read // inject test data before performing a read
state.KeyID = tt.testKeyID state.EncryptedObject.KeyID = tt.testKeyID
if tt.useSeedRead {
state.EncryptedObject.EncryptedDEKSourceType = kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED
} else {
state.EncryptedObject.EncryptedDEKSourceType = kmstypes.EncryptedDEKSourceType_AES_GCM_KEY
}
stateErr = tt.testErr stateErr = tt.testErr
_, stale, err := transformer.TransformFromStorage(ctx, transformedData, dataCtx) _, stale, err := transformer.TransformFromStorage(ctx, transformedData, dataCtx)
@ -330,8 +363,10 @@ func TestEnvelopeTransformerStateFunc(t *testing.T) {
ctx := testContext(t) ctx := testContext(t)
useSeed := randomBool()
envelopeService := newTestEnvelopeService() envelopeService := newTestEnvelopeService()
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{})() state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, useSeed)()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -351,12 +386,18 @@ func TestEnvelopeTransformerStateFunc(t *testing.T) {
if err != stateErr { if err != stateErr {
t.Fatalf("expected state error, got: %v", err) t.Fatalf("expected state error, got: %v", err)
} }
data, err := proto.Marshal(&kmstypes.EncryptedObject{ o := &kmstypes.EncryptedObject{
EncryptedData: []byte{1}, EncryptedData: []byte{1},
KeyID: "2", KeyID: "2",
EncryptedDEK: []byte{3}, EncryptedDEKSource: []byte{3},
Annotations: nil, Annotations: nil,
}) }
if useSeed {
o.EncryptedDEKSourceType = kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED
} else {
o.EncryptedDEKSourceType = kmstypes.EncryptedDEKSourceType_AES_GCM_KEY
}
data, err := proto.Marshal(o)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -401,7 +442,7 @@ func TestEnvelopeTransformerStateFunc(t *testing.T) {
t.Run("writes fail when the plugin is down and the state is invalid", func(t *testing.T) { t.Run("writes fail when the plugin is down and the state is invalid", func(t *testing.T) {
_, err := transformer.TransformToStorage(ctx, originalText, dataCtx) _, err := transformer.TransformToStorage(ctx, originalText, dataCtx)
if !strings.Contains(errString(err), `EDEK with keyID hash "sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" expired at`) { if !strings.Contains(errString(err), `encryptedDEKSource with keyID hash "sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" expired at`) {
t.Fatalf("expected expiration error, got: %v", err) t.Fatalf("expected expiration error, got: %v", err)
} }
}) })
@ -418,7 +459,9 @@ func TestEnvelopeTransformerStateFunc(t *testing.T) {
if err := proto.Unmarshal(encryptedData, obj); err != nil { if err := proto.Unmarshal(encryptedData, obj); err != nil {
t.Fatal(err) t.Fatal(err)
} }
obj.EncryptedDEK = append(obj.EncryptedDEK, 1) // skip StateFunc transformer
obj.EncryptedDEKSource = append(obj.EncryptedDEKSource, 1) // skip StateFunc transformer
data, err := proto.Marshal(obj) data, err := proto.Marshal(obj)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -468,7 +511,7 @@ func TestTransformToStorageError(t *testing.T) {
envelopeService := newTestEnvelopeService() envelopeService := newTestEnvelopeService()
envelopeService.SetAnnotations(tt.annotations) envelopeService.SetAnnotations(tt.annotations)
transformer := NewEnvelopeTransformer(envelopeService, testProviderName, transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
testStateFunc(ctx, envelopeService, clock.RealClock{}), testStateFunc(ctx, envelopeService, clock.RealClock{}, randomBool()),
) )
dataCtx := value.DefaultContext(testContextText) dataCtx := value.DefaultContext(testContextText)
@ -487,9 +530,9 @@ func TestEncodeDecode(t *testing.T) {
transformer := &envelopeTransformer{} transformer := &envelopeTransformer{}
obj := &kmstypes.EncryptedObject{ obj := &kmstypes.EncryptedObject{
EncryptedData: []byte{0x01, 0x02, 0x03}, EncryptedData: []byte{0x01, 0x02, 0x03},
KeyID: "1", KeyID: "1",
EncryptedDEK: []byte{0x04, 0x05, 0x06}, EncryptedDEKSource: []byte{0x04, 0x05, 0x06},
} }
data, err := transformer.doEncode(obj) data, err := transformer.doEncode(obj)
@ -522,24 +565,62 @@ func TestValidateEncryptedObject(t *testing.T) {
{ {
desc: "encrypted data is nil", desc: "encrypted data is nil",
originalData: &kmstypes.EncryptedObject{ originalData: &kmstypes.EncryptedObject{
KeyID: "1", KeyID: "1",
EncryptedDEK: []byte{0x01, 0x02, 0x03}, EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
}, },
expectedError: fmt.Errorf("encrypted data is empty"), expectedError: fmt.Errorf("encrypted data is empty"),
}, },
{ {
desc: "encrypted data is []byte{}", desc: "encrypted data is []byte{}",
originalData: &kmstypes.EncryptedObject{ originalData: &kmstypes.EncryptedObject{
EncryptedDEK: []byte{0x01, 0x02, 0x03}, EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
EncryptedData: []byte{}, EncryptedData: []byte{},
}, },
expectedError: fmt.Errorf("encrypted data is empty"), expectedError: fmt.Errorf("encrypted data is empty"),
}, },
{
desc: "invalid dek source type",
originalData: &kmstypes.EncryptedObject{
EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
EncryptedData: []byte{0},
EncryptedDEKSourceType: 55,
},
expectedError: fmt.Errorf("unknown encryptedDEKSourceType: 55"),
},
{
desc: "empty dek source",
originalData: &kmstypes.EncryptedObject{
EncryptedData: []byte{0},
EncryptedDEKSourceType: 1,
KeyID: "1",
},
expectedError: fmt.Errorf("failed to validate encrypted DEK source: encrypted DEK source is empty"),
},
{
desc: "empty key ID",
originalData: &kmstypes.EncryptedObject{
EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
EncryptedData: []byte{0},
EncryptedDEKSourceType: 1,
},
expectedError: fmt.Errorf("failed to validate key id: keyID is empty"),
},
{
desc: "invalid annotations",
originalData: &kmstypes.EncryptedObject{
EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
EncryptedData: []byte{0},
EncryptedDEKSourceType: 1,
KeyID: "1",
Annotations: map[string][]byte{"@": nil},
},
expectedError: fmt.Errorf(`failed to validate annotations: annotations: Invalid value: "@": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
},
} }
for _, tt := range testCases { for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
err := validateEncryptedObject(tt.originalData) err := ValidateEncryptedObject(tt.originalData)
if err == nil { if err == nil {
t.Fatalf("envelopeTransformer: expected error while decoding data, got nil") t.Fatalf("envelopeTransformer: expected error while decoding data, got nil")
} }
@ -702,32 +783,32 @@ func TestValidateKeyID(t *testing.T) {
} }
} }
func TestValidateEncryptedDEK(t *testing.T) { func TestValidateEncryptedDEKSource(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
encryptedDEK []byte encryptedDEKSource []byte
expectedError string expectedError string
}{ }{
{ {
name: "encrypted DEK is nil", name: "encrypted DEK source is nil",
encryptedDEK: nil, encryptedDEKSource: nil,
expectedError: "encrypted DEK is empty", expectedError: "encrypted DEK source is empty",
}, },
{ {
name: "encrypted DEK is empty", name: "encrypted DEK source is empty",
encryptedDEK: []byte{}, encryptedDEKSource: []byte{},
expectedError: "encrypted DEK is empty", expectedError: "encrypted DEK source is empty",
}, },
{ {
name: "encrypted DEK size is greater than 1 kB", name: "encrypted DEK source size is greater than 1 kB",
encryptedDEK: bytes.Repeat([]byte("a"), 1024+1), encryptedDEKSource: bytes.Repeat([]byte("a"), 1024+1),
expectedError: "which exceeds the max size of", expectedError: "which exceeds the max size of",
}, },
{ {
name: "valid encrypted DEK", name: "valid encrypted DEK source",
encryptedDEK: []byte{0x01, 0x02, 0x03}, encryptedDEKSource: []byte{0x01, 0x02, 0x03},
expectedError: "", expectedError: "",
}, },
} }
@ -735,7 +816,7 @@ func TestValidateEncryptedDEK(t *testing.T) {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
err := validateEncryptedDEK(tt.encryptedDEK) err := validateEncryptedDEKSource(tt.encryptedDEKSource)
if tt.expectedError != "" { if tt.expectedError != "" {
if err == nil { if err == nil {
t.Fatalf("expected error %q, got nil", tt.expectedError) t.Fatalf("expected error %q, got nil", tt.expectedError)
@ -755,7 +836,7 @@ func TestValidateEncryptedDEK(t *testing.T) {
func TestEnvelopeMetrics(t *testing.T) { func TestEnvelopeMetrics(t *testing.T) {
envelopeService := newTestEnvelopeService() envelopeService := newTestEnvelopeService()
transformer := NewEnvelopeTransformer(envelopeService, testProviderName, transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
testStateFunc(testContext(t), envelopeService, clock.RealClock{}), testStateFunc(testContext(t), envelopeService, clock.RealClock{}, randomBool()),
) )
dataCtx := value.DefaultContext(testContextText) dataCtx := value.DefaultContext(testContextText)
@ -809,8 +890,12 @@ func TestEnvelopeMetrics(t *testing.T) {
} }
} }
var flagOnce sync.Once // support running `go test -count X`
func TestEnvelopeLogging(t *testing.T) { func TestEnvelopeLogging(t *testing.T) {
klog.InitFlags(nil) flagOnce.Do(func() {
klog.InitFlags(nil)
})
flag.Set("v", "6") flag.Set("v", "6")
flag.Parse() flag.Parse()
@ -858,7 +943,7 @@ func TestEnvelopeLogging(t *testing.T) {
envelopeService := newTestEnvelopeService() envelopeService := newTestEnvelopeService()
fakeClock := testingclock.NewFakeClock(time.Now()) fakeClock := testingclock.NewFakeClock(time.Now())
transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName, transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName,
testStateFunc(tc.ctx, envelopeService, clock.RealClock{}), testStateFunc(tc.ctx, envelopeService, clock.RealClock{}, randomBool()),
1*time.Second, fakeClock) 1*time.Second, fakeClock)
dataCtx := value.DefaultContext([]byte(testContextText)) dataCtx := value.DefaultContext([]byte(testContextText))
@ -905,7 +990,7 @@ func TestCacheNotCorrupted(t *testing.T) {
fakeClock := testingclock.NewFakeClock(time.Now()) fakeClock := testingclock.NewFakeClock(time.Now())
state, err := testStateFunc(ctx, envelopeService, fakeClock)() state, err := testStateFunc(ctx, envelopeService, fakeClock, randomBool())()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -923,15 +1008,15 @@ func TestCacheNotCorrupted(t *testing.T) {
} }
// this is to mimic a plugin that sets a static response for ciphertext // this is to mimic a plugin that sets a static response for ciphertext
// but uses the annotation field to send the actual encrypted DEK. // but uses the annotation field to send the actual encrypted DEK source.
envelopeService.SetCiphertext(state.EncryptedDEK) envelopeService.SetCiphertext(state.EncryptedObject.EncryptedDEKSource)
// for this plugin, it indicates a change in the remote key ID as the returned // for this plugin, it indicates a change in the remote key ID as the returned
// encrypted DEK is different. // encrypted DEK source is different.
envelopeService.SetAnnotations(map[string][]byte{ envelopeService.SetAnnotations(map[string][]byte{
"encrypted-dek.kms.kubernetes.io": []byte("encrypted-dek-1"), "encrypted-dek.kms.kubernetes.io": []byte("encrypted-dek-1"),
}) })
state, err = testStateFunc(ctx, envelopeService, fakeClock)() state, err = testStateFunc(ctx, envelopeService, fakeClock, randomBool())()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -954,28 +1039,40 @@ func TestCacheNotCorrupted(t *testing.T) {
} }
func TestGenerateCacheKey(t *testing.T) { func TestGenerateCacheKey(t *testing.T) {
encryptedDEK1 := []byte{1, 2, 3} encryptedDEKSource1 := []byte{1, 2, 3}
keyID1 := "id1" keyID1 := "id1"
annotations1 := map[string][]byte{"a": {4, 5}, "b": {6, 7}} annotations1 := map[string][]byte{"a": {4, 5}, "b": {6, 7}}
encryptedDEKSourceType1 := kmstypes.EncryptedDEKSourceType_AES_GCM_KEY
encryptedDEK2 := []byte{4, 5, 6} encryptedDEKSource2 := []byte{4, 5, 6}
keyID2 := "id2" keyID2 := "id2"
annotations2 := map[string][]byte{"x": {9, 10}, "y": {11, 12}} annotations2 := map[string][]byte{"x": {9, 10}, "y": {11, 12}}
encryptedDEKSourceType2 := kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED
// generate all possible combinations of the above // generate all possible combinations of the above
testCases := []struct { testCases := []struct {
encryptedDEK []byte encryptedDEKSourceType kmstypes.EncryptedDEKSourceType
keyID string encryptedDEKSource []byte
annotations map[string][]byte keyID string
annotations map[string][]byte
}{ }{
{encryptedDEK1, keyID1, annotations1}, {encryptedDEKSourceType1, encryptedDEKSource1, keyID1, annotations1},
{encryptedDEK1, keyID1, annotations2}, {encryptedDEKSourceType1, encryptedDEKSource1, keyID1, annotations2},
{encryptedDEK1, keyID2, annotations1}, {encryptedDEKSourceType1, encryptedDEKSource1, keyID2, annotations1},
{encryptedDEK1, keyID2, annotations2}, {encryptedDEKSourceType1, encryptedDEKSource1, keyID2, annotations2},
{encryptedDEK2, keyID1, annotations1}, {encryptedDEKSourceType1, encryptedDEKSource2, keyID1, annotations1},
{encryptedDEK2, keyID1, annotations2}, {encryptedDEKSourceType1, encryptedDEKSource2, keyID1, annotations2},
{encryptedDEK2, keyID2, annotations1}, {encryptedDEKSourceType1, encryptedDEKSource2, keyID2, annotations1},
{encryptedDEK2, keyID2, annotations2}, {encryptedDEKSourceType1, encryptedDEKSource2, keyID2, annotations2},
{encryptedDEKSourceType2, encryptedDEKSource1, keyID1, annotations1},
{encryptedDEKSourceType2, encryptedDEKSource1, keyID1, annotations2},
{encryptedDEKSourceType2, encryptedDEKSource1, keyID2, annotations1},
{encryptedDEKSourceType2, encryptedDEKSource1, keyID2, annotations2},
{encryptedDEKSourceType2, encryptedDEKSource2, keyID1, annotations1},
{encryptedDEKSourceType2, encryptedDEKSource2, keyID1, annotations2},
{encryptedDEKSourceType2, encryptedDEKSource2, keyID2, annotations1},
{encryptedDEKSourceType2, encryptedDEKSource2, keyID2, annotations2},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -983,8 +1080,8 @@ func TestGenerateCacheKey(t *testing.T) {
for _, tc2 := range testCases { for _, tc2 := range testCases {
tc2 := tc2 tc2 := tc2
t.Run(fmt.Sprintf("%+v-%+v", tc, tc2), func(t *testing.T) { t.Run(fmt.Sprintf("%+v-%+v", tc, tc2), func(t *testing.T) {
key1, err1 := generateCacheKey(tc.encryptedDEK, tc.keyID, tc.annotations) key1, err1 := generateCacheKey(tc.encryptedDEKSourceType, tc.encryptedDEKSource, tc.keyID, tc.annotations)
key2, err2 := generateCacheKey(tc2.encryptedDEK, tc2.keyID, tc2.annotations) key2, err2 := generateCacheKey(tc2.encryptedDEKSourceType, tc2.encryptedDEKSource, tc2.keyID, tc2.annotations)
if err1 != nil || err2 != nil { if err1 != nil || err2 != nil {
t.Errorf("generateCacheKey() want err=nil, got err1=%q, err2=%q", errString(err1), errString(err2)) t.Errorf("generateCacheKey() want err=nil, got err1=%q, err2=%q", errString(err1), errString(err2))
} }
@ -1028,7 +1125,7 @@ func TestGenerateTransformer(t *testing.T) {
envelopeService.SetCiphertext([]byte{}) envelopeService.SetCiphertext([]byte{})
return envelopeService return envelopeService
}, },
expectedErr: "failed to validate encrypted DEK: encrypted DEK is empty", expectedErr: "failed to validate encrypted DEK source: encrypted DEK source is empty",
}, },
{ {
name: "invalid annotations", name: "invalid annotations",
@ -1053,7 +1150,7 @@ func TestGenerateTransformer(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
transformer, encryptResp, cacheKey, err := GenerateTransformer(testContext(t), "panda", tc.envelopeService()) transformer, encObject, cacheKey, err := GenerateTransformer(testContext(t), "panda", tc.envelopeService(), randomBool())
if tc.expectedErr == "" { if tc.expectedErr == "" {
if err != nil { if err != nil {
t.Errorf("expected no error, got %q", errString(err)) t.Errorf("expected no error, got %q", errString(err))
@ -1061,7 +1158,7 @@ func TestGenerateTransformer(t *testing.T) {
if transformer == nil { if transformer == nil {
t.Error("expected transformer, got nil") t.Error("expected transformer, got nil")
} }
if encryptResp == nil { if encObject == nil {
t.Error("expected encrypt response, got nil") t.Error("expected encrypt response, got nil")
} }
if cacheKey == nil { if cacheKey == nil {
@ -1083,3 +1180,5 @@ func errString(err error) string {
return err.Error() return err.Error()
} }
func randomBool() bool { return utilrand.Int()%2 == 1 }

View File

@ -370,6 +370,7 @@ func TestKMSOperationsMetric(t *testing.T) {
} }
defer destroyService(service) defer destroyService(service)
metrics.RegisterMetrics() metrics.RegisterMetrics()
metrics.KMSOperationsLatencyMetric.Reset() // support running `go test -count X`
testCases := []struct { testCases := []struct {
name string name string

View File

@ -36,19 +36,52 @@ var _ = math.Inf
// proto package needs to be updated. // proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type EncryptedDEKSourceType int32
const (
// AES_GCM_KEY means that the plaintext of encryptedDEKSource is the DEK itself, with AES-GCM as the encryption algorithm.
EncryptedDEKSourceType_AES_GCM_KEY EncryptedDEKSourceType = 0
// HKDF_SHA256_XNONCE_AES_GCM_SEED means that the plaintext of encryptedDEKSource is the pseudo random key
// (referred to as the seed throughout the code) that is fed into HKDF expand. SHA256 is the hash algorithm
// and first 32 bytes of encryptedData are the info param. The first 32 bytes from the HKDF stream are used
// as the DEK with AES-GCM as the encryption algorithm.
EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED EncryptedDEKSourceType = 1
)
var EncryptedDEKSourceType_name = map[int32]string{
0: "AES_GCM_KEY",
1: "HKDF_SHA256_XNONCE_AES_GCM_SEED",
}
var EncryptedDEKSourceType_value = map[string]int32{
"AES_GCM_KEY": 0,
"HKDF_SHA256_XNONCE_AES_GCM_SEED": 1,
}
func (x EncryptedDEKSourceType) String() string {
return proto.EnumName(EncryptedDEKSourceType_name, int32(x))
}
func (EncryptedDEKSourceType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_00212fb1f9d3bf1c, []int{0}
}
// EncryptedObject is the representation of data stored in etcd after envelope encryption. // EncryptedObject is the representation of data stored in etcd after envelope encryption.
type EncryptedObject struct { type EncryptedObject struct {
// EncryptedData is the encrypted data. // EncryptedData is the encrypted data.
EncryptedData []byte `protobuf:"bytes,1,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"` EncryptedData []byte `protobuf:"bytes,1,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"`
// KeyID is the KMS key ID used for encryption operations. // KeyID is the KMS key ID used for encryption operations.
KeyID string `protobuf:"bytes,2,opt,name=keyID,proto3" json:"keyID,omitempty"` KeyID string `protobuf:"bytes,2,opt,name=keyID,proto3" json:"keyID,omitempty"`
// EncryptedDEK is the encrypted DEK. // EncryptedDEKSource is the ciphertext of the source of the DEK used to encrypt the data stored in encryptedData.
EncryptedDEK []byte `protobuf:"bytes,3,opt,name=encryptedDEK,proto3" json:"encryptedDEK,omitempty"` // encryptedDEKSourceType defines the process of using the plaintext of this field to determine the aforementioned DEK.
EncryptedDEKSource []byte `protobuf:"bytes,3,opt,name=encryptedDEKSource,proto3" json:"encryptedDEKSource,omitempty"`
// Annotations is additional metadata that was provided by the KMS plugin. // Annotations is additional metadata that was provided by the KMS plugin.
Annotations map[string][]byte `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Annotations map[string][]byte `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` // encryptedDEKSourceType defines the process of using the plaintext of encryptedDEKSource to determine the DEK.
XXX_unrecognized []byte `json:"-"` EncryptedDEKSourceType EncryptedDEKSourceType `protobuf:"varint,5,opt,name=encryptedDEKSourceType,proto3,enum=v2.EncryptedDEKSourceType" json:"encryptedDEKSourceType,omitempty"`
XXX_sizecache int32 `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
} }
func (m *EncryptedObject) Reset() { *m = EncryptedObject{} } func (m *EncryptedObject) Reset() { *m = EncryptedObject{} }
@ -89,9 +122,9 @@ func (m *EncryptedObject) GetKeyID() string {
return "" return ""
} }
func (m *EncryptedObject) GetEncryptedDEK() []byte { func (m *EncryptedObject) GetEncryptedDEKSource() []byte {
if m != nil { if m != nil {
return m.EncryptedDEK return m.EncryptedDEKSource
} }
return nil return nil
} }
@ -103,7 +136,15 @@ func (m *EncryptedObject) GetAnnotations() map[string][]byte {
return nil return nil
} }
func (m *EncryptedObject) GetEncryptedDEKSourceType() EncryptedDEKSourceType {
if m != nil {
return m.EncryptedDEKSourceType
}
return EncryptedDEKSourceType_AES_GCM_KEY
}
func init() { func init() {
proto.RegisterEnum("v2.EncryptedDEKSourceType", EncryptedDEKSourceType_name, EncryptedDEKSourceType_value)
proto.RegisterType((*EncryptedObject)(nil), "v2.EncryptedObject") proto.RegisterType((*EncryptedObject)(nil), "v2.EncryptedObject")
proto.RegisterMapType((map[string][]byte)(nil), "v2.EncryptedObject.AnnotationsEntry") proto.RegisterMapType((map[string][]byte)(nil), "v2.EncryptedObject.AnnotationsEntry")
} }
@ -111,21 +152,26 @@ func init() {
func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) }
var fileDescriptor_00212fb1f9d3bf1c = []byte{ var fileDescriptor_00212fb1f9d3bf1c = []byte{
// 244 bytes of a gzipped FileDescriptorProto // 329 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x90, 0xb1, 0x4b, 0x03, 0x31, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0xe1, 0x4b, 0xc2, 0x40,
0x14, 0xc6, 0xc9, 0x9d, 0x0a, 0x97, 0x9e, 0x58, 0x82, 0xc3, 0xe1, 0x74, 0x94, 0x0e, 0x37, 0x25, 0x18, 0xc6, 0xdb, 0xcc, 0xc0, 0xd3, 0x72, 0x1c, 0x21, 0xc3, 0x2f, 0x8d, 0xf2, 0xc3, 0xe8, 0xc3,
0x10, 0x97, 0x22, 0x52, 0x50, 0x7a, 0x82, 0x38, 0x08, 0x19, 0xdd, 0xd2, 0xfa, 0x28, 0x67, 0x6a, 0x0e, 0x16, 0x85, 0x44, 0x08, 0xe6, 0xce, 0x0c, 0x49, 0x61, 0xeb, 0x43, 0xf5, 0x65, 0x9c, 0xf6,
0x12, 0x92, 0x18, 0xc8, 0x9f, 0xee, 0x26, 0x4d, 0x95, 0xda, 0xdb, 0xde, 0xf7, 0xf1, 0xfb, 0xe0, 0x22, 0x6b, 0xb6, 0x1b, 0xb7, 0xf3, 0x60, 0x7f, 0x6a, 0xff, 0x4d, 0x38, 0x13, 0xd3, 0xec, 0xdb,
0xc7, 0xc3, 0x95, 0xb4, 0x03, 0xb5, 0xce, 0x04, 0x43, 0x8a, 0xc8, 0x67, 0xdf, 0x08, 0x5f, 0xf5, 0xbd, 0xef, 0xfd, 0xde, 0xe7, 0xb9, 0x7b, 0x5e, 0x54, 0x61, 0x69, 0xe4, 0xa4, 0x82, 0x4b, 0x8e,
0x7a, 0xe3, 0x92, 0x0d, 0xf0, 0xfe, 0xba, 0xfe, 0x80, 0x4d, 0x20, 0x73, 0x7c, 0x09, 0x7f, 0xd5, 0x75, 0xe5, 0x9e, 0x7f, 0xe9, 0xa8, 0x4e, 0x93, 0xa9, 0xc8, 0x53, 0x09, 0xef, 0xe3, 0xc9, 0x07,
0x4a, 0x06, 0xd9, 0xa0, 0x16, 0x75, 0xb5, 0x38, 0x2d, 0xc9, 0x35, 0x3e, 0x57, 0x90, 0x9e, 0x57, 0x4c, 0x25, 0x6e, 0xa1, 0x63, 0x58, 0xb7, 0x3c, 0x26, 0x99, 0xa9, 0x59, 0x9a, 0x5d, 0xf3, 0xb7,
0x4d, 0xd1, 0xa2, 0xae, 0x12, 0x87, 0x40, 0x66, 0xb8, 0x3e, 0x62, 0xfd, 0x4b, 0x53, 0xe6, 0xe9, 0x9b, 0xf8, 0x14, 0x95, 0x63, 0xc8, 0x1f, 0x3d, 0x53, 0xb7, 0x34, 0xbb, 0xe2, 0xaf, 0x0a, 0xec,
0x49, 0x47, 0x9e, 0xf0, 0x44, 0x6a, 0x6d, 0x82, 0x0c, 0x83, 0xd1, 0xbe, 0x39, 0x6b, 0xcb, 0x6e, 0x20, 0xbc, 0xc1, 0xe8, 0x30, 0xe0, 0x0b, 0x31, 0x05, 0xb3, 0x54, 0x08, 0xec, 0xb9, 0xc1, 0x7d,
0xc2, 0xe7, 0x34, 0x72, 0x3a, 0x32, 0xa1, 0x0f, 0x47, 0xac, 0xd7, 0xc1, 0x25, 0xf1, 0x7f, 0x78, 0x54, 0x65, 0x49, 0xc2, 0x25, 0x93, 0x11, 0x4f, 0x32, 0xf3, 0xd0, 0x2a, 0xd9, 0x55, 0xb7, 0xe5,
0xb3, 0xc4, 0xd3, 0x31, 0x40, 0xa6, 0xb8, 0x54, 0x90, 0xb2, 0x71, 0x25, 0xf6, 0xe7, 0xde, 0x33, 0x28, 0xd7, 0xd9, 0x79, 0x95, 0xd3, 0xdd, 0x60, 0x34, 0x91, 0x22, 0xf7, 0x7f, 0x0f, 0x62, 0x1f,
0xca, 0xdd, 0x17, 0x64, 0xcf, 0x5a, 0x1c, 0xc2, 0x5d, 0xb1, 0x40, 0x8f, 0xcb, 0xb7, 0x7b, 0xb5, 0x35, 0xfe, 0xaa, 0x3f, 0xe7, 0x29, 0x98, 0x65, 0x4b, 0xb3, 0x4f, 0xdc, 0xe6, 0x96, 0xe4, 0x16,
0xf0, 0x74, 0x30, 0x4c, 0xda, 0xc1, 0x83, 0x8b, 0xe0, 0x98, 0x55, 0x5b, 0xe6, 0x83, 0x71, 0x72, 0xe1, 0xff, 0x33, 0xd9, 0xec, 0x20, 0x63, 0xd7, 0x14, 0x1b, 0xa8, 0x14, 0x43, 0x5e, 0x24, 0x52,
0x0b, 0x2c, 0x93, 0xec, 0x57, 0x9d, 0x81, 0x8e, 0xb0, 0x33, 0x16, 0x98, 0xfa, 0xf4, 0x91, 0xb3, 0xf1, 0x97, 0xc7, 0x65, 0x0e, 0x8a, 0xcd, 0x17, 0x50, 0xe4, 0x50, 0xf3, 0x57, 0xc5, 0xad, 0xde,
0xc8, 0xd7, 0x17, 0xf9, 0x8d, 0xb7, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x80, 0x43, 0x93, 0xd6, 0x2e, 0x47, 0xa8, 0xb1, 0xdf, 0x11, 0xd7, 0x51, 0xb5, 0x4b, 0x83, 0xf0, 0xa1, 0xf7, 0x14,
0x53, 0x01, 0x00, 0x00, 0x0e, 0xe9, 0xab, 0x71, 0x80, 0x2f, 0xd0, 0xd9, 0x60, 0xe8, 0xf5, 0xc3, 0x60, 0xd0, 0x75, 0xaf,
0x6f, 0xc2, 0x97, 0xd1, 0x78, 0xd4, 0xa3, 0xe1, 0x9a, 0x09, 0x28, 0xf5, 0x0c, 0xed, 0xbe, 0xf3,
0x76, 0x17, 0xb7, 0x33, 0x27, 0xe2, 0x84, 0xa5, 0x51, 0x06, 0x42, 0x81, 0x20, 0x69, 0x3c, 0x23,
0x99, 0xe4, 0x82, 0xcd, 0x80, 0x14, 0xce, 0xe4, 0xe7, 0x33, 0x04, 0x12, 0x05, 0x73, 0x9e, 0x02,
0x89, 0x3f, 0x33, 0xe5, 0x12, 0xe5, 0x4e, 0x8e, 0x8a, 0xb5, 0x5f, 0x7d, 0x07, 0x00, 0x00, 0xff,
0xff, 0xcc, 0x0f, 0x2b, 0x2e, 0x03, 0x02, 0x00, 0x00,
} }

View File

@ -28,9 +28,24 @@ message EncryptedObject {
// KeyID is the KMS key ID used for encryption operations. // KeyID is the KMS key ID used for encryption operations.
string keyID = 2; string keyID = 2;
// EncryptedDEK is the encrypted DEK. // EncryptedDEKSource is the ciphertext of the source of the DEK used to encrypt the data stored in encryptedData.
bytes encryptedDEK = 3; // encryptedDEKSourceType defines the process of using the plaintext of this field to determine the aforementioned DEK.
bytes encryptedDEKSource = 3;
// Annotations is additional metadata that was provided by the KMS plugin. // Annotations is additional metadata that was provided by the KMS plugin.
map<string, bytes> annotations = 4; map<string, bytes> annotations = 4;
// encryptedDEKSourceType defines the process of using the plaintext of encryptedDEKSource to determine the DEK.
EncryptedDEKSourceType encryptedDEKSourceType = 5;
}
enum EncryptedDEKSourceType {
// AES_GCM_KEY means that the plaintext of encryptedDEKSource is the DEK itself, with AES-GCM as the encryption algorithm.
AES_GCM_KEY = 0;
// HKDF_SHA256_XNONCE_AES_GCM_SEED means that the plaintext of encryptedDEKSource is the pseudo random key
// (referred to as the seed throughout the code) that is fed into HKDF expand. SHA256 is the hash algorithm
// and first 32 bytes of encryptedData are the info param. The first 32 bytes from the HKDF stream are used
// as the DEK with AES-GCM as the encryption algorithm.
HKDF_SHA256_XNONCE_AES_GCM_SEED = 1;
} }

View File

@ -60,7 +60,7 @@ type Base64Plugin struct {
} }
// NewBase64Plugin is a constructor for Base64Plugin. // NewBase64Plugin is a constructor for Base64Plugin.
func NewBase64Plugin(t *testing.T, socketPath string) *Base64Plugin { func NewBase64Plugin(t testing.TB, socketPath string) *Base64Plugin {
server := grpc.NewServer() server := grpc.NewServer()
result := &Base64Plugin{ result := &Base64Plugin{
grpcServer: server, grpcServer: server,