UCP Message Signatures: How Every Request Proves Who Sent It (RFC 9421)
Two posts ago, an AI agent proved its identity to a store. One post ago, a store proved an order update back to the agent. Both times, the same three words did the actual work: signed HTTP request. This post is about those three words — the UCP message signature — because it's the one mechanism every other trust claim in the protocol rests on.
Strip it away and the whole thing collapses. An agent profile (ours lives at /.well-known/ucp-agent) is just a JSON file anyone can copy. A UCP-Agent header is just a string anyone can set. What makes them mean something is that the request carrying them is signed — cryptographically bound to a key that only the real sender holds. UCP message signatures are how a stranger's request becomes a named party's request. Here's how the spec builds that, and the one detail that trips up almost every first implementation.
What a signature actually defends against
UCP's signing model (built on RFC 9421, HTTP Message Signatures) exists to stop four specific attacks, and it's worth naming them because each one shapes a design choice:
- Impersonation — someone claims to be a platform (or a store) they aren't. Defeated by signing with a key the profile publishes.
- Tampering — a request is modified in flight (change the shipping address, swap the cart). Defeated by signing a digest of the body.
- Replay — a captured-but-valid request is re-sent later. UCP defeats this at the business layer, not in the signature: every state-changing request carries an
Idempotency-Key, the key is covered by the signature, and a replayed key returns the cached response instead of re-executing. (The RFC 9421createdtimestamp is explicitly optional in UCP — idempotency keys, not timestamps, are the replay defense.) - Method/endpoint confusion — a signature valid for one request is reused on another (a signed
GETreplayed as aPOST, or pointed at a different path). Defeated by signing the method and target URI themselves.
The signature isn't a checkbox. It's a precise statement: "the holder of this key sent exactly this method, to exactly this URI, with exactly this body, as exactly this identity." Everything below is how that sentence gets encoded.
What gets signed
RFC 9421 doesn't sign the raw bytes of a request. It builds a signature base — an explicit, ordered list of covered components — and signs that. The components vary by context, but for UCP they're drawn from:
@method— the HTTP verb@authority+@path— where the request is going (the spec's component table requires these two by name, rather than the equivalent@target-uri)@query— the query string, when the request has onecontent-digest— a hash of the body (more on this next)content-type— so the declared body type can't be swappeducp-agent— the advertised identity (which profile to resolve keys from)idempotency-key— required coverage on state-changing requests (POST, PUT, PATCH, DELETE); this is the replay defense from the list above@status— for signed responses, the status code
The spec's canonical request example covers @method @authority @path ucp-agent idempotency-key content-digest content-type; the order-webhook example covers @method @authority @path content-digest content-type. Signature-Input always names the exact set, so verifier and signer build identical bases.
A verifier reconstructs the same base from the request it received and checks the signature against it. If even one covered component differs — a flipped method, a changed path, a tampered body — the base no longer matches and the signature fails. The Signature-Input header is what makes this deterministic: it names exactly which components were covered and in what order, so both sides build byte-identical bases.
Body integrity: Content-Digest, not the body
You'll notice content-digest is signed, not the body itself. That's deliberate, and it's where a subtle interop rule lives.
Per RFC 9530, the sender computes a SHA-256 hash of the raw request bytes and puts it in the Content-Digest header; the signature then covers that header. The rule that matters: it's a hash of the raw bytes, with no JSON canonicalization. Don't re-serialize, re-order keys, or pretty-print before hashing — hash the exact bytes you put on the wire. A verifier recomputes SHA-256 over the body it received and compares. Match means the body is intact; mismatch means tampering (or a serialization bug on the sender's side, which in practice is the more common cause).
Signing the digest instead of the body keeps the signature base small and lets the verification of "is the body intact?" and "is the signature valid?" be two clean, separable checks.
Where the keys come from
A signature is only as good as the verifier's ability to find the right public key — without any prior key exchange. UCP solves this with the profile, and it's the exact mechanism the agent profile post described:
- The request carries a
UCP-Agentheader (RFC 8941 dictionary) pointing at the sender's profile URL. - The verifier fetches that profile and reads its
signing_keysarray — public keys in JWK format (RFC 7517). - The
Signature-Inputnames akeyid. The verifier matches it to akidinsigning_keys[]. - It verifies the signature with that key.
The spec doesn't dictate how a kid is chosen — its own examples use plain names like platform-2026. One practice we recommend (and use ourselves): make the kid the key's RFC 7638 JWK thumbprint — a hash derived from the key material itself, not a name someone typed. Because the identifier is computed from the key, the kid a verifier matches on can never silently drift from the key it points to. Publish a new key, get a new kid, for free.
The algorithm — and the one bug everyone ships first
UCP signatures use ECDSA on the P-256 curve: ES256 is mandatory, ES384 (P-384) is an optional stronger tier. Standard, boring, well-supported.
And then there's the trap. RFC 9421 requires ECDSA signatures to use fixed-width raw r||s encoding — the two 32-byte integers concatenated — not ASN.1/DER. This matters because most crypto libraries (OpenSSL, Java, Go, Node's crypto, Python's cryptography) hand you a DER-encoded signature by default. If you sign with the default output and ship it, your signature is well-formed, looks correct, and fails every spec-compliant verifier — because they're expecting raw r||s and getting DER. It is, by a wide margin, the single most common UCP signing bug. If your signatures verify locally but fail against someone else's stack, check this first: decode the DER, take r and s, left-pad each to 32 bytes, concatenate. Done.
Who signs what
UCP's signing obligations are intentionally asymmetric:
- Platforms SHOULD sign their requests. An agent calling a store's checkout should sign, so the store can identify and trust it — but the spec permits alternative authentication (API keys, OAuth, mTLS) in its place where the parties have agreed on one.
- Businesses MUST sign their webhooks. When a store pushes an order update back to a platform, that delivery must be signed. The reasoning is sharp: a webhook is an unsolicited, state-changing message arriving out of band. Without a signature there's nothing stopping anyone from POSTing a fake "order shipped" to a platform's endpoint. So the spec hardens the one direction where the receiver didn't initiate the conversation.
That asymmetry is the whole webhooks post in one line: the order webhook is the place a SHOULD becomes a MUST.
How this ties the trilogy together
Read together, the three pieces are one system:
- The agent profile is where the keys live — the published
signing_keysthat make verification possible without prior key exchange. - Order webhooks are signatures pointed the other way — the store signing, the platform verifying.
- Message signatures — this post — are the engine underneath both: the covered components, the digest, the key resolution, the algorithm.
Profile without signatures is an unlocked door with a nameplate. Signatures without a profile are a locked door with no key handout. UCP needs both, and the spec wires them together through the single UCP-Agent → signing_keys → kid path.
How Playground implements it
On its outbound calls to a store, Playground signs per RFC 9421 with ES256 / P-256 — method, target URI, body digest and agent identity covered — and publishes the verifying key as a signing_keys JWK set in its agent profile, kid as the RFC 7638 thumbprint. In the other direction, it verifies the RFC 9421 signatures on inbound order webhooks before accepting them. Same engine, both directions: sign what we send, verify what we receive.
Why it matters
For merchants, signatures are what let you treat agent traffic as named rather than anonymous — different rate limits, different inventory, different payment methods for parties you can cryptographically identify. For platforms, signing is the price of being trusted at all. And for the ecosystem, a shared, RFC-based signing model is what stops every platform-merchant pair from inventing its own bespoke auth — the thing that has historically made integrations slow and brittle.
It's the least glamorous part of UCP and the part everything else depends on. Get the r||s encoding right and the rest is just plumbing.
Try it
See our keys: ucpplayground.com/.well-known/ucp-agent — the signing_keys a verifier resolves
Read the spec: ucp.dev/specification/signatures · RFC 9421 · RFC 9530
Check your store: ucpchecker.com/check
Related reading: This completes a three-part series on how UCP makes agent commerce trustworthy — the agent profile (where keys live), order webhooks (signatures in reverse), and message signatures (the engine). For the buyer-identity layer that sits on top, see UCP identity linking.
About UCP Checker
UCP Checker is the independent validation and monitoring layer for the Universal Commerce Protocol. We crawl, validate and grade every public UCP profile we can find, run the merchant directory, the UCP Score and live adoption stats, and — through UCP Playground — test how real AI agents behave against real stores. Signed requests are how those agents prove who they are.
- See our agent profile: ucpplayground.com/.well-known/ucp-agent
- Check your store: ucpchecker.com/check
- Grade it (UCP Score): ucpchecker.com/score
- Browse the directory: ucpchecker.com/directory
- Get notified on changes: ucpchecker.com/alerts
Check your domain's UCP status
See if your storefront is ready for agentic commerce in seconds.
Get the agentic commerce digest every Monday
Real adoption data, ecosystem trends, new spec versions, and the stores that broke or recovered this week. Read by founders and engineers building the next generation of commerce.

