Phase 4b: ed25519 signed measurements (x402 trust layer) - #69
Conversation
Every paid catalog 2xx JSON response now carries a detached ed25519 signature over its exact body, so a buyer can prove a measurement came from Hyrule and was not altered — the ecosystem's most-requested missing piece. Ships dark: no key configured => no signing, no manifest advertisement. - services/signing.py: ResponseSigner (ed25519), verify_signature, load_signer (None when unconfigured), and the published key document. - middleware/signing.py: pure-ASGI middleware that buffers the body of a paid 2xx application/json response and adds Hyrule-Signature (ed25519=<b64>) + Hyrule-Signature-Key, exposed via CORS. 402/501, free endpoints, and non-JSON/streaming responses pass through untouched; a signing failure returns the body unsigned rather than 500ing. - app.py: load the signer in lifespan (dark unless keyed), register the middleware outermost, serve /.well-known/hyrule-signing-key.json (404 when unconfigured), add the llms.txt verification note. - discovery.py: manifest advertises signingKey only when configured. - config.py/state.py: response_signing_key/_id + AppState.response_signer. - canary: --verify-signature asserts every paid 2xx carries a valid signature against the published key. - runbook + .env.example document key provisioning and dual-publish rotation. Tests: signer roundtrip + tamper, load gating, key document, middleware (paid-2xx signed & verifies; 402/501/non-JSON/free not signed; no-signer no-ops), manifest + well-known advertisement. 416 pass, ruff + mypy strict clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 62e1c3aaa0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "keys": [ | ||
| { | ||
| "kid": signer.key_id, |
There was a problem hiding this comment.
Publish the retiring signing key during rotation
After an operator rotates HYRULE_RESPONSE_SIGNING_KEY, this document always publishes only the new active key, despite the response header carrying a key ID and the runbook requiring dual publication. A buyer who retrieves a response signed before the rotation and then fetches the well-known document cannot locate that response's old key or verify it, so the promised durable verifiability breaks at every rotation. Add configuration/storage for retired public keys and include them in keys[] until their verification window expires.
Useful? React with 👍 / 👎.
Bring #69's ed25519 response-body signing under the canonical trust layer instead of shipping a parallel signing stack. Measurements attest the DATA; receipts attest the transaction — independent flags, one key namespace, one JWKS. - trust/measurements.py: MeasurementSigner (ed25519 detached signature over exact 2xx JSON body bytes), verify_measurement_signature, load_measurement_signer, enforce_measurement_key_guard (fail-closed boot guard mirroring receipts), measurement_jwks_entries. - middleware/signing.py: ResponseSigningMiddleware — signs paid 2xx JSON from the enabled catalog, Hyrule-Signature/-Key headers + CORS expose; passthrough when unconfigured (soft-fail). - config TrustConfig: TRUST_MEASUREMENT_SIGNING_ENABLED/KEY/KEY_ID/ RETIRED_JWKS_JSON (default off). - Unified JWKS: build_jwks appends the OKP/Ed25519 key (active + retired) so one /.well-known/jwks.json verifies receipts AND measurements. - Advertise under agent-registration signedMeasurements only — NOT the x402 manifest, preserving the byte-identical-off invariant. - app.py: enforce_measurement_key_guard + middleware install. - x402_canary.py: --verify-signature re-derives the pubkey from jwks. - Docs: trust-layer component row, trust-keys runbook section + rotation, .env.example block. 9 new tests; 397 pass, mypy strict + ruff clean. Supersedes and closes #69. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Folded into the canonical trust layer (#56) rather than shipping a parallel signing stack — per operator decision to keep one key namespace, one JWKS, and one runbook for everything the service signs. Where it went (commit
9 dedicated tests carried over; full suite 397 pass, mypy strict + ruff clean. Closing in favor of #56. |
What
Every paid catalog 2xx JSON response now carries a detached ed25519 signature over its exact body, so a buyer (or a third party) can prove a Hyrule measurement is authentic and unaltered. This is the x402 ecosystem's most-requested missing piece — a verifiable trust layer, which no other provider offers.
Ships dark: no key configured ⇒ no signing, nothing advertised. Stacked on #67. Operator enablement in
docs/runbooks/response-signing.md.How
services/signing.py—ResponseSigner(ed25519),verify_signature,load_signer(returnsNonewhen unconfigured), and the published key document.middleware/signing.py— pure-ASGI middleware that buffers a paid 2xxapplication/jsonbody and addsHyrule-Signature: ed25519=<b64>+Hyrule-Signature-Key: <kid>, exposed viaAccess-Control-Expose-Headers. 402/501, free (non-catalog) endpoints, and non-JSON/streaming (icons, snapshots) pass through untouched. A signing failure returns the body unsigned rather than 500ing.app.py— loads the signer in lifespan (dark unless keyed), registers the middleware outermost, serves/.well-known/hyrule-signing-key.json(404 when unconfigured), adds the/llms.txtverification note.discovery.py—build_x402_manifestadvertises asigningKeyblock only when configured.response_signing_key/response_signing_key_id,AppState.response_signer.--verify-signatureasserts every paid 2xx carries a valid signature against the published key.Signed vs not
application/jsonHyrule-Signature+ key id/health, pricing, capabilities)Tests
tests/test_signing.py: signer roundtrip + tamper-detection, load-gating, key-document shape, middleware behaviour (paid-2xx signed & verifies against the published key; 402/501/non-JSON/free not signed; no-signer no-ops), and manifest + well-known advertisement.416 passed,ruff check+mypy --strictclean.Operator enablement (not in this PR)
Generate a 32-byte seed → Vault
kv/hyrule-cloudresponse_signing_key+response_signing_key_id→ restart api. Verify withx402_canary.py dns --verify-signature. Rotation = dual-publish. See the runbook.🤖 Generated with Claude Code