Skip to content

Commit 7cb2498

Browse files
committed
feat(gcpkms): support asymmetric key
1 parent 9124783 commit 7cb2498

1 file changed

Lines changed: 162 additions & 8 deletions

File tree

gcpkms/keysource.go

Lines changed: 162 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ package gcpkms // import "go.mozilla.org/sops/v3/gcpkms"
22

33
import (
44
"context"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/sha1"
8+
"crypto/sha256"
9+
"crypto/sha512"
10+
"crypto/x509"
511
"encoding/base64"
12+
"encoding/pem"
13+
"errors"
614
"fmt"
15+
"hash"
16+
"hash/crc32"
717
"os"
818
"regexp"
919
"strings"
@@ -14,6 +24,7 @@ import (
1424
"google.golang.org/api/option"
1525
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
1626
"google.golang.org/grpc"
27+
"google.golang.org/protobuf/types/known/wrapperspb"
1728

1829
"go.mozilla.org/sops/v3/logging"
1930
)
@@ -103,11 +114,53 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
103114
}
104115
}()
105116

117+
ctx := context.Background()
118+
purpose, err := key.purpose(ctx, service)
119+
if err != nil {
120+
return err
121+
}
122+
123+
switch purpose {
124+
case kmspb.CryptoKey_ENCRYPT_DECRYPT:
125+
return key.encryptSymmetric(ctx, service, dataKey)
126+
case kmspb.CryptoKey_ASYMMETRIC_DECRYPT:
127+
return key.encryptAsymmetric(ctx, service, dataKey)
128+
default:
129+
log.WithField("resourceID", key.ResourceID).WithField("purpose", purpose.String()).Error("This key is not for encryption")
130+
return fmt.Errorf("this key is not for encryption, purpose: %v", purpose.String())
131+
}
132+
}
133+
134+
func (key *MasterKey) purpose(ctx context.Context, service *kms.KeyManagementClient) (kmspb.CryptoKey_CryptoKeyPurpose, error) {
135+
req := &kmspb.GetCryptoKeyRequest{
136+
Name: key.resourceIDWithoutVersion(),
137+
}
138+
cryptoKey, err := service.GetCryptoKey(ctx, req)
139+
if err != nil {
140+
log.WithError(err).WithField("resourceID", key.ResourceID).Error("Get key metadata failed")
141+
return kmspb.CryptoKey_CRYPTO_KEY_PURPOSE_UNSPECIFIED, fmt.Errorf("failed to get key metadata from GCP KMS service: %w", err)
142+
}
143+
144+
return cryptoKey.GetPurpose(), nil
145+
}
146+
147+
// assume key.ResourceID is in following format
148+
// - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key`
149+
// - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key/cryptoKeyVersions/version`
150+
func (key MasterKey) resourceIDWithoutVersion() string {
151+
re := regexp.MustCompile(`^(projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+)(?:/cryptoKeyVersions/[^/]+)?$`)
152+
matches := re.FindStringSubmatch(key.ResourceID)
153+
if len(matches) < 2 {
154+
return ""
155+
}
156+
return matches[1]
157+
}
158+
159+
func (key *MasterKey) encryptSymmetric(ctx context.Context, service *kms.KeyManagementClient, dataKey []byte) error {
106160
req := &kmspb.EncryptRequest{
107161
Name: key.ResourceID,
108162
Plaintext: dataKey,
109163
}
110-
ctx := context.Background()
111164
resp, err := service.Encrypt(ctx, req)
112165
if err != nil {
113166
log.WithError(err).WithField("resourceID", key.ResourceID).Error("Encryption failed")
@@ -117,7 +170,70 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
117170
// The previous GCP KMS client used to work with base64 encoded
118171
// strings.
119172
key.EncryptedKey = base64.StdEncoding.EncodeToString(resp.Ciphertext)
120-
log.WithField("resourceID", key.ResourceID).Info("Encryption succeeded")
173+
log.WithField("resourceID", key.ResourceID).Info("Symmetric encryption succeeded")
174+
return nil
175+
}
176+
177+
func (key *MasterKey) encryptAsymmetric(ctx context.Context, service *kms.KeyManagementClient, dataKey []byte) error {
178+
req := &kmspb.GetPublicKeyRequest{
179+
Name: key.ResourceID,
180+
}
181+
resp, err := service.GetPublicKey(ctx, req)
182+
if err != nil {
183+
log.WithError(err).WithField("resourceID", key.ResourceID).Error("Get public key failed")
184+
return fmt.Errorf("failed to get public key from GCP KMS service: %w", err)
185+
}
186+
187+
if resp.GetPemCrc32C().GetValue() != wrapperspb.Int64(int64(crc32c([]byte(resp.GetPem())))).Value {
188+
log.WithField("resourceID", key.ResourceID).Error("Get public key response corrupted in-transit")
189+
return errors.New("get public key response corrupted in-transit")
190+
}
191+
192+
block, _ := pem.Decode([]byte(resp.Pem))
193+
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
194+
if err != nil {
195+
log.WithError(err).WithField("resourceID", key.ResourceID).Info("Failed to parse public key")
196+
return fmt.Errorf("Failed to parse public key: %w", err)
197+
}
198+
rsaKey, ok := publicKey.(*rsa.PublicKey)
199+
if !ok {
200+
log.WithField("resourceID", key.ResourceID).Info("Public key is not RSA")
201+
return errors.New("public key is not RSA")
202+
}
203+
204+
var hash hash.Hash
205+
206+
switch resp.GetAlgorithm() {
207+
case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA256:
208+
hash = sha256.New()
209+
case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA256:
210+
hash = sha256.New()
211+
case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA256:
212+
hash = sha256.New()
213+
case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA512:
214+
hash = sha512.New()
215+
case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA1:
216+
hash = sha1.New()
217+
case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA1:
218+
hash = sha1.New()
219+
case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA1:
220+
hash = sha1.New()
221+
default:
222+
log.WithField("resourceID", key.ResourceID).WithField("algorithm", resp.GetAlgorithm().String()).Error("Unsupported algorithm")
223+
return fmt.Errorf("Key with unsupported algorithm: %s", resp.GetAlgorithm().String())
224+
}
225+
226+
ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, rsaKey, dataKey, nil)
227+
if err != nil {
228+
log.WithError(err).WithField("resourceID", key.ResourceID).Error("rsa.EncryptOAEP() error")
229+
return fmt.Errorf("rsa.EncryptOAEP: %w", err)
230+
}
231+
232+
// NB: base64 encoding is for compatibility with SOPS <=3.8.x.
233+
// The previous GCP KMS client used to work with base64 encoded
234+
// strings.
235+
key.EncryptedKey = base64.StdEncoding.EncodeToString(ciphertext)
236+
log.WithField("resourceID", key.ResourceID).Info("Asymmetric encryption succeeded")
121237
return nil
122238
}
123239

@@ -162,18 +278,51 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
162278
return nil, err
163279
}
164280

281+
ctx := context.Background()
282+
283+
purpose, err := key.purpose(ctx, service)
284+
if err != nil {
285+
return nil, err
286+
}
287+
288+
switch purpose {
289+
case kmspb.CryptoKey_ENCRYPT_DECRYPT:
290+
return key.decryptSymmetric(ctx, service, decodedCipher)
291+
case kmspb.CryptoKey_ASYMMETRIC_DECRYPT:
292+
return key.decryptAsymmetric(ctx, service, decodedCipher)
293+
default:
294+
log.WithField("resourceID", key.ResourceID).WithField("purpose", purpose.String()).Info("This key cannot be used for decryption")
295+
return nil, fmt.Errorf("This key cannot be used for decryption, purpose: %s", purpose.String())
296+
}
297+
}
298+
299+
func (key *MasterKey) decryptSymmetric(ctx context.Context, service *kms.KeyManagementClient, decodedCipher []byte) ([]byte, error) {
165300
req := &kmspb.DecryptRequest{
166301
Name: key.ResourceID,
167302
Ciphertext: decodedCipher,
168303
}
169-
ctx := context.Background()
170304
resp, err := service.Decrypt(ctx, req)
171305
if err != nil {
172-
log.WithError(err).WithField("resourceID", key.ResourceID).Error("Decryption failed")
306+
log.WithError(err).WithField("resourceID", key.ResourceID).Error("Symmetric decryption failed")
307+
return nil, fmt.Errorf("failed to decrypt sops data key with GCP KMS key: %w", err)
308+
}
309+
310+
log.WithField("resourceID", key.ResourceID).Info("Symmetric decryption succeeded")
311+
return resp.Plaintext, nil
312+
}
313+
314+
func (key *MasterKey) decryptAsymmetric(ctx context.Context, service *kms.KeyManagementClient, decodedCipher []byte) ([]byte, error) {
315+
req := &kmspb.AsymmetricDecryptRequest{
316+
Name: key.ResourceID,
317+
Ciphertext: decodedCipher,
318+
}
319+
resp, err := service.AsymmetricDecrypt(ctx, req)
320+
if err != nil {
321+
log.WithError(err).WithField("resourceID", key.ResourceID).Error("Asymmetric decryption failed")
173322
return nil, fmt.Errorf("failed to decrypt sops data key with GCP KMS key: %w", err)
174323
}
175324

176-
log.WithField("resourceID", key.ResourceID).Info("Decryption succeeded")
325+
log.WithField("resourceID", key.ResourceID).Info("Asymmetric decryption succeeded")
177326
return resp.Plaintext, nil
178327
}
179328

@@ -201,7 +350,7 @@ func (key MasterKey) ToMap() map[string]interface{} {
201350
// It returns an error if the ResourceID is invalid, or if the setup of the
202351
// client fails.
203352
func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) {
204-
re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$`)
353+
re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+(?:/cryptoKeyVersions/[^/]+)?$`)
205354
matches := re.FindStringSubmatch(key.ResourceID)
206355
if matches == nil {
207356
return nil, fmt.Errorf("no valid resource ID found in %q", key.ResourceID)
@@ -216,8 +365,8 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) {
216365
if err != nil {
217366
return nil, err
218367
}
219-
if credentials != nil {
220-
opts = append(opts, option.WithCredentialsJSON(key.credentialJSON))
368+
if len(credentials) > 0 {
369+
opts = append(opts, option.WithCredentialsJSON(credentials))
221370
}
222371
}
223372
if key.grpcConn != nil {
@@ -244,3 +393,8 @@ func getGoogleCredentials() ([]byte, error) {
244393
}
245394
return []byte(defaultCredentials), nil
246395
}
396+
397+
func crc32c(data []byte) uint32 {
398+
t := crc32.MakeTable(crc32.Castagnoli)
399+
return crc32.Checksum(data, t)
400+
}

0 commit comments

Comments
 (0)