# Canonicalization algorithm v0.2 Two hashes exist on every presentation. | Hash | Input | Enters the signature? | |---|---|---| | `view_hash` | binding document | yes, for every binding verb at `organizational` and above | | `awareness_hash` | awareness document | no, unless a `promote` disposition copied named fields into `awareness_promoted` | That split is the whole point of pre-sign vs post-sign. Reference implementation: `canonicalize.py`. ## 1. Profile Restricted JCS (RFC 8785 subset): 1. UTF-8. 2. No insignificant whitespace. 3. Object keys sorted by raw UTF-8 bytes (all keys in this spec are ASCII). 4. Arrays keep author order except where this spec sorts (`packet` by `item_id`, `highlights` by `id`, hats/scopes/identities by `id`, permission lists lexicographically). 5. Numbers are integers only. Timestamps stay strings (`YYYY-MM-DDTHH:MM:SSZ`). 6. Strings use RFC 8259 escaping; U+0000–U+001F as `\u00xx`. 7. Drop keys whose value is `null`. Keep empty arrays. 8. Unknown keys are stripped. Adding a field to the live memo does not change a hash until it is added to the allow-list below. ## 2. Binding document (`view_hash`) Allow-list, then normalize: ``` memo_id memo_version question requested_act binding_level brief locale ui_release packet[] → {item_id, hash} sorted by item_id highlights[] → {id, item_id, locator, required_ack, severity} sorted by id binding → BindingSlice (pre-sign identity + scope) awareness_promoted → only fields explicitly promoted into the bind ``` `binding` allow-list: ``` principal available_identities[] sorted by id target the scope this act enters available_bind_scopes[] scopes choosable as *this* bind, sorted by id granted_at_bind roles/permissions sorted justification blast_radius terms ``` Hats, last-used role, other-tenant orientation, situation notes **do not appear here**. `hash` form is always `alg:hex` (`sha256:…`). Then: ``` canonical = dumps(binding_document) view_hash = SHA-256(UTF-8(canonical)) ``` `dumps` is the JCS-subset serializer in `canonicalize.py`. ## 3. Awareness document (`awareness_hash`) Allow-list: ``` memo_id memo_version locale ui_release proposed_hat proposed_hat_source system_default | last_used | policy | explicit | inferred available_hats[] sorted by id available_scopes[] sorted by id (orientation only) last_session situation_note ``` Same `dumps` + SHA-256. ## 4. Dynamic linking A binding disposition stores both hashes on the presentation and signs only `view_hash`. Signed attributes (AES/QES) MUST contain at least: ``` memo_id memo_version disposition.verb presentation.view_hash ``` They MUST NOT contain `awareness_hash` unless `promote` ran. ## 5. Promote `promote` copies named awareness fields into `binding.awareness_promoted` on a **new memo version**. Typical case: an elevating hat (`hat.elevates=true`) must become part of the bind, or a tenant that looked like a hat is actually a bind-scope. After promote: - new `view_hash` - previous presentations invalid - route restarts or current step is re-presented ## 6. Test vectors Computed with `canonicalize.py`. ### V1 — login binding (identity + tenant) Input: `vectors/login-binding.json` `view_hash.hex` = ``` 492d9d311bf44ec9de0d0abef28abac7d31df2781527e8fe276a3186ee1b06b8 ``` What is inside: chosen principal, break-glass identity as an *available bind identity*, target tenant ACME, sibling tenant Beta as an *available bind-scope*, gate privileges `authenticated` + `session.create`, monitoring terms. What is not inside: Finance Controller hat, last session, Payroll Admin. ### V2 — login awareness (hats + last session) Input: `vectors/login-awareness.json` `awareness_hash.hex` = ``` 2be7742970a01e7a879ae5040660659fb8c9a5c024e6c7fb338f3944a4fe05d1 ``` Proposed hat `hat:finance-controller` source `last_used`. Payroll Admin is listed with `elevates=true` so the UI can show it and the state machine can refuse `configure` on it. ### V3 — ADR accept Input: `vectors/adr-binding.json` `view_hash.hex` = ``` 1c89ec07c3cc9d16f85a1ba1be5169456b3c55d21161f64037787779ae91f202 ``` ### Isolation checks the implementation must keep green 1. Shuffling object keys in the input JSON does not change either hash. 2. Adding or changing awareness fields on a combined object does not change `view_hash`. 3. Changing `binding.target.id` from `tenant:acme` to `tenant:beta` **does** change `view_hash` (that is a different login). 4. Changing only `proposed_hat` changes `awareness_hash` only. 5. Selecting `hat:finance-controller` after login emits `session.hat_selected` and does not rewrite `view_hash`. ## 7. UI mapping (login) ``` ┌─────────────────────────────────────────────────────────┐ │ PRE-SIGN (editable, bound) │ │ Identity [ Bernd Worsch ▾ ] │ │ Scope [ ACME Corp / Payroll-Prod ▾ ] │ │ At gate authenticated · session.create │ │ Terms sessions recorded │ └─────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────┐ │ AWARENESS (shown, not signed) │ │ After login you will wear │ │ Finance Controller (last used, Tue 18:12) │ │ other hats: Auditor · Payroll Admin (needs new bind) │ │ Other tenants you support: Beta GmbH (needs new bind) │ └─────────────────────────────────────────────────────────┘ [ Switch identity ] [ Change tenant ] [ Proceed ] ``` Proceed signs V1. After session start, hat dropdown is `configure`. Tenant dropdown to Beta opens a child memo.