...

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

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

     1  package redis
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"math/big"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/jmhodges/clock"
    11  	"github.com/letsencrypt/boulder/core"
    12  	"github.com/letsencrypt/boulder/log"
    13  	"github.com/letsencrypt/boulder/metrics"
    14  	"github.com/letsencrypt/boulder/ocsp/responder"
    15  	ocsp_test "github.com/letsencrypt/boulder/ocsp/test"
    16  	"github.com/letsencrypt/boulder/rocsp"
    17  	"github.com/letsencrypt/boulder/test"
    18  	"github.com/prometheus/client_golang/prometheus"
    19  	"golang.org/x/crypto/ocsp"
    20  )
    21  
    22  // notFoundRedis is a mock *rocsp.WritingClient that (a) returns "not found"
    23  // for all GetResponse, and (b) sends all StoreResponse serial numbers to
    24  // a channel. The latter is necessary because the code under test calls
    25  // StoreResponse from a goroutine, so we need something to synchronize back to
    26  // the testing goroutine.
    27  // For tests where you do not expect StoreResponse to be called, set the chan
    28  // to nil so sends will panic.
    29  type notFoundRedis struct {
    30  	serialStored chan *big.Int
    31  }
    32  
    33  func (nfr *notFoundRedis) GetResponse(ctx context.Context, serial string) ([]byte, error) {
    34  	return nil, rocsp.ErrRedisNotFound
    35  }
    36  
    37  func (nfr *notFoundRedis) StoreResponse(ctx context.Context, resp *ocsp.Response) error {
    38  	nfr.serialStored <- resp.SerialNumber
    39  	return nil
    40  }
    41  
    42  type recordingSigner struct {
    43  	serialRequested *big.Int
    44  }
    45  
    46  func (rs *recordingSigner) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
    47  	if rs.serialRequested != nil {
    48  		panic("signed twice")
    49  	}
    50  	rs.serialRequested = req.SerialNumber
    51  	// Return a fake response with only serial number filled, because that's
    52  	// all the test cares about.
    53  	return &responder.Response{Response: &ocsp.Response{
    54  		SerialNumber: req.SerialNumber,
    55  	}}, nil
    56  }
    57  
    58  func TestNotFound(t *testing.T) {
    59  	recordingSigner := recordingSigner{}
    60  	src, err := NewRedisSource(nil, &recordingSigner, time.Second, clock.NewFake(), metrics.NoopRegisterer, log.NewMock(), 1)
    61  	test.AssertNotError(t, err, "making source")
    62  	notFoundRedis := &notFoundRedis{make(chan *big.Int)}
    63  	src.client = notFoundRedis
    64  
    65  	serial := big.NewInt(987654321)
    66  	_, err = src.Response(context.Background(), &ocsp.Request{
    67  		SerialNumber: serial,
    68  	})
    69  	test.AssertNotError(t, err, "signing response when not found")
    70  	if recordingSigner.serialRequested.Cmp(serial) != 0 {
    71  		t.Errorf("issued signing request for serial %x; expected %x", recordingSigner.serialRequested, serial)
    72  	}
    73  	stored := <-notFoundRedis.serialStored
    74  	if stored == nil {
    75  		t.Fatalf("response was never stored")
    76  	}
    77  	if stored.Cmp(serial) != 0 {
    78  		t.Errorf("stored response for serial %x; expected %x", notFoundRedis.serialStored, serial)
    79  	}
    80  }
    81  
    82  type panicSource struct{}
    83  
    84  func (ps panicSource) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
    85  	panic("shouldn't happen")
    86  }
    87  
    88  type errorRedis struct{}
    89  
    90  func (er errorRedis) GetResponse(ctx context.Context, serial string) ([]byte, error) {
    91  	return nil, errors.New("the enzabulators florbled")
    92  }
    93  
    94  func (er errorRedis) StoreResponse(ctx context.Context, resp *ocsp.Response) error {
    95  	return nil
    96  }
    97  
    98  // When the initial Redis lookup returns an error, we should
    99  // proceed with live signing.
   100  func TestQueryError(t *testing.T) {
   101  	serial := big.NewInt(314159)
   102  	thisUpdate := time.Now().Truncate(time.Second).UTC()
   103  	resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
   104  		SerialNumber: serial,
   105  		Status:       ocsp.Good,
   106  		ThisUpdate:   thisUpdate,
   107  	})
   108  	test.AssertNotError(t, err, "making fake response")
   109  	source := echoSource{resp: resp}
   110  
   111  	src, err := NewRedisSource(nil, source, time.Second, clock.NewFake(), metrics.NoopRegisterer, log.NewMock(), 1)
   112  	test.AssertNotError(t, err, "making source")
   113  	src.client = errorRedis{}
   114  
   115  	receivedResp, err := src.Response(context.Background(), &ocsp.Request{
   116  		SerialNumber: serial,
   117  	})
   118  	test.AssertNotError(t, err, "expected no error when Redis errored")
   119  	test.AssertDeepEquals(t, resp.Raw, receivedResp.Raw)
   120  	test.AssertMetricWithLabelsEquals(t, src.counter, prometheus.Labels{"result": "lookup_error"}, 1)
   121  }
   122  
   123  type garbleRedis struct{}
   124  
   125  func (er garbleRedis) GetResponse(ctx context.Context, serial string) ([]byte, error) {
   126  	return []byte("not a valid OCSP response, I can tell by the pixels"), nil
   127  }
   128  
   129  func (er garbleRedis) StoreResponse(ctx context.Context, resp *ocsp.Response) error {
   130  	panic("shouldn't happen")
   131  }
   132  
   133  func TestParseError(t *testing.T) {
   134  	src, err := NewRedisSource(nil, panicSource{}, time.Second, clock.NewFake(), metrics.NoopRegisterer, log.NewMock(), 1)
   135  	test.AssertNotError(t, err, "making source")
   136  	src.client = garbleRedis{}
   137  
   138  	_, err = src.Response(context.Background(), &ocsp.Request{
   139  		SerialNumber: big.NewInt(314159),
   140  	})
   141  	test.AssertError(t, err, "expected error when Redis returned junk")
   142  	if errors.Is(err, rocsp.ErrRedisNotFound) {
   143  		t.Errorf("incorrect error value ErrRedisNotFound; expected general error")
   144  	}
   145  }
   146  
   147  func TestSignError(t *testing.T) {
   148  	src, err := NewRedisSource(nil, errorSource{}, time.Second, clock.NewFake(), metrics.NoopRegisterer, log.NewMock(), 1)
   149  	test.AssertNotError(t, err, "making source")
   150  	src.client = &notFoundRedis{nil}
   151  
   152  	_, err = src.Response(context.Background(), &ocsp.Request{
   153  		SerialNumber: big.NewInt(2718),
   154  	})
   155  	test.AssertError(t, err, "Expected error when signer errored")
   156  }
   157  
   158  // staleRedis is a mock *rocsp.WritingClient that (a) returns response with a
   159  // fixed ThisUpdate for all GetResponse, and (b) sends all StoreResponse serial
   160  // numbers to a channel. The latter is necessary because the code under test
   161  // calls StoreResponse from a goroutine, so we need something to synchronize
   162  // back to the testing goroutine.
   163  type staleRedis struct {
   164  	serialStored chan *big.Int
   165  	thisUpdate   time.Time
   166  }
   167  
   168  func (sr *staleRedis) GetResponse(ctx context.Context, serial string) ([]byte, error) {
   169  	serInt, err := core.StringToSerial(serial)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
   174  		SerialNumber: serInt,
   175  		ThisUpdate:   sr.thisUpdate,
   176  	})
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	return resp.Raw, nil
   181  }
   182  
   183  func (sr *staleRedis) StoreResponse(ctx context.Context, resp *ocsp.Response) error {
   184  	sr.serialStored <- resp.SerialNumber
   185  	return nil
   186  }
   187  
   188  func TestStale(t *testing.T) {
   189  	recordingSigner := recordingSigner{}
   190  	clk := clock.NewFake()
   191  	src, err := NewRedisSource(nil, &recordingSigner, time.Second, clk, metrics.NoopRegisterer, log.NewMock(), 1)
   192  	test.AssertNotError(t, err, "making source")
   193  	staleRedis := &staleRedis{
   194  		serialStored: make(chan *big.Int),
   195  		thisUpdate:   clk.Now().Add(-time.Hour),
   196  	}
   197  	src.client = staleRedis
   198  
   199  	serial := big.NewInt(8675309)
   200  	_, err = src.Response(context.Background(), &ocsp.Request{
   201  		SerialNumber: serial,
   202  	})
   203  	test.AssertNotError(t, err, "signing response when not found")
   204  	if recordingSigner.serialRequested == nil {
   205  		t.Fatalf("signing source was never called")
   206  	}
   207  	if recordingSigner.serialRequested.Cmp(serial) != 0 {
   208  		t.Errorf("issued signing request for serial %x; expected %x", recordingSigner.serialRequested, serial)
   209  	}
   210  	stored := <-staleRedis.serialStored
   211  	if stored == nil {
   212  		t.Fatalf("response was never stored")
   213  	}
   214  	if stored.Cmp(serial) != 0 {
   215  		t.Errorf("stored response for serial %x; expected %x", staleRedis.serialStored, serial)
   216  	}
   217  }
   218  
   219  // notFoundSigner is a Source that always returns NotFound.
   220  type notFoundSigner struct{}
   221  
   222  func (nfs notFoundSigner) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
   223  	return nil, responder.ErrNotFound
   224  }
   225  
   226  func TestCertificateNotFound(t *testing.T) {
   227  	src, err := NewRedisSource(nil, notFoundSigner{}, time.Second, clock.NewFake(), metrics.NoopRegisterer, log.NewMock(), 1)
   228  	test.AssertNotError(t, err, "making source")
   229  	notFoundRedis := &notFoundRedis{nil}
   230  	src.client = notFoundRedis
   231  
   232  	_, err = src.Response(context.Background(), &ocsp.Request{
   233  		SerialNumber: big.NewInt(777777777),
   234  	})
   235  	if !errors.Is(err, responder.ErrNotFound) {
   236  		t.Errorf("expected NotFound error, got %s", err)
   237  	}
   238  }
   239  
   240  func TestNoServeStale(t *testing.T) {
   241  	clk := clock.NewFake()
   242  	src, err := NewRedisSource(nil, errorSource{}, time.Second, clk, metrics.NoopRegisterer, log.NewMock(), 1)
   243  	test.AssertNotError(t, err, "making source")
   244  	staleRedis := &staleRedis{
   245  		serialStored: nil,
   246  		thisUpdate:   clk.Now().Add(-time.Hour),
   247  	}
   248  	src.client = staleRedis
   249  
   250  	serial := big.NewInt(111111)
   251  	_, err = src.Response(context.Background(), &ocsp.Request{
   252  		SerialNumber: serial,
   253  	})
   254  	test.AssertError(t, err, "expected to error when signer was down")
   255  }
   256  

View as plain text