> ## Documentation Index
> Fetch the complete documentation index at: https://docs.algovoi.co.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Signed-Token Anchoring

> Which bytes you hash when anchoring a signed receipt: anchor the raw signed JWS or SD-JWT bytes, never a re-canonicalised decoded payload.

[JCS canonicalisation](/canonicalisation-substrate) 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

<Note>
  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.
</Note>

```
signed token (JWS / SD-JWT)   anchor = sha256(raw token bytes)
unsigned object              anchor = sha256(JCS(object))
```

## 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:

```
issuance form     <JWT>~<disclosure-1>~<disclosure-2>~
presentation      <JWT>~<disclosure-1>~
```

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:

| What you hash                             | Digest        |
| ----------------------------------------- | ------------- |
| issuer JWT (segment before the first `~`) | `98f1d108...` |
| presentation `JWT~d1~`                    | `4b1df247...` |
| issuance `JWT~d1~d2~`                     | `3af3c0d9...` |

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`](/canonicalisation-substrate) 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`](https://github.com/chopmob-cloud/algovoi-jcs-conformance-vectors/tree/main/vectors/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:

<CardGroup cols={2}>
  <Card title="I1 recanon diverges" icon="code-compare">
    Re-canonicalising a decoded payload never reproduces the signed-token anchor.
  </Card>

  <Card title="I2 disclosure-invariant" icon="eye-slash">
    The issuer-JWT anchor is stable across disclosures; presentation, issuance and issuer JWT are three distinct digests.
  </Card>

  <Card title="I3 signatures verify" icon="signature">
    Every signed token verifies under the RFC 8032 section 7.1 public key, in all eight crypto-capable implementations.
  </Card>

  <Card title="I4 canon-attributable" icon="fingerprint">
    The canon-sensitive divergence is attributable to a jcs\_edge\_v1 case: literal U+2028 and `1.0` rendered as `1`.
  </Card>
</CardGroup>

### 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:

| Claim                                         | Population                                                                                     | Result             |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------ |
| the JCS-dependent points                      | **10 implementations** (Python, JavaScript, Ruby, PHP, Go, Rust, Java, .NET, Elixir, Kotlin)   | 3 x 10 = **30/30** |
| signature verification and the anchoring rule | **8 crypto-capable implementations** (Python, Node, Go, Rust, Java, PHP/libsodium, .NET, Ruby) | **8/8 pass**       |

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](https://github.com/chopmob-cloud/algovoi-jcs-conformance-vectors/blob/main/_attestations/2026-07-21-jws-anchor-v1-ten-impl.md).

## Checklist for implementers

<Steps>
  <Step title="Buffer the raw bytes">
    Keep the token exactly as received. Do not parse-and-reserialise before hashing.
  </Step>

  <Step title="Pick the anchor form deliberately">
    For SD-JWT, anchor the issuer JWT (before the first `~`) unless you specifically intend to bind one disclosure subset.
  </Step>

  <Step title="Canonicalise only unsigned objects">
    JCS is the rule for objects you are hashing directly, not for payloads lifted out of a signed token.
  </Step>

  <Step title="Verify against the vectors">
    Run `jws_anchor_v1` in your language. If your verifier re-canonicalises anywhere, vector 002 will catch it.
  </Step>
</Steps>

<CardGroup cols={2}>
  <Card title="JCS Canonicalisation Substrate" icon="fingerprint" href="/canonicalisation-substrate">
    The canonicalisation floor this anchoring rule sits on top of.
  </Card>

  <Card title="Conformance vector sets" icon="list-check" href="/conformance-vectors">
    The full corpus, including jws\_anchor\_v1 and its cross-implementation matrix.
  </Card>
</CardGroup>

Apache-2.0. The set and its runners are AlgoVoi-authored.
