Skip to content

Commit 1507275

Browse files
committed
feat(gcpkms): support asymmetric key
1 parent e1edc05 commit 1507275

1 file changed

Lines changed: 152 additions & 2 deletions

File tree

gcpkms/keysource.go

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

33
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"crypto/sha1"
7+
"crypto/sha256"
8+
"crypto/sha512"
9+
"crypto/x509"
410
"encoding/base64"
11+
"encoding/pem"
12+
"errors"
513
"fmt"
6-
"google.golang.org/api/option"
14+
"hash"
15+
"hash/crc32"
716
"os"
817
"regexp"
918
"strings"
@@ -14,6 +23,7 @@ import (
1423
"github.com/sirupsen/logrus"
1524
"golang.org/x/net/context"
1625
cloudkms "google.golang.org/api/cloudkms/v1"
26+
"google.golang.org/api/option"
1727
)
1828

1929
var log *logrus.Logger
@@ -46,6 +56,37 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
4656
log.WithField("resourceID", key.ResourceID).Info("Encryption failed")
4757
return fmt.Errorf("Cannot create GCP KMS service: %w", err)
4858
}
59+
60+
purpose, err := key.purpose(cloudkmsService)
61+
if err != nil {
62+
return err
63+
}
64+
65+
switch purpose {
66+
case "ENCRYPT_DECRYPT":
67+
return key.encryptSymmetric(cloudkmsService, dataKey)
68+
case "ASYMMETRIC_DECRYPT":
69+
return key.encryptAsymmetric(cloudkmsService, dataKey)
70+
}
71+
72+
log.WithField("resourceID", key.ResourceID).WithField("purpose", purpose).Info("This key cannot be used for encryption")
73+
return fmt.Errorf("This key cannot be used for encryption, purpose: %s", purpose)
74+
}
75+
76+
func (key *MasterKey) purpose(cloudkmsService *cloudkms.Service) (string, error) {
77+
// It needs to use `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key` to request.
78+
// If request with format `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key/cryptoKeyVersions/version`,
79+
// KMS will response without `purpose`.
80+
resp, err := cloudkmsService.Projects.Locations.KeyRings.CryptoKeys.Get(key.resourceIDWithoutVersion()).Do()
81+
if err != nil {
82+
log.WithField("resourceID", key.ResourceID).Info("Get key info failed")
83+
return "", fmt.Errorf("Get key from GCP KMS failed: %w", err)
84+
}
85+
86+
return resp.Purpose, nil
87+
}
88+
89+
func (key *MasterKey) encryptSymmetric(cloudkmsService *cloudkms.Service, dataKey []byte) error {
4990
req := &cloudkms.EncryptRequest{
5091
Plaintext: base64.StdEncoding.EncodeToString(dataKey),
5192
}
@@ -59,6 +100,63 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
59100
return nil
60101
}
61102

103+
func (key *MasterKey) encryptAsymmetric(cloudkmsService *cloudkms.Service, dataKey []byte) error {
104+
resp, err := cloudkmsService.Projects.Locations.KeyRings.CryptoKeys.CryptoKeyVersions.GetPublicKey(key.ResourceID).Do()
105+
if err != nil {
106+
log.WithField("resourceID", key.ResourceID).Info("Get public key failed")
107+
return fmt.Errorf("Get public key from GCP KMS failed: %w", err)
108+
}
109+
110+
if resp.PemCrc32c != int64(crc32c([]byte(resp.Pem))) {
111+
log.WithField("resourceID", key.ResourceID).Info("Get public key response corrupted in-transit")
112+
return errors.New("Get public key response corrupted in-transit")
113+
}
114+
115+
block, _ := pem.Decode([]byte(resp.Pem))
116+
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
117+
if err != nil {
118+
log.WithField("resourceID", key.ResourceID).Info("Failed to parse public key")
119+
return fmt.Errorf("Failed to parse public key: %w", err)
120+
}
121+
rsaKey, ok := publicKey.(*rsa.PublicKey)
122+
if !ok {
123+
log.WithField("resourceID", key.ResourceID).Info("Public key is not RSA")
124+
return errors.New("Public key is not RSA")
125+
}
126+
127+
var hash hash.Hash
128+
129+
switch resp.Algorithm {
130+
case "RSA_DECRYPT_OAEP_2048_SHA256":
131+
hash = sha256.New()
132+
case "RSA_DECRYPT_OAEP_3072_SHA256":
133+
hash = sha256.New()
134+
case "RSA_DECRYPT_OAEP_4096_SHA256":
135+
hash = sha256.New()
136+
case "RSA_DECRYPT_OAEP_4096_SHA512":
137+
hash = sha512.New()
138+
case "RSA_DECRYPT_OAEP_2048_SHA1":
139+
hash = sha1.New()
140+
case "RSA_DECRYPT_OAEP_3072_SHA1":
141+
hash = sha1.New()
142+
case "RSA_DECRYPT_OAEP_4096_SHA1":
143+
hash = sha1.New()
144+
default:
145+
log.WithField("resourceID", key.ResourceID).WithField("algorithm", resp.Algorithm).Info("Unsupported algorithm")
146+
return fmt.Errorf("Key with unsupported algorithm: %s", resp.Algorithm)
147+
}
148+
149+
ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, rsaKey, dataKey, nil)
150+
if err != nil {
151+
log.WithField("resourceID", key.ResourceID).Info("rsa.EncryptOAEP() error")
152+
return fmt.Errorf("rsa.EncryptOAEP: %w", err)
153+
}
154+
155+
key.EncryptedKey = base64.StdEncoding.EncodeToString(ciphertext)
156+
157+
return nil
158+
}
159+
62160
// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet
63161
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
64162
if key.EncryptedKey == "" {
@@ -75,6 +173,23 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
75173
return nil, fmt.Errorf("Cannot create GCP KMS service: %w", err)
76174
}
77175

176+
purpose, err := key.purpose(cloudkmsService)
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
switch purpose {
182+
case "ENCRYPT_DECRYPT":
183+
return key.decryptSymmetric(cloudkmsService)
184+
case "ASYMMETRIC_DECRYPT":
185+
return key.decryptAsymmetric(cloudkmsService)
186+
default:
187+
log.WithField("resourceID", key.ResourceID).WithField("purpose", purpose).Info("This key cannot be used for decryption")
188+
return nil, fmt.Errorf("This key cannot be used for decryption, purpose: %s", purpose)
189+
}
190+
}
191+
192+
func (key *MasterKey) decryptSymmetric(cloudkmsService *cloudkms.Service) ([]byte, error) {
78193
req := &cloudkms.DecryptRequest{
79194
Ciphertext: key.EncryptedKey,
80195
}
@@ -92,6 +207,24 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
92207
return encryptedKey, nil
93208
}
94209

210+
func (key *MasterKey) decryptAsymmetric(cloudkmsService *cloudkms.Service) ([]byte, error) {
211+
req := &cloudkms.AsymmetricDecryptRequest{
212+
Ciphertext: key.EncryptedKey,
213+
}
214+
resp, err := cloudkmsService.Projects.Locations.KeyRings.CryptoKeys.CryptoKeyVersions.AsymmetricDecrypt(key.ResourceID, req).Do()
215+
if err != nil {
216+
log.WithField("resourceID", key.ResourceID).Info("Asymmetric decryption failed")
217+
return nil, fmt.Errorf("Error decrypting key: %w", err)
218+
}
219+
encryptedKey, err := base64.StdEncoding.DecodeString(resp.Plaintext)
220+
if err != nil {
221+
log.WithField("resourceID", key.ResourceID).Info("Asymmetric decryption failed")
222+
return nil, err
223+
}
224+
log.WithField("resourceID", key.ResourceID).Info("Asymmetric decryption succeeded")
225+
return encryptedKey, nil
226+
}
227+
95228
// NeedsRotation returns whether the data key needs to be rotated or not.
96229
func (key *MasterKey) NeedsRotation() bool {
97230
return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6)
@@ -124,7 +257,7 @@ func MasterKeysFromResourceIDString(resourceID string) []*MasterKey {
124257
}
125258

126259
func (key MasterKey) createCloudKMSService() (*cloudkms.Service, error) {
127-
re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$`)
260+
re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+(?:/cryptoKeyVersions/[^/]+)?$`)
128261
matches := re.FindStringSubmatch(key.ResourceID)
129262
if matches == nil {
130263
return nil, fmt.Errorf("No valid resourceId found in %q", key.ResourceID)
@@ -155,6 +288,18 @@ func (key MasterKey) ToMap() map[string]interface{} {
155288
return out
156289
}
157290

291+
// assume key.ResourceID is in following format
292+
// - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key`
293+
// - `projects/project-id/locations/location/keyRings/keyring/cryptoKeys/key/cryptoKeyVersions/version`
294+
func (key MasterKey) resourceIDWithoutVersion() string {
295+
re := regexp.MustCompile(`^(projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+)(?:/cryptoKeyVersions/[^/]+)?$`)
296+
matches := re.FindStringSubmatch(key.ResourceID)
297+
if len(matches) < 2 {
298+
return ""
299+
}
300+
return matches[1]
301+
}
302+
158303
// getGoogleCredentials looks for a GCP Service Account in the environment
159304
// variable: GOOGLE_CREDENTIALS, set as either a path to a credentials file or directly as the
160305
// variable's value in JSON format.
@@ -167,3 +312,8 @@ func getGoogleCredentials() ([]byte, error) {
167312
}
168313
return []byte(defaultCredentials), nil
169314
}
315+
316+
func crc32c(data []byte) uint32 {
317+
t := crc32.MakeTable(crc32.Castagnoli)
318+
return crc32.Checksum(data, t)
319+
}

0 commit comments

Comments
 (0)