Signed Bytes
Canonicalization · JCS RFC 8785 · Ed25519 RFC 8032
Runs real Ed25519 signatures over JSON byte strings so you can watch two documents that mean the same thing verify differently — and one validly-signed document mean two different things.
Start here
What is this?
A digital signature never signs an object, a meaning, or a document — it signs one exact string of bytes. Yet almost everything we sign (JSON above all) has many byte spellings that carry the same meaning: reordered keys, respelled numbers, differently-composed Unicode. Every layer that re-encodes a message between signing and verifying is therefore a chance for the bytes to change while the meaning doesn't — or, worse, for the meaning to change while the bytes don't.
Below, you cause each failure yourself against a real signer and a real verifier. The two lights you'll see are deliberately separate: Signature is the raw answer the Ed25519 verifier returned about bytes; Verdict is what actually happened to the system. The whole lab is the gap between them.
Teaching demo, not production crypto. Everything cryptographic here is real and runs in your browser: Ed25519 per RFC 8032 (@noble/ed25519, strict verification), SHA-256, a hand-rolled RFC 8785 canonicalizer, and a hand-rolled RFC 8259 parser — nothing is simulated. Keys are generated fresh per page load and live only in memory. What this page does not prove: anything about key management, transport security, or the security of Ed25519 itself.
Exhibit 1 — the headline mechanism
One document, two byte strings
A producer signs an invoice, a well-meaning gateway re-serializes it in transit (same data, tidier form), and the receiver verifies. Step through what actually happens to the bytes.
Exhibit 2 — byte-diff sandbox
Same meaning, different bytes
Edit either document. Both mean the same thing to a JSON parser — but watch the UTF-8 bytes, the SHA-256 digests, and a signature made over the left document diverge from the right one.
Stage 1 — key order
Reordered keys, broken signature
{"a":1,"b":2} and {"b":2,"a":1} are the same object to every JSON parser. Sign one,
verify the other.
Stage 2 — Unicode normalization
Two spellings of é
Unicode lets the same rendered text be encoded with different code points: precomposed é (U+00E9, NFC) or e plus a combining accent (U+0301, NFD). Your screen cannot tell them apart. The signature can.
Stage 3 — duplicate keys (the centerpiece)
One valid signature, two meanings
RFC 8259 §4 only says object keys should be unique — what a parser does with
{"role":"user","role":"admin"} is implementation-defined. Some keep the first value, some the
last. Here the verifier's policy check parses first-wins, the application parses last-wins
(like JSON.parse), and the signature over the exact bytes stays valid the whole time.
Nothing in this stage forges or even touches the signature.
Stage 4 — number spellings
1, 1.0, 1e0 — one float, many byte strings
JSON's grammar distinguishes 1, 1.0, 1e0, and even
1.0000000000000001 — but a float64 parser recovers exactly the same number from all four. Any
hop that parses and re-serializes silently rewrites the spelling, and the signature notices.
Stage 5 — the signature boundary
Where does the signature attach?
Every received message crosses the same pipeline: wire bytes are parsed, NFC-normalized, and re-serialized in canonical form. Drag the signature boundary between the taps, flip on in-transit re-encodings, and watch the real verifier's tolerance change — then read the full measured matrix. Everything the signature no longer notices is a byte surface an attacker controls without detection; everything it still notices will also break for every honest middlebox.
The tally
What JCS fixes — and what it cannot
This table is computed live by re-running every stage above with canonicalization on (nothing here is asserted by hand).
Honest scope
What this lab isn't
- Not a JSON parser conformance suite — two duplicate-key policies are implemented because the divergence is the lesson, not to catalogue parsers.
- Not about JWS/JWT algorithm confusion — signing tokens has its own failure modes; see the sibling demo JWT Forge.
- Not about Ed25519's internals or verifier malleability — see Ed25519 Forge for the curve, determinism, and the ZIP215 story.
- No CBOR/COSE — deterministic CBOR encoding (RFC 8949 §4.2) is the binary-format answer to this whole problem class; it is named here as the alternative, not implemented.
- No X.509/DER canonicalization — same disease, different serialization, out of scope.