package storer import ( "bytes" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "errors" "io" "math/big" "net/http" "testing" "time" "github.com/aws/aws-sdk-go-v2/service/s3" smithyhttp "github.com/aws/smithy-go/transport/http" "github.com/jmhodges/clock" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" "github.com/letsencrypt/boulder/crl/idp" cspb "github.com/letsencrypt/boulder/crl/storer/proto" "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) type fakeUploadCRLServerStream struct { grpc.ServerStream input <-chan *cspb.UploadCRLRequest } func (s *fakeUploadCRLServerStream) Recv() (*cspb.UploadCRLRequest, error) { next, ok := <-s.input if !ok { return nil, io.EOF } return next, nil } func (s *fakeUploadCRLServerStream) SendAndClose(*emptypb.Empty) error { return nil } func (s *fakeUploadCRLServerStream) Context() context.Context { return context.Background() } func setupTestUploadCRL(t *testing.T) (*crlStorer, *issuance.Issuer) { t.Helper() r3, err := issuance.LoadCertificate("../../test/hierarchy/int-r3.cert.pem") test.AssertNotError(t, err, "loading fake RSA issuer cert") issuerE1, err := issuance.LoadIssuer( issuance.IssuerConfig{ Location: issuance.IssuerLoc{ File: "../../test/hierarchy/int-e1.key.pem", CertFile: "../../test/hierarchy/int-e1.cert.pem", }, IssuerURL: "http://not-example.com/issuer-url", OCSPURL: "http://not-example.com/ocsp", CRLURLBase: "http://not-example.com/crl/", }, clock.NewFake()) test.AssertNotError(t, err, "loading fake ECDSA issuer cert") storer, err := New( []*issuance.Certificate{r3, issuerE1.Cert}, nil, "le-crl.s3.us-west.amazonaws.com", metrics.NoopRegisterer, blog.NewMock(), clock.NewFake(), ) test.AssertNotError(t, err, "creating test crl-storer") return storer, issuerE1 } // Test that we get an error when no metadata is sent. func TestUploadCRLNoMetadata(t *testing.T) { storer, _ := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() close(ins) err := <-errs test.AssertError(t, err, "can't upload CRL with no metadata") test.AssertContains(t, err.Error(), "no metadata") } // Test that we get an error when incomplete metadata is sent. func TestUploadCRLIncompleteMetadata(t *testing.T) { storer, _ := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{}, }, } close(ins) err := <-errs test.AssertError(t, err, "can't upload CRL with incomplete metadata") test.AssertContains(t, err.Error(), "incomplete metadata") } // Test that we get an error when a bad issuer is sent. func TestUploadCRLUnrecognizedIssuer(t *testing.T) { storer, _ := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: 1, Number: 1, }, }, } close(ins) err := <-errs test.AssertError(t, err, "can't upload CRL with unrecognized issuer") test.AssertContains(t, err.Error(), "unrecognized") } // Test that we get an error when two metadata are sent. func TestUploadCRLMultipleMetadata(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } close(ins) err := <-errs test.AssertError(t, err, "can't upload CRL with multiple metadata") test.AssertContains(t, err.Error(), "more than one") } // Test that we get an error when a malformed CRL is sent. func TestUploadCRLMalformedBytes(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_CrlChunk{ CrlChunk: []byte("this is not a valid crl"), }, } close(ins) err := <-errs test.AssertError(t, err, "can't upload unparsable CRL") test.AssertContains(t, err.Error(), "parsing CRL") } // Test that we get an error when an invalid CRL (signed by a throwaway // private key but tagged as being from a "real" issuer) is sent. func TestUploadCRLInvalidSignature(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } fakeSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "creating throwaway signer") crlBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: time.Now(), NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(1), }, iss.Cert.Certificate, fakeSigner, ) test.AssertNotError(t, err, "creating test CRL") ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_CrlChunk{ CrlChunk: crlBytes, }, } close(ins) err = <-errs test.AssertError(t, err, "can't upload unverifiable CRL") test.AssertContains(t, err.Error(), "validating signature") } // Test that we get an error if the CRL Numbers mismatch. func TestUploadCRLMismatchedNumbers(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } crlBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: time.Now(), NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(2), }, iss.Cert.Certificate, iss.Signer, ) test.AssertNotError(t, err, "creating test CRL") ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_CrlChunk{ CrlChunk: crlBytes, }, } close(ins) err = <-errs test.AssertError(t, err, "can't upload CRL with mismatched number") test.AssertContains(t, err.Error(), "mismatched") } // fakeSimpleS3 implements the simpleS3 interface, provides prevBytes for // downloads, and checks that uploads match the expectBytes. type fakeSimpleS3 struct { prevBytes []byte expectBytes []byte } func (p *fakeSimpleS3) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) { recvBytes, err := io.ReadAll(params.Body) if err != nil { return nil, err } if !bytes.Equal(p.expectBytes, recvBytes) { return nil, errors.New("received bytes did not match expectation") } return &s3.PutObjectOutput{}, nil } func (p *fakeSimpleS3) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { if p.prevBytes != nil { return &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(p.prevBytes))}, nil } return nil, &smithyhttp.ResponseError{Response: &smithyhttp.Response{Response: &http.Response{StatusCode: 404}}} } // Test that the correct bytes get propagated to S3. func TestUploadCRLSuccess(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) idpExt, err := idp.MakeUserCertsExt([]string{"http://c.ex.org"}) test.AssertNotError(t, err, "creating test IDP extension") ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 2, }, }, } prevCRLBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: storer.clk.Now(), NextUpdate: storer.clk.Now().Add(time.Hour), Number: big.NewInt(1), RevokedCertificateEntries: []x509.RevocationListEntry{ {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)}, }, ExtraExtensions: []pkix.Extension{idpExt}, }, iss.Cert.Certificate, iss.Signer, ) test.AssertNotError(t, err, "creating test CRL") storer.clk.Sleep(time.Minute) crlBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: storer.clk.Now(), NextUpdate: storer.clk.Now().Add(time.Hour), Number: big.NewInt(2), RevokedCertificateEntries: []x509.RevocationListEntry{ {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)}, }, ExtraExtensions: []pkix.Extension{idpExt}, }, iss.Cert.Certificate, iss.Signer, ) test.AssertNotError(t, err, "creating test CRL") storer.s3Client = &fakeSimpleS3{prevBytes: prevCRLBytes, expectBytes: crlBytes} ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_CrlChunk{ CrlChunk: crlBytes, }, } close(ins) err = <-errs test.AssertNotError(t, err, "uploading valid CRL should work") } // Test that the correct bytes get propagated to S3 for a CRL with to predecessor. func TestUploadNewCRLSuccess(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } crlBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: time.Now(), NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(1), RevokedCertificateEntries: []x509.RevocationListEntry{ {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)}, }, }, iss.Cert.Certificate, iss.Signer, ) test.AssertNotError(t, err, "creating test CRL") storer.s3Client = &fakeSimpleS3{expectBytes: crlBytes} ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_CrlChunk{ CrlChunk: crlBytes, }, } close(ins) err = <-errs test.AssertNotError(t, err, "uploading valid CRL should work") } // Test that we get an error when the previous CRL has a higher CRL number. func TestUploadCRLBackwardsNumber(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } prevCRLBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: storer.clk.Now(), NextUpdate: storer.clk.Now().Add(time.Hour), Number: big.NewInt(2), RevokedCertificateEntries: []x509.RevocationListEntry{ {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)}, }, }, iss.Cert.Certificate, iss.Signer, ) test.AssertNotError(t, err, "creating test CRL") storer.clk.Sleep(time.Minute) crlBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: storer.clk.Now(), NextUpdate: storer.clk.Now().Add(time.Hour), Number: big.NewInt(1), RevokedCertificateEntries: []x509.RevocationListEntry{ {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)}, }, }, iss.Cert.Certificate, iss.Signer, ) test.AssertNotError(t, err, "creating test CRL") storer.s3Client = &fakeSimpleS3{prevBytes: prevCRLBytes, expectBytes: crlBytes} ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_CrlChunk{ CrlChunk: crlBytes, }, } close(ins) err = <-errs test.AssertError(t, err, "uploading out-of-order numbers should fail") test.AssertContains(t, err.Error(), "crlNumber not strictly increasing") } // brokenSimpleS3 implements the simpleS3 interface. It returns errors for all // uploads and downloads. type brokenSimpleS3 struct{} func (p *brokenSimpleS3) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) { return nil, errors.New("sorry") } func (p *brokenSimpleS3) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { return nil, errors.New("oops") } // Test that we get an error when S3 falls over. func TestUploadCRLBrokenS3(t *testing.T) { storer, iss := setupTestUploadCRL(t) errs := make(chan error, 1) ins := make(chan *cspb.UploadCRLRequest) go func() { errs <- storer.UploadCRL(&fakeUploadCRLServerStream{input: ins}) }() ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_Metadata{ Metadata: &cspb.CRLMetadata{ IssuerNameID: int64(iss.Cert.NameID()), Number: 1, }, }, } crlBytes, err := x509.CreateRevocationList( rand.Reader, &x509.RevocationList{ ThisUpdate: time.Now(), NextUpdate: time.Now().Add(time.Hour), Number: big.NewInt(1), RevokedCertificateEntries: []x509.RevocationListEntry{ {SerialNumber: big.NewInt(123), RevocationTime: time.Now().Add(-time.Hour)}, }, }, iss.Cert.Certificate, iss.Signer, ) test.AssertNotError(t, err, "creating test CRL") storer.s3Client = &brokenSimpleS3{} ins <- &cspb.UploadCRLRequest{ Payload: &cspb.UploadCRLRequest_CrlChunk{ CrlChunk: crlBytes, }, } close(ins) err = <-errs test.AssertError(t, err, "uploading to broken S3 should fail") test.AssertContains(t, err.Error(), "getting previous CRL") }