Skip to content

Commit

Permalink
support parsing certificate content #4 (#189)
Browse files Browse the repository at this point in the history
* Closes #139 support parsing certificate content #4

* Using hash of certificates as ID for 'tls_certificate' data source

* Update CHANGELOG.md

* Adding BUGFIX + NOTES to the CHANGELOG as we are now fixing issue #79.

Co-authored-by: Ivan De Marino <ivan.demarino@hashicorp.com>
  • Loading branch information
iwarapter and Ivan De Marino authored May 5, 2022
1 parent 226825c commit cd2e3a4
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 51 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
16 changes: 11 additions & 5 deletions docs/data-sources/certificate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -30,6 +31,13 @@ resource "aws_iam_openid_connect_provider" "example" {
}
```

### Content Usage
```terraform
data "tls_certificate" "example_content" {
content = file("example.pem")
}
```

<!--
Schema ORIGINALLY generated by tfplugindocs,
then manually tweaked to circumvent current limitations.
Expand All @@ -38,13 +46,11 @@ resource "aws_iam_openid_connect_provider" "example" {
-->
## 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

Expand Down
3 changes: 3 additions & 0 deletions examples/data-sources/tls_certificate/content-example.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data "tls_certificate" "example_content" {
content = file("example.pem")
}
116 changes: 75 additions & 41 deletions internal/provider/data_source_certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
70 changes: 70 additions & 0 deletions internal/provider/data_source_certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions internal/provider/testdata/tls_certs/certificate.pem
Original file line number Diff line number Diff line change
@@ -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-----
12 changes: 7 additions & 5 deletions templates/data-sources/certificate.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 ORIGINALLY generated by tfplugindocs,
then manually tweaked to circumvent current limitations.
Expand All @@ -21,13 +25,11 @@ description: |-
-->
## 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

Expand Down

0 comments on commit cd2e3a4

Please sign in to comment.