crl: Refuse to publish CRLs that are missing entries - #8984
crl: Refuse to publish CRLs that are missing entries#8984beautifulentropy wants to merge 5 commits into
Conversation
cb9afec to
d1ff4e8
Compare
d1ff4e8 to
8fc11a7
Compare
8fc11a7 to
369879b
Compare
|
@beautifulentropy, this PR appears to contain configuration and/or SQL schema changes. Please ensure that a corresponding deployment ticket has been filed with the new values. |
aarongable
left a comment
There was a problem hiding this comment.
Have only reviewed the CRL code (i.e. not the SA code), initial comments below.
52fb7af to
df0b140
Compare
df0b140 to
ac86fc0
Compare
| ) | ||
| if err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| return nil, berrors.NotFoundError("no revoked certificates for issuer %d shard %d", req.IssuerNameID, req.ShardIdx) |
There was a problem hiding this comment.
I think this will prevent us from generating CRLs from issuers that haven't actually started issuing certs yet, which is part of our normal process for bringing new intermediates online.
jsha
left a comment
There was a problem hiding this comment.
Overall looks great. The index makes sense to me. It will be high cardinality because of the date, but I think that's okay for this table.
| // checkFreshness returns an error unless the entries read from a replica | ||
| // include the primary's most recent revocation for the shard, or neither has | ||
| // any. If the primary in unavailable an error is returned. |
There was a problem hiding this comment.
API design quirk: "returns an error unless the entries read from a replica ..." - the property "these entries were read from a replica" is really a property of the caller, not this function. How about:
// checkFreshness queries the primary's GetLatestRevokedByShared with the given req,
// and errors if entries is doesn't contain the latest revoked serial for that shard, or if
// the primary says there are no revocations but entries is nonempty.
| // | ||
| // TODO(#8983): Default this field to 5 minutes once #8984 has been | ||
| // deployed to production and the freshness check is made unconditional. | ||
| ThisUpdateBackdate config.Duration `validate:"-"` |
There was a problem hiding this comment.
I mentioned at #6456 (comment): I think backdating CRLs is not allowed. Instead I think we can query GetLatestRevokedByShard with a RevokedBefore that is 5 minutes earlier than atTime (the value passed to GetRevokedCertsByShard).
There was a problem hiding this comment.
I think that's worse. Suppose a certificate is revoked at time T=0, expires at time T=+2m, we issue a CRL at time T=+4m, and we issue the next CRL at time T=+20m.
Under the backdating system, we don't include the new entry on the first CRL, because we're computing the set of revoked certs at time T=-1m. When we go to issue the second CRL, we ensure we include the cert because it hasn't appeared on a CRL whose thisUpdate (T=-1m) is after its expiry (T=+2m).
Under your proposed system, the first step is the same, except that the CRL's thisUpdate is T=+4m. But then when we go to issue the second CRL, we incorrectly omit the entry, because its expiry (T=+2m) is before the previous CRL's thisUpdate (T=+4m). This would be a violation of the compliance requirement that the cert appear on at least one CRL after it expires.
We could adjust that check by 5 minutes too, but now I'm getting concerned about correctness, especially if that 5-minute period ever gets changed between deploys.
I'm less convinced than you are that the BRs forbid backdating CRLs. I think that setting the thisUpdate to a time which the CRL doesn't actually represent would be bad, but setting to the time as which the set of revoked certificate entries is correct seems... correct. There will always pass some amount of time between when the thisUpdate field is set and when the CA signature is computed. Even if that's just a single second, the CRL is suddenly "backdated" per your interpretation of the BRs.
Fixes #6456
Notes on the new index
GetLatestRevokedCertByShard asks the primary database for the single most recent revocation in a shard, once per shard per update.
The only existing index that covers this query's filters is (issuerID, shardIdx, notAfterHour), so without a new one that query reads every unexpired row in the shard and sorts them. With (issuerID, shardIdx, revokedDate) the database walks the index backwards and stops at the first row.
Testing: 500k and 5M synthetic revocations were loaded with and without the index applied (2 issuers, 128 shards, revocations spread evenly over 90 and 365 days, certificate lifetimes of 1 to 90 days). The following EXPLAIN query with handler counters was run:
Without the index the query scans every unexpired row in the shard and does a filesort: 1,928 rows in 5.6 ms at 500k, 4,884 rows in about 220 ms at 5M with a small buffer pool. With the new index it reads one entry (with no sort) in 0.2 ms and ~1 ms respectively. The index costs about 36 bytes per row, smaller than the existing notAfterHour index, and built online in under 6 seconds at 5M rows.
Deployment process
This change requires a two-phase deployment. As usual, deploying the code as-is changes nothing in production.
Phase 1: Apply the
revokedCertificatesindex, and addcrl-storerto the SA'ssa.StorageAuthorityReadOnlyallowlist. This phase alone does not change production behavior.Phase 2: Set
saReadOnlyServicein the crl-storer config andcheckFreshness: truein the crl-updater config.