Add multi-issuer support to ocsp-responder (#5154)
The ocsp-responder takes a path to a certificate file as one of its config values. It uses this path as one of the inputs when constructing its DBSource, the object responsible for querying the database for pregenerated OCSP responses to fulfill requests. However, this certificate file is not necessary to query the database; rather, it only acts as a filter: OCSP requests whose IssuerKeyHash do not match the hash of the loaded certificate are rejected outright, without querying the DB. In addition, there is currently only support for a single certificate file in the config. This change adds support for multiple issuer certificate files in the config, and refactors the pre-database filtering of bad OCSP requests into a helper object dedicated solely to that purpose. Fixes #5119
This commit is contained in:
parent
16c7a21a57
commit
8cf597459d
|
|
@ -5,10 +5,10 @@ import (
|
|||
"context"
|
||||
"crypto"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -32,27 +32,100 @@ import (
|
|||
"github.com/letsencrypt/boulder/sa"
|
||||
)
|
||||
|
||||
/*
|
||||
DBSource maps a given Database schema to a CA Key Hash, so we can pick
|
||||
from among them when presented with OCSP requests for different certs.
|
||||
// ocspFilter stores information needed to filter OCSP requests (to ensure we
|
||||
// aren't trying to serve OCSP for certs which aren't ours), and surfaces one
|
||||
// boolean method to determine if a given request should be filtered or not.
|
||||
type ocspFilter struct {
|
||||
issuerKeyHashAlgorithm crypto.Hash
|
||||
issuerKeyHashes [][]byte
|
||||
serialPrefixes []string
|
||||
}
|
||||
|
||||
We assume that OCSP responses are stored in a very simple database table,
|
||||
with two columns: serialNumber and response
|
||||
// check returns a descriptive error if the request does not satisfy any of
|
||||
// the requirements of an OCSP request, or nil if the request should be handled.
|
||||
func (f *ocspFilter) check(req *ocsp.Request) error {
|
||||
if req.HashAlgorithm != f.issuerKeyHashAlgorithm {
|
||||
return fmt.Errorf("Request ca key hash using unsupported algorithm %s: %w", req.HashAlgorithm, bocsp.ErrNotFound)
|
||||
}
|
||||
// Check that this request is for the proper CA
|
||||
match := false
|
||||
for _, keyHash := range f.issuerKeyHashes {
|
||||
if match = bytes.Equal(req.IssuerKeyHash, keyHash); match {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return fmt.Errorf("Request intended for wrong issuer cert %s: %w", hex.EncodeToString(req.IssuerKeyHash), bocsp.ErrNotFound)
|
||||
}
|
||||
|
||||
CREATE TABLE ocsp_responses (serialNumber TEXT, response BLOB);
|
||||
serialString := core.SerialToString(req.SerialNumber)
|
||||
if len(f.serialPrefixes) > 0 {
|
||||
match := false
|
||||
for _, prefix := range f.serialPrefixes {
|
||||
if match = strings.HasPrefix(serialString, prefix); match {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return fmt.Errorf("Request serial has wrong prefix: %w", bocsp.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
The serialNumber field may have any type to which Go will match a string,
|
||||
so you can be more efficient than TEXT if you like. We use it to store the
|
||||
serial number in base64. You probably want to have an index on the
|
||||
serialNumber field, since we will always query on it.
|
||||
return nil
|
||||
}
|
||||
|
||||
*/
|
||||
type DBSource struct {
|
||||
dbMap dbSelector
|
||||
caKeyHash []byte
|
||||
reqSerialPrefixes []string
|
||||
timeout time.Duration
|
||||
log blog.Logger
|
||||
// newFilter creates a new ocspFilter which will filter out all requests for
|
||||
// certs which were not issued by one of the issuerCerts (here, paths to PEM
|
||||
// certs on disk) or which have a serial which does not start with one of the
|
||||
// given prefixes. The resulting filter will also reject all requests which
|
||||
// identify their issuer with a hash other than sha1.
|
||||
func newFilter(issuerCerts []string, serialPrefixes []string) (*ocspFilter, error) {
|
||||
if len(issuerCerts) < 1 {
|
||||
return nil, errors.New("Filter must include at least 1 issuer cert")
|
||||
}
|
||||
var issuerKeyHashes [][]byte
|
||||
for _, issuerCert := range issuerCerts {
|
||||
// Load the certificate from the file path.
|
||||
caCert, err := core.LoadCert(issuerCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not load issuer cert %s: %w", issuerCert, err)
|
||||
}
|
||||
// The issuerKeyHash in OCSP requests is constructed over the DER
|
||||
// encoding of the public key per RFC 6960 (defined in RFC 4055 for
|
||||
// RSA and RFC 5480 for ECDSA). We can't use MarshalPKIXPublicKey
|
||||
// for this since it encodes keys using the SPKI structure itself,
|
||||
// and we just want the contents of the subjectPublicKey for the
|
||||
// hash, so we need to extract it ourselves.
|
||||
var spki struct {
|
||||
Algo pkix.AlgorithmIdentifier
|
||||
BitString asn1.BitString
|
||||
}
|
||||
if _, err := asn1.Unmarshal(caCert.RawSubjectPublicKeyInfo, &spki); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyHash := sha1.Sum(spki.BitString.Bytes)
|
||||
issuerKeyHashes = append(issuerKeyHashes, keyHash[:])
|
||||
}
|
||||
return &ocspFilter{crypto.SHA1, issuerKeyHashes, serialPrefixes}, nil
|
||||
}
|
||||
|
||||
// dbSource represents a database containing pre-generated OCSP responses keyed
|
||||
// by serial number. It also allows for filtering requests by their issuer key
|
||||
// hash and serial number, to prevent unnecessary lookups for rows that we know
|
||||
// will not exist in the database.
|
||||
//
|
||||
// We assume that OCSP responses are stored in a very simple database table,
|
||||
// with at least these two columns: serialNumber (TEXT) and response (BLOB).
|
||||
//
|
||||
// The serialNumber field may have any type to which Go will match a string,
|
||||
// so you can be more efficient than TEXT if you like. We use it to store the
|
||||
// serial number in hex. You must have an index on the serialNumber field,
|
||||
// since we will always query on it.
|
||||
type dbSource struct {
|
||||
dbMap dbSelector
|
||||
filter *ocspFilter
|
||||
timeout time.Duration
|
||||
log blog.Logger
|
||||
}
|
||||
|
||||
// Define an interface with the needed methods from gorp.
|
||||
|
|
@ -62,56 +135,21 @@ type dbSelector interface {
|
|||
WithContext(ctx context.Context) gorp.SqlExecutor
|
||||
}
|
||||
|
||||
// NewSourceFromDatabase produces a DBSource representing the binding of a
|
||||
// given DB schema to a CA key.
|
||||
func NewSourceFromDatabase(
|
||||
dbMap dbSelector,
|
||||
caKeyHash []byte,
|
||||
reqSerialPrefixes []string,
|
||||
timeout time.Duration,
|
||||
log blog.Logger,
|
||||
) (src *DBSource, err error) {
|
||||
src = &DBSource{
|
||||
dbMap: dbMap,
|
||||
caKeyHash: caKeyHash,
|
||||
reqSerialPrefixes: reqSerialPrefixes,
|
||||
timeout: timeout,
|
||||
log: log,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Response is called by the HTTP server to handle a new OCSP request.
|
||||
func (src *DBSource) Response(req *ocsp.Request) ([]byte, http.Header, error) {
|
||||
if req.HashAlgorithm != crypto.SHA1 {
|
||||
// We only support SHA1 requests
|
||||
return nil, nil, bocsp.ErrNotFound
|
||||
}
|
||||
// Check that this request is for the proper CA
|
||||
if !bytes.Equal(req.IssuerKeyHash, src.caKeyHash) {
|
||||
src.log.Debugf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash))
|
||||
return nil, nil, bocsp.ErrNotFound
|
||||
func (src *dbSource) Response(req *ocsp.Request) ([]byte, http.Header, error) {
|
||||
err := src.filter.check(req)
|
||||
if err != nil {
|
||||
src.log.Debugf("Not responding to filtered OCSP request: %s", err.Error())
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
serialString := core.SerialToString(req.SerialNumber)
|
||||
if len(src.reqSerialPrefixes) > 0 {
|
||||
match := false
|
||||
for _, prefix := range src.reqSerialPrefixes {
|
||||
if match = strings.HasPrefix(serialString, prefix); match {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return nil, nil, bocsp.ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
src.log.Debugf("Searching for OCSP issued by us for serial %s", serialString)
|
||||
|
||||
var certStatus core.CertificateStatus
|
||||
defer func() {
|
||||
if len(certStatus.OCSPResponse) != 0 {
|
||||
src.log.Debugf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString)
|
||||
src.log.Debugf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(req.IssuerKeyHash), serialString)
|
||||
}
|
||||
}()
|
||||
ctx := context.Background()
|
||||
|
|
@ -120,7 +158,7 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, http.Header, error) {
|
|||
ctx, cancel = context.WithTimeout(ctx, src.timeout)
|
||||
defer cancel()
|
||||
}
|
||||
certStatus, err := sa.SelectCertificateStatus(src.dbMap.WithContext(ctx), serialString)
|
||||
certStatus, err = sa.SelectCertificateStatus(src.dbMap.WithContext(ctx), serialString)
|
||||
if err != nil {
|
||||
if db.IsNoRows(err) {
|
||||
return nil, nil, bocsp.ErrNotFound
|
||||
|
|
@ -129,7 +167,7 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, http.Header, error) {
|
|||
return nil, nil, err
|
||||
}
|
||||
if certStatus.OCSPLastUpdated.IsZero() {
|
||||
src.log.Debugf("OCSP Response not sent (ocspLastUpdated is zero) for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString)
|
||||
src.log.Debugf("OCSP Response not sent (ocspLastUpdated is zero) for CA=%s, Serial=%s", hex.EncodeToString(req.IssuerKeyHash), serialString)
|
||||
return nil, nil, bocsp.ErrNotFound
|
||||
} else if certStatus.IsExpired {
|
||||
return nil, nil, bocsp.ErrNotFound
|
||||
|
|
@ -137,35 +175,6 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, http.Header, error) {
|
|||
return certStatus.OCSPResponse, nil, nil
|
||||
}
|
||||
|
||||
func makeDBSource(dbMap dbSelector, issuerCert string, reqSerialPrefixes []string, timeout time.Duration, log blog.Logger) (*DBSource, error) {
|
||||
// Construct the key hash for the issuer
|
||||
caCertDER, err := cmd.LoadCert(issuerCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read issuer cert %s: %s", issuerCert, err)
|
||||
}
|
||||
caCert, err := x509.ParseCertificate(caCertDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not parse issuer cert %s: %s", issuerCert, err)
|
||||
}
|
||||
// The issuerKeyHash in OCSP requests is constructed over the DER
|
||||
// encoding of the public key per RFC 6960 (defined in RFC 4055 for
|
||||
// RSA and RFC 5480 for ECDSA). We can't use MarshalPKIXPublicKey
|
||||
// for this since it encodes keys using the SPKI structure itself,
|
||||
// and we just want the contents of the subjectPublicKey for the
|
||||
// hash, so we need to extract it ourselves.
|
||||
var spki struct {
|
||||
Algo pkix.AlgorithmIdentifier
|
||||
BitString asn1.BitString
|
||||
}
|
||||
if _, err := asn1.Unmarshal(caCert.RawSubjectPublicKeyInfo, &spki); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyHash := sha1.Sum(spki.BitString.Bytes)
|
||||
|
||||
// Construct a DB backed response source
|
||||
return NewSourceFromDatabase(dbMap, keyHash[:], reqSerialPrefixes, timeout, log)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
OCSPResponder struct {
|
||||
cmd.ServiceConfig
|
||||
|
|
@ -197,7 +206,9 @@ type config struct {
|
|||
Syslog cmd.SyslogConfig
|
||||
|
||||
Common struct {
|
||||
IssuerCert string
|
||||
// TODO(#5162): Remove singular IssuerCert config value.
|
||||
IssuerCert string
|
||||
IssuerCerts []string
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,13 +262,16 @@ as generated by Boulder's ceremony command.
|
|||
sa.SetSQLDebug(dbMap, logger)
|
||||
sa.InitDBMetrics(dbMap, stats)
|
||||
|
||||
source, err = makeDBSource(
|
||||
dbMap,
|
||||
c.Common.IssuerCert,
|
||||
c.OCSPResponder.RequiredSerialPrefixes,
|
||||
c.OCSPResponder.Timeout.Duration,
|
||||
logger)
|
||||
cmd.FailOnError(err, "Couldn't load OCSP DB")
|
||||
issuerCerts := c.Common.IssuerCerts
|
||||
if len(issuerCerts) == 0 {
|
||||
issuerCerts = []string{c.Common.IssuerCert}
|
||||
}
|
||||
|
||||
filter, err := newFilter(issuerCerts, c.OCSPResponder.RequiredSerialPrefixes)
|
||||
cmd.FailOnError(err, "Couldn't create OCSP filter")
|
||||
|
||||
source = &dbSource{dbMap, filter, c.OCSPResponder.Timeout.Duration, logger}
|
||||
|
||||
// Export the MaxDBConns
|
||||
dbConnStat := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "max_db_connections",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
|
|
@ -10,7 +11,6 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -34,6 +34,14 @@ var (
|
|||
stats = metrics.NoopRegisterer
|
||||
)
|
||||
|
||||
func mustRead(path string) []byte {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("read %#v: %s", path, err))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestMux(t *testing.T) {
|
||||
ocspReq, err := ocsp.ParseRequest(req)
|
||||
if err != nil {
|
||||
|
|
@ -81,11 +89,53 @@ func TestMux(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewFilter(t *testing.T) {
|
||||
_, err := newFilter([]string{}, []string{})
|
||||
test.AssertError(t, err, "Didn't error when creating empty filter")
|
||||
|
||||
_, err = newFilter([]string{"/tmp/doesnotexist.foo"}, []string{})
|
||||
test.AssertError(t, err, "Didn't error on non-existent issuer cert")
|
||||
|
||||
f, err := newFilter([]string{"./testdata/test-ca.der.pem"}, []string{"00"})
|
||||
test.AssertNotError(t, err, "Errored when creating good filter")
|
||||
test.AssertEquals(t, len(f.issuerKeyHashes), 1)
|
||||
test.AssertEquals(t, len(f.serialPrefixes), 1)
|
||||
}
|
||||
|
||||
func TestOcspFilter(t *testing.T) {
|
||||
f, err := newFilter([]string{"./testdata/test-ca.der.pem"}, []string{"00"})
|
||||
test.AssertNotError(t, err, "Errored when creating good filter")
|
||||
|
||||
ocspReq, err := ocsp.ParseRequest(req)
|
||||
test.AssertNotError(t, err, "Failed to prepare fake ocsp request")
|
||||
// Select a bad hash algorithm.
|
||||
ocspReq.HashAlgorithm = crypto.MD5
|
||||
err = f.check(ocspReq)
|
||||
test.AssertError(t, err, "Accepted ocsp request with bad hash algorithm")
|
||||
|
||||
ocspReq, err = ocsp.ParseRequest(req)
|
||||
test.AssertNotError(t, err, "Failed to prepare fake ocsp request")
|
||||
// Make the hash invalid.
|
||||
ocspReq.IssuerKeyHash[0]++
|
||||
err = f.check(ocspReq)
|
||||
test.AssertError(t, err, "Accepted ocsp request with bad issuer key hash")
|
||||
|
||||
ocspReq, err = ocsp.ParseRequest(req)
|
||||
test.AssertNotError(t, err, "Failed to prepare fake ocsp request")
|
||||
// Make the serial prefix wrong by incrementing the first byte by 1.
|
||||
serialStr := []byte(core.SerialToString(ocspReq.SerialNumber))
|
||||
serialStr[0] = serialStr[0] + 1
|
||||
ocspReq.SerialNumber.SetString(string(serialStr), 16)
|
||||
err = f.check(ocspReq)
|
||||
test.AssertError(t, err, "Accepted ocsp request with bad serial prefix")
|
||||
}
|
||||
|
||||
func TestDBHandler(t *testing.T) {
|
||||
src, err := makeDBSource(mockSelector{}, "./testdata/test-ca.der.pem", nil, time.Second, blog.NewMock())
|
||||
f, err := newFilter([]string{"./testdata/test-ca.der.pem"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("makeDBSource: %s", err)
|
||||
t.Fatalf("newFilter: %s", err)
|
||||
}
|
||||
src := &dbSource{mockSelector{}, f, time.Second, blog.NewMock()}
|
||||
|
||||
h := bocsp.NewResponder(src, stats, blog.NewMock())
|
||||
w := httptest.NewRecorder()
|
||||
|
|
@ -204,8 +254,11 @@ func (bs brokenSelector) WithContext(context.Context) gorp.SqlExecutor {
|
|||
|
||||
func TestErrorLog(t *testing.T) {
|
||||
mockLog := blog.NewMock()
|
||||
src, err := makeDBSource(brokenSelector{}, "./testdata/test-ca.der.pem", nil, time.Second, mockLog)
|
||||
test.AssertNotError(t, err, "Failed to create broken dbMap")
|
||||
f, err := newFilter([]string{"./testdata/test-ca.der.pem"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("newFilter: %s", err)
|
||||
}
|
||||
src := &dbSource{brokenSelector{}, f, time.Second, mockLog}
|
||||
|
||||
ocspReq, err := ocsp.ParseRequest(req)
|
||||
test.AssertNotError(t, err, "Failed to parse OCSP request")
|
||||
|
|
@ -216,33 +269,26 @@ func TestErrorLog(t *testing.T) {
|
|||
test.AssertEquals(t, len(mockLog.GetAllMatching("Looking up OCSP response")), 1)
|
||||
}
|
||||
|
||||
func mustRead(path string) []byte {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("open %#v: %s", path, err))
|
||||
}
|
||||
b, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("read all %#v: %s", path, err))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestRequiredSerialPrefix(t *testing.T) {
|
||||
mockLog := blog.NewMock()
|
||||
src, err := makeDBSource(mockSelector{}, "./testdata/test-ca.der.pem", []string{"nope"}, time.Second, mockLog)
|
||||
test.AssertNotError(t, err, "failed to create DBSource")
|
||||
f, err := newFilter([]string{"./testdata/test-ca.der.pem"}, []string{"nope"})
|
||||
if err != nil {
|
||||
t.Fatalf("newFilter: %s", err)
|
||||
}
|
||||
src := &dbSource{mockSelector{}, f, time.Second, blog.NewMock()}
|
||||
|
||||
ocspReq, err := ocsp.ParseRequest(req)
|
||||
test.AssertNotError(t, err, "Failed to parse OCSP request")
|
||||
|
||||
_, _, err = src.Response(ocspReq)
|
||||
test.AssertEquals(t, err, bocsp.ErrNotFound)
|
||||
test.AssertErrorIs(t, err, bocsp.ErrNotFound)
|
||||
|
||||
fmt.Println(core.SerialToString(ocspReq.SerialNumber))
|
||||
|
||||
src, err = makeDBSource(mockSelector{}, "./testdata/test-ca.der.pem", []string{"00", "nope"}, time.Second, mockLog)
|
||||
test.AssertNotError(t, err, "failed to create DBSource")
|
||||
f, err = newFilter([]string{"./testdata/test-ca.der.pem"}, []string{"00", "nope"})
|
||||
if err != nil {
|
||||
t.Fatalf("newFilter: %s", err)
|
||||
}
|
||||
src = &dbSource{mockSelector{}, f, time.Second, blog.NewMock()}
|
||||
_, _, err = src.Response(ocspReq)
|
||||
test.AssertNotError(t, err, "src.Response failed with acceptable prefix")
|
||||
}
|
||||
|
|
@ -263,18 +309,23 @@ func (es expiredSelector) WithContext(context.Context) gorp.SqlExecutor {
|
|||
}
|
||||
|
||||
func TestExpiredUnauthorized(t *testing.T) {
|
||||
src, err := makeDBSource(expiredSelector{}, "./testdata/test-ca.der.pem", []string{"00"}, time.Second, blog.NewMock())
|
||||
test.AssertNotError(t, err, "makeDBSource failed")
|
||||
f, err := newFilter([]string{"./testdata/test-ca.der.pem"}, []string{"00"})
|
||||
if err != nil {
|
||||
t.Fatalf("newFilter: %s", err)
|
||||
}
|
||||
src := &dbSource{expiredSelector{}, f, time.Second, blog.NewMock()}
|
||||
|
||||
ocspReq, err := ocsp.ParseRequest(req)
|
||||
test.AssertNotError(t, err, "Failed to parse OCSP request")
|
||||
|
||||
_, _, err = src.Response(ocspReq)
|
||||
test.AssertEquals(t, err, bocsp.ErrNotFound)
|
||||
test.AssertErrorIs(t, err, bocsp.ErrNotFound)
|
||||
}
|
||||
|
||||
func TestKeyHashing(t *testing.T) {
|
||||
src, err := makeDBSource(mockSelector{}, "./testdata/test-ca.der.pem", []string{"00"}, time.Second, blog.NewMock())
|
||||
test.AssertNotError(t, err, "makeDBSource failed")
|
||||
test.AssertEquals(t, hex.EncodeToString(src.caKeyHash), "fb784f12f96015832c9f177f3419b32e36ea4189")
|
||||
f, err := newFilter([]string{"./testdata/test-ca.der.pem"}, []string{"00"})
|
||||
if err != nil {
|
||||
t.Fatalf("newFilter: %s", err)
|
||||
}
|
||||
test.AssertEquals(t, hex.EncodeToString(f.issuerKeyHashes[0]), "fb784f12f96015832c9f177f3419b32e36ea4189")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ func (rs Responder) ServeHTTP(response http.ResponseWriter, request *http.Reques
|
|||
// Look up OCSP response from source
|
||||
ocspResponse, headers, err := rs.Source.Response(ocspRequest)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
rs.log.Infof("No response found for request: serial %x, request body %s",
|
||||
ocspRequest.SerialNumber, b64Body)
|
||||
response.Write(ocsp.UnauthorizedErrorResponse)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@
|
|||
},
|
||||
|
||||
"common": {
|
||||
"issuerCert": "/tmp/intermediate-cert-rsa-a.pem"
|
||||
"issuerCerts": [
|
||||
"/tmp/intermediate-cert-rsa-a.pem",
|
||||
"/tmp/intermediate-cert-rsa-b.pem",
|
||||
"/tmp/intermediate-cert-ecdsa-a.pem"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,67 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
func TestPrecertificateOCSP(t *testing.T) {
|
||||
// TODO(#5172): Fill out these test stubs.
|
||||
func TestOCSPBadRequestMethod(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPBadGetUrl(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPBadGetBody(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPBadPostBody(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPBadHashAlgorithm(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPBadIssuerCert(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPBadSerialPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
domain := random_domain()
|
||||
os.Setenv("DIRECTORY", "http://boulder:4001/directory")
|
||||
res, err := authAndIssue(nil, nil, []string{domain})
|
||||
if err != nil || len(res.certs) < 1 {
|
||||
t.Fatal("Failed to issue dummy cert for OCSP testing")
|
||||
}
|
||||
cert := res.certs[0]
|
||||
// Increment the first byte of the cert's serial number by 1, making the
|
||||
// prefix invalid. This works because ocsp_helper.Req (and the underlying
|
||||
// ocsp.CreateRequest) completely ignore the cert's .Raw value.
|
||||
serialStr := []byte(core.SerialToString(cert.SerialNumber))
|
||||
ocspConfig := ocsp_helper.DefaultConfig.WithOutput(ioutil.Discard)
|
||||
serialStr[0] = serialStr[0] + 1
|
||||
cert.SerialNumber.SetString(string(serialStr), 16)
|
||||
_, err = ocsp_helper.Req(cert, ocspConfig)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error getting OCSP for request with invalid serial")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCSPNonexistentSerial(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPExpiredCert(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestOCSPRejectedPrecertificate(t *testing.T) {
|
||||
t.Parallel()
|
||||
domain := random_domain()
|
||||
err := ctAddRejectHost(domain)
|
||||
|
|
@ -23,9 +79,8 @@ func TestPrecertificateOCSP(t *testing.T) {
|
|||
os.Setenv("DIRECTORY", "http://boulder:4001/directory")
|
||||
_, err = authAndIssue(nil, nil, []string{domain})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") &&
|
||||
strings.Contains(err.Error(), "SCT embedding") {
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") ||
|
||||
!strings.Contains(err.Error(), "SCT embedding") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ stale.
|
|||
os.Exit(0)
|
||||
}
|
||||
for _, f := range flag.Args() {
|
||||
_, err := helper.Req(f, helper.ConfigFromFlags())
|
||||
_, err := helper.ReqFile(f, helper.ConfigFromFlags())
|
||||
if err != nil {
|
||||
log.Printf("error for %s: %s\n", f, err)
|
||||
errors = true
|
||||
|
|
|
|||
|
|
@ -160,9 +160,9 @@ func parseCMS(body []byte) (*x509.Certificate, error) {
|
|||
return cert, nil
|
||||
}
|
||||
|
||||
// Req makes an OCSP request using the given config for the PEM certificate in
|
||||
// fileName, and returns the response.
|
||||
func Req(fileName string, config Config) (*ocsp.Response, error) {
|
||||
// ReqFle makes an OCSP request using the given config for the PEM-encoded
|
||||
// certificate in fileName, and returns the response.
|
||||
func ReqFile(fileName string, config Config) (*ocsp.Response, error) {
|
||||
contents, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -180,12 +180,15 @@ func ReqDER(der []byte, config Config) (*ocsp.Response, error) {
|
|||
if time.Now().After(cert.NotAfter) {
|
||||
if config.ignoreExpiredCerts {
|
||||
return nil, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("certificate expired %s ago: %s",
|
||||
time.Since(cert.NotAfter), cert.NotAfter)
|
||||
}
|
||||
return nil, fmt.Errorf("certificate expired %s ago: %s", time.Since(cert.NotAfter), cert.NotAfter)
|
||||
}
|
||||
return Req(cert, config)
|
||||
}
|
||||
|
||||
// Req makes an OCSP request using the given config for the given in-memory
|
||||
// certificate, and returns the response.
|
||||
func Req(cert *x509.Certificate, config Config) (*ocsp.Response, error) {
|
||||
issuer, err := getIssuer(cert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting issuer: %s", err)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func init() {
|
|||
|
||||
func do(f string) {
|
||||
start := time.Now()
|
||||
resp, err := helper.Req(f, helper.ConfigFromFlags())
|
||||
resp, err := helper.ReqFile(f, helper.ConfigFromFlags())
|
||||
latency := time.Since(start)
|
||||
if err != nil {
|
||||
errors_count.With(prom.Labels{}).Inc()
|
||||
|
|
|
|||
Loading…
Reference in New Issue