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.
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
// kep: https://kep.k8s.io/2887
// alpha: v1.23
@ -251,6 +258,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
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},
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"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope"
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/identity"
"k8s.io/apiserver/pkg/storage/value/encrypt/secretbox"
@ -63,13 +64,13 @@ const (
kmsTransformerPrefixV2 = "k8s:enc:kms:v2:"
// 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
// 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
// worst case window divided by the negative interval defines the
// 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
// 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
// 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
// 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.
kmsv2PluginHealthzPositiveInterval = 1 * time.Minute
kmsv2PluginHealthzNegativeInterval = 10 * time.Second
kmsv2PluginWriteDEKMaxTTL = 3 * time.Minute
kmsv2PluginWriteDEKSourceMaxTTL = 3 * time.Minute
kmsPluginHealthzNegativeTTL = 3 * time.Second
kmsPluginHealthzPositiveTTL = 20 * time.Second
@ -332,8 +333,8 @@ func (h *kmsv2PluginProbe) check(ctx context.Context) error {
return nil
}
// rotateDEKOnKeyIDChange tries to rotate to a new DEK 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.
// 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/seed and keyID overwrite the existing state.
// 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).
// 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 writes indefinitely as long as there is no error
// allow writes for only up to kmsv2PluginWriteDEKMaxTTL 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
expirationTimestamp := envelopekmsv2.NowFunc().Add(kmsv2PluginWriteDEKMaxTTL)
// allow writes for only up to kmsv2PluginWriteDEKSourceMaxTTL from now when there are errors
// we start the timer before we make the network call because kmsv2PluginWriteDEKSourceMaxTTL is meant to be the upper bound
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
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
h.state.Store(&state)
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 {
resp = &kmsservice.EncryptResponse{} // avoid nil panics
if encObject == nil {
encObject = &kmstypes.EncryptedObject{} // avoid nil panics
}
// happy path, should be the common case
// TODO maybe add success metrics?
if errGen == nil && resp.KeyID == statusKeyID {
if errGen == nil && encObject.KeyID == statusKeyID {
h.state.Store(&envelopekmsv2.State{
Transformer: transformer,
EncryptedDEK: resp.Ciphertext,
KeyID: resp.KeyID,
Annotations: resp.Annotations,
EncryptedObject: *encObject,
UID: uid,
ExpirationTimestamp: expirationTimestamp,
CacheKey: cacheKey,
@ -384,8 +391,9 @@ func (h *kmsv2PluginProbe) rotateDEKOnKeyIDChange(ctx context.Context, statusKey
if klogV6.Enabled() {
klogV6.InfoS("successfully rotated DEK",
"uid", uid,
"newKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(resp.KeyID),
"oldKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(state.KeyID),
"useSeed", useSeed,
"newKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(encObject.KeyID),
"oldKeyIDHash", envelopekmsv2.GetHashIfNotEmpty(state.EncryptedObject.KeyID),
"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",
uid, errState, errGen, envelopekmsv2.GetHashIfNotEmpty(statusKeyID), envelopekmsv2.GetHashIfNotEmpty(resp.KeyID), envelopekmsv2.GetHashIfNotEmpty(state.KeyID), state.ExpirationTimestamp.Format(time.RFC3339))
return fmt.Errorf("failed to rotate DEK uid=%q, useSeed=%v, errState=%v, errGen=%v, statusKeyIDHash=%q, encryptKeyIDHash=%q, stateKeyIDHash=%q, expirationTimestamp=%s",
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.
@ -407,12 +415,13 @@ func (h *kmsv2PluginProbe) getCurrentState() (envelopekmsv2.State, error) {
return envelopekmsv2.State{}, fmt.Errorf("got unexpected nil transformer")
}
if len(state.EncryptedDEK) == 0 {
return envelopekmsv2.State{}, fmt.Errorf("got unexpected empty EncryptedDEK")
encryptedObjectCopy := state.EncryptedObject
if len(encryptedObjectCopy.EncryptedData) != 0 {
return envelopekmsv2.State{}, fmt.Errorf("got unexpected non-empty EncryptedData")
}
if len(state.KeyID) == 0 {
return envelopekmsv2.State{}, fmt.Errorf("got unexpected empty keyID")
encryptedObjectCopy.EncryptedData = []byte{0} // any non-empty value to pass validation
if err := envelopekmsv2.ValidateEncryptedObject(&encryptedObjectCopy); err != nil {
return envelopekmsv2.State{}, fmt.Errorf("got invalid EncryptedObject: %w", err)
}
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
// 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(
ctx,
kmsv2PluginHealthzPositiveInterval,

View File

@ -39,6 +39,7 @@ import (
"k8s.io/apiserver/pkg/storage/value"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope"
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"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
@ -606,7 +607,7 @@ func TestKMSPluginHealthz(t *testing.T) {
ttl: 3 * time.Second,
}
keyID := "1"
kmsv2Probe.state.Store(&envelopekmsv2.State{KeyID: keyID})
kmsv2Probe.state.Store(&envelopekmsv2.State{EncryptedObject: kmstypes.EncryptedObject{KeyID: keyID}})
testCases := []struct {
desc string
@ -1711,6 +1712,7 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
name string
service *testKMSv2EnvelopeService
state envelopekmsv2.State
useSeed bool
statusKeyID string
wantState envelopekmsv2.State
wantEncryptCalls int
@ -1723,13 +1725,13 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: envelopekmsv2.State{},
statusKeyID: "1",
wantState: envelopekmsv2.State{
KeyID: "1",
EncryptedObject: kmstypes.EncryptedObject{KeyID: "1"},
ExpirationTimestamp: now.Add(3 * time.Minute),
},
wantEncryptCalls: 1,
wantLogs: []string{
`"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)),
},
wantErr: "",
@ -1740,20 +1742,38 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "2", now),
statusKeyID: "2",
wantState: envelopekmsv2.State{
KeyID: "2",
EncryptedObject: kmstypes.EncryptedObject{KeyID: "2"},
ExpirationTimestamp: now.Add(3 * time.Minute),
},
wantEncryptCalls: 0,
wantLogs: nil,
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",
service: &testKMSv2EnvelopeService{err: fmt.Errorf("broken")}, // not called
state: validState(t, "3", now.Add(-time.Hour)),
statusKeyID: "3",
wantState: envelopekmsv2.State{
KeyID: "3",
EncryptedObject: kmstypes.EncryptedObject{KeyID: "3"},
ExpirationTimestamp: now.Add(3 * time.Minute),
},
wantEncryptCalls: 0,
@ -1766,13 +1786,13 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "3", now.Add(-time.Hour)),
statusKeyID: "4",
wantState: envelopekmsv2.State{
KeyID: "4",
EncryptedObject: kmstypes.EncryptedObject{KeyID: "4"},
ExpirationTimestamp: now.Add(3 * time.Minute),
},
wantEncryptCalls: 1,
wantLogs: []string{
`"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)),
},
wantErr: "",
@ -1783,14 +1803,14 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "4", now.Add(7*time.Minute)),
statusKeyID: "5",
wantState: envelopekmsv2.State{
KeyID: "4",
EncryptedObject: kmstypes.EncryptedObject{KeyID: "4"},
ExpirationTimestamp: now.Add(7 * time.Minute),
},
wantEncryptCalls: 1,
wantLogs: []string{
`"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", ` +
`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{
`"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": ` +
`should be a domain with at least two segments separated by dots, statusKeyIDHash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b", ` +
`encryptKeyIDHash="", stateKeyIDHash="", expirationTimestamp=` + (time.Time{}).Format(time.RFC3339),
@ -1815,14 +1835,14 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
state: validState(t, "2", now),
statusKeyID: "3",
wantState: envelopekmsv2.State{
KeyID: "2",
EncryptedObject: kmstypes.EncryptedObject{KeyID: "2"},
ExpirationTimestamp: now,
},
wantEncryptCalls: 1,
wantLogs: []string{
`"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": ` +
`should be a domain with at least two segments separated by dots, statusKeyIDHash="sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce", ` +
`encryptKeyIDHash="", stateKeyIDHash="sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35", expirationTimestamp=` + now.Format(time.RFC3339),
@ -1830,6 +1850,8 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv2KDF, tt.useSeed)()
var buf bytes.Buffer
klog.SetOutput(&buf)
@ -1850,14 +1872,29 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) {
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()),
); len(diff) > 0 {
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 {
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 {
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 {
t.Fatal(err)
}
return envelopekmsv2.State{
Transformer: transformer,
EncryptedDEK: resp.Ciphertext,
KeyID: resp.KeyID,
Annotations: resp.Annotations,
EncryptedObject: *encObject,
ExpirationTimestamp: exp,
CacheKey: cacheKey,
}

View File

@ -34,33 +34,11 @@ import (
"k8s.io/klog/v2"
)
type gcm struct {
aead cipher.AEAD
nonceFunc func([]byte) error
}
// commonSize is the length of various security sensitive byte slices such as encryption keys.
// Do not change this value. It would be a backward incompatible change.
const commonSize = 32
// 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
}
const keySizeCounterNonceGCM = commonSize
// NewGCMTransformerWithUniqueKeyUnsafe is the same as NewGCMTransformer but is unsafe for general
// 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
// of decrypting values encrypted by this transformer (that transformer must not be used for encryption).
func NewGCMTransformerWithUniqueKeyUnsafe() (value.Transformer, []byte, error) {
key, err := generateKey(32)
key, err := GenerateKey(keySizeCounterNonceGCM)
if err != nil {
return nil, nil, err
}
@ -126,17 +104,6 @@ func newGCMTransformerWithUniqueKeyUnsafe(block cipher.Block, nonceGen *nonceGen
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 {
_, err := rand.Read(b)
return err
@ -164,8 +131,8 @@ func die(msg string) {
klog.FatalDepth(1, msg)
}
// generateKey generates a random key using system randomness.
func generateKey(length int) (key []byte, err error) {
// GenerateKey generates a random key using system randomness.
func GenerateKey(length int) (key []byte, err error) {
defer func(start time.Time) {
value.RecordDataKeyGeneration(start, err)
}(time.Now())
@ -177,6 +144,45 @@ func generateKey(length int) (key []byte, err error) {
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) {
nonceSize := t.aead.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)
}
transformerDecrypt := newGCMTransformer(t, block)
transformerDecrypt := newGCMTransformer(t, block, nil)
ctx := context.Background()
dataCtx := value.DefaultContext("authenticated_data")
@ -184,7 +184,7 @@ func TestGCMLegacyDataCompatibility(t *testing.T) {
t.Fatal(err)
}
transformerDecrypt := newGCMTransformer(t, block)
transformerDecrypt := newGCMTransformer(t, block, nil)
// 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"
@ -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) {
block, err := aes.NewCipher([]byte("abcdefghijklmnop"))
if err != nil {
t.Fatal(err)
}
transformer := newGCMTransformerWithUniqueKeyUnsafeTest(t, block)
transformer := newGCMTransformerWithUniqueKeyUnsafeTest(t, block, nil)
ctx := context.Background()
dataCtx := value.DefaultContext("authenticated_data")
@ -270,7 +294,7 @@ func TestGCMUnsafeNonceGen(t *testing.T) {
func TestGCMNonce(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))) {
t.Error("got all zeros for nonce")
}
@ -278,21 +302,30 @@ func TestGCMNonce(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)
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.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)) {
block, err := aes.NewCipher([]byte("abcdefghijklmnop"))
func testGCMNonce(t *testing.T, f transformerFunc, infoLen int, check func(int, []byte)) {
key := []byte("abcdefghijklmnopabcdefghijklmnop")
block, err := aes.NewCipher(key)
if err != nil {
t.Fatal(err)
}
transformer := f(t, block)
transformer := f(t, block, key)
ctx := context.Background()
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)
}
nonce := out[:12]
info := out[:infoLen]
nonce := out[infoLen : 12+infoLen]
randomN := nonce[:4]
if bytes.Equal(randomN, make([]byte, len(randomN))) {
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:])
}
}
@ -326,15 +366,22 @@ func TestGCMKeyRotation(t *testing.T) {
t.Run("gcm unsafe", func(t *testing.T) {
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")
block1, err := aes.NewCipher([]byte("abcdefghijklmnop"))
block1, err := aes.NewCipher(key1)
if err != nil {
t.Fatal(err)
}
block2, err := aes.NewCipher([]byte("0123456789abcdef"))
block2, err := aes.NewCipher(key2)
if err != nil {
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")
p := value.NewPrefixTransformers(testErr,
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1, key1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2, key2)},
)
out, err := p.TransformToStorage(ctx, []byte("firstvalue"), dataCtx)
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
p = value.NewPrefixTransformers(testErr,
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(t, block2, key2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(t, block1, key1)},
)
from, stale, err = p.TransformFromStorage(ctx, out, dataCtx)
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) {
tests := []struct {
keyLength int
@ -448,7 +501,16 @@ func BenchmarkGCMRead(b *testing.B) {
for _, t := range tests {
name := fmt.Sprintf("%vKeyLength/%vValueLength/%vExpectStale", t.keyLength, t.valueLength, t.expectStale)
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 {
name := fmt.Sprintf("%vKeyLength/%vValueLength", t.keyLength, t.valueLength)
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) {
block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength))
func benchmarkGCMRead(b *testing.B, f transformerFunc, keyLength int, valueLength int, expectStale bool) {
key1 := bytes.Repeat([]byte("a"), keyLength)
key2 := bytes.Repeat([]byte("b"), keyLength)
block1, err := aes.NewCipher(key1)
if err != nil {
b.Fatal(err)
}
block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength))
block2, err := aes.NewCipher(key2)
if err != nil {
b.Fatal(err)
}
p := value.NewPrefixTransformers(nil,
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: newGCMTransformer(b, block1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: newGCMTransformer(b, block2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(b, block1, key1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(b, block2, key2)},
)
ctx := context.Background()
@ -495,8 +569,8 @@ func benchmarkGCMRead(b *testing.B, keyLength int, valueLength int, expectStale
// reverse the key order if expecting stale
if expectStale {
p = value.NewPrefixTransformers(nil,
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: newGCMTransformer(b, block2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: newGCMTransformer(b, block1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(b, block2, key2)},
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()
}
func benchmarkGCMWrite(b *testing.B, keyLength int, valueLength int) {
block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength))
func benchmarkGCMWrite(b *testing.B, f transformerFunc, keyLength int, valueLength int) {
key1 := bytes.Repeat([]byte("a"), keyLength)
key2 := bytes.Repeat([]byte("b"), keyLength)
block1, err := aes.NewCipher(key1)
if err != nil {
b.Fatal(err)
}
block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength))
block2, err := aes.NewCipher(key2)
if err != nil {
b.Fatal(err)
}
p := value.NewPrefixTransformers(nil,
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: newGCMTransformer(b, block1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: newGCMTransformer(b, block2)},
value.PrefixTransformer{Prefix: []byte("first:"), Transformer: f(b, block1, key1)},
value.PrefixTransformer{Prefix: []byte("second:"), Transformer: f(b, block2, key2)},
)
ctx := context.Background()
@ -657,31 +734,29 @@ func TestRoundTrip(t *testing.T) {
if err != nil {
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 {
t.Fatal(err)
}
ctx := context.Background()
tests := []struct {
name string
dataCtx value.Context
t value.Transformer
name string
t value.Transformer
}{
{name: "GCM 16 byte key", t: newGCMTransformer(t, aes16block)},
{name: "GCM 24 byte key", t: newGCMTransformer(t, aes24block)},
{name: "GCM 32 byte key", t: newGCMTransformer(t, aes32block)},
{name: "GCM 16 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes16block)},
{name: "GCM 24 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes24block)},
{name: "GCM 32 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes32block)},
{name: "GCM 16 byte key", t: newGCMTransformer(t, aes16block, nil)},
{name: "GCM 24 byte key", t: newGCMTransformer(t, aes24block, nil)},
{name: "GCM 32 byte key", t: newGCMTransformer(t, aes32block, nil)},
{name: "GCM 16 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes16block, nil)},
{name: "GCM 24 byte unsafe key", t: newGCMTransformerWithUniqueKeyUnsafeTest(t, aes24block, nil)},
{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)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dataCtx := tt.dataCtx
if dataCtx == nil {
dataCtx = value.DefaultContext("")
}
dataCtx := value.DefaultContext("/foo/bar")
for _, l := range lengths {
data := make([]byte, l)
if _, err := io.ReadFull(rand.Reader, data); err != nil {
@ -718,12 +793,14 @@ func TestRoundTrip(t *testing.T) {
}
}
type testingT interface {
Helper()
Fatal(...any)
type namedTransformerFunc struct {
name string
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()
transformer, err := NewGCMTransformer(block)
@ -734,7 +811,7 @@ func newGCMTransformer(t testingT, block cipher.Block) value.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()
nonceGen := &nonceGenerator{fatal: die}
@ -745,3 +822,14 @@ func newGCMTransformerWithUniqueKeyUnsafeTest(t testingT, block cipher.Block) va
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)
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 {
t.Fatal(err)
}

View File

@ -20,6 +20,7 @@ package kmsv2
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"fmt"
"sort"
@ -43,6 +44,8 @@ import (
"k8s.io/utils/clock"
)
// TODO integration test with old AES GCM data recorded and new KDF data recorded
func init() {
value.RegisterMetrics()
metrics.RegisterMetrics()
@ -55,22 +58,22 @@ const (
annotationsMaxSize = 32 * 1024 // 32 kB
// KeyIDMaxSize is the maximum size of the keyID.
KeyIDMaxSize = 1 * 1024 // 1 kB
// encryptedDEKMaxSize is the maximum size of the encrypted DEK.
encryptedDEKMaxSize = 1 * 1024 // 1 kB
// encryptedDEKSourceMaxSize is the maximum size of the encrypted DEK source.
encryptedDEKSourceMaxSize = 1 * 1024 // 1 kB
// 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 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
// 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
// and no storage migration, the number of entries in this cache
// because the cache will likely never have more than a few thousand entries.
// each entry can be large due to an internal cache that maps the DEK seed to individual
// 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
// combined with the number of server restarts. If storage migration
// 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
// servers in use (once old entries expire out from the TTL).
cacheTTL = 24 * time.Hour
// error code
// key ID related error codes for metrics
errKeyIDOKCode ErrCodeKeyID = "ok"
errKeyIDEmptyCode ErrCodeKeyID = "empty"
errKeyIDTooLongCode ErrCodeKeyID = "too_long"
@ -83,23 +86,22 @@ type StateFunc func() (State, error)
type ErrCodeKeyID string
type State struct {
Transformer value.Transformer
EncryptedDEK []byte
KeyID string
Annotations map[string][]byte
Transformer value.Transformer
EncryptedObject kmstypes.EncryptedObject
UID string
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
}
func (s *State) ValidateEncryptCapability() error {
if now := NowFunc(); now.After(s.ExpirationTimestamp) {
return fmt.Errorf("EDEK with keyID hash %q expired at %s (current time is %s)",
GetHashIfNotEmpty(s.KeyID), s.ExpirationTimestamp.Format(time.RFC3339), now.Format(time.RFC3339))
return fmt.Errorf("encryptedDEKSource with keyID hash %q expired at %s (current time is %s)",
GetHashIfNotEmpty(s.EncryptedObject.KeyID), s.ExpirationTimestamp.Format(time.RFC3339), now.Format(time.RFC3339))
}
return nil
}
@ -137,6 +139,8 @@ func (t *envelopeTransformer) TransformFromStorage(ctx context.Context, data []b
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
// 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
@ -144,7 +148,7 @@ func (t *envelopeTransformer) TransformFromStorage(ctx context.Context, data []b
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 {
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)
key, err := t.envelopeService.Decrypt(ctx, uid, &kmsservice.DecryptRequest{
Ciphertext: encryptedObject.EncryptedDEK,
Ciphertext: encryptedObject.EncryptedDEKSource,
KeyID: encryptedObject.KeyID,
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)
}
transformer, err = t.addTransformerForDecryption(encryptedObjectCacheKey, key)
transformer, err = t.addTransformerForDecryption(encryptedObjectCacheKey, key, useSeed)
if err != nil {
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
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.
@ -201,7 +208,7 @@ func (t *envelopeTransformer) TransformToStorage(ctx context.Context, data []byt
// this prevents a cache miss every time the DEK rotates
// 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(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)
requestInfo := getRequestInfoFromContext(ctx)
@ -214,39 +221,43 @@ func (t *envelopeTransformer) TransformToStorage(ctx context.Context, data []byt
return nil, err
}
metrics.RecordKeyID(metrics.ToStorageLabel, t.providerName, state.KeyID)
metrics.RecordKeyID(metrics.ToStorageLabel, t.providerName, state.EncryptedObject.KeyID)
encObject := &kmstypes.EncryptedObject{
KeyID: state.KeyID,
EncryptedDEK: state.EncryptedDEK,
EncryptedData: result,
Annotations: state.Annotations,
}
encObjectCopy := state.EncryptedObject
encObjectCopy.EncryptedData = result
// 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.
func (t *envelopeTransformer) addTransformerForDecryption(cacheKey []byte, key []byte) (value.Read, error) {
block, err := aes.NewCipher(key)
func (t *envelopeTransformer) addTransformerForDecryption(cacheKey []byte, key []byte, useSeed bool) (value.Read, error) {
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 {
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 {
return nil, err
}
// TODO(aramase): Add metrics for cache fill percentage with custom cache implementation.
// TODO(aramase): Add metrics for cache size.
t.cache.set(cacheKey, transformer)
return transformer, nil
}
// doEncode encodes the EncryptedObject to a byte array.
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 proto.Marshal(request)
@ -258,18 +269,31 @@ func (t *envelopeTransformer) doDecode(originalData []byte) (*kmstypes.Encrypted
if err := proto.Unmarshal(originalData, o); err != nil {
return nil, err
}
// validate the EncryptedObject
if err := validateEncryptedObject(o); err != nil {
if err := ValidateEncryptedObject(o); err != nil {
return nil, err
}
return o, nil
}
// GenerateTransformer generates a new transformer and encrypts the DEK using the envelope service.
// It returns the transformer, the encrypted DEK, cache key and error.
func GenerateTransformer(ctx context.Context, uid string, envelopeService kmsservice.Service) (value.Transformer, *kmsservice.EncryptResponse, []byte, error) {
transformer, newKey, err := aestransformer.NewGCMTransformerWithUniqueKeyUnsafe()
// GenerateTransformer generates a new transformer and encrypts the DEK/seed using the envelope service.
// It returns the transformer, the encrypted DEK/seed, cache key and error.
func GenerateTransformer(ctx context.Context, uid string, envelopeService kmsservice.Service, useSeed bool) (value.Transformer, *kmstypes.EncryptedObject, []byte, error) {
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 {
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)
}
if err := validateEncryptedObject(&kmstypes.EncryptedObject{
KeyID: resp.KeyID,
EncryptedDEK: resp.Ciphertext,
EncryptedData: []byte{0}, // any non-empty value to pass validation
Annotations: resp.Annotations,
}); err != nil {
o := &kmstypes.EncryptedObject{
KeyID: resp.KeyID,
EncryptedDEKSource: resp.Ciphertext,
EncryptedData: []byte{0}, // any non-empty value to pass validation
Annotations: resp.Annotations,
}
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
}
cacheKey, err := generateCacheKey(resp.Ciphertext, resp.KeyID, resp.Annotations)
cacheKey, err := generateCacheKey(o.EncryptedDEKSourceType, resp.Ciphertext, resp.KeyID, resp.Annotations)
if err != nil {
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 {
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 {
return fmt.Errorf("encrypted data is empty")
}
if err := validateEncryptedDEK(o.EncryptedDEK); err != nil {
return fmt.Errorf("failed to validate encrypted DEK: %w", err)
if err := validateEncryptedDEKSource(o.EncryptedDEKSource); err != nil {
return fmt.Errorf("failed to validate encrypted DEK source: %w", err)
}
if _, err := ValidateKeyID(o.KeyID); err != nil {
return fmt.Errorf("failed to validate key id: %w", err)
@ -317,15 +357,15 @@ func validateEncryptedObject(o *kmstypes.EncryptedObject) error {
return nil
}
// validateEncryptedDEK tests the following:
// 1. The encrypted DEK is not empty.
// 2. The size of encrypted DEK is less than 1 kB.
func validateEncryptedDEK(encryptedDEK []byte) error {
if len(encryptedDEK) == 0 {
return fmt.Errorf("encrypted DEK is empty")
// validateEncryptedDEKSource tests the following:
// 1. The encrypted DEK source is not empty.
// 2. The size of encrypted DEK source is less than 1 kB.
func validateEncryptedDEKSource(encryptedDEKSource []byte) error {
if len(encryptedDEKSource) == 0 {
return fmt.Errorf("encrypted DEK source is empty")
}
if len(encryptedDEK) > encryptedDEKMaxSize {
return fmt.Errorf("encrypted DEK is %d bytes, which exceeds the max size of %d", len(encryptedDEK), encryptedDEKMaxSize)
if len(encryptedDEKSource) > encryptedDEKSourceMaxSize {
return fmt.Errorf("encrypted DEK source is %d bytes, which exceeds the max size of %d", len(encryptedDEKSource), encryptedDEKSourceMaxSize)
}
return nil
}
@ -370,17 +410,19 @@ func getRequestInfoFromContext(ctx context.Context) *genericapirequest.RequestIn
// generateCacheKey returns a key for the cache.
// The key is a concatenation of:
// 1. encryptedDEK
// 0. encryptedDEKSourceType
// 1. encryptedDEKSource
// 2. keyID
// 3. length of annotations
// 4. annotations (sorted by key) - each annotation is a concatenation of:
// a. annotation key
// 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
b := cryptobyte.NewBuilder(nil)
b.AddUint32(uint32(encryptedDEKSourceType))
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(encryptedDEK)
b.AddBytes(encryptedDEKSource)
})
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(toBytes(keyID))

View File

@ -27,11 +27,13 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/gogo/protobuf/proto"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/util/uuid"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage/value"
@ -167,7 +169,9 @@ func TestEnvelopeCaching(t *testing.T) {
envelopeService := newTestEnvelopeService()
fakeClock := testingclock.NewFakeClock(time.Now())
state, err := testStateFunc(ctx, envelopeService, fakeClock)()
useSeed := randomBool()
state, err := testStateFunc(ctx, envelopeService, fakeClock, useSeed)()
if err != nil {
t.Fatal(err)
}
@ -196,7 +200,7 @@ func TestEnvelopeCaching(t *testing.T) {
// force GC to run by performing a write
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 {
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) {
transformer, resp, cacheKey, errGen := GenerateTransformer(ctx, string(uuid.NewUUID()), envelopeService)
transformer, encObject, cacheKey, errGen := GenerateTransformer(ctx, string(uuid.NewUUID()), envelopeService, useSeed)
if errGen != nil {
return State{}, errGen
}
return State{
Transformer: transformer,
EncryptedDEK: resp.Ciphertext,
KeyID: resp.KeyID,
Annotations: resp.Annotations,
EncryptedObject: *encObject,
UID: "panda",
ExpirationTimestamp: clock.Now().Add(time.Hour),
CacheKey: cacheKey,
@ -254,6 +256,8 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
expectedStale bool
testErr error
testKeyID string
useSeedWrite bool
useSeedRead bool
}{
{
desc: "stateFunc returns err",
@ -262,11 +266,35 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
testKeyID: "",
},
{
desc: "stateFunc returns same keyID",
desc: "stateFunc returns same keyID, not using seed",
expectedStale: false,
testErr: nil,
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",
expectedStale: true,
@ -283,7 +311,7 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
ctx := testContext(t)
envelopeService := newTestEnvelopeService()
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{})()
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, tt.useSeedWrite)()
if err != nil {
t.Fatal(err)
}
@ -302,7 +330,12 @@ func TestEnvelopeTransformerStaleness(t *testing.T) {
}
// 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
_, stale, err := transformer.TransformFromStorage(ctx, transformedData, dataCtx)
@ -330,8 +363,10 @@ func TestEnvelopeTransformerStateFunc(t *testing.T) {
ctx := testContext(t)
useSeed := randomBool()
envelopeService := newTestEnvelopeService()
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{})()
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, useSeed)()
if err != nil {
t.Fatal(err)
}
@ -351,12 +386,18 @@ func TestEnvelopeTransformerStateFunc(t *testing.T) {
if err != stateErr {
t.Fatalf("expected state error, got: %v", err)
}
data, err := proto.Marshal(&kmstypes.EncryptedObject{
EncryptedData: []byte{1},
KeyID: "2",
EncryptedDEK: []byte{3},
Annotations: nil,
})
o := &kmstypes.EncryptedObject{
EncryptedData: []byte{1},
KeyID: "2",
EncryptedDEKSource: []byte{3},
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 {
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) {
_, 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)
}
})
@ -418,7 +459,9 @@ func TestEnvelopeTransformerStateFunc(t *testing.T) {
if err := proto.Unmarshal(encryptedData, obj); err != nil {
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)
if err != nil {
t.Fatal(err)
@ -468,7 +511,7 @@ func TestTransformToStorageError(t *testing.T) {
envelopeService := newTestEnvelopeService()
envelopeService.SetAnnotations(tt.annotations)
transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
testStateFunc(ctx, envelopeService, clock.RealClock{}),
testStateFunc(ctx, envelopeService, clock.RealClock{}, randomBool()),
)
dataCtx := value.DefaultContext(testContextText)
@ -487,9 +530,9 @@ func TestEncodeDecode(t *testing.T) {
transformer := &envelopeTransformer{}
obj := &kmstypes.EncryptedObject{
EncryptedData: []byte{0x01, 0x02, 0x03},
KeyID: "1",
EncryptedDEK: []byte{0x04, 0x05, 0x06},
EncryptedData: []byte{0x01, 0x02, 0x03},
KeyID: "1",
EncryptedDEKSource: []byte{0x04, 0x05, 0x06},
}
data, err := transformer.doEncode(obj)
@ -522,24 +565,62 @@ func TestValidateEncryptedObject(t *testing.T) {
{
desc: "encrypted data is nil",
originalData: &kmstypes.EncryptedObject{
KeyID: "1",
EncryptedDEK: []byte{0x01, 0x02, 0x03},
KeyID: "1",
EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
},
expectedError: fmt.Errorf("encrypted data is empty"),
},
{
desc: "encrypted data is []byte{}",
originalData: &kmstypes.EncryptedObject{
EncryptedDEK: []byte{0x01, 0x02, 0x03},
EncryptedData: []byte{},
EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
EncryptedData: []byte{},
},
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 {
t.Run(tt.desc, func(t *testing.T) {
err := validateEncryptedObject(tt.originalData)
err := ValidateEncryptedObject(tt.originalData)
if err == 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()
testCases := []struct {
name string
encryptedDEK []byte
expectedError string
name string
encryptedDEKSource []byte
expectedError string
}{
{
name: "encrypted DEK is nil",
encryptedDEK: nil,
expectedError: "encrypted DEK is empty",
name: "encrypted DEK source is nil",
encryptedDEKSource: nil,
expectedError: "encrypted DEK source is empty",
},
{
name: "encrypted DEK is empty",
encryptedDEK: []byte{},
expectedError: "encrypted DEK is empty",
name: "encrypted DEK source is empty",
encryptedDEKSource: []byte{},
expectedError: "encrypted DEK source is empty",
},
{
name: "encrypted DEK size is greater than 1 kB",
encryptedDEK: bytes.Repeat([]byte("a"), 1024+1),
expectedError: "which exceeds the max size of",
name: "encrypted DEK source size is greater than 1 kB",
encryptedDEKSource: bytes.Repeat([]byte("a"), 1024+1),
expectedError: "which exceeds the max size of",
},
{
name: "valid encrypted DEK",
encryptedDEK: []byte{0x01, 0x02, 0x03},
expectedError: "",
name: "valid encrypted DEK source",
encryptedDEKSource: []byte{0x01, 0x02, 0x03},
expectedError: "",
},
}
@ -735,7 +816,7 @@ func TestValidateEncryptedDEK(t *testing.T) {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := validateEncryptedDEK(tt.encryptedDEK)
err := validateEncryptedDEKSource(tt.encryptedDEKSource)
if tt.expectedError != "" {
if err == nil {
t.Fatalf("expected error %q, got nil", tt.expectedError)
@ -755,7 +836,7 @@ func TestValidateEncryptedDEK(t *testing.T) {
func TestEnvelopeMetrics(t *testing.T) {
envelopeService := newTestEnvelopeService()
transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
testStateFunc(testContext(t), envelopeService, clock.RealClock{}),
testStateFunc(testContext(t), envelopeService, clock.RealClock{}, randomBool()),
)
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) {
klog.InitFlags(nil)
flagOnce.Do(func() {
klog.InitFlags(nil)
})
flag.Set("v", "6")
flag.Parse()
@ -858,7 +943,7 @@ func TestEnvelopeLogging(t *testing.T) {
envelopeService := newTestEnvelopeService()
fakeClock := testingclock.NewFakeClock(time.Now())
transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName,
testStateFunc(tc.ctx, envelopeService, clock.RealClock{}),
testStateFunc(tc.ctx, envelopeService, clock.RealClock{}, randomBool()),
1*time.Second, fakeClock)
dataCtx := value.DefaultContext([]byte(testContextText))
@ -905,7 +990,7 @@ func TestCacheNotCorrupted(t *testing.T) {
fakeClock := testingclock.NewFakeClock(time.Now())
state, err := testStateFunc(ctx, envelopeService, fakeClock)()
state, err := testStateFunc(ctx, envelopeService, fakeClock, randomBool())()
if err != nil {
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
// but uses the annotation field to send the actual encrypted DEK.
envelopeService.SetCiphertext(state.EncryptedDEK)
// but uses the annotation field to send the actual encrypted DEK source.
envelopeService.SetCiphertext(state.EncryptedObject.EncryptedDEKSource)
// 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{
"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 {
t.Fatal(err)
}
@ -954,28 +1039,40 @@ func TestCacheNotCorrupted(t *testing.T) {
}
func TestGenerateCacheKey(t *testing.T) {
encryptedDEK1 := []byte{1, 2, 3}
encryptedDEKSource1 := []byte{1, 2, 3}
keyID1 := "id1"
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"
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
testCases := []struct {
encryptedDEK []byte
keyID string
annotations map[string][]byte
encryptedDEKSourceType kmstypes.EncryptedDEKSourceType
encryptedDEKSource []byte
keyID string
annotations map[string][]byte
}{
{encryptedDEK1, keyID1, annotations1},
{encryptedDEK1, keyID1, annotations2},
{encryptedDEK1, keyID2, annotations1},
{encryptedDEK1, keyID2, annotations2},
{encryptedDEK2, keyID1, annotations1},
{encryptedDEK2, keyID1, annotations2},
{encryptedDEK2, keyID2, annotations1},
{encryptedDEK2, keyID2, annotations2},
{encryptedDEKSourceType1, encryptedDEKSource1, keyID1, annotations1},
{encryptedDEKSourceType1, encryptedDEKSource1, keyID1, annotations2},
{encryptedDEKSourceType1, encryptedDEKSource1, keyID2, annotations1},
{encryptedDEKSourceType1, encryptedDEKSource1, keyID2, annotations2},
{encryptedDEKSourceType1, encryptedDEKSource2, keyID1, annotations1},
{encryptedDEKSourceType1, encryptedDEKSource2, keyID1, annotations2},
{encryptedDEKSourceType1, encryptedDEKSource2, keyID2, annotations1},
{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 {
@ -983,8 +1080,8 @@ func TestGenerateCacheKey(t *testing.T) {
for _, tc2 := range testCases {
tc2 := tc2
t.Run(fmt.Sprintf("%+v-%+v", tc, tc2), func(t *testing.T) {
key1, err1 := generateCacheKey(tc.encryptedDEK, tc.keyID, tc.annotations)
key2, err2 := generateCacheKey(tc2.encryptedDEK, tc2.keyID, tc2.annotations)
key1, err1 := generateCacheKey(tc.encryptedDEKSourceType, tc.encryptedDEKSource, tc.keyID, tc.annotations)
key2, err2 := generateCacheKey(tc2.encryptedDEKSourceType, tc2.encryptedDEKSource, tc2.keyID, tc2.annotations)
if err1 != nil || err2 != nil {
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{})
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",
@ -1053,7 +1150,7 @@ func TestGenerateTransformer(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
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 err != nil {
t.Errorf("expected no error, got %q", errString(err))
@ -1061,7 +1158,7 @@ func TestGenerateTransformer(t *testing.T) {
if transformer == nil {
t.Error("expected transformer, got nil")
}
if encryptResp == nil {
if encObject == nil {
t.Error("expected encrypt response, got nil")
}
if cacheKey == nil {
@ -1083,3 +1180,5 @@ func errString(err error) string {
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)
metrics.RegisterMetrics()
metrics.KMSOperationsLatencyMetric.Reset() // support running `go test -count X`
testCases := []struct {
name string

View File

@ -36,19 +36,52 @@ var _ = math.Inf
// proto package needs to be updated.
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.
type EncryptedObject struct {
// EncryptedData is the encrypted data.
EncryptedData []byte `protobuf:"bytes,1,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"`
// KeyID is the KMS key ID used for encryption operations.
KeyID string `protobuf:"bytes,2,opt,name=keyID,proto3" json:"keyID,omitempty"`
// EncryptedDEK is the encrypted DEK.
EncryptedDEK []byte `protobuf:"bytes,3,opt,name=encryptedDEK,proto3" json:"encryptedDEK,omitempty"`
// EncryptedDEKSource is the ciphertext of the source of the DEK used to encrypt the data stored in encryptedData.
// 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 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:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
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"`
// encryptedDEKSourceType defines the process of using the plaintext of encryptedDEKSource to determine the DEK.
EncryptedDEKSourceType EncryptedDEKSourceType `protobuf:"varint,5,opt,name=encryptedDEKSourceType,proto3,enum=v2.EncryptedDEKSourceType" json:"encryptedDEKSourceType,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *EncryptedObject) Reset() { *m = EncryptedObject{} }
@ -89,9 +122,9 @@ func (m *EncryptedObject) GetKeyID() string {
return ""
}
func (m *EncryptedObject) GetEncryptedDEK() []byte {
func (m *EncryptedObject) GetEncryptedDEKSource() []byte {
if m != nil {
return m.EncryptedDEK
return m.EncryptedDEKSource
}
return nil
}
@ -103,7 +136,15 @@ func (m *EncryptedObject) GetAnnotations() map[string][]byte {
return nil
}
func (m *EncryptedObject) GetEncryptedDEKSourceType() EncryptedDEKSourceType {
if m != nil {
return m.EncryptedDEKSourceType
}
return EncryptedDEKSourceType_AES_GCM_KEY
}
func init() {
proto.RegisterEnum("v2.EncryptedDEKSourceType", EncryptedDEKSourceType_name, EncryptedDEKSourceType_value)
proto.RegisterType((*EncryptedObject)(nil), "v2.EncryptedObject")
proto.RegisterMapType((map[string][]byte)(nil), "v2.EncryptedObject.AnnotationsEntry")
}
@ -111,21 +152,26 @@ func init() {
func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) }
var fileDescriptor_00212fb1f9d3bf1c = []byte{
// 244 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x90, 0xb1, 0x4b, 0x03, 0x31,
0x14, 0xc6, 0xc9, 0x9d, 0x0a, 0x97, 0x9e, 0x58, 0x82, 0xc3, 0xe1, 0x74, 0x94, 0x0e, 0x37, 0x25,
0x10, 0x97, 0x22, 0x52, 0x50, 0x7a, 0x82, 0x38, 0x08, 0x19, 0xdd, 0xd2, 0xfa, 0x28, 0x67, 0x6a,
0x12, 0x92, 0x18, 0xc8, 0x9f, 0xee, 0x26, 0x4d, 0x95, 0xda, 0xdb, 0xde, 0xf7, 0xf1, 0xfb, 0xe0,
0xc7, 0xc3, 0x95, 0xb4, 0x03, 0xb5, 0xce, 0x04, 0x43, 0x8a, 0xc8, 0x67, 0xdf, 0x08, 0x5f, 0xf5,
0x7a, 0xe3, 0x92, 0x0d, 0xf0, 0xfe, 0xba, 0xfe, 0x80, 0x4d, 0x20, 0x73, 0x7c, 0x09, 0x7f, 0xd5,
0x4a, 0x06, 0xd9, 0xa0, 0x16, 0x75, 0xb5, 0x38, 0x2d, 0xc9, 0x35, 0x3e, 0x57, 0x90, 0x9e, 0x57,
0x4d, 0xd1, 0xa2, 0xae, 0x12, 0x87, 0x40, 0x66, 0xb8, 0x3e, 0x62, 0xfd, 0x4b, 0x53, 0xe6, 0xe9,
0x49, 0x47, 0x9e, 0xf0, 0x44, 0x6a, 0x6d, 0x82, 0x0c, 0x83, 0xd1, 0xbe, 0x39, 0x6b, 0xcb, 0x6e,
0xc2, 0xe7, 0x34, 0x72, 0x3a, 0x32, 0xa1, 0x0f, 0x47, 0xac, 0xd7, 0xc1, 0x25, 0xf1, 0x7f, 0x78,
0xb3, 0xc4, 0xd3, 0x31, 0x40, 0xa6, 0xb8, 0x54, 0x90, 0xb2, 0x71, 0x25, 0xf6, 0xe7, 0xde, 0x33,
0xca, 0xdd, 0x17, 0x64, 0xcf, 0x5a, 0x1c, 0xc2, 0x5d, 0xb1, 0x40, 0x8f, 0xcb, 0xb7, 0x7b, 0xb5,
0xf0, 0x74, 0x30, 0x4c, 0xda, 0xc1, 0x83, 0x8b, 0xe0, 0x98, 0x55, 0x5b, 0xe6, 0x83, 0x71, 0x72,
0x0b, 0x2c, 0x93, 0xec, 0x57, 0x9d, 0x81, 0x8e, 0xb0, 0x33, 0x16, 0x98, 0xfa, 0xf4, 0x91, 0xb3,
0xc8, 0xd7, 0x17, 0xf9, 0x8d, 0xb7, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x80, 0x43, 0x93,
0x53, 0x01, 0x00, 0x00,
// 329 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0xe1, 0x4b, 0xc2, 0x40,
0x18, 0xc6, 0xdb, 0xcc, 0xc0, 0xd3, 0x72, 0x1c, 0x21, 0xc3, 0x2f, 0x8d, 0xf2, 0xc3, 0xe8, 0xc3,
0x0e, 0x16, 0x85, 0x44, 0x08, 0xe6, 0xce, 0x0c, 0x49, 0x61, 0xeb, 0x43, 0xf5, 0x65, 0x9c, 0xf6,
0x22, 0x6b, 0xb6, 0x1b, 0xb7, 0xf3, 0x60, 0x7f, 0x6a, 0xff, 0x4d, 0x38, 0x13, 0xd3, 0xec, 0xdb,
0xbd, 0xef, 0xfd, 0xde, 0xe7, 0xb9, 0x7b, 0x5e, 0x54, 0x61, 0x69, 0xe4, 0xa4, 0x82, 0x4b, 0x8e,
0x75, 0xe5, 0x9e, 0x7f, 0xe9, 0xa8, 0x4e, 0x93, 0xa9, 0xc8, 0x53, 0x09, 0xef, 0xe3, 0xc9, 0x07,
0x4c, 0x25, 0x6e, 0xa1, 0x63, 0x58, 0xb7, 0x3c, 0x26, 0x99, 0xa9, 0x59, 0x9a, 0x5d, 0xf3, 0xb7,
0x9b, 0xf8, 0x14, 0x95, 0x63, 0xc8, 0x1f, 0x3d, 0x53, 0xb7, 0x34, 0xbb, 0xe2, 0xaf, 0x0a, 0xec,
0x20, 0xbc, 0xc1, 0xe8, 0x30, 0xe0, 0x0b, 0x31, 0x05, 0xb3, 0x54, 0x08, 0xec, 0xb9, 0xc1, 0x7d,
0x54, 0x65, 0x49, 0xc2, 0x25, 0x93, 0x11, 0x4f, 0x32, 0xf3, 0xd0, 0x2a, 0xd9, 0x55, 0xb7, 0xe5,
0x28, 0xd7, 0xd9, 0x79, 0x95, 0xd3, 0xdd, 0x60, 0x34, 0x91, 0x22, 0xf7, 0x7f, 0x0f, 0x62, 0x1f,
0x35, 0xfe, 0xaa, 0x3f, 0xe7, 0x29, 0x98, 0x65, 0x4b, 0xb3, 0x4f, 0xdc, 0xe6, 0x96, 0xe4, 0x16,
0xe1, 0xff, 0x33, 0xd9, 0xec, 0x20, 0x63, 0xd7, 0x14, 0x1b, 0xa8, 0x14, 0x43, 0x5e, 0x24, 0x52,
0xf1, 0x97, 0xc7, 0x65, 0x0e, 0x8a, 0xcd, 0x17, 0x50, 0xe4, 0x50, 0xf3, 0x57, 0xc5, 0xad, 0xde,
0xd6, 0x2e, 0x47, 0xa8, 0xb1, 0xdf, 0x11, 0xd7, 0x51, 0xb5, 0x4b, 0x83, 0xf0, 0xa1, 0xf7, 0x14,
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.
string keyID = 2;
// EncryptedDEK is the encrypted DEK.
bytes encryptedDEK = 3;
// EncryptedDEKSource is the ciphertext of the source of the DEK used to encrypt the data stored in encryptedData.
// 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.
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.
func NewBase64Plugin(t *testing.T, socketPath string) *Base64Plugin {
func NewBase64Plugin(t testing.TB, socketPath string) *Base64Plugin {
server := grpc.NewServer()
result := &Base64Plugin{
grpcServer: server,