A session token is not an audit record
The temptation is obvious. You already issue signed tokens, so store one as the record of an approval. Five years later the signing key is gone, the serialisation has changed, and the token proves nothing.
Why isn't a JWT a good audit record?
Because three of its operating assumptions expire. The signing key will have rotated and been purged, the issuer's configuration will have changed, and the payload schema will have moved on. A token built to carry claims for fifteen minutes is being asked to hold evidence for seven years.
- JWTs are designed for short-lived claim transport, and their operational assumptions do not hold over audit retention periods.
- Without a canonical serialisation, semantically identical data produces different digests, which breaks verification for reasons unrelated to security.
- A long-lived receipt needs deterministic canonicalisation, a fixed algorithm, durable published keys and a self-contained statement.
Part of Receipt cryptography and standards
Three assumptions that expire
| JWT assumption | Holds for a session | Holds for a seven-year record |
|---|---|---|
| The signing key is available for verification | Yes | No — keys rotate and old ones are purged |
| The verifier trusts the issuer's current configuration | Yes | No — the issuer may not exist |
| The payload is interpreted by a system that knows the schema | Yes | No — the schema will have changed |
| Algorithm negotiation is acceptable | Mostly | No — it is an attack surface with no upside |
The first row is the one that bites first and hardest. Identity providers rotate signing keys on a schedule measured in weeks and publish only current keys. A token signed three years ago cannot be verified because the key that signed it is no longer anywhere.
The canonicalisation problem
JSON permits variation that does not change meaning: key order, whitespace, number formatting, string escaping. A digest over the serialised bytes is sensitive to all of it.
# Semantically identical, different bytes, different digest
{"amount":84000,"currency":"GBP"}
{"currency": "GBP", "amount": 84000}
{"amount": 8.4e4, "currency": "GBP"}
# RFC 8785 fixes: key ordering, number formatting,
# string escaping and whitespace. One serialisation
# per semantic value.
digest = sha256(JCS(statement))
Without this, a verifier that reconstructs the statement from a database — which is what happens years later — computes a different digest and the verification fails for formatting reasons. The signature was fine; the comparison was impossible.
Algorithm agility as a liability
JWT carries the signing algorithm in the header, which means the verifier reads it from data the sender controlled. This has produced a well-documented class of implementation errors, including accepting tokens that assert no algorithm at all.
Defences exist and are widely implemented. The point for a long-lived record is different: agility is a feature with a cost and no benefit here. Fixing the algorithm removes the class entirely.
What a durable receipt requires
- Deterministic canonicalisation. RFC 8785 or equivalent, so the digest is reproducible from the data.
- A fixed algorithm. Not negotiated, not in the payload. Ed25519 is a reasonable choice: small signatures, no parameter choices, no nonce-reuse hazard.
- Durable key publication. Keys published at a stable location with a documented rotation history, retained for the full retention period.
- A self-contained statement. The record contains what was authorised, in readable form, not identifiers into systems that will change.
- No network dependency. Verification requires the receipt and the key, nothing else.
Point four is the one most often overlooked. A receipt containing {"payment_id": 88213} is worthless once that table is archived. It must contain the beneficiary, the amount and the account.
Comparing the options fairly
| JWT | X.509 / CMS | JCS + Ed25519 receipt | |
|---|---|---|---|
| Designed for | Short-lived claim transport | Document and code signing | Long-lived action records |
| Canonicalisation | None specified | Defined for the format | RFC 8785 |
| Algorithm handling | Negotiated in the header | Negotiated | Fixed |
| Key availability over years | Poor in practice | Good — chains are archivable | Good, if published durably |
| Signature size | Moderate | Large | Small |
| Tooling ubiquity | Very high | High | Lower |
The last row is a genuine disadvantage worth acknowledging. JWT libraries exist everywhere and receipt tooling does not; the counter is that a verifier for this format is small enough to audit, which is the subject of a separate discussion.
Where JWTs remain right
Everywhere they are currently used well: access tokens, identity assertions, short-lived authorisation carried between services. None of that changes.
The mistake is a category error rather than a technology failure. A token that expires in fifteen minutes is being asked to serve as evidence for seven years, and the properties required are different in every dimension.
Why canonicalisation is the quiet requirement
# Semantically identical, three different digests
{"amount":84000,"currency":"GBP"}
{"currency": "GBP", "amount": 84000}
{"amount": 8.4e4, "currency": "GBP"}
# RFC 8785 fixes key order, number formatting,
# string escaping and whitespace.
digest = sha256(JCS(statement))
Without it, a verifier reconstructing the statement from a database years later computes a different digest and the check fails for formatting reasons. The signature was fine; the comparison was impossible.
Objections and honest limits
“Algorithm agility is good practice.” For a protocol negotiating between parties, yes. For a fixed-purpose long-lived record it is a feature with a cost and no benefit, and it has produced a documented class of implementation errors.
“JWT tooling is everywhere and receipt tooling is not.” A genuine disadvantage. The counter is that a verifier for this format is small enough to audit, and a format whose verifier can be reimplemented is one where a dispute can be settled by both sides writing their own.
What a durable receipt requires
- Deterministic canonicalisation. So the digest is reproducible from the data.
- A fixed algorithm. Not negotiated, not carried in the payload.
- Durable key publication. Old keys retained for the full retention period.
- A self-contained statement. Beneficiary, amount, account — not an identifier into a table that will be archived.
- No network dependency. The receipt and the key, nothing else.
Terms used here
- Canonicalisation
- Reducing structured data to one deterministic serialisation so the same facts always produce the same digest.
- Algorithm agility
- Carrying the signing algorithm in the message, so a verifier reads it from data the sender controlled.
- Self-contained statement
- A record holding the facts rather than references to systems that will change.
Frequently asked questions
Why do old JWTs fail to verify? Identity providers rotate signing keys frequently and publish only current ones. A token from years ago cannot be verified because its key is gone.
Why does canonicalisation matter? JSON permits key ordering, whitespace and number formatting variation. A verifier reconstructing the statement later computes a different digest and verification fails for formatting reasons.
Is algorithm agility bad? It is a feature with a cost and no benefit for a fixed-purpose long-lived record. Fixing the algorithm removes a whole class of implementation error.
Should we stop using JWTs? No. They are right for access tokens and short-lived claim transport. The error is using one as a seven-year evidentiary record.
What does canonicalisation fix? It makes the digest reproducible from the data, so a verifier reconstructing the statement later computes the same value.
Where this fits in Manav
Manav's receipt is deliberately small: RFC 8785 canonicalisation, one signature algorithm, keys published at a well-known URL, and a verifier short enough to audit or reimplement.