Skip to content

Commit e12bd43

Browse files
astreterVladimir Kuznichenkov
authored andcommitted
Implement tests for YC KMS provider and credentials forward
gRPC calls `Encrypt` and `Decrypt` are mocked with dummy responses using base64 instead of actual encryption. Since YC KMS responce with bynary data we are storing it as base64, together with mocked server where we encode it one more time instead of actual encryption we are using double encoding and double decoding in tests cases.
1 parent f320eeb commit e12bd43

File tree

2 files changed

+236
-19
lines changed

2 files changed

+236
-19
lines changed

yckms/keysource.go

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/yandex-cloud/go-sdk/gen/kmscrypto"
1111
"github.com/yandex-cloud/go-sdk/iamkey"
1212
"go.mozilla.org/sops/v3/logging"
13+
"google.golang.org/grpc"
1314
"os"
1415
"strings"
1516
"time"
@@ -39,13 +40,20 @@ type MasterKey struct {
3940
// CreationDate is the creation timestamp of the MasterKey. Used
4041
// for NeedsRotation.
4142
CreationDate time.Time
43+
44+
credentials ycsdk.Credentials
45+
46+
// grpcConn can be used to inject a custom YC KMS client connection.
47+
// Mostly useful for testing at present, to wire the client to a mock
48+
// server.
49+
grpcConn *grpc.ClientConn
4250
}
4351

4452
func NewMasterKeyFromKeyID(keyID string) *MasterKey {
45-
k := &MasterKey{}
46-
k.KeyID = keyID
47-
k.CreationDate = time.Now().UTC()
48-
return k
53+
return &MasterKey{
54+
KeyID: keyID,
55+
CreationDate: time.Now().UTC(),
56+
}
4957
}
5058

5159
func NewMasterKeyFromKeyIDString(keyID string) []*MasterKey {
@@ -54,18 +62,34 @@ func NewMasterKeyFromKeyIDString(keyID string) []*MasterKey {
5462
return keys
5563
}
5664
for _, s := range strings.Split(keyID, ",") {
57-
keys = append(keys, NewMasterKeyFromKeyID(s))
65+
keys = append(keys, NewMasterKeyFromKeyID(strings.TrimSpace(s)))
5866
}
5967
return keys
6068
}
6169

62-
func (key *MasterKey) Encrypt(dataKey []byte) error {
63-
client, ctx, err := key.newKMSClient()
70+
// YCCredentials is a ycsdk.Credentials used for authenticating towards YC KMS
71+
type YCCredentials struct {
72+
credentials ycsdk.Credentials
73+
}
74+
75+
// NewYCCredentials creates a new YCCredentials with the provided ycsdk.Credentials.
76+
func NewYCCredentials(credentials ycsdk.Credentials) *YCCredentials {
77+
return &YCCredentials{credentials: credentials}
78+
}
79+
80+
// ApplyToMasterKey configures the TokenCredential on the provided key.
81+
func (c YCCredentials) ApplyToMasterKey(key *MasterKey) {
82+
key.credentials = c.credentials
83+
}
84+
85+
func (key *MasterKey) Encrypt(dataKey []byte) (err error) {
86+
client, err := key.newKMSClient()
6487
if err != nil {
6588
log.WithError(err).WithField("keyID", key.KeyID).Error("Encryption failed")
6689
return fmt.Errorf("cannot create YC KMS service: %w", err)
6790
}
68-
ciphertextResponse, err := client.Encrypt(ctx, &yckms.SymmetricEncryptRequest{
91+
92+
ciphertextResponse, err := client.Encrypt(context.Background(), &yckms.SymmetricEncryptRequest{
6993
KeyId: key.KeyID,
7094
Plaintext: dataKey,
7195
})
@@ -100,13 +124,14 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
100124
// Decrypt decrypts the EncryptedKey field with YC KMS and returns
101125
// the result.
102126
func (key *MasterKey) Decrypt() ([]byte, error) {
103-
client, ctx, err := key.newKMSClient()
127+
client, err := key.newKMSClient()
104128
if err != nil {
105129
log.WithError(err).WithField("keyID", key.KeyID).Error("Decryption failed")
106130
return nil, fmt.Errorf("cannot create YC KMS service: %w", err)
107131
}
132+
108133
decodedCipher, err := base64.StdEncoding.DecodeString(string(key.EncryptedDataKey()))
109-
plaintextResponse, err := client.Decrypt(ctx, &yckms.SymmetricDecryptRequest{
134+
plaintextResponse, err := client.Decrypt(context.Background(), &yckms.SymmetricDecryptRequest{
110135
KeyId: key.KeyID,
111136
Ciphertext: decodedCipher,
112137
})
@@ -129,7 +154,7 @@ func (key *MasterKey) ToString() string {
129154
}
130155

131156
// ToMap converts the MasterKey to a map for serialization purposes.
132-
func (key MasterKey) ToMap() map[string]interface{} {
157+
func (key *MasterKey) ToMap() map[string]interface{} {
133158
out := make(map[string]interface{})
134159
out["key_id"] = key.KeyID
135160
out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
@@ -141,21 +166,36 @@ func (key MasterKey) ToMap() map[string]interface{} {
141166
// and/or grpcConn, falling back to environmental defaults.
142167
// It returns an error if the ResourceID is invalid, or if the setup of the
143168
// client fails.
144-
func (key *MasterKey) newKMSClient() (*kms.SymmetricCryptoServiceClient, context.Context, error) {
145-
ctx := context.Background()
146-
cred, err := getYandexCloudCredentials()
147-
if err != nil {
148-
return nil, nil, err
169+
func (key *MasterKey) newKMSClient() (*kms.SymmetricCryptoServiceClient, error) {
170+
var (
171+
cred ycsdk.Credentials
172+
err error
173+
)
174+
175+
switch {
176+
case key.credentials != nil:
177+
cred = key.credentials
178+
default:
179+
cred, err = getYandexCloudCredentials()
180+
if err != nil {
181+
return nil, err
182+
}
149183
}
150184

151-
client, err := ycsdk.Build(ctx, ycsdk.Config{
185+
client, err := ycsdk.Build(context.Background(), ycsdk.Config{
152186
Credentials: cred,
153187
})
154188
if err != nil {
155-
return nil, nil, err
189+
return nil, err
190+
}
191+
192+
if key.grpcConn != nil {
193+
return kms.NewKMSCrypto(func(ctx context.Context) (*grpc.ClientConn, error) {
194+
return key.grpcConn, nil
195+
}).SymmetricCrypto(), nil
156196
}
157197

158-
return client.KMSCrypto().SymmetricCrypto(), ctx, nil
198+
return client.KMSCrypto().SymmetricCrypto(), nil
159199
}
160200

161201
// getYandexCloudCredentials trying to locate credentials in the following order

yckms/keysource_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package yckms
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
yckms "github.com/yandex-cloud/go-genproto/yandex/cloud/kms/v1"
7+
"google.golang.org/grpc"
8+
"google.golang.org/grpc/codes"
9+
"google.golang.org/grpc/credentials/insecure"
10+
"google.golang.org/grpc/status"
11+
"google.golang.org/grpc/test/bufconn"
12+
"net"
13+
"testing"
14+
"time"
15+
16+
"github.com/stretchr/testify/assert"
17+
)
18+
19+
const (
20+
dummyKey = "920aff2e-c5f1-4040-943a-047fa387b27e"
21+
anotherDummyKey = "920aff2e-c5f1-4040-943a-047fa587b27e"
22+
dummyKeys = dummyKey + ", " + anotherDummyKey
23+
decodedKey = "I want to be a DJ"
24+
)
25+
26+
const bufSize = 1024 * 1024
27+
28+
var lis *bufconn.Listener
29+
30+
func init() {
31+
lis = bufconn.Listen(bufSize)
32+
s := grpc.NewServer()
33+
yckms.RegisterSymmetricCryptoServiceServer(s, mockSymmetricCryptoServiceServer{})
34+
go func() {
35+
if err := s.Serve(lis); err != nil {
36+
log.Fatalf("Server exited with error: %v", err)
37+
}
38+
}()
39+
}
40+
41+
func bufDialer(context.Context, string) (net.Conn, error) {
42+
return lis.Dial()
43+
}
44+
45+
type mockSymmetricCryptoServiceServer struct {
46+
}
47+
48+
func (mockSymmetricCryptoServiceServer) Encrypt(ctx context.Context, req *yckms.SymmetricEncryptRequest) (*yckms.SymmetricEncryptResponse, error) {
49+
return &yckms.SymmetricEncryptResponse{
50+
Ciphertext: []byte(base64.StdEncoding.EncodeToString(req.Plaintext)),
51+
}, nil
52+
}
53+
54+
func (mockSymmetricCryptoServiceServer) Decrypt(ctx context.Context, req *yckms.SymmetricDecryptRequest) (*yckms.SymmetricDecryptResponse, error) {
55+
plain, err := base64.StdEncoding.DecodeString(string(req.Ciphertext))
56+
if err != nil {
57+
return nil, err
58+
}
59+
return &yckms.SymmetricDecryptResponse{
60+
Plaintext: plain,
61+
}, nil
62+
}
63+
func (mockSymmetricCryptoServiceServer) ReEncrypt(context.Context, *yckms.SymmetricReEncryptRequest) (*yckms.SymmetricReEncryptResponse, error) {
64+
return nil, status.Errorf(codes.Unimplemented, "method ReEncrypt not implemented")
65+
}
66+
func (mockSymmetricCryptoServiceServer) GenerateDataKey(context.Context, *yckms.GenerateDataKeyRequest) (*yckms.GenerateDataKeyResponse, error) {
67+
return nil, status.Errorf(codes.Unimplemented, "method GenerateDataKey not implemented")
68+
}
69+
70+
func TestNewMasterKeyFromKeyID(t *testing.T) {
71+
key := NewMasterKeyFromKeyID(dummyKey)
72+
assert.Equal(t, dummyKey, key.KeyID)
73+
assert.NotNil(t, key.CreationDate)
74+
}
75+
76+
func TestNewMasterKeyFromKeyIDString(t *testing.T) {
77+
keys := NewMasterKeyFromKeyIDString(dummyKeys)
78+
assert.Len(t, keys, 2)
79+
80+
k1 := keys[0]
81+
k2 := keys[1]
82+
83+
assert.Equal(t, dummyKey, k1.KeyID)
84+
assert.Equal(t, anotherDummyKey, k2.KeyID)
85+
}
86+
87+
func TestMasterKey_Encrypt(t *testing.T) {
88+
t.Run("encrypt", func(t *testing.T) {
89+
grpcConn, err := createMockGRPCClient()
90+
assert.NoError(t, err)
91+
92+
key := &MasterKey{
93+
grpcConn: grpcConn,
94+
}
95+
96+
dataKey := []byte(decodedKey)
97+
err = key.Encrypt(dataKey)
98+
assert.NoError(t, err)
99+
100+
// Double base64 is used because encrypted data stored as base64
101+
// and our mock uses base64 instead of actual encryption
102+
assert.EqualValues(t, base64.StdEncoding.EncodeToString([]byte(base64.StdEncoding.EncodeToString([]byte(decodedKey)))), key.EncryptedDataKey())
103+
})
104+
}
105+
106+
func TestMasterKey_Decrypt(t *testing.T) {
107+
t.Run("decrypt", func(t *testing.T) {
108+
grpcConn, err := createMockGRPCClient()
109+
assert.NoError(t, err)
110+
111+
// Double base64 is used because encrypted data stored as base64
112+
// and our mock uses base64 instead of actual encryption
113+
key := &MasterKey{
114+
EncryptedKey: base64.StdEncoding.EncodeToString([]byte(base64.StdEncoding.EncodeToString([]byte(decodedKey)))),
115+
grpcConn: grpcConn,
116+
}
117+
118+
got, err := key.Decrypt()
119+
assert.NoError(t, err)
120+
assert.Equal(t, []byte(decodedKey), got)
121+
})
122+
}
123+
124+
func TestMasterKey_EncryptedDataKey(t *testing.T) {
125+
key := &MasterKey{EncryptedKey: "some key"}
126+
assert.EqualValues(t, key.EncryptedKey, key.EncryptedDataKey())
127+
}
128+
129+
func TestMasterKey_SetEncryptedDataKey(t *testing.T) {
130+
key := &MasterKey{}
131+
data := []byte("some data")
132+
key.SetEncryptedDataKey(data)
133+
assert.EqualValues(t, data, key.EncryptedKey)
134+
}
135+
136+
func TestMasterKey_NeedsRotation(t *testing.T) {
137+
t.Run("false", func(t *testing.T) {
138+
k := &MasterKey{}
139+
k.CreationDate = time.Now().UTC()
140+
141+
assert.False(t, k.NeedsRotation())
142+
})
143+
144+
t.Run("true", func(t *testing.T) {
145+
k := &MasterKey{}
146+
k.CreationDate = time.Now().UTC().Add(-kmsTTL - 1)
147+
148+
assert.True(t, k.NeedsRotation())
149+
})
150+
}
151+
152+
func TestMasterKey_ToString(t *testing.T) {
153+
key := NewMasterKeyFromKeyID(dummyKey)
154+
155+
assert.Equal(t, dummyKey, key.ToString())
156+
}
157+
158+
func TestMasterKey_ToMap(t *testing.T) {
159+
key := NewMasterKeyFromKeyID(dummyKey)
160+
161+
data := []byte("some data")
162+
key.SetEncryptedDataKey(data)
163+
164+
res := key.ToMap()
165+
assert.Equal(t, dummyKey, res["key_id"])
166+
assert.Equal(t, key.CreationDate.UTC().Format(time.RFC3339), res["created_at"])
167+
assert.Equal(t, "some data", res["enc"])
168+
}
169+
170+
func createMockGRPCClient() (*grpc.ClientConn, error) {
171+
return grpc.DialContext(
172+
context.Background(),
173+
"bufnet",
174+
grpc.WithContextDialer(bufDialer),
175+
grpc.WithTransportCredentials(insecure.NewCredentials()),
176+
)
177+
}

0 commit comments

Comments
 (0)