diff --git a/cmd/admin-revoker/main_test.go b/cmd/admin-revoker/main_test.go index 76043114f..e0d77dbb4 100644 --- a/cmd/admin-revoker/main_test.go +++ b/cmd/admin-revoker/main_test.go @@ -27,6 +27,8 @@ import ( "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/ra" + "github.com/letsencrypt/boulder/rocsp" + rocsp_config "github.com/letsencrypt/boulder/rocsp/config" "github.com/letsencrypt/boulder/sa" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/sa/satest" @@ -61,7 +63,11 @@ func TestRevokeBatch(t *testing.T) { if err != nil { t.Fatalf("Failed to create dbMap: %s", err) } - ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1) + rocspIssuers, err := rocsp_config.LoadIssuers(map[string]int{ + "../../test/hierarchy/int-r3.cert.pem": 102, + }) + test.AssertNotError(t, err, "error loading issuers") + ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, rocsp.NewMockWriteSucceedClient(), rocspIssuers, fc, log, metrics.NoopRegisterer, 1) if err != nil { t.Fatalf("Failed to create SA: %s", err) } @@ -503,8 +509,11 @@ func setup(t *testing.T) testCtx { if err != nil { t.Fatalf("Failed to create dbMap: %s", err) } - - ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1) + rocspIssuers, err := rocsp_config.LoadIssuers(map[string]int{ + "../../test/hierarchy/int-r3.cert.pem": 102, + }) + test.AssertNotError(t, err, "error loading issuers") + ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, rocsp.NewMockWriteSucceedClient(), rocspIssuers, fc, log, metrics.NoopRegisterer, 1) if err != nil { t.Fatalf("Failed to create SA: %s", err) } diff --git a/cmd/boulder-sa/main.go b/cmd/boulder-sa/main.go index 456e5805e..8abf08f25 100644 --- a/cmd/boulder-sa/main.go +++ b/cmd/boulder-sa/main.go @@ -12,6 +12,8 @@ import ( "github.com/letsencrypt/boulder/db" "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" + "github.com/letsencrypt/boulder/rocsp" + rocsp_config "github.com/letsencrypt/boulder/rocsp/config" "github.com/letsencrypt/boulder/sa" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -21,6 +23,8 @@ type Config struct { cmd.ServiceConfig DB cmd.DBConfig ReadOnlyDB cmd.DBConfig + Redis *rocsp_config.RedisConfig + Issuers map[string]int Features map[string]bool @@ -81,11 +85,20 @@ func main() { clk := cmd.Clock() + redisConf := c.SA.Redis + var rocspWriteClient *rocsp.WritingClient + if redisConf != nil { + rocspWriteClient, err = rocsp_config.MakeClient(redisConf, clk, scope) + cmd.FailOnError(err, "making Redis client") + } + shortIssuers, err := rocsp_config.LoadIssuers(c.SA.Issuers) + cmd.FailOnError(err, "loading issuers") + parallel := c.SA.ParallelismPerRPC if parallel < 1 { parallel = 1 } - sai, err := sa.NewSQLStorageAuthority(dbMap, dbReadOnlyMap, clk, logger, scope, parallel) + sai, err := sa.NewSQLStorageAuthority(dbMap, dbReadOnlyMap, rocspWriteClient, shortIssuers, clk, logger, scope, parallel) cmd.FailOnError(err, "Failed to create SA impl") tls, err := c.SA.TLS.Load() diff --git a/cmd/cert-checker/main_test.go b/cmd/cert-checker/main_test.go index a3cc7e5b5..ed1c29e54 100644 --- a/cmd/cert-checker/main_test.go +++ b/cmd/cert-checker/main_test.go @@ -329,7 +329,7 @@ func TestGetAndProcessCerts(t *testing.T) { fc.Set(fc.Now().Add(time.Hour)) checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations) - sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, fc, blog.NewMock(), metrics.NoopRegisterer, 1) + sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, nil, nil, fc, blog.NewMock(), metrics.NoopRegisterer, 1) test.AssertNotError(t, err, "Couldn't create SA to insert certificates") saCleanUp := test.ResetSATestDatabase(t) defer func() { diff --git a/cmd/contact-auditor/main_test.go b/cmd/contact-auditor/main_test.go index 923910f8d..cd78af3fa 100644 --- a/cmd/contact-auditor/main_test.go +++ b/cmd/contact-auditor/main_test.go @@ -351,7 +351,7 @@ func setup(t *testing.T) testCtx { t.Fatalf("Couldn't connect to the database: %s", err) } - ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, clock.New(), log, metrics.NoopRegisterer, 1) + ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, clock.New(), log, metrics.NoopRegisterer, 1) if err != nil { t.Fatalf("unable to create SQLStorageAuthority: %s", err) } diff --git a/cmd/expiration-mailer/main_test.go b/cmd/expiration-mailer/main_test.go index 50756a51d..4ea208fec 100644 --- a/cmd/expiration-mailer/main_test.go +++ b/cmd/expiration-mailer/main_test.go @@ -801,7 +801,7 @@ func setup(t *testing.T, nagTimes []time.Duration) *testCtx { } fc := newFakeClock(t) - ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1) + ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1) if err != nil { t.Fatalf("unable to create SQLStorageAuthority: %s", err) } diff --git a/cmd/id-exporter/main_test.go b/cmd/id-exporter/main_test.go index 49f817ca7..afd50b1ce 100644 --- a/cmd/id-exporter/main_test.go +++ b/cmd/id-exporter/main_test.go @@ -449,7 +449,7 @@ func setup(t *testing.T) testCtx { cleanUp := test.ResetSATestDatabase(t) fc := newFakeClock(t) - ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1) + ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1) if err != nil { t.Fatalf("unable to create SQLStorageAuthority: %s", err) } diff --git a/ocsp/updater/updater_test.go b/ocsp/updater/updater_test.go index 7974fe774..73f13de34 100644 --- a/ocsp/updater/updater_test.go +++ b/ocsp/updater/updater_test.go @@ -62,7 +62,7 @@ func setup(t *testing.T) (*OCSPUpdater, sapb.StorageAuthorityClient, *db.Wrapped fc := clock.NewFake() fc.Add(1 * time.Hour) - sa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1) + sa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1) test.AssertNotError(t, err, "Failed to create SA") updater, err := New( diff --git a/ra/ra_test.go b/ra/ra_test.go index d560660a0..e3ab0096a 100644 --- a/ra/ra_test.go +++ b/ra/ra_test.go @@ -309,7 +309,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho if err != nil { t.Fatalf("Failed to create dbMap: %s", err) } - ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1) + ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1) if err != nil { t.Fatalf("Failed to create SA: %s", err) } diff --git a/rocsp/mocks.go b/rocsp/mocks.go new file mode 100644 index 000000000..f0b7991f2 --- /dev/null +++ b/rocsp/mocks.go @@ -0,0 +1,30 @@ +package rocsp + +import ( + "context" + "fmt" + "time" +) + +// MockWriteClient is a mock +type MockWriteClient struct { + StoreReponseReturnError error +} + +// StoreResponse mocks a rocsp.StoreResponse method and returns nil or an +// error depending on the desired state. +func (r MockWriteClient) StoreResponse(ctx context.Context, respBytes []byte, shortIssuerID byte, ttl time.Duration) error { + return r.StoreReponseReturnError +} + +// NewMockWriteSucceedClient returns a mock MockWriteClient with a +// StoreResponse method that will always succeed. +func NewMockWriteSucceedClient() MockWriteClient { + return MockWriteClient{nil} +} + +// NewMockWriteFailClient returns a mock MockWriteClient with a +// StoreResponse method that will always fail. +func NewMockWriteFailClient() MockWriteClient { + return MockWriteClient{StoreReponseReturnError: fmt.Errorf("could not store response")} +} diff --git a/sa/precertificates.go b/sa/precertificates.go index 59108dd31..3e927c48d 100644 --- a/sa/precertificates.go +++ b/sa/precertificates.go @@ -152,6 +152,24 @@ func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb if overallError != nil { return nil, overallError } + + // Store the OCSP response in Redis (if configured) on a best effort + // basis. We don't want to fail on an error here while mysql is the + // source of truth. + if ssa.rocspWriteClient != nil { + // Use a new context for the goroutine. We aren't going to wait on + // the goroutine to complete, so we don't want it to be canceled + // when the parent function ends. The rocsp client has a + // configurable timeout that can be set during creation. + rocspCtx := context.Background() + rocspTTL := parsed.NotAfter.Sub(ssa.clk.Now()) + + // Send the response off to redis in a goroutine. + go func() { + err = ssa.storeOCSPRedis(rocspCtx, req.Ocsp, req.IssuerID, rocspTTL) + ssa.log.Debugf("failed to store OCSP response in redis: %v", err) + }() + } return &emptypb.Empty{}, nil } diff --git a/sa/rocsp_sa.go b/sa/rocsp_sa.go new file mode 100644 index 000000000..d9d0bc32d --- /dev/null +++ b/sa/rocsp_sa.go @@ -0,0 +1,28 @@ +package sa + +import ( + "context" + "time" + + rocsp_config "github.com/letsencrypt/boulder/rocsp/config" +) + +type rocspWriter interface { + StoreResponse(ctx context.Context, respBytes []byte, shortIssuerID byte, ttl time.Duration) error +} + +// storeOCSPRedis stores an OCSP response in a redis cluster. +func (ssa *SQLStorageAuthority) storeOCSPRedis(ctx context.Context, resp []byte, issuerID int64, ttl time.Duration) error { + shortIssuerID, err := rocsp_config.FindIssuerByID(issuerID, ssa.shortIssuers) + if err != nil { + ssa.redisStoreResponse.WithLabelValues("find_issuer_error").Inc() + return err + } + err = ssa.rocspWriteClient.StoreResponse(ctx, resp, shortIssuerID.ShortID(), ttl) + if err != nil { + ssa.redisStoreResponse.WithLabelValues("store_response_error").Inc() + return err + } + ssa.redisStoreResponse.WithLabelValues("success").Inc() + return nil +} diff --git a/sa/rocsp_sa_test.go b/sa/rocsp_sa_test.go new file mode 100644 index 000000000..f95379f5c --- /dev/null +++ b/sa/rocsp_sa_test.go @@ -0,0 +1,39 @@ +package sa + +import ( + "context" + "testing" + "time" + + "github.com/letsencrypt/boulder/rocsp" + "github.com/letsencrypt/boulder/test" +) + +func TestStoreOCSPRedis(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + response := []byte{0, 0, 1} + ctx := context.Background() + err := sa.storeOCSPRedis(ctx, response, 58923463773186183, time.Hour) + test.AssertNotError(t, err, "unexpected error") +} + +func TestStoreOCSPRedisInvalidIssuer(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + response := []byte{0, 0, 1} + ctx := context.Background() + // 1234 is expected to not be a valid issuerID + err := sa.storeOCSPRedis(ctx, response, 1234, time.Hour) + test.AssertContains(t, err.Error(), "no issuer found for an ID in certificateStatus: 1234") +} + +func TestStoreOCSPRedisFail(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + sa.rocspWriteClient = rocsp.NewMockWriteFailClient() + response := []byte{0, 0, 1} + ctx := context.Background() + err := sa.storeOCSPRedis(ctx, response, 58923463773186183, time.Hour) + test.AssertContains(t, err.Error(), "could not store response") +} diff --git a/sa/sa.go b/sa/sa.go index aaaf1ded2..d6ec2d622 100644 --- a/sa/sa.go +++ b/sa/sa.go @@ -28,6 +28,7 @@ import ( "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/revocation" + rocsp_config "github.com/letsencrypt/boulder/rocsp/config" sapb "github.com/letsencrypt/boulder/sa/proto" ) @@ -40,8 +41,15 @@ type SQLStorageAuthority struct { sapb.UnimplementedStorageAuthorityServer dbMap *db.WrappedMap dbReadOnlyMap *db.WrappedMap - clk clock.Clock - log blog.Logger + + // Redis client for storing OCSP responses in Redis. + rocspWriteClient rocspWriter + + // Short issuer map used by rocsp. + shortIssuers []rocsp_config.ShortIDIssuer + + clk clock.Clock + log blog.Logger // For RPCs that generate multiple, parallelizable SQL queries, this is the // max parallelism they will use (to avoid consuming too many MariaDB @@ -58,6 +66,10 @@ type SQLStorageAuthority struct { // transactions fail and so use this stat to maintain visibility into the rate // this occurs. rateLimitWriteErrors prometheus.Counter + + // redisStoreResponse is a counter of OCSP responses written to redis by + // result. + redisStoreResponse *prometheus.CounterVec } // orderFQDNSet contains the SHA256 hash of the lowercased, comma joined names @@ -77,6 +89,8 @@ type orderFQDNSet struct { func NewSQLStorageAuthority( dbMap *db.WrappedMap, dbReadOnlyMap *db.WrappedMap, + rocspWriteClient rocspWriter, + shortIssuers []rocsp_config.ShortIDIssuer, clk clock.Clock, logger blog.Logger, stats prometheus.Registerer, @@ -90,13 +104,22 @@ func NewSQLStorageAuthority( }) stats.MustRegister(rateLimitWriteErrors) + redisStoreResponse := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "redis_store_response", + Help: "Count of OCSP Response writes to redis", + }, []string{"result"}) + stats.MustRegister(redisStoreResponse) + ssa := &SQLStorageAuthority{ dbMap: dbMap, dbReadOnlyMap: dbReadOnlyMap, + rocspWriteClient: rocspWriteClient, + shortIssuers: shortIssuers, clk: clk, log: logger, parallelismPerRPC: parallelismPerRPC, rateLimitWriteErrors: rateLimitWriteErrors, + redisStoreResponse: redisStoreResponse, } ssa.countCertificatesByName = ssa.countCertificates diff --git a/sa/sa_test.go b/sa/sa_test.go index 262ccd3e6..4526f8f31 100644 --- a/sa/sa_test.go +++ b/sa/sa_test.go @@ -29,6 +29,8 @@ import ( blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/probs" + "github.com/letsencrypt/boulder/rocsp" + rocsp_config "github.com/letsencrypt/boulder/rocsp/config" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test/vars" @@ -65,7 +67,17 @@ func initSA(t *testing.T) (*SQLStorageAuthority, clock.FakeClock, func()) { fc := clock.NewFake() fc.Set(time.Date(2015, 3, 4, 5, 0, 0, 0, time.UTC)) - sa, err := NewSQLStorageAuthority(dbMap, dbMap, fc, log, metrics.NoopRegisterer, 1) + // Load the standard list of signing certificates from the hierarchy. + rocspIssuers, err := rocsp_config.LoadIssuers(map[string]int{ + "../test/hierarchy/int-e1.cert.pem": 100, + "../test/hierarchy/int-e2.cert.pem": 101, + "../test/hierarchy/int-r3.cert.pem": 102, + "../test/hierarchy/int-r4.cert.pem": 103, + }) + if err != nil { + t.Fatalf("failed to load issuers: %s", err) + } + sa, err := NewSQLStorageAuthority(dbMap, dbMap, rocsp.NewMockWriteSucceedClient(), rocspIssuers, fc, log, metrics.NoopRegisterer, 1) if err != nil { t.Fatalf("Failed to create SA: %s", err) } diff --git a/test/config-next/sa.json b/test/config-next/sa.json index 656ebcd58..6da4976e2 100644 --- a/test/config-next/sa.json +++ b/test/config-next/sa.json @@ -29,6 +29,25 @@ "wfe.boulder" ] }, + "redis": { + "username": "boulder-sa", + "passwordFile": "test/secrets/sa_redis_password", + "addrs": [ + "10.33.33.7:4218" + ], + "timeout": "5s", + "tls": { + "caCertFile": "test/redis-tls/minica.pem", + "certFile": "test/redis-tls/boulder/cert.pem", + "keyFile": "test/redis-tls/boulder/key.pem" + } + }, + "issuers": { + ".hierarchy/intermediate-cert-ecdsa-a.pem": 1, + ".hierarchy/intermediate-cert-ecdsa-b.pem": 2, + ".hierarchy/intermediate-cert-rsa-a.pem": 3, + ".hierarchy/intermediate-cert-rsa-b.pem": 4 + }, "features": { "FasterNewOrdersRateLimit": true, "StoreRevokerInfo": true, diff --git a/test/redis.config b/test/redis.config index 6143f0771..ae59f4bcb 100644 --- a/test/redis.config +++ b/test/redis.config @@ -22,6 +22,7 @@ rename-command SREM "" user default off user ocsp-updater on +@all ~* >e4e9ce7845cb6adbbc44fb1d9deb05e6b4dc1386 user ocsp-responder on +@all ~* >0e5a4c8b5faaf3194c8ad83c3dd9a0dd8a75982b +user boulder-sa on +@all ~* >de75ae663596735b90e461e5924f71a4c5f622ab user boulder-ra on +@all ~* >b3b2fcbbf46fe39fd522c395a51f84d93a98ff2f user replication-user on +@all ~* >435e9c4225f08813ef3af7c725f0d30d263b9cd3 user unittest-rw on +@all ~* >824968fa490f4ecec1e52d5e34916bdb60d45f8d diff --git a/test/secrets/sa_redis_password b/test/secrets/sa_redis_password new file mode 100644 index 000000000..f6ea0069d --- /dev/null +++ b/test/secrets/sa_redis_password @@ -0,0 +1 @@ +de75ae663596735b90e461e5924f71a4c5f622ab \ No newline at end of file