...

Source file src/github.com/letsencrypt/boulder/crl/storer/storer_test.go

Documentation: github.com/letsencrypt/boulder/crl/storer

     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  // Test that we get an error when no metadata is sent.
    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  // Test that we get an error when incomplete metadata is sent.
    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  // Test that we get an error when a bad issuer is sent.
   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  // Test that we get an error when two metadata are sent.
   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  // Test that we get an error when a malformed CRL is sent.
   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  // Test that we get an error when an invalid CRL (signed by a throwaway
   194  // private key but tagged as being from a "real" issuer) is sent.
   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  // Test that we get an error if the CRL Numbers mismatch.
   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  // fakeSimpleS3 implements the simpleS3 interface, provides prevBytes for
   275  // downloads, and checks that uploads match the expectBytes.
   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  // Test that the correct bytes get propagated to S3.
   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  // Test that the correct bytes get propagated to S3 for a CRL with to predecessor.
   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  // Test that we get an error when the previous CRL has a higher CRL number.
   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  // brokenSimpleS3 implements the simpleS3 interface. It returns errors for all
   467  // uploads and downloads.
   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  // Test that we get an error when S3 falls over.
   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