Skip to content

Sign requests with RFC 9421 HTTP Message Signatures - #39

Open
thomas-waite wants to merge 10 commits into
new-clifrom
rfc9421-signatures
Open

Sign requests with RFC 9421 HTTP Message Signatures#39
thomas-waite wants to merge 10 commits into
new-clifrom
rfc9421-signatures

Conversation

@thomas-waite

@thomas-waite thomas-waite commented Aug 20, 2026

Copy link
Copy Markdown

Stacked on #38. Replaces the bare EIP-191 body signature with RFC 9421 HTTP Message Signatures + RFC 9530 Content-Digest.

Why

The X-AgentKit bare personal_sign over body bytes has two issues:

  1. Permanent universal bearer credential — the signature bound nothing but the body: no host, method, path, or expiry. Any server that received one could replay it to every AgentKit service forever.
  2. Cross-protocol signing oracleprove signed arbitrary attacker-influenceable strings with personal_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-Input
  • Signature
  • Content-Digest

RFC 9421 has been adopted by Cloudflare and others.

Next steps (follow-up PRs)

  • Nonce-based single-use signatures. This PR deliberately ships without a nonce: replay of a byte-identical request is bounded by the five-minute created/expires window, and the docs state this explicitly. Follow up PR to add nonce support and storage.

thomas-waite and others added 5 commits August 19, 2026 16:47
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>
Comment thread core/src/signature.ts Outdated
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows signatures generated 5 seconds into the future, accounts for machines slightly out of sync

@thomas-waite

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread x402/src/hooks.ts
Comment thread x402/src/client.ts
Comment on lines +59 to +62
signatureHeaders = await createSignatureHeaders({
method: request.method,
url: request.url,
body,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread x402/src/hooks.ts Outdated
Comment thread x402/src/hooks.ts
Comment thread package.json
thomas-waite and others added 4 commits August 21, 2026 15:37
…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>
@thomas-waite thomas-waite changed the title feat!: sign requests with RFC 9421 HTTP Message Signatures Sign requests with RFC 9421 HTTP Message Signatures Aug 22, 2026
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.

2 participants