Skip to main content
JCS canonicalisation answers one question: given an object, what are its exact canonical bytes. Anchoring a signed receipt raises a second question that canonicalisation alone does not answer: Given a signed token, which bytes do you hash? This matters because two parties can both be perfectly JCS-conformant and still disagree on the anchor. Nothing in RFC 8785 is violated. One side simply hashed a different thing.

The rule

Anchor the raw signed bytes. Never re-serialise, re-canonicalise, or round-trip a decoded payload before hashing it. Canonicalisation applies to unsigned objects; a signed token is already a byte string, and those are the bytes the signature covers.

Why re-canonicalising breaks the bind

A compact JWS is header.payload.signature. The payload is base64url of some JSON. It is tempting to decode that payload, canonicalise it, and hash the result, since the object is right there and JCS gives a canonical form. That produces a different digest, and the record stops recomputing:
  • The signature covers header.payload as literal ASCII. Re-canonicalising throws away the exact bytes that were signed.
  • The issuer’s serialiser and your canonicaliser need not agree on key order, whitespace, or number form. JCS fixes that for the canonical form, but the token was not necessarily produced in canonical form to begin with.
  • Anything the canonicaliser normalises, such as 1.0 becoming 1, silently changes the preimage.
The jws_anchor_v1 set pins this as a hard negative: for the same mandate, sha256(JCS(decoded_payload)) and sha256(raw_signed_bytes) are recorded as two distinct values with an invariant asserting they must never be equal.

SD-JWT: anchor the issuer JWT, not the presentation

Selective disclosure makes the failure sharper. An SD-JWT is an issuer JWT followed by tilde-separated disclosures:
A holder presenting a subset produces different bytes from the issuance form, even though the credential and its signature are unchanged. So the three forms give three different digests. Measured values from the set: Anchoring a presentation therefore binds your record to one particular disclosure subset. Present a different subset later and the anchor no longer matches, even though nothing was tampered with. The issuer-JWT anchor is disclosure-invariant: it is stable across every presentation derived from that credential, which is what an anchor is supposed to be.

Where this compounds with canonicalisation

The worst case is a payload that is itself canonicalisation-sensitive. jws_anchor_v1 includes one deliberately: its payload carries U+2028 and the 1.0 integral-float form, both jcs_edge_v1 classes. For that vector, the re-canonicalised digest diverges from the signed-bytes digest specifically because of the canonicalisation subtlety. The anchoring bug is worst exactly where the canonicalisation edge cases live, which is why the two sets are siblings.

Conformance set

jws_anchor_v1 pins 6 vectors and 4 invariants. Signing uses the RFC 8032 section 7.1 Test 1 Ed25519 keypair, and EdDSA is deterministic, so a given signing input always yields byte-identical tokens. Anyone regenerates the exact tokens and anchors rather than taking the published values on trust. The four invariants:

I1 recanon diverges

Re-canonicalising a decoded payload never reproduces the signed-token anchor.

I2 disclosure-invariant

The issuer-JWT anchor is stable across disclosures; presentation, issuance and issuer JWT are three distinct digests.

I3 signatures verify

Every signed token verifies under the RFC 8032 section 7.1 public key, in all eight crypto-capable implementations.

I4 canon-attributable

The canon-sensitive divergence is attributable to a jcs_edge_v1 case: literal U+2028 and 1.0 rendered as 1.

How it is validated, stated as two populations

Five of the six vectors are signed_bytes rather than canonicalisation vectors, so the set is deliberately not reported as a single uniform grid: Elixir and Kotlin participate in the JCS side only and are not claimed for Ed25519 verification. Full matrix with library and version detail: ten-implementation attestation.

Checklist for implementers

1

Buffer the raw bytes

Keep the token exactly as received. Do not parse-and-reserialise before hashing.
2

Pick the anchor form deliberately

For SD-JWT, anchor the issuer JWT (before the first ~) unless you specifically intend to bind one disclosure subset.
3

Canonicalise only unsigned objects

JCS is the rule for objects you are hashing directly, not for payloads lifted out of a signed token.
4

Verify against the vectors

Run jws_anchor_v1 in your language. If your verifier re-canonicalises anywhere, vector 002 will catch it.

JCS Canonicalisation Substrate

The canonicalisation floor this anchoring rule sits on top of.

Conformance vector sets

The full corpus, including jws_anchor_v1 and its cross-implementation matrix.
Apache-2.0. The set and its runners are AlgoVoi-authored.