295 lines
9.8 KiB
Go
295 lines
9.8 KiB
Go
package redis
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ocsp"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
"github.com/letsencrypt/boulder/db"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/metrics"
|
|
"github.com/letsencrypt/boulder/ocsp/responder"
|
|
ocsp_test "github.com/letsencrypt/boulder/ocsp/test"
|
|
"github.com/letsencrypt/boulder/sa"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/letsencrypt/boulder/test"
|
|
)
|
|
|
|
// echoSource implements rocspSourceInterface, returning the provided response
|
|
// and panicking if signAndSave is called.
|
|
type echoSource struct {
|
|
resp *ocsp.Response
|
|
}
|
|
|
|
func (es echoSource) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
|
|
return &responder.Response{Response: es.resp, Raw: es.resp.Raw}, nil
|
|
}
|
|
|
|
func (es echoSource) signAndSave(ctx context.Context, req *ocsp.Request, cause signAndSaveCause) (*responder.Response, error) {
|
|
panic("should not happen")
|
|
}
|
|
|
|
// recordingEchoSource acts like echoSource, but instead of panicking on signAndSave,
|
|
// it records the serial number it was called with and returns the given secondResp.
|
|
type recordingEchoSource struct {
|
|
echoSource
|
|
secondResp *responder.Response
|
|
ch chan string
|
|
}
|
|
|
|
func (res recordingEchoSource) signAndSave(ctx context.Context, req *ocsp.Request, cause signAndSaveCause) (*responder.Response, error) {
|
|
res.ch <- req.SerialNumber.String()
|
|
return res.secondResp, nil
|
|
}
|
|
|
|
// errorSource implements rocspSourceInterface, and always returns an error.
|
|
type errorSource struct{}
|
|
|
|
func (es errorSource) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
|
|
return nil, errors.New("sad trombone")
|
|
}
|
|
|
|
func (es errorSource) signAndSave(ctx context.Context, req *ocsp.Request, cause signAndSaveCause) (*responder.Response, error) {
|
|
panic("should not happen")
|
|
}
|
|
|
|
// echoSelector always returns the given certificateStatus.
|
|
type echoSelector struct {
|
|
db.MockSqlExecutor
|
|
status sa.RevocationStatusModel
|
|
}
|
|
|
|
func (s echoSelector) SelectOne(_ context.Context, output interface{}, _ string, _ ...interface{}) error {
|
|
outputPtr, ok := output.(*sa.RevocationStatusModel)
|
|
if !ok {
|
|
return fmt.Errorf("incorrect output type %T", output)
|
|
}
|
|
*outputPtr = s.status
|
|
return nil
|
|
}
|
|
|
|
// errorSelector always returns an error.
|
|
type errorSelector struct {
|
|
db.MockSqlExecutor
|
|
}
|
|
|
|
func (s errorSelector) SelectOne(_ context.Context, _ interface{}, _ string, _ ...interface{}) error {
|
|
return errors.New("oops")
|
|
}
|
|
|
|
// notFoundSelector always returns an NoRows error.
|
|
type notFoundSelector struct {
|
|
db.MockSqlExecutor
|
|
}
|
|
|
|
func (s notFoundSelector) SelectOne(_ context.Context, _ interface{}, _ string, _ ...interface{}) error {
|
|
return db.ErrDatabaseOp{Err: sql.ErrNoRows}
|
|
}
|
|
|
|
// echoSA always returns the given revocation status.
|
|
type echoSA struct {
|
|
sapb.StorageAuthorityReadOnlyClient
|
|
status *sapb.RevocationStatus
|
|
}
|
|
|
|
func (s *echoSA) GetRevocationStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.RevocationStatus, error) {
|
|
return s.status, nil
|
|
}
|
|
|
|
// errorSA always returns an error.
|
|
type errorSA struct {
|
|
sapb.StorageAuthorityReadOnlyClient
|
|
}
|
|
|
|
func (s *errorSA) GetRevocationStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.RevocationStatus, error) {
|
|
return nil, errors.New("oops")
|
|
}
|
|
|
|
// notFoundSA always returns a NotFound error.
|
|
type notFoundSA struct {
|
|
sapb.StorageAuthorityReadOnlyClient
|
|
}
|
|
|
|
func (s *notFoundSA) GetRevocationStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.RevocationStatus, error) {
|
|
return nil, berrors.NotFoundError("purged")
|
|
}
|
|
|
|
func TestCheckedRedisSourceSuccess(t *testing.T) {
|
|
serial := big.NewInt(17777)
|
|
thisUpdate := time.Now().Truncate(time.Second).UTC()
|
|
|
|
resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
|
|
SerialNumber: serial,
|
|
Status: ocsp.Good,
|
|
ThisUpdate: thisUpdate,
|
|
})
|
|
test.AssertNotError(t, err, "making fake response")
|
|
|
|
status := sa.RevocationStatusModel{
|
|
Status: core.OCSPStatusGood,
|
|
}
|
|
src := newCheckedRedisSource(echoSource{resp: resp}, echoSelector{status: status}, nil, metrics.NoopRegisterer, blog.NewMock())
|
|
responderResponse, err := src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertNotError(t, err, "getting response")
|
|
test.AssertEquals(t, responderResponse.SerialNumber.String(), resp.SerialNumber.String())
|
|
}
|
|
|
|
func TestCheckedRedisSourceDBError(t *testing.T) {
|
|
serial := big.NewInt(404040)
|
|
thisUpdate := time.Now().Truncate(time.Second).UTC()
|
|
|
|
resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
|
|
SerialNumber: serial,
|
|
Status: ocsp.Good,
|
|
ThisUpdate: thisUpdate,
|
|
})
|
|
test.AssertNotError(t, err, "making fake response")
|
|
|
|
src := newCheckedRedisSource(echoSource{resp: resp}, errorSelector{}, nil, metrics.NoopRegisterer, blog.NewMock())
|
|
_, err = src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertError(t, err, "getting response")
|
|
test.AssertContains(t, err.Error(), "oops")
|
|
|
|
src = newCheckedRedisSource(echoSource{resp: resp}, notFoundSelector{}, nil, metrics.NoopRegisterer, blog.NewMock())
|
|
_, err = src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertError(t, err, "getting response")
|
|
test.AssertErrorIs(t, err, responder.ErrNotFound)
|
|
}
|
|
|
|
func TestCheckedRedisSourceSAError(t *testing.T) {
|
|
serial := big.NewInt(404040)
|
|
thisUpdate := time.Now().Truncate(time.Second).UTC()
|
|
|
|
resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
|
|
SerialNumber: serial,
|
|
Status: ocsp.Good,
|
|
ThisUpdate: thisUpdate,
|
|
})
|
|
test.AssertNotError(t, err, "making fake response")
|
|
|
|
src := newCheckedRedisSource(echoSource{resp: resp}, nil, &errorSA{}, metrics.NoopRegisterer, blog.NewMock())
|
|
_, err = src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertError(t, err, "getting response")
|
|
test.AssertContains(t, err.Error(), "oops")
|
|
|
|
src = newCheckedRedisSource(echoSource{resp: resp}, nil, ¬FoundSA{}, metrics.NoopRegisterer, blog.NewMock())
|
|
_, err = src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertError(t, err, "getting response")
|
|
test.AssertErrorIs(t, err, responder.ErrNotFound)
|
|
}
|
|
|
|
func TestCheckedRedisSourceRedisError(t *testing.T) {
|
|
serial := big.NewInt(314159262)
|
|
|
|
status := sa.RevocationStatusModel{
|
|
Status: core.OCSPStatusGood,
|
|
}
|
|
src := newCheckedRedisSource(errorSource{}, echoSelector{status: status}, nil, metrics.NoopRegisterer, blog.NewMock())
|
|
_, err := src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertError(t, err, "getting response")
|
|
}
|
|
|
|
func TestCheckedRedisStatusDisagreement(t *testing.T) {
|
|
serial := big.NewInt(2718)
|
|
thisUpdate := time.Now().Truncate(time.Second).UTC()
|
|
|
|
resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
|
|
SerialNumber: serial,
|
|
Status: ocsp.Good,
|
|
ThisUpdate: thisUpdate.Add(-time.Minute),
|
|
})
|
|
test.AssertNotError(t, err, "making fake response")
|
|
|
|
secondResp, _, err := ocsp_test.FakeResponse(ocsp.Response{
|
|
SerialNumber: serial,
|
|
Status: ocsp.Revoked,
|
|
RevokedAt: thisUpdate,
|
|
RevocationReason: ocsp.KeyCompromise,
|
|
ThisUpdate: thisUpdate,
|
|
})
|
|
test.AssertNotError(t, err, "making fake response")
|
|
status := sa.RevocationStatusModel{
|
|
Status: core.OCSPStatusRevoked,
|
|
RevokedDate: thisUpdate,
|
|
RevokedReason: ocsp.KeyCompromise,
|
|
}
|
|
source := recordingEchoSource{
|
|
echoSource: echoSource{resp: resp},
|
|
secondResp: &responder.Response{Response: secondResp, Raw: secondResp.Raw},
|
|
ch: make(chan string, 1),
|
|
}
|
|
src := newCheckedRedisSource(source, echoSelector{status: status}, nil, metrics.NoopRegisterer, blog.NewMock())
|
|
fetchedResponse, err := src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertNotError(t, err, "getting re-signed response")
|
|
test.Assert(t, fetchedResponse.ThisUpdate.Equal(thisUpdate), "thisUpdate not updated")
|
|
test.AssertEquals(t, fetchedResponse.SerialNumber.String(), serial.String())
|
|
test.AssertEquals(t, fetchedResponse.RevokedAt, thisUpdate)
|
|
test.AssertEquals(t, fetchedResponse.RevocationReason, ocsp.KeyCompromise)
|
|
test.AssertEquals(t, fetchedResponse.ThisUpdate, thisUpdate)
|
|
}
|
|
|
|
func TestCheckedRedisStatusSADisagreement(t *testing.T) {
|
|
serial := big.NewInt(2718)
|
|
thisUpdate := time.Now().Truncate(time.Second).UTC()
|
|
|
|
resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
|
|
SerialNumber: serial,
|
|
Status: ocsp.Good,
|
|
ThisUpdate: thisUpdate.Add(-time.Minute),
|
|
})
|
|
test.AssertNotError(t, err, "making fake response")
|
|
|
|
secondResp, _, err := ocsp_test.FakeResponse(ocsp.Response{
|
|
SerialNumber: serial,
|
|
Status: ocsp.Revoked,
|
|
RevokedAt: thisUpdate,
|
|
RevocationReason: ocsp.KeyCompromise,
|
|
ThisUpdate: thisUpdate,
|
|
})
|
|
test.AssertNotError(t, err, "making fake response")
|
|
statusPB := sapb.RevocationStatus{
|
|
Status: 1,
|
|
RevokedDate: timestamppb.New(thisUpdate),
|
|
RevokedReason: ocsp.KeyCompromise,
|
|
}
|
|
source := recordingEchoSource{
|
|
echoSource: echoSource{resp: resp},
|
|
secondResp: &responder.Response{Response: secondResp, Raw: secondResp.Raw},
|
|
ch: make(chan string, 1),
|
|
}
|
|
src := newCheckedRedisSource(source, nil, &echoSA{status: &statusPB}, metrics.NoopRegisterer, blog.NewMock())
|
|
fetchedResponse, err := src.Response(context.Background(), &ocsp.Request{
|
|
SerialNumber: serial,
|
|
})
|
|
test.AssertNotError(t, err, "getting re-signed response")
|
|
test.Assert(t, fetchedResponse.ThisUpdate.Equal(thisUpdate), "thisUpdate not updated")
|
|
test.AssertEquals(t, fetchedResponse.SerialNumber.String(), serial.String())
|
|
test.AssertEquals(t, fetchedResponse.RevokedAt, thisUpdate)
|
|
test.AssertEquals(t, fetchedResponse.RevocationReason, ocsp.KeyCompromise)
|
|
test.AssertEquals(t, fetchedResponse.ThisUpdate, thisUpdate)
|
|
}
|