mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-06 02:34:03 +00:00
Merge pull request #120896 from aramase/aramase/f/kmsv2_pkcs11
[KMSv2] pkcs11 reference implementation using SoftHSM
This commit is contained in:
commit
46728a8fa2
@ -26,7 +26,7 @@ source "${KUBE_ROOT}/hack/lib/init.sh"
|
||||
MODULES=(
|
||||
hack/tools
|
||||
staging/src/k8s.io/code-generator/examples
|
||||
staging/src/k8s.io/kms/internal/plugins/mock
|
||||
staging/src/k8s.io/kms/internal/plugins/_mock
|
||||
)
|
||||
|
||||
# Explicitly opt into go modules, even though we're inside a GOPATH directory
|
||||
|
@ -301,12 +301,9 @@
|
||||
|
||||
- baseImportPath: "./vendor/k8s.io/kms/"
|
||||
allowedImports:
|
||||
- k8s.io/api
|
||||
- k8s.io/apimachinery
|
||||
- k8s.io/client-go
|
||||
- k8s.io/klog
|
||||
- k8s.io/kms
|
||||
- k8s.io/utils
|
||||
|
||||
- baseImportPath: "./vendor/k8s.io/endpointslice/"
|
||||
allowedImports:
|
||||
|
@ -296,10 +296,6 @@ rules:
|
||||
dependencies:
|
||||
- repository: apimachinery
|
||||
branch: master
|
||||
- repository: api
|
||||
branch: master
|
||||
- repository: client-go
|
||||
branch: master
|
||||
source:
|
||||
branch: master
|
||||
dir: staging/src/k8s.io/kms
|
||||
|
@ -8,7 +8,6 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
google.golang.org/grpc v1.54.0
|
||||
k8s.io/apimachinery v0.0.0
|
||||
k8s.io/client-go v0.0.0
|
||||
k8s.io/klog/v2 v2.100.1
|
||||
)
|
||||
|
||||
@ -18,15 +17,12 @@ require (
|
||||
golang.org/x/net v0.13.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
k8s.io/api => ../api
|
||||
k8s.io/apimachinery => ../apimachinery
|
||||
k8s.io/client-go => ../client-go
|
||||
k8s.io/kms => ../kms
|
||||
)
|
||||
|
12
staging/src/k8s.io/kms/go.sum
generated
12
staging/src/k8s.io/kms/go.sum
generated
@ -6,7 +6,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
@ -20,11 +19,9 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
@ -32,9 +29,6 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@ -44,11 +38,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
@ -69,7 +60,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -83,7 +74,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
@ -1,96 +0,0 @@
|
||||
/*
|
||||
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: "v2beta1",
|
||||
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
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
/*
|
||||
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 = "v2beta1"
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
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,
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
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),
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
@ -18,19 +18,18 @@ WORKDIR /workspace
|
||||
|
||||
# Copy the source
|
||||
COPY apimachinery/ apimachinery/
|
||||
COPY client-go/ client-go/
|
||||
COPY kms/ kms/
|
||||
WORKDIR /workspace/kms/internal/plugins/mock
|
||||
WORKDIR /workspace/kms/internal/plugins/_mock
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o mock-kms-plugin plugin.go
|
||||
RUN chmod +x mock-kms-plugin
|
||||
RUN CGO_ENABLED=1 GOOS=linux GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o mock-kms-plugin plugin.go
|
||||
|
||||
# Use distroless as minimal base image to package the manager binary
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} gcr.io/distroless/static:nonroot
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/kms/internal/plugins/mock/mock-kms-plugin .
|
||||
FROM alpine:latest
|
||||
|
||||
ENTRYPOINT [ "/mock-kms-plugin" ]
|
||||
RUN apk add --update --no-cache ca-certificates gcompat
|
||||
RUN apk add --no-cache softhsm
|
||||
|
||||
COPY --from=builder /workspace/kms/internal/plugins/_mock/mock-kms-plugin /usr/local/bin/mock-kms-plugin
|
||||
|
||||
ENTRYPOINT [ "mock-kms-plugin" ]
|
5
staging/src/k8s.io/kms/internal/plugins/_mock/README.md
Normal file
5
staging/src/k8s.io/kms/internal/plugins/_mock/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Mock KMS Plugin
|
||||
|
||||
This is a mock KMS plugin for testing purposes. It implements the KMS plugin using PKCS#11 interface backed by [SoftHSM](https://www.opendnssec.org/softhsm/). It is intended to be used for testing only and not for production use.
|
||||
|
||||
The directory is named `_mock` so that it is ignored by the `go mod` tooling in the root directory.
|
@ -3,6 +3,7 @@ module k8s.io/kms/plugins/mock
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5
|
||||
k8s.io/klog/v2 v2.100.1
|
||||
k8s.io/kms v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
@ -11,15 +12,15 @@ require (
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/thales-e-security/pool v0.0.2 // indirect
|
||||
golang.org/x/net v0.13.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/grpc v1.54.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
k8s.io/client-go v0.0.0 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
)
|
||||
|
||||
replace (
|
@ -1,3 +1,7 @@
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E=
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@ -10,6 +14,17 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ=
|
||||
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
|
||||
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@ -35,8 +50,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@ -56,4 +69,3 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
|
||||
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
69
staging/src/k8s.io/kms/internal/plugins/_mock/kms.yaml
Normal file
69
staging/src/k8s.io/kms/internal/plugins/_mock/kms.yaml
Normal file
@ -0,0 +1,69 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: mock-kmsv2-provider
|
||||
namespace: kube-system
|
||||
labels:
|
||||
tier: control-plane
|
||||
component: mock-kmsv2-provider
|
||||
spec:
|
||||
# hostNetwork: true is required because the plugin is run as a static pod
|
||||
# on the control plane node and needs to run before the CNI plugins are initialized.
|
||||
hostNetwork: true
|
||||
initContainers:
|
||||
- args:
|
||||
- |
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# if token exists, skip initialization
|
||||
if [ $(ls -1 /var/lib/softhsm/tokens | wc -l) -ge 1 ]; then
|
||||
echo "Skipping initialization of softhsm"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p /var/lib/softhsm/tokens
|
||||
apk add --update --no-cache ca-certificates jq
|
||||
apk add --no-cache ccid opensc softhsm
|
||||
|
||||
TOKEN_LABEL=$(jq -r '.tokenLabel' /etc/softhsm-config.json)
|
||||
PIN=$(jq -r '.pin' /etc/softhsm-config.json)
|
||||
MODULE_PATH=$(jq -r '.path' /etc/softhsm-config.json)
|
||||
|
||||
softhsm2-util --init-token --free --label $TOKEN_LABEL --pin $PIN --so-pin $PIN
|
||||
pkcs11-tool --module $MODULE_PATH --keygen --key-type aes:32 --pin $PIN --token-label $TOKEN_LABEL --label kms-test
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
image: alpine:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: init-mock-kmsv2-provider
|
||||
volumeMounts:
|
||||
- mountPath: /var/lib/softhsm/tokens
|
||||
name: softhsm-tokens
|
||||
- mountPath: /etc/softhsm-config.json
|
||||
name: softhsm-config
|
||||
containers:
|
||||
- name: mock-kmsv2-provider
|
||||
image: localhost:5000/mock-kms-provider:e2e
|
||||
imagePullPolicy: IfNotPresent
|
||||
volumeMounts:
|
||||
- name: sock
|
||||
mountPath: /tmp
|
||||
- name: softhsm-config
|
||||
mountPath: /etc/softhsm-config.json
|
||||
- name: softhsm-tokens
|
||||
mountPath: /var/lib/softhsm/tokens
|
||||
volumes:
|
||||
- name: sock
|
||||
hostPath:
|
||||
path: /tmp
|
||||
- name: softhsm-config
|
||||
hostPath:
|
||||
path: /etc/softhsm-config.json
|
||||
type: File
|
||||
- name: softhsm-tokens
|
||||
hostPath:
|
||||
path: /var/lib/softhsm/tokens
|
||||
type: DirectoryOrCreate
|
118
staging/src/k8s.io/kms/internal/plugins/_mock/pkcs11/pkcs11.go
Normal file
118
staging/src/k8s.io/kms/internal/plugins/_mock/pkcs11/pkcs11.go
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
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 pkcs11
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
crypot11 "github.com/ThalesIgnite/crypto11"
|
||||
|
||||
"k8s.io/kms/pkg/service"
|
||||
)
|
||||
|
||||
const (
|
||||
mockAnnotationKey = "version.encryption.remote.io"
|
||||
)
|
||||
|
||||
var _ service.Service = &pkcs11RemoteService{}
|
||||
|
||||
type pkcs11RemoteService struct {
|
||||
keyID string
|
||||
aead cipher.AEAD
|
||||
}
|
||||
|
||||
// NewPKCS11RemoteService creates a new PKCS11 remote service with SoftHSMv2 configuration file and keyID
|
||||
func NewPKCS11RemoteService(configFilePath, keyID string) (service.Service, error) {
|
||||
ctx, err := crypot11.ConfigureFromFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(keyID) == 0 {
|
||||
return nil, fmt.Errorf("invalid keyID")
|
||||
}
|
||||
|
||||
remoteService := &pkcs11RemoteService{
|
||||
keyID: keyID,
|
||||
}
|
||||
|
||||
key, err := ctx.FindKey(nil, []byte(keyID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key == nil {
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
if remoteService.aead, err = key.NewGCM(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return remoteService, nil
|
||||
}
|
||||
|
||||
func (s *pkcs11RemoteService) Encrypt(ctx context.Context, uid string, plaintext []byte) (*service.EncryptResponse, error) {
|
||||
nonceSize := s.aead.NonceSize()
|
||||
result := make([]byte, nonceSize+s.aead.Overhead()+len(plaintext))
|
||||
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 := s.aead.Seal(result[nonceSize:nonceSize], result[:nonceSize], plaintext, []byte(s.keyID))
|
||||
|
||||
return &service.EncryptResponse{
|
||||
Ciphertext: result[:nonceSize+len(cipherText)],
|
||||
KeyID: s.keyID,
|
||||
Annotations: map[string][]byte{
|
||||
mockAnnotationKey: []byte("1"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *pkcs11RemoteService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) {
|
||||
if len(req.Annotations) != 1 {
|
||||
return nil, fmt.Errorf("invalid annotations")
|
||||
}
|
||||
if v, ok := req.Annotations[mockAnnotationKey]; !ok || string(v) != "1" {
|
||||
return nil, fmt.Errorf("invalid version in annotations")
|
||||
}
|
||||
|
||||
if req.KeyID != s.keyID {
|
||||
return nil, fmt.Errorf("invalid keyID")
|
||||
}
|
||||
|
||||
nonceSize := s.aead.NonceSize()
|
||||
data := req.Ciphertext
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("the stored data was shorter than the required size")
|
||||
}
|
||||
|
||||
return s.aead.Open(nil, data[:nonceSize], data[nonceSize:], []byte(s.keyID))
|
||||
}
|
||||
|
||||
func (s *pkcs11RemoteService) Status(ctx context.Context) (*service.StatusResponse, error) {
|
||||
return &service.StatusResponse{
|
||||
Version: "v2beta1",
|
||||
Healthz: "ok",
|
||||
KeyID: s.keyID,
|
||||
}, nil
|
||||
}
|
@ -25,14 +25,15 @@ import (
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kms/internal"
|
||||
"k8s.io/kms/pkg/service"
|
||||
"k8s.io/kms/pkg/util"
|
||||
"k8s.io/kms/plugins/mock/pkcs11"
|
||||
)
|
||||
|
||||
var (
|
||||
listenAddr = flag.String("listen-addr", "unix:///tmp/kms.socket", "gRPC listen address")
|
||||
timeout = flag.Duration("timeout", 5*time.Second, "gRPC timeout")
|
||||
listenAddr = flag.String("listen-addr", "unix:///tmp/kms.socket", "gRPC listen address")
|
||||
timeout = flag.Duration("timeout", 5*time.Second, "gRPC timeout")
|
||||
configFilePath = flag.String("config-file-path", "/etc/softhsm-config.json", "SoftHSM config file path")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -44,7 +45,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
remoteKMSService, err := internal.NewMockAESService("somerandomstring", "aes-key-id")
|
||||
remoteKMSService, err := pkcs11.NewPKCS11RemoteService(*configFilePath, "kms-test")
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to create remote service")
|
||||
os.Exit(1)
|
@ -1,21 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: mock-kmsv2-provider
|
||||
namespace: kube-system
|
||||
labels:
|
||||
tier: control-plane
|
||||
component: mock-kmsv2-provider
|
||||
spec:
|
||||
hostNetwork: true
|
||||
containers:
|
||||
- name: mock-kmsv2-provider
|
||||
image: localhost:5000/mock-kms-provider:e2e
|
||||
imagePullPolicy: IfNotPresent
|
||||
volumeMounts:
|
||||
- name: sock
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: sock
|
||||
hostPath:
|
||||
path: /tmp
|
@ -1,85 +0,0 @@
|
||||
/*
|
||||
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
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
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 }
|
@ -1,6 +1,6 @@
|
||||
# Encryption at rest testing manifests
|
||||
|
||||
This directory contains manifests for testing encryption at rest with a [mock KMS provider](../../../../../staging/src/k8s.io/kms/internal/plugins/mock). The mock KMS provider is a fake KMS provider that does not communicate with any external KMS. It is used for testing purposes only.
|
||||
This directory contains manifests for testing encryption at rest with a [mock KMS provider](../../../../../staging/src/k8s.io/kms/internal/plugins/_mock). The mock KMS provider is a fake KMS provider that does not communicate with any external KMS. It is used for testing purposes only.
|
||||
|
||||
## run-e2e.sh
|
||||
|
||||
|
@ -13,7 +13,11 @@ nodes:
|
||||
readOnly: true
|
||||
propagation: None
|
||||
- containerPath: /etc/kubernetes/manifests/kubernetes-kms.yaml
|
||||
hostPath: staging/src/k8s.io/kms/internal/plugins/mock/kms.yaml
|
||||
hostPath: staging/src/k8s.io/kms/internal/plugins/_mock/kms.yaml
|
||||
readOnly: true
|
||||
propagation: None
|
||||
- containerPath: /etc/softhsm-config.json
|
||||
hostPath: test/e2e/testing-manifests/auth/encrypt/softhsm-config.json
|
||||
readOnly: true
|
||||
propagation: None
|
||||
kubeadmConfigPatches:
|
||||
|
@ -36,7 +36,7 @@ build_and_push_mock_plugin() {
|
||||
--platform linux/amd64 \
|
||||
--output=type=docker \
|
||||
-t localhost:5000/mock-kms-provider:e2e \
|
||||
-f staging/src/k8s.io/kms/internal/plugins/mock/Dockerfile staging/src/k8s.io/ \
|
||||
-f staging/src/k8s.io/kms/internal/plugins/_mock/Dockerfile staging/src/k8s.io/ \
|
||||
--progress=plain;
|
||||
|
||||
docker push localhost:5000/mock-kms-provider:e2e
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"path": "/usr/lib/softhsm/libsofthsm2.so",
|
||||
"tokenLabel": "kms-test",
|
||||
"pin": "kms-test"
|
||||
}
|
Loading…
Reference in New Issue
Block a user