...

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

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

     1  // Package redis provides a Redis-based OCSP responder.
     2  //
     3  // This responder will first look for a response cached in Redis. If there is
     4  // no response, or the response is too old, it will make a request to the RA
     5  // for a freshly-signed response. If that succeeds, this responder will return
     6  // the response to the user right away, while storing a copy to Redis in a
     7  // separate goroutine.
     8  //
     9  // If the response was too old, but the request to the RA failed, this
    10  // responder will serve the response anyhow. This allows for graceful
    11  // degradation: it is better to serve a response that is 5 days old (outside
    12  // the Baseline Requirements limits) than to serve no response at all.
    13  // It's assumed that this will be wrapped in a responder.filterSource, which
    14  // means that if a response is past its NextUpdate, we'll generate a 500.
    15  package redis
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"time"
    21  
    22  	"github.com/jmhodges/clock"
    23  	"github.com/letsencrypt/boulder/core"
    24  	blog "github.com/letsencrypt/boulder/log"
    25  	"github.com/letsencrypt/boulder/ocsp/responder"
    26  	"github.com/letsencrypt/boulder/rocsp"
    27  	"github.com/prometheus/client_golang/prometheus"
    28  	"golang.org/x/crypto/ocsp"
    29  
    30  	berrors "github.com/letsencrypt/boulder/errors"
    31  )
    32  
    33  type rocspClient interface {
    34  	GetResponse(ctx context.Context, serial string) ([]byte, error)
    35  	StoreResponse(ctx context.Context, resp *ocsp.Response) error
    36  }
    37  
    38  type redisSource struct {
    39  	client             rocspClient
    40  	signer             responder.Source
    41  	counter            *prometheus.CounterVec
    42  	signAndSaveCounter *prometheus.CounterVec
    43  	cachedResponseAges prometheus.Histogram
    44  	clk                clock.Clock
    45  	liveSigningPeriod  time.Duration
    46  	// Error logs will be emitted at a rate of 1 in logSampleRate.
    47  	// If logSampleRate is 0, no logs will be emitted.
    48  	logSampleRate int
    49  	// Note: this logger is not currently used, as all audit log events are from
    50  	// the dbSource right now, but it should and will be used in the future.
    51  	log blog.Logger
    52  }
    53  
    54  // NewRedisSource returns a responder.Source which will look up OCSP responses in a
    55  // Redis table.
    56  func NewRedisSource(
    57  	client *rocsp.RWClient,
    58  	signer responder.Source,
    59  	liveSigningPeriod time.Duration,
    60  	clk clock.Clock,
    61  	stats prometheus.Registerer,
    62  	log blog.Logger,
    63  	logSampleRate int,
    64  ) (*redisSource, error) {
    65  	counter := prometheus.NewCounterVec(prometheus.CounterOpts{
    66  		Name: "ocsp_redis_responses",
    67  		Help: "Count of OCSP requests/responses by action taken by the redisSource",
    68  	}, []string{"result"})
    69  	stats.MustRegister(counter)
    70  
    71  	signAndSaveCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
    72  		Name: "ocsp_redis_sign_and_save",
    73  		Help: "Count of OCSP sign and save requests",
    74  	}, []string{"cause", "result"})
    75  	stats.MustRegister(signAndSaveCounter)
    76  
    77  	// Set up 12-hour-wide buckets, measured in seconds.
    78  	buckets := make([]float64, 14)
    79  	for i := range buckets {
    80  		buckets[i] = 43200 * float64(i)
    81  	}
    82  
    83  	cachedResponseAges := prometheus.NewHistogram(prometheus.HistogramOpts{
    84  		Name:    "ocsp_redis_cached_response_ages",
    85  		Help:    "How old are the cached OCSP responses when we successfully retrieve them.",
    86  		Buckets: buckets,
    87  	})
    88  	stats.MustRegister(cachedResponseAges)
    89  
    90  	var rocspReader rocspClient
    91  	if client != nil {
    92  		rocspReader = client
    93  	}
    94  	return &redisSource{
    95  		client:             rocspReader,
    96  		signer:             signer,
    97  		counter:            counter,
    98  		signAndSaveCounter: signAndSaveCounter,
    99  		cachedResponseAges: cachedResponseAges,
   100  		liveSigningPeriod:  liveSigningPeriod,
   101  		clk:                clk,
   102  		log:                log,
   103  	}, nil
   104  }
   105  
   106  // Response implements the responder.Source interface. It looks up the requested OCSP
   107  // response in the redis cluster.
   108  func (src *redisSource) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
   109  	serialString := core.SerialToString(req.SerialNumber)
   110  
   111  	respBytes, err := src.client.GetResponse(ctx, serialString)
   112  	if err != nil {
   113  		if errors.Is(err, rocsp.ErrRedisNotFound) {
   114  			src.counter.WithLabelValues("not_found").Inc()
   115  		} else {
   116  			src.counter.WithLabelValues("lookup_error").Inc()
   117  			responder.SampledError(src.log, src.logSampleRate, "looking for cached response: %s", err)
   118  			// Proceed despite the error; when Redis is down we'd like to limp along with live signing
   119  			// rather than returning an error to the client.
   120  		}
   121  		return src.signAndSave(ctx, req, causeNotFound)
   122  	}
   123  
   124  	resp, err := ocsp.ParseResponse(respBytes, nil)
   125  	if err != nil {
   126  		src.counter.WithLabelValues("parse_error").Inc()
   127  		return nil, err
   128  	}
   129  
   130  	if src.isStale(resp) {
   131  		src.counter.WithLabelValues("stale").Inc()
   132  		freshResp, err := src.signAndSave(ctx, req, causeStale)
   133  		// Note: we could choose to return the stale response (up to its actual
   134  		// NextUpdate date), but if we pass the BR/root program limits, that
   135  		// becomes a compliance problem; returning an error is an availability
   136  		// problem and only becomes a compliance problem if we serve too many
   137  		// of them for too long (the exact conditions are not clearly defined
   138  		// by the BRs or root programs).
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  		return freshResp, nil
   143  	}
   144  
   145  	src.counter.WithLabelValues("success").Inc()
   146  	return &responder.Response{Response: resp, Raw: respBytes}, nil
   147  }
   148  
   149  func (src *redisSource) isStale(resp *ocsp.Response) bool {
   150  	age := src.clk.Since(resp.ThisUpdate)
   151  	src.cachedResponseAges.Observe(age.Seconds())
   152  	return age > src.liveSigningPeriod
   153  }
   154  
   155  type signAndSaveCause string
   156  
   157  const (
   158  	causeStale    signAndSaveCause = "stale"
   159  	causeNotFound signAndSaveCause = "not_found"
   160  	causeMismatch signAndSaveCause = "mismatch"
   161  )
   162  
   163  func (src *redisSource) signAndSave(ctx context.Context, req *ocsp.Request, cause signAndSaveCause) (*responder.Response, error) {
   164  	resp, err := src.signer.Response(ctx, req)
   165  	if errors.Is(err, responder.ErrNotFound) {
   166  		src.signAndSaveCounter.WithLabelValues(string(cause), "certificate_not_found").Inc()
   167  		return nil, responder.ErrNotFound
   168  	} else if errors.Is(err, berrors.UnknownSerial) {
   169  		// UnknownSerial is more interesting than NotFound, because it means we don't
   170  		// have a record in the `serials` table, which is kept longer-term than the
   171  		// `certificateStatus` table. That could mean someone is making up silly serial
   172  		// numbers in their requests to us, or it could mean there's site on the internet
   173  		// using a certificate that we don't have a record of in the `serials` table.
   174  		src.signAndSaveCounter.WithLabelValues(string(cause), "unknown_serial").Inc()
   175  		responder.SampledError(src.log, src.logSampleRate, "unknown serial: %s", core.SerialToString(req.SerialNumber))
   176  		return nil, responder.ErrNotFound
   177  	} else if err != nil {
   178  		src.signAndSaveCounter.WithLabelValues(string(cause), "signing_error").Inc()
   179  		return nil, err
   180  	}
   181  	src.signAndSaveCounter.WithLabelValues(string(cause), "signing_success").Inc()
   182  	go func() {
   183  		// We don't care about the error here, because if storing the response
   184  		// fails, we'll just generate a new one on the next request.
   185  		_ = src.client.StoreResponse(context.Background(), resp.Response)
   186  	}()
   187  	return resp, nil
   188  }
   189  

View as plain text