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