185 lines
6.1 KiB
Markdown
185 lines
6.1 KiB
Markdown
|
|
# 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.
|