Web Bot Auth lets an AI agent prove who it is by signing each request with a private key and publishing the matching public key on its operator's domain. A site verifies by reading the Signature-Agent header, fetching that domain's key directory, picking the key whose thumbprint matches the signature's keyid, rebuilding the signed parts of the request, and checking both the signature and its time window. If any step fails, the request is simply unverified.
Why not just trust the user agent?
Because it is free text. Any script can send a user agent naming a well-known assistant, and plenty do, precisely because sites treat those names kindly. IP allowlists are the traditional fix, but agents increasingly run from shared cloud ranges and residential proxies, and published IP lists go stale. A signature binds the request to a key, and the key to a domain, without either side maintaining lists.
What does a signed request look like?
Three headers carry it. Signature-Input lists what was signed and the parameters. Signature holds the signature itself. Signature-Agent says where the keys live.
Signature-Agent: sig1="https://agent.example.com"
Signature-Input: sig1=("@authority" "signature-agent";key="sig1");
created=1794330000;expires=1794330060;
keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";
tag="web-bot-auth"
Signature: sig1=:jdq0SqOwHdyHr9+r5jw3iYZH6aNGKijYp/EstF4RQTQdi5N5YYKrD+mCT1HA1nZDsi6nJKuHxUi/5Syp3rLWBA==:The tag value web-bot-auth marks the signature's purpose. The keyid is the key's JWK thumbprint (RFC 7638), so the verifier can find the right key without trusting a name. Older deployments send Signature-Agent as a plain quoted URL rather than a dictionary member; a verifier should accept both.
How does a site verify it?
- Find the signature labelled with tag="web-bot-auth" in Signature-Input.
- Check the window: created is not in the future and expires has not passed, allowing a minute of clock skew.
- Check coverage: the signature must include @authority (or @target-uri) and the signature-agent header. Otherwise it could be lifted onto a different host or pointed at different keys.
- Resolve the key directory from Signature-Agent: an HTTPS URL, by default /.well-known/http-message-signatures-directory on that origin.
- Fetch the directory (a JSON Web Key Set), take the Ed25519 key whose thumbprint equals keyid, and check the key's own nbf and exp if present.
- Rebuild the RFC 9421 signature base from your view of the request, one line per covered component, ending with @signature-params, and verify the signature over it.
Which mistakes let forgeries through?
- Not requiring @authority: a signature made for another site's request then verifies on yours.
- Not requiring signature-agent to be signed: an attacker keeps a real signature and points the header at their own keys.
- Fetching the key directory carelessly: the header is attacker-controlled, so only fetch HTTPS URLs that resolve to public addresses, cap the response size and time, and cache results so one agent costs one fetch.
- Ignoring time: without the created and expires check, a captured request can be replayed indefinitely.
- Failing open: a malformed header should mean unverified, never verified and never an error that blocks the request.
What does a verified signature give you?
Identity, not permission. You learn which operator sent the request, which is enough to apply a policy you choose: let a known assistant's bookings through to a separate queue, rate limit by operator rather than by IP, or keep a record of which agents act on your site. Content still needs its own checks.
Where WebForms uses it
Every form submission to WebForms is checked for a Web Bot Auth signature. A valid one tags the submission as a verified AI agent, with the operator's domain alongside, and routing rules, webhooks and the API all see it. An unsigned or invalid signature never blocks a submission; it just is not called verified, and the dashboard shows why.