Skip to content

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.

JCS rewrites a JSON document into one fixed spelling: sorted keys, shortest-round-trip numbers, minimal string escapes, no whitespace. The scoreboard at the end tallies which failures it repairs — and which it cannot.

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