From 32d7778529b167e7d29cd7f46d554b897cc4e382 Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Fri, 24 Apr 2026 16:10:40 -0500 Subject: [PATCH 1/3] docs: add RFC 9421 request signing guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds docs/building/implementation/request-signing.mdx — practical step-by-step guide covering key generation, JWKS/brand.json publication, client-side signing, server-side verification, webhook signing, capability declaration, key rotation, and conformance testing. - Adds a Request Signing section to docs/building/build-an-agent.mdx showing requireSignatureWhenPresent composition and webhook signer config. - Adds request-signing to the Implementation Patterns nav in docs.json. - Adds a cross-link Note in the security.mdx quickstart section pointing to the new guide. Ported from adcontextprotocol/adcp-client#914. --- docs.json | 1 + docs/building/build-an-agent.mdx | 45 ++ .../implementation/request-signing.mdx | 395 ++++++++++++++++++ docs/building/implementation/security.mdx | 4 + 4 files changed, 445 insertions(+) create mode 100644 docs/building/implementation/request-signing.mdx diff --git a/docs.json b/docs.json index 429603e08a..e3077c8af2 100644 --- a/docs.json +++ b/docs.json @@ -148,6 +148,7 @@ "docs/building/implementation/mcp-response-extraction", "docs/building/implementation/a2a-response-extraction", "docs/building/implementation/security", + "docs/building/implementation/request-signing", "docs/building/implementation/webhook-verifier-tuning", "docs/building/implementation/seller-integration", "docs/building/implementation/storyboard-troubleshooting", diff --git a/docs/building/build-an-agent.mdx b/docs/building/build-an-agent.mdx index d7dd3a1f55..d8cadb0f6b 100644 --- a/docs/building/build-an-agent.mdx +++ b/docs/building/build-an-agent.mdx @@ -188,6 +188,51 @@ Each skill includes variant storyboards for different business models — non-gu See **[Validate Your Agent](/docs/building/validate-your-agent)** for the full testing workflow — debugging failing steps, running compliance checks, and validating interactively through Addie. +## Request signing + +If your seller agent receives signed requests from buyers, use `requireSignatureWhenPresent` to compose signature verification with your existing bearer or API key authentication. When signature headers are present, only signature auth runs; when absent, the fallback runs. This prevents bypass attacks where an invalid signature falls through to weaker auth. + +```typescript +import { createAdcpServer, serve } from '@adcp/client'; +import { verifySignatureAsAuthenticator, verifyApiKey, requireSignatureWhenPresent } from '@adcp/client/server'; +import { BrandJsonJwksResolver, InMemoryReplayStore, InMemoryRevocationStore } from '@adcp/client/signing/server'; + +const signatureAuth = verifySignatureAsAuthenticator({ + capability: { + supported: true, + required_for: ['create_media_buy', 'update_media_buy'], + covers_content_digest: 'either', + }, + jwks: new BrandJsonJwksResolver(), + replayStore: new InMemoryReplayStore(), + revocationStore: new InMemoryRevocationStore(), + resolveOperation: req => { + try { + const body = JSON.parse(req.rawBody ?? ''); + if (body.method === 'tools/call') return body.params?.name; + } catch {} + return undefined; + }, +}); + +serve(() => createAdcpServer({ name: 'My Seller', version: '1.0.0', mediaBuy: { /* ... */ } }), { + authenticate: requireSignatureWhenPresent(signatureAuth, verifyApiKey({ keys: { /* ... */ } })), +}); +``` + +For outbound webhook signing, pass a `signerKey` to `createAdcpServer`: + +```typescript +createAdcpServer({ + webhooks: { + signerKey: { keyid: 'my-webhook-key-2026', alg: 'ed25519', privateKey: webhookPrivateJwk }, + }, + // ... +}); +``` + +See **[Request Signing Guide](/docs/building/implementation/request-signing)** for the full walkthrough: key generation, JWKS publication, brand.json setup, buyer-side signing, and conformance testing. + ## Additional resources The JS/TS SDK includes documentation designed for both humans and coding agents: diff --git a/docs/building/implementation/request-signing.mdx b/docs/building/implementation/request-signing.mdx new file mode 100644 index 0000000000..93b4d400b0 --- /dev/null +++ b/docs/building/implementation/request-signing.mdx @@ -0,0 +1,395 @@ +--- +title: Request Signing Guide +description: "Step-by-step guide to RFC 9421 request signing in AdCP: key generation, JWKS publication, brand.json setup, client-side signing, server-side verification, webhook signing, key rotation, and conformance testing." +"og:title": "AdCP — Request Signing Guide" +--- + +AdCP 3.0 supports [HTTP Message Signatures (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421) for cryptographic request authentication. A buyer signs outbound requests so the seller can verify who sent them and that the payload wasn't tampered with. A seller signs outbound webhooks so the buyer can verify authenticity. + +Signing is **optional in AdCP 3.0** and becomes **mandatory in AdCP 4.0** for all spend-committing operations. Agents that don't sign yet must still tolerate signature headers (`Signature`, `Signature-Input`, `Content-Digest`) on inbound requests without breaking. + + +This is the practical implementation guide. For the normative specification — covered components, canonicalization rules, the full verifier checklist, replay dedup sizing, and the complete error taxonomy — see [Security: Signed Requests](/docs/building/implementation/security#signed-requests-transport-layer). + + +## When you need this + +| You are a... | You need to... | Why | +|---|---|---| +| **Buyer** (calls seller tools) | Sign outbound requests | Sellers may require proof the request came from you | +| **Buyer** (receives webhooks) | Verify inbound webhook signatures | Confirm the webhook came from the seller | +| **Seller** (receives tool calls) | Verify inbound request signatures | Confirm the buyer is who they claim to be | +| **Seller** (sends webhooks) | Sign outbound webhooks | Let buyers verify webhook authenticity | +| **Orchestrator** (proxies to sellers) | Sign outbound requests + verify inbound webhooks | You're the buyer from the seller's perspective | + +## Key concepts + +### Signature coverage + +The AdCP signing profile covers these request components: + +- `@method` — HTTP method +- `@target-uri` — full canonicalized request URL +- `@authority` — lowercased host header +- `content-type` — media type +- `content-digest` — SHA-256 or SHA-512 hash of the request body (see `covers_content_digest` capability) + +If any covered component changes after signing, verification fails. + +### Key separation + +Every agent needs **separate keys per purpose**, each with a distinct `kid` and `adcp_use` tag: + +- `adcp_use: "request-signing"` — for signing outbound tool calls +- `adcp_use: "webhook-signing"` — for signing outbound webhooks + +Reusing a key across purposes is forbidden by the spec. + +### Discovery chain + +Verifiers find your public key through a three-step chain: + +``` +Your domain (e.g., agent.example.com) + -> /.well-known/brand.json # brand manifest with agent declarations + -> agents[].jwks_uri # pointer to your key store + -> /.well-known/jwks.json # JSON Web Key Set with public keys +``` + +The `@adcp/client` SDK provides `BrandJsonJwksResolver` which handles this chain automatically with caching and refresh. + +## Step 1: Generate a signing key + +### CLI + +```bash +adcp signing generate-key --alg ed25519 --kid my-agent-2026 \ + --private-out ./private.jwk --public-out ./public-jwks.json +``` + +This generates an Ed25519 keypair and writes: +- `private.jwk` — the private key (JWK with `d` field). Keep this secret. +- `public-jwks.json` — the public key in JWKS format. Publish this. + +### Programmatic (TypeScript) + +```typescript +import { generateKeyPair, exportJWK } from 'jose'; + +const { publicKey, privateKey } = await generateKeyPair('EdDSA', { crv: 'Ed25519' }); +const publicJwk = await exportJWK(publicKey); +const privateJwk = await exportJWK(privateKey); + +const kid = 'my-agent-2026'; +publicJwk.kid = kid; +publicJwk.use = 'sig'; +publicJwk.key_ops = ['verify']; +publicJwk.adcp_use = 'request-signing'; +``` + +### Supported algorithms + +| Algorithm | `alg` value | Key type | Notes | +|---|---|---|---| +| Ed25519 | `ed25519` (RFC 9421) / `EdDSA` (JWK) | `OKP` / `Ed25519` | Preferred. Fast, small signatures. | +| ECDSA P-256 | `ecdsa-p256-sha256` (RFC 9421) / `ES256` (JWK) | `EC` / `P-256` | Edge-runtime friendly (Cloudflare Workers, Vercel Edge). | + + +The algorithm name differs between the JWK entry (`"alg": "EdDSA"`) and the RFC 9421 `Signature-Input` parameter (`alg="ed25519"`). See the [algorithm naming table](/docs/building/implementation/security#adcp-rfc-9421-profile) in the spec. + + +### Storing the private key + +- **Environment variable**: `ADCP_SIGNING_PRIVATE_KEY='{"kid":"...","kty":"OKP",...}'` +- **Secret manager** (GCP Secret Manager, AWS Secrets Manager, etc.): load at boot, keep in memory for the process lifetime +- **File**: for development only. Never commit to version control. + +## Step 2: Publish your public keys + +### JWKS endpoint + +Serve a JSON Web Key Set at a stable HTTPS URL (defaults to `/.well-known/jwks.json`): + +```json +{ + "keys": [ + { + "kid": "my-agent-2026", + "kty": "OKP", + "crv": "Ed25519", + "x": "", + "use": "sig", + "key_ops": ["verify"], + "adcp_use": "request-signing" + } + ] +} +``` + +Only public keys go here — no `d` field. Set `Cache-Control: max-age=3600` or similar. If you serve both request-signing and webhook-signing keys, include both in the same JWKS with different `kid` values and `adcp_use` tags. + +### brand.json + +Serve at `/.well-known/brand.json` on your brand domain. The `jwks_uri` is how verifiers find your keys: + +```json +{ + "name": "My Company", + "domain": "example.com", + "agents": [ + { + "url": "https://agent.example.com", + "jwks_uri": "https://agent.example.com/.well-known/jwks.json", + "capabilities": ["media-buy"], + "adcp_use": ["request-signing"] + } + ] +} +``` + +## Step 3: Sign outbound requests (buyer / orchestrator) + +### Wrapping fetch + +`createSigningFetch` wraps any `fetch`-compatible function to sign outbound requests automatically: + +```typescript +import { createSigningFetch } from '@adcp/client/signing'; + +const privateJwk = JSON.parse(process.env.ADCP_SIGNING_PRIVATE_KEY); + +const signingFetch = createSigningFetch(fetch, { + keyid: 'my-agent-2026', + alg: 'ed25519', + privateKey: privateJwk, +}); + +// Use signingFetch anywhere you'd use fetch. +// Signature, Signature-Input, and Content-Digest headers are added automatically. +await signingFetch('https://seller.example.com/mcp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), +}); +``` + +### Capability-aware signing + +`buildAgentSigningFetch` checks whether the target seller supports `signed-requests` and only signs when supported. This is the recommended approach for production: + +```typescript +import { buildAgentSigningFetch, CapabilityCache } from '@adcp/client/signing/client'; + +const capabilityCache = new CapabilityCache(); + +const signingFetch = buildAgentSigningFetch({ + upstream: fetch, + signing: { + kid: 'my-agent-2026', + alg: 'ed25519', + private_key: privateJwk, + agent_url: 'https://agent.example.com', + sign_supported: true, + }, + getCapability: () => capabilityCache.get('https://seller.example.com'), +}); +``` + +This avoids sending signatures to agents that don't expect them and caches capability lookups. + +## Step 4: Verify inbound signatures (seller) + +### Express middleware + +```typescript +import { + createExpressVerifier, + StaticJwksResolver, + InMemoryReplayStore, + InMemoryRevocationStore, +} from '@adcp/client/signing'; + +app.post( + '/mcp', + rawBodyMiddleware(), // req.rawBody must hold the byte-exact body + createExpressVerifier({ + capability: { + supported: true, + covers_content_digest: 'required', + required_for: ['create_media_buy', 'update_media_buy'], + }, + jwks: new StaticJwksResolver(buyerPublicKeys), + replayStore: new InMemoryReplayStore(), + revocationStore: new InMemoryRevocationStore(), + resolveOperation: req => req.body?.method ?? 'unknown', + }), + handler +); +// On verify: req.verifiedSigner = { keyid, agent_url?, verified_at }. +// On reject: 401 with WWW-Authenticate: Signature error="". +``` + +### Composing with bearer auth + +`requireSignatureWhenPresent` prevents bypass attacks where an invalid signature falls through to weaker authentication. When signature headers are present, only signature auth runs. When absent, the fallback (bearer/API key) runs as normal: + +```typescript +import { verifySignatureAsAuthenticator, verifyApiKey, requireSignatureWhenPresent } from '@adcp/client/server'; +import { BrandJsonJwksResolver, InMemoryReplayStore, InMemoryRevocationStore } from '@adcp/client/signing/server'; + +const signatureAuth = verifySignatureAsAuthenticator({ + capability: { + supported: true, + required_for: ['create_media_buy', 'update_media_buy'], + covers_content_digest: 'either', + }, + jwks: new BrandJsonJwksResolver(), + replayStore: new InMemoryReplayStore(), + revocationStore: new InMemoryRevocationStore(), + resolveOperation: req => { + try { + const body = JSON.parse(req.rawBody ?? ''); + if (body.method === 'tools/call') return body.params?.name; + } catch {} + return undefined; + }, +}); + +const bearerAuth = verifyApiKey({ keys: { 'sk_live_abc': { principal: 'acct_42' } } }); + +serve(createAgent, { + authenticate: requireSignatureWhenPresent(signatureAuth, bearerAuth), +}); +``` + +### JWKS resolver options + +| Resolver | Use case | +|---|---| +| `StaticJwksResolver` | Fixed set of known buyer keys. Good for dev/testing. | +| `HttpsJwksResolver` | Fetches JWKS from a URL with caching and refresh. | +| `BrandJsonJwksResolver` | Full discovery chain: brand.json → jwks_uri → JWKS. Recommended for production. | + +## Step 5: Verify inbound webhooks (buyer / orchestrator) + +When sellers send webhooks, verify the signature to confirm authenticity: + +```typescript +import { + verifyWebhookSignature, + BrandJsonJwksResolver, + InMemoryReplayStore, +} from '@adcp/client/signing/server'; + +const jwks = new BrandJsonJwksResolver(); +const replayStore = new InMemoryReplayStore(); + +app.post('/webhook', async (req, res) => { + try { + await verifyWebhookSignature(req, { jwks, replayStore }); + } catch { + return res.status(401).json({ error: 'invalid webhook signature' }); + } + + // Process the verified webhook... +}); +``` + +## Step 6: Sign outbound webhooks (seller) + +Pass a `signerKey` to `createAdcpServer` and the framework signs every outbound webhook automatically: + +```typescript +serve(() => createAdcpServer({ + name: 'My Seller', + version: '1.0.0', + webhooks: { + signerKey: { + keyid: 'my-seller-webhook-2026', + alg: 'ed25519', + privateKey: webhookPrivateJwk, + }, + }, + mediaBuy: { /* ... */ }, +})); +``` + +Publish a separate JWK with `"adcp_use": "webhook-signing"` in your JWKS alongside your request-signing key. + +## Step 7: Declare the capability + +If your seller verifies inbound signatures, declare `signed_requests` in your capabilities so buyers know to sign: + +```typescript +createAdcpServer({ + capabilities: { + overrides: { + signed_requests: { + supported: true, + required_for: ['create_media_buy', 'update_media_buy'], + supported_for: ['sync_creatives', 'sync_audiences'], + covers_content_digest: 'either', + }, + }, + }, + mediaBuy: { /* ... */ }, +}); +``` + +Buyers call `get_adcp_capabilities` and read `request_signing.required_for` and `supported_for` to know which operations you expect them to sign. + +## Key rotation + +The JWKS endpoint supports multiple keys simultaneously for zero-downtime rotation: + +1. Generate a new keypair with a new `kid` +2. Add the new public key to JWKS (both old and new are published) +3. Update signing configuration to use the new private key +4. After 24–48 hours, remove the old public key from JWKS + +For emergency rotation (key compromise), add the old `kid` to `revoked_kids` in your revocation list and rotate to a new key immediately. See [Revocation](/docs/building/implementation/security#revocation) for the revocation list format. + +## Testing + +### Conformance vectors + +The spec ships test vectors at `compliance/cache/3.0.0/test-vectors/request-signing/` (source at `static/compliance/source/test-vectors/request-signing/`): + +- **Positive vectors**: valid signed requests your verifier must accept (non-4xx) +- **Negative vectors**: invalid requests your verifier must reject with `401` and the correct error code + +```bash +# Debug a single vector +adcp signing verify-vector \ + --vector compliance/cache/3.0.0/test-vectors/request-signing/positive/001-basic-post.json +``` + +### Grade your verifier + +```bash +adcp grade request-signing https://agent.example.com/mcp --auth-token $TOKEN +``` + +### Error codes + +When verification fails, return `401` with `WWW-Authenticate: Signature error=""`: + +| Code | Meaning | +|---|---| +| `missing_signature` | Signature headers not present when required | +| `invalid_signature` | Signature doesn't verify against the public key | +| `expired_signature` | Signature timestamp too old | +| `replayed_nonce` | Nonce was already used | +| `revoked_key` | Key has been revoked | +| `unknown_key` | Key ID not found in JWKS | +| `unsupported_algorithm` | Algorithm not in allowlist | + +For the full error code taxonomy, see [Transport error taxonomy](/docs/building/implementation/security#transport-error-taxonomy). + +## Related + +- [Security: Signed Requests](/docs/building/implementation/security#signed-requests-transport-layer) — normative spec with verifier checklist, canonicalization rules, and replay dedup sizing +- [Push Notifications](/docs/building/implementation/webhooks) — webhook setup including signature verification +- [Validate Your Agent](/docs/building/validate-your-agent) — full compliance validation including signing conformance +- [Build an Agent](/docs/building/build-an-agent) — SDK setup and storyboard validation +- [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) — HTTP Message Signatures specification diff --git a/docs/building/implementation/security.mdx b/docs/building/implementation/security.mdx index 18f28f3c03..b743aa7605 100644 --- a/docs/building/implementation/security.mdx +++ b/docs/building/implementation/security.mdx @@ -793,6 +793,10 @@ Read calls remain bearer-authenticated. Signing read traffic adds verification c #### Quickstart: opt into request signing in 3.0 + +Looking for step-by-step setup with code examples? See the **[Request Signing Guide](/docs/building/implementation/request-signing)** — key generation, JWKS publication, brand.json, SDK signing/verification patterns, and conformance testing. This section is the normative specification that guide implements. + + For implementers who want to pilot signing in 3.0 before the 4.0 flip: **As an agent that signs requests:** From 9676f5b76b5d6fce0bc375547c1eeda46fc4c444 Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Fri, 24 Apr 2026 16:20:22 -0500 Subject: [PATCH 2/3] docs: apply signing guide feedback from bokelley MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace requireSignatureWhenPresent manual composition with requireAuthenticatedOrSigned in Step 4 and build-an-agent.mdx — the higher-level helper also enforces request_signature_required for the unsigned-no-credentials path - Use mcpToolNameResolver instead of inline JSON-RPC resolver boilerplate throughout - Fix resolveOperation in the Express middleware example (was incorrectly using req.body.method) - Add conformance vector counts: 39 total (12 positive, 27 negative) Addresses feedback from adcontextprotocol/adcp-client#914 comment by bokelley; tracks mcpToolNameResolver landing in adcp-client#916. --- docs/building/build-an-agent.mdx | 41 +++++++------ .../implementation/request-signing.mdx | 59 ++++++++++--------- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/docs/building/build-an-agent.mdx b/docs/building/build-an-agent.mdx index d8cadb0f6b..4a939fe237 100644 --- a/docs/building/build-an-agent.mdx +++ b/docs/building/build-an-agent.mdx @@ -190,33 +190,32 @@ See **[Validate Your Agent](/docs/building/validate-your-agent)** for the full t ## Request signing -If your seller agent receives signed requests from buyers, use `requireSignatureWhenPresent` to compose signature verification with your existing bearer or API key authentication. When signature headers are present, only signature auth runs; when absent, the fallback runs. This prevents bypass attacks where an invalid signature falls through to weaker auth. +If your seller agent receives signed requests from buyers, use `requireAuthenticatedOrSigned` to compose signature verification with existing bearer or API key auth. It handles the full matrix: signature-only when headers are present, fallback-only when absent, and `request_signature_required` 401 for unsigned requests on operations listed in `requiredFor`. ```typescript import { createAdcpServer, serve } from '@adcp/client'; -import { verifySignatureAsAuthenticator, verifyApiKey, requireSignatureWhenPresent } from '@adcp/client/server'; +import { + verifyApiKey, + verifySignatureAsAuthenticator, + requireAuthenticatedOrSigned, + mcpToolNameResolver, + MUTATING_TASKS, +} from '@adcp/client/server'; import { BrandJsonJwksResolver, InMemoryReplayStore, InMemoryRevocationStore } from '@adcp/client/signing/server'; -const signatureAuth = verifySignatureAsAuthenticator({ - capability: { - supported: true, - required_for: ['create_media_buy', 'update_media_buy'], - covers_content_digest: 'either', - }, - jwks: new BrandJsonJwksResolver(), - replayStore: new InMemoryReplayStore(), - revocationStore: new InMemoryRevocationStore(), - resolveOperation: req => { - try { - const body = JSON.parse(req.rawBody ?? ''); - if (body.method === 'tools/call') return body.params?.name; - } catch {} - return undefined; - }, -}); - serve(() => createAdcpServer({ name: 'My Seller', version: '1.0.0', mediaBuy: { /* ... */ } }), { - authenticate: requireSignatureWhenPresent(signatureAuth, verifyApiKey({ keys: { /* ... */ } })), + authenticate: requireAuthenticatedOrSigned({ + signature: verifySignatureAsAuthenticator({ + capability: { supported: true, required_for: ['create_media_buy'], covers_content_digest: 'either' }, + jwks: new BrandJsonJwksResolver(), + replayStore: new InMemoryReplayStore(), + revocationStore: new InMemoryRevocationStore(), + resolveOperation: mcpToolNameResolver, + }), + fallback: verifyApiKey({ keys: { /* ... */ } }), + requiredFor: [...MUTATING_TASKS], + resolveOperation: mcpToolNameResolver, + }), }); ``` diff --git a/docs/building/implementation/request-signing.mdx b/docs/building/implementation/request-signing.mdx index 93b4d400b0..ed3675c572 100644 --- a/docs/building/implementation/request-signing.mdx +++ b/docs/building/implementation/request-signing.mdx @@ -201,6 +201,8 @@ This avoids sending signatures to agents that don't expect them and caches capab ### Express middleware +For raw Express routes, mount `createExpressVerifier` after a raw-body middleware. Use `mcpToolNameResolver` for the `resolveOperation` callback — it parses the JSON-RPC envelope and returns the MCP tool name: + ```typescript import { createExpressVerifier, @@ -208,6 +210,7 @@ import { InMemoryReplayStore, InMemoryRevocationStore, } from '@adcp/client/signing'; +import { mcpToolNameResolver } from '@adcp/client/server'; app.post( '/mcp', @@ -221,7 +224,7 @@ app.post( jwks: new StaticJwksResolver(buyerPublicKeys), replayStore: new InMemoryReplayStore(), revocationStore: new InMemoryRevocationStore(), - resolveOperation: req => req.body?.method ?? 'unknown', + resolveOperation: mcpToolNameResolver, }), handler ); @@ -229,39 +232,39 @@ app.post( // On reject: 401 with WWW-Authenticate: Signature error="". ``` -### Composing with bearer auth +### Composing signature + bearer auth with `requireAuthenticatedOrSigned` -`requireSignatureWhenPresent` prevents bypass attacks where an invalid signature falls through to weaker authentication. When signature headers are present, only signature auth runs. When absent, the fallback (bearer/API key) runs as normal: +`requireAuthenticatedOrSigned` bundles the full composition: presence-gated routing (signature auth when headers present, fallback otherwise) plus `requiredFor` enforcement — unauthenticated requests for signing-required operations get `401 request_signature_required` even when no credentials at all are supplied. ```typescript -import { verifySignatureAsAuthenticator, verifyApiKey, requireSignatureWhenPresent } from '@adcp/client/server'; +import { + serve, + verifyApiKey, + verifySignatureAsAuthenticator, + requireAuthenticatedOrSigned, + mcpToolNameResolver, + MUTATING_TASKS, +} from '@adcp/client/server'; import { BrandJsonJwksResolver, InMemoryReplayStore, InMemoryRevocationStore } from '@adcp/client/signing/server'; -const signatureAuth = verifySignatureAsAuthenticator({ - capability: { - supported: true, - required_for: ['create_media_buy', 'update_media_buy'], - covers_content_digest: 'either', - }, - jwks: new BrandJsonJwksResolver(), - replayStore: new InMemoryReplayStore(), - revocationStore: new InMemoryRevocationStore(), - resolveOperation: req => { - try { - const body = JSON.parse(req.rawBody ?? ''); - if (body.method === 'tools/call') return body.params?.name; - } catch {} - return undefined; - }, -}); - -const bearerAuth = verifyApiKey({ keys: { 'sk_live_abc': { principal: 'acct_42' } } }); - serve(createAgent, { - authenticate: requireSignatureWhenPresent(signatureAuth, bearerAuth), + authenticate: requireAuthenticatedOrSigned({ + signature: verifySignatureAsAuthenticator({ + capability: { supported: true, required_for: ['create_media_buy'], covers_content_digest: 'either' }, + jwks: new BrandJsonJwksResolver(), + replayStore: new InMemoryReplayStore(), + revocationStore: new InMemoryRevocationStore(), + resolveOperation: mcpToolNameResolver, + }), + fallback: verifyApiKey({ keys: { 'sk_live_abc': { principal: 'acct_42' } } }), + requiredFor: [...MUTATING_TASKS], + resolveOperation: mcpToolNameResolver, + }), }); ``` +`MUTATING_TASKS` is the full list of spend-committing and state-changing operations exported from `@adcp/client/server` — use it rather than maintaining your own list. + ### JWKS resolver options | Resolver | Use case | @@ -353,10 +356,10 @@ For emergency rotation (key compromise), add the old `kid` to `revoked_kids` in ### Conformance vectors -The spec ships test vectors at `compliance/cache/3.0.0/test-vectors/request-signing/` (source at `static/compliance/source/test-vectors/request-signing/`): +The spec ships **39 test vectors** at `compliance/cache/3.0.0/test-vectors/request-signing/` (source at `static/compliance/source/test-vectors/request-signing/`): -- **Positive vectors**: valid signed requests your verifier must accept (non-4xx) -- **Negative vectors**: invalid requests your verifier must reject with `401` and the correct error code +- **12 positive vectors**: valid signed requests your verifier must accept (non-4xx) +- **27 negative vectors**: invalid requests your verifier must reject with `401` and the correct error code ```bash # Debug a single vector From c403fcfe5b87052b4ee72c803888543f297a7e0c Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Fri, 24 Apr 2026 16:22:52 -0500 Subject: [PATCH 3/3] chore: add empty changeset for docs-only signing guide --- .changeset/docs-rfc9421-signing-guide.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/docs-rfc9421-signing-guide.md diff --git a/.changeset/docs-rfc9421-signing-guide.md b/.changeset/docs-rfc9421-signing-guide.md new file mode 100644 index 0000000000..0314cc09ed --- /dev/null +++ b/.changeset/docs-rfc9421-signing-guide.md @@ -0,0 +1,4 @@ +--- +--- + +Add RFC 9421 request signing guide: new `docs/building/implementation/request-signing.mdx` covering key generation, JWKS/brand.json publication, buyer-side signing, seller-side verification with `requireAuthenticatedOrSigned` + `mcpToolNameResolver`, webhook signing, key rotation, and conformance testing (39 vectors: 12 positive, 27 negative). Adds a Request Signing section to `build-an-agent.mdx` and cross-links from the `security.mdx` quickstart. Ported from adcontextprotocol/adcp-client#914.