Skip to content

docs: add RFC 9421 request signing guide#3064

Merged
bokelley merged 1 commit into
mainfrom
docs/rfc-9421-signing-guide
May 9, 2026
Merged

docs: add RFC 9421 request signing guide#3064
bokelley merged 1 commit into
mainfrom
docs/rfc-9421-signing-guide

Conversation

@benminer
Copy link
Copy Markdown
Collaborator

Summary

Ported documentation from adcontextprotocol/adcp-client#914, which was accidentally opened against the wrong repo.

  • Adds docs/building/implementation/request-signing.mdx — practical step-by-step guide covering key generation, JWKS/brand.json publication, buyer-side signing, seller-side verification, webhook signing, capability declaration, key rotation, and conformance testing
  • Adds a Request Signing section to docs/building/build-an-agent.mdx showing the requireSignatureWhenPresent + verifySignatureAsAuthenticator composition pattern 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 (framing the spec as the normative source the guide implements)

Notes

The normative RFC 9421 spec already lives in docs/building/implementation/security.mdx. This guide is the approachable complement — what you actually run and configure — pointing back to the spec for the verifier checklist, canonicalization rules, and error taxonomy.

@bokelley
Copy link
Copy Markdown
Contributor

Heads-up: two SDK ergonomics PRs landed on adcp-client main that this guide can take advantage of once a new @adcp/client release ships them:

  • adcp-client#916 (merged) — mcpToolNameResolver exported from @adcp/client/server. Already adopted in this guide.
  • adcp-client#917 (merged) — defaults replayStore / revocationStore on both verifySignatureAsAuthenticator AND createExpressVerifier; defaults upstream to globalThis.fetch on buildAgentSigningFetch; new createAgentSignedFetch({ signing, sellerAgentUri }) preset bundling buildAgentSigningFetch + CapabilityCache for single-seller buyers.

Specific tightening once the release ships

Step 4 — Express middleware (createExpressVerifier): drop the two store lines.

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: mcpToolNameResolver,
})

Step 4 — requireAuthenticatedOrSigned: same drop on the verifySignatureAsAuthenticator block. Imports for InMemoryReplayStore / InMemoryRevocationStore from @adcp/client/signing/server can come out too.

Step 3 — Capability-aware signing: the buildAgentSigningFetch + CapabilityCache + getCapability example collapses to:

import { createAgentSignedFetch } from '@adcp/client/signing';

export const signedFetch = createAgentSignedFetch({
  signing: {
    kid: 'my-agent-2026',
    alg: 'ed25519',
    private_key: privateJwk,
    agent_url: 'https://agent.example.com',
  },
  sellerAgentUri: 'https://seller.example.com',
});

Worth keeping the longer buildAgentSigningFetch form too, as a section labeled "multi-seller" — it's still the right shape when one buyer talks to N sellers. The preset is explicitly the single-seller shortcut.

No need to wait — happy for this to merge as-is and follow up with a tightening PR after the next @adcp/client release. Or I can push the tightening directly into this branch if you'd prefer one-and-done. Whichever's easier.

@bokelley
Copy link
Copy Markdown
Contributor

we should also have examples for python and go

@bokelley
Copy link
Copy Markdown
Contributor

SDK ergonomics PRs are now out across all three official SDKs — happy to update this guide once they ship in a release. Posting the cross-SDK status so the docs can cite them in lockstep.

SDK PR Status
TypeScript (`@adcp/client`) adcp-client#917 ✅ merged
Python (`adcp`) adcp-client-python#272 open
Go (`adcp-go`) adcp-go#88 open

All three land the same security-by-default story:

  • Replay store defaults to in-memory when omitted on the seller-side verifier surface. Previously `nil`/`None`/`undefined` silently disabled replay protection — the regression AdCP verifier checklist step 12/13 (and conformance vector `016-replayed-nonce`) exists to prevent.
  • Single-seller buyer preset. Each SDK exposes its idiomatic equivalent: TS `createAgentSignedFetch({ signing, sellerAgentUri })` returning a `FetchLike`, Python `install_signing_event_hook(client, signing=, capability_provider=)` plus `signing_operation()` context manager, Go `NewSignedHTTPClient(SignedHTTPClientOptions{..., CapabilityProvider})` returning an `*http.Client` with redirect-following disabled.
  • Capability-aware signing. Buyer presets can take a `capability_provider` / `getCapability` / `CapabilityProvider` callable that returns the seller's `request_signing` capability per request — only signs operations the seller listed in `required_for` / `warn_for` / `supported_for`, and honors `covers_content_digest` per-call.

Once Python and Go merge and release-please cuts new versions, the docs here can:

  1. Drop the explicit `new InMemoryReplayStore()` / `new InMemoryRevocationStore()` lines from Step 4's `createExpressVerifier` and `requireAuthenticatedOrSigned` examples (TS).
  2. Collapse the `buildAgentSigningFetch` + `CapabilityCache` + `getCapability` setup into a single `createAgentSignedFetch` call (TS).
  3. Optionally add a "Python" tab under Step 3/4/5/6 showing `install_signing_event_hook` + `signing_operation` and `verify_starlette_request` with default stores. Python migration walkthrough is at docs/request-signing-migration.md (mirrors adcp-go's existing `MIGRATION.md` shape).
  4. Optionally add a "Go" tab showing `NewSignedHTTPClient` + capability-aware signing.

Happy to send a follow-up tightening PR after the Python/Go releases ship, or to push the rewrite directly into this branch — your call. Either way no need to block this PR on it.

cc @benminer

@bokelley
Copy link
Copy Markdown
Contributor

Heads up — three SDK PRs landed (or are in flight) on @adcp/client 5.20.0 that touch sections of this guide. Worth either pulling the additions into this PR or filing a follow-up. Each is small (~10 lines of guide each), and each fills a real gap a reader landing on this page in 5.20.0+ will hit.

1. Step 1.3 "Storing the private key" — add KMS option.

adcp-client#1017 (merged) ships SigningProvider, an async signer interface so private keys can live in GCP KMS / AWS KMS / Azure Key Vault / Vault Transit instead of process memory. The current advice ("load at boot, keep in memory for the process lifetime") is now the non-recommended path for production. Suggested addition right under the existing storage list:

Production: KMS-backed signers. @adcp/client 5.20.0+ supports a pluggable SigningProvider so the private key never leaves your managed key store. See the SDK guide § Production Key Storage for the GCP KMS reference adapter and the IAM / JWKS-publication walkthrough.

2. Step 3 "Sign outbound requests" — show the kind: 'provider' shape.

AgentRequestSigningConfig is now a discriminated union on kind. Existing private_key literals still work (kind defaults to 'inline'), but production callers should show:

const provider = await createGcpKmsSigningProvider({
  versionName: process.env.ADCP_KMS_VERSION!,
  kid: 'my-agent-2026',
  algorithm: 'ed25519',
  client: kmsClient,
});

const signingFetch = buildAgentSigningFetch({
  upstream: fetch,
  signing: { kind: 'provider', provider, agent_url: 'https://agent.example.com' },
  getCapability: () => capabilityCache.get('https://seller.example.com'),
});

Wire format unchanged — counterparties can't tell the difference between in-process and KMS-backed signing.

3. Step 4 "Verify inbound signatures" — call out multi-instance verifier deployments.

adcp-client#1018 (merged) ships PostgresReplayStore. The default InMemoryReplayStore is per-process — multi-instance verifier fleets leak replay protection across the boundary because each box has its own cache. RFC 9421's 5-minute window bounds the gap but it's plenty of time for an in-flight replay. Suggested addition near the JWKS-resolver-options table:

Multi-instance verifier deployments need a shared replay store. The default InMemoryReplayStore is per-process; on a fleet, a captured signed request can be replayed against a sibling instance. Use PostgresReplayStore (5.20.0+) — same ReplayStore interface, one-line swap. See the SDK guide § Verify Inbound Signatures.

4. Step 6 "Sign outbound webhooks" — flag the in-process-only caveat.

createAdcpServer.webhooks.signerKey currently accepts only an in-process SignerKey — KMS-backed webhook signing on the server side is a follow-up. Worth a one-line caveat so readers don't expect symmetry with outbound request signing.

5. Testing — add adcp grade signer next to the existing verifier grader.

adcp-client#1019 (open, all CI green, accumulates into the same 5.20.0 release) ships the signer-side grader:

# KMS-backed signer via signing oracle
adcp grade signer https://addie.example.com \
  --signer-url https://signer.internal/sign --signer-auth "Bearer ${SIGNER_TOKEN}" \
  --kid addie-2026-04 --alg ed25519 \
  --jwks-url https://addie.example.com/.well-known/jwks.json

Pairs with adcp grade request-signing (verifier side) — together they cover both sides of the wire. Surfaces DER-vs-P1363 / kid-mismatch / algorithm-mismatch / missing-Content-Digest as specific verifier error_codes before pushing live signed traffic.

My spec-side PR #3255 (docs(security): production key storage subsection for RFC 9421 signing) adds a "Production key storage" subsection to security.mdx that pairs with this guide — different files, complementary content. They should merge cleanly (your <Note> is at line ~798, mine is in the steps below + a new subsection further down).

Happy to push these as a follow-up commit on this branch, or land yours as-is and I file a separate guide-update PR after #1019 merges and 5.20.0 publishes — whichever fits your release rhythm. Either order works.

cc @bokelley

bokelley added a commit that referenced this pull request May 9, 2026
…ption

Extends Ben's RFC 9421 guide so it covers all three official SDKs in
parallel rather than being TypeScript-only. Three additions:

1. Multi-language tabs (JS/TS, Python, Go) on every code-bearing step:
   - Step 1 programmatic keygen — adcp.signing.generate_signing_keypair,
     signing.GenerateKeyForProfile
   - Step 3 sign outbound — ADCPClient(signing=...) and sign_request,
     signing.NewSigner + RoundTripper
   - Step 4 verify inbound — verify_starlette_request /
     verify_flask_request, signing.Middleware
   - Step 5 verify webhook — verify_webhook_signature,
     signing.Middleware with ProfileWebhookSigning
   - Step 6 sign webhook — sign_webhook, NewSigner with
     ProfileWebhookSigning
   - Step 7 capability declaration — capabilities_response and
     adcp.RequestSigningCapability

2. KMS as a fourth (preferred) storage option in §1.3. Cloud HSM-backed
   keys never leave the boundary; signing happens via the KMS API. Calls
   out the TS SDK's createKmsSigner adapter and notes the latency
   tradeoff. The other options are reordered most-to-least-secure with
   memory-resident risk called out for env-var and secret-manager paths.

3. Up-front <Info> note that all three SDKs implement the same wire
   profile against the same conformance vectors — the API surface
   differs but the bytes on the wire are identical. Readers in any
   language can fall back to the spec + vectors if their SDK isn't
   listed.

Closes the cross-language ask from #3064 review comments.

Co-Authored-By: Ben Miner <ben@scope3.com>
@bokelley bokelley marked this pull request as ready for review May 9, 2026 14:46
@bokelley
Copy link
Copy Markdown
Contributor

bokelley commented May 9, 2026

Pushing the additions from my earlier review comments directly to this branch so this can land:

  • Multi-language tabs (JS/TS / Python / Go) on every code-bearing step: keygen, sign outbound, verify inbound, verify webhook, sign webhook, capability declaration. APIs verified against the actual modules in adcp-client-python (adcp.signing.*) and adcp-go (adcp/signing.*).
  • KMS as preferred storage option in §1.3. Cloud HSM-backed keys never leave the boundary; signing via the KMS API. Calls out @adcp/client/signing/kms (per createKmsSigner from adcp-client#1017) and notes the latency tradeoff. Other options reordered most-to-least-secure.
  • Cross-language framing up front so a reader in any language knows the wire profile is identical and the conformance vectors are the language-agnostic floor.

Total addition: +361 / -10 on request-signing.mdx. Ben's original guide is preserved verbatim where the TS code lives — the additions slot in as new tabs alongside it.

Marking ready for review. No structural changes to the existing diff.

New `docs/building/by-layer/L1/request-signing.mdx` — practical
implementation walkthrough complementing the normative spec at
`L1/security.mdx` (which covers the wire profile and verifier
checklist) and the operator-facing tuning page at
`L1/webhook-verifier-tuning.mdx`. Closes the missing third L1 page
flagged in `L1/index.mdx`.

Coverage:

- Step-by-step keygen, JWKS / brand.json publication, signing,
  verification, webhook signing, and capability declaration.
- Multi-language tabs (JS/TS, Python, Go) on every code-bearing step.
  APIs verified against `adcp-client-python` (`adcp.signing.*`) and
  `adcp-go` (`adcp/signing.*`) module exports.
- Storage section orders options most-to-least-secure: cloud KMS
  (preferred for spend-committing agents) → secret manager → env var
  → file. Calls out the TS SDK's `createKmsSigner` adapter (per
  adcp-client#1017) and notes the latency tradeoff.
- Up-front <Info> note that all three SDKs implement the same wire
  profile against the same conformance vectors — the API surface
  differs but the bytes on the wire are identical.

`docs.json`: adds the page to the L1 nav (both nav variants) and a
redirect from the legacy `/docs/building/implementation/request-signing`
path so external links to Ben's drafted location resolve.

Originally drafted by Ben Miner at `docs/building/implementation/`.
Re-ported to the L1 layered IA after the docs reorg, with the multi-
language and KMS additions folded in.

Co-Authored-By: Ben Miner <ben@scope3.com>
@bokelley bokelley force-pushed the docs/rfc-9421-signing-guide branch from 9434434 to 95f33a3 Compare May 9, 2026 15:18
@bokelley bokelley merged commit 5dc0795 into main May 9, 2026
18 checks passed
@bokelley bokelley deleted the docs/rfc-9421-signing-guide branch May 9, 2026 15:24
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