github.com/letsencrypt/boulder@v0.20251208.0/crl/updater/updater.go (about)

     1  package updater
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"fmt"
     7  	"io"
     8  	"time"
     9  
    10  	"github.com/jmhodges/clock"
    11  	"github.com/prometheus/client_golang/prometheus"
    12  	"github.com/prometheus/client_golang/prometheus/promauto"
    13  	"google.golang.org/protobuf/types/known/timestamppb"
    14  
    15  	capb "github.com/letsencrypt/boulder/ca/proto"
    16  	"github.com/letsencrypt/boulder/core"
    17  	"github.com/letsencrypt/boulder/core/proto"
    18  	"github.com/letsencrypt/boulder/crl"
    19  	cspb "github.com/letsencrypt/boulder/crl/storer/proto"
    20  	"github.com/letsencrypt/boulder/issuance"
    21  	blog "github.com/letsencrypt/boulder/log"
    22  	sapb "github.com/letsencrypt/boulder/sa/proto"
    23  )
    24  
    25  type crlUpdater struct {
    26  	issuers        map[issuance.NameID]*issuance.Certificate
    27  	numShards      int
    28  	shardWidth     time.Duration
    29  	lookbackPeriod time.Duration
    30  	updatePeriod   time.Duration
    31  	updateTimeout  time.Duration
    32  	maxParallelism int
    33  	maxAttempts    int
    34  
    35  	cacheControl  string
    36  	expiresMargin time.Duration
    37  
    38  	sa sapb.StorageAuthorityClient
    39  	ca capb.CRLGeneratorClient
    40  	cs cspb.CRLStorerClient
    41  
    42  	tickHistogram  *prometheus.HistogramVec
    43  	updatedCounter *prometheus.CounterVec
    44  
    45  	log blog.Logger
    46  	clk clock.Clock
    47  }
    48  
    49  func NewUpdater(
    50  	issuers []*issuance.Certificate,
    51  	numShards int,
    52  	shardWidth time.Duration,
    53  	lookbackPeriod time.Duration,
    54  	updatePeriod time.Duration,
    55  	updateTimeout time.Duration,
    56  	maxParallelism int,
    57  	maxAttempts int,
    58  	cacheControl string,
    59  	expiresMargin time.Duration,
    60  	sa sapb.StorageAuthorityClient,
    61  	ca capb.CRLGeneratorClient,
    62  	cs cspb.CRLStorerClient,
    63  	stats prometheus.Registerer,
    64  	log blog.Logger,
    65  	clk clock.Clock,
    66  ) (*crlUpdater, error) {
    67  	issuersByNameID := make(map[issuance.NameID]*issuance.Certificate, len(issuers))
    68  	for _, issuer := range issuers {
    69  		issuersByNameID[issuer.NameID()] = issuer
    70  	}
    71  
    72  	if numShards < 1 {
    73  		return nil, fmt.Errorf("must have positive number of shards, got: %d", numShards)
    74  	}
    75  
    76  	if updatePeriod >= 24*time.Hour {
    77  		return nil, fmt.Errorf("must update CRLs at least every 24 hours, got: %s", updatePeriod)
    78  	}
    79  
    80  	if updateTimeout >= updatePeriod {
    81  		return nil, fmt.Errorf("update timeout must be less than period: %s !< %s", updateTimeout, updatePeriod)
    82  	}
    83  
    84  	if lookbackPeriod < 2*updatePeriod {
    85  		return nil, fmt.Errorf("lookbackPeriod must be at least 2x updatePeriod: %s !< 2 * %s", lookbackPeriod, updatePeriod)
    86  	}
    87  
    88  	if maxParallelism <= 0 {
    89  		maxParallelism = 1
    90  	}
    91  
    92  	if maxAttempts <= 0 {
    93  		maxAttempts = 1
    94  	}
    95  
    96  	tickHistogram := promauto.With(stats).NewHistogramVec(prometheus.HistogramOpts{
    97  		Name:    "crl_updater_ticks",
    98  		Help:    "A histogram of crl-updater tick latencies labeled by issuer and result",
    99  		Buckets: []float64{0.01, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000},
   100  	}, []string{"issuer", "result"})
   101  
   102  	updatedCounter := promauto.With(stats).NewCounterVec(prometheus.CounterOpts{
   103  		Name: "crl_updater_generated",
   104  		Help: "A counter of CRL generation calls labeled by result",
   105  	}, []string{"issuer", "result"})
   106  
   107  	return &crlUpdater{
   108  		issuersByNameID,
   109  		numShards,
   110  		shardWidth,
   111  		lookbackPeriod,
   112  		updatePeriod,
   113  		updateTimeout,
   114  		maxParallelism,
   115  		maxAttempts,
   116  		cacheControl,
   117  		expiresMargin,
   118  		sa,
   119  		ca,
   120  		cs,
   121  		tickHistogram,
   122  		updatedCounter,
   123  		log,
   124  		clk,
   125  	}, nil
   126  }
   127  
   128  // updateShardWithRetry calls updateShard repeatedly (with exponential backoff
   129  // between attempts) until it succeeds or the max number of attempts is reached.
   130  func (cu *crlUpdater) updateShardWithRetry(ctx context.Context, atTime time.Time, issuerNameID issuance.NameID, shardIdx int) error {
   131  	deadline := cu.clk.Now().Add(cu.updateTimeout)
   132  	ctx, cancel := context.WithDeadline(ctx, deadline)
   133  	defer cancel()
   134  
   135  	_, err := cu.sa.LeaseCRLShard(ctx, &sapb.LeaseCRLShardRequest{
   136  		IssuerNameID: int64(issuerNameID),
   137  		MinShardIdx:  int64(shardIdx),
   138  		MaxShardIdx:  int64(shardIdx),
   139  		Until:        timestamppb.New(deadline.Add(time.Minute)),
   140  	})
   141  	if err != nil {
   142  		return fmt.Errorf("leasing shard: %w", err)
   143  	}
   144  
   145  	crlID := crl.Id(issuerNameID, shardIdx, crl.Number(atTime))
   146  
   147  	for i := range cu.maxAttempts {
   148  		// core.RetryBackoff always returns 0 when its first argument is zero.
   149  		sleepTime := core.RetryBackoff(i, time.Second, time.Minute, 2)
   150  		if i != 0 {
   151  			cu.log.Errf(
   152  				"Generating CRL failed, will retry in %vs: id=[%s] err=[%s]",
   153  				sleepTime.Seconds(), crlID, err)
   154  		}
   155  		cu.clk.Sleep(sleepTime)
   156  
   157  		err = cu.updateShard(ctx, atTime, issuerNameID, shardIdx)
   158  		if err == nil {
   159  			break
   160  		}
   161  	}
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	// Notify the database that that we're done.
   167  	_, err = cu.sa.UpdateCRLShard(ctx, &sapb.UpdateCRLShardRequest{
   168  		IssuerNameID: int64(issuerNameID),
   169  		ShardIdx:     int64(shardIdx),
   170  		ThisUpdate:   timestamppb.New(atTime),
   171  	})
   172  	if err != nil {
   173  		return fmt.Errorf("updating db metadata: %w", err)
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  // updateShard processes a single shard. It computes the shard's boundaries, gets
   180  // the list of revoked certs in that shard from the SA, gets the CA to sign the
   181  // resulting CRL, and gets the crl-storer to upload it. It returns an error if
   182  // any of these operations fail.
   183  func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerNameID issuance.NameID, shardIdx int) (err error) {
   184  	if shardIdx <= 0 {
   185  		return fmt.Errorf("invalid shard %d", shardIdx)
   186  	}
   187  	ctx, cancel := context.WithCancel(ctx)
   188  	defer cancel()
   189  
   190  	crlID := crl.Id(issuerNameID, shardIdx, crl.Number(atTime))
   191  
   192  	start := cu.clk.Now()
   193  	defer func() {
   194  		// This func closes over the named return value `err`, so can reference it.
   195  		result := "success"
   196  		if err != nil {
   197  			result = "failed"
   198  		}
   199  		cu.tickHistogram.WithLabelValues(cu.issuers[issuerNameID].Subject.CommonName, result).Observe(cu.clk.Since(start).Seconds())
   200  		cu.updatedCounter.WithLabelValues(cu.issuers[issuerNameID].Subject.CommonName, result).Inc()
   201  	}()
   202  
   203  	cu.log.Infof("Generating CRL shard: id=[%s]", crlID)
   204  
   205  	// Query for unexpired certificates, with padding to ensure that revoked certificates show
   206  	// up in at least one CRL, even if they expire between revocation and CRL generation.
   207  	expiresAfter := cu.clk.Now().Add(-cu.lookbackPeriod)
   208  
   209  	saStream, err := cu.sa.GetRevokedCertsByShard(ctx, &sapb.GetRevokedCertsByShardRequest{
   210  		IssuerNameID:  int64(issuerNameID),
   211  		ShardIdx:      int64(shardIdx),
   212  		ExpiresAfter:  timestamppb.New(expiresAfter),
   213  		RevokedBefore: timestamppb.New(atTime),
   214  	})
   215  	if err != nil {
   216  		return fmt.Errorf("GetRevokedCertsByShard: %w", err)
   217  	}
   218  
   219  	var crlEntries []*proto.CRLEntry
   220  	for {
   221  		entry, err := saStream.Recv()
   222  		if err != nil {
   223  			if err == io.EOF {
   224  				break
   225  			}
   226  			return fmt.Errorf("retrieving entry from SA: %w", err)
   227  		}
   228  		crlEntries = append(crlEntries, entry)
   229  	}
   230  
   231  	cu.log.Infof("Queried SA for CRL shard: id=[%s] shardIdx=[%d] numEntries=[%d]", crlID, shardIdx, len(crlEntries))
   232  
   233  	// Send the full list of CRL Entries to the CA.
   234  	caStream, err := cu.ca.GenerateCRL(ctx)
   235  	if err != nil {
   236  		return fmt.Errorf("connecting to CA: %w", err)
   237  	}
   238  
   239  	err = caStream.Send(&capb.GenerateCRLRequest{
   240  		Payload: &capb.GenerateCRLRequest_Metadata{
   241  			Metadata: &capb.CRLMetadata{
   242  				IssuerNameID: int64(issuerNameID),
   243  				ThisUpdate:   timestamppb.New(atTime),
   244  				ShardIdx:     int64(shardIdx),
   245  			},
   246  		},
   247  	})
   248  	if err != nil {
   249  		return fmt.Errorf("sending CA metadata: %w", err)
   250  	}
   251  
   252  	for _, entry := range crlEntries {
   253  		err = caStream.Send(&capb.GenerateCRLRequest{
   254  			Payload: &capb.GenerateCRLRequest_Entry{
   255  				Entry: entry,
   256  			},
   257  		})
   258  		if err != nil {
   259  			return fmt.Errorf("sending entry to CA: %w", err)
   260  		}
   261  	}
   262  
   263  	err = caStream.CloseSend()
   264  	if err != nil {
   265  		return fmt.Errorf("closing CA request stream: %w", err)
   266  	}
   267  
   268  	// Receive the full bytes of the signed CRL from the CA.
   269  	crlLen := 0
   270  	crlHash := sha256.New()
   271  	var crlChunks [][]byte
   272  	for {
   273  		out, err := caStream.Recv()
   274  		if err != nil {
   275  			if err == io.EOF {
   276  				break
   277  			}
   278  			return fmt.Errorf("receiving CRL bytes: %w", err)
   279  		}
   280  
   281  		crlLen += len(out.Chunk)
   282  		crlHash.Write(out.Chunk)
   283  		crlChunks = append(crlChunks, out.Chunk)
   284  	}
   285  
   286  	// Send the full bytes of the signed CRL to the Storer.
   287  	csStream, err := cu.cs.UploadCRL(ctx)
   288  	if err != nil {
   289  		return fmt.Errorf("connecting to CRLStorer: %w", err)
   290  	}
   291  
   292  	err = csStream.Send(&cspb.UploadCRLRequest{
   293  		Payload: &cspb.UploadCRLRequest_Metadata{
   294  			Metadata: &cspb.CRLMetadata{
   295  				IssuerNameID: int64(issuerNameID),
   296  				Number:       atTime.UnixNano(),
   297  				ShardIdx:     int64(shardIdx),
   298  				CacheControl: cu.cacheControl,
   299  				Expires:      timestamppb.New(atTime.Add(cu.updatePeriod).Add(cu.expiresMargin)),
   300  			},
   301  		},
   302  	})
   303  	if err != nil {
   304  		return fmt.Errorf("sending CRLStorer metadata: %w", err)
   305  	}
   306  
   307  	for _, chunk := range crlChunks {
   308  		err = csStream.Send(&cspb.UploadCRLRequest{
   309  			Payload: &cspb.UploadCRLRequest_CrlChunk{
   310  				CrlChunk: chunk,
   311  			},
   312  		})
   313  		if err != nil {
   314  			return fmt.Errorf("uploading CRL bytes: %w", err)
   315  		}
   316  	}
   317  
   318  	_, err = csStream.CloseAndRecv()
   319  	if err != nil {
   320  		return fmt.Errorf("closing CRLStorer upload stream: %w", err)
   321  	}
   322  
   323  	cu.log.Infof(
   324  		"Generated CRL shard: id=[%s] size=[%d] hash=[%x]",
   325  		crlID, crlLen, crlHash.Sum(nil))
   326  
   327  	return nil
   328  }