Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added data source google kms secret asymmetric #4609

Merged
merged 7 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the first datasource/resource in TPG to use the new context-aware CRUD ops.

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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side note, this will also need to use tf-test- rather than terraform-

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 @@ -232,6 +232,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