Sign requests with RFC 9421 HTTP Message Signatures - #39
Conversation
Override ajv to v8 (root-hoisted v6 from eslint broke ajv-draft-04 via incur -> @readme/openapi-parser) and build core before x402/cli tests, since the workspace symlink resolves through core's dist/ exports map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The X-AgentKit bare EIP-191 body signature was a permanent, universal bearer credential (no audience, method, path, expiry, or nonce binding) and a cross-protocol signing oracle. Requests are now signed under a closed RFC 9421 profile covering @method, @authority, @path, @query, and content-digest (RFC 9530), with created/expires/nonce/keyid/tag params, EIP-191 over the signature base, and recovered-signer == keyid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… protection The hooks now verify against the request's real method and URL through a single core verifyRequest call (dropping the duplicate address recovery), and enforce single-use nonces via restored hasUsedNonce/recordNonce storage methods. The client signs the RFC 9421 signature base and retries with Signature-Input, Signature, and Content-Digest instead of X-AgentKit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prove now takes <method> <url> [body] and returns the Content-Digest, Signature-Input, and Signature header values instead of a bare EIP-191 body signature, using the shared profile implementation from @worldcoin/agentkit-core. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single-use signatures via a nonce move to a follow-up PR. Replay of a byte-identical request is bounded by the five-minute created/expires window until then; docs state this explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| export const CONTENT_DIGEST_HEADER = 'Content-Digest' | ||
| export const SIGNATURE_LABEL = 'agentkit' | ||
| export const MAX_SIGNATURE_AGE_SECONDS = 300 | ||
| export const CLOCK_SKEW_SECONDS = 5 |
There was a problem hiding this comment.
Allows signatures generated 5 seconds into the future, accounts for machines slightly out of sync
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52265a962b
ℹ️ 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".
| signatureHeaders = await createSignatureHeaders({ | ||
| method: request.method, | ||
| url: request.url, | ||
| body, |
There was a problem hiding this comment.
Handle redirects before signing the request URL
When the initial fetch follows a redirect to an AgentKit-enabled 402, response.url is the final protected URL but request.url remains the original URL. The wrapper therefore signs the original authority/path and retries it; after the redirect, the server rebuilds the signature base from the final URL and rejects the signature. This breaks common canonical-host, HTTP-to-HTTPS, and trailing-slash redirects, so the wrapper should either sign/retry the effective request target or explicitly avoid unsupported redirects.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Replaces body-only EIP-191 authentication with request-bound RFC 9421 signatures across Core, x402, and CLI.
Changes:
- Adds signature creation, parsing, expiry, digest, and signer verification.
- Updates x402 and CLI request-signing flows.
- Refreshes tests, documentation, dependencies, and release metadata.
Unresolved issues include bodyless JSON request rejection, case-sensitive discount matching, and an incompatible global AJV override.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
core/src/signature.ts |
Implements signature profile. |
core/src/verify.ts |
Verifies signed requests. |
core/src/index.ts |
Exports new APIs. |
core/tests/signature.test.ts |
Tests signature handling. |
core/tests/verify.test.ts |
Tests request verification. |
core/tests/exports.test.ts |
Tests public exports. |
x402/src/client.ts |
Signs x402 retries. |
x402/src/hooks.ts |
Verifies incoming requests. |
x402/src/protocol.ts |
Defines signature headers. |
x402/src/index.ts |
Exports protocol constants. |
x402/tests/client.test.ts |
Tests client signing. |
x402/tests/client-e2e.test.ts |
Adds end-to-end verification. |
x402/tests/hooks.test.ts |
Tests server hooks. |
x402/package.json |
Adds test preparation. |
x402/DOCS.md |
Documents the new flow. |
cli/src/prove.ts |
Creates proof headers. |
cli/src/index.ts |
Updates prove command. |
cli/test/prove.test.ts |
Tests CLI proofs. |
cli/README.md |
Updates CLI usage. |
cli/REGISTRATION.md |
Updates signing guidance. |
cli/package.json |
Adds Core dependency. |
skills/integrate-agentkit/SKILL.md |
Updates Core integration guidance. |
skills/integrate-agentkit-x402/SKILL.md |
Updates x402 integration guidance. |
skills/agentkit-x402/SKILL.md |
Updates agent signing workflow. |
package.json |
Adds AJV override. |
bun.lock |
Updates dependency resolution. |
.changeset/rfc9421-signatures.md |
Records release changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…rfaced addresses verifyRequest now returned the lowercase wire keyid while extractPayer reads the payment payload's from verbatim (usually EIP-55 checksummed), so the pendingDiscounts lookup never matched and discount recovery silently stopped firing. Discount keys now lowercase both sides, and core surfaces EIP-55 checksummed addresses (results, error addresses, lookups), restoring the pre-RFC-9421 observable behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deriveComponents interpolated the caller's method string raw, so a direct createSignatureHeaders caller could inject extra lines into the base being signed. Verification was never spoofable (the verifier rebuilds a fixed six-line base from a real Request) and existing callers were guarded upstream, but core is the public signing API, so the letters-only check now lives at the shared choke point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Body normalization ran before the method check, so a bodyless GET carrying Content-Type: application/json (a common HTTP-client default) passed undefined into normalizeAgentkitJsonBody and was rejected before verification. Determine the method first, skip body retrieval for GET/HEAD, and treat a missing adapter body as the signed empty body — matching what clients sign. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five seconds only covered well-synced clocks; laptops after sleep, VMs, and containers without NTP are routinely seconds fast and would fail every request with SIGNATURE_NOT_YET_VALID. Slow clocks were already tolerated for up to 300 seconds via the age check, so this only evens out the asymmetry and extends the worst-case replay window marginally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stacked on #38. Replaces the bare EIP-191 body signature with RFC 9421 HTTP Message Signatures + RFC 9530 Content-Digest.
Why
The
X-AgentKitbarepersonal_signover body bytes has two issues:provesigned arbitrary attacker-influenceable strings withpersonal_sign. A body that happens to be a valid message in another EIP-191 protocol (e.g. a SIWE login message) yielded a real signature from the agent's key.The AgentKit RFC 9421 profile
Contains three headers:
Signature-InputSignatureContent-DigestRFC 9421 has been adopted by Cloudflare and others.
Next steps (follow-up PRs)
created/expireswindow, and the docs state this explicitly. Follow up PR to add nonce support and storage.