-
Notifications
You must be signed in to change notification settings - Fork 3
Sign requests with RFC 9421 HTTP Message Signatures #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
6ae1e41
chore: fix test runs on fresh checkouts
thomas-waite a23cb3f
feat(core): replace bare body signature with RFC 9421 message signatures
thomas-waite 6e8f677
feat(x402): adopt RFC 9421 signature headers and restore nonce replay…
thomas-waite f8c0c72
feat(cli): prove signs method, URL, and body via RFC 9421 headers
thomas-waite dc110d9
docs: describe the RFC 9421 signing flow across docs and skills
thomas-waite d1a2a2a
feat!: drop the nonce from the signature profile
thomas-waite e653d12
fix(x402): match pending discounts case-insensitively and checksum su…
thomas-waite f4e05dd
fix(core): reject non-token HTTP methods in the signature base
thomas-waite 7ed6c07
fix(x402): skip body normalization for bodyless requests
thomas-waite 78bce2b
fix(core): tolerate 30 seconds of client clock skew
thomas-waite 6e67da6
Merge branch 'new-cli' into rfc9421-signatures
thomas-waite 9de434d
fix(cli): normalize the prove method input to uppercase
thomas-waite 3c7ab59
docs(skills): cite RFC 9421 and RFC 9530 for the signature headers
thomas-waite File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@worldcoin/agentkit-core': minor | ||
| '@worldcoin/agentkit': minor | ||
| '@worldcoin/agentkit-cli': minor | ||
| --- | ||
|
|
||
| Replace the bare EIP-191 body signature with RFC 9421 HTTP Message Signatures. Requests are now signed under a closed profile covering `@method`, `@authority`, `@path`, `@query`, and `content-digest` (RFC 9530), with `created`/`expires`/`keyid` parameters, transported in the standard `Signature-Input`, `Signature`, and `Content-Digest` headers instead of `X-AgentKit`. The CLI's `prove` command now takes `<method> <url> [body]` and returns the three header values, and `verify` enforces the five-minute validity window and keyid binding. Nonce-based single-use signatures are a planned follow-up. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,34 @@ | ||
| import { z } from 'incur' | ||
| import { createSignatureHeaders, type AgentkitSignatureHeaders } from '@worldcoin/agentkit-core' | ||
| import type { AgentSigner } from './key.js' | ||
|
|
||
| export const requestBodyInputSchema = z.string().describe('Exact UTF-8 request body to sign') | ||
| export const methodInputSchema = z | ||
| .string() | ||
| .regex(/^[A-Za-z]+$/, 'Invalid HTTP method') | ||
| .toUpperCase() | ||
| .describe('HTTP method of the request, e.g. GET or POST') | ||
|
|
||
| export type MessageSigner = { | ||
| signMessage: (message: string) => Promise<`0x${string}`> | ||
| } | ||
| export const urlInputSchema = z | ||
| .string() | ||
| .regex(/^https?:\/\/\S+$/, 'Invalid request URL') | ||
| .describe('Full request URL, including any query string') | ||
|
|
||
| export const bodyInputSchema = z | ||
| .string() | ||
| .default('') | ||
| .describe('Exact UTF-8 request body; omit for bodyless requests') | ||
|
|
||
| export function signRequestBody(body: string, signer: MessageSigner): Promise<`0x${string}`> { | ||
| return signer.signMessage(body) | ||
| export function createProofHeaders(input: { | ||
| method: string | ||
| url: string | ||
| body: string | ||
| signer: AgentSigner | ||
| }): Promise<AgentkitSignatureHeaders> { | ||
| return createSignatureHeaders({ | ||
| method: input.method, | ||
| url: input.url, | ||
| body: input.body, | ||
| address: input.signer.address, | ||
| signMessage: message => input.signer.signMessage(message), | ||
| }) | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,82 @@ | ||
| import { verifyMessage } from 'viem' | ||
| import { describe, expect, test } from 'bun:test' | ||
| import { signRequestBody } from '../src/prove.js' | ||
| import { describe, expect, it } from 'bun:test' | ||
| import { verifyRequest } from '@worldcoin/agentkit-core' | ||
| import { privateKeyToAccount } from 'viem/accounts' | ||
| import { createProofHeaders, methodInputSchema } from '../src/prove.js' | ||
| import type { AgentSigner } from '../src/key.js' | ||
|
|
||
| describe('request body proof', () => { | ||
| test('returns a raw EIP-191 signature over the exact body', async () => { | ||
| const account = privateKeyToAccount(`0x${'01'.padStart(64, '0')}`) | ||
| const body = '{"hello":"world","unicode":"你好"}' | ||
| const signature = await signRequestBody(body, { | ||
| signMessage: message => account.signMessage({ message }), | ||
| function createSigner(privateKey: `0x${string}`): AgentSigner & { account: ReturnType<typeof privateKeyToAccount> } { | ||
| const account = privateKeyToAccount(privateKey) | ||
| return { | ||
| account, | ||
| address: account.address, | ||
| signMessage: message => account.signMessage({ message }), | ||
| } | ||
| } | ||
|
|
||
| function registeredLookup(signer: { address: string }) { | ||
| return async (address: string) => (address === signer.address ? '0x1234' : null) | ||
| } | ||
|
|
||
| describe('methodInputSchema', () => { | ||
| it('normalizes the method to uppercase', () => { | ||
| expect(methodInputSchema.parse('post')).toBe('POST') | ||
| expect(methodInputSchema.parse('GET')).toBe('GET') | ||
| }) | ||
|
|
||
| it('rejects non-token methods', () => { | ||
| expect(() => methodInputSchema.parse('P0ST')).toThrow() | ||
| expect(() => methodInputSchema.parse('GET /')).toThrow() | ||
| }) | ||
| }) | ||
|
|
||
| describe('createProofHeaders', () => { | ||
| it('produces headers that pass core verification for the same request', async () => { | ||
| const signer = createSigner(`0x${'01'.padStart(64, '0')}`) | ||
| const body = '{"a":1}' | ||
| const headers = await createProofHeaders({ | ||
| method: 'post', | ||
| url: 'https://api.example.com/data?x=1', | ||
| body, | ||
| signer, | ||
| }) | ||
|
|
||
| expect(signature).toMatch(/^0x[0-9a-f]{130}$/) | ||
| expect(await verifyMessage({ address: account.address, message: body, signature })).toBe(true) | ||
| const request = new Request('https://api.example.com/data?x=1', { method: 'POST', headers, body }) | ||
| const result = await verifyRequest(request, { lookupId: registeredLookup(signer) }) | ||
|
|
||
| expect(result.lookupId).toBe('0x1234') | ||
| expect(result.address).toBe(signer.address) | ||
| }) | ||
|
|
||
| it('signs bodyless GET requests with an empty-body digest', async () => { | ||
| const signer = createSigner(`0x${'02'.padStart(64, '0')}`) | ||
| const headers = await createProofHeaders({ | ||
| method: 'GET', | ||
| url: 'https://api.example.com/data', | ||
| body: '', | ||
| signer, | ||
| }) | ||
|
|
||
| expect(headers['Content-Digest']).toBe('sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:') | ||
|
|
||
| const request = new Request('https://api.example.com/data', { method: 'GET', headers }) | ||
| const result = await verifyRequest(request, { lookupId: registeredLookup(signer) }) | ||
|
|
||
| expect(result.lookupId).toBe('0x1234') | ||
| }) | ||
|
|
||
| test('supports the empty body used by GET requests', async () => { | ||
| const account = privateKeyToAccount(`0x${'02'.padStart(64, '0')}`) | ||
| const signature = await signRequestBody('', { | ||
| signMessage: message => account.signMessage({ message }), | ||
| it('rejects headers replayed against a different URL', async () => { | ||
| const signer = createSigner(`0x${'03'.padStart(64, '0')}`) | ||
| const body = '{"a":1}' | ||
| const headers = await createProofHeaders({ | ||
| method: 'POST', | ||
| url: 'https://api.example.com/data', | ||
| body, | ||
| signer, | ||
| }) | ||
|
|
||
| expect(await verifyMessage({ address: account.address, message: '', signature })).toBe(true) | ||
| const request = new Request('https://api.example.com/other', { method: 'POST', headers, body }) | ||
| await expect(verifyRequest(request, { lookupId: registeredLookup(signer) })).rejects.toThrow( | ||
| 'Signature does not match the keyid address' | ||
| ) | ||
| }) | ||
| }) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,4 @@ | ||
| export { verify } from './verify' | ||
| export { verify, verifyRequest } from './verify' | ||
| export type { VerifiedAgentRequest } from './verify' | ||
| export { createSignatureHeaders } from './signature' | ||
| export type { AgentkitSignatureHeaders, CreateSignatureHeadersInput } from './signature' |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this have case normalization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's already normalised one layer down
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will move it up to here to make clearer