Skip to content

docs: add RFC 9421 request signing guide#914

Merged
bokelley merged 4 commits intomainfrom
feature/adcp-signing-keys
Apr 25, 2026
Merged

docs: add RFC 9421 request signing guide#914
bokelley merged 4 commits intomainfrom
feature/adcp-signing-keys

Conversation

@benminer
Copy link
Copy Markdown
Collaborator

Summary

  • Adds docs/guides/SIGNING-GUIDE.md — a step-by-step guide covering the full RFC 9421 signing workflow: key generation, JWKS/brand.json publication, client-side signing, server-side verification, webhook signing, capability declaration, key rotation, and conformance testing.
  • Updates README.md to link the guide from the "For AI Agents" section and rewrites the Security > Request Signing section with better conceptual framing before code examples.
  • Adds a Request Signing section to BUILD-AN-AGENT.md showing verifySignatureAsAuthenticator + requireSignatureWhenPresent composition and webhook signer config.

Test plan

  • Verify all relative links resolve correctly on GitHub
  • Confirm code examples match current @adcp/client API surface (verified against src/lib/signing/ and src/lib/server/auth-signature.ts)

@benminer benminer requested a review from bokelley as a code owner April 24, 2026 19:58
@bokelley
Copy link
Copy Markdown
Contributor

Nice guide — picking this up after a walk-through with the existing signing helpers. Two ergonomic improvements would cut Step 4 almost in half without changing the conceptual framing.

1. requireAuthenticatedOrSigned already bundles the composition

The SDK ships requireAuthenticatedOrSigned which wraps signature + fallback + requiredFor + resolveOperation into one call. The three-variable manual composition in Step 4 (signatureAuth, bearerAuth, requireSignatureWhenPresent(...)) collapses to:

import {
  serve,
  verifyApiKey,
  anyOf,
  verifySignatureAsAuthenticator,
  requireAuthenticatedOrSigned,
  MUTATING_TASKS,
} from '@adcp/client/server';

serve(createAgent, {
  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,
  }),
});

The docstring at src/lib/server/auth-signature.ts:414 spells out the full behavior matrix — same presence-gated semantics you describe, plus requiredFor enforcement for the unsigned-no-credentials path (the spec's request_signature_required 401).

2. mcpToolNameResolver removes the resolveOperation boilerplate

The guide (and BUILD-AN-AGENT.md snippet) hand-writes the same JSON-RPC parser three times:

resolveOperation: req => {
  try {
    const body = JSON.parse(req.rawBody ?? '');
    if (body.method === 'tools/call') return body.params?.name;
  } catch {}
  return undefined;
}

I opened #916 to export this as mcpToolNameResolver from @adcp/client/server, so once it lands adapter authors write resolveOperation: mcpToolNameResolver everywhere. The guide above already assumes that — feel free to wait for #916 to merge before the rewrite, or merge as-is and I'll send a follow-up cleanup.

Minor polish (orthogonal to the above)

  • Step 4 of the new guide has verifySignatureAsAuthenticator imported twice on adjacent lines — merge into one import block.
  • README rewrite drops the conformance-vector count; the guide correctly cites 39 (12 positive + 27 negative = confirmed against compliance/cache/3.0.0/test-vectors/request-signing/).

Happy to push the rewrite into this branch after #916 merges if that's easier than a follow-up PR — just say the word.

benminer added a commit to adcontextprotocol/adcp that referenced this pull request Apr 24, 2026
- 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.
@bokelley
Copy link
Copy Markdown
Contributor

Quick update — the SDK ergonomics that motivated some of the early review comments here have shipped on main:

The canonical home for the docs is now adcontextprotocol/adcp#3064 — once that merges and a release of @adcp/client carrying #917 ships, the examples there can drop the explicit new InMemoryReplayStore() / new InMemoryRevocationStore() lines and the verbose buildAgentSigningFetch + CapabilityCache + getCapability setup. I've left a parallel comment on #3064 with the specific spots to tighten.

Closing this one out makes sense at this point unless you want to keep it open for cross-reference.

benminer and others added 3 commits April 24, 2026 19:46
…-AGENT

Adds docs/guides/SIGNING-GUIDE.md covering the full RFC 9421 signing
workflow: key generation, JWKS/brand.json publication, client-side
signing, server-side verification, webhook signing, capability
declaration, key rotation, and conformance testing.

Updates README.md to link the guide from the AI Agents section and
rewrites the Security > Request Signing section with better orientation.

Adds a Request Signing section to BUILD-AN-AGENT.md showing
requireSignatureWhenPresent composition and webhook signer config.
…edOrSigned

Apply the review feedback on #914 against the SDK ergonomics
shipped in #916 (mcpToolNameResolver) and #917 (defaults +
createAgentSignedFetch preset):

SIGNING-GUIDE.md
- Step 3 (buyer-side): lead with createAgentSignedFetch one-call
  preset for the single-seller case; keep buildAgentSigningFetch
  as the multi-seller fallback. Drops the verbose CapabilityCache
  wiring from the headline example.
- Step 4 (seller-side, Express middleware): drop explicit
  InMemoryReplayStore / InMemoryRevocationStore (now default
  per #917). Use mcpToolNameResolver instead of the inlined
  JSON-RPC parser. Add a one-liner about swapping in shared
  stores for horizontally scaled fleets.
- Step 4 (composing with bearer auth): replace the three-variable
  manual composition (signatureAuth / bearerAuth /
  requireSignatureWhenPresent) with requireAuthenticatedOrSigned
  in one call. Now also enforces the spec's
  request_signature_required 401 via requiredFor: [...MUTATING_TASKS]
  — previously absent from the example.

BUILD-AN-AGENT.md
- Mirror the requireAuthenticatedOrSigned + mcpToolNameResolver +
  MUTATING_TASKS pattern in the Request Signing snippet so the
  two guides agree.

Functional surface unchanged; the manual composition still works
for callers that need finer control. The recommended path is just
shorter.
@bokelley bokelley force-pushed the feature/adcp-signing-keys branch from cf640a7 to e7e1d25 Compare April 24, 2026 23:49
Six must-fix items raised by code-reviewer + ad-tech-protocol-expert
on the post-polish state:

1. MUTATING_TASKS imports were wrong — only @adcp/client (root)
   exports it, not @adcp/client/server. Buyer copies would have
   compile-errored. Split the imports.

2. Capability override key is `request_signing`, not
   `signed_requests`. Wrong key gets silently dropped — verifier
   wires up but get_adcp_capabilities advertises nothing, so
   buyers don't sign. Fixed in both guides; add a clarifying
   sentence so the trap is visible.

3. Widen `mcpToolNameResolver` parameter type from
   `IncomingMessage & { rawBody?: string }` to `{ rawBody?: string }`.
   Function only reads rawBody, so widening lets it satisfy both
   the `verifySignatureAsAuthenticator` (IncomingMessage-shaped)
   and `createExpressVerifier` (ExpressLike-shaped) call sites
   without casts. No runtime change.

4. Error code table was wrong — `missing_signature` /
   `invalid_signature` etc. don't exist. The real codes
   (cross-checked against compliance/cache/3.0.0/test-vectors/
   request-signing/negative/) are `request_signature_*`. Replace
   the seven-row table with the full 15-row table from the vectors
   and note that they're a separate signature-error namespace
   surfaced via WWW-Authenticate (not entries in
   enums/error-code.json).

5. Signature coverage misstated. content-digest is conditional
   on covers_content_digest, not always covered. Reword.

6. brand.json shape was wrong. agents[] requires {type, url, id};
   `capabilities` and `adcp_use` arrays don't exist in
   schemas/cache/3.0.0/brand.json. Fix the example, list the
   `type` enum.

Also reframed the "mandatory in 3.1+" claim to "4.0,
spend-committing operations" per
schemas/cache/3.0.0/protocol/get-adcp-capabilities-response.json,
and softened `requiredFor: [...MUTATING_TASKS]` to a narrow
example with MUTATING_TASKS as the upper bound (the spec stance
in 3.0 is "empty by default; populate selectively per
counterparty").

Conformance vector counts (12 positive + 27 negative = 39)
verified against the disk layout.
@bokelley
Copy link
Copy Markdown
Contributor

Cross-SDK update on the ergonomics work this PR's review surfaced:

  • TS (`@adcp/client`): #917 merged.
  • Python (`adcp`): adcp-client-python#272 open — default `replay_store` on `VerifyOptions`, new `install_signing_event_hook` + `signing_operation` preset, full migration guide.
  • Go (`adcp-go`): adcp-go#88 open — default `Replay` store, new `NewSignedHTTPClient` preset with capability-aware signing.

All three carry the same security-by-default replay-store default and the same single-seller buyer preset shape (idiomatic to each language). Canonical docs PR is adcp#3064 — I left a cross-SDK status comment there.

@bokelley bokelley merged commit 6ad6450 into main Apr 25, 2026
9 checks passed
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.

3 participants