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 }