Skip to content

fix(pki): authenticate CRLs, enforce their freshness in prod, and load them in prod - #179

Open
sinkog wants to merge 1 commit into
mainfrom
feature/relay-164-crl-verification
Open

sinkog wants to merge 1 commit into
mainfrom
feature/relay-164-crl-verification

Conversation

@sinkog

@sinkog sinkog commented Nov 14, 2026

Copy link
Copy Markdown
Member

Refs #164

What

checkRevocation accepted any CRL whose issuer name matched: nothing verified the signature or NextUpdate, an unparsable CRL was only logged, and in prod no CRL was ever loaded (bootstrapPKI read CIC_PLATFORM_CA_BUNDLE for certificates only), so revocation was inert in both modes.

  • cabinet.VerifyCRLSignature: a CRL must verify against a CA certificate of the trust store. One that does not is never used, in any mode.
  • cabinet.CRLFreshness / VerifyOptions.CRLFreshness / CheckCRLFresh: a CRL past NextUpdate (or without one) is rejected at verification time when the rule is set, so a CRL that goes stale while the relay runs stops being trusted. A nil rule keeps the old behaviour (dev).
  • prod: CRLs travel as X509 CRL blocks in CIC_PLATFORM_CA_BUNDLE; an unparsable, unauthenticated or stale CRL is a start-up error. CIC_CRL_MAX_STALENESS sets an explicit grace (default none; an invalid value is a start-up error).
  • dev stays lenient: a bad signature drops the CRL with a warning; an unparsable or stale CRL is logged.
  • prod states what is off: "no CRL in the bundle" and "SPKI pin set is empty" are logged as warnings.
  • context/SYMBOLS.md and MANIFEST.sha256 regenerated (3 new exported symbols); the CI manifest diff is empty locally.

Claim check against the issue

Confirmed: no signature or freshness check, unparsable CRL only warned, empty CRL set and empty pin set both pass. Additions the issue did not state: prod never loaded a CRL at all, and PinnedSPKIs is never populated in production code (pinning has no configuration source, not just an empty default).

Not done

"A missing CRL for a configured issuer is an error" needs a way to say which issuers require a CRL; none exists, and a root CA without a CRL is legitimate. Either a separate setting or "every CA in a prod bundle needs a CRL" would do it; that is a decision for the owner. Until then the absence is a visible warning, not an error. No SPKI pin source was added.

Tests

With the old behaviour emulated under the new API (signature and freshness checks off, prod not loading CRLs), these fail: TestBootstrapPKI_Prod_CRL_ForeignKeySignature_Error, TestLoadPKIAssets_Dev_CRL_ForeignKeySignature_NotUsed, ..._UnknownIssuer_Error, ..._Stale_Error, ..._InvalidTolerance_Error, ..._CRL_Unparsable_Error, ..._RevokedLeafStaysRejectedAfterRestart, ..._NoCRL_NoPins_AreVisible, TestVerifyCRLSignature, TestCheckCRLFresh, TestVerifyCertificateChain_StaleCRL. TestCrlToleranceFromEnv is characterization only (new function).

"Restart" in the revoked-leaf test means loading the same bundle twice, not a process restart.

Local gates (Docker golang:1.26.11): gofmt -s -l empty, go vet ./... clean, staticcheck (cabinet, cmd/relay) clean, go test -count=1 ./... all ok, no new entry in deadcode ./..., scripts/gen_manifest.sh diff empty.

Reachability

cmd/relay/main.go:694 bootstrapPKI -> prod branch -> parseBundleCRLs / acceptCRLs (cmd/relay/pki_crl.go) -> cabinet.VerifyCRLSignature / CheckCRLFresh. Verification: cmd/relay/bootstrap.go:78, :554, cmd/relay/pki_companion.go:78 pass CRLFreshness into cabinet.VerifyCertificateChain -> checkRevocation.

Notes

🤖 Generated with Claude Code

…d them in prod

checkRevocation trusted any CRL whose issuer name matched: nothing verified the
signature or NextUpdate, an unparsable CRL was only logged, and in prod no CRL
was ever loaded (bootstrapPKI read the CA bundle for certificates only), so
revocation was inert.

- cabinet.VerifyCRLSignature: a CRL must verify against a CA certificate of the
  trust store; one that does not is never used, in any mode
- cabinet.CheckCRLFresh / VerifyOptions.CRLFreshness: a CRL past NextUpdate (or
  without one) is rejected at verification time when the rule is set, so a CRL that
  went stale while the relay runs stops being trusted
- prod: CRLs travel as X509 CRL blocks in CIC_PLATFORM_CA_BUNDLE; an unparsable,
  unauthenticated or stale CRL is a start-up error. CIC_CRL_MAX_STALENESS sets an
  explicit grace (default none)
- dev stays lenient: a bad signature drops the CRL with a warning, an unparsable or
  stale CRL is logged
- prod states what is off: no CRL in the bundle and an empty SPKI pin set are
  logged as warnings

Not done: "a missing CRL for a configured issuer is an error" needs a way to
configure which issuers require a CRL; none exists. SYMBOLS.md and MANIFEST.sha256
are regenerated for the new exported API.

Refs #164

---
[signing-metadata]
key = cic-my-sign-key
signature = vault:v1:MEUCIQDrWjgkhxLJD46159ABItbjEpD4q1oDvQP6QUTjbLfyfQIgfv+LZhO3qUMG6PPi0UBjfUZ7/VOq5T91MS2AuUXXNNI=
hash-algorithm = sha256
digest = j7rmxT44e3XhwczyVCIpGBnQ818nzHrsxfVNF8W7Or8=

[certificate]
-----BEGIN CERTIFICATE-----
MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw
QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV
BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy
MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb
MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe
i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU
yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS
WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF
BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0
lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW
-----END CERTIFICATE-----
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