diff --git a/internal/provider/data_source_certificate.go b/internal/provider/data_source_certificate.go index 1f01b10f..d5b514ab 100644 --- a/internal/provider/data_source_certificate.go +++ b/internal/provider/data_source_certificate.go @@ -3,6 +3,7 @@ package provider import ( "crypto/tls" "crypto/x509" + "encoding/pem" "fmt" "net/http" "net/url" @@ -24,18 +25,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())), + ConflictsWith: []string{"content"}, + }, + "content": { + Type: schema.TypeString, + Optional: true, + Description: "The content of the PEM encoded certificate", + ConflictsWith: []string{"url", "verify_chain"}, }, "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, @@ -115,51 +124,69 @@ func dataSourceCertificate() *schema.Resource { func dataSourceCertificateRead(d *schema.ResourceData, m interface{}) error { config := m.(*providerConfig) - targetURL, err := url.Parse(d.Get("url").(string)) - if err != nil { - return 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" + if v, ok := d.GetOk("content"); ok { + block, _ := pem.Decode([]byte(v.(string))) + if block == nil { + return fmt.Errorf("failed to decode pem content") + } + if block.Type != "CERTIFICATE" { + return fmt.Errorf("pem must be of type 'CERTIFICATE'") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("unable to parse the certificate %w", err) + } + err = d.Set("certificates", []interface{}{certificateToMap(cert)}) + if err != nil { + return err + } + } else { + targetURL, err := url.Parse(d.Get("url").(string)) + if err != nil { + return 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 { + // 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 fmt.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 fmt.Errorf("unsupported scheme: %s", targetURL.Scheme) } - case TLSScheme.String(): - if targetURL.Port() == "" { - return fmt.Errorf("port missing from URL: %s", targetURL.String()) + if err != nil { + return err } - peerCerts, err = fetchPeerCertificatesViaTLS(targetURL, shouldVerifyChain) - default: - // NOTE: This should never happen, given we validate this at the schema level - return fmt.Errorf("unsupported scheme: %s", targetURL.Scheme) - } - if err != nil { - return 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) - } - err = d.Set("certificates", certs) - if err != nil { - return 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) + } + err = d.Set("certificates", certs) + if err != nil { + return err + } } d.SetId(time.Now().UTC().String()) diff --git a/internal/provider/data_source_certificate_test.go b/internal/provider/data_source_certificate_test.go index 91433a87..89b3356b 100644 --- a/internal/provider/data_source_certificate_test.go +++ b/internal/provider/data_source_certificate_test.go @@ -8,6 +8,53 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func TestAccDataSourceCertificate_CertificateContent(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + Providers: 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"), + ), + }, + { + 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'"), + }, + }, + }) +} + 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