1435 lines
47 KiB
Go
1435 lines
47 KiB
Go
/*
|
|
Copyright 2022 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 kmsv2 transforms values for storage at rest using a Envelope v2 provider
|
|
package kmsv2
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"flag"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
"go.opentelemetry.io/otel/sdk/trace"
|
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
|
|
|
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"
|
|
kmstypes "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2/v2"
|
|
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope/metrics"
|
|
"k8s.io/component-base/metrics/legacyregistry"
|
|
"k8s.io/component-base/metrics/testutil"
|
|
"k8s.io/klog/v2"
|
|
kmsservice "k8s.io/kms/pkg/service"
|
|
"k8s.io/utils/clock"
|
|
testingclock "k8s.io/utils/clock/testing"
|
|
)
|
|
|
|
const (
|
|
testText = "abcdefghijklmnopqrstuvwxyz"
|
|
testContextText = "0123456789"
|
|
testKeyHash = "sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"
|
|
testKeyVersion = "1"
|
|
testAPIServerID = "testAPIServerID"
|
|
testAPIServerIDHash = "sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37"
|
|
)
|
|
|
|
// testEnvelopeService is a mock Envelope service which can be used to simulate remote Envelope services
|
|
// for testing of Envelope based encryption providers.
|
|
type testEnvelopeService struct {
|
|
annotations map[string][]byte
|
|
disabled bool
|
|
keyVersion string
|
|
ciphertext []byte
|
|
decryptCalls int32
|
|
}
|
|
|
|
func (t *testEnvelopeService) Decrypt(ctx context.Context, uid string, req *kmsservice.DecryptRequest) ([]byte, error) {
|
|
atomic.AddInt32(&t.decryptCalls, 1)
|
|
if t.disabled {
|
|
return nil, fmt.Errorf("Envelope service was disabled")
|
|
}
|
|
if len(uid) == 0 {
|
|
return nil, fmt.Errorf("uid is required")
|
|
}
|
|
if len(req.KeyID) == 0 {
|
|
return nil, fmt.Errorf("keyID is required")
|
|
}
|
|
return base64.StdEncoding.DecodeString(string(req.Ciphertext))
|
|
}
|
|
|
|
func (t *testEnvelopeService) Encrypt(ctx context.Context, uid string, data []byte) (*kmsservice.EncryptResponse, error) {
|
|
if t.disabled {
|
|
return nil, fmt.Errorf("Envelope service was disabled")
|
|
}
|
|
if len(uid) == 0 {
|
|
return nil, fmt.Errorf("uid is required")
|
|
}
|
|
annotations := make(map[string][]byte)
|
|
if t.annotations != nil {
|
|
for k, v := range t.annotations {
|
|
annotations[k] = v
|
|
}
|
|
} else {
|
|
annotations["local-kek.kms.kubernetes.io"] = []byte("encrypted-local-kek")
|
|
}
|
|
|
|
ciphertext := t.ciphertext
|
|
if ciphertext == nil {
|
|
ciphertext = []byte(base64.StdEncoding.EncodeToString(data))
|
|
}
|
|
|
|
return &kmsservice.EncryptResponse{Ciphertext: ciphertext, KeyID: t.keyVersion, Annotations: annotations}, nil
|
|
}
|
|
|
|
func (t *testEnvelopeService) Status(ctx context.Context) (*kmsservice.StatusResponse, error) {
|
|
if t.disabled {
|
|
return nil, fmt.Errorf("Envelope service was disabled")
|
|
}
|
|
return &kmsservice.StatusResponse{KeyID: t.keyVersion}, nil
|
|
}
|
|
|
|
func (t *testEnvelopeService) SetDisabledStatus(status bool) {
|
|
t.disabled = status
|
|
}
|
|
|
|
func (t *testEnvelopeService) SetAnnotations(annotations map[string][]byte) {
|
|
t.annotations = annotations
|
|
}
|
|
|
|
func (t *testEnvelopeService) SetCiphertext(ciphertext []byte) {
|
|
t.ciphertext = ciphertext
|
|
}
|
|
|
|
func (t *testEnvelopeService) Rotate() {
|
|
i, _ := strconv.Atoi(t.keyVersion)
|
|
t.keyVersion = strconv.FormatInt(int64(i+1), 10)
|
|
}
|
|
|
|
func newTestEnvelopeService() *testEnvelopeService {
|
|
return &testEnvelopeService{
|
|
keyVersion: testKeyVersion,
|
|
}
|
|
}
|
|
|
|
// Throw error if Envelope transformer tries to contact Envelope without hitting cache.
|
|
func TestEnvelopeCaching(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
cacheTTL time.Duration
|
|
simulateKMSPluginFailure bool
|
|
expectedError string
|
|
expectedDecryptCalls int
|
|
}{
|
|
{
|
|
desc: "entry in cache should withstand plugin failure",
|
|
cacheTTL: 5 * time.Minute,
|
|
simulateKMSPluginFailure: true,
|
|
expectedDecryptCalls: 0, // should not hit KMS plugin
|
|
},
|
|
{
|
|
desc: "cache entry expired should not withstand plugin failure",
|
|
cacheTTL: 1 * time.Millisecond,
|
|
simulateKMSPluginFailure: true,
|
|
expectedError: "failed to decrypt DEK, error: Envelope service was disabled",
|
|
expectedDecryptCalls: 10, // should hit KMS plugin for each read after cache entry expired and fail
|
|
},
|
|
{
|
|
desc: "cache entry expired should work after cache refresh",
|
|
cacheTTL: 1 * time.Millisecond,
|
|
simulateKMSPluginFailure: false,
|
|
expectedDecryptCalls: 1, // should hit KMS plugin just for the 1st read after cache entry expired
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
ctx := testContext(t)
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
|
|
useSeed := randomBool()
|
|
|
|
state, err := testStateFunc(ctx, envelopeService, fakeClock, useSeed)()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName,
|
|
func() (State, error) { return state, nil }, testAPIServerID,
|
|
tt.cacheTTL, fakeClock)
|
|
|
|
dataCtx := value.DefaultContext(testContextText)
|
|
originalText := []byte(testText)
|
|
|
|
transformedData, err := transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: error while transforming data to storage: %s", err)
|
|
}
|
|
untransformedData, _, err := transformer.TransformFromStorage(ctx, transformedData, dataCtx)
|
|
if err != nil {
|
|
t.Fatalf("could not decrypt Envelope transformer's encrypted data even once: %v", err)
|
|
}
|
|
if !bytes.Equal(untransformedData, originalText) {
|
|
t.Fatalf("envelopeTransformer transformed data incorrectly. Expected: %v, got %v", originalText, untransformedData)
|
|
}
|
|
|
|
// advance the clock to allow cache entries to expire depending on TTL
|
|
fakeClock.Step(2 * time.Minute)
|
|
// force GC to run by performing a write
|
|
transformer.(*envelopeTransformer).cache.set([]byte("some-other-unrelated-key"), &envelopeTransformer{})
|
|
|
|
state, err = testStateFunc(ctx, envelopeService, fakeClock, useSeed)()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
envelopeService.SetDisabledStatus(tt.simulateKMSPluginFailure)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
// Subsequent reads for the same data should work fine due to caching.
|
|
untransformedData, _, err = transformer.TransformFromStorage(ctx, transformedData, dataCtx)
|
|
if tt.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error: %v, got nil", tt.expectedError)
|
|
}
|
|
if err.Error() != tt.expectedError {
|
|
t.Fatalf("expected error: %v, got: %v", tt.expectedError, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !bytes.Equal(untransformedData, originalText) {
|
|
t.Fatalf("envelopeTransformer transformed data incorrectly. Expected: %v, got %v", originalText, untransformedData)
|
|
}
|
|
}
|
|
}
|
|
if int(envelopeService.decryptCalls) != tt.expectedDecryptCalls {
|
|
t.Fatalf("expected %d decrypt calls, got %d", tt.expectedDecryptCalls, envelopeService.decryptCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testStateFunc(ctx context.Context, envelopeService kmsservice.Service, clock clock.Clock, useSeed bool) func() (State, error) {
|
|
return func() (State, error) {
|
|
transformer, encObject, cacheKey, errGen := GenerateTransformer(ctx, string(uuid.NewUUID()), envelopeService, useSeed)
|
|
if errGen != nil {
|
|
return State{}, errGen
|
|
}
|
|
return State{
|
|
Transformer: transformer,
|
|
EncryptedObject: *encObject,
|
|
UID: "panda",
|
|
ExpirationTimestamp: clock.Now().Add(time.Hour),
|
|
CacheKey: cacheKey,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// TestEnvelopeTransformerStaleness validates that staleness checks on read honor the data returned from the StateFunc.
|
|
func TestEnvelopeTransformerStaleness(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
desc string
|
|
expectedStale bool
|
|
testErr error
|
|
testKeyID string
|
|
useSeedWrite bool
|
|
useSeedRead bool
|
|
}{
|
|
{
|
|
desc: "stateFunc returns err",
|
|
expectedStale: false,
|
|
testErr: fmt.Errorf("failed to perform status section of the healthz check for KMS Provider"),
|
|
testKeyID: "",
|
|
},
|
|
{
|
|
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,
|
|
testErr: nil,
|
|
testKeyID: "2",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testContext(t)
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, tt.useSeedWrite)()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var stateErr error
|
|
|
|
transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
|
|
func() (State, error) { return state, stateErr }, testAPIServerID,
|
|
)
|
|
|
|
dataCtx := value.DefaultContext(testContextText)
|
|
originalText := []byte(testText)
|
|
|
|
transformedData, err := transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: error while transforming data (%v) to storage: %s", originalText, err)
|
|
}
|
|
|
|
// inject test data before performing a read
|
|
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)
|
|
if tt.testErr != nil {
|
|
if err == nil {
|
|
t.Fatalf("envelopeTransformer: expected error: %v, got nil", tt.testErr)
|
|
}
|
|
if err.Error() != tt.testErr.Error() {
|
|
t.Fatalf("envelopeTransformer: expected error: %v, got: %v", tt.testErr, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: unexpected error: %v", err)
|
|
}
|
|
if stale != tt.expectedStale {
|
|
t.Fatalf("envelopeTransformer TransformFromStorage determined keyID staleness incorrectly, expected: %v, got %v", tt.expectedStale, stale)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnvelopeTransformerStateFunc(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testContext(t)
|
|
|
|
useSeed := randomBool()
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, useSeed)()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// start with a broken state
|
|
stateErr := fmt.Errorf("some state error")
|
|
|
|
transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
|
|
func() (State, error) { return state, stateErr }, testAPIServerID,
|
|
)
|
|
|
|
dataCtx := value.DefaultContext(testContextText)
|
|
originalText := []byte(testText)
|
|
|
|
t.Run("nothing works when the state is broken", func(t *testing.T) {
|
|
_, err := transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
if err != stateErr {
|
|
t.Fatalf("expected state error, got: %v", err)
|
|
}
|
|
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)
|
|
}
|
|
_, _, err = transformer.TransformFromStorage(ctx, data, dataCtx)
|
|
if err != stateErr {
|
|
t.Fatalf("expected state error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
// fix the state
|
|
stateErr = nil
|
|
|
|
var encryptedData []byte
|
|
|
|
t.Run("everything works when the state is fixed", func(t *testing.T) {
|
|
encryptedData, err = transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, err = transformer.TransformFromStorage(ctx, encryptedData, dataCtx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
// break the plugin
|
|
envelopeService.SetDisabledStatus(true)
|
|
|
|
t.Run("everything works even when the plugin is down but the state is valid", func(t *testing.T) {
|
|
data, err := transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, err = transformer.TransformFromStorage(ctx, data, dataCtx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
// make the state invalid
|
|
state.ExpirationTimestamp = time.Now().Add(-time.Hour)
|
|
|
|
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), `encryptedDEKSource with keyID hash "sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" expired at`) {
|
|
t.Fatalf("expected expiration error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("reads succeed when the plugin is down and the state is invalid", func(t *testing.T) {
|
|
_, _, err = transformer.TransformFromStorage(ctx, encryptedData, dataCtx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("reads for a different DEK fail when the plugin is down and the state is invalid", func(t *testing.T) {
|
|
obj := &kmstypes.EncryptedObject{}
|
|
if err := proto.Unmarshal(encryptedData, obj); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
obj.EncryptedDEKSource = append(obj.EncryptedDEKSource, 1) // skip StateFunc transformer
|
|
|
|
data, err := proto.Marshal(obj)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, _, err = transformer.TransformFromStorage(ctx, data, dataCtx)
|
|
if errString(err) != "failed to decrypt DEK, error: Envelope service was disabled" {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTransformToStorageError(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
annotations map[string][]byte
|
|
}{
|
|
{
|
|
name: "invalid annotation key",
|
|
annotations: map[string][]byte{
|
|
"http://foo.example.com": []byte("bar"),
|
|
},
|
|
},
|
|
{
|
|
name: "annotation value size too large",
|
|
annotations: map[string][]byte{
|
|
"simple": []byte(strings.Repeat("a", 32*1024)),
|
|
},
|
|
},
|
|
{
|
|
name: "annotations size too large",
|
|
annotations: map[string][]byte{
|
|
"simple": []byte(strings.Repeat("a", 31*1024)),
|
|
"simple2": []byte(strings.Repeat("a", 1024)),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testContext(t)
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
envelopeService.SetAnnotations(tt.annotations)
|
|
transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
|
|
testStateFunc(ctx, envelopeService, clock.RealClock{}, randomBool()),
|
|
testAPIServerID,
|
|
)
|
|
dataCtx := value.DefaultContext(testContextText)
|
|
|
|
_, err := transformer.TransformToStorage(ctx, []byte(testText), dataCtx)
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to validate annotations") {
|
|
t.Fatalf("expected error to contain 'failed to validate annotations', got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeDecode(t *testing.T) {
|
|
transformer := &envelopeTransformer{}
|
|
|
|
obj := &kmstypes.EncryptedObject{
|
|
EncryptedData: []byte{0x01, 0x02, 0x03},
|
|
KeyID: "1",
|
|
EncryptedDEKSource: []byte{0x04, 0x05, 0x06},
|
|
}
|
|
|
|
data, err := transformer.doEncode(obj)
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: error while encoding data: %s", err)
|
|
}
|
|
got, err := transformer.doDecode(data)
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: error while decoding data: %s", err)
|
|
}
|
|
// reset internal field modified by marshaling obj
|
|
obj.XXX_sizecache = 0
|
|
if !reflect.DeepEqual(got, obj) {
|
|
t.Fatalf("envelopeTransformer: decoded data does not match original data. Got: %v, want %v", got, obj)
|
|
}
|
|
}
|
|
|
|
func TestValidateEncryptedObject(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
desc string
|
|
originalData *kmstypes.EncryptedObject
|
|
expectedError error
|
|
}{
|
|
{
|
|
desc: "encrypted object is nil",
|
|
originalData: nil,
|
|
expectedError: fmt.Errorf("encrypted object is nil"),
|
|
},
|
|
{
|
|
desc: "encrypted data is nil",
|
|
originalData: &kmstypes.EncryptedObject{
|
|
KeyID: "1",
|
|
EncryptedDEKSource: []byte{0x01, 0x02, 0x03},
|
|
},
|
|
expectedError: fmt.Errorf("encrypted data is empty"),
|
|
},
|
|
{
|
|
desc: "encrypted data is []byte{}",
|
|
originalData: &kmstypes.EncryptedObject{
|
|
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)
|
|
if err == nil {
|
|
t.Fatalf("envelopeTransformer: expected error while decoding data, got nil")
|
|
}
|
|
|
|
if err.Error() != tt.expectedError.Error() {
|
|
t.Fatalf("doDecode() error: expected %v, got %v", tt.expectedError, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateAnnotations(t *testing.T) {
|
|
t.Parallel()
|
|
successCases := []map[string][]byte{
|
|
{"a.com": []byte("bar")},
|
|
{"k8s.io": []byte("bar")},
|
|
{"dev.k8s.io": []byte("bar")},
|
|
{"dev.k8s.io.": []byte("bar")},
|
|
{"foo.example.com": []byte("bar")},
|
|
{"this.is.a.really.long.fqdn": []byte("bar")},
|
|
{"bbc.co.uk": []byte("bar")},
|
|
{"10.0.0.1": []byte("bar")}, // DNS labels can start with numbers and there is no requirement for letters.
|
|
{"hyphens-are-good.k8s.io": []byte("bar")},
|
|
{strings.Repeat("a", 63) + ".k8s.io": []byte("bar")},
|
|
{strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + strings.Repeat("c", 63) + "." + strings.Repeat("d", 54) + ".k8s.io": []byte("bar")},
|
|
}
|
|
t.Run("success", func(t *testing.T) {
|
|
for i := range successCases {
|
|
i := i
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
t.Parallel()
|
|
if err := validateAnnotations(successCases[i]); err != nil {
|
|
t.Errorf("case[%d] expected success, got %#v", i, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
atleastTwoSegmentsErrorMsg := "should be a domain with at least two segments separated by dots"
|
|
moreThan63CharsErrorMsg := "must be no more than 63 characters"
|
|
moreThan253CharsErrorMsg := "must be no more than 253 characters"
|
|
dns1123SubdomainErrorMsg := "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"
|
|
|
|
annotationsNameErrorCases := []struct {
|
|
annotations map[string][]byte
|
|
expect string
|
|
}{
|
|
{map[string][]byte{".": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"...": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{".io": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"com": []byte("bar")}, atleastTwoSegmentsErrorMsg},
|
|
{map[string][]byte{".com": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"Dev.k8s.io": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{".foo.example.com": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"*.example.com": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"*.bar.com": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"*.foo.bar.com": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"underscores_are_bad.k8s.io": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"foo@bar.example.com": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{"http://foo.example.com": []byte("bar")}, dns1123SubdomainErrorMsg},
|
|
{map[string][]byte{strings.Repeat("a", 64) + ".k8s.io": []byte("bar")}, moreThan63CharsErrorMsg},
|
|
{map[string][]byte{strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + strings.Repeat("c", 63) + "." + strings.Repeat("d", 55) + ".k8s.io": []byte("bar")}, moreThan253CharsErrorMsg},
|
|
}
|
|
|
|
t.Run("name error", func(t *testing.T) {
|
|
for i := range annotationsNameErrorCases {
|
|
i := i
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
t.Parallel()
|
|
err := validateAnnotations(annotationsNameErrorCases[i].annotations)
|
|
if err == nil {
|
|
t.Errorf("case[%d]: expected failure", i)
|
|
} else {
|
|
if !strings.Contains(err.Error(), annotationsNameErrorCases[i].expect) {
|
|
t.Errorf("case[%d]: error details do not include %q: %q", i, annotationsNameErrorCases[i].expect, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
maxSizeErrMsg := "which exceeds the max size of"
|
|
annotationsSizeErrorCases := []struct {
|
|
annotations map[string][]byte
|
|
expect string
|
|
}{
|
|
{map[string][]byte{"simple": []byte(strings.Repeat("a", 33*1024))}, maxSizeErrMsg},
|
|
{map[string][]byte{"simple": []byte(strings.Repeat("a", 32*1024))}, maxSizeErrMsg},
|
|
{map[string][]byte{"simple": []byte(strings.Repeat("a", 64*1024))}, maxSizeErrMsg},
|
|
{map[string][]byte{"simple": []byte(strings.Repeat("a", 31*1024)), "simple2": []byte(strings.Repeat("a", 1024))}, maxSizeErrMsg},
|
|
{map[string][]byte{strings.Repeat("a", 253): []byte(strings.Repeat("a", 32*1024))}, maxSizeErrMsg},
|
|
}
|
|
t.Run("size error", func(t *testing.T) {
|
|
for i := range annotationsSizeErrorCases {
|
|
i := i
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
t.Parallel()
|
|
err := validateAnnotations(annotationsSizeErrorCases[i].annotations)
|
|
if err == nil {
|
|
t.Errorf("case[%d]: expected failure", i)
|
|
} else {
|
|
if !strings.Contains(err.Error(), annotationsSizeErrorCases[i].expect) {
|
|
t.Errorf("case[%d]: error details do not include %q: %q", i, annotationsSizeErrorCases[i].expect, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateKeyID(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
keyID string
|
|
expectedError string
|
|
expectedErrorCode string
|
|
}{
|
|
{
|
|
name: "valid key ID",
|
|
keyID: "1234",
|
|
expectedError: "",
|
|
expectedErrorCode: "ok",
|
|
},
|
|
{
|
|
name: "empty key ID",
|
|
keyID: "",
|
|
expectedError: "keyID is empty",
|
|
expectedErrorCode: "empty",
|
|
},
|
|
{
|
|
name: "keyID size is greater than 1 kB",
|
|
keyID: strings.Repeat("a", 1024+1),
|
|
expectedError: "which exceeds the max size of",
|
|
expectedErrorCode: "too_long",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
errCode, err := ValidateKeyID(tt.keyID)
|
|
if tt.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error %q, got nil", tt.expectedError)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.expectedError) {
|
|
t.Fatalf("expected error %q, got %q", tt.expectedError, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
}
|
|
if tt.expectedErrorCode != string(errCode) {
|
|
t.Fatalf("expected %s errCode, got %s", tt.expectedErrorCode, string(errCode))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateEncryptedDEKSource(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
encryptedDEKSource []byte
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "encrypted DEK source is nil",
|
|
encryptedDEKSource: nil,
|
|
expectedError: "encrypted DEK source is empty",
|
|
},
|
|
{
|
|
name: "encrypted DEK source is empty",
|
|
encryptedDEKSource: []byte{},
|
|
expectedError: "encrypted DEK source is empty",
|
|
},
|
|
{
|
|
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 source",
|
|
encryptedDEKSource: []byte{0x01, 0x02, 0x03},
|
|
expectedError: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
err := validateEncryptedDEKSource(tt.encryptedDEKSource)
|
|
if tt.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error %q, got nil", tt.expectedError)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.expectedError) {
|
|
t.Fatalf("expected error %q, got %q", tt.expectedError, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnvelopeMetrics(t *testing.T) {
|
|
envelopeService := newTestEnvelopeService()
|
|
transformer := NewEnvelopeTransformer(envelopeService, testProviderName,
|
|
testStateFunc(testContext(t), envelopeService, clock.RealClock{}, randomBool()),
|
|
testAPIServerID,
|
|
)
|
|
|
|
dataCtx := value.DefaultContext(testContextText)
|
|
|
|
kmsv2Transformer := value.PrefixTransformer{Prefix: []byte("k8s:enc:kms:v2:"), Transformer: transformer}
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
keyVersionFromEncrypt string
|
|
prefix value.Transformer
|
|
metrics []string
|
|
want string
|
|
}{
|
|
{
|
|
desc: "keyIDHash total",
|
|
keyVersionFromEncrypt: testKeyVersion,
|
|
prefix: value.NewPrefixTransformers(nil, kmsv2Transformer),
|
|
metrics: []string{
|
|
"apiserver_envelope_encryption_key_id_hash_total",
|
|
},
|
|
want: fmt.Sprintf(`
|
|
# HELP apiserver_envelope_encryption_key_id_hash_total [ALPHA] Number of times a keyID is used split by transformation type, provider, and apiserver identity.
|
|
# TYPE apiserver_envelope_encryption_key_id_hash_total counter
|
|
apiserver_envelope_encryption_key_id_hash_total{apiserver_id_hash="%s",key_id_hash="%s",provider_name="%s",transformation_type="%s"} 1
|
|
apiserver_envelope_encryption_key_id_hash_total{apiserver_id_hash="%s",key_id_hash="%s",provider_name="%s",transformation_type="%s"} 1
|
|
`, testAPIServerIDHash, testKeyHash, testProviderName, metrics.FromStorageLabel, testAPIServerIDHash, testKeyHash, testProviderName, metrics.ToStorageLabel),
|
|
},
|
|
}
|
|
|
|
metrics.KeyIDHashTotal.Reset()
|
|
metrics.InvalidKeyIDFromStatusTotal.Reset()
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
defer metrics.KeyIDHashTotal.Reset()
|
|
defer metrics.InvalidKeyIDFromStatusTotal.Reset()
|
|
ctx := testContext(t)
|
|
envelopeService.keyVersion = tt.keyVersionFromEncrypt
|
|
transformedData, err := tt.prefix.TransformToStorage(ctx, []byte(testText), dataCtx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := tt.prefix.TransformFromStorage(ctx, transformedData, dataCtx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEnvelopeMetricsCache validates the correctness of the apiserver_envelope_encryption_dek_source_cache_size metric
|
|
// and asserts that all of the associated logic is go routine safe.
|
|
// 1. Multiple transformers are created, which should result in unique cache size for each provider
|
|
// 2. A transformer with known number of states was created to encrypt, then on restart, another transformer
|
|
// was created, which should result in expected number of cache keys for all the decryption calls for each
|
|
// state used previously for encryption.
|
|
func TestEnvelopeMetricsCache(t *testing.T) {
|
|
envelopeService := newTestEnvelopeService()
|
|
envelopeService.keyVersion = testKeyVersion
|
|
state, err := testStateFunc(testContext(t), envelopeService, clock.RealClock{}, randomBool())()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := testContext(t)
|
|
dataCtx := value.DefaultContext(testContextText)
|
|
provider1 := "one"
|
|
provider2 := "two"
|
|
numOfStates := 10
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
metrics []string
|
|
want string
|
|
}{
|
|
{
|
|
desc: "dek source cache size",
|
|
metrics: []string{
|
|
"apiserver_envelope_encryption_dek_source_cache_size",
|
|
},
|
|
want: fmt.Sprintf(`
|
|
# HELP apiserver_envelope_encryption_dek_source_cache_size [ALPHA] Number of records in data encryption key (DEK) source cache. On a restart, this value is an approximation of the number of decrypt RPC calls the server will make to the KMS plugin.
|
|
# TYPE apiserver_envelope_encryption_dek_source_cache_size gauge
|
|
apiserver_envelope_encryption_dek_source_cache_size{provider_name="%s"} %d
|
|
apiserver_envelope_encryption_dek_source_cache_size{provider_name="%s"} 1
|
|
`, provider1, numOfStates, provider2),
|
|
},
|
|
}
|
|
transformer1 := NewEnvelopeTransformer(envelopeService, provider1, func() (State, error) {
|
|
// return different states to ensure we get expected number of cache keys after restart on decryption
|
|
return testStateFunc(ctx, envelopeService, clock.RealClock{}, randomBool())()
|
|
}, testAPIServerID)
|
|
transformer2 := NewEnvelopeTransformer(envelopeService, provider2, func() (State, error) { return state, nil }, testAPIServerID)
|
|
// used for restart
|
|
transformer3 := NewEnvelopeTransformer(envelopeService, provider1, func() (State, error) { return state, nil }, testAPIServerID)
|
|
var transformedDatas [][]byte
|
|
for j := 0; j < numOfStates; j++ {
|
|
transformedData, err := transformer1.TransformToStorage(ctx, []byte(testText), dataCtx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
transformedDatas = append(transformedDatas, transformedData)
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
metrics.DekSourceCacheSize.Reset()
|
|
var wg sync.WaitGroup
|
|
wg.Add(2 * numOfStates)
|
|
for i := 0; i < numOfStates; i++ {
|
|
i := i
|
|
go func() {
|
|
defer wg.Done()
|
|
// mimick a restart, the server will make decrypt RPC calls to the KMS plugin
|
|
// check cache metrics for the decrypt / read flow, which should repopulate the cache
|
|
if _, _, err := transformer3.TransformFromStorage(ctx, transformedDatas[i], dataCtx); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
// check cache metrics for the encrypt / write flow
|
|
_, err := transformer2.TransformToStorage(ctx, []byte(testText), dataCtx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var flagOnce sync.Once // support running `go test -count X`
|
|
|
|
func TestEnvelopeLogging(t *testing.T) {
|
|
flagOnce.Do(func() {
|
|
klog.InitFlags(nil)
|
|
})
|
|
flag.Set("v", "6")
|
|
flag.Parse()
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
ctx context.Context
|
|
wantLogs []string
|
|
}{
|
|
{
|
|
desc: "no request info in context",
|
|
ctx: testContext(t),
|
|
wantLogs: []string{
|
|
`"encrypting content using envelope service" uid="UID"`,
|
|
`"encrypting content using DEK" uid="UID" key="0123456789" group="" version="" resource="" subresource="" verb="" namespace="" name=""`,
|
|
`"decrypting content using envelope service" uid="UID" key="0123456789" group="" version="" resource="" subresource="" verb="" namespace="" name=""`,
|
|
},
|
|
},
|
|
{
|
|
desc: "request info in context",
|
|
ctx: genericapirequest.WithRequestInfo(testContext(t), &genericapirequest.RequestInfo{
|
|
APIGroup: "awesome.bears.com",
|
|
APIVersion: "v1",
|
|
Resource: "pandas",
|
|
Subresource: "status",
|
|
Namespace: "kube-system",
|
|
Name: "panda",
|
|
Verb: "update",
|
|
}),
|
|
wantLogs: []string{
|
|
`"encrypting content using envelope service" uid="UID"`,
|
|
`"encrypting content using DEK" uid="UID" key="0123456789" group="awesome.bears.com" version="v1" resource="pandas" subresource="status" verb="update" namespace="kube-system" name="panda"`,
|
|
`"decrypting content using envelope service" uid="UID" key="0123456789" group="awesome.bears.com" version="v1" resource="pandas" subresource="status" verb="update" namespace="kube-system" name="panda"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
klog.SetOutput(&buf)
|
|
klog.LogToStderr(false)
|
|
defer klog.LogToStderr(true)
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName,
|
|
testStateFunc(tc.ctx, envelopeService, clock.RealClock{}, randomBool()), testAPIServerID, 1*time.Second, fakeClock)
|
|
|
|
dataCtx := value.DefaultContext([]byte(testContextText))
|
|
originalText := []byte(testText)
|
|
|
|
transformedData, err := transformer.TransformToStorage(tc.ctx, originalText, dataCtx)
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: error while transforming data to storage: %v", err)
|
|
}
|
|
|
|
// advance the clock to trigger cache to expire, so we make a decrypt call that will log
|
|
fakeClock.Step(2 * time.Second)
|
|
// force GC to run by performing a write
|
|
transformer.(*envelopeTransformer).cache.set([]byte("some-other-unrelated-key"), &envelopeTransformer{})
|
|
|
|
_, _, err = transformer.TransformFromStorage(tc.ctx, transformedData, dataCtx)
|
|
if err != nil {
|
|
t.Fatalf("could not decrypt Envelope transformer's encrypted data even once: %v", err)
|
|
}
|
|
|
|
klog.Flush()
|
|
klog.SetOutput(&bytes.Buffer{}) // prevent further writes into buf
|
|
capturedOutput := buf.String()
|
|
|
|
// replace the uid with a constant to make the test output stable and assertable
|
|
capturedOutput = regexp.MustCompile(`uid="[^"]+"`).ReplaceAllString(capturedOutput, `uid="UID"`)
|
|
|
|
for _, wantLog := range tc.wantLogs {
|
|
if !strings.Contains(capturedOutput, wantLog) {
|
|
t.Errorf("expected log %q, got %q", wantLog, capturedOutput)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCacheNotCorrupted(t *testing.T) {
|
|
ctx := testContext(t)
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
envelopeService.SetAnnotations(map[string][]byte{
|
|
"encrypted-dek.kms.kubernetes.io": []byte("encrypted-dek-0"),
|
|
})
|
|
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
|
|
state, err := testStateFunc(ctx, envelopeService, fakeClock, randomBool())()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName,
|
|
func() (State, error) { return state, nil }, testAPIServerID,
|
|
1*time.Second, fakeClock)
|
|
|
|
dataCtx := value.DefaultContext(testContextText)
|
|
originalText := []byte(testText)
|
|
|
|
transformedData1, err := transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: error while transforming data to storage: %s", err)
|
|
}
|
|
|
|
// this is to mimic a plugin that sets a static response for ciphertext
|
|
// 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 source is different.
|
|
envelopeService.SetAnnotations(map[string][]byte{
|
|
"encrypted-dek.kms.kubernetes.io": []byte("encrypted-dek-1"),
|
|
})
|
|
|
|
state, err = testStateFunc(ctx, envelopeService, fakeClock, randomBool())()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
transformer = newEnvelopeTransformerWithClock(envelopeService, testProviderName,
|
|
func() (State, error) { return state, nil }, testAPIServerID,
|
|
1*time.Second, fakeClock)
|
|
|
|
transformedData2, err := transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
if err != nil {
|
|
t.Fatalf("envelopeTransformer: error while transforming data to storage: %s", err)
|
|
}
|
|
|
|
if _, _, err := transformer.TransformFromStorage(ctx, transformedData1, dataCtx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := transformer.TransformFromStorage(ctx, transformedData2, dataCtx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestGenerateCacheKey(t *testing.T) {
|
|
encryptedDEKSource1 := []byte{1, 2, 3}
|
|
keyID1 := "id1"
|
|
annotations1 := map[string][]byte{"a": {4, 5}, "b": {6, 7}}
|
|
encryptedDEKSourceType1 := kmstypes.EncryptedDEKSourceType_AES_GCM_KEY
|
|
|
|
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 {
|
|
encryptedDEKSourceType kmstypes.EncryptedDEKSourceType
|
|
encryptedDEKSource []byte
|
|
keyID string
|
|
annotations map[string][]byte
|
|
}{
|
|
{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 {
|
|
tc := tc
|
|
for _, tc2 := range testCases {
|
|
tc2 := tc2
|
|
t.Run(fmt.Sprintf("%+v-%+v", tc, tc2), func(t *testing.T) {
|
|
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))
|
|
}
|
|
if bytes.Equal(key1, key2) != reflect.DeepEqual(tc, tc2) {
|
|
t.Errorf("expected %v, got %v", reflect.DeepEqual(tc, tc2), bytes.Equal(key1, key2))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateTransformer(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
envelopeService func() kmsservice.Service
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "encrypt call fails",
|
|
envelopeService: func() kmsservice.Service {
|
|
envelopeService := newTestEnvelopeService()
|
|
envelopeService.SetDisabledStatus(true)
|
|
return envelopeService
|
|
},
|
|
expectedErr: "Envelope service was disabled",
|
|
},
|
|
{
|
|
name: "invalid key ID",
|
|
envelopeService: func() kmsservice.Service {
|
|
envelopeService := newTestEnvelopeService()
|
|
envelopeService.keyVersion = ""
|
|
return envelopeService
|
|
},
|
|
expectedErr: "failed to validate key id: keyID is empty",
|
|
},
|
|
{
|
|
name: "invalid encrypted DEK",
|
|
envelopeService: func() kmsservice.Service {
|
|
envelopeService := newTestEnvelopeService()
|
|
envelopeService.SetCiphertext([]byte{})
|
|
return envelopeService
|
|
},
|
|
expectedErr: "failed to validate encrypted DEK source: encrypted DEK source is empty",
|
|
},
|
|
{
|
|
name: "invalid annotations",
|
|
envelopeService: func() kmsservice.Service {
|
|
envelopeService := newTestEnvelopeService()
|
|
envelopeService.SetAnnotations(map[string][]byte{"invalid": {}})
|
|
return envelopeService
|
|
},
|
|
expectedErr: "failed to validate annotations: annotations: Invalid value: \"invalid\": should be a domain with at least two segments separated by dots",
|
|
},
|
|
{
|
|
name: "success",
|
|
envelopeService: func() kmsservice.Service {
|
|
return newTestEnvelopeService()
|
|
},
|
|
expectedErr: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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))
|
|
}
|
|
if transformer == nil {
|
|
t.Error("expected transformer, got nil")
|
|
}
|
|
if encObject == nil {
|
|
t.Error("expected encrypt response, got nil")
|
|
}
|
|
if cacheKey == nil {
|
|
t.Error("expected cache key, got nil")
|
|
}
|
|
} else {
|
|
if err == nil || !strings.Contains(err.Error(), tc.expectedErr) {
|
|
t.Errorf("expected error %q, got %q", tc.expectedErr, errString(err))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnvelopeTracing_TransformToStorage(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
expected []string
|
|
}{
|
|
{
|
|
desc: "encrypt",
|
|
expected: []string{
|
|
"About to encrypt data using DEK",
|
|
"Data encryption succeeded",
|
|
"About to encode encrypted object",
|
|
"Encoded encrypted object",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
fakeRecorder := tracetest.NewSpanRecorder()
|
|
otelTracer := trace.NewTracerProvider(trace.WithSpanProcessor(fakeRecorder)).Tracer("test")
|
|
|
|
ctx := testContext(t)
|
|
ctx, span := otelTracer.Start(ctx, "parent")
|
|
defer span.End()
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, randomBool())()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName,
|
|
func() (State, error) { return state, nil }, testAPIServerID, 1*time.Second, fakeClock)
|
|
|
|
dataCtx := value.DefaultContext([]byte(testContextText))
|
|
originalText := []byte(testText)
|
|
|
|
if _, err := transformer.TransformToStorage(ctx, originalText, dataCtx); err != nil {
|
|
t.Fatalf("envelopeTransformer: error while transforming data to storage: %v", err)
|
|
}
|
|
|
|
output := fakeRecorder.Ended()
|
|
if len(output) != 1 {
|
|
t.Fatalf("expected 1 span, got %d", len(output))
|
|
}
|
|
out := output[0]
|
|
validateTraceSpan(t, out, "TransformToStorage with envelopeTransformer", testProviderName, testAPIServerID, tc.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnvelopeTracing_TransformFromStorage(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
cacheTTL time.Duration
|
|
simulateKMSPluginFailure bool
|
|
expected []string
|
|
}{
|
|
{
|
|
desc: "decrypt",
|
|
cacheTTL: 5 * time.Second,
|
|
expected: []string{
|
|
"About to decode encrypted object",
|
|
"Decoded encrypted object",
|
|
"About to decrypt data using DEK",
|
|
"Data decryption succeeded",
|
|
},
|
|
},
|
|
{
|
|
desc: "decrypt with cache miss",
|
|
cacheTTL: 1 * time.Second,
|
|
expected: []string{
|
|
"About to decode encrypted object",
|
|
"Decoded encrypted object",
|
|
"About to decrypt DEK using remote service",
|
|
"DEK decryption succeeded",
|
|
"About to decrypt data using DEK",
|
|
"Data decryption succeeded",
|
|
},
|
|
},
|
|
{
|
|
desc: "decrypt with cache miss, simulate KMS plugin failure",
|
|
cacheTTL: 1 * time.Second,
|
|
simulateKMSPluginFailure: true,
|
|
expected: []string{
|
|
"About to decode encrypted object",
|
|
"Decoded encrypted object",
|
|
"About to decrypt DEK using remote service",
|
|
"DEK decryption failed",
|
|
"exception",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
fakeRecorder := tracetest.NewSpanRecorder()
|
|
otelTracer := trace.NewTracerProvider(trace.WithSpanProcessor(fakeRecorder)).Tracer("test")
|
|
|
|
ctx := testContext(t)
|
|
|
|
envelopeService := newTestEnvelopeService()
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
state, err := testStateFunc(ctx, envelopeService, clock.RealClock{}, randomBool())()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
transformer := newEnvelopeTransformerWithClock(envelopeService, testProviderName,
|
|
func() (State, error) { return state, nil }, testAPIServerID, tc.cacheTTL, fakeClock)
|
|
|
|
dataCtx := value.DefaultContext([]byte(testContextText))
|
|
originalText := []byte(testText)
|
|
|
|
transformedData, _ := transformer.TransformToStorage(ctx, originalText, dataCtx)
|
|
|
|
// advance the clock to allow cache entries to expire depending on TTL
|
|
fakeClock.Step(2 * time.Second)
|
|
// force GC to run by performing a write
|
|
transformer.(*envelopeTransformer).cache.set([]byte("some-other-unrelated-key"), &envelopeTransformer{})
|
|
|
|
envelopeService.SetDisabledStatus(tc.simulateKMSPluginFailure)
|
|
|
|
// start recording only for the decrypt call
|
|
ctx, span := otelTracer.Start(ctx, "parent")
|
|
defer span.End()
|
|
|
|
_, _, _ = transformer.TransformFromStorage(ctx, transformedData, dataCtx)
|
|
|
|
output := fakeRecorder.Ended()
|
|
validateTraceSpan(t, output[0], "TransformFromStorage with envelopeTransformer", testProviderName, testAPIServerID, tc.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func validateTraceSpan(t *testing.T, span trace.ReadOnlySpan, spanName, providerName, apiserverID string, expected []string) {
|
|
t.Helper()
|
|
|
|
if span.Name() != spanName {
|
|
t.Fatalf("expected span name %q, got %q", spanName, span.Name())
|
|
}
|
|
attrs := span.Attributes()
|
|
if len(attrs) != 1 {
|
|
t.Fatalf("expected 1 attributes, got %d", len(attrs))
|
|
}
|
|
if attrs[0].Key != "transformer.provider.name" && attrs[0].Value.AsString() != providerName {
|
|
t.Errorf("expected providerName %q, got %q", providerName, attrs[0].Value.AsString())
|
|
}
|
|
if len(span.Events()) != len(expected) {
|
|
t.Fatalf("expected %d events, got %d", len(expected), len(span.Events()))
|
|
}
|
|
for i, event := range span.Events() {
|
|
if event.Name != expected[i] {
|
|
t.Errorf("expected event %q, got %q", expected[i], event.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func errString(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
|
|
return err.Error()
|
|
}
|
|
|
|
func randomBool() bool { return utilrand.Int()%2 == 1 }
|