Do CRL shard math with shorter durations (#6425)

Change the anchor time used to stabilize shard boundaries from
the zero time (time.Time{}) to the notBefore date of Let's Encrypt's
first self-signed root certificate.

This prevents us from attempting to compute durations that are
greater than 290 years, the maximum representable duration in
Go. Previously, using the zero time was causing our durations to
all be equal to the maximum duration, removing the utility of the
anchor time and causing our shard boundaries to drift with time.
This commit is contained in:
Aaron Gable 2022-10-03 11:24:40 -07:00 committed by GitHub
parent a3ddc42c86
commit 9bd0c7967f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 55 additions and 24 deletions

View File

@ -3,8 +3,10 @@ package updater
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"math"
"math/big"
"sort"
"strings"
@ -299,7 +301,11 @@ func (cu *crlUpdater) tickShard(ctx context.Context, atTime time.Time, issuerNam
cu.updatedCounter.WithLabelValues(cu.issuers[issuerNameID].Subject.CommonName, result).Inc()
}()
expiresAfter, expiresBefore := cu.getShardBoundaries(atTime, shardIdx)
expiresAfter, expiresBefore, err := cu.getShardBoundaries(atTime, shardIdx)
if err != nil {
return err
}
cu.log.Infof(
"Generating CRL shard: id=[%s] expiresAfter=[%s] expiresBefore=[%s]",
crlID, expiresAfter, expiresBefore)
@ -425,10 +431,20 @@ func (cu *crlUpdater) tickShard(ctx context.Context, atTime time.Time, issuerNam
return nil
}
// anchorTime is used as a universal starting point against which other times
// can be compared. This time must be less than 290 years (2^63-1 nanoseconds)
// in the past, to ensure that Go's time.Duration can represent that difference.
// The significance of 2015-06-04 11:04:38 UTC is left as an exercise to the
// reader.
func anchorTime() time.Time {
return time.Date(2015, time.June, 04, 11, 04, 38, 0, time.UTC)
}
// getShardBoundaries computes the start (inclusive) and end (exclusive) times
// for a given integer-indexed CRL shard. The idea here is that shards should be
// stable. Picture a timeline, divided into chunks. Number those chunks from 0
// to cu.numShards, then repeat the cycle when you run out of numbers:
// (starting at the anchor time) up to cu.numShards, then repeat the cycle when
// you run out of numbers:
//
// chunk: 5 0 1 2 3 4 5 0 1 2 3
// ...-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----...
@ -470,17 +486,24 @@ func (cu *crlUpdater) tickShard(ctx context.Context, atTime time.Time, issuerNam
// there is a buffer of at least one whole chunk width between the actual
// furthest-future expiration (generally atTime+90d) and the right-hand edge of
// the window (atTime+lookforwardPeriod).
func (cu *crlUpdater) getShardBoundaries(atTime time.Time, shardIdx int) (time.Time, time.Time) {
func (cu *crlUpdater) getShardBoundaries(atTime time.Time, shardIdx int) (time.Time, time.Time, error) {
// Ensure that the given shard index falls within the space of acceptable indices.
shardIdx = shardIdx % cu.numShards
// Compute the width of the full window.
windowWidth := cu.lookbackPeriod + cu.lookforwardPeriod
// Compute the amount of time between the current time and the anchor time.
timeSinceAnchor := atTime.Sub(anchorTime())
if timeSinceAnchor == time.Duration(math.MaxInt64) || timeSinceAnchor == time.Duration(math.MinInt64) {
return time.Time{}, time.Time{}, errors.New("shard boundary math broken: anchor time too far away")
}
// Compute the amount of time between the left-hand edge of the most recent
// "0" chunk and the current time.
atTimeOffset := time.Duration(atTime.Sub(time.Time{}).Nanoseconds() % windowWidth.Nanoseconds())
timeSinceZeroChunk := time.Duration(timeSinceAnchor.Nanoseconds() % windowWidth.Nanoseconds())
// Compute the left-hand edge of the most recent "0" chunk.
zeroStart := atTime.Add(-atTimeOffset)
zeroStart := atTime.Add(-timeSinceZeroChunk)
// Compute the width of a single shard.
shardWidth := time.Duration(windowWidth.Nanoseconds() / int64(cu.numShards))
@ -499,5 +522,5 @@ func (cu *crlUpdater) getShardBoundaries(atTime time.Time, shardIdx int) (time.T
shardStart = shardStart.Add(windowWidth)
shardEnd = shardEnd.Add(windowWidth)
}
return shardStart, shardEnd
return shardStart, shardEnd, nil
}

View File

@ -285,32 +285,40 @@ func TestGetWindowForShard(t *testing.T) {
lookbackPeriod: 7 * 24 * time.Hour,
lookforwardPeriod: 100 * 24 * time.Hour,
}
zeroTime := time.Time{}
// At just a moment past the 0 time, the zeroth shard should start at time 0,
// and end exactly one day later.
start, end := tcu.getShardBoundaries(zeroTime.Add(time.Minute), 0)
test.Assert(t, start.IsZero(), "start time should be zero")
test.AssertEquals(t, end, zeroTime.Add(24*time.Hour))
// At just a moment past the anchor time, the zeroth shard should start at
// time 0, and end exactly one day later.
start, end, err := tcu.getShardBoundaries(anchorTime().Add(time.Minute), 0)
test.AssertNotError(t, err, "")
test.Assert(t, start.Equal(anchorTime()), "start time should be the anchor time")
test.AssertEquals(t, end, anchorTime().Add(24*time.Hour))
// At the same moment, the 93rd shard should start 93 days later.
start, end = tcu.getShardBoundaries(zeroTime.Add(time.Minute), 93)
test.AssertEquals(t, start, zeroTime.Add(93*24*time.Hour))
test.AssertEquals(t, end, zeroTime.Add(94*24*time.Hour))
start, end, err = tcu.getShardBoundaries(anchorTime().Add(time.Minute), 93)
test.AssertNotError(t, err, "")
test.AssertEquals(t, start, anchorTime().Add(93*24*time.Hour))
test.AssertEquals(t, end, anchorTime().Add(94*24*time.Hour))
// If we jump 100 days into the future, now the 0th shard should start 107
// days after the zero time.
start, end = tcu.getShardBoundaries(zeroTime.Add(100*24*time.Hour+time.Minute), 0)
test.AssertEquals(t, start, zeroTime.Add(107*24*time.Hour))
test.AssertEquals(t, end, zeroTime.Add(108*24*time.Hour))
start, end, err = tcu.getShardBoundaries(anchorTime().Add(100*24*time.Hour+time.Minute), 0)
test.AssertNotError(t, err, "")
test.AssertEquals(t, start, anchorTime().Add(107*24*time.Hour))
test.AssertEquals(t, end, anchorTime().Add(108*24*time.Hour))
// During day 100, the 93rd shard should still start at the same time (just
// over 7 days ago), because we haven't fully left it behind yet. The 92nd
// shard, however, should have jumped into the future.
start, end = tcu.getShardBoundaries(zeroTime.Add(100*24*time.Hour+time.Minute), 93)
test.AssertEquals(t, start, zeroTime.Add(93*24*time.Hour))
test.AssertEquals(t, end, zeroTime.Add(94*24*time.Hour))
start, end = tcu.getShardBoundaries(zeroTime.Add(100*24*time.Hour+time.Minute), 92)
test.AssertEquals(t, start, zeroTime.Add(199*24*time.Hour))
test.AssertEquals(t, end, zeroTime.Add(200*24*time.Hour))
start, end, err = tcu.getShardBoundaries(anchorTime().Add(100*24*time.Hour+time.Minute), 93)
test.AssertNotError(t, err, "")
test.AssertEquals(t, start, anchorTime().Add(93*24*time.Hour))
test.AssertEquals(t, end, anchorTime().Add(94*24*time.Hour))
start, end, err = tcu.getShardBoundaries(anchorTime().Add(100*24*time.Hour+time.Minute), 92)
test.AssertNotError(t, err, "")
test.AssertEquals(t, start, anchorTime().Add(199*24*time.Hour))
test.AssertEquals(t, end, anchorTime().Add(200*24*time.Hour))
// If we jump more than 290 years into the future, the math should break.
_, _, err = tcu.getShardBoundaries(anchorTime().Add(150*365*24*time.Hour).Add(150*365*24*time.Hour), 0)
test.AssertError(t, err, "")
}