Skip to content

Refresh release-pinned TSA CRL snapshots (unblocks all releases) + add refresh tooling#48

Merged
sftimeless merged 3 commits into
mainfrom
codex/refresh-tsa-crl-snapshots
Jul 15, 2026
Merged

Refresh release-pinned TSA CRL snapshots (unblocks all releases) + add refresh tooling#48
sftimeless merged 3 commits into
mainfrom
codex/refresh-tsa-crl-snapshots

Conversation

@sftimeless

Copy link
Copy Markdown
Member

The release-pinned TSA CRL snapshots have gone stale, and the 7-day CI freshness guard has been failing every keel-verifier PR since roughly July 8 — silently, because no PR was opened in that window. It only surfaced when #47 went up.

CRL was now
GlobalSign × 3 2026-07-15 (expiring) 2026-10-15 / 2027-01-15
DigiCert × 3 2026-07-22 (inside the guard) 2026-07-29

Earliest next_update moves 2026-07-15 → 2026-07-29, so the guard passes with headroom. This blocks all releases, not just #47, which is why it is split out rather than folded into that PR.

Safety

Each CRL was re-fetched from its CA's published distribution point and accepted only after:

  1. its issuer matched the vendored issuer certificate's subject, and
  2. its signature verified against that certificate's public key.

The vendored CA certificate remains the trust anchor, so a wrong URL, a stale mirror, or a poisoned response cannot enter the bundle — it fails verification and the refresh aborts. (That is also what made probing candidate URLs safe: an incorrect guess simply cannot pass.)

Regenerating the bundle also updates TSA_TRUST_BUNDLE_V1_HASH. The embedded release manifest is deliberately not regenerated here — release.yml rebuilds it from the actual files at tag time.

Why this rotted unnoticed

The CRL source URLs were never recorded anywhere, and an intermediate CA's own CRL URL appears only in the leaf certificates it issues — GlobalSign's had to be recovered from a live TSA response to find it at all.

scripts/refresh_tsa_trust_bundle.py performs exactly the verified refresh described above, and now records each CRL's source_url in the bundle, so the next refresh is a single command:

python scripts/refresh_tsa_trust_bundle.py            # refresh in place
python scripts/refresh_tsa_trust_bundle.py --dry-run  # verify only

GlobalSign issues ~13-day CRLs against a 7-day guard, so this recurs roughly fortnightly. A scheduled job that runs the script and opens a PR would retire the chore entirely — worth doing, but out of scope here.

Verification

check_tsa_trust_bundle_freshness.py --min-valid-days 7 passes. Full suite: 532 passed. The one failure, test_released_artifact_parity, fails identically on clean main with these changes stashed — pre-existing and unrelated.

🤖 Generated with Claude Code

sftimeless and others added 3 commits July 14, 2026 15:30
The bundled GlobalSign CRLs expire 2026-07-15 and DigiCert's 2026-07-22, so the
7-day CI freshness guard had been failing every keel-verifier PR since roughly
July 8 -- silently, because no PR was opened in that window. This refreshes all
six pinned CRLs; the earliest next_update moves from 2026-07-15 to 2026-07-29.

Each CRL was re-fetched from its CA's published distribution point and accepted
only after its issuer matched the vendored issuer certificate's subject and its
signature verified against that certificate's public key. The vendored CA
certificate remains the trust anchor, so a wrong URL or a poisoned response
cannot enter the bundle.

Adds scripts/refresh_tsa_trust_bundle.py, which performs exactly that verified
refresh and records each CRL's source_url in the bundle. Those URLs were never
written down, and an intermediate CA's own CRL URL appears only in the leaf
certificates it issues -- GlobalSign's had to be recovered from a live TSA
response -- which is much of why this rotted unnoticed. Recording them makes the
next refresh a single command.

Regenerating the bundle also updates TSA_TRUST_BUNDLE_V1_HASH. The embedded
release manifest is deliberately not regenerated here: release.yml rebuilds it
from the actual files at tag time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ruff (F841) flagged a leftover assignment in refresh_tsa_trust_bundle.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cript

Refreshing the pinned CRLs forward surfaced a second, coupled staleness. The
release-pinned public-CA receipt fixtures carry real RFC3161 tokens with a fixed
genTime (2026-07-02), and the verifier requires last_update <= genTime <
next_update for every chain CRL. Once the DigiCert CRLs rolled to last_update
2026-07-08, the July-02 receipts fell before the window and
tsa_chain_validation flipped to "invalid" -- caught by the real-crypto suite
(gated on OpenSSL 3, so it does not run in the default local pytest).

Re-mint both receipts against the same messageImprint from each CA's TSA; the
fresh genTimes land inside the new windows. The fixture stores the bare
timeStampToken (CMS ContentInfo), not the enclosing TimeStampResp, matching what
the verifier parses.

Fold this into refresh_tsa_trust_bundle.py so a refresh is atomic: the script now
re-mints the receipts after refreshing the CRLs and fails if any fresh genTime is
not covered by the new windows. A CRL-only refresh would otherwise leave the
real-crypto tests red, which is exactly how this was missed the first time.

Verified with the real-crypto suite (KEEL_OPENSSL_BIN=openssl@3): 9 passed,
including release_pinned_public_ca_receipts_validate_offline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sftimeless
sftimeless merged commit c4669a1 into main Jul 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant