Skip to content

Commit

Permalink
Added data source google kms secret asymmetric (#4609)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
melinath and mvanholsteijn authored Mar 22, 2021
1 parent 18229a6 commit d332482
Show file tree
Hide file tree
Showing 5 changed files with 467 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}
24 changes: 23 additions & 1 deletion mmv1/third_party/terraform/utils/config.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions mmv1/third_party/terraform/utils/provider.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit d332482

Please sign in to comment.