-
Notifications
You must be signed in to change notification settings - Fork 120
Expand file tree
/
Copy pathdata_source_certificate.go
More file actions
210 lines (190 loc) · 6.88 KB
/
data_source_certificate.go
File metadata and controls
210 lines (190 loc) · 6.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
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 {
return &schema.Resource{
Read: dataSourceCertificateRead,
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 a URL.",
Schema: map[string]*schema.Schema{
"url": {
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,
Optional: true,
Default: true,
Description: "Whether to verify the certificate chain while parsing it or not (default: `true`).",
},
"certificates": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"signature_algorithm": {
Type: schema.TypeString,
Computed: true,
Description: "The algorithm used to sign the certificate.",
},
"public_key_algorithm": {
Type: schema.TypeString,
Computed: true,
Description: "The key algorithm used to create the certificate.",
},
"serial_number": {
Type: schema.TypeString,
Computed: true,
Description: "Number that uniquely identifies the certificate with the CA's system. " +
"The `format` function can be used to convert this _base 10_ number " +
"into other bases, such as hex.",
},
"is_ca": {
Type: schema.TypeBool,
Computed: true,
Description: "`true` if the certificate is of a CA (Certificate Authority).",
},
"version": {
Type: schema.TypeInt,
Computed: true,
Description: "The version the certificate is in.",
},
"issuer": {
Type: schema.TypeString,
Computed: true,
Description: "Who verified and signed the certificate, roughly following " +
"[RFC2253](https://tools.ietf.org/html/rfc2253).",
},
"subject": {
Type: schema.TypeString,
Computed: true,
Description: "The entity the certificate belongs to, roughly following " +
"[RFC2253](https://tools.ietf.org/html/rfc2253).",
},
"not_before": {
Type: schema.TypeString,
Computed: true,
Description: "The time after which the certificate is valid, as an " +
"[RFC3339](https://tools.ietf.org/html/rfc3339) timestamp.",
},
"not_after": {
Type: schema.TypeString,
Computed: true,
Description: "The time until which the certificate is invalid, as an " +
"[RFC3339](https://tools.ietf.org/html/rfc3339) timestamp.",
},
"sha1_fingerprint": {
Type: schema.TypeString,
Computed: true,
Description: "The SHA1 fingerprint of the public key of the certificate.",
},
},
},
Description: "The certificates protecting the site, with the root of the chain first.",
},
"id": {
Type: schema.TypeString,
Computed: true,
Description: "Unique identifier of this data source: " +
"randomly generated string (UTC time when data source was read).",
},
},
}
}
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"
}
// 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)
}
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())
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
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)
}