@@ -5,33 +5,35 @@ import (
5
5
"crypto/tls"
6
6
"crypto/x509"
7
7
"encoding/base64"
8
+ "errors"
8
9
"fmt"
9
10
"io"
10
11
"net"
11
12
"net/http"
12
13
"time"
13
14
14
- "github.com/letsencrypt/boulder/observer/obsdialer"
15
15
"github.com/prometheus/client_golang/prometheus"
16
16
"golang.org/x/crypto/ocsp"
17
+
18
+ "github.com/letsencrypt/boulder/observer/obsdialer"
17
19
)
18
20
19
21
type reason int
20
22
21
23
const (
22
24
none reason = iota
23
25
internalError
24
- ocspError
26
+ revocationStatusError
25
27
rootDidNotMatch
26
- responseDidNotMatch
28
+ statusDidNotMatch
27
29
)
28
30
29
31
var reasonToString = map [reason ]string {
30
- none : "nil" ,
31
- internalError : "internalError" ,
32
- ocspError : "ocspError " ,
33
- rootDidNotMatch : "rootDidNotMatch" ,
34
- responseDidNotMatch : "responseDidNotMatch " ,
32
+ none : "nil" ,
33
+ internalError : "internalError" ,
34
+ revocationStatusError : "revocationStatusError " ,
35
+ rootDidNotMatch : "rootDidNotMatch" ,
36
+ statusDidNotMatch : "statusDidNotMatch " ,
35
37
}
36
38
37
39
func getReasons () []string {
@@ -65,14 +67,19 @@ func (p TLSProbe) Kind() string {
65
67
}
66
68
67
69
// Get OCSP status (good, revoked or unknown) of certificate
68
- func checkOCSP (cert , issuer * x509.Certificate , want int ) (bool , error ) {
70
+ func checkOCSP (ctx context. Context , cert , issuer * x509.Certificate , want int ) (bool , error ) {
69
71
req , err := ocsp .CreateRequest (cert , issuer , nil )
70
72
if err != nil {
71
73
return false , err
72
74
}
73
75
74
76
url := fmt .Sprintf ("%s/%s" , cert .OCSPServer [0 ], base64 .StdEncoding .EncodeToString (req ))
75
- res , err := http .Get (url )
77
+ r , err := http .NewRequestWithContext (ctx , "GET" , url , nil )
78
+ if err != nil {
79
+ return false , err
80
+ }
81
+
82
+ res , err := http .DefaultClient .Do (r )
76
83
if err != nil {
77
84
return false , err
78
85
}
@@ -90,6 +97,45 @@ func checkOCSP(cert, issuer *x509.Certificate, want int) (bool, error) {
90
97
return ocspRes .Status == want , nil
91
98
}
92
99
100
+ func checkCRL (ctx context.Context , cert , issuer * x509.Certificate , want int ) (bool , error ) {
101
+ if len (cert .CRLDistributionPoints ) != 1 {
102
+ return false , errors .New ("cert does not contain CRLDP URI" )
103
+ }
104
+
105
+ req , err := http .NewRequestWithContext (ctx , "GET" , cert .CRLDistributionPoints [0 ], nil )
106
+ if err != nil {
107
+ return false , fmt .Errorf ("creating HTTP request: %w" , err )
108
+ }
109
+
110
+ resp , err := http .DefaultClient .Do (req )
111
+ if err != nil {
112
+ return false , fmt .Errorf ("downloading CRL: %w" , err )
113
+ }
114
+ defer resp .Body .Close ()
115
+
116
+ der , err := io .ReadAll (resp .Body )
117
+ if err != nil {
118
+ return false , fmt .Errorf ("reading CRL: %w" , err )
119
+ }
120
+
121
+ crl , err := x509 .ParseRevocationList (der )
122
+ if err != nil {
123
+ return false , fmt .Errorf ("parsing CRL: %w" , err )
124
+ }
125
+
126
+ err = crl .CheckSignatureFrom (issuer )
127
+ if err != nil {
128
+ return false , fmt .Errorf ("validating CRL: %w" , err )
129
+ }
130
+
131
+ for _ , entry := range crl .RevokedCertificateEntries {
132
+ if entry .SerialNumber .Cmp (cert .SerialNumber ) == 0 {
133
+ return want == ocsp .Revoked , nil
134
+ }
135
+ }
136
+ return want == ocsp .Good , nil
137
+ }
138
+
93
139
// Return an error if the root settings are nonempty and do not match the
94
140
// expected root.
95
141
func (p TLSProbe ) checkRoot (rootOrg , rootCN string ) error {
@@ -109,40 +155,54 @@ func (p TLSProbe) exportMetrics(cert *x509.Certificate, reason reason) {
109
155
}
110
156
111
157
func (p TLSProbe ) probeExpired (timeout time.Duration ) bool {
112
- config := & tls.Config {
113
- // Set InsecureSkipVerify to skip the default validation we are
114
- // replacing. This will not disable VerifyConnection.
115
- InsecureSkipVerify : true ,
116
- VerifyConnection : func (cs tls.ConnectionState ) error {
117
- opts := x509.VerifyOptions {
118
- CurrentTime : cs .PeerCertificates [0 ].NotAfter ,
119
- Intermediates : x509 .NewCertPool (),
120
- }
121
- for _ , cert := range cs .PeerCertificates [1 :] {
122
- opts .Intermediates .AddCert (cert )
123
- }
124
- _ , err := cs .PeerCertificates [0 ].Verify (opts )
125
- return err
126
- },
158
+ addr := p .hostname
159
+ _ , _ , err := net .SplitHostPort (addr )
160
+ if err != nil {
161
+ addr = net .JoinHostPort (addr , "443" )
127
162
}
128
- ctx , cancel := context .WithTimeout (context .Background (), timeout )
129
- defer cancel ()
163
+
130
164
tlsDialer := tls.Dialer {
131
165
NetDialer : & obsdialer .Dialer ,
132
- Config : config ,
166
+ Config : & tls.Config {
167
+ // Set InsecureSkipVerify to skip the default validation we are
168
+ // replacing. This will not disable VerifyConnection.
169
+ InsecureSkipVerify : true ,
170
+ VerifyConnection : func (cs tls.ConnectionState ) error {
171
+ issuers := x509 .NewCertPool ()
172
+ for _ , cert := range cs .PeerCertificates [1 :] {
173
+ issuers .AddCert (cert )
174
+ }
175
+ opts := x509.VerifyOptions {
176
+ // We set the current time to be the cert's expiration date so that
177
+ // the validation routine doesn't complain that the cert is expired.
178
+ CurrentTime : cs .PeerCertificates [0 ].NotAfter ,
179
+ // By settings roots and intermediates to be whatever was presented
180
+ // in the handshake, we're saying that we don't care about the cert
181
+ // chaining up to the system trust store. This is safe because we
182
+ // check the root ourselves in checkRoot().
183
+ Intermediates : issuers ,
184
+ Roots : issuers ,
185
+ }
186
+ _ , err := cs .PeerCertificates [0 ].Verify (opts )
187
+ return err
188
+ },
189
+ },
133
190
}
134
- conn , err := tlsDialer .DialContext (ctx , "tcp" , p .hostname + ":443" )
191
+
192
+ ctx , cancel := context .WithTimeout (context .Background (), timeout )
193
+ defer cancel ()
194
+
195
+ conn , err := tlsDialer .DialContext (ctx , "tcp" , addr )
135
196
if err != nil {
136
197
p .exportMetrics (nil , internalError )
137
198
return false
138
199
}
139
200
defer conn .Close ()
140
201
141
202
// tls.Dialer.DialContext is documented to always return *tls.Conn
142
- tlsConn := conn .(* tls.Conn )
143
- peers := tlsConn .ConnectionState ().PeerCertificates
203
+ peers := conn .(* tls.Conn ).ConnectionState ().PeerCertificates
144
204
if time .Until (peers [0 ].NotAfter ) > 0 {
145
- p .exportMetrics (peers [0 ], responseDidNotMatch )
205
+ p .exportMetrics (peers [0 ], statusDidNotMatch )
146
206
return false
147
207
}
148
208
@@ -158,35 +218,77 @@ func (p TLSProbe) probeExpired(timeout time.Duration) bool {
158
218
}
159
219
160
220
func (p TLSProbe ) probeUnexpired (timeout time.Duration ) bool {
161
- conn , err := tls .DialWithDialer (& net.Dialer {Timeout : timeout }, "tcp" , p .hostname + ":443" , & tls.Config {})
221
+ addr := p .hostname
222
+ _ , _ , err := net .SplitHostPort (addr )
223
+ if err != nil {
224
+ addr = net .JoinHostPort (addr , "443" )
225
+ }
226
+
227
+ tlsDialer := tls.Dialer {
228
+ NetDialer : & obsdialer .Dialer ,
229
+ Config : & tls.Config {
230
+ // Set InsecureSkipVerify to skip the default validation we are
231
+ // replacing. This will not disable VerifyConnection.
232
+ InsecureSkipVerify : true ,
233
+ VerifyConnection : func (cs tls.ConnectionState ) error {
234
+ issuers := x509 .NewCertPool ()
235
+ for _ , cert := range cs .PeerCertificates [1 :] {
236
+ issuers .AddCert (cert )
237
+ }
238
+ opts := x509.VerifyOptions {
239
+ // By settings roots and intermediates to be whatever was presented
240
+ // in the handshake, we're saying that we don't care about the cert
241
+ // chaining up to the system trust store. This is safe because we
242
+ // check the root ourselves in checkRoot().
243
+ Intermediates : issuers ,
244
+ Roots : issuers ,
245
+ }
246
+ _ , err := cs .PeerCertificates [0 ].Verify (opts )
247
+ return err
248
+ },
249
+ },
250
+ }
251
+
252
+ ctx , cancel := context .WithTimeout (context .Background (), timeout )
253
+ defer cancel ()
254
+
255
+ conn , err := tlsDialer .DialContext (ctx , "tcp" , addr )
162
256
if err != nil {
163
257
p .exportMetrics (nil , internalError )
164
258
return false
165
259
}
166
-
167
260
defer conn .Close ()
168
- peers := conn .ConnectionState ().PeerCertificates
261
+
262
+ // tls.Dialer.DialContext is documented to always return *tls.Conn
263
+ peers := conn .(* tls.Conn ).ConnectionState ().PeerCertificates
169
264
root := peers [len (peers )- 1 ].Issuer
170
265
err = p .checkRoot (root .Organization [0 ], root .CommonName )
171
266
if err != nil {
172
267
p .exportMetrics (peers [0 ], rootDidNotMatch )
173
268
return false
174
269
}
175
270
176
- var ocspStatus bool
271
+ var wantStatus int
177
272
switch p .response {
178
273
case "valid" :
179
- ocspStatus , err = checkOCSP ( peers [ 0 ], peers [ 1 ], ocsp .Good )
274
+ wantStatus = ocsp .Good
180
275
case "revoked" :
181
- ocspStatus , err = checkOCSP (peers [0 ], peers [1 ], ocsp .Revoked )
276
+ wantStatus = ocsp .Revoked
277
+ }
278
+
279
+ var statusMatch bool
280
+ if len (peers [0 ].OCSPServer ) != 0 {
281
+ statusMatch , err = checkOCSP (ctx , peers [0 ], peers [1 ], wantStatus )
282
+ } else {
283
+ statusMatch , err = checkCRL (ctx , peers [0 ], peers [1 ], wantStatus )
182
284
}
183
285
if err != nil {
184
- p .exportMetrics (peers [0 ], ocspError )
286
+ p .exportMetrics (peers [0 ], revocationStatusError )
185
287
return false
186
288
}
187
289
188
- if ! ocspStatus {
189
- p .exportMetrics (peers [0 ], responseDidNotMatch )
290
+ if ! statusMatch {
291
+ p .exportMetrics (peers [0 ], statusDidNotMatch )
190
292
return false
191
293
}
192
294
0 commit comments