Skip to content

Commit b74c6cc

Browse files
committed
GODRIVER-2395 Never return errors when attempting to contact OCSP responders. (#926)
1 parent d76de5c commit b74c6cc

File tree

2 files changed

+83
-85
lines changed

2 files changed

+83
-85
lines changed

x/mongo/driver/ocsp/ocsp.go

Lines changed: 34 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"io/ioutil"
1919
"math/big"
2020
"net/http"
21-
"net/url"
2221
"time"
2322

2423
"golang.org/x/crypto/ocsp"
@@ -28,9 +27,6 @@ import (
2827
var (
2928
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
3029
mustStapleFeatureValue = big.NewInt(5)
31-
32-
defaultRequestTimeout = 5 * time.Second
33-
errGotOCSPResponse = errors.New("done")
3430
)
3531

3632
// Error represents an OCSP verification error
@@ -126,10 +122,7 @@ func getParsedResponse(ctx context.Context, cfg config, connState tls.Connection
126122
if cfg.disableEndpointChecking {
127123
return nil, nil
128124
}
129-
externalResponse, err := contactResponders(ctx, cfg)
130-
if err != nil {
131-
return nil, err
132-
}
125+
externalResponse := contactResponders(ctx, cfg)
133126
if externalResponse == nil {
134127
// None of the responders were available.
135128
return nil, nil
@@ -210,40 +203,33 @@ func isMustStapleCertificate(cert *x509.Certificate) (bool, error) {
210203
return false, nil
211204
}
212205

213-
// contactResponders will send a request to the OCSP responders reported by cfg.serverCert. The first response that
214-
// conclusively identifies cfg.serverCert as good or revoked will be returned. If all responders are unavailable or no
215-
// responder returns a conclusive status, (nil, nil) will be returned.
216-
func contactResponders(ctx context.Context, cfg config) (*ResponseDetails, error) {
206+
// contactResponders will send a request to all OCSP responders reported by cfg.serverCert. The
207+
// first response that conclusively identifies cfg.serverCert as good or revoked will be returned.
208+
// If all responders are unavailable or no responder returns a conclusive status, it returns nil.
209+
// contactResponders will wait for up to 5 seconds to get a certificate status response.
210+
func contactResponders(ctx context.Context, cfg config) *ResponseDetails {
217211
if len(cfg.serverCert.OCSPServer) == 0 {
218-
return nil, nil
212+
return nil
219213
}
220214

221-
requestCtx := ctx // Either ctx or a new context derived from ctx with a five second timeout.
222-
userContextUsed := true
223-
var cancelFn context.CancelFunc
224-
225-
// Use a context with defaultRequestTimeout if ctx does not have a deadline set or the current deadline is further
226-
// out than defaultRequestTimeout. If the current deadline is less than less than defaultRequestTimeout out, respect
227-
// it. Calling context.WithTimeout would do this for us, but we need to know which context we're using.
228-
wantDeadline := time.Now().Add(defaultRequestTimeout)
229-
if deadline, ok := ctx.Deadline(); !ok || deadline.After(wantDeadline) {
230-
userContextUsed = false
231-
requestCtx, cancelFn = context.WithDeadline(ctx, wantDeadline)
232-
}
233-
defer func() {
234-
if cancelFn != nil {
235-
cancelFn()
236-
}
237-
}()
215+
// Limit all OCSP responder calls to a maximum of 5 seconds or when the passed-in context expires,
216+
// whichever happens first.
217+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
218+
defer cancel()
238219

239-
group, groupCtx := errgroup.WithContext(requestCtx)
220+
group, ctx := errgroup.WithContext(ctx)
240221
ocspResponses := make(chan *ocsp.Response, len(cfg.serverCert.OCSPServer))
241222
defer close(ocspResponses)
242223

243224
for _, endpoint := range cfg.serverCert.OCSPServer {
244225
// Re-assign endpoint so it gets re-scoped rather than using the iteration variable in the goroutine. See
245226
// https://golang.org/doc/faq#closures_and_goroutines.
246227
endpoint := endpoint
228+
229+
// Start a group of goroutines that each attempt to request the certificate status from one
230+
// of the OCSP endpoints listed in the server certificate. We want to "soft fail" on all
231+
// errors, so this function never returns actual errors. Only a "done" error is returned
232+
// when a response is received so the errgroup cancels any other in-progress requests.
247233
group.Go(func() error {
248234
// Use bytes.NewReader instead of bytes.NewBuffer because a bytes.Buffer is an owning representation and the
249235
// docs recommend not using the underlying []byte after creating the buffer, so a new copy of the request
@@ -252,35 +238,16 @@ func contactResponders(ctx context.Context, cfg config) (*ResponseDetails, error
252238
if err != nil {
253239
return nil
254240
}
255-
request = request.WithContext(groupCtx)
256-
257-
// Execute the request and handle errors as follows:
258-
//
259-
// 1. If the original context expired or was cancelled, propagate the error up so the caller will abort the
260-
// verification and return control to the user.
261-
//
262-
// 2. If any other errors occurred, including the defaultRequestTimeout expiring, or the response has a
263-
// non-200 status code, suppress the error because we want to ignore this responder and wait for a different
264-
// one to responsd.
241+
request = request.WithContext(ctx)
242+
265243
httpResponse, err := http.DefaultClient.Do(request)
266244
if err != nil {
267-
urlErr, ok := err.(*url.Error)
268-
if !ok {
269-
return nil
270-
}
271-
272-
timeout := urlErr.Timeout()
273-
cancelled := urlErr.Err == context.Canceled // Timeout() does not return true for context.Cancelled.
274-
if cancelled || (userContextUsed && timeout) {
275-
// Handle the original context expiring or being cancelled. The url.Error type supports Unwrap, so
276-
// users can use errors.Is to check for context errors.
277-
return err
278-
}
279-
return nil // Ignore all other errors.
245+
return nil
280246
}
281247
defer func() {
282248
_ = httpResponse.Body.Close()
283249
}()
250+
284251
if httpResponse.StatusCode != 200 {
285252
return nil
286253
}
@@ -292,26 +259,27 @@ func contactResponders(ctx context.Context, cfg config) (*ResponseDetails, error
292259

293260
ocspResponse, err := ocsp.ParseResponseForCert(httpBytes, cfg.serverCert, cfg.issuer)
294261
if err != nil || verifyResponse(cfg, ocspResponse) != nil || ocspResponse.Status == ocsp.Unknown {
295-
// If there was an error parsing/validating the response or the response was inconclusive, suppress
296-
// the error because we want to ignore this responder.
262+
// If there was an error parsing/validating the response or the response was
263+
// inconclusive, suppress the error because we want to ignore this responder.
297264
return nil
298265
}
299266

300-
// Store the response and return a sentinel error so the error group will exit and any in-flight requests
301-
// will be cancelled.
267+
// Send the conclusive response on the response channel and return a "done" error that
268+
// will cause the errgroup to cancel all other in-progress requests.
302269
ocspResponses <- ocspResponse
303-
return errGotOCSPResponse
270+
return errors.New("done")
304271
})
305272
}
306273

307-
if err := group.Wait(); err != nil && err != errGotOCSPResponse {
308-
return nil, err
309-
}
310-
if len(ocspResponses) == 0 {
311-
// None of the responders gave a conclusive response.
312-
return nil, nil
274+
_ = group.Wait()
275+
select {
276+
case res := <-ocspResponses:
277+
return extractResponseDetails(res)
278+
default:
279+
// If there is no OCSP response on the response channel, all OCSP calls either failed or
280+
// were inconclusive. Return nil.
281+
return nil
313282
}
314-
return extractResponseDetails(<-ocspResponses), nil
315283
}
316284

317285
// verifyResponse checks that the provided OCSP response is valid.

x/mongo/driver/ocsp/ocsp_test.go

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,59 @@ package ocsp
1111
import (
1212
"context"
1313
"crypto/x509"
14-
"errors"
14+
"net"
1515
"testing"
16+
"time"
1617

1718
"go.mongodb.org/mongo-driver/internal/testutil/assert"
1819
)
1920

20-
func TestOCSP(t *testing.T) {
21-
t.Run("contactResponders", func(t *testing.T) {
22-
t.Run("cancelled context is propagated", func(t *testing.T) {
23-
ctx, cancel := context.WithCancel(context.Background())
24-
cancel()
25-
26-
serverCert := &x509.Certificate{
27-
OCSPServer: []string{"https://localhost:5000"},
28-
}
29-
cfg := config{
30-
serverCert: serverCert,
31-
issuer: &x509.Certificate{},
32-
cache: NewCache(),
33-
}
34-
35-
_, err := contactResponders(ctx, cfg)
36-
assert.True(t, errors.Is(err, context.Canceled), "expected error %v, got %v", context.Canceled, err)
37-
})
21+
func TestContactResponders(t *testing.T) {
22+
t.Run("context cancellation is honored", func(t *testing.T) {
23+
t.Parallel()
24+
25+
ctx, cancel := context.WithCancel(context.Background())
26+
cancel()
27+
28+
serverCert := &x509.Certificate{
29+
OCSPServer: []string{"https://localhost:5000"},
30+
}
31+
cfg := config{
32+
serverCert: serverCert,
33+
issuer: &x509.Certificate{},
34+
cache: NewCache(),
35+
}
36+
37+
res := contactResponders(ctx, cfg)
38+
assert.Nil(t, res, "expected nil response details, but got %v", res)
39+
})
40+
t.Run("context timeout is honored", func(t *testing.T) {
41+
t.Parallel()
42+
43+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
44+
defer cancel()
45+
46+
// Create a TCP listener on a random port that doesn't accept any connections, causing
47+
// connection attempts to hang indefinitely from the client's perspective.
48+
l, err := net.Listen("tcp", "localhost:0")
49+
assert.Nil(t, err, "tls.Listen() error: %v", err)
50+
defer l.Close()
51+
52+
serverCert := &x509.Certificate{
53+
OCSPServer: []string{"https://" + l.Addr().String()},
54+
}
55+
cfg := config{
56+
serverCert: serverCert,
57+
issuer: &x509.Certificate{},
58+
cache: NewCache(),
59+
}
60+
61+
// Expect that contactResponders() returns a nil response but does not cause any errors when
62+
// the passed-in context times out.
63+
start := time.Now()
64+
res := contactResponders(ctx, cfg)
65+
duration := time.Since(start)
66+
assert.Nil(t, res, "expected nil response, but got: %v", res)
67+
assert.True(t, duration <= 5*time.Second, "expected duration to be <= 5s, but was %v", duration)
3868
})
3969
}

0 commit comments

Comments
 (0)