Skip to content

Commit a01916a

Browse files
Added data source google kms secret asymmetric (#4609) (#3076)
* Added data source google_kms_secret_asymmetric * typo in the name * added missing reference to google_kms_crypto_key_version * processed lint errors * remove superfluous brackets * make it explicit that the crc32 is calculated using castagnoli * Removed duplicative beta-only imports Co-authored-by: Mark van Holsteijn <[email protected]> Signed-off-by: Modular Magician <[email protected]> Co-authored-by: Mark van Holsteijn <[email protected]>
1 parent ee4d6d7 commit a01916a

File tree

8 files changed

+476
-0
lines changed

8 files changed

+476
-0
lines changed

.changelog/4609.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-datasource
2+
`google_kms_secret_asymmetric`
3+
```

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module github.com/hashicorp/terraform-provider-google-beta
22

33
require (
4+
cloud.google.com/go v0.78.0
45
cloud.google.com/go/bigtable v1.7.1
56
github.com/GoogleCloudPlatform/declarative-resource-client-library v0.0.0-20210209234318-2149dbf673cf
67
github.com/apparentlymart/go-cidr v1.1.0
@@ -29,6 +30,8 @@ require (
2930
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
3031
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93
3132
google.golang.org/api v0.41.0
33+
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb
34+
google.golang.org/protobuf v1.25.0
3235
gopkg.in/yaml.v2 v2.2.8 // indirect
3336
)
3437

google-beta/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"log"
77
"net/http"
8+
"net/url"
89
"regexp"
910
"strings"
1011
"time"
@@ -13,6 +14,7 @@ import (
1314
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
1415
"google.golang.org/api/option"
1516

17+
kms "cloud.google.com/go/kms/apiv1"
1618
dcl "github.com/GoogleCloudPlatform/declarative-resource-client-library/dcl"
1719
eventarcDcl "github.com/GoogleCloudPlatform/declarative-resource-client-library/services/google/eventarc/beta"
1820
"golang.org/x/oauth2"
@@ -449,6 +451,26 @@ func (c *Config) NewKmsClient(userAgent string) *cloudkms.Service {
449451
return clientKms
450452
}
451453

454+
func (c *Config) NewKeyManagementClient(ctx context.Context, userAgent string) *kms.KeyManagementClient {
455+
u, err := url.Parse(c.KMSBasePath)
456+
if err != nil {
457+
log.Printf("[WARN] Error creating client kms invalid base path url %s, %s", c.KMSBasePath, err)
458+
return nil
459+
}
460+
endpoint := u.Host
461+
if u.Port() == "" {
462+
endpoint = fmt.Sprintf("%s:443", u.Host)
463+
}
464+
465+
log.Printf("[INFO] Instantiating Google Cloud KMS client for path on endpoint %s", endpoint)
466+
clientKms, err := kms.NewKeyManagementClient(ctx, option.WithUserAgent(userAgent), option.WithEndpoint(endpoint))
467+
if err != nil {
468+
log.Printf("[WARN] Error creating client kms: %s", err)
469+
return nil
470+
}
471+
return clientKms
472+
}
473+
452474
func (c *Config) NewLoggingClient(userAgent string) *cloudlogging.Service {
453475
loggingClientBasePath := removeBasePathVersion(c.LoggingBasePath)
454476
log.Printf("[INFO] Instantiating Google Stackdriver Logging client for path %s", loggingClientBasePath)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package google
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
10+
"google.golang.org/protobuf/types/known/wrapperspb"
11+
"hash/crc32"
12+
"regexp"
13+
"strconv"
14+
)
15+
16+
var (
17+
cryptoKeyVersionRegexp = regexp.MustCompile(`^(//[^/]*/[^/]*/)?(projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+/cryptoKeyVersions/[^/]+)$`)
18+
)
19+
20+
func dataSourceGoogleKmsSecretAsymmetric() *schema.Resource {
21+
return &schema.Resource{
22+
ReadContext: dataSourceGoogleKmsSecretAsymmetricReadContext,
23+
Schema: map[string]*schema.Schema{
24+
"crypto_key_version": {
25+
Type: schema.TypeString,
26+
Description: "The fully qualified KMS crypto key version name",
27+
ValidateFunc: validateRegexp(cryptoKeyVersionRegexp.String()),
28+
Required: true,
29+
},
30+
"ciphertext": {
31+
Type: schema.TypeString,
32+
Description: "The public key encrypted ciphertext in base64 encoding",
33+
ValidateFunc: validateBase64WithWhitespaces,
34+
Required: true,
35+
},
36+
"crc32": {
37+
Type: schema.TypeString,
38+
Description: "The crc32 checksum of the ciphertext, hexadecimal encoding",
39+
ValidateFunc: validateHexadecimalUint32,
40+
Optional: true,
41+
},
42+
"plaintext": {
43+
Type: schema.TypeString,
44+
Computed: true,
45+
Sensitive: true,
46+
},
47+
},
48+
}
49+
}
50+
51+
func dataSourceGoogleKmsSecretAsymmetricReadContext(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
52+
var diags diag.Diagnostics
53+
54+
err := dataSourceGoogleKmsSecretAsymmetricRead(ctx, d, meta)
55+
if err != nil {
56+
diags = diag.FromErr(err)
57+
}
58+
return diags
59+
}
60+
61+
func dataSourceGoogleKmsSecretAsymmetricRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
62+
config := meta.(*Config)
63+
userAgent, err := generateUserAgentString(d, config.userAgent)
64+
if err != nil {
65+
return err
66+
}
67+
68+
// `google_kms_crypto_key_version` returns an id with the prefix
69+
// //cloudkms.googleapis.com/v1, which is an invalid name. To allow for the most elegant
70+
// configuration, we will allow it as an input.
71+
keyVersion := cryptoKeyVersionRegexp.FindStringSubmatch(d.Get("crypto_key_version").(string))
72+
cryptoKeyVersion := keyVersion[len(keyVersion)-1]
73+
74+
base64CipherText := removeWhiteSpaceFromString(d.Get("ciphertext").(string))
75+
ciphertext, err := base64.StdEncoding.DecodeString(base64CipherText)
76+
if err != nil {
77+
return err
78+
}
79+
80+
crc32c := func(data []byte) uint32 {
81+
t := crc32.MakeTable(crc32.Castagnoli)
82+
return crc32.Checksum(data, t)
83+
}
84+
85+
ciphertextCRC32C := crc32c(ciphertext)
86+
if s, ok := d.Get("crc32").(string); ok && s != "" {
87+
u, err := strconv.ParseUint(s, 16, 32)
88+
if err != nil {
89+
return fmt.Errorf("failed to convert crc32 into uint32, %s", err)
90+
}
91+
ciphertextCRC32C = uint32(u)
92+
} else {
93+
if err := d.Set("crc32", fmt.Sprintf("%x", ciphertextCRC32C)); err != nil {
94+
return fmt.Errorf("failed to set crc32, %s", err)
95+
}
96+
}
97+
98+
req := &kmspb.AsymmetricDecryptRequest{
99+
Name: cryptoKeyVersion,
100+
Ciphertext: ciphertext,
101+
CiphertextCrc32C: wrapperspb.Int64(int64(ciphertextCRC32C)),
102+
}
103+
104+
client := config.NewKeyManagementClient(ctx, userAgent)
105+
result, err := client.AsymmetricDecrypt(ctx, req)
106+
if err != nil {
107+
return fmt.Errorf("failed to decrypt ciphertext: %v", err)
108+
}
109+
110+
if !result.VerifiedCiphertextCrc32C || int64(crc32c(result.Plaintext)) != result.PlaintextCrc32C.Value {
111+
return fmt.Errorf("asymmetricDecrypt request corrupted in-transit")
112+
}
113+
114+
if err := d.Set("plaintext", string(result.Plaintext)); err != nil {
115+
return fmt.Errorf("error setting plaintext: %s", err)
116+
}
117+
118+
d.SetId(fmt.Sprintf("%s:%x:%s", cryptoKeyVersion, ciphertextCRC32C, base64CipherText))
119+
return nil
120+
}
121+
122+
func removeWhiteSpaceFromString(s string) string {
123+
whitespaceRegexp := regexp.MustCompile(`(?m)[\s]+`)
124+
return whitespaceRegexp.ReplaceAllString(s, "")
125+
}
126+
127+
func validateBase64WithWhitespaces(i interface{}, val string) ([]string, []error) {
128+
_, err := base64.StdEncoding.DecodeString(removeWhiteSpaceFromString(i.(string)))
129+
if err != nil {
130+
return nil, []error{fmt.Errorf("could not decode %q as a valid base64 value. Please use the terraform base64 functions such as base64encode() or filebase64() to supply a valid base64 string", val)}
131+
}
132+
return nil, nil
133+
}
134+
135+
func validateHexadecimalUint32(i interface{}, val string) ([]string, []error) {
136+
_, err := strconv.ParseUint(i.(string), 16, 32)
137+
if err != nil {
138+
return nil, []error{fmt.Errorf("could not decode %q as a unsigned 32 bit hexadecimal integer", val)}
139+
}
140+
return nil, nil
141+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package google
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"crypto/sha256"
7+
"crypto/x509"
8+
"encoding/base64"
9+
"encoding/pem"
10+
"fmt"
11+
"hash/crc32"
12+
"log"
13+
"testing"
14+
15+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
16+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
17+
)
18+
19+
func TestAccKmsSecretAsymmetricBasic(t *testing.T) {
20+
// Nested tests confuse VCR
21+
skipIfVcr(t)
22+
t.Parallel()
23+
24+
projectOrg := getTestOrgFromEnv(t)
25+
projectBillingAccount := getTestBillingAccountFromEnv(t)
26+
27+
projectID := "terraform-" + randString(t, 10)
28+
keyRingName := fmt.Sprintf("tf-test-%s", randString(t, 10))
29+
cryptoKeyName := fmt.Sprintf("tf-test-%s", randString(t, 10))
30+
31+
plaintext := fmt.Sprintf("secret-%s", randString(t, 10))
32+
33+
// The first test creates resources needed to encrypt plaintext and produce ciphertext
34+
vcrTest(t, resource.TestCase{
35+
PreCheck: func() { testAccPreCheck(t) },
36+
Providers: testAccProviders,
37+
Steps: []resource.TestStep{
38+
{
39+
Config: kmsCryptoKeyAsymmetricDecryptBasic(projectID, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName),
40+
Check: func(s *terraform.State) error {
41+
ciphertext, cryptoKeyVersionID, crc, err := testAccEncryptSecretDataAsymmetricWithPublicKey(t, s, "data.google_kms_crypto_key_version.crypto_key", plaintext)
42+
if err != nil {
43+
return err
44+
}
45+
46+
// The second test asserts that the data source has the correct plaintext, given the created ciphertext
47+
vcrTest(t, resource.TestCase{
48+
PreCheck: func() { testAccPreCheck(t) },
49+
Providers: testAccProviders,
50+
Steps: []resource.TestStep{
51+
{
52+
Config: googleKmsSecretAsymmetricDatasource(cryptoKeyVersionID, ciphertext),
53+
Check: resource.TestCheckResourceAttr("data.google_kms_secret_asymmetric.acceptance", "plaintext", plaintext),
54+
},
55+
{
56+
Config: googleKmsSecretAsymmetricDatasourceWithCrc(cryptoKeyVersionID, ciphertext, crc),
57+
Check: resource.TestCheckResourceAttr("data.google_kms_secret_asymmetric.acceptance_with_crc", "plaintext", plaintext),
58+
},
59+
},
60+
})
61+
62+
return nil
63+
},
64+
},
65+
},
66+
})
67+
}
68+
69+
func testAccEncryptSecretDataAsymmetricWithPublicKey(t *testing.T, s *terraform.State, cryptoKeyResourceName, plaintext string) (string, string, uint32, error) {
70+
rs, ok := s.RootModule().Resources[cryptoKeyResourceName]
71+
if !ok {
72+
return "", "", 0, fmt.Errorf("resource not found: %s", cryptoKeyResourceName)
73+
}
74+
75+
cryptoKeyVersionID := rs.Primary.Attributes["id"]
76+
77+
block, _ := pem.Decode([]byte(rs.Primary.Attributes["public_key.0.pem"]))
78+
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
79+
if err != nil {
80+
return "", "", 0, fmt.Errorf("failed to parse public key: %v", err)
81+
}
82+
rsaKey, ok := publicKey.(*rsa.PublicKey)
83+
if !ok {
84+
return "", "", 0, fmt.Errorf("public key is not rsa")
85+
}
86+
87+
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaKey, []byte(plaintext), nil)
88+
if err != nil {
89+
return "", "", 0, fmt.Errorf("rsa.EncryptOAEP: %v", err)
90+
}
91+
92+
crc := crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli))
93+
94+
result := base64.StdEncoding.EncodeToString(ciphertext)
95+
log.Printf("[INFO] Successfully encrypted plaintext and got ciphertext: %s", result)
96+
97+
return result, cryptoKeyVersionID, crc, nil
98+
}
99+
100+
func googleKmsSecretAsymmetricDatasource(cryptoKeyTerraformID, ciphertext string) string {
101+
return fmt.Sprintf(`
102+
data "google_kms_secret_asymmetric" "acceptance" {
103+
crypto_key_version = "%s"
104+
ciphertext = "%s"
105+
}
106+
`, cryptoKeyTerraformID, ciphertext)
107+
}
108+
109+
func googleKmsSecretAsymmetricDatasourceWithCrc(cryptoKeyTerraformID, ciphertext string, crc uint32) string {
110+
return fmt.Sprintf(`
111+
data "google_kms_secret_asymmetric" "acceptance_with_crc" {
112+
crypto_key_version = "%s"
113+
ciphertext = "%s"
114+
crc32 = "%x"
115+
}
116+
`, cryptoKeyTerraformID, ciphertext, crc)
117+
}
118+
119+
func kmsCryptoKeyAsymmetricDecryptBasic(projectID, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName string) string {
120+
return fmt.Sprintf(`
121+
resource "google_project" "acceptance" {
122+
name = "%s"
123+
project_id = "%s"
124+
org_id = "%s"
125+
billing_account = "%s"
126+
}
127+
128+
resource "google_project_service" "acceptance" {
129+
project = google_project.acceptance.project_id
130+
service = "cloudkms.googleapis.com"
131+
}
132+
133+
resource "google_kms_key_ring" "key_ring" {
134+
project = google_project_service.acceptance.project
135+
name = "%s"
136+
location = "us-central1"
137+
depends_on = [google_project_service.acceptance]
138+
}
139+
140+
resource "google_kms_crypto_key" "crypto_key" {
141+
name = "%s"
142+
key_ring = google_kms_key_ring.key_ring.self_link
143+
purpose = "ASYMMETRIC_DECRYPT"
144+
version_template {
145+
algorithm = "RSA_DECRYPT_OAEP_4096_SHA256"
146+
}
147+
}
148+
149+
data "google_kms_crypto_key_version" "crypto_key" {
150+
crypto_key = google_kms_crypto_key.crypto_key.id
151+
}
152+
`, projectID, projectID, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName)
153+
}

google-beta/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,7 @@ func Provider() *schema.Provider {
789789
"google_kms_key_ring": dataSourceGoogleKmsKeyRing(),
790790
"google_kms_secret": dataSourceGoogleKmsSecret(),
791791
"google_kms_secret_ciphertext": dataSourceGoogleKmsSecretCiphertext(),
792+
"google_kms_secret_asymmetric": dataSourceGoogleKmsSecretAsymmetric(),
792793
"google_firebase_web_app": dataSourceGoogleFirebaseWebApp(),
793794
"google_firebase_web_app_config": dataSourceGoogleFirebaseWebappConfig(),
794795
"google_folder": dataSourceGoogleFolder(),

0 commit comments

Comments
 (0)