From 22bfbbcf2250054e622743c4a35a872e43ed5aed Mon Sep 17 00:00:00 2001 From: The Magician Date: Wed, 29 Jun 2022 09:36:44 -0700 Subject: [PATCH] Support subordinate CA activation via first and third party issuers. (#6161) (#4422) Signed-off-by: Modular Magician --- .changelog/6161.txt | 3 + google-beta/privateca_ca_utils.go | 221 +++++++++++++++ google-beta/resource_privateca_certificate.go | 13 +- ...esource_privateca_certificate_authority.go | 256 ++++++++++++++---- ...ca_certificate_authority_generated_test.go | 47 +++- ...ce_privateca_certificate_authority_test.go | 137 ++++++++++ .../r/privateca_certificate.html.markdown | 7 +- ...vateca_certificate_authority.html.markdown | 78 +++++- 8 files changed, 694 insertions(+), 68 deletions(-) create mode 100644 .changelog/6161.txt create mode 100644 google-beta/privateca_ca_utils.go diff --git a/.changelog/6161.txt b/.changelog/6161.txt new file mode 100644 index 0000000000..b3520fd595 --- /dev/null +++ b/.changelog/6161.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +privateca: added support to subordinate CA activation +``` diff --git a/google-beta/privateca_ca_utils.go b/google-beta/privateca_ca_utils.go new file mode 100644 index 0000000000..6cc7b6d72b --- /dev/null +++ b/google-beta/privateca_ca_utils.go @@ -0,0 +1,221 @@ +package google + +import ( + "fmt" + "log" + "math/rand" + "regexp" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// CA related utilities. + +func enableCA(config *Config, d *schema.ResourceData, project string, billingProject string, userAgent string) error { + enableUrl, err := replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:enable") + if err != nil { + return err + } + + log.Printf("[DEBUG] Enabling CertificateAuthority") + + res, err := sendRequest(config, "POST", billingProject, enableUrl, userAgent, nil) + if err != nil { + return fmt.Errorf("Error enabling CertificateAuthority: %s", err) + } + + var opRes map[string]interface{} + err = privatecaOperationWaitTimeWithResponse( + config, res, &opRes, project, "Enabling CertificateAuthority", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error waiting to enable CertificateAuthority: %s", err) + } + return nil +} + +func disableCA(config *Config, d *schema.ResourceData, project string, billingProject string, userAgent string) error { + disableUrl, err := replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:disable") + if err != nil { + return err + } + + log.Printf("[DEBUG] Disabling CA") + + dRes, err := sendRequest(config, "POST", billingProject, disableUrl, userAgent, nil) + if err != nil { + return fmt.Errorf("Error disabling CA: %s", err) + } + + var opRes map[string]interface{} + err = privatecaOperationWaitTimeWithResponse( + config, dRes, &opRes, project, "Disabling CA", userAgent, + d.Timeout(schema.TimeoutDelete)) + if err != nil { + return fmt.Errorf("Error waiting to disable CA: %s", err) + } + return nil +} + +func activateSubCAWithThirdPartyIssuer(config *Config, d *schema.ResourceData, project string, billingProject string, userAgent string) error { + // 1. prepare parameters + signedCACert := d.Get("pem_ca_certificate").(string) + + sc, ok := d.GetOk("subordinate_config") + if !ok { + return fmt.Errorf("subordinate_config is required to activate subordinate CA") + } + c := sc.([]interface{}) + if len(c) == 0 || c[0] == nil { + return fmt.Errorf("subordinate_config is required to activate subordinate CA") + } + chain, ok := c[0].(map[string]interface{})["pem_issuer_chain"] + if !ok { + return fmt.Errorf("subordinate_config.pem_issuer_chain is required to activate subordinate CA with third party issuer") + } + issuerChain := chain.([]interface{}) + if len(issuerChain) == 0 || issuerChain[0] == nil { + return fmt.Errorf("subordinate_config.pem_issuer_chain is required to activate subordinate CA with third party issuer") + } + pc := issuerChain[0].(map[string]interface{})["pem_certificates"].([]interface{}) + pemIssuerChain := make([]string, 0, len(pc)) + for _, pem := range pc { + pemIssuerChain = append(pemIssuerChain, pem.(string)) + } + + // 2. activate CA + activateObj := make(map[string]interface{}) + activateObj["pemCaCertificate"] = signedCACert + activateObj["subordinateConfig"] = make(map[string]interface{}) + activateObj["subordinateConfig"].(map[string]interface{})["pemIssuerChain"] = make(map[string]interface{}) + activateObj["subordinateConfig"].(map[string]interface{})["pemIssuerChain"].(map[string]interface{})["pemCertificates"] = pemIssuerChain + + activateUrl, err := replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:activate") + if err != nil { + return err + } + + log.Printf("[DEBUG] Activating CertificateAuthority: %#v", activateObj) + res, err := sendRequest(config, "POST", billingProject, activateUrl, userAgent, activateObj) + if err != nil { + return fmt.Errorf("Error enabling CertificateAuthority: %s", err) + } + + var opRes map[string]interface{} + err = privatecaOperationWaitTimeWithResponse( + config, res, &opRes, project, "Activating CertificateAuthority", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error waiting to actiavte CertificateAuthority: %s", err) + } + return nil +} + +func activateSubCAWithFirstPartyIssuer(config *Config, d *schema.ResourceData, project string, billingProject string, userAgent string) error { + // 1. get issuer + sc, ok := d.GetOk("subordinate_config") + if !ok { + return fmt.Errorf("subordinate_config is required to activate subordinate CA") + } + c := sc.([]interface{}) + if len(c) == 0 || c[0] == nil { + return fmt.Errorf("subordinate_config is required to activate subordinate CA") + } + ca, ok := c[0].(map[string]interface{})["certificate_authority"] + if !ok { + return fmt.Errorf("subordinate_config.certificate_authority is required to activate subordinate CA with first party issuer") + } + issuer := ca.(string) + + // 2. fetch CSR + fetchCSRUrl, err := replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:fetch") + if err != nil { + return err + } + res, err := sendRequest(config, "GET", billingProject, fetchCSRUrl, userAgent, nil) + if err != nil { + return fmt.Errorf("failed to fetch CSR: %v", err) + } + csr := res["pemCsr"] + + // 3. sign the CSR with first party issuer + genCertId := func() string { + currentTime := time.Now() + dateStr := currentTime.Format("20060102") + + rand.Seed(time.Now().UnixNano()) + const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + rand1 := make([]byte, 3) + for i := range rand1 { + rand1[i] = letters[rand.Intn(len(letters))] + } + rand2 := make([]byte, 3) + for i := range rand2 { + rand2[i] = letters[rand.Intn(len(letters))] + } + return fmt.Sprintf("subordinate-%v-%v-%v", dateStr, string(rand1), string(rand2)) + } + + // parseCAName parses a CA name and return the CaPool name and CaId. + parseCAName := func(n string) (string, string, error) { + parts := regexp.MustCompile(`(projects/[a-z0-9-]+/locations/[a-z0-9-]+/caPools/[a-zA-Z0-9-]+)/certificateAuthorities/([a-zA-Z0-9-]+)`).FindStringSubmatch(n) + if len(parts) != 3 { + return "", "", fmt.Errorf("failed to parse CA name: %v, parts: %v", n, parts) + } + return parts[1], parts[2], err + } + + obj := make(map[string]interface{}) + obj["pemCsr"] = csr + obj["lifetime"] = d.Get("lifetime") + + certId := genCertId() + poolName, issuerId, err := parseCAName(issuer) + if err != nil { + return err + } + + PrivatecaBasePath, err := replaceVars(d, config, "{{PrivatecaBasePath}}") + if err != nil { + return err + } + signUrl := fmt.Sprintf("%v%v/certificates?certificateId=%v", PrivatecaBasePath, poolName, certId) + signUrl, err = addQueryParams(signUrl, map[string]string{"issuingCertificateAuthorityId": issuerId}) + if err != nil { + return err + } + + log.Printf("[DEBUG] Signing CA Certificate: %#v", obj) + res, err = sendRequestWithTimeout(config, "POST", billingProject, signUrl, userAgent, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Certificate: %s", err) + } + signedCACert := res["pemCertificate"] + + // 4. activate sub CA with the signed CA cert. + activateObj := make(map[string]interface{}) + activateObj["pemCaCertificate"] = signedCACert + activateObj["subordinateConfig"] = make(map[string]interface{}) + activateObj["subordinateConfig"].(map[string]interface{})["certificateAuthority"] = issuer + + activateUrl, err := replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:activate") + if err != nil { + return err + } + + log.Printf("[DEBUG] Activating CertificateAuthority: %#v", activateObj) + res, err = sendRequest(config, "POST", billingProject, activateUrl, userAgent, activateObj) + if err != nil { + return fmt.Errorf("Error enabling CertificateAuthority: %s", err) + } + + var opRes map[string]interface{} + err = privatecaOperationWaitTimeWithResponse( + config, res, &opRes, project, "Enabling CertificateAuthority", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error waiting to actiavte CertificateAuthority: %s", err) + } + return nil +} diff --git a/google-beta/resource_privateca_certificate.go b/google-beta/resource_privateca_certificate.go index 13f391704b..6155a88bdc 100644 --- a/google-beta/resource_privateca_certificate.go +++ b/google-beta/resource_privateca_certificate.go @@ -73,10 +73,13 @@ omitted, no template will be used. This template must be in the same location as the Certificate.`, }, "certificate_authority": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: `Certificate Authority name.`, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The Certificate Authority ID that should issue the certificate. For example, to issue a Certificate from +a Certificate Authority with resource name 'projects/my-project/locations/us-central1/caPools/my-pool/certificateAuthorities/my-ca', +argument 'pool' should be set to 'projects/my-project/locations/us-central1/caPools/my-pool', argument 'certificate_authority' +should be set to 'my-ca'.`, }, "config": { Type: schema.TypeList, @@ -1129,7 +1132,7 @@ This is in RFC3339 text format.`, "issuer_certificate_authority": { Type: schema.TypeString, Computed: true, - Description: `The resource name of the issuing CertificateAuthority in the format projects/*/locations/*/caPools/*/certificateAuthorities/*.`, + Description: `The resource name of the issuing CertificateAuthority in the format 'projects/*/locations/*/caPools/*/certificateAuthorities/*'.`, }, "pem_certificate": { Type: schema.TypeString, diff --git a/google-beta/resource_privateca_certificate_authority.go b/google-beta/resource_privateca_certificate_authority.go index c4cd85e718..1cbef84c57 100644 --- a/google-beta/resource_privateca_certificate_authority.go +++ b/google-beta/resource_privateca_certificate_authority.go @@ -550,6 +550,52 @@ An object containing a list of "key": value pairs. Example: { "name": "wrench", fractional digits, terminated by 's'. Example: "3.5s".`, Default: "315360000s", }, + "pem_ca_certificate": { + Type: schema.TypeString, + Optional: true, + Description: `The signed CA certificate issued from the subordinated CA's CSR. This is needed when activating the subordiante CA with a third party issuer.`, + }, + "subordinate_config": { + Type: schema.TypeList, + Optional: true, + Description: `If this is a subordinate CertificateAuthority, this field will be set +with the subordinate configuration, which describes its issuers.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "certificate_authority": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: compareResourceNames, + Description: `This can refer to a CertificateAuthority that was used to create a +subordinate CertificateAuthority. This field is used for information +and usability purposes only. The resource name is in the format +'projects/*/locations/*/caPools/*/certificateAuthorities/*'.`, + ExactlyOneOf: []string{"subordinate_config.0.certificate_authority", "subordinate_config.0.pem_issuer_chain"}, + }, + "pem_issuer_chain": { + Type: schema.TypeList, + Optional: true, + Description: `Contains the PEM certificate chain for the issuers of this CertificateAuthority, +but not pem certificate for this CA itself.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "pem_certificates": { + Type: schema.TypeList, + Optional: true, + Description: `Expected to be in leaf-to-root order according to RFC 5246.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + ExactlyOneOf: []string{"subordinate_config.0.certificate_authority", "subordinate_config.0.pem_issuer_chain"}, + }, + }, + }, + }, "type": { Type: schema.TypeString, Optional: true, @@ -558,8 +604,7 @@ fractional digits, terminated by 's'. Example: "3.5s".`, Description: `The Type of this CertificateAuthority. ~> **Note:** For 'SUBORDINATE' Certificate Authorities, they need to -be manually activated (via Cloud Console of 'gcloud') before they can -issue certificates. Default value: "SELF_SIGNED" Possible values: ["SELF_SIGNED", "SUBORDINATE"]`, +be activated before they can issue certificates. Default value: "SELF_SIGNED" Possible values: ["SELF_SIGNED", "SUBORDINATE"]`, Default: "SELF_SIGNED", }, "access_urls": { @@ -676,6 +721,12 @@ func resourcePrivatecaCertificateAuthorityCreate(d *schema.ResourceData, meta in } else if v, ok := d.GetOkExists("key_spec"); !isEmptyValue(reflect.ValueOf(keySpecProp)) && (ok || !reflect.DeepEqual(v, keySpecProp)) { obj["keySpec"] = keySpecProp } + subordinateConfigProp, err := expandPrivatecaCertificateAuthoritySubordinateConfig(d.Get("subordinate_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("subordinate_config"); !isEmptyValue(reflect.ValueOf(subordinateConfigProp)) && (ok || !reflect.DeepEqual(v, subordinateConfigProp)) { + obj["subordinateConfig"] = subordinateConfigProp + } gcsBucketProp, err := expandPrivatecaCertificateAuthorityGcsBucket(d.Get("gcs_bucket"), d, config) if err != nil { return err @@ -708,6 +759,9 @@ func resourcePrivatecaCertificateAuthorityCreate(d *schema.ResourceData, meta in billingProject = bp } + // Drop `subordinateConfig` as it can not be set during CA creation. + // It can be used to activate CA during post_create or pre_update. + delete(obj, "subordinateConfig") res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutCreate)) if err != nil { return fmt.Errorf("Error creating CertificateAuthority: %s", err) @@ -753,27 +807,24 @@ func resourcePrivatecaCertificateAuthorityCreate(d *schema.ResourceData, meta in staged := d.Get("type").(string) == "SELF_SIGNED" + if d.Get("type").(string) == "SUBORDINATE" { + if _, ok := d.GetOk("subordinate_config"); ok { + // First party issuer + log.Printf("[DEBUG] Activating CertificateAuthority with first party issuer") + if err := activateSubCAWithFirstPartyIssuer(config, d, project, billingProject, userAgent); err != nil { + return fmt.Errorf("Error activating subordinate CA with first party issuer: %v", err) + } + staged = true + log.Printf("[DEBUG] CertificateAuthority activated") + } + } + // Enable the CA if `desired_state` is unspecified or specified as `ENABLED`. if p, ok := d.GetOk("desired_state"); !ok || p.(string) == "ENABLED" { // Skip enablement on SUBORDINATE CA for backward compatible. if staged { - url, err = replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:enable") - if err != nil { - return err - } - - log.Printf("[DEBUG] Enabling CertificateAuthority: %#v", obj) - - res, err = sendRequest(config, "POST", billingProject, url, userAgent, nil) - if err != nil { - return fmt.Errorf("Error enabling CertificateAuthority: %s", err) - } - - err = privatecaOperationWaitTimeWithResponse( - config, res, &opRes, project, "Enabling CertificateAuthority", userAgent, - d.Timeout(schema.TimeoutCreate)) - if err != nil { - return fmt.Errorf("Error waiting to enable CertificateAuthority: %s", err) + if err := enableCA(config, d, project, billingProject, userAgent); err != nil { + return fmt.Errorf("Error enabling CertificateAuthority: %v", err) } } } @@ -850,6 +901,9 @@ func resourcePrivatecaCertificateAuthorityRead(d *schema.ResourceData, meta inte if err := d.Set("key_spec", flattenPrivatecaCertificateAuthorityKeySpec(res["keySpec"], d, config)); err != nil { return fmt.Errorf("Error reading CertificateAuthority: %s", err) } + if err := d.Set("subordinate_config", flattenPrivatecaCertificateAuthoritySubordinateConfig(res["subordinateConfig"], d, config)); err != nil { + return fmt.Errorf("Error reading CertificateAuthority: %s", err) + } if err := d.Set("state", flattenPrivatecaCertificateAuthorityState(res["state"], d, config)); err != nil { return fmt.Errorf("Error reading CertificateAuthority: %s", err) } @@ -891,6 +945,12 @@ func resourcePrivatecaCertificateAuthorityUpdate(d *schema.ResourceData, meta in billingProject = project obj := make(map[string]interface{}) + subordinateConfigProp, err := expandPrivatecaCertificateAuthoritySubordinateConfig(d.Get("subordinate_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("subordinate_config"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, subordinateConfigProp)) { + obj["subordinateConfig"] = subordinateConfigProp + } labelsProp, err := expandPrivatecaCertificateAuthorityLabels(d.Get("labels"), d, config) if err != nil { return err @@ -906,6 +966,10 @@ func resourcePrivatecaCertificateAuthorityUpdate(d *schema.ResourceData, meta in log.Printf("[DEBUG] Updating CertificateAuthority %q: %#v", d.Id(), obj) updateMask := []string{} + if d.HasChange("subordinate_config") { + updateMask = append(updateMask, "subordinateConfig") + } + if d.HasChange("labels") { updateMask = append(updateMask, "labels") } @@ -915,50 +979,43 @@ func resourcePrivatecaCertificateAuthorityUpdate(d *schema.ResourceData, meta in if err != nil { return err } + if d.HasChange("subordinate_config") { + if d.Get("type").(string) != "SUBORDINATE" { + return fmt.Errorf("`subordinate_config` can only be configured on subordinate CA") + } + + // Activate subordinate CA in `AWAITING_USER_ACTIVATION` state. + if d.Get("state") == "AWAITING_USER_ACTIVATION" { + if _, ok := d.GetOk("pem_ca_certificate"); ok { + // Third party issuer + log.Printf("[DEBUG] Activating CertificateAuthority with third party issuer") + if err := activateSubCAWithThirdPartyIssuer(config, d, project, billingProject, userAgent); err != nil { + return fmt.Errorf("Error activating subordinate CA with third party issuer: %v", err) + } + } else { + // First party issuer + log.Printf("[DEBUG] Activating CertificateAuthority with first party issuer") + if err := activateSubCAWithFirstPartyIssuer(config, d, project, billingProject, userAgent); err != nil { + return fmt.Errorf("Error activating subordinate CA with first party issuer: %v", err) + } + } + log.Printf("[DEBUG] CertificateAuthority activated") + } + } + + log.Printf("[DEBUG] checking desired_state") if d.HasChange("desired_state") { // Currently, most CA state update operations are not idempotent. // Try to change state only if the current `state` does not match the `desired_state`. if p, ok := d.GetOk("desired_state"); ok && p.(string) != d.Get("state").(string) { switch p.(string) { case "ENABLED": - enableUrl, err := replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:enable") - if err != nil { - return err - } - - log.Printf("[DEBUG] Enabling CA: %#v", obj) - - res, err := sendRequest(config, "POST", billingProject, enableUrl, userAgent, nil) - if err != nil { - return fmt.Errorf("Error enabling CA: %s", err) - } - - var opRes map[string]interface{} - err = privatecaOperationWaitTimeWithResponse( - config, res, &opRes, project, "Enabling CA", userAgent, - d.Timeout(schema.TimeoutCreate)) - if err != nil { - return fmt.Errorf("Error waiting to enable CA: %s", err) + if err := enableCA(config, d, project, billingProject, userAgent); err != nil { + return fmt.Errorf("Error enabling CertificateAuthority: %v", err) } case "DISABLED": - disableUrl, err := replaceVars(d, config, "{{PrivatecaBasePath}}projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificateAuthorities/{{certificate_authority_id}}:disable") - if err != nil { - return err - } - - log.Printf("[DEBUG] Disabling CA: %#v", obj) - - dRes, err := sendRequest(config, "POST", billingProject, disableUrl, userAgent, nil) - if err != nil { - return fmt.Errorf("Error disabling CA: %s", err) - } - - var opRes map[string]interface{} - err = privatecaOperationWaitTimeWithResponse( - config, dRes, &opRes, project, "Disabling CA", userAgent, - d.Timeout(schema.TimeoutDelete)) - if err != nil { - return fmt.Errorf("Error waiting to disable CA: %s", err) + if err := disableCA(config, d, project, billingProject, userAgent); err != nil { + return fmt.Errorf("Error disabling CertificateAuthority: %v", err) } default: return fmt.Errorf("Unsupported value in field `desired_state`") @@ -1266,6 +1323,42 @@ func flattenPrivatecaCertificateAuthorityKeySpecAlgorithm(v interface{}, d *sche return v } +func flattenPrivatecaCertificateAuthoritySubordinateConfig(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["certificate_authority"] = + flattenPrivatecaCertificateAuthoritySubordinateConfigCertificateAuthority(original["certificateAuthority"], d, config) + transformed["pem_issuer_chain"] = + flattenPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChain(original["pemIssuerChain"], d, config) + return []interface{}{transformed} +} +func flattenPrivatecaCertificateAuthoritySubordinateConfigCertificateAuthority(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChain(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["pem_certificates"] = + flattenPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChainPemCertificates(original["pemCertificates"], d, config) + return []interface{}{transformed} +} +func flattenPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChainPemCertificates(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + func flattenPrivatecaCertificateAuthorityState(v interface{}, d *schema.ResourceData, config *Config) interface{} { return v } @@ -1611,6 +1704,59 @@ func expandPrivatecaCertificateAuthorityKeySpecAlgorithm(v interface{}, d Terraf return v, nil } +func expandPrivatecaCertificateAuthoritySubordinateConfig(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedCertificateAuthority, err := expandPrivatecaCertificateAuthoritySubordinateConfigCertificateAuthority(original["certificate_authority"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedCertificateAuthority); val.IsValid() && !isEmptyValue(val) { + transformed["certificateAuthority"] = transformedCertificateAuthority + } + + transformedPemIssuerChain, err := expandPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChain(original["pem_issuer_chain"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPemIssuerChain); val.IsValid() && !isEmptyValue(val) { + transformed["pemIssuerChain"] = transformedPemIssuerChain + } + + return transformed, nil +} + +func expandPrivatecaCertificateAuthoritySubordinateConfigCertificateAuthority(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChain(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedPemCertificates, err := expandPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChainPemCertificates(original["pem_certificates"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPemCertificates); val.IsValid() && !isEmptyValue(val) { + transformed["pemCertificates"] = transformedPemCertificates + } + + return transformed, nil +} + +func expandPrivatecaCertificateAuthoritySubordinateConfigPemIssuerChainPemCertificates(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + func expandPrivatecaCertificateAuthorityGcsBucket(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { return v, nil } diff --git a/google-beta/resource_privateca_certificate_authority_generated_test.go b/google-beta/resource_privateca_certificate_authority_generated_test.go index 8c82b2532e..d3cf95f2b2 100644 --- a/google-beta/resource_privateca_certificate_authority_generated_test.go +++ b/google-beta/resource_privateca_certificate_authority_generated_test.go @@ -45,7 +45,7 @@ func TestAccPrivatecaCertificateAuthority_privatecaCertificateAuthorityBasicExam ResourceName: "google_privateca_certificate_authority.default", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"ignore_active_certificates_on_deletion", "location", "certificate_authority_id", "pool", "deletion_protection"}, + ImportStateVerifyIgnore: []string{"pem_ca_certificate", "ignore_active_certificates_on_deletion", "location", "certificate_authority_id", "pool", "deletion_protection"}, }, }, }) @@ -126,7 +126,7 @@ func TestAccPrivatecaCertificateAuthority_privatecaCertificateAuthoritySubordina ResourceName: "google_privateca_certificate_authority.default", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"ignore_active_certificates_on_deletion", "location", "certificate_authority_id", "pool", "deletion_protection"}, + ImportStateVerifyIgnore: []string{"pem_ca_certificate", "ignore_active_certificates_on_deletion", "location", "certificate_authority_id", "pool", "deletion_protection"}, }, }, }) @@ -134,13 +134,54 @@ func TestAccPrivatecaCertificateAuthority_privatecaCertificateAuthoritySubordina func testAccPrivatecaCertificateAuthority_privatecaCertificateAuthoritySubordinateExample(context map[string]interface{}) string { return Nprintf(` +resource "google_privateca_certificate_authority" "root-ca" { + pool = "%{pool_name}" + certificate_authority_id = "tf-test-my-certificate-authority%{random_suffix}-root" + location = "us-central1" + deletion_protection = false + ignore_active_certificates_on_deletion = true + config { + subject_config { + subject { + organization = "HashiCorp" + common_name = "my-certificate-authority" + } + subject_alt_name { + dns_names = ["hashicorp.com"] + } + } + x509_config { + ca_options { + # is_ca *MUST* be true for certificate authorities + is_ca = true + } + key_usage { + base_key_usage { + # cert_sign and crl_sign *MUST* be true for certificate authorities + cert_sign = true + crl_sign = true + } + extended_key_usage { + server_auth = false + } + } + } + } + key_spec { + algorithm = "RSA_PKCS1_4096_SHA256" + } +} + resource "google_privateca_certificate_authority" "default" { // This example assumes this pool already exists. // Pools cannot be deleted in normal test circumstances, so we depend on static pools pool = "%{pool_name}" - certificate_authority_id = "tf-test-my-certificate-authority%{random_suffix}" + certificate_authority_id = "tf-test-my-certificate-authority%{random_suffix}-sub" location = "%{pool_location}" deletion_protection = "%{deletion_protection}" + subordinate_config { + certificate_authority = google_privateca_certificate_authority.root-ca.name + } config { subject_config { subject { diff --git a/google-beta/resource_privateca_certificate_authority_test.go b/google-beta/resource_privateca_certificate_authority_test.go index 18cdc7f421..fb508a0633 100644 --- a/google-beta/resource_privateca_certificate_authority_test.go +++ b/google-beta/resource_privateca_certificate_authority_test.go @@ -85,6 +85,32 @@ func TestAccPrivatecaCertificateAuthority_subordinateCaCreatedInAwaitingUserActi }) } +func TestAccPrivatecaCertificateAuthority_subordinateCaActivatedByFirstPartyIssuerOnCreation(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "pool_name": BootstrapSharedCaPoolInLocation(t, "us-central1"), + "pool_location": "us-central1", + "deletion_protection": false, + "random_suffix": randString(t, 10), + } + + resourceName := "google_privateca_certificate_authority.default" + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPrivatecaCertificateAuthorityDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccPrivatecaCertificateAuthority_privatecaCertificateAuthoritySubordinateWithFirstPartyIssuer(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + ), + }, + }, + }) +} + func TestAccPrivatecaCertificateAuthority_privatecaCertificateAuthorityUpdate(t *testing.T) { t.Parallel() @@ -409,3 +435,114 @@ resource "google_privateca_certificate_authority" "default" { } `, context) } + +// testAccPrivatecaCertificateAuthority_privatecaCertificateAuthoritySubordinateWithFirstPartyIssuer provides a config +// which contains +// * A root CA +// * A subordinate CA which should be activated by the above root CA +func testAccPrivatecaCertificateAuthority_privatecaCertificateAuthoritySubordinateWithFirstPartyIssuer(context map[string]interface{}) string { + return Nprintf(` +resource "google_privateca_certificate_authority" "root-1" { + // This example assumes this pool already exists. + // Pools cannot be deleted in normal test circumstances, so we depend on static pools + pool = "%{pool_name}" + certificate_authority_id = "tf-test-my-certificate-authority-root-%{random_suffix}" + location = "%{pool_location}" + deletion_protection = false + ignore_active_certificates_on_deletion = true + config { + subject_config { + subject { + organization = "HashiCorp" + common_name = "my-certificate-authority" + } + subject_alt_name { + dns_names = ["hashicorp.com"] + } + } + x509_config { + ca_options { + is_ca = true + max_issuer_path_length = 10 + } + key_usage { + base_key_usage { + digital_signature = true + content_commitment = true + key_encipherment = false + data_encipherment = true + key_agreement = true + cert_sign = true + crl_sign = true + decipher_only = true + } + extended_key_usage { + server_auth = true + client_auth = false + email_protection = true + code_signing = true + time_stamping = true + } + } + } + } + lifetime = "86400s" + key_spec { + algorithm = "RSA_PKCS1_4096_SHA256" + } +} + +resource "google_privateca_certificate_authority" "default" { + // This example assumes this pool already exists. + // Pools cannot be deleted in normal test circumstances, so we depend on static pools + pool = "%{pool_name}" + certificate_authority_id = "tf-test-my-certificate-authority-sub-%{random_suffix}" + location = "%{pool_location}" + deletion_protection = false + subordinate_config { + certificate_authority = google_privateca_certificate_authority.root-1.name + } + config { + subject_config { + subject { + organization = "HashiCorp" + common_name = "my-certificate-authority" + } + subject_alt_name { + dns_names = ["hashicorp.com"] + } + } + x509_config { + ca_options { + is_ca = true + max_issuer_path_length = 10 + } + key_usage { + base_key_usage { + digital_signature = true + content_commitment = true + key_encipherment = false + data_encipherment = true + key_agreement = true + cert_sign = true + crl_sign = true + decipher_only = true + } + extended_key_usage { + server_auth = true + client_auth = false + email_protection = true + code_signing = true + time_stamping = true + } + } + } + } + lifetime = "86400s" + key_spec { + algorithm = "RSA_PKCS1_4096_SHA256" + } + type = "SUBORDINATE" +} +`, context) +} diff --git a/website/docs/r/privateca_certificate.html.markdown b/website/docs/r/privateca_certificate.html.markdown index 1992cd8f8a..ecd377bf76 100644 --- a/website/docs/r/privateca_certificate.html.markdown +++ b/website/docs/r/privateca_certificate.html.markdown @@ -429,7 +429,10 @@ The following arguments are supported: * `certificate_authority` - (Optional) - Certificate Authority name. + The Certificate Authority ID that should issue the certificate. For example, to issue a Certificate from + a Certificate Authority with resource name `projects/my-project/locations/us-central1/caPools/my-pool/certificateAuthorities/my-ca`, + argument `pool` should be set to `projects/my-project/locations/us-central1/caPools/my-pool`, argument `certificate_authority` + should be set to `my-ca`. * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. @@ -703,7 +706,7 @@ In addition to the arguments listed above, the following computed attributes are * `id` - an identifier for the resource with format `projects/{{project}}/locations/{{location}}/caPools/{{pool}}/certificates/{{name}}` * `issuer_certificate_authority` - - The resource name of the issuing CertificateAuthority in the format projects/*/locations/*/caPools/*/certificateAuthorities/*. + The resource name of the issuing CertificateAuthority in the format `projects/*/locations/*/caPools/*/certificateAuthorities/*`. * `revocation_details` - Output only. Details regarding the revocation of this Certificate. This Certificate is diff --git a/website/docs/r/privateca_certificate_authority.html.markdown b/website/docs/r/privateca_certificate_authority.html.markdown index 0d06a03774..01b924608c 100644 --- a/website/docs/r/privateca_certificate_authority.html.markdown +++ b/website/docs/r/privateca_certificate_authority.html.markdown @@ -103,13 +103,54 @@ resource "google_privateca_certificate_authority" "default" { ```hcl +resource "google_privateca_certificate_authority" "root-ca" { + pool = "ca-pool" + certificate_authority_id = "my-certificate-authority-root" + location = "us-central1" + deletion_protection = false + ignore_active_certificates_on_deletion = true + config { + subject_config { + subject { + organization = "HashiCorp" + common_name = "my-certificate-authority" + } + subject_alt_name { + dns_names = ["hashicorp.com"] + } + } + x509_config { + ca_options { + # is_ca *MUST* be true for certificate authorities + is_ca = true + } + key_usage { + base_key_usage { + # cert_sign and crl_sign *MUST* be true for certificate authorities + cert_sign = true + crl_sign = true + } + extended_key_usage { + server_auth = false + } + } + } + } + key_spec { + algorithm = "RSA_PKCS1_4096_SHA256" + } +} + resource "google_privateca_certificate_authority" "default" { // This example assumes this pool already exists. // Pools cannot be deleted in normal test circumstances, so we depend on static pools pool = "ca-pool" - certificate_authority_id = "my-certificate-authority" + certificate_authority_id = "my-certificate-authority-sub" location = "us-central1" deletion_protection = "true" + subordinate_config { + certificate_authority = google_privateca_certificate_authority.root-ca.name + } config { subject_config { subject { @@ -515,6 +556,10 @@ The following arguments are supported: - - - +* `pem_ca_certificate` - + (Optional) + The signed CA certificate issued from the subordinated CA's CSR. This is needed when activating the subordiante CA with a third party issuer. + * `ignore_active_certificates_on_deletion` - (Optional) This field allows the CA to be deleted even if the CA has active certs. Active certs include both unrevoked and unexpired certs. @@ -524,8 +569,7 @@ The following arguments are supported: (Optional) The Type of this CertificateAuthority. ~> **Note:** For `SUBORDINATE` Certificate Authorities, they need to - be manually activated (via Cloud Console of `gcloud`) before they can - issue certificates. + be activated before they can issue certificates. Default value is `SELF_SIGNED`. Possible values are `SELF_SIGNED` and `SUBORDINATE`. @@ -535,6 +579,12 @@ The following arguments are supported: "notAfterTime" fields inside an X.509 certificate. A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". +* `subordinate_config` - + (Optional) + If this is a subordinate CertificateAuthority, this field will be set + with the subordinate configuration, which describes its issuers. + Structure is [documented below](#nested_subordinate_config). + * `gcs_bucket` - (Optional) The name of a Cloud Storage bucket where this CertificateAuthority will publish content, @@ -558,6 +608,28 @@ in Terraform state, a `terraform destroy` or `terraform apply` that would delete * `desired_state` - (Optional) Desired state of the CertificateAuthority. Set this field to `STAGED` to create a `STAGED` root CA. +The `subordinate_config` block supports: + +* `certificate_authority` - + (Optional) + This can refer to a CertificateAuthority that was used to create a + subordinate CertificateAuthority. This field is used for information + and usability purposes only. The resource name is in the format + `projects/*/locations/*/caPools/*/certificateAuthorities/*`. + +* `pem_issuer_chain` - + (Optional) + Contains the PEM certificate chain for the issuers of this CertificateAuthority, + but not pem certificate for this CA itself. + Structure is [documented below](#nested_pem_issuer_chain). + + +The `pem_issuer_chain` block supports: + +* `pem_certificates` - + (Optional) + Expected to be in leaf-to-root order according to RFC 5246. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are exported: