Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
4 changes: 2 additions & 2 deletions docs/data-sources/certificate.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ page_title: "tls_certificate Data Source - terraform-provider-tls"
subcategory: ""
description: |-
Get information about the TLS certificates securing a host.
Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects an HTTPS website.
Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects a URL.
---

# tls_certificate (Data Source)

Get information about the TLS certificates securing a host.

Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects an HTTPS website.
Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects a URL.

## Example Usage

Expand Down
51 changes: 51 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,57 @@ resource "aws_iam_server_certificate" "example" {
}
```

### Configuring Proxy

```terraform
# This example fetches the TLS certificate chain
# from `example.com` using an HTTP Proxy.

provider "tls" {
proxy {
url = "https://corporate.proxy.service"
}
}

data "tls_certificate" "test" {
url = "https://example.com"
}
```

```terraform
# This example fetches the TLS certificate chain
# from `example.com` using an HTTP Proxy.
# The Proxy is discovered via environment variables:
# see https://pkg.go.dev/net/http#ProxyFromEnvironment for details.

provider "tls" {
proxy {
from_env = true
}
}

data "tls_certificate" "test" {
url = "https://example.com"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `proxy` (Block List, Max: 1) Proxy used by resources and data sources that connect to external endpoints. (see [below for nested schema](#nestedblock--proxy))

<a id="nestedblock--proxy"></a>
### Nested Schema for `proxy`

Optional:

- `from_env` (Boolean) When `true` the provider will discover the proxy configuration from environment variables. This is based upon [`http.ProxyFromEnvironment`](https://pkg.go.dev/net/http#ProxyFromEnvironment) and it supports the same environment variables (default: `false`). **NOTE**: the default value for this argument will be change to `true` in the next major release.
- `password` (String, Sensitive) Password used for Basic authentication against the Proxy.
- `url` (String) URL used to connect to the Proxy. Accepted schemes are: `http`, `https`, `socks5`.
- `username` (String) Username (or Token) used for Basic authentication against the Proxy.

## Limitations

### `ECDSA` with `P224` elliptic curve
Expand Down
12 changes: 12 additions & 0 deletions examples/provider/provider_with_proxy.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This example fetches the TLS certificate chain
# from `example.com` using an HTTP Proxy.

provider "tls" {
proxy {
url = "https://corporate.proxy.service"
}
}

data "tls_certificate" "test" {
url = "https://example.com"
}
14 changes: 14 additions & 0 deletions examples/provider/provider_with_proxy_from_env.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This example fetches the TLS certificate chain
# from `example.com` using an HTTP Proxy.
# The Proxy is discovered via environment variables:
# see https://pkg.go.dev/net/http#ProxyFromEnvironment for details.

provider "tls" {
proxy {
from_env = true
}
}

data "tls_certificate" "test" {
url = "https://example.com"
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module github.com/terraform-providers/terraform-provider-tls
go 1.17

require (
github.com/elazarl/goproxy v0.0.0-20220328115640-894aeddb713e
Comment thread
detro marked this conversation as resolved.
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
github.com/hashicorp/terraform-plugin-docs v0.7.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.13.0
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20220328115640-894aeddb713e h1:99KFda6F/mw8xSfceY2JEVCrYWX7l+Ms6BcO5wEct+Q=
github.com/elazarl/goproxy v0.0.0-20220328115640-894aeddb713e/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -215,6 +219,7 @@ github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
Expand Down
109 changes: 89 additions & 20 deletions internal/provider/data_source_certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package provider

import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

func dataSourceCertificate() *schema.Resource {
Expand All @@ -15,13 +19,17 @@ func dataSourceCertificate() *schema.Resource {

Description: "Get information about the TLS certificates securing a host.\n\n" +
"Use this data source to get information, such as SHA1 fingerprint or serial number, " +
"about the TLS certificates that protects an HTTPS website.",
"about the TLS certificates that protects a URL.",

Schema: map[string]*schema.Schema{
"url": {
Type: schema.TypeString,
Required: true,
Description: "The URL of the website to get the certificates from.",
Type: schema.TypeString,
Required: 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())),
},
"verify_chain": {
Type: schema.TypeBool,
Expand Down Expand Up @@ -104,32 +112,51 @@ func dataSourceCertificate() *schema.Resource {
}
}

func dataSourceCertificateRead(d *schema.ResourceData, _ interface{}) error {
u, err := url.Parse(d.Get("url").(string))
func dataSourceCertificateRead(d *schema.ResourceData, m interface{}) error {
config := m.(*providerConfig)

targetURL, err := url.Parse(d.Get("url").(string))
if err != nil {
return err
}
if u.Scheme != "https" {
return fmt.Errorf("invalid scheme")
}
if u.Port() == "" {
u.Host += ":443"
}

verifyChain := d.Get("verify_chain").(bool)
// Determine if we should verify the chain of certificates, or skip said verification
shouldVerifyChain := d.Get("verify_chain").(bool)

conn, err := tls.Dial("tcp", u.Host, &tls.Config{InsecureSkipVerify: !verifyChain})
// 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() {
Comment on lines +134 to +136
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I still think the next major version should still perform this type of check and use the TLS client whenever possible for simplicity -- the check could see if http.ProxyFromEnvironment would actually use a proxy for the request by checking for nil.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason I insist on using an actual HTTP client, is that we were and are requiring a scheme for the url argument. The fact that practitioners have to write https:// is, in my opinion, an implicit implication we are making.

The original implementation simply didn't respect that. It would have made more sense if there was no scheme, and then it only (and exclusively) used tls.Client. Though that would have prevented even talking about supporting HTTP Proxies.

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)
}
if err != nil {
return err
}
defer conn.Close()
state := conn.ConnectionState()

var certs []interface{}
for i := len(state.PeerCertificates) - 1; i >= 0; i-- {
certs = append(certs, certificateToMap(state.PeerCertificates[i]))
// 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
Expand All @@ -139,3 +166,45 @@ func dataSourceCertificateRead(d *schema.ResourceData, _ interface{}) error {

return nil
}

func fetchPeerCertificatesViaTLS(targetURL *url.URL, shouldVerifyChain bool) ([]*x509.Certificate, error) {
conn, err := tls.Dial("tcp", targetURL.Host, &tls.Config{
InsecureSkipVerify: !shouldVerifyChain,
})
if err != nil {
return nil, fmt.Errorf("unable to execute TLS connection towards %s: %w", targetURL.Host, err)
}
defer conn.Close()

return conn.ConnectionState().PeerCertificates, nil
}

func fetchPeerCertificatesViaHTTPS(targetURL *url.URL, shouldVerifyChain bool, config *providerConfig) ([]*x509.Certificate, error) {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: !shouldVerifyChain,
},
Proxy: config.proxyForRequestFunc(),
},
}

// Fist attempting an HTTP HEAD: if it fails, ignore errors and move on
Comment thread
detro marked this conversation as resolved.
Outdated
resp, err := client.Head(targetURL.String())
if err == nil && resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
defer resp.Body.Close()
return resp.TLS.PeerCertificates, nil
}

// Then attempting HTTP GET: if this fails we will than report the error
resp, err = client.Get(targetURL.String())
if err != nil {
return nil, fmt.Errorf("failed to fetch certificates from URL '%s': %w", targetURL.Scheme, err)
}
defer resp.Body.Close()
if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
return resp.TLS.PeerCertificates, nil
}

return nil, fmt.Errorf("got back response (status: %s) with no certificates from URL '%s': %w", resp.Status, targetURL.Scheme, err)
}
Loading