...

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

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

     1  package storer
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/sha256"
     7  	"crypto/x509"
     8  	"crypto/x509/pkix"
     9  	"encoding/asn1"
    10  	"encoding/base64"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"math/big"
    15  	"time"
    16  
    17  	"github.com/aws/aws-sdk-go-v2/service/s3"
    18  	"github.com/aws/aws-sdk-go-v2/service/s3/types"
    19  	smithyhttp "github.com/aws/smithy-go/transport/http"
    20  	"github.com/jmhodges/clock"
    21  	"github.com/prometheus/client_golang/prometheus"
    22  	"google.golang.org/protobuf/types/known/emptypb"
    23  
    24  	"github.com/letsencrypt/boulder/crl"
    25  	cspb "github.com/letsencrypt/boulder/crl/storer/proto"
    26  	"github.com/letsencrypt/boulder/issuance"
    27  	blog "github.com/letsencrypt/boulder/log"
    28  )
    29  
    30  // simpleS3 matches the subset of the s3.Client interface which we use, to allow
    31  // simpler mocking in tests.
    32  type simpleS3 interface {
    33  	PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
    34  	GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
    35  }
    36  
    37  type crlStorer struct {
    38  	cspb.UnimplementedCRLStorerServer
    39  	s3Client         simpleS3
    40  	s3Bucket         string
    41  	issuers          map[issuance.IssuerNameID]*issuance.Certificate
    42  	uploadCount      *prometheus.CounterVec
    43  	sizeHistogram    *prometheus.HistogramVec
    44  	latencyHistogram *prometheus.HistogramVec
    45  	log              blog.Logger
    46  	clk              clock.Clock
    47  }
    48  
    49  func New(
    50  	issuers []*issuance.Certificate,
    51  	s3Client simpleS3,
    52  	s3Bucket string,
    53  	stats prometheus.Registerer,
    54  	log blog.Logger,
    55  	clk clock.Clock,
    56  ) (*crlStorer, error) {
    57  	issuersByNameID := make(map[issuance.IssuerNameID]*issuance.Certificate, len(issuers))
    58  	for _, issuer := range issuers {
    59  		issuersByNameID[issuer.NameID()] = issuer
    60  	}
    61  
    62  	uploadCount := prometheus.NewCounterVec(prometheus.CounterOpts{
    63  		Name: "crl_storer_uploads",
    64  		Help: "A counter of the number of CRLs uploaded by crl-storer",
    65  	}, []string{"issuer", "result"})
    66  	stats.MustRegister(uploadCount)
    67  
    68  	sizeHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{
    69  		Name:    "crl_storer_sizes",
    70  		Help:    "A histogram of the sizes (in bytes) of CRLs uploaded by crl-storer",
    71  		Buckets: []float64{0, 256, 1024, 4096, 16384, 65536},
    72  	}, []string{"issuer"})
    73  	stats.MustRegister(sizeHistogram)
    74  
    75  	latencyHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{
    76  		Name:    "crl_storer_upload_times",
    77  		Help:    "A histogram of the time (in seconds) it took crl-storer to upload CRLs",
    78  		Buckets: []float64{0.01, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000},
    79  	}, []string{"issuer"})
    80  	stats.MustRegister(latencyHistogram)
    81  
    82  	return &crlStorer{
    83  		issuers:          issuersByNameID,
    84  		s3Client:         s3Client,
    85  		s3Bucket:         s3Bucket,
    86  		uploadCount:      uploadCount,
    87  		sizeHistogram:    sizeHistogram,
    88  		latencyHistogram: latencyHistogram,
    89  		log:              log,
    90  		clk:              clk,
    91  	}, nil
    92  }
    93  
    94  // TODO(#6261): Unify all error messages to identify the shard they're working
    95  // on as a JSON object including issuer, crl number, and shard number.
    96  
    97  // UploadCRL implements the gRPC method of the same name. It takes a stream of
    98  // bytes as its input, parses and runs some sanity checks on the CRL, and then
    99  // uploads it to S3.
   100  func (cs *crlStorer) UploadCRL(stream cspb.CRLStorer_UploadCRLServer) error {
   101  	var issuer *issuance.Certificate
   102  	var shardIdx int64
   103  	var crlNumber *big.Int
   104  	crlBytes := make([]byte, 0)
   105  
   106  	// Read all of the messages from the input stream.
   107  	for {
   108  		in, err := stream.Recv()
   109  		if err != nil {
   110  			if err == io.EOF {
   111  				break
   112  			}
   113  			return err
   114  		}
   115  
   116  		switch payload := in.Payload.(type) {
   117  		case *cspb.UploadCRLRequest_Metadata:
   118  			if crlNumber != nil || issuer != nil {
   119  				return errors.New("got more than one metadata message")
   120  			}
   121  			if payload.Metadata.IssuerNameID == 0 || payload.Metadata.Number == 0 {
   122  				return errors.New("got incomplete metadata message")
   123  			}
   124  
   125  			shardIdx = payload.Metadata.ShardIdx
   126  			crlNumber = crl.Number(time.Unix(0, payload.Metadata.Number))
   127  
   128  			var ok bool
   129  			issuer, ok = cs.issuers[issuance.IssuerNameID(payload.Metadata.IssuerNameID)]
   130  			if !ok {
   131  				return fmt.Errorf("got unrecognized IssuerID: %d", payload.Metadata.IssuerNameID)
   132  			}
   133  
   134  		case *cspb.UploadCRLRequest_CrlChunk:
   135  			crlBytes = append(crlBytes, payload.CrlChunk...)
   136  		}
   137  	}
   138  
   139  	// Do some basic sanity checks on the received metadata and CRL.
   140  	if issuer == nil || crlNumber == nil {
   141  		return errors.New("got no metadata message")
   142  	}
   143  
   144  	crlId := crl.Id(issuer.NameID(), int(shardIdx), crlNumber)
   145  
   146  	cs.sizeHistogram.WithLabelValues(issuer.Subject.CommonName).Observe(float64(len(crlBytes)))
   147  
   148  	crl, err := x509.ParseRevocationList(crlBytes)
   149  	if err != nil {
   150  		return fmt.Errorf("parsing CRL for %s: %w", crlId, err)
   151  	}
   152  
   153  	if crl.Number.Cmp(crlNumber) != 0 {
   154  		return errors.New("got mismatched CRL Number")
   155  	}
   156  
   157  	err = crl.CheckSignatureFrom(issuer.Certificate)
   158  	if err != nil {
   159  		return fmt.Errorf("validating signature for %s: %w", crlId, err)
   160  	}
   161  
   162  	// Before uploading this CRL, we want to compare it against the previous CRL
   163  	// to ensure that the CRL Number field is not going backwards. This is an
   164  	// additional safety check against clock skew and potential races, if multiple
   165  	// crl-updaters are working on the same shard at the same time. We only run
   166  	// these checks if we found a CRL, so we don't block uploading brand new CRLs.
   167  	filename := fmt.Sprintf("%d/%d.crl", issuer.NameID(), shardIdx)
   168  	prevObj, err := cs.s3Client.GetObject(stream.Context(), &s3.GetObjectInput{
   169  		Bucket: &cs.s3Bucket,
   170  		Key:    &filename,
   171  	})
   172  	if err != nil {
   173  		var smithyErr *smithyhttp.ResponseError
   174  		if !errors.As(err, &smithyErr) || smithyErr.HTTPStatusCode() != 404 {
   175  			return fmt.Errorf("getting previous CRL for %s: %w", crlId, err)
   176  		}
   177  		cs.log.Infof("No previous CRL found for %s, proceeding", crlId)
   178  	} else {
   179  		prevBytes, err := io.ReadAll(prevObj.Body)
   180  		if err != nil {
   181  			return fmt.Errorf("downloading previous CRL for %s: %w", crlId, err)
   182  		}
   183  
   184  		prevCRL, err := x509.ParseRevocationList(prevBytes)
   185  		if err != nil {
   186  			return fmt.Errorf("parsing previous CRL for %s: %w", crlId, err)
   187  		}
   188  
   189  		idp := getIDPExt(crl.Extensions)
   190  		prevIdp := getIDPExt(prevCRL.Extensions)
   191  		if !bytes.Equal(idp, prevIdp) {
   192  			return fmt.Errorf("IDP does not match previous: %x != %x", idp, prevIdp)
   193  		}
   194  
   195  		if crl.Number.Cmp(prevCRL.Number) <= 0 {
   196  			return fmt.Errorf("crlNumber not strictly increasing: %d <= %d", crl.Number, prevCRL.Number)
   197  		}
   198  	}
   199  
   200  	// Finally actually upload the new CRL.
   201  	start := cs.clk.Now()
   202  
   203  	checksum := sha256.Sum256(crlBytes)
   204  	checksumb64 := base64.StdEncoding.EncodeToString(checksum[:])
   205  	crlContentType := "application/pkix-crl"
   206  	_, err = cs.s3Client.PutObject(stream.Context(), &s3.PutObjectInput{
   207  		Bucket:            &cs.s3Bucket,
   208  		Key:               &filename,
   209  		Body:              bytes.NewReader(crlBytes),
   210  		ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
   211  		ChecksumSHA256:    &checksumb64,
   212  		ContentType:       &crlContentType,
   213  		Metadata:          map[string]string{"crlNumber": crlNumber.String()},
   214  	})
   215  
   216  	latency := cs.clk.Now().Sub(start)
   217  	cs.latencyHistogram.WithLabelValues(issuer.Subject.CommonName).Observe(latency.Seconds())
   218  
   219  	if err != nil {
   220  		cs.uploadCount.WithLabelValues(issuer.Subject.CommonName, "failed").Inc()
   221  		cs.log.AuditErrf("CRL upload failed: id=[%s] err=[%s]", crlId, err)
   222  		return fmt.Errorf("uploading to S3: %w", err)
   223  	}
   224  
   225  	cs.uploadCount.WithLabelValues(issuer.Subject.CommonName, "success").Inc()
   226  	cs.log.AuditInfof(
   227  		"CRL uploaded: id=[%s] issuerCN=[%s] thisUpdate=[%s] nextUpdate=[%s] numEntries=[%d]",
   228  		crlId, issuer.Subject.CommonName, crl.ThisUpdate, crl.NextUpdate, len(crl.RevokedCertificateEntries),
   229  	)
   230  
   231  	return stream.SendAndClose(&emptypb.Empty{})
   232  }
   233  
   234  // getIDPExt returns the contents of the issuingDistributionPoint extension, if present.
   235  func getIDPExt(exts []pkix.Extension) []byte {
   236  	for _, ext := range exts {
   237  		if ext.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 28}) { // id-ce-issuingDistributionPoint
   238  			return ext.Value
   239  		}
   240  	}
   241  	return nil
   242  }
   243  

View as plain text