1 package redis
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "math/big"
9 "testing"
10 "time"
11
12 "golang.org/x/crypto/ocsp"
13 "google.golang.org/grpc"
14 "google.golang.org/protobuf/types/known/timestamppb"
15
16 "github.com/letsencrypt/boulder/core"
17 "github.com/letsencrypt/boulder/db"
18 berrors "github.com/letsencrypt/boulder/errors"
19 blog "github.com/letsencrypt/boulder/log"
20 "github.com/letsencrypt/boulder/metrics"
21 "github.com/letsencrypt/boulder/mocks"
22 "github.com/letsencrypt/boulder/ocsp/responder"
23 ocsp_test "github.com/letsencrypt/boulder/ocsp/test"
24 "github.com/letsencrypt/boulder/sa"
25 sapb "github.com/letsencrypt/boulder/sa/proto"
26 "github.com/letsencrypt/boulder/test"
27 )
28
29
30
31 type echoSource struct {
32 resp *ocsp.Response
33 }
34
35 func (es echoSource) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
36 return &responder.Response{Response: es.resp, Raw: es.resp.Raw}, nil
37 }
38
39 func (es echoSource) signAndSave(ctx context.Context, req *ocsp.Request, cause signAndSaveCause) (*responder.Response, error) {
40 panic("should not happen")
41 }
42
43
44
45 type recordingEchoSource struct {
46 echoSource
47 secondResp *responder.Response
48 ch chan string
49 }
50
51 func (res recordingEchoSource) signAndSave(ctx context.Context, req *ocsp.Request, cause signAndSaveCause) (*responder.Response, error) {
52 res.ch <- req.SerialNumber.String()
53 return res.secondResp, nil
54 }
55
56
57 type errorSource struct{}
58
59 func (es errorSource) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) {
60 return nil, errors.New("sad trombone")
61 }
62
63 func (es errorSource) signAndSave(ctx context.Context, req *ocsp.Request, cause signAndSaveCause) (*responder.Response, error) {
64 panic("should not happen")
65 }
66
67
68 type echoSelector struct {
69 db.MockSqlExecutor
70 status sa.RevocationStatusModel
71 }
72
73 func (s echoSelector) SelectOne(_ context.Context, output interface{}, _ string, _ ...interface{}) error {
74 outputPtr, ok := output.(*sa.RevocationStatusModel)
75 if !ok {
76 return fmt.Errorf("incorrect output type %T", output)
77 }
78 *outputPtr = s.status
79 return nil
80 }
81
82
83 type errorSelector struct {
84 db.MockSqlExecutor
85 }
86
87 func (s errorSelector) SelectOne(_ context.Context, _ interface{}, _ string, _ ...interface{}) error {
88 return errors.New("oops")
89 }
90
91
92 type notFoundSelector struct {
93 db.MockSqlExecutor
94 }
95
96 func (s notFoundSelector) SelectOne(_ context.Context, _ interface{}, _ string, _ ...interface{}) error {
97 return db.ErrDatabaseOp{Err: sql.ErrNoRows}
98 }
99
100
101 type echoSA struct {
102 mocks.StorageAuthorityReadOnly
103 status *sapb.RevocationStatus
104 }
105
106 func (s *echoSA) GetRevocationStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.RevocationStatus, error) {
107 return s.status, nil
108 }
109
110
111 type errorSA struct {
112 mocks.StorageAuthorityReadOnly
113 }
114
115 func (s *errorSA) GetRevocationStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.RevocationStatus, error) {
116 return nil, errors.New("oops")
117 }
118
119
120 type notFoundSA struct {
121 mocks.StorageAuthorityReadOnly
122 }
123
124 func (s *notFoundSA) GetRevocationStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.RevocationStatus, error) {
125 return nil, berrors.NotFoundError("purged")
126 }
127
128 func TestCheckedRedisSourceSuccess(t *testing.T) {
129 serial := big.NewInt(17777)
130 thisUpdate := time.Now().Truncate(time.Second).UTC()
131
132 resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
133 SerialNumber: serial,
134 Status: ocsp.Good,
135 ThisUpdate: thisUpdate,
136 })
137 test.AssertNotError(t, err, "making fake response")
138
139 status := sa.RevocationStatusModel{
140 Status: core.OCSPStatusGood,
141 }
142 src := newCheckedRedisSource(echoSource{resp: resp}, echoSelector{status: status}, nil, metrics.NoopRegisterer, blog.NewMock())
143 responderResponse, err := src.Response(context.Background(), &ocsp.Request{
144 SerialNumber: serial,
145 })
146 test.AssertNotError(t, err, "getting response")
147 test.AssertEquals(t, responderResponse.SerialNumber.String(), resp.SerialNumber.String())
148 }
149
150 func TestCheckedRedisSourceDBError(t *testing.T) {
151 serial := big.NewInt(404040)
152 thisUpdate := time.Now().Truncate(time.Second).UTC()
153
154 resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
155 SerialNumber: serial,
156 Status: ocsp.Good,
157 ThisUpdate: thisUpdate,
158 })
159 test.AssertNotError(t, err, "making fake response")
160
161 src := newCheckedRedisSource(echoSource{resp: resp}, errorSelector{}, nil, metrics.NoopRegisterer, blog.NewMock())
162 _, err = src.Response(context.Background(), &ocsp.Request{
163 SerialNumber: serial,
164 })
165 test.AssertError(t, err, "getting response")
166 test.AssertContains(t, err.Error(), "oops")
167
168 src = newCheckedRedisSource(echoSource{resp: resp}, notFoundSelector{}, nil, metrics.NoopRegisterer, blog.NewMock())
169 _, err = src.Response(context.Background(), &ocsp.Request{
170 SerialNumber: serial,
171 })
172 test.AssertError(t, err, "getting response")
173 test.AssertErrorIs(t, err, responder.ErrNotFound)
174 }
175
176 func TestCheckedRedisSourceSAError(t *testing.T) {
177 serial := big.NewInt(404040)
178 thisUpdate := time.Now().Truncate(time.Second).UTC()
179
180 resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
181 SerialNumber: serial,
182 Status: ocsp.Good,
183 ThisUpdate: thisUpdate,
184 })
185 test.AssertNotError(t, err, "making fake response")
186
187 src := newCheckedRedisSource(echoSource{resp: resp}, nil, &errorSA{}, metrics.NoopRegisterer, blog.NewMock())
188 _, err = src.Response(context.Background(), &ocsp.Request{
189 SerialNumber: serial,
190 })
191 test.AssertError(t, err, "getting response")
192 test.AssertContains(t, err.Error(), "oops")
193
194 src = newCheckedRedisSource(echoSource{resp: resp}, nil, ¬FoundSA{}, metrics.NoopRegisterer, blog.NewMock())
195 _, err = src.Response(context.Background(), &ocsp.Request{
196 SerialNumber: serial,
197 })
198 test.AssertError(t, err, "getting response")
199 test.AssertErrorIs(t, err, responder.ErrNotFound)
200 }
201
202 func TestCheckedRedisSourceRedisError(t *testing.T) {
203 serial := big.NewInt(314159262)
204
205 status := sa.RevocationStatusModel{
206 Status: core.OCSPStatusGood,
207 }
208 src := newCheckedRedisSource(errorSource{}, echoSelector{status: status}, nil, metrics.NoopRegisterer, blog.NewMock())
209 _, err := src.Response(context.Background(), &ocsp.Request{
210 SerialNumber: serial,
211 })
212 test.AssertError(t, err, "getting response")
213 }
214
215 func TestCheckedRedisStatusDisagreement(t *testing.T) {
216 serial := big.NewInt(2718)
217 thisUpdate := time.Now().Truncate(time.Second).UTC()
218
219 resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
220 SerialNumber: serial,
221 Status: ocsp.Good,
222 ThisUpdate: thisUpdate.Add(-time.Minute),
223 })
224 test.AssertNotError(t, err, "making fake response")
225
226 secondResp, _, err := ocsp_test.FakeResponse(ocsp.Response{
227 SerialNumber: serial,
228 Status: ocsp.Revoked,
229 RevokedAt: thisUpdate,
230 RevocationReason: ocsp.KeyCompromise,
231 ThisUpdate: thisUpdate,
232 })
233 test.AssertNotError(t, err, "making fake response")
234 status := sa.RevocationStatusModel{
235 Status: core.OCSPStatusRevoked,
236 RevokedDate: thisUpdate,
237 RevokedReason: ocsp.KeyCompromise,
238 }
239 source := recordingEchoSource{
240 echoSource: echoSource{resp: resp},
241 secondResp: &responder.Response{Response: secondResp, Raw: secondResp.Raw},
242 ch: make(chan string, 1),
243 }
244 src := newCheckedRedisSource(source, echoSelector{status: status}, nil, metrics.NoopRegisterer, blog.NewMock())
245 fetchedResponse, err := src.Response(context.Background(), &ocsp.Request{
246 SerialNumber: serial,
247 })
248 test.AssertNotError(t, err, "getting re-signed response")
249 test.Assert(t, fetchedResponse.ThisUpdate.Equal(thisUpdate), "thisUpdate not updated")
250 test.AssertEquals(t, fetchedResponse.SerialNumber.String(), serial.String())
251 test.AssertEquals(t, fetchedResponse.RevokedAt, thisUpdate)
252 test.AssertEquals(t, fetchedResponse.RevocationReason, ocsp.KeyCompromise)
253 test.AssertEquals(t, fetchedResponse.ThisUpdate, thisUpdate)
254 }
255
256 func TestCheckedRedisStatusSADisagreement(t *testing.T) {
257 serial := big.NewInt(2718)
258 thisUpdate := time.Now().Truncate(time.Second).UTC()
259
260 resp, _, err := ocsp_test.FakeResponse(ocsp.Response{
261 SerialNumber: serial,
262 Status: ocsp.Good,
263 ThisUpdate: thisUpdate.Add(-time.Minute),
264 })
265 test.AssertNotError(t, err, "making fake response")
266
267 secondResp, _, err := ocsp_test.FakeResponse(ocsp.Response{
268 SerialNumber: serial,
269 Status: ocsp.Revoked,
270 RevokedAt: thisUpdate,
271 RevocationReason: ocsp.KeyCompromise,
272 ThisUpdate: thisUpdate,
273 })
274 test.AssertNotError(t, err, "making fake response")
275 statusPB := sapb.RevocationStatus{
276 Status: 1,
277 RevokedDate: timestamppb.New(thisUpdate),
278 RevokedReason: ocsp.KeyCompromise,
279 }
280 source := recordingEchoSource{
281 echoSource: echoSource{resp: resp},
282 secondResp: &responder.Response{Response: secondResp, Raw: secondResp.Raw},
283 ch: make(chan string, 1),
284 }
285 src := newCheckedRedisSource(source, nil, &echoSA{status: &statusPB}, metrics.NoopRegisterer, blog.NewMock())
286 fetchedResponse, err := src.Response(context.Background(), &ocsp.Request{
287 SerialNumber: serial,
288 })
289 test.AssertNotError(t, err, "getting re-signed response")
290 test.Assert(t, fetchedResponse.ThisUpdate.Equal(thisUpdate), "thisUpdate not updated")
291 test.AssertEquals(t, fetchedResponse.SerialNumber.String(), serial.String())
292 test.AssertEquals(t, fetchedResponse.RevokedAt, thisUpdate)
293 test.AssertEquals(t, fetchedResponse.RevocationReason, ocsp.KeyCompromise)
294 test.AssertEquals(t, fetchedResponse.ThisUpdate, thisUpdate)
295 }
296
View as plain text