...

Source file src/github.com/letsencrypt/boulder/ocsp/responder/responder_test.go

Documentation: github.com/letsencrypt/boulder/ocsp/responder

     1  /*
     2  This code was originally forked from https://github.com/cloudflare/cfssl/blob/1a911ca1b1d6e899bf97dcfa4a14b38db0d31134/ocsp/responder_test.go
     3  
     4  Copyright (c) 2014 CloudFlare Inc.
     5  
     6  Redistribution and use in source and binary forms, with or without
     7  modification, are permitted provided that the following conditions
     8  are met:
     9  
    10  Redistributions of source code must retain the above copyright notice,
    11  this list of conditions and the following disclaimer.
    12  
    13  Redistributions in binary form must reproduce the above copyright notice,
    14  this list of conditions and the following disclaimer in the documentation
    15  and/or other materials provided with the distribution.
    16  
    17  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    18  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    19  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    20  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    21  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    22  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
    23  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    24  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    25  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    26  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    27  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    28  */
    29  
    30  package responder
    31  
    32  import (
    33  	"bytes"
    34  	"context"
    35  	"encoding/hex"
    36  	"fmt"
    37  	"net/http"
    38  	"net/http/httptest"
    39  	"net/url"
    40  	"strings"
    41  	"testing"
    42  	"time"
    43  
    44  	"github.com/jmhodges/clock"
    45  	"github.com/prometheus/client_golang/prometheus"
    46  	"golang.org/x/crypto/ocsp"
    47  
    48  	blog "github.com/letsencrypt/boulder/log"
    49  	"github.com/letsencrypt/boulder/test"
    50  )
    51  
    52  const (
    53  	responseFile       = "testdata/resp64.pem"
    54  	binResponseFile    = "testdata/response.der"
    55  	brokenResponseFile = "testdata/response_broken.pem"
    56  	mixResponseFile    = "testdata/response_mix.pem"
    57  )
    58  
    59  type testSource struct{}
    60  
    61  func (ts testSource) Response(_ context.Context, r *ocsp.Request) (*Response, error) {
    62  	respBytes, err := hex.DecodeString("3082031D0A0100A08203163082031206092B060105050730010104820303308202FF3081E8A1453043310B300906035504061302555331123010060355040A1309676F6F6420677579733120301E06035504031317434120696E7465726D6564696174652028525341292041180F32303230303631393030333730305A30818D30818A304C300906052B0E03021A0500041417779CF67D84CD4449A2FC7EAC431F9823D8575A04149F2970E80CF9C75ECC1F2871D8C390CD19F40108021300FF8B2AEC5293C6B31D0BC0BA329CF594E7BAA116180F32303230303631393030333733305AA0030A0101180F32303230303631393030303030305AA011180F32303230303632333030303030305A300D06092A864886F70D01010B0500038202010011688303203098FC522D2C599A234B136930E3C4680F2F3192188B98D6EE90E8479449968C51335FADD1636584ACEA9D01A30790BD90190FA35A47E793718128B19E9ED156382C1B68245A6887F547B0B86C44C2354B8DBA94D8BFCAA768EB55FA84AEB4026DBEFC687DB280D21C0B3497A11909804A20F402BDD95E4843C02E30435C2570FFC4EB152FE2785B8D268AC996619644AEC9CF50959D46DEB21DFE96B4D2881D61ABBCA9B6BFEC2DB9132801CAE737C862F0AEAB4948B63F35740CE93FCDBC148F5070790D7BBA1A87E15078CD8335F83686142CE8AC3AD21FAE45B87A7B12562D9F245352A83E3901E97E5EC77E9817990712D8BE60860ABA58804DDE4ECDCA6AEFD3D8764FDBABF0AB1902FA9A7C4C3F5814C25C5E78E0754469E087CAED81E50A5873CADFCAC42963AB38CFD11096BE4201DE4589B57EC48B3DA05A65800D654160E022F6748CD93B431A17270C1B27E313734FCF85F22547D060F23F594BD68C6330C2705190A04905FBD2389E2DD21C0188809E03D713F56BF95953C9897DA6D4D074D70F164270C41BFB386B69E86EB3B9192FEA8F43CE5368CC9AF8687DEE567672A8580BA6A9F76E6E6705DD2F76F48C2C180C763CF4C48AF78C25D40EA7278CB2FBC78958B3179301825B420A7CAE7ACE4C41B5BA7D567AABC9C2701EE75A28F9181E044EDAAA55A31538AA9C526D4C324B9AE58D2922")
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	resp, err := ocsp.ParseResponse(respBytes, nil)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	return &Response{resp, respBytes}, nil
    71  }
    72  
    73  type expiredSource struct{}
    74  
    75  func (es expiredSource) Response(_ context.Context, r *ocsp.Request) (*Response, error) {
    76  	return nil, errOCSPResponseExpired
    77  }
    78  
    79  type testCase struct {
    80  	method, path string
    81  	expected     int
    82  }
    83  
    84  func TestResponseExpired(t *testing.T) {
    85  	cases := []testCase{
    86  		{"GET", "/MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", 533},
    87  	}
    88  
    89  	responder := Responder{
    90  		Source: expiredSource{},
    91  		responseTypes: prometheus.NewCounterVec(
    92  			prometheus.CounterOpts{
    93  				Name: "ocspResponses-test",
    94  			},
    95  			[]string{"type"},
    96  		),
    97  		clk: clock.NewFake(),
    98  		log: blog.NewMock(),
    99  	}
   100  
   101  	for _, tc := range cases {
   102  		t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) {
   103  			rw := httptest.NewRecorder()
   104  			responder.responseTypes.Reset()
   105  
   106  			responder.ServeHTTP(rw, &http.Request{
   107  				Method: tc.method,
   108  				URL: &url.URL{
   109  					Path: tc.path,
   110  				},
   111  			})
   112  			if rw.Code != tc.expected {
   113  				t.Errorf("Incorrect response code: got %d, wanted %d", rw.Code, tc.expected)
   114  			}
   115  			test.AssertByteEquals(t, ocsp.InternalErrorErrorResponse, rw.Body.Bytes())
   116  		})
   117  	}
   118  }
   119  
   120  func TestOCSP(t *testing.T) {
   121  	cases := []testCase{
   122  		{"OPTIONS", "/", http.StatusMethodNotAllowed},
   123  		{"GET", "/", http.StatusBadRequest},
   124  		// Bad URL encoding
   125  		{"GET", "%ZZFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
   126  		// Bad URL encoding
   127  		{"GET", "%%FQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
   128  		// Bad base64 encoding
   129  		{"GET", "==MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
   130  		// Bad OCSP DER encoding
   131  		{"GET", "AAAMFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
   132  		// Good encoding all around, including a double slash
   133  		{"GET", "MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusOK},
   134  		// Good request, leading slash
   135  		{"GET", "/MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusOK},
   136  	}
   137  
   138  	responder := Responder{
   139  		Source: testSource{},
   140  		responseTypes: prometheus.NewCounterVec(
   141  			prometheus.CounterOpts{
   142  				Name: "ocspResponses-test",
   143  			},
   144  			[]string{"type"},
   145  		),
   146  		responseAges: prometheus.NewHistogram(
   147  			prometheus.HistogramOpts{
   148  				Name:    "ocspAges-test",
   149  				Buckets: []float64{43200},
   150  			},
   151  		),
   152  		clk: clock.NewFake(),
   153  		log: blog.NewMock(),
   154  	}
   155  
   156  	for _, tc := range cases {
   157  		t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) {
   158  			rw := httptest.NewRecorder()
   159  			responder.responseTypes.Reset()
   160  
   161  			responder.ServeHTTP(rw, &http.Request{
   162  				Method: tc.method,
   163  				URL: &url.URL{
   164  					Path: tc.path,
   165  				},
   166  			})
   167  			if rw.Code != tc.expected {
   168  				t.Errorf("Incorrect response code: got %d, wanted %d", rw.Code, tc.expected)
   169  			}
   170  			if rw.Code == http.StatusOK {
   171  				test.AssertMetricWithLabelsEquals(
   172  					t, responder.responseTypes, prometheus.Labels{"type": "Success"}, 1)
   173  			} else if rw.Code == http.StatusBadRequest {
   174  				test.AssertMetricWithLabelsEquals(
   175  					t, responder.responseTypes, prometheus.Labels{"type": "Malformed"}, 1)
   176  			}
   177  		})
   178  	}
   179  	// Exactly two of the cases above result in an OCSP response being sent.
   180  	test.AssertMetricWithLabelsEquals(t, responder.responseAges, prometheus.Labels{}, 2)
   181  }
   182  
   183  func TestRequestTooBig(t *testing.T) {
   184  	responder := Responder{
   185  		Source: testSource{},
   186  		responseTypes: prometheus.NewCounterVec(
   187  			prometheus.CounterOpts{
   188  				Name: "ocspResponses-test",
   189  			},
   190  			[]string{"type"},
   191  		),
   192  		responseAges: prometheus.NewHistogram(
   193  			prometheus.HistogramOpts{
   194  				Name:    "ocspAges-test",
   195  				Buckets: []float64{43200},
   196  			},
   197  		),
   198  		clk: clock.NewFake(),
   199  		log: blog.NewMock(),
   200  	}
   201  
   202  	rw := httptest.NewRecorder()
   203  
   204  	responder.ServeHTTP(rw, httptest.NewRequest("POST", "/",
   205  		bytes.NewBuffer([]byte(strings.Repeat("a", 10001)))))
   206  	expected := 400
   207  	if rw.Code != expected {
   208  		t.Errorf("Incorrect response code: got %d, wanted %d", rw.Code, expected)
   209  	}
   210  }
   211  
   212  func TestCacheHeaders(t *testing.T) {
   213  	source, err := NewMemorySourceFromFile(responseFile, blog.NewMock())
   214  	if err != nil {
   215  		t.Fatalf("Error constructing source: %s", err)
   216  	}
   217  
   218  	fc := clock.NewFake()
   219  	fc.Set(time.Date(2015, 11, 12, 0, 0, 0, 0, time.UTC))
   220  	responder := Responder{
   221  		Source: source,
   222  		responseTypes: prometheus.NewCounterVec(
   223  			prometheus.CounterOpts{
   224  				Name: "ocspResponses-test",
   225  			},
   226  			[]string{"type"},
   227  		),
   228  		responseAges: prometheus.NewHistogram(
   229  			prometheus.HistogramOpts{
   230  				Name:    "ocspAges-test",
   231  				Buckets: []float64{43200},
   232  			},
   233  		),
   234  		clk: fc,
   235  		log: blog.NewMock(),
   236  	}
   237  
   238  	rw := httptest.NewRecorder()
   239  	responder.ServeHTTP(rw, &http.Request{
   240  		Method: "GET",
   241  		URL: &url.URL{
   242  			Path: "MEMwQTA/MD0wOzAJBgUrDgMCGgUABBSwLsMRhyg1dJUwnXWk++D57lvgagQU6aQ/7p6l5vLV13lgPJOmLiSOl6oCAhJN",
   243  		},
   244  	})
   245  	if rw.Code != http.StatusOK {
   246  		t.Errorf("Unexpected HTTP status code %d", rw.Code)
   247  	}
   248  	testCases := []struct {
   249  		header string
   250  		value  string
   251  	}{
   252  		{"Last-Modified", "Tue, 20 Oct 2015 00:00:00 UTC"},
   253  		{"Expires", "Sun, 20 Oct 2030 00:00:00 UTC"},
   254  		{"Cache-Control", "max-age=471398400, public, no-transform, must-revalidate"},
   255  		{"Etag", "\"8169FB0843B081A76E9F6F13FD70C8411597BEACF8B182136FFDD19FBD26140A\""},
   256  	}
   257  	for _, tc := range testCases {
   258  		headers, ok := rw.Result().Header[tc.header]
   259  		if !ok {
   260  			t.Errorf("Header %s missing from HTTP response", tc.header)
   261  			continue
   262  		}
   263  		if len(headers) != 1 {
   264  			t.Errorf("Wrong number of headers in HTTP response. Wanted 1, got %d", len(headers))
   265  			continue
   266  		}
   267  		actual := headers[0]
   268  		if actual != tc.value {
   269  			t.Errorf("Got header %s: %s. Expected %s", tc.header, actual, tc.value)
   270  		}
   271  	}
   272  
   273  	rw = httptest.NewRecorder()
   274  	headers := http.Header{}
   275  	headers.Add("If-None-Match", "\"8169FB0843B081A76E9F6F13FD70C8411597BEACF8B182136FFDD19FBD26140A\"")
   276  	responder.ServeHTTP(rw, &http.Request{
   277  		Method: "GET",
   278  		URL: &url.URL{
   279  			Path: "MEMwQTA/MD0wOzAJBgUrDgMCGgUABBSwLsMRhyg1dJUwnXWk++D57lvgagQU6aQ/7p6l5vLV13lgPJOmLiSOl6oCAhJN",
   280  		},
   281  		Header: headers,
   282  	})
   283  	if rw.Code != http.StatusNotModified {
   284  		t.Fatalf("Got wrong status code: expected %d, got %d", http.StatusNotModified, rw.Code)
   285  	}
   286  }
   287  
   288  func TestNewSourceFromFile(t *testing.T) {
   289  	logger := blog.NewMock()
   290  	_, err := NewMemorySourceFromFile("", logger)
   291  	if err == nil {
   292  		t.Fatal("Didn't fail on non-file input")
   293  	}
   294  
   295  	// expected case
   296  	_, err = NewMemorySourceFromFile(responseFile, logger)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	// binary-formatted file
   302  	_, err = NewMemorySourceFromFile(binResponseFile, logger)
   303  	if err != nil {
   304  		t.Fatal(err)
   305  	}
   306  
   307  	// the response file from before, with stuff deleted
   308  	_, err = NewMemorySourceFromFile(brokenResponseFile, logger)
   309  	if err != nil {
   310  		t.Fatal(err)
   311  	}
   312  
   313  	// mix of a correct and malformed responses
   314  	_, err = NewMemorySourceFromFile(mixResponseFile, logger)
   315  	if err != nil {
   316  		t.Fatal(err)
   317  	}
   318  }
   319  

View as plain text