From 711a9852176ba2c1d1fe3b96a11e438c311a1976 Mon Sep 17 00:00:00 2001 From: Krzysztof Ostrowski Date: Thu, 2 Feb 2023 22:07:51 +0100 Subject: [PATCH 1/2] kmsv2: add LocalKEKService Signed-off-by: Krzysztof Ostrowski --- .../src/k8s.io/kms/encryption/interface.go | 68 ++ staging/src/k8s.io/kms/encryption/service.go | 197 +++++ .../src/k8s.io/kms/encryption/service_test.go | 687 ++++++++++++++++++ 3 files changed, 952 insertions(+) create mode 100644 staging/src/k8s.io/kms/encryption/interface.go create mode 100644 staging/src/k8s.io/kms/encryption/service.go create mode 100644 staging/src/k8s.io/kms/encryption/service_test.go diff --git a/staging/src/k8s.io/kms/encryption/interface.go b/staging/src/k8s.io/kms/encryption/interface.go new file mode 100644 index 00000000000..8ad755646a4 --- /dev/null +++ b/staging/src/k8s.io/kms/encryption/interface.go @@ -0,0 +1,68 @@ +/* +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 encryption + +import "context" + +// Store is a simple interface to store and retrieve Transformer. It is expected +// to be thread-safe. +type Store interface { + // Add adds a transformer to the store with its encrypted key as key. + Add([]byte, Transformer) + // Get returns a transformer from the store by its encrypted key as key. + Get([]byte) (Transformer, bool) +} + +// CreateTransformer enables the creation of a Transformer based on a key. +type CreateTransformer interface { + // Transformer creates a transformer with a given key. + Transformer(context.Context, []byte) (Transformer, error) + // Key creates a key that should match the expectations of Transformer(). + Key() ([]byte, error) +} + +/* +Copied from: + - "k8s.io/apiserver/pkg/storage/value" + - "k8s.io/apiserver/pkg/storage/value/encrypt/aes" +*/ + +// Transformer allows a value to be transformed before being read from or written to the underlying store. The methods +// must be able to undo the transformation caused by the other. +type Transformer interface { + // TransformFromStorage may transform the provided data from its underlying storage representation or return an error. + // Stale is true if the object on disk is stale and a write to etcd should be issued, even if the contents of the object + // have not changed. + TransformFromStorage(ctx context.Context, data []byte, dataCtx Context) (out []byte, stale bool, err error) + // TransformToStorage may transform the provided data into the appropriate form in storage or return an error. + TransformToStorage(ctx context.Context, data []byte, dataCtx Context) (out []byte, err error) +} + +// Context is additional information that a storage transformation may need to verify the data at rest. +type Context interface { + // AuthenticatedData should return an array of bytes that describes the current value. If the value changes, + // the transformer may report the value as unreadable or tampered. This may be nil if no such description exists + // or is needed. For additional verification, set this to data that strongly identifies the value, such as + // the key and creation version of the stored data. + AuthenticatedData() []byte +} + +// DefaultContext is a simple implementation of Context for a slice of bytes. +type DefaultContext []byte + +// AuthenticatedData returns itself. +func (c DefaultContext) AuthenticatedData() []byte { return c } diff --git a/staging/src/k8s.io/kms/encryption/service.go b/staging/src/k8s.io/kms/encryption/service.go new file mode 100644 index 00000000000..caef5e773b5 --- /dev/null +++ b/staging/src/k8s.io/kms/encryption/service.go @@ -0,0 +1,197 @@ +/* +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 encryption + +import ( + "context" + "errors" + + "k8s.io/klog/v2" + "k8s.io/kms/service" +) + +var ( + // ErrNoCipher means that there is no remote kms given and therefore the keys in use can't be protected. + ErrNoCipher = errors.New("no remote encryption service was specified") + // EmptyContext is an empty slice of bytes. + EmptyContext = DefaultContext([]byte{}) + // LocalKEKID is the key used to store the localKEK in the annotations. + LocalKEKID = "kmsv2:local-kek" +) + +// LocalKEKService adds an additional KEK layer to reduce calls to the remote +// KMS. +// The KEKs are stored as transformers in the local store. The encrypted +// form of the KEK is used to pick a transformer from the store. The KEKs should +// be encrypted by the remote KMS. +// There is a distinguished KEK (localKEK), that is generated and used by the +// LocalKEKService to encrypt. +type LocalKEKService struct { + // remoteKMS is the remote kms that is used to encrypt the local KEKs. + remoteKMS service.Service + // remoteKMSID is the ID that helps remoteKMS to decrypt localKEKID. + remoteKMSID string + // localKEKID is the localKEK in encrypted form. + localKEKID []byte + + // transformers is a store that holds all known transformers. + transformers Store + // createTransformer creates a new transformer and appropriate keys. + createTransformer CreateTransformer + // createUID creates a new uid. + createUID func() (string, error) +} + +// NewLocalKEKService is being initialized with a key that is encrypted by the +// remoteService. In the current implementation, the localKEK Service needs to be +// restarted by the caller after security thresholds are met. +func NewLocalKEKService( + ctx context.Context, + remoteService service.Service, + store Store, + createTransformer CreateTransformer, + createUID func() (string, error), // TODO add sensible defaults, use functional options +) (*LocalKEKService, error) { + if remoteService == nil { + klog.V(2).InfoS("can't create LocalKEKService without remoteService") + return nil, ErrNoCipher + } + + key, err := createTransformer.Key() + if err != nil { + klog.V(2).InfoS("create key", "err", err) + return nil, err + } + + transformer, err := createTransformer.Transformer(ctx, key) + if err != nil { + klog.V(2).InfoS("create new cipher", "err", err) + return nil, err + } + + uid, err := createUID() + if err != nil { + klog.V(2).InfoS("create new uid", "err", err) + return nil, err + } + + encRes, err := remoteService.Encrypt(ctx, uid, key) + if err != nil { + klog.V(2).InfoS("encrypt with remote", "err", err) + return nil, err + } + + store.Add(encRes.Ciphertext, transformer) + + return &LocalKEKService{ + remoteKMSID: encRes.KeyID, + remoteKMS: remoteService, + localKEKID: encRes.Ciphertext, + + transformers: store, + createTransformer: createTransformer, + createUID: createUID, + }, nil +} + +// getTransformer returns the transformer for the given keyID. If the keyID is +// not known, the key gets decrypted by the remoteKMS. +func (m *LocalKEKService) getTransformer(ctx context.Context, encKey []byte, uid, keyID string) (Transformer, error) { + transformer, ok := m.transformers.Get(encKey) + if ok { + return transformer, nil + } + + // Decrypt the unknown key with remote KMS. Plainkey must be treated with secrecy. + plainKey, err := m.remoteKMS.Decrypt(ctx, uid, &service.DecryptRequest{ + Ciphertext: encKey, + KeyID: keyID, + }) + if err != nil { + klog.V(2).InfoS("decrypt key with remote key", "id", uid, "err", err) + + return nil, err + } + + t, err := m.createTransformer.Transformer(ctx, plainKey) + if err != nil { + klog.V(2).InfoS("create transformer", "id", uid, "err", err) + return nil, err + } + + // Overwrite the plain key with 0s. + copy(plainKey, make([]byte, len(plainKey))) + + m.transformers.Add(encKey, t) + + return t, nil +} + +// Encrypt encrypts the plaintext with the localKEK. +func (m *LocalKEKService) Encrypt(ctx context.Context, uid string, pt []byte) (*service.EncryptResponse, error) { + // It could happen that the localKEK is not available, if the store is an expiring cache. + transformer, err := m.getTransformer(ctx, m.localKEKID, uid, m.remoteKMSID) + if err != nil { + klog.V(2).InfoS("encrypt plaintext", "id", uid, "err", err) + return nil, err + } + + ct, err := transformer.TransformToStorage(ctx, pt, EmptyContext) + if err != nil { + klog.V(2).InfoS("encrypt plaintext", "id", uid, "err", err) + return nil, err + } + + return &service.EncryptResponse{ + Ciphertext: ct, + KeyID: m.remoteKMSID, + Annotations: map[string][]byte{ + LocalKEKID: m.localKEKID, + }, + }, nil +} + +// Decrypt attempts to decrypt the ciphertext with the localKEK, a KEK from the +// store, or the remote KMS. +func (m *LocalKEKService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + encKEK, ok := req.Annotations[LocalKEKID] + if !ok { + // If there is no local KEK ID in the annotations, we must delegate to remote KMS. + pt, err := m.remoteKMS.Decrypt(ctx, uid, req) + if err != nil { + klog.V(2).InfoS("decrypt key with remote key", "id", uid, "err", err) + + return nil, err + } + + return pt, nil + } + + transformer, err := m.getTransformer(ctx, encKEK, uid, req.KeyID) + if err != nil { + klog.V(2).InfoS("decrypt ciphertext", "id", uid, "err", err) + return nil, err + } + + pt, _, err := transformer.TransformFromStorage(ctx, req.Ciphertext, EmptyContext) + if err != nil { + klog.V(2).InfoS("decrypt ciphertext with pulled key", "id", uid, "err", err) + return nil, err + } + + return pt, nil +} diff --git a/staging/src/k8s.io/kms/encryption/service_test.go b/staging/src/k8s.io/kms/encryption/service_test.go new file mode 100644 index 00000000000..d1f6178df82 --- /dev/null +++ b/staging/src/k8s.io/kms/encryption/service_test.go @@ -0,0 +1,687 @@ +/* +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 encryption + +import ( + "bytes" + "context" + "errors" + "testing" + + "k8s.io/kms/service" +) + +var ( + errCreateKey = errors.New("can't create key") + errCreateTransformer = errors.New("can't create transformer") + errCreateUID = errors.New("can't create uid") + errRemoteEncrypt = errors.New("can't encrypt with remote kms") + errRemoteDecrypt = errors.New("can't decrypt with remote kms") + errLocallyEncrypt = errors.New("can't encrypt with local kms") + errLocallyDecrypt = errors.New("can't decrypt with local kms") + errStoreDecrypt = errors.New("can't decrypt with local store") + errWrongValue = errors.New("wrong value") + errNotBeCalled = errors.New("should not be called") + + keyID = "id:123" + genKey = []byte("genkey:123") + storeKey = []byte("storekey:123") + anonKey = []byte("anonkey:123") + plainKey = []byte("plainkey:123") + encryptKey = []byte("enckey:123") + decryptKey = []byte("deckey:123") + encryptMessage = []byte("encrypt_to_storage") + decryptMessage = []byte("decrypt_from_storage") +) + +func TestService(t *testing.T) { + t.Parallel() + + initTestCases := []struct { + name string + localKEKService func() (*LocalKEKService, error) + err error + }{ + { + name: "should fail if no remote service is given", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + nil, nil, nil, nil, + ) + }, + err: ErrNoCipher, + }, + { + name: "should fail if you can't create a key", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{}, + nil, + &createTransformer{ + key: func() ([]byte, error) { + return nil, errCreateKey + }, + }, + nil, + ) + }, + err: errCreateKey, + }, + { + name: "should fail if you can't create a transformer", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{}, + nil, + &createTransformer{ + key: genLocalKey, + transformer: func(ctx context.Context, key []byte) (Transformer, error) { + if !bytes.Equal(key, genKey) { + return nil, errWrongValue + } + + return nil, errCreateTransformer + }, + }, + nil, + ) + }, + err: errCreateTransformer, + }, + { + name: "should fail if you can't create a uid", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{}, + nil, + &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, + func() (string, error) { + return "", errCreateUID + }, + ) + }, + err: errCreateUID, + }, + { + name: "should fail if you can't encrypt remotely", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: func(ctx context.Context, uid string, key []byte) (*service.EncryptResponse, error) { + return nil, errRemoteEncrypt + }, + }, + nil, + &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, + genUID, + ) + }, + err: errRemoteEncrypt, + }, + { + name: "should succeed initializing", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{encrypt: remoteEncryptDefault}, + makeEmptyStore(), + &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, + genUID, + ) + }, + }, + } + + for _, tc := range initTestCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + localKEKService, err := tc.localKEKService() + if err == tc.err { + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if localKEKService == nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } + + encryptTestCases := []struct { + name string + localKEKService func() (*LocalKEKService, error) + err error + }{ + { + name: "should fail if local kek is not in cache and remote decrypt fails", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: func(ctx context.Context, uid string, decReq *service.DecryptRequest) ([]byte, error) { + return nil, errRemoteDecrypt + }, + }, + makeEmptyStore(), + &createTransformer{ + key: genLocalKey, + transformer: func(context.Context, []byte) (Transformer, error) { + return &testTransformer{ + transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { + return encryptMessage, nil + }, + }, nil + }, + }, + genUID, + ) + }, + err: errRemoteDecrypt, + }, + { + name: "should fail if you can't encrypt locally", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, + &testStore{ + add: func([]byte, Transformer) {}, + get: func(encKey []byte) (Transformer, bool) { + return &testTransformer{ + transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { + return nil, errLocallyEncrypt + }, + }, true + }, + }, + &createTransformer{ + key: genLocalKey, + transformer: func(context.Context, []byte) (Transformer, error) { + return &testTransformer{}, nil + }, + }, + genUID, + ) + }, + err: errLocallyEncrypt, + }, + { + name: "should succeed encrypting with stored transformer", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, + &testStore{ + add: func([]byte, Transformer) {}, + get: func(encKey []byte) (Transformer, bool) { + return &testTransformer{ + transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { + return encryptMessage, nil + }, + }, true + }, + }, + &createTransformer{ + key: genLocalKey, + transformer: func(context.Context, []byte) (Transformer, error) { + return &testTransformer{}, nil + }, + }, + genUID, + ) + }, + }, + { + name: "should succeed encrypting with stored transformer", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, + &testStore{ + add: func([]byte, Transformer) {}, + get: func(encKey []byte) (Transformer, bool) { + return &testTransformer{ + transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { + return encryptMessage, nil + }, + }, true + }, + }, + &createTransformer{ + key: genLocalKey, + transformer: func(context.Context, []byte) (Transformer, error) { + return &testTransformer{}, nil + }, + }, + genUID, + ) + }, + }, + { + name: "should succeed encrypting with created transformer", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: func(ctx context.Context, uid string, decReq *service.DecryptRequest) ([]byte, error) { + return encryptKey, nil + }, + }, + &testStore{ + add: func([]byte, Transformer) {}, + get: func(encKey []byte) (Transformer, bool) { + return nil, false + }, + }, + &createTransformer{ + key: genLocalKey, + transformer: func(context.Context, []byte) (Transformer, error) { + return &testTransformer{ + transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { + return encryptMessage, nil + }, + }, nil + }, + }, + genUID, + ) + }, + }, + } + + for _, tc := range encryptTestCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + localKEKService, err := tc.localKEKService() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + encRes, err := localKEKService.Encrypt(context.Background(), "id:999", []byte("message")) + if err == tc.err { + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(encryptMessage, encRes.Ciphertext) { + t.Fatalf("unexpected ciphertext - want: %s, have: %s", encryptMessage, encRes.Ciphertext) + } + }) + } + + noAnnotationsDecReq := &service.DecryptRequest{ + Ciphertext: encryptMessage, + KeyID: keyID, + Annotations: map[string][]byte{}, + } + + anonDecReq := &service.DecryptRequest{ + Ciphertext: encryptMessage, + KeyID: keyID, + Annotations: map[string][]byte{ + LocalKEKID: anonKey, + }, + } + + decryptTestCases := []struct { + name string + localKEKService func() (*LocalKEKService, error) + decReq *service.DecryptRequest + err error + }{ + { + name: "should fail decrypting without annotations, if remote decryption doesn't work", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + return nil, errRemoteDecrypt + }, + }, + makeEmptyStore(), + &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, + genUID, + ) + }, + decReq: noAnnotationsDecReq, + err: errRemoteDecrypt, + }, + { + name: "should succeed decrypting without annotations, if remote decryption works", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + return decryptMessage, nil + }, + }, + makeEmptyStore(), + &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, + genUID, + ) + }, + decReq: noAnnotationsDecReq, + }, + { + name: "should fail decrypting, if decrypting with remote KMS doesn't work for unknown key", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + return nil, errRemoteDecrypt + }, + }, + &testStore{ + add: func([]byte, Transformer) {}, + get: func(encKey []byte) (Transformer, bool) { return nil, false }, + }, + &createTransformer{ + key: genLocalKey, + transformer: func(context.Context, []byte) (Transformer, error) { + return &testTransformer{ + transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { + return nil, false, errNotBeCalled + }, + }, nil + }, + }, + genUID, + ) + }, + decReq: anonDecReq, + err: errRemoteDecrypt, + }, + { + name: "should fail decrypting, if creating a transformer fails with unknown key", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + return plainKey, nil + }, + }, + &testStore{ + add: func(key []byte, transformer Transformer) {}, + get: func(key []byte) (Transformer, bool) { + return nil, false + }, + }, + &createTransformer{ + key: genLocalKey, + transformer: func(ctx context.Context, key []byte) (Transformer, error) { + if bytes.Equal(key, plainKey) { + return nil, errCreateTransformer + } + + return &testTransformer{}, nil + }, + }, + genUID, + ) + }, + decReq: &service.DecryptRequest{ + Ciphertext: encryptMessage, + KeyID: keyID, + Annotations: map[string][]byte{ + LocalKEKID: anonKey, + }, + }, + err: errCreateTransformer, + }, + { + name: "should fail decrypting, if decryption fails with stored transformer", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: remoteDecryptShouldNotBeCalled, + }, + &testStore{ + add: func(key []byte, transformer Transformer) {}, + get: func(key []byte) (Transformer, bool) { + if !bytes.Equal(key, storeKey) { + return nil, false + } + + return &testTransformer{ + transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { + return nil, false, errLocallyDecrypt + }, + }, true + }, + }, + &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, + genUID, + ) + }, + decReq: &service.DecryptRequest{ + Ciphertext: encryptMessage, + KeyID: keyID, + Annotations: map[string][]byte{ + LocalKEKID: storeKey, + }, + }, + err: errLocallyDecrypt, + }, + { + name: "should succeed decrypting, if decryption works with stored transformer", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: remoteDecryptShouldNotBeCalled, + }, + &testStore{ + add: func(key []byte, transformer Transformer) {}, + get: func(key []byte) (Transformer, bool) { + if !bytes.Equal(key, storeKey) { + return nil, false + } + + return &testTransformer{ + transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { + return decryptMessage, false, nil + }, + }, true + }, + }, + &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, + genUID, + ) + }, + decReq: &service.DecryptRequest{ + Ciphertext: encryptMessage, + KeyID: keyID, + Annotations: map[string][]byte{ + LocalKEKID: storeKey, + }, + }, + }, + { + name: "should succeed decrypting, with newly created transformer", + localKEKService: func() (*LocalKEKService, error) { + return NewLocalKEKService( + context.Background(), + &testService{ + encrypt: remoteEncryptDefault, + decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + return plainKey, nil + }, + }, + makeEmptyStore(), + &createTransformer{ + key: genLocalKey, + transformer: func(ctx context.Context, key []byte) (Transformer, error) { + return &testTransformer{ + transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { + return decryptMessage, false, nil + }, + }, nil + }, + }, + genUID, + ) + }, + decReq: anonDecReq, + }, + } + + for _, tc := range decryptTestCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + localKEKService, err := tc.localKEKService() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + pt, err := localKEKService.Decrypt(context.Background(), tc.name, tc.decReq) + if err == tc.err { + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(pt, decryptMessage) { + t.Fatalf("unexpected plaintext - want: %s, have: %s", decryptMessage, pt) + } + }) + } +} + +type testStore struct { + add func([]byte, Transformer) + get func([]byte) (Transformer, bool) +} + +var _ Store = (*testStore)(nil) + +func (t *testStore) Add(key []byte, transformer Transformer) { + t.add(key, transformer) +} + +func (t *testStore) Get(key []byte) (Transformer, bool) { + return t.get(key) +} + +type testTransformer struct { + transformFromStorage func(context.Context, []byte, Context) ([]byte, bool, error) + transformToStorage func(context.Context, []byte, Context) ([]byte, error) +} + +var _ Transformer = (*testTransformer)(nil) + +func (t *testTransformer) TransformFromStorage(ctx context.Context, data []byte, context Context) ([]byte, bool, error) { + return t.transformFromStorage(ctx, data, context) +} + +func (t *testTransformer) TransformToStorage(ctx context.Context, data []byte, context Context) ([]byte, error) { + return t.transformToStorage(ctx, data, context) +} + +type createTransformer struct { + transformer func(context.Context, []byte) (Transformer, error) + key func() ([]byte, error) +} + +var _ CreateTransformer = (*createTransformer)(nil) + +func (c *createTransformer) Transformer(ctx context.Context, key []byte) (Transformer, error) { + return c.transformer(ctx, key) +} + +func (c *createTransformer) Key() ([]byte, error) { + return c.key() +} + +type testService struct { + decrypt func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) + encrypt func(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) + status func(ctx context.Context) (*service.StatusResponse, error) +} + +var _ service.Service = (*testService)(nil) + +func (s *testService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + return s.decrypt(ctx, uid, req) +} + +func (s *testService) Encrypt(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) { + return s.encrypt(ctx, uid, data) +} + +func (s *testService) Status(ctx context.Context) (*service.StatusResponse, error) { + return s.status(ctx) +} + +func genUID() (string, error) { + return "id:001", nil +} + +func genLocalKey() ([]byte, error) { + return genKey, nil +} + +func makeEmptyTransformer(context.Context, []byte) (Transformer, error) { + return &testTransformer{}, nil +} + +func makeEmptyStore() *testStore { + return &testStore{ + add: func([]byte, Transformer) {}, + get: func(key []byte) (Transformer, bool) { + return nil, false + }, + } +} + +func remoteEncryptDefault(ctx context.Context, uid string, key []byte) (*service.EncryptResponse, error) { + return &service.EncryptResponse{ + Ciphertext: encryptKey, + KeyID: keyID, + Annotations: map[string][]byte{}, + }, nil +} + +func remoteDecryptShouldNotBeCalled(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + return nil, errNotBeCalled + +} From ee2e1ff99ad326ce0efa1b5a8a5ea63bf7b9494e Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Wed, 8 Feb 2023 16:37:41 -0500 Subject: [PATCH 2/2] implement service.Service interface and update localKEK generation Signed-off-by: Anish Ramasekar Co-authored-by: Monis Khan Signed-off-by: Anish Ramasekar --- staging/src/k8s.io/kms/encryption/service.go | 311 +++--- .../src/k8s.io/kms/encryption/service_test.go | 971 ++++++------------ staging/src/k8s.io/kms/go.mod | 2 +- staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go | 85 ++ .../{encryption => pkg/value}/interface.go | 29 +- 5 files changed, 618 insertions(+), 780 deletions(-) create mode 100644 staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go rename staging/src/k8s.io/kms/{encryption => pkg/value}/interface.go (71%) diff --git a/staging/src/k8s.io/kms/encryption/service.go b/staging/src/k8s.io/kms/encryption/service.go index caef5e773b5..89bc3d7dc0a 100644 --- a/staging/src/k8s.io/kms/encryption/service.go +++ b/staging/src/k8s.io/kms/encryption/service.go @@ -18,180 +18,245 @@ package encryption import ( "context" - "errors" + "crypto/aes" + "crypto/rand" + "encoding/base64" + "fmt" + "strings" + "sync" + "time" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" + aestransformer "k8s.io/kms/pkg/encrypt/aes" + "k8s.io/kms/pkg/value" "k8s.io/kms/service" + "k8s.io/utils/lru" ) var ( - // ErrNoCipher means that there is no remote kms given and therefore the keys in use can't be protected. - ErrNoCipher = errors.New("no remote encryption service was specified") - // EmptyContext is an empty slice of bytes. - EmptyContext = DefaultContext([]byte{}) - // LocalKEKID is the key used to store the localKEK in the annotations. - LocalKEKID = "kmsv2:local-kek" + // emptyContext is an empty slice of bytes. This is passed as value.Context to the + // GCM transformer. The grpc interface does not provide any additional authenticated data + // to use with AEAD. + emptyContext = value.DefaultContext([]byte{}) + // errInvalidKMSAnnotationKeySuffix is returned when the annotation key suffix is not allowed. + errInvalidKMSAnnotationKeySuffix = fmt.Errorf("annotation keys are not allowed to use %s", referenceSuffix) + + // these are var instead of const so that we can set them during tests + localKEKGenerationPollInterval = 1 * time.Second + localKEKGenerationPollTimeout = 5 * time.Minute ) +const ( + referenceSuffix = ".reference.encryption.k8s.io" + // referenceKEKAnnotationKey is the key used to store the localKEK in the annotations. + referenceKEKAnnotationKey = "encrypted-kek" + referenceSuffix + numAnnotations = 1 + cacheSize = 1_000 + // keyLength is the length of the local KEK in bytes. + // This is the same length used for the DEKs generated in kube-apiserver. + keyLength = 32 +) + +var _ service.Service = &LocalKEKService{} + // LocalKEKService adds an additional KEK layer to reduce calls to the remote // KMS. -// The KEKs are stored as transformers in the local store. The encrypted -// form of the KEK is used to pick a transformer from the store. The KEKs should -// be encrypted by the remote KMS. -// There is a distinguished KEK (localKEK), that is generated and used by the -// LocalKEKService to encrypt. +// The local KEK is generated once and stored in the LocalKEKService. This KEK +// is used for all encryption operations. For the decrypt operation, if the encrypted +// local KEK is not found in the cache, the remote KMS is used to decrypt the local KEK. type LocalKEKService struct { - // remoteKMS is the remote kms that is used to encrypt the local KEKs. - remoteKMS service.Service - // remoteKMSID is the ID that helps remoteKMS to decrypt localKEKID. - remoteKMSID string - // localKEKID is the localKEK in encrypted form. - localKEKID []byte + // remoteKMS is the remote kms that is used to encrypt and decrypt the local KEKs. + remoteKMS service.Service + remoteOnce sync.Once - // transformers is a store that holds all known transformers. - transformers Store - // createTransformer creates a new transformer and appropriate keys. - createTransformer CreateTransformer - // createUID creates a new uid. - createUID func() (string, error) + // transformers is a thread-safe LRU cache which caches decrypted DEKs indexed by their encrypted form. + transformers *lru.Cache + + remoteKMSResponse *service.EncryptResponse + localTransformer value.Transformer + localTransformerErr error } -// NewLocalKEKService is being initialized with a key that is encrypted by the -// remoteService. In the current implementation, the localKEK Service needs to be +// NewLocalKEKService is being initialized with a remote KMS service. +// In the current implementation, the localKEK Service needs to be // restarted by the caller after security thresholds are met. -func NewLocalKEKService( - ctx context.Context, - remoteService service.Service, - store Store, - createTransformer CreateTransformer, - createUID func() (string, error), // TODO add sensible defaults, use functional options -) (*LocalKEKService, error) { - if remoteService == nil { - klog.V(2).InfoS("can't create LocalKEKService without remoteService") - return nil, ErrNoCipher - } - - key, err := createTransformer.Key() - if err != nil { - klog.V(2).InfoS("create key", "err", err) - return nil, err - } - - transformer, err := createTransformer.Transformer(ctx, key) - if err != nil { - klog.V(2).InfoS("create new cipher", "err", err) - return nil, err - } - - uid, err := createUID() - if err != nil { - klog.V(2).InfoS("create new uid", "err", err) - return nil, err - } - - encRes, err := remoteService.Encrypt(ctx, uid, key) - if err != nil { - klog.V(2).InfoS("encrypt with remote", "err", err) - return nil, err - } - - store.Add(encRes.Ciphertext, transformer) - +// TODO(aramase): handle rotation of local KEKs +// - when the keyID in Status() no longer matches the keyID used during encryption +// - when the local KEK has been used for a certain number of times +func NewLocalKEKService(remoteService service.Service) *LocalKEKService { return &LocalKEKService{ - remoteKMSID: encRes.KeyID, - remoteKMS: remoteService, - localKEKID: encRes.Ciphertext, - - transformers: store, - createTransformer: createTransformer, - createUID: createUID, - }, nil + remoteKMS: remoteService, + transformers: lru.New(cacheSize), + } } -// getTransformer returns the transformer for the given keyID. If the keyID is -// not known, the key gets decrypted by the remoteKMS. -func (m *LocalKEKService) getTransformer(ctx context.Context, encKey []byte, uid, keyID string) (Transformer, error) { - transformer, ok := m.transformers.Get(encKey) - if ok { - return transformer, nil - } +func (m *LocalKEKService) getTransformerForEncryption(uid string) (value.Transformer, *service.EncryptResponse, error) { + // Check if we have a local KEK + // - If exists, use the local KEK for encryption and return + // - Not exists, generate local KEK, encrypt with remote KEK, + // store it in cache encrypt the data and return. This can be + // expensive but only 1 in N calls will incur this additional latency, + // N being number of times local KEK is reused) + m.remoteOnce.Do(func() { + m.localTransformerErr = wait.PollImmediateWithContext(context.Background(), localKEKGenerationPollInterval, localKEKGenerationPollTimeout, + func(ctx context.Context) (done bool, err error) { + key, err := generateKey(keyLength) + if err != nil { + return false, fmt.Errorf("failed to generate local KEK: %w", err) + } + block, err := aes.NewCipher(key) + if err != nil { + return false, fmt.Errorf("failed to create cipher block: %w", err) + } + transformer := aestransformer.NewGCMTransformer(block) - // Decrypt the unknown key with remote KMS. Plainkey must be treated with secrecy. - plainKey, err := m.remoteKMS.Decrypt(ctx, uid, &service.DecryptRequest{ - Ciphertext: encKey, - KeyID: keyID, + resp, err := m.remoteKMS.Encrypt(ctx, uid, key) + if err != nil { + klog.ErrorS(err, "failed to encrypt local KEK with remote KMS", "uid", uid) + return false, nil + } + if err = validateRemoteKMSResponse(resp); err != nil { + return false, fmt.Errorf("response annotations failed validation: %w", err) + } + m.remoteKMSResponse = copyResponseAndAddLocalKEKAnnotation(resp) + m.localTransformer = transformer + m.transformers.Add(base64.StdEncoding.EncodeToString(resp.Ciphertext), transformer) + return true, nil + }) }) - if err != nil { - klog.V(2).InfoS("decrypt key with remote key", "id", uid, "err", err) + return m.localTransformer, m.remoteKMSResponse, m.localTransformerErr +} - return nil, err +func copyResponseAndAddLocalKEKAnnotation(resp *service.EncryptResponse) *service.EncryptResponse { + annotations := make(map[string][]byte, len(resp.Annotations)+numAnnotations) + for s, bytes := range resp.Annotations { + s := s + bytes := bytes + annotations[s] = bytes } + annotations[referenceKEKAnnotationKey] = resp.Ciphertext - t, err := m.createTransformer.Transformer(ctx, plainKey) - if err != nil { - klog.V(2).InfoS("create transformer", "id", uid, "err", err) - return nil, err + return &service.EncryptResponse{ + // Ciphertext is not set on purpose - it is different per Encrypt call + KeyID: resp.KeyID, + Annotations: annotations, } - - // Overwrite the plain key with 0s. - copy(plainKey, make([]byte, len(plainKey))) - - m.transformers.Add(encKey, t) - - return t, nil } // Encrypt encrypts the plaintext with the localKEK. func (m *LocalKEKService) Encrypt(ctx context.Context, uid string, pt []byte) (*service.EncryptResponse, error) { - // It could happen that the localKEK is not available, if the store is an expiring cache. - transformer, err := m.getTransformer(ctx, m.localKEKID, uid, m.remoteKMSID) + transformer, resp, err := m.getTransformerForEncryption(uid) if err != nil { - klog.V(2).InfoS("encrypt plaintext", "id", uid, "err", err) + klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err) return nil, err } - ct, err := transformer.TransformToStorage(ctx, pt, EmptyContext) + ct, err := transformer.TransformToStorage(ctx, pt, emptyContext) if err != nil { - klog.V(2).InfoS("encrypt plaintext", "id", uid, "err", err) + klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err) return nil, err } return &service.EncryptResponse{ - Ciphertext: ct, - KeyID: m.remoteKMSID, - Annotations: map[string][]byte{ - LocalKEKID: m.localKEKID, - }, + Ciphertext: ct, + KeyID: resp.KeyID, // TODO what about rotation ?? + Annotations: resp.Annotations, }, nil } +func (m *LocalKEKService) getTransformerForDecryption(ctx context.Context, uid string, req *service.DecryptRequest) (value.Transformer, error) { + encKEK := req.Annotations[referenceKEKAnnotationKey] + + if _transformer, found := m.transformers.Get(base64.StdEncoding.EncodeToString(encKEK)); found { + return _transformer.(value.Transformer), nil + } + + key, err := m.remoteKMS.Decrypt(ctx, uid, &service.DecryptRequest{ + Ciphertext: encKEK, + KeyID: req.KeyID, + Annotations: annotationsWithoutReferenceKeys(req.Annotations), + }) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + transformer := aestransformer.NewGCMTransformer(block) + + // Overwrite the plain key with 0s. + copy(key, make([]byte, len(key))) + + m.transformers.Add(encKEK, transformer) + + return transformer, nil +} + // Decrypt attempts to decrypt the ciphertext with the localKEK, a KEK from the // store, or the remote KMS. func (m *LocalKEKService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - encKEK, ok := req.Annotations[LocalKEKID] - if !ok { - // If there is no local KEK ID in the annotations, we must delegate to remote KMS. - pt, err := m.remoteKMS.Decrypt(ctx, uid, req) - if err != nil { - klog.V(2).InfoS("decrypt key with remote key", "id", uid, "err", err) - - return nil, err - } - - return pt, nil + if _, ok := req.Annotations[referenceKEKAnnotationKey]; !ok { + return nil, fmt.Errorf("unable to find local KEK for request with uid %q", uid) } - transformer, err := m.getTransformer(ctx, encKEK, uid, req.KeyID) + transformer, err := m.getTransformerForDecryption(ctx, uid, req) if err != nil { - klog.V(2).InfoS("decrypt ciphertext", "id", uid, "err", err) - return nil, err + klog.V(2).InfoS("decrypt ciphertext", "uid", uid, "err", err) + return nil, fmt.Errorf("failed to get transformer for decryption: %w", err) } - pt, _, err := transformer.TransformFromStorage(ctx, req.Ciphertext, EmptyContext) + pt, _, err := transformer.TransformFromStorage(ctx, req.Ciphertext, emptyContext) if err != nil { - klog.V(2).InfoS("decrypt ciphertext with pulled key", "id", uid, "err", err) + klog.V(2).InfoS("decrypt ciphertext with pulled key", "uid", uid, "err", err) return nil, err } return pt, nil } + +// Status returns the status of the remote KMS. +func (m *LocalKEKService) Status(ctx context.Context) (*service.StatusResponse, error) { + // TODO(aramase): the response from the remote KMS is funneled through without any validation/action. + // This needs to handle the case when remote KEK has changed. The local KEK needs to be rotated and + // re-encrypted with the new remote KEK. + return m.remoteKMS.Status(ctx) +} + +func annotationsWithoutReferenceKeys(annotations map[string][]byte) map[string][]byte { + if len(annotations) <= numAnnotations { + return nil + } + + m := make(map[string][]byte, len(annotations)-numAnnotations) + for k, v := range annotations { + k, v := k, v + if strings.HasSuffix(k, referenceSuffix) { + continue + } + m[k] = v + } + return m +} + +func validateRemoteKMSResponse(resp *service.EncryptResponse) error { + // validate annotations don't contain the reference implementation annotations + for k := range resp.Annotations { + if strings.HasSuffix(k, referenceSuffix) { + return errInvalidKMSAnnotationKeySuffix + } + } + return nil +} + +// generateKey generates a random key using system randomness. +func generateKey(length int) (key []byte, err error) { + key = make([]byte, length) + if _, err = rand.Read(key); err != nil { + return nil, err + } + + return key, nil +} diff --git a/staging/src/k8s.io/kms/encryption/service_test.go b/staging/src/k8s.io/kms/encryption/service_test.go index d1f6178df82..4a46b27fcba 100644 --- a/staging/src/k8s.io/kms/encryption/service_test.go +++ b/staging/src/k8s.io/kms/encryption/service_test.go @@ -17,671 +17,378 @@ limitations under the License. package encryption import ( - "bytes" "context" + "encoding/base64" "errors" + "reflect" + "sync" "testing" + "time" "k8s.io/kms/service" ) -var ( - errCreateKey = errors.New("can't create key") - errCreateTransformer = errors.New("can't create transformer") - errCreateUID = errors.New("can't create uid") - errRemoteEncrypt = errors.New("can't encrypt with remote kms") - errRemoteDecrypt = errors.New("can't decrypt with remote kms") - errLocallyEncrypt = errors.New("can't encrypt with local kms") - errLocallyDecrypt = errors.New("can't decrypt with local kms") - errStoreDecrypt = errors.New("can't decrypt with local store") - errWrongValue = errors.New("wrong value") - errNotBeCalled = errors.New("should not be called") - - keyID = "id:123" - genKey = []byte("genkey:123") - storeKey = []byte("storekey:123") - anonKey = []byte("anonkey:123") - plainKey = []byte("plainkey:123") - encryptKey = []byte("enckey:123") - decryptKey = []byte("deckey:123") - encryptMessage = []byte("encrypt_to_storage") - decryptMessage = []byte("decrypt_from_storage") -) - -func TestService(t *testing.T) { - t.Parallel() - - initTestCases := []struct { - name string - localKEKService func() (*LocalKEKService, error) - err error +func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) { + testCases := []struct { + name string + input *service.EncryptResponse + want *service.EncryptResponse }{ { - name: "should fail if no remote service is given", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - nil, nil, nil, nil, - ) + name: "annotations is nil", + input: &service.EncryptResponse{ + Ciphertext: []byte("encryptedLocalKEK"), + KeyID: "keyID", + Annotations: nil, }, - err: ErrNoCipher, - }, - { - name: "should fail if you can't create a key", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{}, - nil, - &createTransformer{ - key: func() ([]byte, error) { - return nil, errCreateKey - }, - }, - nil, - ) - }, - err: errCreateKey, - }, - { - name: "should fail if you can't create a transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{}, - nil, - &createTransformer{ - key: genLocalKey, - transformer: func(ctx context.Context, key []byte) (Transformer, error) { - if !bytes.Equal(key, genKey) { - return nil, errWrongValue - } - - return nil, errCreateTransformer - }, - }, - nil, - ) - }, - err: errCreateTransformer, - }, - { - name: "should fail if you can't create a uid", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{}, - nil, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - func() (string, error) { - return "", errCreateUID - }, - ) - }, - err: errCreateUID, - }, - { - name: "should fail if you can't encrypt remotely", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: func(ctx context.Context, uid string, key []byte) (*service.EncryptResponse, error) { - return nil, errRemoteEncrypt - }, - }, - nil, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - err: errRemoteEncrypt, - }, - { - name: "should succeed initializing", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault}, - makeEmptyStore(), - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - }, - } - - for _, tc := range initTestCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - localKEKService, err := tc.localKEKService() - if err == tc.err { - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if localKEKService == nil { - t.Fatalf("unexpected error: %v", err) - } - }) - } - - encryptTestCases := []struct { - name string - localKEKService func() (*LocalKEKService, error) - err error - }{ - { - name: "should fail if local kek is not in cache and remote decrypt fails", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, decReq *service.DecryptRequest) ([]byte, error) { - return nil, errRemoteDecrypt - }, - }, - makeEmptyStore(), - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, nil - }, - }, - genUID, - ) - }, - err: errRemoteDecrypt, - }, - { - name: "should fail if you can't encrypt locally", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return nil, errLocallyEncrypt - }, - }, true - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - err: errLocallyEncrypt, - }, - { - name: "should succeed encrypting with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, true - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - }, - { - name: "should succeed encrypting with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, true - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - }, - { - name: "should succeed encrypting with created transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, decReq *service.DecryptRequest) ([]byte, error) { - return encryptKey, nil - }, - }, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return nil, false - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, nil - }, - }, - genUID, - ) - }, - }, - } - - for _, tc := range encryptTestCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - localKEKService, err := tc.localKEKService() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - encRes, err := localKEKService.Encrypt(context.Background(), "id:999", []byte("message")) - if err == tc.err { - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !bytes.Equal(encryptMessage, encRes.Ciphertext) { - t.Fatalf("unexpected ciphertext - want: %s, have: %s", encryptMessage, encRes.Ciphertext) - } - }) - } - - noAnnotationsDecReq := &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{}, - } - - anonDecReq := &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{ - LocalKEKID: anonKey, - }, - } - - decryptTestCases := []struct { - name string - localKEKService func() (*LocalKEKService, error) - decReq *service.DecryptRequest - err error - }{ - { - name: "should fail decrypting without annotations, if remote decryption doesn't work", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return nil, errRemoteDecrypt - }, - }, - makeEmptyStore(), - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: noAnnotationsDecReq, - err: errRemoteDecrypt, - }, - { - name: "should succeed decrypting without annotations, if remote decryption works", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return decryptMessage, nil - }, - }, - makeEmptyStore(), - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: noAnnotationsDecReq, - }, - { - name: "should fail decrypting, if decrypting with remote KMS doesn't work for unknown key", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return nil, errRemoteDecrypt - }, - }, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { return nil, false }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return nil, false, errNotBeCalled - }, - }, nil - }, - }, - genUID, - ) - }, - decReq: anonDecReq, - err: errRemoteDecrypt, - }, - { - name: "should fail decrypting, if creating a transformer fails with unknown key", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return plainKey, nil - }, - }, - &testStore{ - add: func(key []byte, transformer Transformer) {}, - get: func(key []byte) (Transformer, bool) { - return nil, false - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(ctx context.Context, key []byte) (Transformer, error) { - if bytes.Equal(key, plainKey) { - return nil, errCreateTransformer - } - - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - decReq: &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, + want: &service.EncryptResponse{ + KeyID: "keyID", Annotations: map[string][]byte{ - LocalKEKID: anonKey, - }, - }, - err: errCreateTransformer, - }, - { - name: "should fail decrypting, if decryption fails with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: remoteDecryptShouldNotBeCalled, - }, - &testStore{ - add: func(key []byte, transformer Transformer) {}, - get: func(key []byte) (Transformer, bool) { - if !bytes.Equal(key, storeKey) { - return nil, false - } - - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return nil, false, errLocallyDecrypt - }, - }, true - }, - }, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{ - LocalKEKID: storeKey, - }, - }, - err: errLocallyDecrypt, - }, - { - name: "should succeed decrypting, if decryption works with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: remoteDecryptShouldNotBeCalled, - }, - &testStore{ - add: func(key []byte, transformer Transformer) {}, - get: func(key []byte) (Transformer, bool) { - if !bytes.Equal(key, storeKey) { - return nil, false - } - - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return decryptMessage, false, nil - }, - }, true - }, - }, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{ - LocalKEKID: storeKey, + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), }, }, }, { - name: "should succeed decrypting, with newly created transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return plainKey, nil - }, - }, - makeEmptyStore(), - &createTransformer{ - key: genLocalKey, - transformer: func(ctx context.Context, key []byte) (Transformer, error) { - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return decryptMessage, false, nil - }, - }, nil - }, - }, - genUID, - ) + name: "remote KMS sent 1 annotation", + input: &service.EncryptResponse{ + Ciphertext: []byte("encryptedLocalKEK"), + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + }, + }, + want: &service.EncryptResponse{ + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + }, + }, + }, + { + name: "remote KMS sent 2 annotations", + input: &service.EncryptResponse{ + Ciphertext: []byte("encryptedLocalKEK"), + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + "key-version.encryption.remote.io": []byte("2"), + }, + }, + want: &service.EncryptResponse{ + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + "key-version.encryption.remote.io": []byte("2"), + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + }, }, - decReq: anonDecReq, }, } - for _, tc := range decryptTestCases { + for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - localKEKService, err := tc.localKEKService() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - pt, err := localKEKService.Decrypt(context.Background(), tc.name, tc.decReq) - if err == tc.err { - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !bytes.Equal(pt, decryptMessage) { - t.Fatalf("unexpected plaintext - want: %s, have: %s", decryptMessage, pt) + got := copyResponseAndAddLocalKEKAnnotation(tc.input) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("copyResponseAndAddLocalKEKAnnotation(%v) = %v, want %v", tc.input, got, tc.want) } }) } } -type testStore struct { - add func([]byte, Transformer) - get func([]byte) (Transformer, bool) -} - -var _ Store = (*testStore)(nil) - -func (t *testStore) Add(key []byte, transformer Transformer) { - t.add(key, transformer) -} - -func (t *testStore) Get(key []byte) (Transformer, bool) { - return t.get(key) -} - -type testTransformer struct { - transformFromStorage func(context.Context, []byte, Context) ([]byte, bool, error) - transformToStorage func(context.Context, []byte, Context) ([]byte, error) -} - -var _ Transformer = (*testTransformer)(nil) - -func (t *testTransformer) TransformFromStorage(ctx context.Context, data []byte, context Context) ([]byte, bool, error) { - return t.transformFromStorage(ctx, data, context) -} - -func (t *testTransformer) TransformToStorage(ctx context.Context, data []byte, context Context) ([]byte, error) { - return t.transformToStorage(ctx, data, context) -} - -type createTransformer struct { - transformer func(context.Context, []byte) (Transformer, error) - key func() ([]byte, error) -} - -var _ CreateTransformer = (*createTransformer)(nil) - -func (c *createTransformer) Transformer(ctx context.Context, key []byte) (Transformer, error) { - return c.transformer(ctx, key) -} - -func (c *createTransformer) Key() ([]byte, error) { - return c.key() -} - -type testService struct { - decrypt func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) - encrypt func(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) - status func(ctx context.Context) (*service.StatusResponse, error) -} - -var _ service.Service = (*testService)(nil) - -func (s *testService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return s.decrypt(ctx, uid, req) -} - -func (s *testService) Encrypt(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) { - return s.encrypt(ctx, uid, data) -} - -func (s *testService) Status(ctx context.Context) (*service.StatusResponse, error) { - return s.status(ctx) -} - -func genUID() (string, error) { - return "id:001", nil -} - -func genLocalKey() ([]byte, error) { - return genKey, nil -} - -func makeEmptyTransformer(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil -} - -func makeEmptyStore() *testStore { - return &testStore{ - add: func([]byte, Transformer) {}, - get: func(key []byte) (Transformer, bool) { - return nil, false +func TestAnnotationsWithoutReferenceKeys(t *testing.T) { + testCases := []struct { + name string + input map[string][]byte + want map[string][]byte + }{ + { + name: "annotations is nil", + input: nil, + want: nil, }, + { + name: "annotations is empty", + input: map[string][]byte{}, + want: nil, + }, + { + name: "annotations only contains reference keys", + input: map[string][]byte{ + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + }, + want: nil, + }, + { + name: "annotations contains 1 reference key and 1 other key", + input: map[string][]byte{ + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + "version.encryption.remote.io": []byte("1"), + }, + want: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := annotationsWithoutReferenceKeys(tc.input) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("annotationsWithoutReferenceKeys(%v) = %v, want %v", tc.input, got, tc.want) + } + }) } } -func remoteEncryptDefault(ctx context.Context, uid string, key []byte) (*service.EncryptResponse, error) { +func TestValidateRemoteKMSResponse(t *testing.T) { + testCases := []struct { + name string + input *service.EncryptResponse + want error + }{ + { + name: "annotations is nil", + input: &service.EncryptResponse{}, + want: nil, + }, + { + name: "annotation key contains reference suffix", + input: &service.EncryptResponse{ + Annotations: map[string][]byte{ + "version.reference.encryption.k8s.io": []byte("1"), + }, + }, + want: errInvalidKMSAnnotationKeySuffix, + }, + { + name: "no annotation key contains reference suffix", + input: &service.EncryptResponse{ + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + "key-version.encryption.remote.io": []byte("2"), + }, + }, + want: nil, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := validateRemoteKMSResponse(tc.input) + if got != tc.want { + t.Errorf("validateRemoteKMSResponse(%v) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +var _ service.Service = &testRemoteService{} + +type testRemoteService struct { + mu sync.Mutex + + keyID string + disabled bool +} + +func (s *testRemoteService) Encrypt(ctx context.Context, uid string, plaintext []byte) (*service.EncryptResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.disabled { + return nil, errors.New("failed to encrypt") + } return &service.EncryptResponse{ - Ciphertext: encryptKey, - KeyID: keyID, - Annotations: map[string][]byte{}, + KeyID: s.keyID, + Ciphertext: []byte(base64.StdEncoding.EncodeToString(plaintext)), + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + }, }, nil } -func remoteDecryptShouldNotBeCalled(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return nil, errNotBeCalled +func (s *testRemoteService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.disabled { + return nil, errors.New("failed to decrypt") + } + if len(req.Annotations) != 1 { + return nil, errors.New("invalid annotations") + } + if v, ok := req.Annotations["version.encryption.remote.io"]; !ok || string(v) != "1" { + return nil, errors.New("invalid version in annotations") + } + return base64.StdEncoding.DecodeString(string(req.Ciphertext)) +} + +func (s *testRemoteService) Status(ctx context.Context) (*service.StatusResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.disabled { + return nil, errors.New("failed to get status") + } + return &service.StatusResponse{ + Version: "v2alpha1", + Healthz: "ok", + KeyID: s.keyID, + }, nil +} + +func (s *testRemoteService) SetDisabledStatus(disabled bool) { + s.mu.Lock() + defer s.mu.Unlock() + s.disabled = true +} + +func TestEncrypt(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + validateResponse := func(got *service.EncryptResponse, t *testing.T) { + if len(got.Annotations) != 2 { + t.Fatalf("Encrypt() annotations = %v, want 2 annotations", got.Annotations) + } + if _, ok := got.Annotations[referenceKEKAnnotationKey]; !ok { + t.Fatalf("Encrypt() annotations = %v, want %v", got.Annotations, referenceKEKAnnotationKey) + } + if got.KeyID != remoteKMS.keyID { + t.Fatalf("Encrypt() keyID = %v, want %v", got.KeyID, remoteKMS.keyID) + } + if localKEKService.localTransformer == nil { + t.Fatalf("Encrypt() localTransformer = %v, want non-nil", localKEKService.localTransformer) + } + } + + ctx := testContext(t) + // local KEK is generated and encryption is successful + got, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + validateResponse(got, t) + + // local KEK is used for encryption even when remote KMS is failing + remoteKMS.SetDisabledStatus(true) + if got, err = localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")); err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + validateResponse(got, t) +} + +func TestEncryptError(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + localKEKGenerationPollTimeout = 5 * time.Second + // first time local KEK generation fails because of remote KMS + remoteKMS.SetDisabledStatus(true) + _, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err == nil { + t.Fatalf("Encrypt() error = %v, want non-nil", err) + } + if localKEKService.localTransformer != nil { + t.Fatalf("Encrypt() localTransformer = %v, want nil", localKEKService.localTransformer) + } + + remoteKMS.SetDisabledStatus(false) +} + +func TestDecrypt(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + // local KEK is generated and encryption/decryption is successful + got, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + if string(got.Ciphertext) == "test-plaintext" { + t.Fatalf("Encrypt() ciphertext = %v, want it to be encrypted", got.Ciphertext) + } + decryptRequest := &service.DecryptRequest{ + Ciphertext: got.Ciphertext, + Annotations: got.Annotations, + KeyID: got.KeyID, + } + plaintext, err := localKEKService.Decrypt(ctx, "test-uid", decryptRequest) + if err != nil { + t.Fatalf("Decrypt() error = %v", err) + } + if string(plaintext) != "test-plaintext" { + t.Fatalf("Decrypt() plaintext = %v, want %v", string(plaintext), "test-plaintext") + } + + // local KEK is used for decryption even when remote KMS is failing + remoteKMS.SetDisabledStatus(true) + if _, err = localKEKService.Decrypt(ctx, "test-uid", decryptRequest); err != nil { + t.Fatalf("Decrypt() error = %v", err) + } +} + +func TestDecryptError(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + got, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + decryptRequest := &service.DecryptRequest{ + Ciphertext: got.Ciphertext, + Annotations: got.Annotations, + KeyID: got.KeyID, + } + // local KEK for decryption not in cache and remote KMS is failing + remoteKMS.SetDisabledStatus(true) + // clear the cache + localKEKService.transformers.Clear() + if _, err = localKEKService.Decrypt(ctx, "test-uid", decryptRequest); err == nil { + t.Fatalf("Decrypt() error = %v, want non-nil", err) + } +} + +func TestStatus(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + got, err := localKEKService.Status(ctx) + if err != nil { + t.Fatalf("Status() error = %v", err) + } + if got.Version != "v2alpha1" { + t.Fatalf("Status() version = %v, want %v", got.Version, "v2alpha1") + } + if got.Healthz != "ok" { + t.Fatalf("Status() healthz = %v, want %v", got.Healthz, "ok") + } + if got.KeyID != "test-key-id" { + t.Fatalf("Status() keyID = %v, want %v", got.KeyID, "test-key-id") + } + + // remote KMS is failing + remoteKMS.SetDisabledStatus(true) + if _, err = localKEKService.Status(ctx); err == nil { + t.Fatalf("Status() error = %v, want non-nil", err) + } +} + +func testContext(t *testing.T) context.Context { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + return ctx } diff --git a/staging/src/k8s.io/kms/go.mod b/staging/src/k8s.io/kms/go.mod index ec13d7bb6b5..e4eb9af5401 100644 --- a/staging/src/k8s.io/kms/go.mod +++ b/staging/src/k8s.io/kms/go.mod @@ -9,6 +9,7 @@ require ( google.golang.org/grpc v1.51.0 k8s.io/apimachinery v0.0.0 k8s.io/klog/v2 v2.80.1 + k8s.io/utils v0.0.0-20230209194617-a36077c30491 ) require ( @@ -19,7 +20,6 @@ require ( golang.org/x/text v0.6.0 // indirect google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect google.golang.org/protobuf v1.28.1 // indirect - k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect ) replace ( diff --git a/staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go b/staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go new file mode 100644 index 00000000000..fad47948737 --- /dev/null +++ b/staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go @@ -0,0 +1,85 @@ +/* +Copyright 2017 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. +*/ + +// Vendored from kubernetes/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go +// * commit: 90b42f91fd904b71fd52ca9ae55a5de73e6b779a +// * link: https://github.com/kubernetes/kubernetes/blob/90b42f91fd904b71fd52ca9ae55a5de73e6b779a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go + +// Package aes transforms values for storage at rest using AES-GCM. +package aes + +import ( + "context" + "crypto/cipher" + "crypto/rand" + "fmt" + + "k8s.io/kms/pkg/value" +) + +// gcm 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. +type gcm struct { + block cipher.Block +} + +// NewGCMTransformer takes the given block cipher and performs encryption and decryption on the given +// data. +func NewGCMTransformer(block cipher.Block) value.Transformer { + return &gcm{block: block} +} + +func (t *gcm) TransformFromStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, bool, error) { + aead, err := cipher.NewGCM(t.block) + if err != nil { + return nil, false, err + } + nonceSize := aead.NonceSize() + if len(data) < nonceSize { + return nil, false, fmt.Errorf("the stored data was shorter than the required size") + } + result, err := aead.Open(nil, data[:nonceSize], data[nonceSize:], dataCtx.AuthenticatedData()) + return result, false, err +} + +func (t *gcm) TransformToStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, error) { + aead, err := cipher.NewGCM(t.block) + if err != nil { + return nil, err + } + nonceSize := aead.NonceSize() + result := make([]byte, nonceSize+aead.Overhead()+len(data)) + n, err := rand.Read(result[:nonceSize]) + if err != nil { + return nil, err + } + if n != nonceSize { + return nil, fmt.Errorf("unable to read sufficient random bytes") + } + cipherText := aead.Seal(result[nonceSize:nonceSize], result[:nonceSize], data, dataCtx.AuthenticatedData()) + return result[:nonceSize+len(cipherText)], nil +} diff --git a/staging/src/k8s.io/kms/encryption/interface.go b/staging/src/k8s.io/kms/pkg/value/interface.go similarity index 71% rename from staging/src/k8s.io/kms/encryption/interface.go rename to staging/src/k8s.io/kms/pkg/value/interface.go index 8ad755646a4..d7ad3013fe5 100644 --- a/staging/src/k8s.io/kms/encryption/interface.go +++ b/staging/src/k8s.io/kms/pkg/value/interface.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Kubernetes Authors. +Copyright 2017 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. @@ -14,32 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package encryption +package value import "context" -// Store is a simple interface to store and retrieve Transformer. It is expected -// to be thread-safe. -type Store interface { - // Add adds a transformer to the store with its encrypted key as key. - Add([]byte, Transformer) - // Get returns a transformer from the store by its encrypted key as key. - Get([]byte) (Transformer, bool) -} - -// CreateTransformer enables the creation of a Transformer based on a key. -type CreateTransformer interface { - // Transformer creates a transformer with a given key. - Transformer(context.Context, []byte) (Transformer, error) - // Key creates a key that should match the expectations of Transformer(). - Key() ([]byte, error) -} - -/* -Copied from: - - "k8s.io/apiserver/pkg/storage/value" - - "k8s.io/apiserver/pkg/storage/value/encrypt/aes" -*/ +// Vendored from kubernetes/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go +// * commit: 59e1a32fc8ed35e328a3971d3a1d640ffc28ff55 +// * link: https://github.com/kubernetes/kubernetes/blob/59e1a32fc8ed35e328a3971d3a1d640ffc28ff55/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go // Transformer allows a value to be transformed before being read from or written to the underlying store. The methods // must be able to undo the transformation caused by the other.