Field Note 7 current
Sign the message, not just the session (HTTP Message Signatures)
TL;DR
A bearer credential authenticates the connection or the caller, but says nothing about the
request it’s attached to — GET /me and POST /transfer?amount=all carry the identical token.
Sign the message itself with HTTP Message Signatures (RFC 9421)RFC 9421 — HTTP Message SignaturesSpecifies how to sign and verify chosen components of an HTTP message — method, target URI, selected headers, a content digest — so a recipient can prove who sent a specific request or response and that it wasn't altered in transit. Supports both shared-key (HMAC) and asymmetric signatures.rfc-editor.org ↗:
the sender signs a chosen set of components (method, path, key headers, a digest of the body) so the
recipient can prove who sent this exact message and that not a byte changed in flight. Sign
requests, and ideally responses too — a signed response lets the client prove the answer
really came from the server and wasn’t tampered with on the way back.
Don’t make PKI a prerequisite. A shared symmetric key (HMAC) is a perfectly good first step and far better than nothing; asymmetric keys are the better end-state (the verifier can’t also forge). Start where you can operate well and climb.
I built tooling for this — a library across several languages and a browser playground — see Sign the Request, Not the Session.
Context
Almost everything authenticates with a bearer token — a cookie, a JWT, an API key. The token grants access to whoever holds it, and crucially it is unbound from the request: it travels unchanged regardless of what you’re actually asking the server to do. Two different problems follow from that:
- Replay. If the token leaks (a log, a proxy, a backup), it’s reusable from anywhere. Sender constraint via DPoP addresses this by binding the token to a holder key.
- Integrity and provenance of the message. Even with a valid credential, the recipient can’t prove that this specific request — this method, path, headers, and body — is what the legitimate sender intended, or that an intermediary didn’t alter it. A bearer token says nothing about the bytes.
Signing the message solves the second problem directly, and (because the signature covers a nonce/timestamp and the request line) substantially mitigates the first as well. It’s the same idea I keep coming back to: bind the proof to what is actually being said, not to a long-lived secret that says nothing about the conversation.
Recommendation
Sign the message with HTTP Message Signatures (RFC 9421) wherever message provenance and integrity matter — service-to-service calls, webhooks you send (and receive), anything passing through intermediaries you don’t fully trust. Concretely:
- Cover the parts that matter. Sign the method, the target URI, the security-relevant headers,
and a content digest of the body (RFC 9421 pairs with the
Content-Digestheader). The signature base is explicit about what’s covered, so both sides agree on exactly what’s protected. - Sign requests, and ideally responses. Request signing lets the server trust the caller and the call. Response signing lets the client prove the response genuinely came from the server and arrived intact — valuable for webhooks, financial responses, and anything a client will act on or store as authoritative. Treat response signing as the goal, not an afterthought.
- Include anti-replay material. A timestamp (
created) and/or nonce in the signature parameters, with a bounded acceptance window, so a captured signed message can’t be replayed indefinitely. - Choose the key model by what you can operate — don’t force PKI.
- Shared symmetric key (HMAC) — the fine first step. Sender and verifier share a secret. Simple, no certificate machinery, and a big improvement over an unbound bearer token. Its limit: anyone who can verify can also sign (they hold the same secret), so a verifier compromise is a forging compromise. Scope and rotate shared keys accordingly.
- Asymmetric — the better step. The signer holds a private key; verifiers hold only the public key. A verifier (or recipient) compromise can’t forge new signed messages. This is where you want to end up for anything crossing a trust boundary.
- Distribute public keys by whatever you already trust (a provisioned key, a key rooted in a KMS, or a platform identity service — see ZFN-5); a JWKS endpoint is one option, not a requirement.
- Use a real implementation; don’t roll your own canonicalization. The signature base construction is where homegrown attempts go wrong. Use a library. (I wrote one — see below.)
Consequences
Easier:
- The recipient can prove provenance and integrity of a specific message, not just possession of a credential. Tampering in flight is detectable.
- Response signing gives clients the same guarantee in reverse — useful anywhere the answer is acted on or stored as authoritative.
- A shared-key start means you can adopt it without standing up PKI; the same wire format upgrades to asymmetric later.
- Pairs naturally with short-lived platform identity tokens (ZFN-5) and with DPoP (ZFN-6).
Harder:
- Both sides need the signing/verification logic and agreed covered components; a mismatch in the signature base is a frustrating class of bug. A good library removes most of this.
- Signing adds per-message work, and signed components must survive intermediaries (proxies that rewrite headers can break a signature unless covered components are chosen with that in mind).
- Shared-key signing only raises the bar so far: a holder of the shared secret can forge. If that matters, you’re at the asymmetric rung, not the first one.
- Key management is now yours to run (rotation, distribution) whichever rung you pick.
References
- RFC 9421 — HTTP Message Signatures; RFC 9530 — Digest Fields (
Content-Digest). - Sign the Request, Not the Session — my write-up, a multi-language library, and a browser playground for signing/verifying.
- ZFN-6 — sender-constrained tokens (DPoP): bind the token to a key rather than signing the message. Complementary.
- ZFN-5 — where the signing identities/keys can come from.
Changelog
- 2026-06-12: First published as a Field Note.