kms_transformation_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. // +build !windows
  2. /*
  3. Copyright 2017 The Kubernetes Authors.
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. */
  14. package master
  15. import (
  16. "bytes"
  17. "context"
  18. "crypto/aes"
  19. "encoding/binary"
  20. "fmt"
  21. "strings"
  22. "testing"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/apiserver/pkg/storage/value"
  25. aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
  26. kmsapi "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1"
  27. )
  28. const (
  29. kmsPrefix = "k8s:enc:kms:v1:grpc-kms-provider:"
  30. dekKeySizeLen = 2
  31. kmsConfigYAML = `
  32. kind: EncryptionConfiguration
  33. apiVersion: apiserver.config.k8s.io/v1
  34. resources:
  35. - resources:
  36. - secrets
  37. providers:
  38. - kms:
  39. name: grpc-kms-provider
  40. cachesize: 1000
  41. endpoint: unix:///@kms-provider.sock
  42. `
  43. )
  44. // rawDEKKEKSecret provides operations for working with secrets transformed with Data Encryption Key(DEK) Key Encryption Kye(KEK) envelop.
  45. type rawDEKKEKSecret []byte
  46. func (r rawDEKKEKSecret) getDEKLen() int {
  47. // DEK's length is stored in the two bytes that follow the prefix.
  48. return int(binary.BigEndian.Uint16(r[len(kmsPrefix) : len(kmsPrefix)+dekKeySizeLen]))
  49. }
  50. func (r rawDEKKEKSecret) getDEK() []byte {
  51. return r[len(kmsPrefix)+dekKeySizeLen : len(kmsPrefix)+dekKeySizeLen+r.getDEKLen()]
  52. }
  53. func (r rawDEKKEKSecret) getStartOfPayload() int {
  54. return len(kmsPrefix) + dekKeySizeLen + r.getDEKLen()
  55. }
  56. func (r rawDEKKEKSecret) getPayload() []byte {
  57. return r[r.getStartOfPayload():]
  58. }
  59. // TestKMSProvider is an integration test between KubeAPI, ETCD and KMS Plugin
  60. // Concretely, this test verifies the following integration contracts:
  61. // 1. Raw records in ETCD that were processed by KMS Provider should be prefixed with k8s:enc:kms:v1:grpc-kms-provider-name:
  62. // 2. Data Encryption Key (DEK) should be generated by envelopeTransformer and passed to KMS gRPC Plugin
  63. // 3. KMS gRPC Plugin should encrypt the DEK with a Key Encryption Key (KEK) and pass it back to envelopeTransformer
  64. // 4. The payload (ex. Secret) should be encrypted via AES CBC transform
  65. // 5. Prefix-EncryptedDEK-EncryptedPayload structure should be deposited to ETCD
  66. func TestKMSProvider(t *testing.T) {
  67. pluginMock, err := newBase64Plugin()
  68. if err != nil {
  69. t.Fatalf("failed to create mock of KMS Plugin: %v", err)
  70. }
  71. defer pluginMock.cleanUp()
  72. serveErr := make(chan error, 1)
  73. go func() {
  74. serveErr <- pluginMock.grpcServer.Serve(pluginMock.listener)
  75. }()
  76. test, err := newTransformTest(t, kmsConfigYAML)
  77. if err != nil {
  78. t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s", kmsConfigYAML)
  79. }
  80. defer test.cleanUp()
  81. // As part of newTransformTest a new secret was created, so KMS Mock should have been exercised by this point.
  82. if len(serveErr) != 0 {
  83. t.Fatalf("KMSPlugin failed while serving requests: %v", <-serveErr)
  84. }
  85. secretETCDPath := test.getETCDPath()
  86. var rawSecretAsSeenByETCD rawDEKKEKSecret
  87. rawSecretAsSeenByETCD, err = test.getRawSecretFromETCD()
  88. if err != nil {
  89. t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
  90. }
  91. if !bytes.HasPrefix(rawSecretAsSeenByETCD, []byte(kmsPrefix)) {
  92. t.Fatalf("expected secret to be prefixed with %s, but got %s", kmsPrefix, rawSecretAsSeenByETCD)
  93. }
  94. // Since Data Encryption Key (DEK) is randomly generated (per encryption operation), we need to ask KMS Mock for it.
  95. dekPlainAsSeenByKMS, err := getDEKFromKMSPlugin(pluginMock)
  96. if err != nil {
  97. t.Fatalf("failed to get DEK from KMS: %v", err)
  98. }
  99. decryptResponse, err := pluginMock.Decrypt(context.Background(),
  100. &kmsapi.DecryptRequest{Version: kmsAPIVersion, Cipher: rawSecretAsSeenByETCD.getDEK()})
  101. if err != nil {
  102. t.Fatalf("failed to decrypt DEK, %v", err)
  103. }
  104. dekPlainAsWouldBeSeenByETCD := decryptResponse.Plain
  105. if !bytes.Equal(dekPlainAsSeenByKMS, dekPlainAsWouldBeSeenByETCD) {
  106. t.Fatalf("expected dekPlainAsSeenByKMS %v to be passed to KMS Plugin, but got %s",
  107. dekPlainAsSeenByKMS, dekPlainAsWouldBeSeenByETCD)
  108. }
  109. plainSecret, err := decryptPayload(dekPlainAsWouldBeSeenByETCD, rawSecretAsSeenByETCD, secretETCDPath)
  110. if err != nil {
  111. t.Fatalf("failed to transform from storage via AESCBC, err: %v", err)
  112. }
  113. if !strings.Contains(string(plainSecret), secretVal) {
  114. t.Fatalf("expected %q after decryption, but got %q", secretVal, string(plainSecret))
  115. }
  116. // Secrets should be un-enveloped on direct reads from Kube API Server.
  117. s, err := test.restClient.CoreV1().Secrets(testNamespace).Get(testSecret, metav1.GetOptions{})
  118. if secretVal != string(s.Data[secretKey]) {
  119. t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
  120. }
  121. test.printMetrics()
  122. }
  123. func getDEKFromKMSPlugin(pluginMock *base64Plugin) ([]byte, error) {
  124. // We expect KMS to already have seen an encryptRequest. Hence non-blocking call.
  125. e, ok := <-pluginMock.encryptRequest
  126. if !ok {
  127. return nil, fmt.Errorf("failed to sense encryptRequest from KMS Plugin Mock")
  128. }
  129. return e.Plain, nil
  130. }
  131. func decryptPayload(key []byte, secret rawDEKKEKSecret, secretETCDPath string) ([]byte, error) {
  132. block, err := aes.NewCipher(key)
  133. if err != nil {
  134. return nil, fmt.Errorf("failed to initialize AES Cipher: %v", err)
  135. }
  136. // etcd path of the key is used as the authenticated context - need to pass it to decrypt
  137. ctx := value.DefaultContext([]byte(secretETCDPath))
  138. aescbcTransformer := aestransformer.NewCBCTransformer(block)
  139. plainSecret, _, err := aescbcTransformer.TransformFromStorage(secret.getPayload(), ctx)
  140. if err != nil {
  141. return nil, fmt.Errorf("failed to transform from storage via AESCBC, err: %v", err)
  142. }
  143. return plainSecret, nil
  144. }