Skip to main content
This page defines an AdCP profile of Web Bot Auth (WBA), the IETF protocol for identifying automated HTTP clients. Under the profile, a seller identifies a signing agent from the request itself. The signer names its origin in a covered Signature-Agent header, and the seller fetches the signer’s keys from that origin’s key directory. The seller then decides, under AdCP’s own rules, what the identified agent may do.
Status: experimental in AdCP 3.3, adopted by DR-0023. The profile is published alongside the AdCP RFC 9421 profile in Signed Requests (Transport Layer), which stays as written. Document signing and transparency logs are separate decisions and are not part of this page.
The profile selects options that WBA and RFC 9421 already define. It adds no headers, tags, key fields, or error codes to the wire. Any implementation of draft-ietf-webbotauth-httpsig-protocol-00 can therefore sign and verify AdCP requests once it is configured with the rules on this page. Section numbers in this page refer to that draft unless they name RFC 9421.

What the profile changes for a seller

Under the AdCP RFC 9421 profile, a seller verifies a signature only from an agent it has onboarded. The agent’s key location comes from onboarding or from a registry the seller trusts. Under this profile, the seller learns the signer’s identity from the first signed request. Onboarding becomes approving a verified identity instead of issuing a credential. The seller approves an identity from a domain allowlist, from a commercial agreement, or with one click in its admin interface. After that, every signed request from the identity is authenticated without a bearer token. Until the seller approves the identity, the identity gets no account access, reads included. See Onboarding.

One resolution rule

A verifier resolves a signer’s keys in one step:
The verifier reads the Signature-Agent member keyed to the signature label and takes the origin it names. The member value is exactly the ASCII serialization of an https origin (RFC 6454 section 6.2), such as https://buyer-7k3q.com: a lowercase host, no default port, no trailing /, and no path, query, fragment, or userinfo (sections 5.2.1 and 5.5). A signer sends that canonical origin. A verifier compares the value with it as a string, without normalizing, and refuses any other value, including an http origin, with 403. It fetches the HTTP Message Signatures Directory at that origin’s well-known path (section 5.5). One fetch returns one file, and the verifier reads no other document to find keys. The directory is the only key location the profile recognizes. The verifier ignores a Signature-Agent member with a type parameter other than directory, as section 5.2.1 permits for a type the verifier does not support. Section 5.2.1 defines type as a Token, and a verifier accepts type=directory as a Token or as a String, as the reference library does. Section 5.5 explains why the well-known path matters: “TLS authenticates the host but not the path”, and “The well-known URI is reserved, so directory additionally names a domain”.

Identity

Under the profile, an identity is an origin, for example https://buyer-7k3q.com. The identity has one key directory, and every key it signs with is published there. The verifier attributes a verified request to that origin and to nothing else. Two agents cannot share an origin. An operator that runs several agents gives each its own origin, for example one subdomain per agent. Section 6.5 recommends this for agents that need to be told apart: “these should use distinct signing keys and signing key directories”. The same rule covers purposes: a governance agent is an agent like any other, on its own origin (see One agent per purpose). brand.json, adagents.json, and a seller’s onboarding records refer to an identity by its origin. So does trust.json: it lists agent origins with their roles and carries no keys. A multi-tenant vendor gives each tenant its own subdomain, for example https://7f3a9c.sentinel-governance.example, which serves that tenant’s key directory. An agent that shares a host with other agents under the AdCP RFC 9421 profile keeps using that profile until it moves to its own origin. When it moves, the seller maps the agent’s principal to that origin explicitly. Key discovery therefore never changes who the seller considers the caller to be. Key lookup is keyed on the pair of origin and key, never on the key alone (section 5.4). A verifier that indexes keys by thumbprint alone attributes a request to whatever origin the signer claims. Negative vector 004-key-not-in-named-directory grades this rule.

One agent per purpose

Purpose belongs to the agent, not to the key. A governance agent is an identity like any other, on its own origin, with its own key directory, so no key and no directory states a purpose. JWS artifacts, such as governance tokens, are outside this profile.

Profile rules

RFC 9421 section 3.2.1 lets an application impose requirements beyond the base specification. Its examples include “Requiring a specific set of header fields to be signed”, “Enforcing a maximum signature age”, and “Enforcing uniqueness of the nonce parameter”. The profile consists of the following rules. Each rule is a requirement of that kind or a choice among options WBA defines. A request that follows the profile is a valid WBA request. A WBA verifier configured with these rules accepts it and rejects a request that falls short. Because the rules are fixed in the profile, a signer knows them before its first request. AdCP adds two checks that run outside the WBA verifier, after it returns: the revocation-list check on a verified request, and the contents of Accept-Signature on a refusal. AdCP checks after Web Bot Auth verification defines both. A standard WBA library does not perform them, and the WBA conformance vectors do not grade them. The profile uses one algorithm. Every WBA library signs and verifies Ed25519, and several WBA tools support no other algorithm. The two nonce encodings are the two the WBA drafts use. The architecture draft’s text names base64url, and the examples in both drafts use padded standard base64. Cloudflare’s web-bot-auth library accepts exactly these two forms in validateNonce, and its generateNonce produces padded standard base64. A verifier accepts either encoding, so the profile states no preference for signers. The 300-second ceiling on expires is provisional. WBA section 5.2 recommends no more than 24 hours for general automated traffic, and the RFC asks the working group to confirm the shorter value for spend-committing traffic.

A signed request

A buyer agent at buyer-7k3q.com sends create_media_buy to a seller over MCP. The domains are placeholders that resolve to nothing, and long lines are wrapped per RFC 8792.
The signature base the buyer signed is the one in positive/001-single-signer-request.json under Conformance vectors. The Signature-Agent value is an origin, so the verifier fetches https://buyer-7k3q.com/.well-known/http-message-signatures-directory.

Key directories

A key directory is a JWK Set served at /.well-known/http-message-signatures-directory with the media type application/http-message-signatures-directory+json (sections 5.5.1 and 8.2). Each key’s kid is its JWK thumbprint, computed per RFC 7638 and, for Ed25519 keys, RFC 8037 Appendix A.3. Publishers omit alg on Ed25519 keys. A publisher adds a key to the directory before signing with it, as section 5.5.2 describes, so that verifiers can resolve it on first use. The directory server signs its response once per Ed25519 key (Appendix B.1). Each signature covers "@authority";req and content-digest, carries created and expires, uses the key’s thumbprint as keyid, and uses tag="http-message-signatures-directory". Covering the request authority stops the key set from being served again from another domain. A verifier validates each signature with the key the directory itself provides and validates Content-Digest against the body; it rejects a response signature whose created is in the future. The verifier stores the signed response as its record of the fetch. A verifier uses each Ed25519 key that has a valid signature on the directory response, and ignores every other key and its signatures. Appendix B.1 lets a verifier use a key without a valid proof, and says the proof “matters for a domain-bound identifier”. An AdCP identity is an origin, so the profile uses only proven keys. The directory draft says the same in section 5: “Clients SHOULD ignore keys from a directory response that do not have a corresponding valid signature”. The keys a verifier ignores include keys the profile does not use, such as a P-256 key for another WBA use (section 5.5.1 permits any registered algorithm), and keys it cannot import, including a malformed Ed25519 key, as RFC 7517 section 5 advises. A directory with no validly signed Ed25519 key yields no usable key, so a request signed with one of its keys fails as an unknown key. A verifier refuses a directory in which any key carries a private member (d, p, q, dp, dq, qi, oth, or k), whatever the key’s type. It compares member names without regard to case, so D is refused as well: some JSON decoders match member names case-insensitively and would read D as the private scalar. It checks every key for these members before it skips the keys the profile does not use, so a P-256 key with a d member is refused, not ignored. Section 5.5.1 says that “A verifier SHOULD validate the directory format and reject malformed entries”, and a directory that publishes private key material is malformed.
Fetch rules. The verifier fetches a directory over HTTPS only, checks the resolved address before connecting, and follows no redirects (section 5.5). It applies the size, key-count, and time limits of section 6.7 and AdCP’s fetch budgets: a 256 KiB body cap and 5 s connect and 10 s total deadlines. The verifier treats any status other than 200 as a discovery failure. It rejects a directory response whose media type is not application/http-message-signatures-directory+json, which section 5.5.1 requires of a directory served at the well-known path. Caching. A directory that resolves and does not contain the key is evidence that the key was removed, and it replaces the cached entry. A directory that fails to resolve is not evidence and does not evict a cached entry (section 6.10). Negative cache entries expire after at most 60 s, within the five-minute bound of Appendix C.5. A cached directory verifies until the earliest expires among its signatures. During a discovery failure, the fail-closed rule of the revocation list bounds spend-committing requests (see AdCP checks after Web Bot Auth verification). A compromised key is revoked, not removed.

Verification

A verifier applies the following procedure to a request that carries Signature-Input.
  1. Parse Signature, Signature-Input, and Signature-Agent as Structured Fields. Every Signature member value is a Byte Sequence and every Signature-Input member value is an Inner List (RFC 9421 sections 4.1 and 4.2), whatever the member’s tag. If any header fails to parse, answer 400 (section 5.4).
  2. Read the one Signature-Input member whose tag is web-bot-auth and the Signature-Agent member with the same label. The verifier does not read members with other tags. A request that carries more than one web-bot-auth member or more than one Signature-Agent member is rejected with 403. If the Signature-Agent member is absent, or the signature does not cover "signature-agent";key=<label>, reject the signature. An uncovered member is a claim the verifier must not attach policy to (section 4.1).
  3. Resolve the origin the member names to its key directory, from cache or by fetching it, and select the key whose kid equals keyid. If the directory holds no such key, answer 403.
  4. Verify the signature with the selected key, and check its covered components, parameters, and window against the rules in Profile rules. If the signature fails verification or the profile rules, answer 403, as described in Error responses.
  5. Check the nonce against the identity’s replay state (see Replay protection). If the identity used it within the signature’s lifetime, answer 429.
  6. Record the nonce in the identity’s replay state. The verifier records a nonce only after steps 1 to 5 pass, so a request that fails verification can’t use up a nonce that its legitimate signer hasn’t sent yet.
  7. Run the AdCP checks in AdCP checks after Web Bot Auth verification, starting with the identity’s revocation list.
  8. Pass the verified identity to the AdCP layer, which applies Onboarding and the account authorization rules that apply to every request, signed or not.
Steps 1 to 6 are WBA verification with the profile’s rules, and a WBA library configured with those rules performs them. Steps 7 and 8 belong to the AdCP layer. When a deployment verifies at the edge, Appendix C.10 applies: the edge “can instead verify at the proxy and pass the result to the origin through a deployment-local trusted channel”. The edge removes any copy of that result a client sends, and the origin accepts the result only over its authenticated channel to its own edge. The edge forwards the signature fields unchanged, because the seller’s own verifier enforces nonce uniqueness whatever the edge does.

Onboarding

A verified identity gets account access only after the seller binds it to a principal, under rule R5 of Agent identity and buyer self-onboarding. Until then it gets what an anonymous caller gets, reads included. After binding, the principal resolves to its accounts, and every later signed request from the identity is authenticated without a bearer token. A seller approves an identity from a domain allowlist, from a commercial agreement, or with one click in its admin interface. For one-click approval, the seller records the identity for review and rejects the request with an AdCP authorization error. It never runs a queued request later, because the request’s signature expires within minutes; after approval, the buyer sends the request again. A seller limits how many identities one origin or one network address can submit for review. The account’s operator comes from the request’s account.operator, not from the authenticated identity. Whether the identity may act for the brand named in the request is the authorization check that Agent identity separates from authentication. This profile changes how the seller learns who is calling, not what the seller lets the caller do.

Replay protection

The seller’s verifier MUST reject a nonce it has already seen for the same identity within the signature’s lifetime, whether or not an edge verifier ran first. The profile requires the nonce on every signed request, reads included. A replayed read returns the response to whoever sent the replay, and proxies and log pipelines see request headers. RFC 9421 section 7.2.2 describes the nonce as “a per-message unique value to allow the verifier to detect replay of the signature itself”. WBA leaves nonce enforcement to deployment policy (Appendix C.6); the profile makes that choice once, so each seller does not have to. idempotency_key keeps its role. A repeated committed mutation returns the stored response without re-running side effects, under Idempotency. The signature covers content-digest, so it binds the key too. Replay-cache sizing is implementation guidance. The entry lifetime for a nonce is the remaining signature validity plus the 60-second skew allowance. The recommendations in Transport replay dedup carry over with the identity in place of keyid.

Error responses

The profile reports signature failures with HTTP status codes, as WBA does (sections 5.3 and 5.4): On a 403 or 429, the AdCP layer SHOULD add an Accept-Signature field that names the components and parameters the profile requires. AdCP checks after Web Bot Auth verification states its contents. The conformance vectors grade the status, not that field. A request signed with a revoked key gets the error that Transport revocation states. An identity without the needed onboarding entry gets an AdCP authorization error, without Accept-Signature. Those errors use the AdCP error envelope and codes, not the transport status codes in the table. An unsigned request to an operation in required_for, from a caller with no other credential the verifier accepts, gets an answer that depends on what the verifier advertises. A verifier that advertises only web-bot-auth answers 403. A verifier that also advertises the AdCP RFC 9421 profile answers as that profile does: 401 with request_signature_required. In both cases the AdCP layer SHOULD add Accept-Signature for this profile.

Webhooks

A webhook signed under this profile is an ordinary signed request from the seller’s identity. The seller signs the delivery with the rules in Profile rules. Its Signature-Agent member names the seller’s origin, and the signature covers content-type and content-digest. The receiver resolves the seller’s keys from the seller’s key directory with the same one resolution rule, and applies Verification and Replay protection as a seller does. A subscriber states which profile it verifies in the optional signing_profile field of every webhook registration: push_notification_config, reporting_webhook, artifact_webhook, accounts[].notification_configs[], and the agent-level notification configuration. web-bot-auth selects this profile, and adcp/webhook-signing/v1 selects the AdCP webhook profile. A registration that omits the field is signed as before: with the AdCP webhook profile, or with the legacy scheme when authentication is present. A registration carries signing_profile or authentication, never both, and the schemas refuse one that carries both. The field has no schema default, so a validator or a generated SDK that fills in defaults never adds it next to authentication. The seller signs every delivery to that registration with the one selected profile and never signs a delivery both ways. This keeps the mode-selection rule of Webhook callbacks. The two profiles encode binary header values differently: the AdCP webhook profile uses unpadded Base64URL, and this profile uses RFC 8941 Base64. A receiver therefore selects one parser from the registration and never tries both. A seller advertises the profiles it can sign webhooks with in webhook_signing.profiles. A seller that does not list the profile a subscriber selects rejects the configuration as a validation failure.

Capability

A seller advertises which request-signing profiles it accepts in request_signing.profiles, named by the tag value each profile puts in Signature-Input. Omitting the field means the AdCP RFC 9421 profile only, so every 3.0 to 3.2 capability document stays valid.
A buyer reads profiles and signs with a profile the seller lists. The operation lists (required_for, warn_for, supported_for, and the protocol_methods_* lists) keep their meaning for both profiles. The profile is experimental: a seller that lists web-bot-auth in either list also lists signing.web_bot_auth in experimental_features.

Relationship to the AdCP RFC 9421 profile

The two profiles share the signature base, the covered request components, the parameter semantics, the fetch rules, and the signed revocation list. They differ in how a verifier finds the signer’s keys and how it reports a failure. The following table lists the mechanisms of the AdCP RFC 9421 profile and the rule that takes their place under this profile. Account authorization, the signed revocation list, and principal boundaries do not change. AdCP checks after Web Bot Auth verification states how the revocation list applies under this profile.

Differences from the reference library

The conformance tooling judges implementations with Cloudflare’s web-bot-auth 0.2.0, which implements -00. The profile differs from that library in one way, and AdCP accepts the difference:
  • Member keyed to the label. The library accepts a signature that covers any one Signature-Agent member, whatever its key. The profile requires the member keyed to the signature’s label, as section 5.2.1 states: “Its member keyed to the signature label MUST be signed as a component.” A verifier built on the library adds this rule through the library’s validate hook. Scenario R19 in scenarios.json grades it, with tier adcp.
The library also does not enforce one WBA MUST yet: section 5.2.1 requires an https origin, and the library accepts http (cloudflare/web-bot-auth#162). This is a gap in the library, not a difference in the profile. Scenario R16 grades the rule, with tier adcp until the library enforces it. The library does not check the rest of the origin rule either: scenarios R17 (a path), R17c (a trailing /), R17d (an uppercase host), and R17e (an explicit :443) grade it, each with tier adcp.

Conformance vectors

The vectors live at static/compliance/source/test-vectors/wba-profile/ and are served at /compliance/latest/test-vectors/wba-profile/. They grade the rules on this page, and each states its expected outcome in the status-code vocabulary of Error responses.
  • positive/001-single-signer-request: the request on this page, signed by https://buyer-7k3q.com.
  • positive/002-signed-directory-response: the directory response on this page, signed per Appendix B.1.
  • negative/001-missing-nonce: 403.
  • negative/002-replayed-nonce: 429; needs preloaded replay state.
  • negative/003-signature-agent-not-covered: 403; the header is present but not a covered component.
  • negative/004-key-not-in-named-directory: 403; the signing key is published by a different origin than the one Signature-Agent names.
  • negative/005-malformed-signature-agent: 400; the header does not parse.
  • negative/006-two-signatures: 403; the request carries two web-bot-auth signatures, each valid on its own.
scenarios.json holds failure scenarios as data: a base request, the covered components, parameter overrides, a change after signing, and the expected status. A harness signs each scenario at run time, so no committed signature expires. Each scenario names a tier: wba when a WBA library with no AdCP rules already gives the expected outcome, and adcp when only the profile’s rules do. The keys in keys.json are public test keys with published private seeds. Section 6.8 says test keys “MUST NOT be used in production”, and verifiers SHOULD reject them when detected. The README in the vector directory describes the file format and the harness state each vector needs.

References