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  }