mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-20 02:11:09 +00:00
Merge pull request #115677 from aramase/kmsv2-middleware
[KMSv2] implement local KEK service
This commit is contained in:
commit
d0db9a959b
262
staging/src/k8s.io/kms/encryption/service.go
Normal file
262
staging/src/k8s.io/kms/encryption/service.go
Normal file
@ -0,0 +1,262 @@
|
||||
/*
|
||||
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"
|
||||
"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 (
|
||||
// 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 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 and decrypt the local KEKs.
|
||||
remoteKMS service.Service
|
||||
remoteOnce sync.Once
|
||||
|
||||
// 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 remote KMS service.
|
||||
// In the current implementation, the localKEK Service needs to be
|
||||
// restarted by the caller after security thresholds are met.
|
||||
// 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{
|
||||
remoteKMS: remoteService,
|
||||
transformers: lru.New(cacheSize),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
return m.localTransformer, m.remoteKMSResponse, m.localTransformerErr
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
return &service.EncryptResponse{
|
||||
// Ciphertext is not set on purpose - it is different per Encrypt call
|
||||
KeyID: resp.KeyID,
|
||||
Annotations: annotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts the plaintext with the localKEK.
|
||||
func (m *LocalKEKService) Encrypt(ctx context.Context, uid string, pt []byte) (*service.EncryptResponse, error) {
|
||||
transformer, resp, err := m.getTransformerForEncryption(uid)
|
||||
if err != nil {
|
||||
klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ct, err := transformer.TransformToStorage(ctx, pt, emptyContext)
|
||||
if err != nil {
|
||||
klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &service.EncryptResponse{
|
||||
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) {
|
||||
if _, ok := req.Annotations[referenceKEKAnnotationKey]; !ok {
|
||||
return nil, fmt.Errorf("unable to find local KEK for request with uid %q", uid)
|
||||
}
|
||||
|
||||
transformer, err := m.getTransformerForDecryption(ctx, uid, req)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
394
staging/src/k8s.io/kms/encryption/service_test.go
Normal file
394
staging/src/k8s.io/kms/encryption/service_test.go
Normal file
@ -0,0 +1,394 @@
|
||||
/*
|
||||
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"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/kms/service"
|
||||
)
|
||||
|
||||
func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input *service.EncryptResponse
|
||||
want *service.EncryptResponse
|
||||
}{
|
||||
{
|
||||
name: "annotations is nil",
|
||||
input: &service.EncryptResponse{
|
||||
Ciphertext: []byte("encryptedLocalKEK"),
|
||||
KeyID: "keyID",
|
||||
Annotations: nil,
|
||||
},
|
||||
want: &service.EncryptResponse{
|
||||
KeyID: "keyID",
|
||||
Annotations: map[string][]byte{
|
||||
referenceKEKAnnotationKey: []byte("encryptedLocalKEK"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := copyResponseAndAddLocalKEKAnnotation(tc.input)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("copyResponseAndAddLocalKEKAnnotation(%v) = %v, want %v", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 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{
|
||||
KeyID: s.keyID,
|
||||
Ciphertext: []byte(base64.StdEncoding.EncodeToString(plaintext)),
|
||||
Annotations: map[string][]byte{
|
||||
"version.encryption.remote.io": []byte("1"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -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 (
|
||||
|
85
staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go
Normal file
85
staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go
Normal file
@ -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
|
||||
}
|
49
staging/src/k8s.io/kms/pkg/value/interface.go
Normal file
49
staging/src/k8s.io/kms/pkg/value/interface.go
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package value
|
||||
|
||||
import "context"
|
||||
|
||||
// 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.
|
||||
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 }
|
Loading…
Reference in New Issue
Block a user