kmsv2: KDF based nonce extension
Signed-off-by: Monis Khan <mok@microsoft.com> Kubernetes-commit: bf49c727ba10881d5378e9242f31dc00dede51be
This commit is contained in:
parent
cf66e8fde8
commit
8e93c650b5
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue