Skip to content

feat(evidence): implement getEvidenceJwks JWK Set endpoint (v0.1.25.32)#194

Merged
amavashev merged 1 commit into
mainfrom
feat/evidence-jwks-endpoint
Jun 15, 2026
Merged

feat(evidence): implement getEvidenceJwks JWK Set endpoint (v0.1.25.32)#194
amavashev merged 1 commit into
mainfrom
feat/evidence-jwks-endpoint

Conversation

@amavashev

Copy link
Copy Markdown
Collaborator

What

The reference implementation of the v0.2 signer-key-resolution publication endpoint — getEvidenceJwks. This is PR 2 of 2; the spec-first half landed in runcycles/cycles-protocol#113 (cycles-protocol-v0.yaml v0.1.25.6). Together they let a consumer resolve a did:cycles signer_did (or confirm a raw-hex one) to a public key and establish signer authority, not just signature validity.

Design signed off by APS on aeoess#43; tracked in #103.

Changes (v0.1.25.31 → .32)

  • JwksController — public GET /v1/.well-known/cycles-jwks.json. Reads the shared cycles.evidence.signing.signer-did + new kid / nbf-ms via @Value (holds no injected bean → loads in every @WebMvcTest without new wiring). Cache-Control: public, max-age=300not immutable (a key set rotates). 404 via the standard NOT_FOUND ErrorResponse when no raw-hex key is configured.
  • JwksDocuments — pure builder. Raw 64-hex signer-did → one active Ed25519 OKP JWK: {kty:OKP, crv:Ed25519, alg:EdDSA, x:base64url(hex-decode(signer_did)), kid (default = first 16 hex), cycles_nbf_ms (default 0), status:active}; cycles_exp_ms omitted ⇒ open-ended. The x is the same 32 bytes EnvelopeSigner signs with, so a verifier resolving the set authenticates the emitted signatures. did:cycles / blank / malformed → empty (404) — a did:cycles signer_did carries no key bytes.
  • SecurityConfig/v1/.well-known/** public (public keys only; private key never served; API-base-relative per the authority-scope rule). SecurityConfigTest updated.
  • application.properties (kid / nbf-ms), pom revision, AUDIT entry.

Scope / deferred

v0.1 raw-hex publication. Retired-key rotation history (a key store with cycles_exp_ms windows) and did:cycles-form publication are the additive v0.2-store follow-up — and can't be built before the endpoint exists.

Tests / verification

  • JwksDocumentsTest (10), JwksControllerTest (4 — 200 + JWK-set body + short public non-immutable cache, @WebMvcTest contract-validated against fix(deps): bump Spring Boot 3.5.11→3.5.13, pin tomcat 10.1.54 (v0.1.25.16) #113's spec on main; unconfigured/did:cyclesNOT_FOUND).
  • Both new classes 100% line-covered; full mvn verify 906 tests green; jacoco 95% gate met.
  • codex review: no findings (confirmed xEnvelopeSigner byte parity, did:cycles 404, no private-key reference, 200/404 contract shapes).

Refs: #113, #103, aeoess#43.

Publication half of the v0.2 signer-key-resolution layer (design on #103 /
aeoess#43; contract added to cycles-protocol-v0.yaml v0.1.25.6 in
runcycles/cycles-protocol#113).

- JwksController: public GET /v1/.well-known/cycles-jwks.json. Reads the shared
  cycles.evidence.signing.signer-did + new kid / nbf-ms via @value (no injected
  bean, so it loads in every @WebMvcTest without extra wiring). Cache-Control:
  public, max-age=300 — NOT immutable (a key set rotates). 404 via the standard
  NOT_FOUND ErrorResponse when no raw-hex key is configured.
- JwksDocuments: pure builder. Raw 64-hex signer-did -> one active Ed25519 OKP
  JWK {kty,crv,alg,x=base64url(hex-decode(signer_did)),kid (default first-16-hex),
  cycles_nbf_ms (default 0), status:active}; cycles_exp_ms omitted = open-ended.
  The x is the SAME 32 bytes EnvelopeSigner signs with, so a verifier resolving
  the set authenticates the emitted signatures. did:cycles / blank / malformed ->
  empty (404): a did:cycles signer_did carries no key bytes (that + retired-key
  rotation history are the v0.2-store follow-up).
- SecurityConfig: /v1/.well-known/** public (public keys only; private key never
  served; API-base-relative per the authority-scope rule). SecurityConfigTest
  updated for the new entry.
- application.properties: cycles.evidence.signing.kid / nbf-ms. pom .31 -> .32.

Tests: JwksDocumentsTest (10), JwksControllerTest (4, contract-validated against
#113's spec). Both new classes 100% line-covered; full mvn verify 906 green;
jacoco 95% gate met. codex review: no findings. No change to existing endpoints.
@amavashev amavashev merged commit a100337 into main Jun 15, 2026
8 checks passed
@amavashev amavashev deleted the feat/evidence-jwks-endpoint branch June 15, 2026 13:24
amavashev added a commit that referenced this pull request Jun 15, 2026
JwksEndpointIntegrationTest (extends BaseIntegrationTest: full @SpringBootTest
RANDOM_PORT, real Tomcat + the Spring Security filter chain ACTIVE, Testcontainers
Redis) proves the JWK Set endpoint serves over real HTTP WITHOUT an API key — the
/v1/.well-known/** public-path exemption actually holds end-to-end through the
filter chain, not just as an array entry (the JwksControllerTest @WebMvcTest runs
with filters disabled, so it can't show this).

With the evidence signing identity set via @TestPropertySource: GET
/v1/.well-known/cycles-jwks.json with no header → 200 + a JWK whose x decodes to
exactly the configured signer_did bytes, correct kid/cycles_nbf_ms/status, and
Cache-Control: public, max-age (NOT immutable); a bogus API key still yields 200
(public, never 401). The base class's contract-validating interceptor also checks
the body against the published CyclesEvidenceJwks schema (cycles-protocol@main,
#113).

codex review: no findings. 2 tests; test-only (impl shipped in v0.1.25.32 / #194;
no production/wire/spec change).
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