diff --git a/mmv1/third_party/terraform/data_sources/data_source_google_kms_secret_asymmetric.go b/mmv1/third_party/terraform/data_sources/data_source_google_kms_secret_asymmetric.go new file mode 100644 index 000000000000..64d82d727a2c --- /dev/null +++ b/mmv1/third_party/terraform/data_sources/data_source_google_kms_secret_asymmetric.go @@ -0,0 +1,141 @@ +package google + +import ( + "context" + "encoding/base64" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" + "google.golang.org/protobuf/types/known/wrapperspb" + "hash/crc32" + "regexp" + "strconv" +) + +var ( + cryptoKeyVersionRegexp = regexp.MustCompile(`^(//[^/]*/[^/]*/)?(projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+/cryptoKeyVersions/[^/]+)$`) +) + +func dataSourceGoogleKmsSecretAsymmetric() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceGoogleKmsSecretAsymmetricReadContext, + Schema: map[string]*schema.Schema{ + "crypto_key_version": { + Type: schema.TypeString, + Description: "The fully qualified KMS crypto key version name", + ValidateFunc: validateRegexp(cryptoKeyVersionRegexp.String()), + Required: true, + }, + "ciphertext": { + Type: schema.TypeString, + Description: "The public key encrypted ciphertext in base64 encoding", + ValidateFunc: validateBase64WithWhitespaces, + Required: true, + }, + "crc32": { + Type: schema.TypeString, + Description: "The crc32 checksum of the ciphertext, hexadecimal encoding", + ValidateFunc: validateHexadecimalUint32, + Optional: true, + }, + "plaintext": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +func dataSourceGoogleKmsSecretAsymmetricReadContext(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + err := dataSourceGoogleKmsSecretAsymmetricRead(ctx, d, meta) + if err != nil { + diags = diag.FromErr(err) + } + return diags +} + +func dataSourceGoogleKmsSecretAsymmetricRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + // `google_kms_crypto_key_version` returns an id with the prefix + // //cloudkms.googleapis.com/v1, which is an invalid name. To allow for the most elegant + // configuration, we will allow it as an input. + keyVersion := cryptoKeyVersionRegexp.FindStringSubmatch(d.Get("crypto_key_version").(string)) + cryptoKeyVersion := keyVersion[len(keyVersion)-1] + + base64CipherText := removeWhiteSpaceFromString(d.Get("ciphertext").(string)) + ciphertext, err := base64.StdEncoding.DecodeString(base64CipherText) + if err != nil { + return err + } + + crc32c := func(data []byte) uint32 { + t := crc32.MakeTable(crc32.Castagnoli) + return crc32.Checksum(data, t) + } + + ciphertextCRC32C := crc32c(ciphertext) + if s, ok := d.Get("crc32").(string); ok && s != "" { + u, err := strconv.ParseUint(s, 16, 32) + if err != nil { + return fmt.Errorf("failed to convert crc32 into uint32, %s", err) + } + ciphertextCRC32C = uint32(u) + } else { + if err := d.Set("crc32", fmt.Sprintf("%x", ciphertextCRC32C)); err != nil { + return fmt.Errorf("failed to set crc32, %s", err) + } + } + + req := &kmspb.AsymmetricDecryptRequest{ + Name: cryptoKeyVersion, + Ciphertext: ciphertext, + CiphertextCrc32C: wrapperspb.Int64(int64(ciphertextCRC32C)), + } + + client := config.NewKeyManagementClient(ctx, userAgent) + result, err := client.AsymmetricDecrypt(ctx, req) + if err != nil { + return fmt.Errorf("failed to decrypt ciphertext: %v", err) + } + + if !result.VerifiedCiphertextCrc32C || int64(crc32c(result.Plaintext)) != result.PlaintextCrc32C.Value { + return fmt.Errorf("asymmetricDecrypt request corrupted in-transit") + } + + if err := d.Set("plaintext", string(result.Plaintext)); err != nil { + return fmt.Errorf("error setting plaintext: %s", err) + } + + d.SetId(fmt.Sprintf("%s:%x:%s", cryptoKeyVersion, ciphertextCRC32C, base64CipherText)) + return nil +} + +func removeWhiteSpaceFromString(s string) string { + whitespaceRegexp := regexp.MustCompile(`(?m)[\s]+`) + return whitespaceRegexp.ReplaceAllString(s, "") +} + +func validateBase64WithWhitespaces(i interface{}, val string) ([]string, []error) { + _, err := base64.StdEncoding.DecodeString(removeWhiteSpaceFromString(i.(string))) + if err != nil { + 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)} + } + return nil, nil +} + +func validateHexadecimalUint32(i interface{}, val string) ([]string, []error) { + _, err := strconv.ParseUint(i.(string), 16, 32) + if err != nil { + return nil, []error{fmt.Errorf("could not decode %q as a unsigned 32 bit hexadecimal integer", val)} + } + return nil, nil +} diff --git a/mmv1/third_party/terraform/tests/data_source_google_kms_secret_asymmetric_test.go b/mmv1/third_party/terraform/tests/data_source_google_kms_secret_asymmetric_test.go new file mode 100644 index 000000000000..74d55ee1fa49 --- /dev/null +++ b/mmv1/third_party/terraform/tests/data_source_google_kms_secret_asymmetric_test.go @@ -0,0 +1,153 @@ +package google + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "hash/crc32" + "log" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccKmsSecretAsymmetricBasic(t *testing.T) { + // Nested tests confuse VCR + skipIfVcr(t) + t.Parallel() + + projectOrg := getTestOrgFromEnv(t) + projectBillingAccount := getTestBillingAccountFromEnv(t) + + projectID := "terraform-" + randString(t, 10) + keyRingName := fmt.Sprintf("tf-test-%s", randString(t, 10)) + cryptoKeyName := fmt.Sprintf("tf-test-%s", randString(t, 10)) + + plaintext := fmt.Sprintf("secret-%s", randString(t, 10)) + + // The first test creates resources needed to encrypt plaintext and produce ciphertext + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: kmsCryptoKeyAsymmetricDecryptBasic(projectID, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName), + Check: func(s *terraform.State) error { + ciphertext, cryptoKeyVersionID, crc, err := testAccEncryptSecretDataAsymmetricWithPublicKey(t, s, "data.google_kms_crypto_key_version.crypto_key", plaintext) + if err != nil { + return err + } + + // The second test asserts that the data source has the correct plaintext, given the created ciphertext + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: googleKmsSecretAsymmetricDatasource(cryptoKeyVersionID, ciphertext), + Check: resource.TestCheckResourceAttr("data.google_kms_secret_asymmetric.acceptance", "plaintext", plaintext), + }, + { + Config: googleKmsSecretAsymmetricDatasourceWithCrc(cryptoKeyVersionID, ciphertext, crc), + Check: resource.TestCheckResourceAttr("data.google_kms_secret_asymmetric.acceptance_with_crc", "plaintext", plaintext), + }, + }, + }) + + return nil + }, + }, + }, + }) +} + +func testAccEncryptSecretDataAsymmetricWithPublicKey(t *testing.T, s *terraform.State, cryptoKeyResourceName, plaintext string) (string, string, uint32, error) { + rs, ok := s.RootModule().Resources[cryptoKeyResourceName] + if !ok { + return "", "", 0, fmt.Errorf("resource not found: %s", cryptoKeyResourceName) + } + + cryptoKeyVersionID := rs.Primary.Attributes["id"] + + block, _ := pem.Decode([]byte(rs.Primary.Attributes["public_key.0.pem"])) + publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return "", "", 0, fmt.Errorf("failed to parse public key: %v", err) + } + rsaKey, ok := publicKey.(*rsa.PublicKey) + if !ok { + return "", "", 0, fmt.Errorf("public key is not rsa") + } + + ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaKey, []byte(plaintext), nil) + if err != nil { + return "", "", 0, fmt.Errorf("rsa.EncryptOAEP: %v", err) + } + + crc := crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli)) + + result := base64.StdEncoding.EncodeToString(ciphertext) + log.Printf("[INFO] Successfully encrypted plaintext and got ciphertext: %s", result) + + return result, cryptoKeyVersionID, crc, nil +} + +func googleKmsSecretAsymmetricDatasource(cryptoKeyTerraformID, ciphertext string) string { + return fmt.Sprintf(` +data "google_kms_secret_asymmetric" "acceptance" { + crypto_key_version = "%s" + ciphertext = "%s" +} +`, cryptoKeyTerraformID, ciphertext) +} + +func googleKmsSecretAsymmetricDatasourceWithCrc(cryptoKeyTerraformID, ciphertext string, crc uint32) string { + return fmt.Sprintf(` +data "google_kms_secret_asymmetric" "acceptance_with_crc" { + crypto_key_version = "%s" + ciphertext = "%s" + crc32 = "%x" +} +`, cryptoKeyTerraformID, ciphertext, crc) +} + +func kmsCryptoKeyAsymmetricDecryptBasic(projectID, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + name = "%s" + project_id = "%s" + org_id = "%s" + billing_account = "%s" +} + +resource "google_project_service" "acceptance" { + project = google_project.acceptance.project_id + service = "cloudkms.googleapis.com" +} + +resource "google_kms_key_ring" "key_ring" { + project = google_project_service.acceptance.project + name = "%s" + location = "us-central1" + depends_on = [google_project_service.acceptance] +} + +resource "google_kms_crypto_key" "crypto_key" { + name = "%s" + key_ring = google_kms_key_ring.key_ring.self_link + purpose = "ASYMMETRIC_DECRYPT" + version_template { + algorithm = "RSA_DECRYPT_OAEP_4096_SHA256" + } +} + +data "google_kms_crypto_key_version" "crypto_key" { + crypto_key = google_kms_crypto_key.crypto_key.id +} +`, projectID, projectID, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName) +} diff --git a/mmv1/third_party/terraform/utils/config.go.erb b/mmv1/third_party/terraform/utils/config.go.erb index d178d2c82c29..31ef4b736734 100644 --- a/mmv1/third_party/terraform/utils/config.go.erb +++ b/mmv1/third_party/terraform/utils/config.go.erb @@ -6,14 +6,16 @@ import ( "fmt" "log" "net/http" + "net/url" "regexp" "strings" "time" - "google.golang.org/api/option" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" + "google.golang.org/api/option" + kms "cloud.google.com/go/kms/apiv1" "golang.org/x/oauth2" googleoauth "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" @@ -333,6 +335,26 @@ func (c *Config) NewKmsClient(userAgent string) *cloudkms.Service { return clientKms } +func (c *Config) NewKeyManagementClient(ctx context.Context, userAgent string) *kms.KeyManagementClient { + u, err := url.Parse(c.KMSBasePath) + if err != nil { + log.Printf("[WARN] Error creating client kms invalid base path url %s, %s", c.KMSBasePath, err) + return nil + } + endpoint := u.Host + if u.Port() == "" { + endpoint = fmt.Sprintf("%s:443", u.Host) + } + + log.Printf("[INFO] Instantiating Google Cloud KMS client for path on endpoint %s", endpoint) + clientKms, err := kms.NewKeyManagementClient(ctx, option.WithUserAgent(userAgent), option.WithEndpoint(endpoint)) + if err != nil { + log.Printf("[WARN] Error creating client kms: %s", err) + return nil + } + return clientKms +} + func (c *Config) NewLoggingClient(userAgent string) *cloudlogging.Service { loggingClientBasePath := removeBasePathVersion(c.LoggingBasePath) log.Printf("[INFO] Instantiating Google Stackdriver Logging client for path %s", loggingClientBasePath) diff --git a/mmv1/third_party/terraform/utils/provider.go.erb b/mmv1/third_party/terraform/utils/provider.go.erb index 986c0282f39a..fb664a4d44ab 100644 --- a/mmv1/third_party/terraform/utils/provider.go.erb +++ b/mmv1/third_party/terraform/utils/provider.go.erb @@ -233,6 +233,7 @@ func Provider() *schema.Provider { "google_kms_key_ring": dataSourceGoogleKmsKeyRing(), "google_kms_secret": dataSourceGoogleKmsSecret(), "google_kms_secret_ciphertext": dataSourceGoogleKmsSecretCiphertext(), + "google_kms_secret_asymmetric": dataSourceGoogleKmsSecretAsymmetric(), <% unless version == 'ga' -%> "google_firebase_web_app": dataSourceGoogleFirebaseWebApp(), "google_firebase_web_app_config": dataSourceGoogleFirebaseWebappConfig(), diff --git a/mmv1/third_party/terraform/website/docs/d/kms_secret_asymmetric.html.markdown b/mmv1/third_party/terraform/website/docs/d/kms_secret_asymmetric.html.markdown new file mode 100644 index 000000000000..808e49bb2318 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/d/kms_secret_asymmetric.html.markdown @@ -0,0 +1,149 @@ +--- +subcategory: "Cloud Key Management Service" +layout: "google" +page_title: "Google: google_kms_secret_asymmetric" +sidebar_current: "docs-google-kms-secret-asymmetric" +description: |- + Provides access to secret data encrypted with Google Cloud KMS asymmetric key +--- + +# google\_kms\_secret\_asymmetric + +This data source allows you to use data encrypted with a Google Cloud KMS asymmetric key +within your resource definitions. + +For more information see +[the official documentation](https://cloud.google.com/kms/docs/encrypt-decrypt-rsa). + +~> **NOTE:** Using this data provider will allow you to conceal secret data within your +resource definitions, but it does not take care of protecting that data in the +logging output, plan output, or state output. Please take care to secure your secret +data outside of resource definitions. + +## Example Usage + +First, create a KMS KeyRing and CryptoKey using the resource definitions: + +```hcl +resource "google_kms_key_ring" "my_key_ring" { + project = "my-project" + name = "my-key-ring" + location = "us-central1" +} + +resource "google_kms_crypto_key" "my_crypto_key" { + name = "my-crypto-key" + key_ring = google_kms_key_ring.my_key_ring.self_link + purpose = "ASYMMETRIC_DECRYPT" + version_template { + algorithm = "RSA_DECRYPT_OAEP_4096_SHA256" + } +} + +data "google_kms_crypto_key" "my_crypto_key" { + crypto_key = google_kms_crypto_key.my_crypto_key.id +} +``` + +Next, use the [Cloud SDK](https://cloud.google.com/kms/docs/encrypt-decrypt-rsa#kms-encrypt-asymmetric-cli) to encrypt +some sensitive information: + +```bash +## get the public key to encrypt the secret with +$ gcloud kms keys versions get-public-key \ + --project my-project \ + --location us-central1 \ + --keyring my-key-ring \ + --key my-crypto-key \ + --output-file public-key.pem + +## encrypt secret with the public key +$ echo -n my-secret-password | \ + openssl pkeyutl -in - \ + -encrypt \ + -pubin \ + -inkey public-key.pem \ + -pkeyopt rsa_padding_mode:oaep \ + -pkeyopt rsa_oaep_md:sha256 \ + -pkeyopt rsa_mgf1_md:sha256 > \ + my-secret-password.enc + +## base64 encode the ciphertext +$ openssl base64 -in my-secret-password.enc +M7nUoba9EGVTu2LjNjBKGdGVBYjyS/i/AY+4yQMQF0Qf/RfUfX31Jw6+VO9OuThq +ylu/7ihX9XD4bM7yYdXnMv9p1OHQUlorSBSbb/J6n1W9UJhcp6um8Tw8/Isx4f75 +4PskYS6f8Y2ItliGt1/A9iR5BTgGtJBwOxMlgoX2Ggq+Nh4E5SbdoaE5o6CO1nBx +eIPsPEebQ6qC4JehQM3IGuV/lrm58+hZhaXAqNzX1cEYyAt5GYqJIVCiI585SUYs +wRToGyTgaN+zthF0HP9IWlR4Am4LmJ/1OcePTnYw11CkU8wNRbDzVAzogwNH+rXr +LTmf7hxVjBm6bBSVSNFcBKAXFlllubSfIeZ5hgzGqn54OmSf6odO12L5JxllddHc +yAd54vWKs2kJtnsKV2V4ZdkI0w6y1TeI67baFZDNGo6qsCpFMPnvv7d46Pg2VOp1 +J6Ivner0NnNHE4MzNmpZRk8WXMwqq4P/gTiT7F/aCX6oFCUQ4AWPQhJYh2dkcOmL +IP+47Veb10aFn61F1CJwpmOOiGNXKdDT1vK8CMnnwhm825K0q/q9Zqpzc1+1ae1z +mSqol1zCoa88CuSN6nTLQlVnN/dzfrGbc0boJPaM0iGhHtSzHk4SWg84LhiJB1q9 +A9XFJmOVdkvRY9nnz/iVLAdd0Q3vFtLqCdUYsNN2yh4= + +## optionally calculate the CRC32 of the ciphertext +$ go get github.com/binxio/crc32 +$ $GOPATH/bin/crc32 -polynomial castagnoli < my-secret-password.enc +12c59e54 +``` + +Finally, reference the encrypted ciphertext in your resource definitions: + +```hcl +data "google_kms_secret_asymmetric" "sql_user_password" { + crypto_key = data.google_kms_crypto_key_version.my_crypto_key.id + crc32 = "12c59e54" + ciphertext = <