1 package storer
2
3 import (
4 "bytes"
5 "context"
6 "crypto/ecdsa"
7 "crypto/elliptic"
8 "crypto/rand"
9 "crypto/x509"
10 "errors"
11 "io"
12 "math/big"
13 "net/http"
14 "testing"
15 "time"
16
17 "github.com/aws/aws-sdk-go-v2/service/s3"
18 smithyhttp "github.com/aws/smithy-go/transport/http"
19 "github.com/jmhodges/clock"
20 "google.golang.org/grpc"
21 "google.golang.org/protobuf/types/known/emptypb"
22
23 cspb "github.com/letsencrypt/boulder/crl/storer/proto"
24 "github.com/letsencrypt/boulder/issuance"
25 blog "github.com/letsencrypt/boulder/log"
26 "github.com/letsencrypt/boulder/metrics"
27 "github.com/letsencrypt/boulder/test"
28 )
29
30 func TestImplementation(t *testing.T) {
31 test.AssertImplementsGRPCServer(t, &crlStorer{}, cspb.UnimplementedCRLStorerServer{})
32 }
33
34 type fakeUploadCRLServerStream struct {
35 grpc.ServerStream
36 input <-chan *cspb.UploadCRLRequest
37 }
38
39 func (s *fakeUploadCRLServerStream) Recv() (*cspb.UploadCRLRequest, error) {
40 next, ok := <-s.input
41 if !ok {
42 return nil, io.EOF
43 }
44 return next, nil
45 }
46
47 func (s *fakeUploadCRLServerStream) SendAndClose(*emptypb.Empty) error {
48 return nil
49 }
50
51 func (s *fakeUploadCRLServerStream) Context() context.Context {
52 return context.Background()
53 }
54
55 func setupTestUploadCRL(t *testing.T) (*crlStorer, *issuance.Issuer) {
56 t.Helper()
57
58 r3, err := issuance.LoadCertificate("../../test/hierarchy/int-r3.cert.pem")
59 test.AssertNotError(t, err, "loading fake RSA issuer cert")
60 e1, e1Signer, err := issuance.LoadIssuer(issuance.IssuerLoc{
61 File: "../../test/hierarchy/int-e1.key.pem",
62 CertFile: "../../test/hierarchy/int-e1.cert.pem",
63 })
64 test.AssertNotError(t, err, "loading fake ECDSA issuer cert")
65
66 storer, err := New(
67 []*issuance.Certificate{r3, e1},
68 nil, "le-crl.s3.us-west.amazonaws.com",
69 metrics.NoopRegisterer, blog.NewMock(), clock.NewFake(),
70 )
71 test.AssertNotError(t, err, "creating test crl-storer")
72
73 return storer, &issuance.Issuer{Cert: e1, Signer: e1Signer}
74 }
75
76
77 func TestUploadCRLNoMetadata(t *testing.T) {
78 storer, _ := setupTestUploadCRL(t)
79 errs := make(chan error, 1)
80
81 ins := make(chan *cspb.UploadCRLRequest)
82 go func() {
83 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
84 }()
85 close(ins)
86 err := <-errs
87 test.AssertError(t, err, "can't upload CRL with no metadata")
88 test.AssertContains(t, err.Error(), "no metadata")
89 }
90
91
92 func TestUploadCRLIncompleteMetadata(t *testing.T) {
93 storer, _ := setupTestUploadCRL(t)
94 errs := make(chan error, 1)
95
96 ins := make(chan *cspb.UploadCRLRequest)
97 go func() {
98 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
99 }()
100 ins <- &cspb.UploadCRLRequest{
101 Payload: &cspb.UploadCRLRequest_Metadata{
102 Metadata: &cspb.CRLMetadata{},
103 },
104 }
105 close(ins)
106 err := <-errs
107 test.AssertError(t, err, "can't upload CRL with incomplete metadata")
108 test.AssertContains(t, err.Error(), "incomplete metadata")
109 }
110
111
112 func TestUploadCRLUnrecognizedIssuer(t *testing.T) {
113 storer, _ := setupTestUploadCRL(t)
114 errs := make(chan error, 1)
115
116 ins := make(chan *cspb.UploadCRLRequest)
117 go func() {
118 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
119 }()
120 ins <- &cspb.UploadCRLRequest{
121 Payload: &cspb.UploadCRLRequest_Metadata{
122 Metadata: &cspb.CRLMetadata{
123 IssuerNameID: 1,
124 Number: 1,
125 },
126 },
127 }
128 close(ins)
129 err := <-errs
130 test.AssertError(t, err, "can't upload CRL with unrecognized issuer")
131 test.AssertContains(t, err.Error(), "unrecognized")
132 }
133
134
135 func TestUploadCRLMultipleMetadata(t *testing.T) {
136 storer, iss := setupTestUploadCRL(t)
137 errs := make(chan error, 1)
138
139 ins := make(chan *cspb.UploadCRLRequest)
140 go func() {
141 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
142 }()
143 ins <- &cspb.UploadCRLRequest{
144 Payload: &cspb.UploadCRLRequest_Metadata{
145 Metadata: &cspb.CRLMetadata{
146 IssuerNameID: int64(iss.Cert.NameID()),
147 Number: 1,
148 },
149 },
150 }
151 ins <- &cspb.UploadCRLRequest{
152 Payload: &cspb.UploadCRLRequest_Metadata{
153 Metadata: &cspb.CRLMetadata{
154 IssuerNameID: int64(iss.Cert.NameID()),
155 Number: 1,
156 },
157 },
158 }
159 close(ins)
160 err := <-errs
161 test.AssertError(t, err, "can't upload CRL with multiple metadata")
162 test.AssertContains(t, err.Error(), "more than one")
163 }
164
165
166 func TestUploadCRLMalformedBytes(t *testing.T) {
167 storer, iss := setupTestUploadCRL(t)
168 errs := make(chan error, 1)
169
170 ins := make(chan *cspb.UploadCRLRequest)
171 go func() {
172 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
173 }()
174 ins <- &cspb.UploadCRLRequest{
175 Payload: &cspb.UploadCRLRequest_Metadata{
176 Metadata: &cspb.CRLMetadata{
177 IssuerNameID: int64(iss.Cert.NameID()),
178 Number: 1,
179 },
180 },
181 }
182 ins <- &cspb.UploadCRLRequest{
183 Payload: &cspb.UploadCRLRequest_CrlChunk{
184 CrlChunk: []byte("this is not a valid crl"),
185 },
186 }
187 close(ins)
188 err := <-errs
189 test.AssertError(t, err, "can't upload unparsable CRL")
190 test.AssertContains(t, err.Error(), "parsing CRL")
191 }
192
193
194
195 func TestUploadCRLInvalidSignature(t *testing.T) {
196 storer, iss := setupTestUploadCRL(t)
197 errs := make(chan error, 1)
198
199 ins := make(chan *cspb.UploadCRLRequest)
200 go func() {
201 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
202 }()
203 ins <- &cspb.UploadCRLRequest{
204 Payload: &cspb.UploadCRLRequest_Metadata{
205 Metadata: &cspb.CRLMetadata{
206 IssuerNameID: int64(iss.Cert.NameID()),
207 Number: 1,
208 },
209 },
210 }
211 fakeSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
212 test.AssertNotError(t, err, "creating throwaway signer")
213 crlBytes, err := x509.CreateRevocationList(
214 rand.Reader,
215 &x509.RevocationList{
216 ThisUpdate: time.Now(),
217 NextUpdate: time.Now().Add(time.Hour),
218 Number: big.NewInt(1),
219 },
220 iss.Cert.Certificate,
221 fakeSigner,
222 )
223 test.AssertNotError(t, err, "creating test CRL")
224 ins <- &cspb.UploadCRLRequest{
225 Payload: &cspb.UploadCRLRequest_CrlChunk{
226 CrlChunk: crlBytes,
227 },
228 }
229 close(ins)
230 err = <-errs
231 test.AssertError(t, err, "can't upload unverifiable CRL")
232 test.AssertContains(t, err.Error(), "validating signature")
233 }
234
235
236 func TestUploadCRLMismatchedNumbers(t *testing.T) {
237 storer, iss := setupTestUploadCRL(t)
238 errs := make(chan error, 1)
239
240 ins := make(chan *cspb.UploadCRLRequest)
241 go func() {
242 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
243 }()
244 ins <- &cspb.UploadCRLRequest{
245 Payload: &cspb.UploadCRLRequest_Metadata{
246 Metadata: &cspb.CRLMetadata{
247 IssuerNameID: int64(iss.Cert.NameID()),
248 Number: 1,
249 },
250 },
251 }
252 crlBytes, err := x509.CreateRevocationList(
253 rand.Reader,
254 &x509.RevocationList{
255 ThisUpdate: time.Now(),
256 NextUpdate: time.Now().Add(time.Hour),
257 Number: big.NewInt(2),
258 },
259 iss.Cert.Certificate,
260 iss.Signer,
261 )
262 test.AssertNotError(t, err, "creating test CRL")
263 ins <- &cspb.UploadCRLRequest{
264 Payload: &cspb.UploadCRLRequest_CrlChunk{
265 CrlChunk: crlBytes,
266 },
267 }
268 close(ins)
269 err = <-errs
270 test.AssertError(t, err, "can't upload CRL with mismatched number")
271 test.AssertContains(t, err.Error(), "mismatched")
272 }
273
274
275
276 type fakeSimpleS3 struct {
277 prevBytes []byte
278 expectBytes []byte
279 }
280
281 func (p *fakeSimpleS3) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
282 recvBytes, err := io.ReadAll(params.Body)
283 if err != nil {
284 return nil, err
285 }
286 if !bytes.Equal(p.expectBytes, recvBytes) {
287 return nil, errors.New("received bytes did not match expectation")
288 }
289 return &s3.PutObjectOutput{}, nil
290 }
291
292 func (p *fakeSimpleS3) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
293 if p.prevBytes != nil {
294 return &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(p.prevBytes))}, nil
295 }
296 return nil, &smithyhttp.ResponseError{Response: &smithyhttp.Response{Response: &http.Response{StatusCode: 404}}}
297 }
298
299
300 func TestUploadCRLSuccess(t *testing.T) {
301 storer, iss := setupTestUploadCRL(t)
302 errs := make(chan error, 1)
303
304 ins := make(chan *cspb.UploadCRLRequest)
305 go func() {
306 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
307 }()
308 ins <- &cspb.UploadCRLRequest{
309 Payload: &cspb.UploadCRLRequest_Metadata{
310 Metadata: &cspb.CRLMetadata{
311 IssuerNameID: int64(iss.Cert.NameID()),
312 Number: 2,
313 },
314 },
315 }
316
317 prevCRLBytes, err := x509.CreateRevocationList(
318 rand.Reader,
319 &x509.RevocationList{
320 ThisUpdate: storer.clk.Now(),
321 NextUpdate: storer.clk.Now().Add(time.Hour),
322 Number: big.NewInt(1),
323 RevokedCertificateEntries: []x509.RevocationListEntry{
324 {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)},
325 },
326 },
327 iss.Cert.Certificate,
328 iss.Signer,
329 )
330 test.AssertNotError(t, err, "creating test CRL")
331
332 storer.clk.Sleep(time.Minute)
333
334 crlBytes, err := x509.CreateRevocationList(
335 rand.Reader,
336 &x509.RevocationList{
337 ThisUpdate: storer.clk.Now(),
338 NextUpdate: storer.clk.Now().Add(time.Hour),
339 Number: big.NewInt(2),
340 RevokedCertificateEntries: []x509.RevocationListEntry{
341 {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)},
342 },
343 },
344 iss.Cert.Certificate,
345 iss.Signer,
346 )
347 test.AssertNotError(t, err, "creating test CRL")
348
349 storer.s3Client = &fakeSimpleS3{prevBytes: prevCRLBytes, expectBytes: crlBytes}
350 ins <- &cspb.UploadCRLRequest{
351 Payload: &cspb.UploadCRLRequest_CrlChunk{
352 CrlChunk: crlBytes,
353 },
354 }
355 close(ins)
356 err = <-errs
357 test.AssertNotError(t, err, "uploading valid CRL should work")
358 }
359
360
361 func TestUploadNewCRLSuccess(t *testing.T) {
362 storer, iss := setupTestUploadCRL(t)
363 errs := make(chan error, 1)
364
365 ins := make(chan *cspb.UploadCRLRequest)
366 go func() {
367 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
368 }()
369 ins <- &cspb.UploadCRLRequest{
370 Payload: &cspb.UploadCRLRequest_Metadata{
371 Metadata: &cspb.CRLMetadata{
372 IssuerNameID: int64(iss.Cert.NameID()),
373 Number: 1,
374 },
375 },
376 }
377
378 crlBytes, err := x509.CreateRevocationList(
379 rand.Reader,
380 &x509.RevocationList{
381 ThisUpdate: time.Now(),
382 NextUpdate: time.Now().Add(time.Hour),
383 Number: big.NewInt(1),
384 RevokedCertificateEntries: []x509.RevocationListEntry{
385 {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)},
386 },
387 },
388 iss.Cert.Certificate,
389 iss.Signer,
390 )
391 test.AssertNotError(t, err, "creating test CRL")
392
393 storer.s3Client = &fakeSimpleS3{expectBytes: crlBytes}
394 ins <- &cspb.UploadCRLRequest{
395 Payload: &cspb.UploadCRLRequest_CrlChunk{
396 CrlChunk: crlBytes,
397 },
398 }
399 close(ins)
400 err = <-errs
401 test.AssertNotError(t, err, "uploading valid CRL should work")
402 }
403
404
405 func TestUploadCRLBackwardsNumber(t *testing.T) {
406 storer, iss := setupTestUploadCRL(t)
407 errs := make(chan error, 1)
408
409 ins := make(chan *cspb.UploadCRLRequest)
410 go func() {
411 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
412 }()
413 ins <- &cspb.UploadCRLRequest{
414 Payload: &cspb.UploadCRLRequest_Metadata{
415 Metadata: &cspb.CRLMetadata{
416 IssuerNameID: int64(iss.Cert.NameID()),
417 Number: 1,
418 },
419 },
420 }
421
422 prevCRLBytes, err := x509.CreateRevocationList(
423 rand.Reader,
424 &x509.RevocationList{
425 ThisUpdate: storer.clk.Now(),
426 NextUpdate: storer.clk.Now().Add(time.Hour),
427 Number: big.NewInt(2),
428 RevokedCertificateEntries: []x509.RevocationListEntry{
429 {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)},
430 },
431 },
432 iss.Cert.Certificate,
433 iss.Signer,
434 )
435 test.AssertNotError(t, err, "creating test CRL")
436
437 storer.clk.Sleep(time.Minute)
438
439 crlBytes, err := x509.CreateRevocationList(
440 rand.Reader,
441 &x509.RevocationList{
442 ThisUpdate: storer.clk.Now(),
443 NextUpdate: storer.clk.Now().Add(time.Hour),
444 Number: big.NewInt(1),
445 RevokedCertificateEntries: []x509.RevocationListEntry{
446 {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)},
447 },
448 },
449 iss.Cert.Certificate,
450 iss.Signer,
451 )
452 test.AssertNotError(t, err, "creating test CRL")
453
454 storer.s3Client = &fakeSimpleS3{prevBytes: prevCRLBytes, expectBytes: crlBytes}
455 ins <- &cspb.UploadCRLRequest{
456 Payload: &cspb.UploadCRLRequest_CrlChunk{
457 CrlChunk: crlBytes,
458 },
459 }
460 close(ins)
461 err = <-errs
462 test.AssertError(t, err, "uploading out-of-order numbers should fail")
463 test.AssertContains(t, err.Error(), "crlNumber not strictly increasing")
464 }
465
466
467
468 type brokenSimpleS3 struct{}
469
470 func (p *brokenSimpleS3) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
471 return nil, errors.New("sorry")
472 }
473
474 func (p *brokenSimpleS3) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
475 return nil, errors.New("oops")
476 }
477
478
479 func TestUploadCRLBrokenS3(t *testing.T) {
480 storer, iss := setupTestUploadCRL(t)
481 errs := make(chan error, 1)
482
483 ins := make(chan *cspb.UploadCRLRequest)
484 go func() {
485 errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins})
486 }()
487 ins <- &cspb.UploadCRLRequest{
488 Payload: &cspb.UploadCRLRequest_Metadata{
489 Metadata: &cspb.CRLMetadata{
490 IssuerNameID: int64(iss.Cert.NameID()),
491 Number: 1,
492 },
493 },
494 }
495 crlBytes, err := x509.CreateRevocationList(
496 rand.Reader,
497 &x509.RevocationList{
498 ThisUpdate: time.Now(),
499 NextUpdate: time.Now().Add(time.Hour),
500 Number: big.NewInt(1),
501 RevokedCertificateEntries: []x509.RevocationListEntry{
502 {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)},
503 },
504 },
505 iss.Cert.Certificate,
506 iss.Signer,
507 )
508 test.AssertNotError(t, err, "creating test CRL")
509 storer.s3Client = &brokenSimpleS3{}
510 ins <- &cspb.UploadCRLRequest{
511 Payload: &cspb.UploadCRLRequest_CrlChunk{
512 CrlChunk: crlBytes,
513 },
514 }
515 close(ins)
516 err = <-errs
517 test.AssertError(t, err, "uploading to broken S3 should fail")
518 test.AssertContains(t, err.Error(), "getting previous CRL")
519 }
520
View as plain text