mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-19 18:02:01 +00:00
Merge pull request #116031 from ritazh/kmsv2_mockkms
kmsv2: add mock kms for reference implementation
This commit is contained in:
commit
c5cd7a1db5
@ -300,6 +300,10 @@ rules:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- repository: apimachinery
|
- repository: apimachinery
|
||||||
branch: master
|
branch: master
|
||||||
|
- repository: api
|
||||||
|
branch: master
|
||||||
|
- repository: client-go
|
||||||
|
branch: master
|
||||||
- name: release-1.26
|
- name: release-1.26
|
||||||
go: 1.19.6
|
go: 1.19.6
|
||||||
source:
|
source:
|
||||||
|
@ -8,6 +8,7 @@ require (
|
|||||||
github.com/gogo/protobuf v1.3.2
|
github.com/gogo/protobuf v1.3.2
|
||||||
google.golang.org/grpc v1.51.0
|
google.golang.org/grpc v1.51.0
|
||||||
k8s.io/apimachinery v0.0.0
|
k8s.io/apimachinery v0.0.0
|
||||||
|
k8s.io/client-go v0.0.0
|
||||||
k8s.io/klog/v2 v2.80.1
|
k8s.io/klog/v2 v2.80.1
|
||||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491
|
k8s.io/utils v0.0.0-20230209194617-a36077c30491
|
||||||
)
|
)
|
||||||
@ -19,11 +20,14 @@ require (
|
|||||||
golang.org/x/net v0.7.0 // indirect
|
golang.org/x/net v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.5.0 // indirect
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
golang.org/x/text v0.7.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
|
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
k8s.io/api => ../api
|
||||||
k8s.io/apimachinery => ../apimachinery
|
k8s.io/apimachinery => ../apimachinery
|
||||||
|
k8s.io/client-go => ../client-go
|
||||||
k8s.io/kms => ../kms
|
k8s.io/kms => ../kms
|
||||||
)
|
)
|
||||||
|
2
staging/src/k8s.io/kms/go.sum
generated
2
staging/src/k8s.io/kms/go.sum
generated
@ -110,6 +110,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||||
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
96
staging/src/k8s.io/kms/internal/mock_aes_remote_service.go
Normal file
96
staging/src/k8s.io/kms/internal/mock_aes_remote_service.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
aestransformer "k8s.io/kms/pkg/encrypt/aes"
|
||||||
|
"k8s.io/kms/pkg/service"
|
||||||
|
"k8s.io/kms/pkg/value"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ service.Service = &mockAESRemoteService{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
mockAnnotationKey = "version.encryption.remote.io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockAESRemoteService struct {
|
||||||
|
keyID string
|
||||||
|
transformer value.Transformer
|
||||||
|
dataCtx value.DefaultContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockAESRemoteService) Encrypt(ctx context.Context, uid string, plaintext []byte) (*service.EncryptResponse, error) {
|
||||||
|
out, err := s.transformer.TransformToStorage(ctx, plaintext, s.dataCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service.EncryptResponse{
|
||||||
|
KeyID: s.keyID,
|
||||||
|
Ciphertext: out,
|
||||||
|
Annotations: map[string][]byte{
|
||||||
|
mockAnnotationKey: []byte("1"),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockAESRemoteService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) {
|
||||||
|
if len(req.Annotations) != 1 {
|
||||||
|
return nil, errors.New("invalid annotations")
|
||||||
|
}
|
||||||
|
if v, ok := req.Annotations[mockAnnotationKey]; !ok || string(v) != "1" {
|
||||||
|
return nil, errors.New("invalid version in annotations")
|
||||||
|
}
|
||||||
|
if req.KeyID != s.keyID {
|
||||||
|
return nil, errors.New("invalid keyID")
|
||||||
|
}
|
||||||
|
from, _, err := s.transformer.TransformFromStorage(ctx, req.Ciphertext, s.dataCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return from, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockAESRemoteService) Status(ctx context.Context) (*service.StatusResponse, error) {
|
||||||
|
resp := &service.StatusResponse{
|
||||||
|
Version: "v2alpha1",
|
||||||
|
Healthz: "ok",
|
||||||
|
KeyID: s.keyID,
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockAESService creates an instance of mockAESRemoteService.
|
||||||
|
func NewMockAESService(aesKey string, keyID string) (service.Service, error) {
|
||||||
|
block, err := aes.NewCipher([]byte(aesKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(keyID) == 0 {
|
||||||
|
return nil, errors.New("invalid keyID")
|
||||||
|
}
|
||||||
|
return &mockAESRemoteService{
|
||||||
|
transformer: aestransformer.NewGCMTransformer(block),
|
||||||
|
keyID: keyID,
|
||||||
|
dataCtx: value.DefaultContext([]byte{}),
|
||||||
|
}, nil
|
||||||
|
}
|
117
staging/src/k8s.io/kms/internal/mock_aes_remote_service_test.go
Normal file
117
staging/src/k8s.io/kms/internal/mock_aes_remote_service_test.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kms/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
version = "v2alpha1"
|
||||||
|
testAESKey = "abcdefghijklmnop"
|
||||||
|
testKeyID = "test-key-id"
|
||||||
|
testPlaintext = "lorem ipsum dolor sit amet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testContext(t *testing.T) context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMockAESRemoteService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := testContext(t)
|
||||||
|
|
||||||
|
plaintext := []byte(testPlaintext)
|
||||||
|
|
||||||
|
kmsService, err := NewMockAESService(testAESKey, testKeyID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("should be able to encrypt and decrypt", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encRes, err := kmsService.Encrypt(ctx, "", plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(plaintext, encRes.Ciphertext) {
|
||||||
|
t.Fatal("plaintext and ciphertext shouldn't be equal!")
|
||||||
|
}
|
||||||
|
|
||||||
|
decRes, err := kmsService.Decrypt(ctx, "", &service.DecryptRequest{
|
||||||
|
Ciphertext: encRes.Ciphertext,
|
||||||
|
KeyID: encRes.KeyID,
|
||||||
|
Annotations: encRes.Annotations,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(decRes, plaintext) {
|
||||||
|
t.Errorf("want: %q, have: %q", plaintext, decRes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error when decrypt with an invalid keyID", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encRes, err := kmsService.Encrypt(ctx, "", plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(plaintext, encRes.Ciphertext) {
|
||||||
|
t.Fatal("plaintext and ciphertext shouldn't be equal!")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = kmsService.Decrypt(ctx, "", &service.DecryptRequest{
|
||||||
|
Ciphertext: encRes.Ciphertext,
|
||||||
|
KeyID: encRes.KeyID + "1",
|
||||||
|
Annotations: encRes.Annotations,
|
||||||
|
})
|
||||||
|
if err.Error() != "invalid keyID" {
|
||||||
|
t.Errorf("should have returned an invalid keyID error. Got %v, requested keyID: %q, remote service keyID: %q", err, encRes.KeyID+"1", testKeyID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return status data", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
status, err := kmsService.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Healthz != "ok" {
|
||||||
|
t.Errorf("want: %q, have: %q", "ok", status.Healthz)
|
||||||
|
}
|
||||||
|
if len(status.KeyID) == 0 {
|
||||||
|
t.Errorf("want: len(keyID) > 0, have: %d", len(status.KeyID))
|
||||||
|
}
|
||||||
|
if status.Version != version {
|
||||||
|
t.Errorf("want %q, have: %q", version, status.Version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/kms/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockLatencyRemoteService struct {
|
||||||
|
delegate service.Service
|
||||||
|
latency time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ service.Service = &mockLatencyRemoteService{}
|
||||||
|
|
||||||
|
func (s *mockLatencyRemoteService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) {
|
||||||
|
time.Sleep(s.latency)
|
||||||
|
return s.delegate.Decrypt(ctx, uid, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockLatencyRemoteService) Encrypt(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) {
|
||||||
|
time.Sleep(s.latency)
|
||||||
|
return s.delegate.Encrypt(ctx, uid, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockLatencyRemoteService) Status(ctx context.Context) (*service.StatusResponse, error) {
|
||||||
|
// Passthrough here, not adding any delays for status as delays are usually negligible compare to encrypt and decrypt requests.
|
||||||
|
return s.delegate.Status(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockLatencyService creates an instance of mockLatencyRemoteService.
|
||||||
|
func NewMockLatencyService(delegate service.Service, latency time.Duration) service.Service {
|
||||||
|
return &mockLatencyRemoteService{
|
||||||
|
delegate: delegate,
|
||||||
|
latency: latency,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/kms/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testLatencyInMillisecond = 100 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMockLatencyRemoteService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := testContext(t)
|
||||||
|
|
||||||
|
plaintext := []byte(testPlaintext)
|
||||||
|
aesService, err := NewMockAESService(testAESKey, testKeyID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
kmsService := NewMockLatencyService(aesService, testLatencyInMillisecond)
|
||||||
|
|
||||||
|
t.Run("should be able to encrypt and decrypt with some known latency", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
start := time.Now()
|
||||||
|
encRes, err := kmsService.Encrypt(ctx, "", plaintext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if bytes.Equal(plaintext, encRes.Ciphertext) {
|
||||||
|
t.Fatal("plaintext and ciphertext shouldn't be equal!")
|
||||||
|
}
|
||||||
|
// Max is set to 3s to limit the risk of a CPU limited CI node taking a long time to do encryption.
|
||||||
|
if duration < testLatencyInMillisecond || duration > 3*time.Second {
|
||||||
|
t.Errorf("duration for encrypt should be around: %q, have: %q", testLatencyInMillisecond, duration)
|
||||||
|
}
|
||||||
|
start = time.Now()
|
||||||
|
decRes, err := kmsService.Decrypt(ctx, "", &service.DecryptRequest{
|
||||||
|
Ciphertext: encRes.Ciphertext,
|
||||||
|
KeyID: encRes.KeyID,
|
||||||
|
Annotations: encRes.Annotations,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
duration = time.Since(start)
|
||||||
|
|
||||||
|
if !bytes.Equal(decRes, plaintext) {
|
||||||
|
t.Errorf("want: %q, have: %q", plaintext, decRes)
|
||||||
|
}
|
||||||
|
if duration < testLatencyInMillisecond || duration > 3*time.Second {
|
||||||
|
t.Errorf("duration decrypt should be around: %q, have: %q", testLatencyInMillisecond, duration)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return status data", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
start := time.Now()
|
||||||
|
status, err := kmsService.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
duration := time.Since(start)
|
||||||
|
if status.Healthz != "ok" {
|
||||||
|
t.Errorf("want: %q, have: %q", "ok", status.Healthz)
|
||||||
|
}
|
||||||
|
if len(status.KeyID) == 0 {
|
||||||
|
t.Errorf("want: len(keyID) > 0, have: %d", len(status.KeyID))
|
||||||
|
}
|
||||||
|
if status.Version != version {
|
||||||
|
t.Errorf("want %q, have: %q", version, status.Version)
|
||||||
|
}
|
||||||
|
if duration > 3*time.Second {
|
||||||
|
t.Errorf("duration status should be less than: 3s, have: %q", duration)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"k8s.io/client-go/util/flowcontrol"
|
||||||
|
"k8s.io/kms/pkg/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockRateLimitRemoteService struct {
|
||||||
|
delegate service.Service
|
||||||
|
limiter flowcontrol.RateLimiter
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ service.Service = &mockRateLimitRemoteService{}
|
||||||
|
|
||||||
|
func (s *mockRateLimitRemoteService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) {
|
||||||
|
if !s.limiter.TryAccept() {
|
||||||
|
return nil, status.New(codes.ResourceExhausted, "remote decrypt rate limit exceeded").Err()
|
||||||
|
}
|
||||||
|
return s.delegate.Decrypt(ctx, uid, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockRateLimitRemoteService) Encrypt(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) {
|
||||||
|
if !s.limiter.TryAccept() {
|
||||||
|
return nil, status.New(codes.ResourceExhausted, "remote encrypt rate limit exceeded").Err()
|
||||||
|
}
|
||||||
|
return s.delegate.Encrypt(ctx, uid, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockRateLimitRemoteService) Status(ctx context.Context) (*service.StatusResponse, error) {
|
||||||
|
// Passthrough here, not adding any rate limiting for status as rate limits are usually for encrypt and decrypt requests.
|
||||||
|
return s.delegate.Status(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockRateLimitService creates an instance of mockRateLimitRemoteService.
|
||||||
|
func NewMockRateLimitService(delegate service.Service, qps float32, burst int) service.Service {
|
||||||
|
return &mockRateLimitRemoteService{
|
||||||
|
delegate: delegate,
|
||||||
|
limiter: flowcontrol.NewTokenBucketRateLimiter(qps, burst),
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testQPS = 1
|
||||||
|
// testBurst should be no more than 9 since 9*100millisecond (test latency) = 900ms, which guarantees there is enough bursts per second.
|
||||||
|
testBurst = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMockRateLimitRemoteService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := testContext(t)
|
||||||
|
plaintext := []byte(testPlaintext)
|
||||||
|
aesService, err := NewMockAESService(testAESKey, testKeyID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mockLatencyService := NewMockLatencyService(aesService, testLatencyInMillisecond)
|
||||||
|
kmsService := NewMockRateLimitService(mockLatencyService, testQPS, testBurst)
|
||||||
|
|
||||||
|
t.Run("should hit rate limit", func(t *testing.T) {
|
||||||
|
rateLimitExceeded := false
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
encRes, err := kmsService.Encrypt(ctx, "", plaintext)
|
||||||
|
if i >= testBurst {
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "rpc error: code = ResourceExhausted desc = remote encrypt rate limit exceeded" {
|
||||||
|
t.Fatalf("should have failed with rate limit exceeded %d, have err: %v", testBurst, err)
|
||||||
|
}
|
||||||
|
rateLimitExceeded = true
|
||||||
|
} else {
|
||||||
|
if bytes.Equal(plaintext, encRes.Ciphertext) {
|
||||||
|
t.Fatal("plaintext and ciphertext shouldn't be equal!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v, i: %d", err, i)
|
||||||
|
}
|
||||||
|
if bytes.Equal(plaintext, encRes.Ciphertext) {
|
||||||
|
t.Fatal("plaintext and ciphertext shouldn't be equal!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// status should not hit any rate limit
|
||||||
|
_, err = kmsService.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !rateLimitExceeded {
|
||||||
|
t.Errorf("should have reached the rate limit of %d", testBurst)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -33,6 +33,11 @@ import (
|
|||||||
testingclock "k8s.io/utils/clock/testing"
|
testingclock "k8s.io/utils/clock/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testAnnotationKey = "version.encryption.remote.io"
|
||||||
|
testAnnotationKeyVersion = "key-version.encryption.remote.io"
|
||||||
|
)
|
||||||
|
|
||||||
func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) {
|
func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@ -60,14 +65,14 @@ func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) {
|
|||||||
Ciphertext: []byte("encryptedLocalKEK"),
|
Ciphertext: []byte("encryptedLocalKEK"),
|
||||||
KeyID: "keyID",
|
KeyID: "keyID",
|
||||||
Annotations: map[string][]byte{
|
Annotations: map[string][]byte{
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &service.EncryptResponse{
|
want: &service.EncryptResponse{
|
||||||
KeyID: "keyID",
|
KeyID: "keyID",
|
||||||
Annotations: map[string][]byte{
|
Annotations: map[string][]byte{
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
referenceKEKAnnotationKey: []byte("encryptedLocalKEK"),
|
referenceKEKAnnotationKey: []byte("encryptedLocalKEK"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -77,16 +82,16 @@ func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) {
|
|||||||
Ciphertext: []byte("encryptedLocalKEK"),
|
Ciphertext: []byte("encryptedLocalKEK"),
|
||||||
KeyID: "keyID",
|
KeyID: "keyID",
|
||||||
Annotations: map[string][]byte{
|
Annotations: map[string][]byte{
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
"key-version.encryption.remote.io": []byte("2"),
|
testAnnotationKeyVersion: []byte("2"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &service.EncryptResponse{
|
want: &service.EncryptResponse{
|
||||||
KeyID: "keyID",
|
KeyID: "keyID",
|
||||||
Annotations: map[string][]byte{
|
Annotations: map[string][]byte{
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
"key-version.encryption.remote.io": []byte("2"),
|
testAnnotationKeyVersion: []byte("2"),
|
||||||
referenceKEKAnnotationKey: []byte("encryptedLocalKEK"),
|
referenceKEKAnnotationKey: []byte("encryptedLocalKEK"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -131,11 +136,11 @@ func TestAnnotationsWithoutReferenceKeys(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "annotations contains 1 reference key and 1 other key",
|
name: "annotations contains 1 reference key and 1 other key",
|
||||||
input: map[string][]byte{
|
input: map[string][]byte{
|
||||||
referenceKEKAnnotationKey: []byte("encryptedLocalKEK"),
|
referenceKEKAnnotationKey: []byte("encryptedLocalKEK"),
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
},
|
},
|
||||||
want: map[string][]byte{
|
want: map[string][]byte{
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -177,8 +182,8 @@ func TestValidateRemoteKMSEncryptResponse(t *testing.T) {
|
|||||||
name: "no annotation key contains reference suffix",
|
name: "no annotation key contains reference suffix",
|
||||||
input: &service.EncryptResponse{
|
input: &service.EncryptResponse{
|
||||||
Annotations: map[string][]byte{
|
Annotations: map[string][]byte{
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
"key-version.encryption.remote.io": []byte("2"),
|
testAnnotationKeyVersion: []byte("2"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: nil,
|
want: nil,
|
||||||
@ -264,7 +269,7 @@ func (s *testRemoteService) Encrypt(ctx context.Context, uid string, plaintext [
|
|||||||
KeyID: s.keyID,
|
KeyID: s.keyID,
|
||||||
Ciphertext: []byte(base64.StdEncoding.EncodeToString(plaintext)),
|
Ciphertext: []byte(base64.StdEncoding.EncodeToString(plaintext)),
|
||||||
Annotations: map[string][]byte{
|
Annotations: map[string][]byte{
|
||||||
"version.encryption.remote.io": []byte("1"),
|
testAnnotationKey: []byte("1"),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -280,7 +285,7 @@ func (s *testRemoteService) Decrypt(ctx context.Context, uid string, req *servic
|
|||||||
if len(req.Annotations) != 1 {
|
if len(req.Annotations) != 1 {
|
||||||
return nil, errors.New("invalid annotations")
|
return nil, errors.New("invalid annotations")
|
||||||
}
|
}
|
||||||
if v, ok := req.Annotations["version.encryption.remote.io"]; !ok || string(v) != "1" {
|
if v, ok := req.Annotations[testAnnotationKey]; !ok || string(v) != "1" {
|
||||||
return nil, errors.New("invalid version in annotations")
|
return nil, errors.New("invalid version in annotations")
|
||||||
}
|
}
|
||||||
return base64.StdEncoding.DecodeString(string(req.Ciphertext))
|
return base64.StdEncoding.DecodeString(string(req.Ciphertext))
|
||||||
|
Loading…
Reference in New Issue
Block a user