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.
Code examples below use JavaScript/TypeScript, Python, and Go SDK helpers in tabs. All three SDKs implement the same RFC 9421 profile against the same conformance vectors — the API surface differs but the wire output is identical. If your language isn’t listed, the conformance vectors and the normative spec are language-agnostic.
When you need this
Key concepts
Signature coverage
The AdCP signing profile covers these request components:@method— HTTP method@target-uri— full canonicalized request URL@authority— lowercased host headercontent-type— media typecontent-digest— SHA-256 or SHA-512 hash of the request body (seecovers_content_digestcapability)
Key separation
Every agent publishes signing keys with a distinctkid and adcp_use tag:
adcp_use: "request-signing"— for signing outbound tool calls and outbound webhooksadcp_use: "webhook-signing"— deprecated; still accepted on the webhook path for backward compatibility
request-signing key under a separate kid for webhook delivery rather than reusing the same key material for tool calls and webhooks.
Discovery chain
Verifiers find your public key through a three-step chain:@adcp/client SDK provides BrandJsonJwksResolver which handles this chain automatically with caching and refresh.
Step 1: Generate a signing key
CLI
private.jwk— the private key (JWK withdfield). Keep this secret.public-jwks.json— the public key in JWKS format. Publish this.
Programmatic
- JavaScript/TypeScript
- Python
- Go
Supported algorithms
Storing the private key
Pick the strongest option your runtime supports. From most to least secure:- Cloud KMS (GCP Cloud KMS, AWS KMS, Azure Key Vault): the private key is generated inside the HSM and never leaves it. Signing is performed by calling the KMS API; you only ever hold the JWK reference, not the key bytes. The TypeScript SDK exposes
createKmsSignerfor GCP Cloud KMS — see@adcp/client/signing/kms. Recommended for any agent handling spend-committing operations. - Secret manager (GCP Secret Manager, AWS Secrets Manager, HashiCorp Vault): load at boot, keep in memory for the process lifetime. Easier than KMS but the key material lives in your process — leaks via memory dumps, logging, or compromised dependencies.
- Environment variable:
ADCP_SIGNING_PRIVATE_KEY='{"kid":"...","kty":"OKP",...}'. Acceptable for dev and small deployments. Same memory-resident risk as a secret manager. - File: development only. Never commit to version control. Use mode
0600andO_EXCLso an existing file is never overwritten —Path.write_bytesinherits the process umask (often0644, world-readable) and is unsafe for private-key material.
Once you choose KMS, signing latency rises (one round-trip to the HSM per request). Profile under load before committing — the TypeScript and Python SDKs cache JWK metadata aggressively and can sustain hundreds of signs/sec against GCP KMS in our internal tests, but your numbers depend on region and concurrency.
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):
d field. Set Cache-Control: max-age=3600 or similar. If you use separate key material for webhook delivery, publish a second request-signing JWK with a distinct kid. Deprecated webhook-signing JWKs remain accepted on the webhook path for backward compatibility.
brand.json
Serve at/.well-known/brand.json on your brand domain. The jwks_uri is how verifiers find your keys:
Step 3: Sign outbound requests (buyer / orchestrator)
Wrapping fetch / HTTP client
- JavaScript/TypeScript
- Python
- Go
createSigningFetch wraps any fetch-compatible function to sign outbound requests automatically:Capability-aware signing
buildAgentSigningFetch checks whether the target seller supports signed-requests and only signs when supported. This is the recommended approach for production:
Step 4: Verify inbound signatures (seller)
Framework middleware
- JavaScript/TypeScript
- Python
- Go
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:Composing signature + bearer auth with requireAuthenticatedOrSigned
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.
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
Step 5: Verify inbound webhooks (buyer / orchestrator)
When sellers send webhooks, verify the signature to confirm authenticity. The webhook profile uses the same RFC 9421 mechanics as request signing but withtag="adcp/webhook-signing/v1" and Content-Digest always covered (no opt-out).
- JavaScript/TypeScript
- Python
- Go
Step 6: Sign outbound webhooks (seller)
- JavaScript/TypeScript
- Python
- Go
Pass a
signerKey to createAdcpServer and the framework signs every outbound webhook automatically:"adcp_use": "request-signing". The webhook verifier still accepts deprecated "webhook-signing" keys for backward compatibility, but new signers should use request-signing. If you want independent webhook rotation or blast-radius isolation, publish a separate request-signing JWK with a webhook-specific kid.
Step 7: Declare the capability
If your seller verifies inbound signatures, declaresigned_requests (alias request_signing in the on-wire schema) in your get_adcp_capabilities response so buyers know to sign:
- JavaScript/TypeScript
- Python
- Go
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:- Generate a new keypair with a new
kid - Add the new public key to JWKS (both old and new are published)
- Update signing configuration to use the new private key
- After 24–48 hours, remove the old public key from JWKS
kid to revoked_kids in your revocation list and rotate to a new key immediately. See Revocation for the revocation list format.
Testing
Conformance vectors
The spec ships 39 test vectors atcompliance/cache/3.0.0/test-vectors/request-signing/ (source at static/compliance/source/test-vectors/request-signing/):
- 12 positive vectors: valid signed requests your verifier must accept (non-4xx)
- 27 negative vectors: invalid requests your verifier must reject with
401and the correct error code
Grade your verifier
Error codes
When verification fails, return401 with WWW-Authenticate: Signature error="<code>":
For the full error code taxonomy, see Transport error taxonomy.
Related
- Security: Signed Requests — normative spec with verifier checklist, canonicalization rules, and replay dedup sizing
- Push Notifications — webhook setup including signature verification
- Validate Your Agent — full compliance validation including signing conformance
- Build an Agent — SDK setup and storyboard validation
- RFC 9421 — HTTP Message Signatures specification