HTTP Message Signatures became an IETF standard in February 2024 as RFC 9421. It defines a way to produce a cryptographic signature over selected parts of an HTTP request or response, and to carry that signature in two headers so the recipient can verify it. The concept is not new. For years, projects used the earlier Cavage draft, and services like Amazon's request signing and Mastodon's federation solved the same problem with bespoke schemes. RFC 9421 is the standardized, carefully specified version of that idea.
The gap TLS leaves open
Consider a payment request that flows from a client through a CDN, then an API gateway, then to the backend service that actually moves money. TLS protects each of those three legs separately. The CDN and the gateway both decrypt the request, look at it, and re-encrypt it for the next hop. This is by design; a gateway that could not read the request could not route it.
The consequence is that the backend has no cryptographic assurance about who originally sent the request or whether an intermediary altered it. It has to trust every hop in the chain. For most traffic that is fine. For a request that authorizes a transaction, that is a lot of trust spread across infrastructure the backend does not control. This is the same intermediary-visibility problem that TLS termination at a CDN raises for privacy, viewed from the integrity angle.
TLS secures the pipe. A message signature secures the letter. If the letter is signed, it does not matter how many post offices it passes through: the recipient can verify it came from the stated sender and was not edited in transit. RFC 9421 makes the HTTP request the letter.
How a signature is built
The signer does not sign the raw bytes of the request. Raw HTTP is too malleable, since proxies legitimately reorder headers, change capitalization, and rewrite whitespace. Instead, RFC 9421 defines a canonical form. The signer picks which components to cover, assembles them into a well-defined string called the signature base, and signs that.
Components come in two kinds. Ordinary header fields are referenced by name. Special values about the request itself are called derived components and are written with a leading @ symbol:
- @method: the request method, such as POST
- @authority: the host and port
- @path: the request path
- @target-uri: the full request URI
- content-digest: a hash of the body, so the payload is covered too
By covering the method, path, authority, and a digest of the body, the signature binds the request's meaning. An attacker cannot change POST to DELETE, redirect the path, or swap the body without invalidating the signature. Signing a hash of the body rather than the body itself, using the Content-Digest header from RFC 9530, keeps the signing input small while still protecting the payload.
The two headers
A signed request carries two related header fields. Signature-Input declares what was signed and the signature's parameters. Signature carries the actual signature bytes, keyed to the same label. A single request can carry more than one signature, each with its own label, which is how a gateway can add its own signature without disturbing the client's.
Signature-Input names the ingredients and the recipe. Signature is the result. A verifier reads Signature-Input to reconstruct the exact signature base from the request it received, then checks that the bytes in Signature validate against it with the identified key. The two-header split, in plain terms
The parameters inside Signature-Input are where much of the security lives:
| Parameter | What it does |
|---|---|
| keyid | Identifies which key the verifier should use |
| alg | The signature algorithm, such as ed25519 or ecdsa-p256-sha256 |
| created | A timestamp for when the signature was made |
| expires | When the signature should stop being accepted |
| nonce | A unique value to detect and reject replays |
| tag | An application-specific label describing the signature's purpose |
Replay protection, which TLS does not give you either
A captured, validly signed request could otherwise be replayed. Signing "transfer 100 dollars" once and then sending it ten times would be a serious flaw. RFC 9421 addresses this with the created, expires, and nonce parameters. A verifier can reject signatures that are too old, refuse ones past their expiry, and remember recently seen nonces to reject duplicates. Because those parameters are inside the signed base, an attacker cannot alter them to sidestep the check. The signature covers its own freshness guarantees.
Choosing an algorithm
RFC 9421 supports several signature algorithms, and the choice is a real decision. Asymmetric algorithms such as Ed25519, ECDSA over P-256, and RSA-PSS let the verifier hold only a public key, which is the right model when many parties verify signatures from a signer they do not share a secret with. HMAC, a symmetric option, is faster and simpler but requires the two parties to share a secret key, which suits closed service-to-service links. The elliptic curve options are generally preferred for new designs on size and speed grounds.
Never let the verifier blindly trust the alg parameter from the request to decide how to check the signature. A verifier that accepts whatever algorithm the message claims can be tricked into downgrading to a weaker one, or into treating a public key as an HMAC secret. Pin the expected algorithm to the key, identified by keyid, on the verifier side. This is the same class of mistake that produced the well-known JWT algorithm-confusion vulnerabilities.
Where it is actually used
HTTP Message Signatures show up wherever integrity has to outlast the connection. Open banking and payment APIs use request signing so a transaction authorization is provably intact end to end. The GNAP authorization protocol uses it to bind requests to a key. Federated social systems sign server-to-server requests so one instance can verify another. Any design where a request crosses trust boundaries, and the endpoint needs to know the request was not tampered with along the way, is a candidate.
What it does not replace
Message signatures are an integrity and authenticity tool, not a confidentiality one. A signed request is not a secret; anyone who sees it can read it. You still want TLS for confidentiality on every hop. The two compose cleanly: TLS keeps intermediaries from reading the traffic they should not, and the signature keeps them from tampering with the parts they can read. Signing also does not manage keys for you. It assumes a key distribution and rotation story already exists, which for many teams is the harder half of the work.
Where Haven fits
We build for a threat model where infrastructure between two parties should not have to be trusted, which is the same instinct RFC 9421 encodes. Our messaging protects content end to end so that servers relaying it cannot read or alter it, using MLS for group chat and PGP for email. HTTP Message Signatures solve the adjacent problem for the request layer: proving that a request means what it says regardless of how many hops it survived. Both come from the same principle, that a security guarantee should belong to the message, not to the fragile connection carrying it.