{
 "slug": "cryptographic-receipt-primitive-rfc8785-ed25519-vs-jwt",
 "topic_id": "TOPIC-090",
 "cluster": "Cryptography & Standards",
 "tier": "Tier A",
 "title": "A session token is not an audit record",
 "summary": "JWTs are excellent at what they were designed for: carrying claims for minutes. Using them as decade-long evidence fails on canonicalisation, key availability and algorithm handling.",
 "lede": "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.",
 "date": "2026-03-10",
 "category": "Compliance",
 "author_id": "priya-venkatraman",
 "tags": [
  "JWT",
  "Ed25519",
  "canonicalization",
  "RFC 8785",
  "audit records",
  "cryptography"
 ],
 "image_title": "Session Token Not Audit Record",
 "schema": "Article",
 "key_takeaways": [
  "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."
 ],
 "body": [
  {
   "type": "h2",
   "text": "Three assumptions that expire"
  },
  {
   "type": "diagram",
   "kind": "chain",
   "alt": "The same token, at issue and seven years later",
   "caption": "Nothing was tampered with. The verification is simply no longer possible.",
   "nodes": [
    {
     "label": "Token issued",
     "sub": "signed",
     "note": "verifiable",
     "good": true
    },
    {
     "label": "Key rotated",
     "sub": "weeks later",
     "note": "old key unpublished",
     "bad": true
    },
    {
     "label": "Schema changed",
     "sub": "months later",
     "note": "payload unreadable",
     "bad": true
    },
    {
     "label": "Audit, year seven",
     "sub": "cannot verify",
     "note": "",
     "bad": true
    }
   ]
  },
  {
   "type": "table",
   "head": [
    "JWT assumption",
    "Holds for a session",
    "Holds for a seven-year record"
   ],
   "rows": [
    [
     "The signing key is available for verification",
     "Yes",
     "<strong style=\"font-weight:600\">No — keys rotate and old ones are purged</strong>"
    ],
    [
     "The verifier trusts the issuer's current configuration",
     "Yes",
     "<strong style=\"font-weight:600\">No — the issuer may not exist</strong>"
    ],
    [
     "The payload is interpreted by a system that knows the schema",
     "Yes",
     "<strong style=\"font-weight:600\">No — the schema will have changed</strong>"
    ],
    [
     "Algorithm negotiation is acceptable",
     "Mostly",
     "<strong style=\"font-weight:600\">No — it is an attack surface with no upside</strong>"
    ]
   ]
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "The canonicalisation problem"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "code",
   "text": "# Semantically identical, different bytes, different digest\n{\"amount\":84000,\"currency\":\"GBP\"}\n{\"currency\": \"GBP\", \"amount\": 84000}\n{\"amount\": 8.4e4, \"currency\": \"GBP\"}\n\n# RFC 8785 fixes: key ordering, number formatting,\n# string escaping and whitespace. One serialisation\n# per semantic value.\n\ndigest = sha256(JCS(statement))"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "Algorithm agility as a liability"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "What a durable receipt requires"
  },
  {
   "type": "ol",
   "items": [
    "<strong style=\"font-weight:600\">Deterministic canonicalisation.</strong> RFC 8785 or equivalent, so the digest is reproducible from the data.",
    "<strong style=\"font-weight:600\">A fixed algorithm.</strong> Not negotiated, not in the payload. Ed25519 is a reasonable choice: small signatures, no parameter choices, no nonce-reuse hazard.",
    "<strong style=\"font-weight:600\">Durable key publication.</strong> Keys published at a stable location with a documented rotation history, retained for the full retention period.",
    "<strong style=\"font-weight:600\">A self-contained statement.</strong> The record contains what was authorised, in readable form, not identifiers into systems that will change.",
    "<strong style=\"font-weight:600\">No network dependency.</strong> Verification requires the receipt and the key, nothing else."
   ]
  },
  {
   "type": "p",
   "html": "Point four is the one most often overlooked. A receipt containing <code>{\"payment_id\": 88213}</code> is worthless once that table is archived. It must contain the beneficiary, the amount and the account."
  },
  {
   "type": "h2",
   "text": "Comparing the options fairly"
  },
  {
   "type": "table",
   "head": [
    "",
    "JWT",
    "X.509 / CMS",
    "JCS + Ed25519 receipt"
   ],
   "rows": [
    [
     "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"
    ]
   ]
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "Where JWTs remain right"
  },
  {
   "type": "p",
   "html": "Everywhere they are currently used well: access tokens, identity assertions, short-lived authorisation carried between services. None of that changes."
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "Why canonicalisation is the quiet requirement"
  },
  {
   "type": "code",
   "text": "# Semantically identical, three different digests\n{\"amount\":84000,\"currency\":\"GBP\"}\n{\"currency\": \"GBP\", \"amount\": 84000}\n{\"amount\": 8.4e4, \"currency\": \"GBP\"}\n\n# RFC 8785 fixes key order, number formatting,\n# string escaping and whitespace.\ndigest = sha256(JCS(statement))"
  },
  {
   "type": "p",
   "html": "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."
  },
  {
   "type": "h2",
   "text": "Objections and honest limits"
  },
  {
   "type": "p",
   "html": "<strong style=\"font-weight:600\">“Algorithm agility is good practice.”</strong> 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."
  },
  {
   "type": "p",
   "html": "<strong style=\"font-weight:600\">“JWT tooling is everywhere and receipt tooling is not.”</strong> 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."
  }
 ],
 "faq": [
  {
   "q": "Why do old JWTs fail to verify?",
   "a": "Identity providers rotate signing keys frequently and publish only current ones. A token from years ago cannot be verified because its key is gone."
  },
  {
   "q": "Why does canonicalisation matter?",
   "a": "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."
  },
  {
   "q": "Is algorithm agility bad?",
   "a": "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."
  },
  {
   "q": "Should we stop using JWTs?",
   "a": "No. They are right for access tokens and short-lived claim transport. The error is using one as a seven-year evidentiary record."
  },
  {
   "q": "What does canonicalisation fix?",
   "a": "It makes the digest reproducible from the data, so a verifier reconstructing the statement later computes the same value."
  }
 ],
 "sources": [
  {
   "t": "RFC 7519 — JSON Web Token",
   "u": "https://www.rfc-editor.org/rfc/rfc7519"
  },
  {
   "t": "RFC 8785 — JSON Canonicalization Scheme",
   "u": "https://www.rfc-editor.org/rfc/rfc8785"
  },
  {
   "t": "RFC 8037 — CFRG elliptic curve signatures for JOSE",
   "u": "https://www.rfc-editor.org/rfc/rfc8037"
  },
  {
   "t": "RFC 8032 — Edwards-curve Digital Signature Algorithm (EdDSA)",
   "u": "https://www.rfc-editor.org/rfc/rfc8032"
  }
 ],
 "related": [
  {
   "slug": "building-manav-verify-write-open-source-offline-verifier",
   "title": "Writing an offline verifier with no dependencies",
   "category": "Compliance"
  },
  {
   "slug": "federal-rules-evidence-rule-902-13-14",
   "title": "Self-authenticating electronic records",
   "category": "Compliance"
  },
  {
   "slug": "fail-closed-cryptographic-key-management-implementing-dual-key",
   "title": "Dual-key rotation windows",
   "category": "Compliance"
  },
  {
   "slug": "zero-knowledge-fallacy-enterprise-approvals-plaintext-statements",
   "title": "Zero-knowledge proofs and the audit that needs to see the data",
   "category": "Compliance"
  }
 ],
 "image": "https://cdn.twc.sh/images/igcache/Session%20Token%20Not%20Audit%20Record/1500_900/blog.jpg",
 "wordcount": 901,
 "url": "/blog/cryptographic-receipt-primitive-rfc8785-ed25519-vs-jwt.html",
 "reading_time": "4 min read",
 "meta_description": "JWTs carry claims for minutes, which is what they were built for. As decade-long evidence they fail on canonicalisation and key availability.",
 "hub": {
  "slug": "topics/receipt-cryptography",
  "title": "Receipt cryptography and standards"
 },
 "answer": "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.",
 "answer_q": "Why isn't a JWT a good audit record?",
 "glossary": [
  {
   "term": "Canonicalisation",
   "def": "Reducing structured data to one deterministic serialisation so the same facts always produce the same digest."
  },
  {
   "term": "Algorithm agility",
   "def": "Carrying the signing algorithm in the message, so a verifier reads it from data the sender controlled."
  },
  {
   "term": "Self-contained statement",
   "def": "A record holding the facts rather than references to systems that will change."
  }
 ],
 "checklist": {
  "title": "What a durable receipt requires",
  "id": "durable",
  "desc": "Five properties.",
  "steps": [
   {
    "name": "Deterministic canonicalisation.",
    "text": "So the digest is reproducible from the data."
   },
   {
    "name": "A fixed algorithm.",
    "text": "Not negotiated, not carried in the payload."
   },
   {
    "name": "Durable key publication.",
    "text": "Old keys retained for the full retention period."
   },
   {
    "name": "A self-contained statement.",
    "text": "Beneficiary, amount, account — not an identifier into a table that will be archived."
   },
   {
    "name": "No network dependency.",
    "text": "The receipt and the key, nothing else."
   }
  ]
 },
 "cta": {
  "title": "Where this fits in Manav",
  "html": "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.",
  "href": "../architecture.html",
  "label": "Read the architecture"
 }
}