> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Request Signing Guide

> 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.

AdCP 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 remains **optional through AdCP 3.2** and becomes **mandatory in AdCP 4.0** for all spend-committing operations. Starting in 3.2, however, every accepted signature on a request with a body must cover `Content-Digest`. Agents that don't support signing may continue using their existing authentication path.

<Note>
  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](/dist/docs/3.2.1/building/by-layer/L1/security#signed-requests-transport-layer).
</Note>

<Info>
  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](#testing) and the [normative spec](/dist/docs/3.2.1/building/by-layer/L1/security#signed-requests-transport-layer) are language-agnostic.
</Info>

## 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 exact request body bytes; REQUIRED for every body-bearing request signed under the 3.2 profile

If any covered component changes after signing, verification fails.

For DNS hosts, signers and verifiers MUST remove one trailing root-label dot
after IDNA ToASCII processing and before deriving both `@target-uri` and
`@authority`. Thus `https://example.com./p` signs as
`https://example.com/p` with authority `example.com`; U-label and A-label IDN
forms produce the same bytes. Multiple trailing dots are malformed and MUST be
rejected. This codifies the shipping SDK behavior under
[DR-0008](https://github.com/adcontextprotocol/adcp/blob/main/governance/decisions/DR-0008-sdk-canon-for-wire-contracts.md).

### Key separation

Every agent publishes signing keys with a distinct `kid` and `adcp_use` tag:

* `adcp_use: "request-signing"` — for signing outbound tool calls and outbound webhooks
* `adcp_use: "webhook-signing"` — deprecated; still accepted on the webhook path for backward compatibility

For blast-radius isolation, publish a second `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:

```
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 theme={null}
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

<Tabs>
  <Tab title="JavaScript/TypeScript">
    ```typescript theme={null}
    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';
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from adcp.signing import generate_signing_keypair

    # CLI equivalence: adcp-keygen --alg ed25519 --purpose request-signing --kid my-agent-2026
    pem_bytes, public_jwk = generate_signing_keypair(
        alg="ed25519",
        purpose="request-signing",
        kid="my-agent-2026",
    )
    # pem_bytes — write to disk with mode 0600 and O_EXCL, or pass to a secret manager.
    # public_jwk — publish in your JWKS endpoint. Already includes kid, use, key_ops, and adcp_use.
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    import "github.com/adcontextprotocol/adcp-go/adcp/signing"

    res, err := signing.GenerateKeyForProfile(
        signing.AlgEd25519,
        "my-agent-2026",
        signing.ProfileRequestSigning,
    )
    if err != nil { /* handle */ }
    // res.PrivateKeyPEM — write to disk with mode 0600, or pass to a secret manager.
    // res.PublicJWK    — serialize and publish in your JWKS endpoint. AdCP-required
    //                    fields (kid, kty, crv, alg, use, key_ops, adcp_use) are set.
    ```

    Use `signing.ProfileWebhookSigning` for the webhook RFC 9421 profile. Publish the corresponding public key with `adcp_use: "request-signing"`; if you want webhook-only key material, use a separate `kid`.
  </Tab>
</Tabs>

### 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). |

<Warning>
  The algorithm name differs between the JWK entry (`"alg": "EdDSA"`) and the RFC 9421 `Signature-Input` parameter (`alg="ed25519"`). See the [algorithm naming table](/dist/docs/3.2.1/building/by-layer/L1/security#adcp-rfc-9421-profile) in the spec.
</Warning>

### 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 `createKmsSigner` for GCP Cloud KMS — see [`@adcp/client/signing/kms`](https://github.com/adcontextprotocol/adcp-client/tree/main/src/lib/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 `0600` and `O_EXCL` so an existing file is never overwritten — `Path.write_bytes` inherits the process umask (often `0644`, world-readable) and is unsafe for private-key material.

<Note>
  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.
</Note>

## 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 theme={null}
{
  "keys": [
    {
      "kid": "my-agent-2026",
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "<base64url-encoded-public-key>",
      "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 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:

```json theme={null}
{
  "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 / HTTP client

<Tabs>
  <Tab title="JavaScript/TypeScript">
    `createSigningFetch` wraps any `fetch`-compatible function to sign outbound requests automatically:

    ```typescript theme={null}
    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),
    });
    ```
  </Tab>

  <Tab title="Python">
    The Python SDK auto-signs every outbound request when `signing` is configured on `ADCPClient`:

    ```python theme={null}
    from adcp import ADCPClient
    from adcp.signing import SigningConfig, load_private_key_pem

    private_key = load_private_key_pem(open("private-key.pem", "rb").read())

    client = ADCPClient(
        base_url="https://seller.example.com/mcp",
        signing=SigningConfig(
            key_id="my-agent-2026",
            alg="ed25519",
            private_key=private_key,
            cover_content_digest=True,
        ),
    )
    # Every request the client sends carries Signature, Signature-Input, and
    # Content-Digest headers.
    ```

    For lower-level control (e.g., signing an arbitrary `httpx.Request` outside the client), call `sign_request` directly:

    ```python theme={null}
    from adcp.signing import sign_request

    signed = sign_request(
        method="POST",
        url="https://seller.example.com/mcp",
        headers={"Content-Type": "application/json"},
        body=request_body_bytes,
        private_key=private_key,
        key_id="my-agent-2026",
        alg="ed25519",
        cover_content_digest=True,
    )
    # signed.as_dict() returns the headers to attach to the outgoing request.
    ```
  </Tab>

  <Tab title="Go">
    The Go SDK exposes a `Signer` you wrap your `http.Client` transport with:

    ```go theme={null}
    import (
        "net/http"
        "github.com/adcontextprotocol/adcp-go/adcp/signing"
    )

    priv, _, _ := signing.LoadPrivateKey(pemBytes)
    signer, _ := signing.NewSigner(signing.SignerOptions{
        KeyID:      "my-agent-2026",
        Algorithm:  signing.AlgEd25519,
        PrivateKey: priv,
    })

    client := &http.Client{
        Transport: signer.RoundTripper(http.DefaultTransport, true /* cover content-digest */),
    }

    req, _ := http.NewRequest("POST", "https://seller.example.com/mcp", body)
    req.Header.Set("Content-Type", "application/json")
    resp, _ := client.Do(req)
    // Signature, Signature-Input, and Content-Digest are added by the transport.
    ```

    For one-off signing without the transport, call `signer.SignRequest(req, signing.SignOptions{CoverContentDigest: true})` directly on the request.
  </Tab>
</Tabs>

### 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 theme={null}
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)

### Framework middleware

<Tabs>
  <Tab title="JavaScript/TypeScript">
    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 theme={null}
    import {
      createExpressVerifier,
      StaticJwksResolver,
      InMemoryReplayStore,
      InMemoryRevocationStore,
    } from '@adcp/client/signing';
    import { mcpToolNameResolver } from '@adcp/client/server';

    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: mcpToolNameResolver,
      }),
      handler
    );
    // On verify: req.verifiedSigner = { keyid, agent_url?, verified_at }.
    // On reject: 401 with WWW-Authenticate: Signature error="<code>".
    ```
  </Tab>

  <Tab title="Python">
    The Python SDK ships framework wrappers for Flask and Starlette/FastAPI. Both call into the same `verify_request_signature` and raise `SignatureVerificationError` on rejection. Map that to a 401 with `unauthorized_response_headers`.

    A JWKS resolver covers one JWKS, so it's bound to one buyer agent. `CachingJwksResolver` takes that agent's `jwks_uri`. A seller with several buyers keeps one resolver per onboarded agent and picks it by `keyid`. The request carries no agent URL, so the `keyid` → agent index is built from onboarding records (see [Seller walkthrough: buyer-declared accounts](/dist/docs/3.2.1/building/by-layer/L2/accounts-and-agents#seller-walkthrough-buyer-declared-accounts)). `VerifyOptions` also needs the per-request `now` and `operation`, so build it for each request and reuse one replay store:

    ```python test=false theme={null}
    import json
    import time

    from fastapi import FastAPI, HTTPException, Request
    from adcp.signing import (
        AgentResolverError,
        InMemoryReplayStore,
        StaticJwksResolver,
        VerifierCapability,
        VerifyOptions,
        async_resolve_agent,
        check_key_origin_consistency,
        parse_signature_input_header,
    )
    from adcp.signing.errors import SignatureVerificationError
    from adcp.signing.middleware import (
        unauthorized_response_headers,
        verify_starlette_request,
    )

    app = FastAPI()

    SIGNING_PROFILE = "3.2"  # the profile this endpoint advertises; never read it from the request
    SIG_LABEL = "sig1"
    CAPABILITY = VerifierCapability(
        supported=True,
        covers_content_digest="required",
        supported_for=frozenset({"create_media_buy", "update_media_buy"}),
        required_for=frozenset({"create_media_buy", "update_media_buy"}),
    )
    REPLAY_STORE = InMemoryReplayStore()  # one per process; use a shared store across replicas
    NO_KEYS = StaticJwksResolver({"keys": []})  # unknown keyid -> request_signature_key_unknown
    AGENT_INDEX: dict[str, tuple[str, StaticJwksResolver]] = {}  # kid -> (agent URL, its keys)


    async def rebuild_agent_index(onboarded_agent_urls: list[str]) -> None:
        """Run at startup and at least once per revocation polling interval.

        Agent URLs come from your onboarding records, never from a request.
        """
        global AGENT_INDEX
        owners: dict[str, list[tuple[str, StaticJwksResolver]]] = {}
        for url in onboarded_agent_urls:
            try:
                res = await async_resolve_agent(url, agent_type="buying")
                check_key_origin_consistency(
                    jwks_uri=res.jwks_uri, key_origins=res.key_origins, purpose="request_signing"
                )
            except (AgentResolverError, SignatureVerificationError):
                continue  # delisted or misconfigured: the agent drops out of the index
            agent = (res.agent_entry["url"], StaticJwksResolver(res.jwks))
            for key in res.jwks.get("keys", []):
                owners.setdefault(key.get("kid"), []).append(agent)
        # A kid published by more than one agent maps to neither: both fail closed.
        AGENT_INDEX = {kid: agents[0] for kid, agents in owners.items() if kid and len(agents) == 1}


    def _reject_duplicate_keys(pairs):
        obj = {}
        for key, value in pairs:
            if key in obj:
                raise ValueError(f"duplicate key {key!r}")
            obj[key] = value
        return obj


    def parse_envelope(body: bytes) -> dict | None:
        """Parse once, strictly (checklist step 14). None means malformed."""
        try:
            envelope = json.loads(body, object_pairs_hook=_reject_duplicate_keys)
        except ValueError:
            return None
        return envelope if isinstance(envelope, dict) else None


    def mcp_operation(envelope: dict | None) -> str:
        if envelope is None or envelope.get("method") != "tools/call":
            return ""  # JSON-RPC protocol methods are matched by protocol_methods_*, not here
        params = envelope.get("params")
        name = params.get("name") if isinstance(params, dict) else None
        return name if isinstance(name, str) else ""


    def signature_keyid(headers) -> str | None:
        try:
            label = parse_signature_input_header(headers.get("signature-input", ""))[SIG_LABEL]
        except (ValueError, KeyError):
            return None  # the verifier reports the precise header error
        return str(label.params.get("keyid"))


    @app.post("/mcp")
    async def mcp(request: Request):
        body = await request.body()
        envelope = parse_envelope(body)
        operation = mcp_operation(envelope)

        if "signature" not in request.headers and "signature-input" not in request.headers:
            if envelope is None:
                raise HTTPException(status_code=400, detail="malformed JSON-RPC body")
            principal = authenticate_fallback(request, envelope, operation)  # bearer / API key / mTLS
            if principal is None and operation in CAPABILITY.required_for:
                raise HTTPException(
                    status_code=401,
                    detail="request_signature_required",
                    headers={"WWW-Authenticate": 'Signature error="request_signature_required"'},
                )
            if principal is None:
                return auth_error(request)  # AUTH_MISSING, or AUTH_INVALID for a rejected credential
            return await handle_mcp(envelope, principal)

        agent_url, jwks = AGENT_INDEX.get(signature_keyid(request.headers), (None, NO_KEYS))
        options = VerifyOptions(
            now=time.time(),
            capability=CAPABILITY,
            operation=operation,
            jwks_resolver=jwks,
            agent_url=agent_url,  # copied onto signer.agent_url
            replay_store=REPLAY_STORE,
            label=SIG_LABEL,
            signing_profile_version=SIGNING_PROFILE,
        )
        try:
            signer = await verify_starlette_request(request, options=options)
            if envelope is None:
                # Verified first so step 13 burned the nonce; now reject the ambiguous body.
                raise SignatureVerificationError("request_body_malformed", step=14)
        except SignatureVerificationError as exc:
            raise HTTPException(
                status_code=401,
                detail=exc.code,
                headers=unauthorized_response_headers(exc),
            )
        # signer.key_id, signer.agent_url, signer.verified_at are available for audit.
        return await handle_mcp(envelope, signer)
    ```

    `authenticate_fallback`, `auth_error`, and `handle_mcp` stand for your own code, which is why the block is marked `test=false`. Some points the example depends on:

    * **Unsigned requests.** `authenticate_fallback` returns `None` for a credential you don't accept. It also has to refuse an operation where signing must be unconditional (the strict posture), and a payload that carries a webhook `authentication` block when you support signing. Those cases get `request_signature_required` too ([Composition with fallback authenticators](/dist/docs/3.2.1/building/by-layer/L1/security#composition-with-fallback-authenticators)).
    * **One strict parse.** The SDK verifier doesn't run the step-14 duplicate-key check, so the example parses the body once with duplicate keys rejected and hands `envelope` to `handle_mcp`. Business logic never parses the body again.
    * **Key rotation.** A `kid` published between rebuilds fails with `request_signature_key_unknown` until the next rebuild, so rebuild at least as often as your JWKS cache TTL. Never fall back to trying every agent's keys.
    * **Blocking fetches.** `StaticJwksResolver` never fetches, which keeps the request path off the network. `CachingJwksResolver(jwks_uri=...)` refetches on a `kid` miss, but it fetches synchronously. If you use it in an async handler, prime it at startup or call `verify_request_signature` through `run_in_threadpool`.

    The SDK doesn't derive `signer.agent_url` from the request. It copies the `agent_url` you pass in `VerifyOptions`, and it's `None` when you pass none. `await verify_from_agent_url(request, agent_url, agent_type="buying", operation=..., capability=CAPABILITY)` runs the full discovery chain and fills `agent_url` from the matched `agents[]` entry. It fetches on every call, so pass an agent URL from your onboarding record, never one taken from the request. Always pass `capability`: the default is `VerifierCapability(supported=True)`, which accepts signatures without `content-digest`, and the helper can't set `signing_profile_version`, so it verifies under the 3.1 profile. Pass `revocation_checker` (for example, `CachingRevocationChecker`) if you track revoked keys.

    For Flask: swap `verify_starlette_request` for `verify_flask_request` (sync). For non-Starlette ASGI frameworks, call `verify_request_signature` directly.
  </Tab>

  <Tab title="Go">
    Mount `signing.Middleware` in your `http.Handler` chain. The middleware verifies inbound signatures, populates a `VerifiedSigner` in the request context on success, and writes a `401` with `WWW-Authenticate: Signature error="<code>"` on failure:

    ```go theme={null}
    import (
        "net/http"
        "github.com/adcontextprotocol/adcp-go/adcp/signing"
    )

    resolver := signing.NewCachingJWKSResolver()
    replay := signing.NewMemoryReplayStore(0 /* default cap */)
    revocation := signing.NewStaticRevocationSource(nil)

    mw := signing.Middleware(signing.MiddlewareOptions{
        Resolver:            resolver,
        Replay:              replay,
        Revocation:          revocation,
        OperationResolver:   signing.DefaultOperationResolver, // /adcp/<op>
        ContentDigestPolicy: signing.DigestRequired,
        RequiredFor:         []string{"create_media_buy", "update_media_buy"},
    })

    http.Handle("/mcp", mw(handler))

    // Inside handler:
    func handler(w http.ResponseWriter, r *http.Request) {
        v := signing.VerifiedSignerFromContext(r.Context())
        if v == nil {
            // Operation not in RequiredFor and request was unsigned — proceed
            // with bearer auth or whatever fallback you've configured.
        }
        // v.KeyID, v.AgentURL, v.VerifiedAt, v.Algorithm — available for audit.
    }
    ```

    For MCP servers: replace `signing.DefaultOperationResolver` with a custom resolver that parses the JSON-RPC envelope and returns the MCP tool name (the equivalent of `mcpToolNameResolver` in the TS SDK).
  </Tab>
</Tabs>

### 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.

```typescript theme={null}
import {
  serve,
  verifyApiKey,
  verifySignatureAsAuthenticator,
  requireAuthenticatedOrSigned,
  mcpToolNameResolver,
  MUTATING_TASKS,
} from '@adcp/client/server';
import { BrandJsonJwksResolver, InMemoryReplayStore, InMemoryRevocationStore } from '@adcp/client/signing/server';

serve(createAgent, {
  authenticate: requireAuthenticatedOrSigned({
    signature: verifySignatureAsAuthenticator({
      capability: { supported: true, required_for: ['create_media_buy'], covers_content_digest: 'required' },
      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 |
| - | - |
| `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. The webhook profile uses the same RFC 9421 mechanics as request signing but with `tag="adcp/webhook-signing/v1"` and `Content-Digest` always covered (no opt-out).

<Tabs>
  <Tab title="JavaScript/TypeScript">
    ```typescript theme={null}
    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...
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from adcp.signing import (
        WebhookVerifyOptions,
        BrandJsonJwksResolver,
        InMemoryReplayStore,
        verify_webhook_signature,
    )
    from adcp.signing.errors import SignatureVerificationError

    webhook_options = WebhookVerifyOptions(
        jwks_resolver=BrandJsonJwksResolver(),
        replay_store=InMemoryReplayStore(),
    )

    @app.post("/webhook")
    async def webhook(request: Request):
        body = await request.body()
        try:
            sender = verify_webhook_signature(
                method=request.method,
                url=str(request.url),
                headers=dict(request.headers),
                body=body,
                options=webhook_options,
            )
        except SignatureVerificationError:
            raise HTTPException(status_code=401, detail="invalid webhook signature")
        # sender.key_id, sender.agent_url available for audit; process the webhook.
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    import (
        "github.com/adcontextprotocol/adcp-go/adcp/signing"
    )

    // Mount the same Middleware on your webhook receiver, but configure it for
    // the webhook profile — adcp_use="request-signing" (deprecated
    // "webhook-signing" also accepted), Content-Digest required, no required_for
    // gating (webhooks always carry signatures).
    webhookMW := signing.Middleware(signing.MiddlewareOptions{
        Resolver:            signing.NewBrandJSONJWKSResolver(),
        Replay:              signing.NewMemoryReplayStore(0),
        Revocation:          signing.NewStaticRevocationSource(nil),
        Profile:             signing.ProfileWebhookSigning,
        ContentDigestPolicy: signing.DigestRequired,
    })

    http.Handle("/webhook", webhookMW(webhookHandler))

    func webhookHandler(w http.ResponseWriter, r *http.Request) {
        sender := signing.VerifiedSignerFromContext(r.Context())
        // sender.KeyID, sender.AgentURL — process the verified webhook.
    }
    ```
  </Tab>
</Tabs>

## Step 6: Sign outbound webhooks (seller)

<Tabs>
  <Tab title="JavaScript/TypeScript">
    Pass a `signerKey` to `createAdcpServer` and the framework signs every outbound webhook automatically:

    ```typescript theme={null}
    serve(() => createAdcpServer({
      name: 'My Seller',
      version: '1.0.0',
      webhooks: {
        signerKey: {
          keyid: 'my-seller-webhook-2026',
          alg: 'ed25519',
          privateKey: webhookPrivateJwk,
        },
      },
      mediaBuy: { /* ... */ },
    }));
    ```
  </Tab>

  <Tab title="Python">
    Sign each outbound webhook with `sign_webhook` and attach the returned headers before sending:

    ```python theme={null}
    from adcp.signing import sign_webhook, load_private_key_pem
    import httpx, json

    private_key = load_private_key_pem(open("webhook-private-key.pem", "rb").read())

    async def post_webhook(url: str, payload: dict) -> None:
        body = json.dumps(payload).encode("utf-8")
        headers = {"Content-Type": "application/json"}
        signed = sign_webhook(
            method="POST",
            url=url,
            headers=headers,
            body=body,
            private_key=private_key,
            key_id="my-seller-webhook-2026",
            alg="ed25519",
        )
        headers.update(signed.as_dict())  # adds Signature, Signature-Input, Content-Digest
        async with httpx.AsyncClient() as client:
            await client.post(url, content=body, headers=headers)
    ```
  </Tab>

  <Tab title="Go">
    Configure a `Signer` with `ProfileWebhookSigning` and use it via `SignRequest` or its `RoundTripper`:

    ```go theme={null}
    priv, _, _ := signing.LoadPrivateKey(webhookPemBytes)
    webhookSigner, _ := signing.NewSigner(signing.SignerOptions{
        KeyID:      "my-seller-webhook-2026",
        Algorithm:  signing.AlgEd25519,
        PrivateKey: priv,
        Profile:    signing.ProfileWebhookSigning,
    })

    webhookClient := &http.Client{
        Transport: webhookSigner.RoundTripper(http.DefaultTransport, true /* always cover content-digest for webhooks */),
    }
    // Use webhookClient.Post / .Do to deliver webhooks; signatures are added automatically.
    ```
  </Tab>
</Tabs>

Publish the webhook signing public key as a JWK with `"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, declare `signed_requests` (alias `request_signing` in the on-wire schema) in your `get_adcp_capabilities` response so buyers know to sign:

For AdCP 3.2, `covers_content_digest: 'required'` is part of the signing profile rather than an operation-level choice. The operation lists still control where signing is supported, shadowed, or required; they never weaken body binding. Keep `either` or `forbidden` only on an explicitly configured 3.0/3.1 compatibility endpoint.

<Tabs>
  <Tab title="JavaScript/TypeScript">
    ```typescript theme={null}
    createAdcpServer({
      capabilities: {
        overrides: {
          signed_requests: {
            supported: true,
            required_for: ['create_media_buy', 'update_media_buy'],
            supported_for: ['sync_creatives', 'sync_audiences'],
            covers_content_digest: 'required',
          },
        },
      },
      mediaBuy: { /* ... */ },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from adcp.server.responses import capabilities_response

    class MySeller(ADCPHandler):
        async def get_adcp_capabilities(self, params, context=None):
            return capabilities_response(
                ["media_buy"],
                request_signing={
                    "supported": True,
                    "required_for": ["create_media_buy", "update_media_buy"],
                    "supported_for": ["sync_creatives", "sync_audiences"],
                    "covers_content_digest": "required",
                },
            )
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    // In your get_adcp_capabilities handler, set the request_signing block on
    // the response builder:
    return adcp.CapabilitiesResponse(adcp.CapabilitiesData{
        SupportedProtocols: []string{"media-buy"},
        RequestSigning: &adcp.RequestSigningCapability{
            Supported:           true,
            RequiredFor:         []string{"create_media_buy", "update_media_buy"},
            SupportedFor:        []string{"sync_creatives", "sync_audiences"},
            CoversContentDigest: "required",
        },
    }), nil
    ```
  </Tab>
</Tabs>

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](/dist/docs/3.2.1/building/by-layer/L1/security#revocation) for the revocation list format.

## Testing

### Conformance vectors

The spec ships legacy-profile wire vectors, dedicated 3.2 wire vectors, and the 3.2 post-parse body-integrity reference-evaluator matrix at `/compliance/latest/test-vectors/request-signing/` (source at `static/compliance/source/test-vectors/request-signing/`). Select fixtures by `signing_profile_version`; the root positive and negative directories are 3.1 compatibility fixtures, not 3.2 conformance cases.

* **Legacy 3.1 positive vectors**: valid signed requests using the legacy binary encoding
* **Legacy 3.1 negative vectors**: invalid requests a 3.1 verifier must reject with the stated error code
* **Profile-3.2 wire vectors**: 3.2 versions of the legacy positive vectors and of the checklist step 2–12 negatives. They sign with `content-digest` covered (except the one negative that tests its absence) and assume a verifier advertising `covers_content_digest: "required"`. The set also includes rejections of malformed RFC 8941 `sf-binary` and malformed authorities.

```bash theme={null}
# Debug a single vector with the matching public key fixture.
# Use "latest" for convenience. Pin and record exact released values for both
# versions when you need to reproduce a verifier debugging run.
VECTOR_VERSION=latest
SDK_VERSION=latest
VECTOR_DIR="$(mktemp -d)"
trap 'rm -rf "$VECTOR_DIR"' EXIT
VECTOR_BASE="https://adcontextprotocol.org/compliance/${VECTOR_VERSION}/test-vectors/request-signing"

curl --fail --silent --show-error \
  --output "${VECTOR_DIR}/vector.json" \
  "${VECTOR_BASE}/positive/001-basic-post.json"
curl --fail --silent --show-error \
  --output "${VECTOR_DIR}/keys.json" \
  "${VECTOR_BASE}/keys.json"

npx "@adcp/sdk@${SDK_VERSION}" signing verify-vector \
  --vector "${VECTOR_DIR}/vector.json" \
  --keys "${VECTOR_DIR}/keys.json"
```

### Grade your verifier

```bash theme={null}
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>"`.

The negative vectors grade `<code>` **byte-for-byte** against the vector's `expected_outcome.error_code`, so a rejection that is correct in substance still fails its vector if the code string differs. Take the code from the taxonomy rather than from memory.

For the codes — with retry semantics for each, and the rules for the `WWW-Authenticate` header itself — see [Transport error taxonomy](/dist/docs/3.2.1/building/by-layer/L1/security#transport-error-taxonomy). Signing-key discovery via `brand_json_url` raises its own `request_signature_brand_*` and `request_signature_key_origin_*` codes, listed under [Discovering an agent's signing keys via `brand_json_url`](/dist/docs/3.2.1/building/by-layer/L1/security#discovering-an-agents-signing-keys-via-brand_json_url).

## Related

* [Security: Signed Requests](/dist/docs/3.2.1/building/by-layer/L1/security#signed-requests-transport-layer) — normative spec with verifier checklist, canonicalization rules, and replay dedup sizing
* [Push Notifications](/dist/docs/3.2.1/building/by-layer/L3/webhooks) — webhook setup including signature verification
* [Validate Your Agent](/dist/docs/3.2.1/building/verification/validate-your-agent) — full compliance validation including signing conformance
* [Build an Agent](/dist/docs/3.2.1/building/by-layer/L4/build-an-agent) — SDK setup and storyboard validation
* [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) — HTTP Message Signatures specification
