# Canonical Authority Representation v1

**Status: normative for new signed and digest-bound formats. It does NOT apply
retroactively.**

> **Scope.** Every format created after this document exists MUST conform. No
> format in [signing-registry.md](signing-registry.md) is changed to conform —
> each one describes bytes that existing signatures were made over, and
> retrofitting consistency would invalidate evidence that has already been
> relied upon.

The registry's §5 is the problem statement: three timestamp conventions, three
"absent" conventions, three hash casings and five representations, in one
system, all discovered by transcription rather than by design. CAR v1 exists so
that the next format does not add a fourth of anything.

The test this document has to pass is narrow and absolute:

> **The same authority object, serialized independently by .NET, TypeScript,
> Python, Java and Go, MUST produce identical bytes and an identical digest.**

Everything below follows from that. Where a rule looks pedantic, it is because
one of these runtimes does something different by default.

---

## 1. Nothing may depend on runtime behaviour

> **CAR-1.** A canonical representation MUST NOT depend on any of the
> following. An implementation that reads any of them while canonicalizing is
> non-conformant, whatever bytes it happens to produce today.

| Forbidden dependency | What it breaks |
|---|---|
| Serializer defaults | `System.Text.Json` rewrites `+` inside a string as `+`. The grant signature carried six characters where every other language emits one, and no non-.NET verifier could reproduce the bytes. This actually happened; see registry §4.4. |
| Native number formatting | `JSON.stringify(100.00)` is `100`. `JSON.parse("12.50")` is `12.5`. A decimal that round-trips a JavaScript number has lost its scale before anything sees it. |
| Locale / culture | Turkish dotless-i lowercases `I` to `ı`. A server in `tr-TR` normalizes `Invoice` to `ınvoice` and matches nothing written anywhere else. |
| Machine timezone | A timestamp formatted in local time is a different instant's bytes depending on which replica signed. |
| Dictionary iteration order | Go randomizes map iteration deliberately. Python preserves insertion order. Neither is an ordering. |
| Platform newline | `\r\n` on Windows, `\n` elsewhere. One byte, every signature. |
| Record/class declaration order | Reproducible only by an implementation holding the same type definitions, which is not a specification. |

**Build the bytes explicitly.** Construct the canonical form character by
character from a specified field list. Do not hand an object to a serializer and
hope. The grant canonicaliser is hand-built for exactly this reason and says so
in a comment.

---

## 2. Representation and algorithm are separate

> **CAR-2.** A canonical representation MUST NOT embed its signature algorithm,
> key type, curve or digest algorithm in its canonical bytes.

An algorithm migration must be possible without redefining a single business
semantic. The format's domain separator names the **format**; the algorithm is
named in the envelope that carries the signature (a JWS header, a registry
entry, a `v1=` label), where it can change without the signed bytes changing
meaning.

### Algorithm binding

| Purpose | v1 binding |
|---|---|
| Digest | SHA-256 |
| Asymmetric signature | ES256 (P-256, SHA-256), signature as **raw 64-byte R‖S**, Base64Url, never DER |
| Symmetric authentication | HMAC-SHA256, and **only** where proving identity to a third party is not required |

> A provider signature arriving in DER MUST be converted before use. AWS and GCP
> return DER, which is valid and rejected by every conforming ES256 verifier.
> `EcdsaSignatureFormat` does this. **Never forward a provider signature
> unconverted.**

Symmetric authentication proves the holder of the secret produced the bytes. It
proves nothing to a third party, because the verifier could have produced them
too. It is therefore **prohibited** for any format that asserts a human
decision, an authority grant, or an executed effect.

---

## 3. Encoding and framing

> **CAR-3.** UTF-8, no BOM. Line endings `\n` only. Where a format is
> line-oriented, every line including the last is terminated by `\n` — a
> canonical form is a sequence of terminated lines, not a join.

> **CAR-4.** **No Unicode normalization is applied.** An identifier that differs
> by composition is a different identifier.

NFC would be the conventional choice and is wrong here. Silently folding
composed and decomposed forms makes two distinct counterparties compare equal,
which is the failure the field exists to prevent. A caller that needs a composed
form normalizes **once, before** constructing the object, and then carries it.

> **CAR-5.** Control characters (U+0000–U+001F, U+007F) in any string value are
> a **refusal**, not something to escape.

A value containing `\n` in a line-oriented format could forge an entire line. An
escaping layer is a thing to get wrong; a refusal is not.

---

## 4. Field ordering

> **CAR-6.** Object keys are sorted by **Unicode code point**, ascending, at
> every level of nesting. Line-oriented formats list fields in **ascending
> code-point order of the field name**, after the domain separator line.

Alphabetical ordering is required because it is **derivable**. A reader who
knows the field names can reconstruct the order without consulting a table, so
there is nothing to transcribe and nothing to get wrong.

This is the rule `verid.contract.sign.v1` breaks. It is registered as
`LEGACY_COMPATIBLE` and is the reason this rule is stated first among the
orderings: an implementer who reasoned from the majority of families would
produce a correct CAR v1 statement and a broken contract signature, and a
signature failure reads as a key problem rather than a field-order problem.

> **CAR-7.** Field names are `snake_case`, ASCII lower case, digits and `_`
> only. A canonical field name is never derived from a programming-language
> member name.

---

## 5. Strings

> **CAR-8.** All strings are trimmed of leading and trailing whitespace.
> Interior whitespace is preserved.

> **CAR-9.** Case folding is **per field and specified per format**. There is no
> global rule, and a format that does not state a field's casing is incomplete.

| Kind | Rule |
|---|---|
| Identifiers issued by 2AYE | Lower case, invariant |
| Identifiers issued elsewhere (`agent_id`, `resource_id`, external ids) | **Trim only. Case is significant** |
| Currency codes, region codes | Upper case, invariant |
| Free vocabulary (`tool`, `action`, data classes) | Lower case, invariant |

> **CAR-10.** Every casing operation is **invariant-culture**. Never
> locale-aware.

**Normalize what we define; preserve what they issue.** Lower-casing an upstream
identifier makes two distinct principals compare equal.

---

## 6. Decimals

> **CAR-11.** A decimal is a **string**, never a JSON number. Compared
> numerically, canonicalized textually. **Scale is significant.**

IEEE-754 binary floating point cannot represent `0.1`, and a spending limit off
by a fraction is a defective control. The string requirement is not a
convenience — a JSON number cannot survive the trip through any of the five
target languages without losing scale.

### Accepted syntax

```abnf
decimal = [ "-" ] int [ "." frac ]
int     = "0" / ( digit1-9 *digit )
frac    = 1*digit
```

Bounds: at most **18 integer digits** and **6 fractional digits**.

### Why scale is significant

`100.00` and `100.0` are the same number and different strings. They compare
equal numerically and canonicalize differently.

Scale carries information the approver saw. A human who authorized `$100.00`
authorized that, and a receipt that can prove what was displayed is worth more
than one that can prove only a numeric value. The alternative is worse:
normalizing to a fixed scale is wrong per currency — JPY has no minor unit, BHD
has three — and normalizing per currency embeds a currency table inside a hash
function.

The cost is that an implementation which *rebuilds* a decimal rather than
carrying it will fail at redemption. That is the intended failure: loud, at a
boundary, rather than silently authorizing a different amount.

### The vectors, stated exactly

| Input | Status | Canonical | Notes |
|---|---|---|---|
| `"100"` | Accepted | `100` | |
| `"100.0"` | Accepted | `100.0` | **Distinct from `100`** |
| `"100.00"` | Accepted | `100.00` | **Distinct from both above** |
| `"0"` | Accepted | `0` | |
| `"0.01"` | Accepted | `0.01` | |
| `"123456789.123456"` | Accepted | `123456789.123456` | 9 integer, 6 fractional — within bounds |
| `" 100.00 "` | Accepted | `100.00` | Trimmed per CAR-8 |
| `"-0"` | **Rejected** | — | Negative zero is not a quantity, and `0`/`-0` would be two spellings of one value |
| `"-0.00"` | **Rejected** | — | Same reason |
| `"00.50"` | **Rejected** | — | Leading zeros prohibited except the single `0` before a point |
| `"007"` | **Rejected** | — | Same reason |
| `"100."` | **Rejected** | — | Trailing point prohibited |
| `".5"` | **Rejected** | — | Integer part required |
| `"1e2"` | **Rejected** | — | Exponent notation prohibited |
| `"+100"` | **Rejected** | — | Leading plus prohibited |
| `"1,000.00"` | **Rejected** | — | Group separators prohibited |
| `"100.0000001"` | **Rejected** | — | Exceeds 6 fractional digits |
| `""` | **Rejected** | — | Absent is expressed by absence (§8), not by an empty string |

To summarise the three distinctions an implementer must get right:

- **Canonicalize identically:** only inputs differing by surrounding whitespace.
- **Remain intentionally distinct:** `100`, `100.0`, `100.00`.
- **Rejected:** everything in the table above marked so, and anything the ABNF
  does not accept. **Rejection is a refusal, never a coercion.**

> A conforming implementation MUST NOT "helpfully" normalize `100.` to `100` or
> `-0` to `0`. A coerced input is an input the signer did not sign.

---

## 7. Timestamps

> **CAR-12.** RFC 3339, **UTC only**, `Z` suffix, **exactly three** fractional
> digits.

```
2026-07-27T10:15:30.000Z
```

### Offsets do NOT normalize. They are rejected.

Stated explicitly because the directive asks for it to be:

> **A non-`Z` offset is REJECTED, not converted.** An input of
> `2026-07-27T11:15:30.000+01:00` is refused, even though it denotes the same
> instant as the example above. `+00:00` is refused, even though it denotes UTC.

Two reasons. One representation is what makes a canonical form canonical, and
accepting two spellings of one instant means two callers produce two digests for
one authority object. And a producer sending a local offset usually has a
timezone defect; surfacing it at the boundary is better than absorbing it into
bytes somebody will later rely on.

Conversion to UTC is the **caller's** job, done once, before constructing the
object.

### The vectors, stated exactly

| Input | Status | Notes |
|---|---|---|
| `2026-07-27T10:15:30.000Z` | Accepted | The only shape |
| `2026-07-27T10:15:30.000z` | **Rejected** | Lower-case `z` is a second spelling |
| `2026-07-27T10:15:30Z` | **Rejected** | No fractional part |
| `2026-07-27T10:15:30.0Z` | **Rejected** | One fractional digit |
| `2026-07-27T10:15:30.00Z` | **Rejected** | Two |
| `2026-07-27T10:15:30.0000Z` | **Rejected** | Four |
| `2026-07-27T10:15:30.000000Z` | **Rejected** | Six — the microsecond form many languages emit by default |
| `2026-07-27T10:15:30.0000000Z` | **Rejected** | Seven — the .NET round-trip form, which registry §4.1 shows two live formats using |
| `2026-07-27T10:15:30.000+00:00` | **Rejected** | Offset, even though it is UTC |
| `2026-07-27T11:15:30.000+01:00` | **Rejected** | Positive offset |
| `2026-07-27T09:15:30.000-01:00` | **Rejected** | Negative offset |
| `2026-07-27 10:15:30.000Z` | **Rejected** | Space instead of `T` |
| `2026-12-31T23:59:59.999Z` | Accepted | Year boundary |
| `2026-02-29T00:00:00.000Z` | **Rejected** | 2026 is not a leap year; not a real instant |
| `2026-07-27T23:59:60.000Z` | **Rejected** | Leap second. No conforming format represents one |
| `1970-01-01T00:00:00.000Z` | Accepted | Epoch |

> Sub-millisecond precision is **truncated by the caller, not by the
> canonicalizer** — and if the caller's value has nonzero sub-millisecond
> digits, that is the caller's choice to make and record, not something the
> canonical form silently discards.

Unix-seconds timestamps appear in two registered formats (egress, OAuth claims).
They are **not** permitted in a CAR v1 authority object: a second-resolution
instant cannot express the expiry of a 60-second grant precisely enough to
reason about.

---

## 8. Absent, null and empty

> **CAR-13.** The three are different and MUST remain distinguishable.

| State | Representation | Meaning |
|---|---|---|
| **Absent** | The key is **omitted** | Unconstrained. Not stated |
| **Empty** | `[]` or `""` where the format permits it | Stated, and it authorizes nothing |
| **Null** | **Prohibited.** `null` is never a valid CAR v1 value | — |

> **Absent is unconstrained. Empty authorizes nothing. They are never
> interchangeable.**

This is security-critical for every collection-valued constraint. A contract
with no `allowed_models` does not constrain which model acts, so contracts
signed before the field existed keep working. A contract with
`allowed_models: []` permits **no** model and every action fails.

An implementation that coerces absent to empty silently narrows every old
contract to authorize nothing. One that coerces empty to absent silently widens
a contract that was deliberately locked. Both are severe, both are silent, and
neither raises an error.

`null` is prohibited because it is a third spelling with no distinct meaning —
and because the registry already records two other conventions (`none` in the
statement families, JSON `null` in attenuations). A new format adding a fourth
is exactly what this document exists to prevent.

> For a line-oriented CAR v1 format, which cannot omit a line and keep a fixed
> field list, absence is expressed by omitting the line entirely. The field list
> is then **not** fixed, and the format MUST specify which fields are optional.
> It MUST NOT adopt a sentinel value such as `none`.

---

## 9. Booleans, enums, integers

> **CAR-14.** Booleans are the bare tokens `true` and `false`, lower case.
> Never `1`, `0`, `yes`, `True`.

> **CAR-15.** An enum is its **member name**, verbatim as registered, never its
> ordinal.

An ordinal is unreadable in an audit trail and silently changes meaning when a
member is inserted mid-enum. Member names are a registered vocabulary: adding
one is additive, renaming one is a breaking change requiring a new format
version.

> **CAR-16.** An integer is its shortest decimal form, invariant culture, no
> group separators, no leading zeros, no exponent, no leading `+`.

---

## 10. Collections

> **CAR-17.** A **set** is sorted by **Unicode code point**, ascending, and
> deduplicated before canonicalization.

> **CAR-18.** A **sequence** whose order is semantically meaningful is kept in
> order, and the format MUST say which of the two a field is.

Ordinal comparison is byte order. Culture-aware comparison orders `a` against
`B` differently per locale, so the same set produces different bytes on
different machines — and the machine that signed would not be the one that
verified.

A set's order is not semantically meaningful, so it must not change the digest:
otherwise a caller that serialized its predicates in a different order would
find its own grant unredeemable, which is a failure mode with no security
benefit and a baffling error message.

> **CAR-19.** Nested collections specify a separator per level, and separators
> MUST NOT be able to occur in a member value. Where a member could contain the
> separator, the format MUST refuse the value rather than escape it.

---

## 11. Identifiers and resource references

> **CAR-20.** A GUID is lower case, hyphenated, no braces, no URN prefix:
> `8-4-4-4-12`.

> **CAR-21.** An identifier issued outside 2AYE is **trimmed only**. Its case,
> composition and internal punctuation are preserved exactly.

> **CAR-22.** A resource reference names the resource **and** the authority
> boundary it exists in, so that two resources with the same local name in
> different accounts are different references. See
> [resource-reference.md](resource-reference.md).

---

## 12. Hashes and digests

> **CAR-23.** A digest is SHA-256, rendered as **uppercase** hexadecimal, with
> no separators and no prefix.

Uppercase uniformly, because registry §5 records three different casings in use
and a digest that differs only in case compares unequal on every single
operation. Dart's `crypto` package returns lower case; Go's `hex` returns lower
case; `Convert.ToHexString` returns upper case. One of those has to be
converted, so the specification says which.

> **CAR-24.** A hash carried as a field value is trimmed and uppercased, with no
> per-field exceptions.

This is the rule `credential_hash` breaks (registry §4.1).

---

## 13. Verification

> **CAR-25.** A verifier MUST check, in this order, and MUST refuse on the first
> failure:
>
> 1. The domain separator is **exactly** the expected format identifier.
> 2. The field set and order are exactly as specified. **An unexpected field is
>    a refusal, not something to ignore.**
> 3. The signature verifies over the canonical bytes **as received**.
> 4. The key is the one **registered** for the asserted signer.

Step 2 is not pedantry. A verifier that checks the fields it knows and ignores
the rest accepts a format with an appended line, and an appended line is a
free-text channel into a representation whose entire purpose is being closed.

Step 4 has been got wrong here: an early quorum signer carried its own public
key, so a caller could sign with any key it liked and pass. **A key never
travels with the signature it verifies.** Keys come from the enrolment record or
a published key set. This is the same rule the SAML validator applies when it
refuses a certificate embedded in `KeyInfo`.

> **CAR-26.** Verify over the bytes **as received**, never over a reserialized
> object.

A producer may additionally compare its own reserialization against what it
signed — that is an internal consistency check only the producer can perform. An
external verifier copying that approach has to reproduce one runtime's
serializer byte for byte. That bug has shipped from this repository once.

---

## 14. What is demonstrated, and what is not

Stated precisely, because the easy overclaim here is to call this
representation proven:

| Demonstrated | Not demonstrated |
|---|---|
| Nine existing statement vectors reproduce from published **raw** inputs, independently, in Node | That CAR v1 itself is agreed across **independent implementations** — three worked objects reproduced by one author's second implementation is not cross-language interoperability |
| The implementation produces exactly the pinned bytes, tested per family | That any **live format** uses CAR v1. No signed format in the registry conforms to it, and none is being migrated |
| Rejections are refusals rather than coercions | Third-party conformance. The specification, the vectors and the verifier share one authorship boundary |

**No format in production uses CAR v1 yet.** It governs the next one. The
existing families stay compatibility formats, and the two concrete failures that
proved they should — `12.50` becoming `12.5`, and a seven-digit `+00:00`
timestamp contradicting the envelope's own rules in the same specification — are
exactly why they are not the foundation to build on.

`verid.audit.egress.v1` remains outside every vector set, including this one. It
is HMAC over a JSON body and needs a receiver-side vector of a different shape.

---

## 15. Conformance

A format claiming CAR v1 MUST publish:

1. **Positive vectors** — input object, canonical bytes, canonical text where
   the format is textual, digest, signing input, and expected verification
   result.
2. **Negative vectors** — representations that MUST NOT verify. Equality alone
   is not portability; see
   [car-v1-vectors.json](car-v1-vectors.json) for the required categories.
3. **An entry in [signing-registry.md](signing-registry.md)**, every attribute
   filled in.

> A format is conformant when an implementation **in another language**,
> written from its specification alone, reproduces every positive vector and
> refuses every negative one.

Five implementations are not required now. The vectors are, because they are
what makes the fifth implementation possible without a conversation.

---

## 16. Summary card

| | CAR v1 |
|---|---|
| Encoding | UTF-8, no BOM |
| Line endings | `\n`, every line terminated |
| Unicode normalization | None. Control characters refused |
| Field names | `snake_case`, ASCII |
| Field order | Unicode code point, ascending, every level |
| Strings | Trimmed; casing per field, invariant culture |
| Decimals | Strings. Scale significant. 18 int / 6 frac. No `-0`, no exponent |
| Timestamps | RFC 3339, UTC, `Z`, exactly 3 fractional digits. Offsets **rejected** |
| Booleans | `true` / `false` |
| Enums | Member name |
| Integers | Shortest form, invariant, no separators |
| Sets | Code-point sorted, deduplicated |
| Sequences | Order preserved, declared as such |
| Absent | Key omitted |
| Empty | `[]` / `""`, distinct from absent |
| Null | Prohibited |
| GUIDs | Lower case, hyphenated |
| External ids | Trim only, case significant |
| Digests | SHA-256, **uppercase** hex |
| Signatures | ES256, raw R‖S, Base64Url, never DER |
| Symmetric | HMAC-SHA256, never for authority or human decisions |
| Algorithm in bytes | **Prohibited** |
