diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e622ee8..f98580ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 3.4.0 (UNRELEASED) + +NEW FEATURES: + +* data-source/tls_certificate: Support for `content`. When used, the provider will read the PEM formatted data directly. ([#189](https://github.com/hashicorp/terraform-provider-tls/pull/189)). + +NOTES: + +* data-source/tls_certificate: The `id` attribute has changed to the hashing of all certificates information in the chain. The first apply of this updated data source may show this difference ([#189](https://github.com/hashicorp/terraform-provider-tls/pull/189)). + +BUG FIXES: + +* data-source/tls_certificate: Prevent plan differences with the `id` attribute ([#79](https://github.com/hashicorp/terraform-provider-tls/issues/79), [#189](https://github.com/hashicorp/terraform-provider-tls/pull/189)). + ## 3.3.0 (April 07, 2022) NEW FEATURES: diff --git a/docs/data-sources/certificate.md b/docs/data-sources/certificate.md index 99053fdb..a5bb98fb 100644 --- a/docs/data-sources/certificate.md +++ b/docs/data-sources/certificate.md @@ -14,6 +14,7 @@ Use this data source to get information, such as SHA1 fingerprint or serial numb ## Example Usage +### URL Usage ```terraform resource "aws_eks_cluster" "example" { name = "example" @@ -30,6 +31,13 @@ resource "aws_iam_openid_connect_provider" "example" { } ``` +### Content Usage +```terraform +data "tls_certificate" "example_content" { + content = file("example.pem") +} +``` + ## Schema -### Required - -- `url` (String) The URL of the website to get the certificates from. - ### Optional -- `verify_chain` (Boolean) Whether to verify the certificate chain while parsing it or not (default: `true`). +- `url` (String) The URL of the website to get the certificates from. Cannot be used with `content`. +- `content` (String) The content of the certificate in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format. Cannot be used with `url`. +- `verify_chain` (Boolean) Whether to verify the certificate chain while parsing it or not (default: `true`). Cannot be used with `content`. ### Read-Only diff --git a/examples/data-sources/tls_certificate/content-example.tf b/examples/data-sources/tls_certificate/content-example.tf new file mode 100644 index 00000000..755390d6 --- /dev/null +++ b/examples/data-sources/tls_certificate/content-example.tf @@ -0,0 +1,3 @@ +data "tls_certificate" "example_content" { + content = file("example.pem") +} \ No newline at end of file diff --git a/internal/provider/data_source_certificate.go b/internal/provider/data_source_certificate.go index d4abd189..5bd44e56 100644 --- a/internal/provider/data_source_certificate.go +++ b/internal/provider/data_source_certificate.go @@ -4,11 +4,11 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/pem" "fmt" "net/http" "net/url" "strings" - "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -26,18 +26,26 @@ func dataSourceCertificate() *schema.Resource { Schema: map[string]*schema.Schema{ "url": { Type: schema.TypeString, - Required: true, + Optional: true, Description: "URL of the endpoint to get the certificates from. " + fmt.Sprintf("Accepted schemes are: `%s`. ", strings.Join(SupportedURLSchemesStr(), "`, `")) + "For scheme `https://` it will use the HTTP protocol and apply the `proxy` configuration " + "of the provider, if set. For scheme `tls://` it will instead use a secure TCP socket.", ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithScheme(SupportedURLSchemesStr())), + ExactlyOneOf: []string{"content", "url"}, + }, + "content": { + Type: schema.TypeString, + Optional: true, + Description: "The content of the certificate in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format.", + ExactlyOneOf: []string{"content", "url"}, }, "verify_chain": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Whether to verify the certificate chain while parsing it or not (default: `true`).", + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether to verify the certificate chain while parsing it or not (default: `true`).", + ConflictsWith: []string{"content"}, }, "certificates": { Type: schema.TypeList, @@ -114,57 +122,83 @@ func dataSourceCertificate() *schema.Resource { } } -func dataSourceCertificateRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func dataSourceCertificateRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { config := m.(*providerConfig) - targetURL, err := url.Parse(d.Get("url").(string)) - if err != nil { - return diag.FromErr(err) - } + var certs []interface{} - // Determine if we should verify the chain of certificates, or skip said verification - shouldVerifyChain := d.Get("verify_chain").(bool) + if v, ok := d.GetOk("content"); ok { + block, _ := pem.Decode([]byte(v.(string))) + if block == nil { + return diag.Errorf("failed to decode pem content") + } + + preamble, err := PEMBlockToPEMPreamble(block) + if err != nil { + return diag.FromErr(err) + } + + if preamble != PreambleCertificate { + return diag.Errorf("PEM must be of type 'CERTIFICATE'") + } - // Ensure a port is set on the URL, or return an error - var peerCerts []*x509.Certificate - switch targetURL.Scheme { - case HTTPSScheme.String(): - if targetURL.Port() == "" { - targetURL.Host += ":443" + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return diag.Errorf("unable to parse the certificate %v", err) } - // TODO remove this branch and default to use `fetchPeerCertificatesViaHTTPS` - // as part of https://github.com/hashicorp/terraform-provider-tls/issues/183 - if config.isProxyConfigured() { - peerCerts, err = fetchPeerCertificatesViaHTTPS(targetURL, shouldVerifyChain, config) - } else { + certs = []interface{}{certificateToMap(cert)} + } else { + targetURL, err := url.Parse(d.Get("url").(string)) + if err != nil { + return diag.FromErr(err) + } + + // Determine if we should verify the chain of certificates, or skip said verification + shouldVerifyChain := d.Get("verify_chain").(bool) + + // Ensure a port is set on the URL, or return an error + var peerCerts []*x509.Certificate + switch targetURL.Scheme { + case HTTPSScheme.String(): + if targetURL.Port() == "" { + targetURL.Host += ":443" + } + + // TODO remove this branch and default to use `fetchPeerCertificatesViaHTTPS` + // as part of https://github.com/hashicorp/terraform-provider-tls/issues/183 + if config.isProxyConfigured() { + peerCerts, err = fetchPeerCertificatesViaHTTPS(targetURL, shouldVerifyChain, config) + } else { + peerCerts, err = fetchPeerCertificatesViaTLS(targetURL, shouldVerifyChain) + } + case TLSScheme.String(): + if targetURL.Port() == "" { + return diag.Errorf("port missing from URL: %s", targetURL.String()) + } + peerCerts, err = fetchPeerCertificatesViaTLS(targetURL, shouldVerifyChain) + default: + // NOTE: This should never happen, given we validate this at the schema level + return diag.Errorf("unsupported scheme: %s", targetURL.Scheme) } - case TLSScheme.String(): - if targetURL.Port() == "" { - return diag.Errorf("port missing from URL: %s", targetURL.String()) + if err != nil { + return diag.FromErr(err) } - peerCerts, err = fetchPeerCertificatesViaTLS(targetURL, shouldVerifyChain) - default: - // NOTE: This should never happen, given we validate this at the schema level - return diag.Errorf("unsupported scheme: %s", targetURL.Scheme) - } - if err != nil { - return diag.FromErr(err) + // Convert peer certificates to a simple map + certs = make([]interface{}, len(peerCerts)) + for i, peerCert := range peerCerts { + certs[len(peerCerts)-i-1] = certificateToMap(peerCert) + } } - // Convert peer certificates to a simple map - certs := make([]interface{}, len(peerCerts)) - for i, peerCert := range peerCerts { - certs[len(peerCerts)-i-1] = certificateToMap(peerCert) - } - err = d.Set("certificates", certs) + err := d.Set("certificates", certs) if err != nil { return diag.FromErr(err) } - d.SetId(time.Now().UTC().String()) + d.SetId(hashForState(fmt.Sprintf("%v", certs))) return nil } diff --git a/internal/provider/data_source_certificate_test.go b/internal/provider/data_source_certificate_test.go index a9ec7ad7..27da2418 100644 --- a/internal/provider/data_source_certificate_test.go +++ b/internal/provider/data_source_certificate_test.go @@ -8,6 +8,76 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func TestAccDataSourceCertificate_CertificateContent(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProviderFactories: testProviders, + + Steps: []resource.TestStep{ + { + Config: ` + data "tls_certificate" "test" { + content = file("testdata/tls_certs/certificate.pem") + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.#", "1"), + + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.signature_algorithm", "SHA256-RSA"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.public_key_algorithm", "RSA"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.serial_number", "266244246501122064554217434340898012243"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.is_ca", "false"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.version", "3"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.issuer", "CN=Root CA,O=Test Org,L=Here"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.subject", "CN=Child Cert,O=Child Co.,L=Everywhere"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.not_before", "2019-11-08T09:01:36Z"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.not_after", "2019-11-08T19:01:36Z"), + resource.TestCheckResourceAttr("data.tls_certificate.test", "certificates.0.sha1_fingerprint", "61b65624427d75b61169100836904e44364df817"), + ), + }, + }, + }) +} + +func TestAccDataSourceCertificate_CertificateContentNegativeTests(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProviderFactories: testProviders, + + Steps: []resource.TestStep{ + { + Config: ` + data "tls_certificate" "test" { + content = "not a pem" + } + `, + ExpectError: regexp.MustCompile("failed to decode pem content"), + }, + { + Config: ` + data "tls_certificate" "test" { + content = file("testdata/tls_certs/private.pem") + } + `, + ExpectError: regexp.MustCompile("PEM must be of type 'CERTIFICATE'"), + }, + { + Config: ` + data "tls_certificate" "test" { + content = file("testdata/tls_certs/private.pem") + url = "https://www.hashicorp.com" + } + `, + ExpectError: regexp.MustCompile("\"content\": only one of `content,url` can be specified, but `content,url` were\nspecified"), + }, + { + Config: ` + data "tls_certificate" "test" {} + `, + ExpectError: regexp.MustCompile("\"url\": one of `content,url` must be specified"), + }, + }, + }) +} + func TestAccDataSourceCertificate_HTTPSScheme(t *testing.T) { server, err := newHTTPServer() if err != nil { diff --git a/internal/provider/testdata/tls_certs/certificate.pem b/internal/provider/testdata/tls_certs/certificate.pem new file mode 100644 index 00000000..84432ce1 --- /dev/null +++ b/internal/provider/testdata/tls_certs/certificate.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIRAMhMxtTgTXHTmo6ZU7OafFMwDQYJKoZIhvcNAQELBQAw +NDENMAsGA1UEBxMESGVyZTERMA8GA1UEChMIVGVzdCBPcmcxEDAOBgNVBAMTB1Jv +b3QgQ0EwHhcNMTkxMTA4MDkwMTM2WhcNMTkxMTA4MTkwMTM2WjA+MRMwEQYDVQQH +EwpFdmVyeXdoZXJlMRIwEAYDVQQKEwlDaGlsZCBDby4xEzARBgNVBAMTCkNoaWxk +IENlcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQTeCu466xxnGr +CCrl823J4gGnp9AYb0laTP3uB4orXblTFq45ehDnEJXNykT+7acT8IrAjQlVQdl0 +gLjNM6XjGkFQ7xRw5xi041vRrOtUzC1KxVqrcfT4WrKj6zM/MuK3hznc4NvvwdAx +Mb3Sk46yQ1PrMslsidDvhTAqXkVi3lD1bV/bpnDo3NRCldVpedE1wlR+6thXZN/Y +MggNuDdv6LDadVGlXgKw5KkEIgenGOzpX1o+GKGo5UWu1xoTHikVwEC1iVuCZax+ +9FnHQO/q7SyF4Lb9d0j6vzrIAjzauGbiAsJya1GhYMF7INxzpSolzk0UYjT5Dxcq +d3VX1prxAgMBAAGjVjBUMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEF +BQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFIBRoM9+w7/obXaqAmaCPyVf +ldxEMA0GCSqGSIb3DQEBCwUAA4IBAQCuXJkT+qD3STmyDlsJOQRLBKaECH+/0mw4 +mn3oMikNfneybjhao+fpwTgFup3KIrdIgbuciHfSTZzWT6mDs9bUdZZLccU6cVRh +WiX0I1eppjQyOT7PuXDsOsBUMf+et5WuGYrtKsib07q2rHPtTq72iftANtWbznfq +DsM3TQL4LuEE9V2lU2L2f3kXKrkYzLJj7R4sGck5Fo/E8eeIFm1Z5FCPcia82N+C +xDsNFvV3r8TsRH60IxFekKddI+ivepa97SvC4r+69MPyxULHNwDtSL+8T4q01LEP +VKT7dWjBK3K0xxH0SPCtlqRbGalWz4adNNHazN/x7ebK+WB9ReSM +-----END CERTIFICATE----- \ No newline at end of file diff --git a/templates/data-sources/certificate.md.tmpl b/templates/data-sources/certificate.md.tmpl index 3eaf9d18..b2449f90 100644 --- a/templates/data-sources/certificate.md.tmpl +++ b/templates/data-sources/certificate.md.tmpl @@ -11,8 +11,12 @@ description: |- ## Example Usage +### URL Usage {{ tffile "examples/data-sources/tls_certificate/data-source.tf" }} +### Content Usage +{{ tffile "examples/data-sources/tls_certificate/content-example.tf" }} + ## Schema -### Required - -- `url` (String) The URL of the website to get the certificates from. - ### Optional -- `verify_chain` (Boolean) Whether to verify the certificate chain while parsing it or not (default: `true`). +- `url` (String) The URL of the website to get the certificates from. Cannot be used with `content`. +- `content` (String) The content of the certificate in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format. Cannot be used with `url`. +- `verify_chain` (Boolean) Whether to verify the certificate chain while parsing it or not (default: `true`). Cannot be used with `content`. ### Read-Only