Reference · agent identity
How do I verify that a request came from an AI agent, and from which one?
Checked against the primary text on 2026-08-09. Where a date is derived rather than written down, the arithmetic is shown.
You verify a signature the agent put on its own request. An agent that follows the
Web Bot Auth profile signs a handful of components of the HTTP request with an
Ed25519 key using HTTP Message Signatures (RFC 9421), and names the key it used in
the Signature-Input header. You rebuild the signature base from the
request exactly as it arrived, resolve the key, and check it. Everything below is
detail about doing that without fooling yourself.
What does a signed agent request look like?
Three headers carry the whole mechanism. Signature-Input lists which
parts of the request were covered and with which key; Signature holds
the bytes; Signature-Agent optionally names the origin that publishes
the key.
POST /api/orders HTTP/1.1
Host: shop.example.eu
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest");\
created=1754558062;expires=1754558362;\
keyid="poqkLGiUqyqJZmZ…";alg="ed25519";tag="web-bot-auth"
Signature: sig1=:2xLd7k…:
Signature-Agent: "https://agents.example"
Which checks have to pass, and in which order?
Order matters, because each step decides whether the next one is worth doing. A verifier that checks the cryptography first and the covered components afterwards can be talked into accepting a signature over a request nobody made.
-
Parse
Signature-Input. Reject anything you cannot parse rather than guessing at it. -
Insist on the components you care about. At minimum
@method,@authorityand@path. A signature that covers only@pathis valid and worthless: the same bytes authorise aDELETEto a different host. -
Cover the body, or accept that it is unsigned. A request with a
body needs
content-digestin the covered list, and you have to recompute that digest yourself. Otherwise the signature protects the envelope while the contents travel free. -
Check
createdandexpires. Reject signatures older than a window you choose — minutes, not hours — or you have built a replay oracle. Allow a little clock skew, and count skew against the window rather than adding to it. -
Resolve
keyidto a public key. See the next section. A key you cannot resolve is an unverified request, not an error page. -
Check that
algmatches the key you resolved, rather than trusting the header to tell you which algorithm to run. Take the algorithm from the key material. - Rebuild the signature base and verify. From the request as it arrived at you, not as your framework normalised it.
Where does the public key come from?
From one of two places, and the difference is a security decision rather than a
convenience. Either you registered the key in advance, in which
case keyid — a JWK thumbprint — is simply a lookup in your own table;
or the request carries Signature-Agent and you fetch the operator's
key directory at
/.well-known/http-message-signatures-directory on that origin.
The second path means making an outbound HTTP request to a URL supplied by an unauthenticated caller, which is a server-side request forgery surface in the middle of your request path. If you do it: allowlist the schemes and hosts, resolve and pin the address, refuse private ranges, cap the response size, set a timeout below your own latency budget, cache aggressively by origin, and put the whole thing behind an egress proxy. Mandaton implements this path and ships with it off by default for exactly these reasons.
What does a valid signature still not tell you?
That the agent is allowed to do what it is asking. This is the distinction that most integrations collapse and should not: a signature establishes which software is calling, and says nothing about on whose behalf it acts or whether that party ever agreed. A verified agent without a mandate is a stranger wearing a name badge.
- It does not authenticate a human, a company, or a customer relationship.
- It does not protect anything outside the covered components — headers you did not list can be changed in transit and the signature still verifies.
- It does not make the agent honest. A signed request is an accountable request, which is a different and more useful property.
- It does not prove the key belongs to the operator whose name is on the directory, beyond the fact that the directory was served from that origin over TLS.
How is this different from bot detection?
Bot detection answers a probability; a signature answers a question. Fingerprinting, behavioural scoring and IP reputation produce a number you cannot show anyone afterwards, and they degrade the moment the traffic they classify has an incentive to look different. A signature check either passes or fails, for a reason you can write down, against a public key that a third party can fetch and re-check a year later. The two are not competitors — one is a filter, the other is evidence.
How settled is this standard?
Partly. RFC 9421 (HTTP Message Signatures) is a published IETF standard
from February 2024 and is not going to move. Web Bot Auth is a profile of
it that is still an Internet-Draft, driven largely by the network operators
who see the traffic first, and the details most likely to shift are the directory
path, the exact tag string, and how key rotation is announced. Build the verifier
against RFC 9421, keep the profile-specific parts — the tag, the directory location
— in one place you can edit, and do not scatter the string
web-bot-auth through your codebase.
What Mandaton does with this
One call, POST /v1/verify, does the seven checks above, applies your
policy to the result, and returns a receipt that records what was verified: the key
id, the operator, the algorithm, the exact list of covered components, and — when
verification failed — the reason it failed. That last field is the one people
underestimate. Most of what you want to answer months later is not "was this
signed" but "what did we see, and why did we let it through".
The quickstart is here, and a receipt can be checked without an account.