All posts
Engineering

RFC 9421: How to Cryptographically Verify an AI Agent

User agent strings are a suggestion. HTTP Message Signatures are a proof. A walkthrough of the standard now underpinning verified AI agent traffic.

Jay Patel11 min read
user-agent:unverifiedsignature valid

Every bot detection system built before 2024 rests, ultimately, on a string that anyone can type. The user agent header is self-declared, unauthenticated, and trivially forgeable — you can claim to be Googlebot with a single line of curl. The entire industry's response has been reverse DNS lookups, published IP ranges, and behavioural heuristics: an elaborate scaffolding erected around the fact that HTTP has no native way to prove who is calling.

RFC 9421 changes that. It gives HTTP a standard mechanism for signing a request so the recipient can verify cryptographically that it came from the holder of a particular key. For AI agents — which are about to make a very large number of requests on behalf of people who are not watching — this is the difference between a claim and a proof.

The problem in one line

User-Agent: Mozilla/5.0 (compatible; ExampleBot/1.0) is a claim. Nothing anywhere checks it.

Everything the industry does to compensate is inference on top of that gap. IP allowlists require the operator to publish ranges and keep them current, and they break the moment traffic moves. Reverse DNS verification works but only for operators who run it properly and costs a lookup on the request path. Behavioural heuristics are probabilistic and adversarial by nature.

None of these verify the request. They verify something near the request and hope the association holds.

What a signature actually covers

The core insight of RFC 9421 is that you do not sign the raw bytes of an HTTP message. You cannot: proxies legitimately reorder headers, normalise whitespace, add hops, and re-encode. A byte-exact signature would break in transit constantly.

Instead, the signer chooses a list of components to cover, and both parties construct a canonical signature base from them in an agreed way. Sign that.

Components come in two kinds:

  • Header fields — ordinary headers by name: content-type, content-digest, authorization.
  • Derived components — properties of the message itself, distinguished by an @ prefix: @method, @authority, @path, @target-uri, @scheme, @query.

Derived components are the important addition. @method and @target-uri mean the signature covers what is being requested and where, not just the headers alongside it — so a captured signature cannot be replayed against a different endpoint.

How a signature is produced and checked

Both sides build the same canonical string independently. If either side's view of the message differs, verification fails — which is the point.

01

Choose components

The signer picks which headers and derived components the signature will cover, plus parameters like created and expires.

02

Build the base

Both parties serialise those components into a canonical signature base string, in the declared order.

03

Sign and send

The signer signs the base and sends Signature-Input describing the recipe, plus Signature carrying the bytes.

04

Rebuild and verify

The verifier reconstructs the base from the received message and checks the signature against the public key.

The two headers

A signed request carries both:

Signature-Input: sig1=("@authority" "@method" "@path" "signature-agent");\
  created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";\
  alg="ed25519";expires=1735693200;nonce="b3n5jxr2"
Signature: sig1=:MEUCIQ...base64...==:

Read Signature-Input as a recipe. Inside the parentheses is the ordered list of covered components. After the semicolons are the signature parameters:

  • created and expires — a validity window, in seconds since the epoch. Together they bound replay.
  • keyid — an identifier for the key needed to verify. This is what turns a signature into something you can actually check.
  • alg — the algorithm. Ed25519 is the common choice for agent traffic: small keys, small signatures, fast verification.
  • nonce — a unique value, so a verifier tracking recently-seen nonces can reject an exact replay inside the validity window.

The label (sig1 here) lets one message carry several signatures — an agent's own signature plus, say, a gateway's countersignature.

The bracketed colons are not a typo

:MEUCIQ...: is Structured Field Values syntax (RFC 8941): a byte sequence is base64 wrapped in colons. RFC 9421 is built entirely on structured fields, which is what makes canonical parsing possible in the first place. If you are hand-rolling a parser, this is where you will lose an afternoon — use a structured-fields library.

The bootstrap problem, and Web Bot Auth

RFC 9421 verifies a signature against a key. It says nothing about how you obtain that key.

For a partner integration this is fine — exchange keys out of band, done. For the open web it is the whole difficulty. An agent you have never heard of arrives at your server. Its signature references keyid=.... You have no relationship with its operator and no prior knowledge of the key.

Web Bot Auth is the IETF work addressing exactly this. Its architecture adds two things on top of RFC 9421:

A Signature-Agent header. The agent states where its keys can be found — a URL under a domain it controls. That domain becomes the identity being verified, which is the useful property: you are no longer verifying "a bot claiming to be ExampleBot", you are verifying "the holder of a key published at example.com". Domain control is something you can reason about.

A key directory. A well-known, fetchable, cacheable document listing the agent operator's public keys in a standard format. Verifiers fetch it once, cache it, and verify many requests against it.

You stop verifying a name a bot chose for itself, and start verifying control of a domain. That is a claim with consequences attached.

The result is a verification chain with no prior relationship required: signature → key → directory → domain. Every step is checkable, and the last one is anchored in something that already has real-world accountability.

Cloudflare has been the main driver here and has shipped support at scale, which matters more than the specification text — a verification standard is worth precisely as much as its deployment.

How HTTP request verification got here

  1. 1996 →

    The user agent string

    Self-declared identity with no verification mechanism. Still the primary way most systems identify bots.

  2. 2000s →

    Reverse DNS and IP allowlists

    Verify something adjacent to the request and infer the rest. Works when operators cooperate; breaks whenever infrastructure moves.

  3. Feb 2024

    RFC 9421 published

    HTTP Message Signatures becomes a standards-track RFC, giving HTTP a native way to prove request origin and integrity.

  4. 2025 →

    Web Bot Auth

    IETF work applies message signatures to automated agents, adding discoverable key directories so verification needs no prior relationship.

What verification does and does not give you

This distinction gets muddled constantly, and getting it wrong produces bad security decisions.

Verification versus authorisation

QuestionAnswered by a valid signature?
Did this request come from the holder of this key?Yes
Is that key published under a domain I can identify?Yes, with a key directory
Was the request modified in transit?Yes, for covered components
Is this a replay of an earlier request?Mostly — via created, expires and nonce
Should I serve this agent?No. Entirely your policy decision
Is the agent honest about its purpose?No. It proves identity, not intent

The last two rows are the ones that matter operationally. A verified agent is one whose identity you know. That is enormously more than you had before, and it is not permission. You can verify an agent perfectly and still decide to rate-limit it, serve it something different, or refuse it — and now those decisions can be about a known party rather than a guess.

Do not treat unsigned as hostile

Signed traffic is a small minority today and will be for some time. A verifier that blocks anything unsigned will block most legitimate traffic, including every human. Signature verification is a strong positive signal, not the absence of a negative one — it belongs on the "confidently identified" path, not the "everything else is malicious" path.

Implementation notes

If you are building a verifier, these are the parts that bite.

Canonicalisation is the whole game. Verification fails when the two sides build different base strings — from a proxy rewriting a covered header, a differing view of @authority behind a load balancer, or a mishandled structured-field parse. Log the reconstructed base on failure during development. You will need it.

Clock skew is real. created and expires are absolute timestamps. Machines disagree about the time. Allow a tolerance window measured in seconds; a zero-tolerance verifier will reject legitimate traffic and the failures will look random.

Cache the key directory aggressively, but bound it. Fetching a directory per request converts every incoming request into an outbound one — a fine denial-of-service amplifier if someone points traffic at you with a novel Signature-Agent. Cache by domain, respect cache headers, apply a floor, and rate-limit directory fetches for domains you have not seen before.

Verification is cheap; do not make it free. Ed25519 verification is fast, but "fast" is not "free" and it sits on the request path. Only attempt verification when a signature is actually present. Keep the unsigned path completely clear of the crypto.

Fail closed on verification, open on availability. If a signature is present and invalid, that is a real signal and should be treated as such. If your key directory fetch times out, that is your problem, not the agent's — do not convert your own outage into a wave of rejections.

Why this matters for attribution specifically

The connection to traffic measurement is direct.

The whole reason AI referral attribution is hard is that every available signal is weak or forgeable. The referrer is missing. The user agent is a claim. Client characteristics are statistical. You end up combining several unreliable signals and reporting a confidence score, because that is genuinely the best available.

A cryptographic signature is categorically different. It is the one signal in the entire stack that is not a guess. When an agent signs its request and the signature verifies against a key published under a domain you can name, you are not inferring anything.

Today that covers agents rather than the humans they act for, so it addresses the crawler side of the problem more than the referral side — and keeping those two apart is its own discipline. But the trajectory is clear. As more activity becomes genuinely agentic — software fetching pages, comparing options and completing transactions on someone's behalf — the fraction of traffic that can carry a proof rather than a claim only goes up.

The measurement stack of 2030 is likely to look like: verified where possible, inferred where not, with the boundary between them clearly marked. RFC 9421 is what makes the first half of that sentence possible.

Frequently asked

RFC 9421 is the IETF standard for HTTP Message Signatures, published in February 2024. It defines how a sender can cryptographically sign selected components of an HTTP message — chosen headers plus derived components like the method and target URI — and how a recipient verifies that signature, so the recipient can confirm both who sent the request and that its covered parts were not altered in transit.

Sources & further reading

  1. 01RFC 9421: HTTP Message SignaturesIETF
  2. 02RFC 8941: Structured Field Values for HTTPIETF
  3. 03Web Bot Auth Architecture (Internet-Draft)IETF Datatracker
  4. 04Message Signatures for Web BotsCloudflare Blog
  5. 05Overview of OpenAI crawlers and user agentsOpenAI
Share