feat(evidence): publish retired-key rotation history in the JWK Set (v0.1.25.33)#196
Merged
Merged
Conversation
…v0.1.25.33) Extend getEvidenceJwks (GET /v1/.well-known/cycles-jwks.json) to publish operator-configured RETIRED signing keys alongside the active key, so CyclesEvidence signed before a key rotation still verifies against the key whose [cycles_nbf_ms, cycles_exp_ms) window covers its issued_at_ms. - JwksController: new @value cycles.evidence.signing.retired-keys (env EVIDENCE_SIGNING_RETIRED_KEYS), a JSON array parsed via a local ObjectMapper (no injected bean, preserving @WebMvcTest loadability); fail-safe to empty on malformed/non-array config — the active key always still publishes. - JwksDocuments.jwkSet: 4-arg overload (3-arg delegates with List.of()) that appends each retired key as a bounded-window JWK (status:retired, cycles_exp_ms EXCLUSIVE/required). - Defensive skips (codex review, 3 Medium): malformed hex; null exp_ms; empty/inverted window (exp <= nbf); missing/non-integral nbf_ms (not coerced to epoch 0); same key material as the active key (ambiguous window); duplicate kid (set-wide uniqueness). No spec change (CyclesEvidenceJwks already supports multi-key + windows + status:retired) and no wire change to existing endpoints. Scope: publication of configured rotation history only — Redis-backed auto-rotation and did:cycles-form producer stamping remain follow-ups. Tests: JwksDocumentsTest +7, JwksControllerTest +4; both classes 100% line- covered. Full mvn verify green; jacoco 95% gate met. AUDIT.md updated.
…ries The CyclesEvidence run (the getEvidenceJwks integration test, v0.1.25.22 through .33, and the two WIP entries) had been crammed onto a single physical line as one run-on paragraph, and each entry was a 130-360 word wall of nested parentheticals, ALL-CAPS, and review-round play-by-play — inconsistent with the clean ~110-170 word prose entries from v0.1.25.21 down. - Split the run-on line so each entry sits on its own line, matching the format used for v0.1.25.20 and below. - Rewrite each CyclesEvidence entry to the established compact voice: flowing prose, one level of parentheses, no ALL-CAPS, a simple closing test/build line. Every entry's version, what-changed, why, and test outcome are preserved; only the verbosity is cut. Docs-only; no code, spec, or wire change.
… retired key material Two review findings on the retired-key JWK Set: - Rotation must advance the active key's nbf-ms. The previous rotation doc only set exp_ms on the retiring key; leaving nbf-ms at the default 0 publishes the new active key as valid since epoch, so it could sign a backdated envelope (issued_at_ms before the rotation) that still resolves as authentic — defeating window-gated selection. Fixed the documented procedure and added a startup warning when retired keys are configured but the active nbf-ms is at/before a retired window's end. - Skip same key material with an overlapping window. JwksDocuments now tracks emitted [nbf, exp) windows per key material (including the active key's open-ended window) and skips a retired entry whose window overlaps an already-published one for the same material; disjoint windows for one key (legitimate reuse across non-overlapping periods) are preserved. Replaces the unconditional active-material skip, which wrongly dropped a disjoint retired window. JwksDocumentsTest +10, JwksControllerTest +6; both classes 100% line- covered. Full mvn verify green, jacoco 95% gate met. AUDIT.md updated. Folded into v0.1.25.33 (unmerged).
…-safe) Review finding: detecting active nbf-ms < latest retired exp_ms but only warning still publishes the unsafe JWKS — a logged warning doesn't close the hole, so the current key can sign backdated evidence (issued_at_ms before the rotation) that resolves as authentic. Fail closed by clamping instead of failing the endpoint: JwksDocuments advances the published active cycles_nbf_ms up to the latest declared retired exp_ms (Math.max), so the active key is never valid for pre-rotation issued_at_ms while the endpoint stays up and post-rotation evidence still resolves. The floor counts EVERY declared bounded retired window — including one whose key material is malformed and therefore not published — so a typo in rotation history can't reopen the hole. The controller warning is kept as an operator nudge and its predicate aligned to the same bounded-window filter. Also drops the now-redundant active-key overlap seed: the clamp guarantees the active window starts at/after every retired exp, so a same-material retired key is always disjoint (its history is preserved, not dropped). JwksDocumentsTest +13, JwksControllerTest +6; both classes 100% line- covered. Full mvn verify green, jacoco 95% gate met. AUDIT.md updated. Folded into v0.1.25.33 (unmerged).
The retired-keys property comment still said leaving nbf-ms at 0 "publishes the new active key as valid since epoch" — stale since the active cycles_nbf_ms is now warned on and fail-safe clamped up to the latest retired exp_ms. Reword to describe the clamp while still recommending an explicit nbf-ms. Comment-only; no code/behavior change.
… wording - parseRetiredKeys checked isIntegralNumber() then asLong(), which wraps out-of-range integers (Jackson treats a BigInteger as integral, and asLong() saturates 2^63 to Long.MIN_VALUE). Also require canConvertToLong() for both nbf_ms and exp_ms so an out-of-range bound is dropped, not wrapped into a corrupt window. JwksControllerTest +1 (out-of-range exp skipped). - AUDIT.md said retired entries are skipped when "key material matching the active key," but same-material entries now publish once the clamp makes their window disjoint. Corrected the skip list (and noted out-of-range bounds are rejected). Both classes 100% line-covered; full mvn verify green, 95% gate met. Folded into v0.1.25.33 (unmerged).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends
getEvidenceJwks(GET /v1/.well-known/cycles-jwks.json) to publish operator-configured retired signing keys (rotation history) alongside the active key — the v0.2-store follow-up flagged in v0.1.25.32. Evidence signed before a key rotation now still verifies against the key whose[cycles_nbf_ms, cycles_exp_ms)window covers itsissued_at_ms(never "the current key").v0.1.25.33· no spec change · no wire change to existing endpoints.What changed
JwksController— new@Valuecycles.evidence.signing.retired-keys(envEVIDENCE_SIGNING_RETIRED_KEYS): a JSON array of{signer_did (raw 64-hex), kid, nbf_ms, exp_ms}, parsed via a localObjectMapper(no injected bean, so@WebMvcTestloadability is preserved). Fail-safe to empty on malformed/non-array config — the active key always still publishes.JwksDocuments.jwkSet— 4-arg overload (the old 3-arg delegates withList.of()) that appends each retired key as a bounded-window JWK (status:retired,cycles_exp_msEXCLUSIVE/required).exp_ms; empty/inverted window (exp <= nbf); missing/non-integralnbf_ms(NOT coerced to epoch 0); same key material as the active key (ambiguous window); duplicatekid(set-wide uniqueness).Rotation procedure
On rotation: set the new key as
signer-didactive, append the old one toretired-keyswithexp_ms= the rotation time.Review
Codex read-only review surfaced 3 Medium window-validation findings (required-integral
nbf_ms, empty/inverted-window guard, active/retired overlap guard) — all applied in this PR.Scope
Publication of configured rotation history only. Redis-backed auto-rotation mechanics and
did:cycles-formsigner_didproducer stamping remain separate follow-ups.Tests
JwksDocumentsTest+7,JwksControllerTest+4; both classes 100% line-covered. Fullmvn verifygreen; jacoco 95% gate met.AUDIT.mdupdated.