@@ -5,33 +5,35 @@ import (
55 "crypto/tls"
66 "crypto/x509"
77 "encoding/base64"
8+ "errors"
89 "fmt"
910 "io"
1011 "net"
1112 "net/http"
1213 "time"
1314
14- "github.com/letsencrypt/boulder/observer/obsdialer"
1515 "github.com/prometheus/client_golang/prometheus"
1616 "golang.org/x/crypto/ocsp"
17+
18+ "github.com/letsencrypt/boulder/observer/obsdialer"
1719)
1820
1921type reason int
2022
2123const (
2224 none reason = iota
2325 internalError
24- ocspError
26+ revocationStatusError
2527 rootDidNotMatch
26- responseDidNotMatch
28+ statusDidNotMatch
2729)
2830
2931var 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 " ,
3537}
3638
3739func getReasons () []string {
@@ -65,14 +67,19 @@ func (p TLSProbe) Kind() string {
6567}
6668
6769// 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 ) {
6971 req , err := ocsp .CreateRequest (cert , issuer , nil )
7072 if err != nil {
7173 return false , err
7274 }
7375
7476 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 )
7683 if err != nil {
7784 return false , err
7885 }
@@ -90,6 +97,45 @@ func checkOCSP(cert, issuer *x509.Certificate, want int) (bool, error) {
9097 return ocspRes .Status == want , nil
9198}
9299
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+
93139// Return an error if the root settings are nonempty and do not match the
94140// expected root.
95141func (p TLSProbe ) checkRoot (rootOrg , rootCN string ) error {
@@ -109,40 +155,54 @@ func (p TLSProbe) exportMetrics(cert *x509.Certificate, reason reason) {
109155}
110156
111157func (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" )
127162 }
128- ctx , cancel := context .WithTimeout (context .Background (), timeout )
129- defer cancel ()
163+
130164 tlsDialer := tls.Dialer {
131165 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+ },
133190 }
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 )
135196 if err != nil {
136197 p .exportMetrics (nil , internalError )
137198 return false
138199 }
139200 defer conn .Close ()
140201
141202 // 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
144204 if time .Until (peers [0 ].NotAfter ) > 0 {
145- p .exportMetrics (peers [0 ], responseDidNotMatch )
205+ p .exportMetrics (peers [0 ], statusDidNotMatch )
146206 return false
147207 }
148208
@@ -158,35 +218,77 @@ func (p TLSProbe) probeExpired(timeout time.Duration) bool {
158218}
159219
160220func (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 )
162256 if err != nil {
163257 p .exportMetrics (nil , internalError )
164258 return false
165259 }
166-
167260 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
169264 root := peers [len (peers )- 1 ].Issuer
170265 err = p .checkRoot (root .Organization [0 ], root .CommonName )
171266 if err != nil {
172267 p .exportMetrics (peers [0 ], rootDidNotMatch )
173268 return false
174269 }
175270
176- var ocspStatus bool
271+ var wantStatus int
177272 switch p .response {
178273 case "valid" :
179- ocspStatus , err = checkOCSP ( peers [ 0 ], peers [ 1 ], ocsp .Good )
274+ wantStatus = ocsp .Good
180275 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 )
182284 }
183285 if err != nil {
184- p .exportMetrics (peers [0 ], ocspError )
286+ p .exportMetrics (peers [0 ], revocationStatusError )
185287 return false
186288 }
187289
188- if ! ocspStatus {
189- p .exportMetrics (peers [0 ], responseDidNotMatch )
290+ if ! statusMatch {
291+ p .exportMetrics (peers [0 ], statusDidNotMatch )
190292 return false
191293 }
192294
0 commit comments