198 lines
9 KiB
Markdown
198 lines
9 KiB
Markdown
|
|
# Evidence Model
|
||
|
|
|
||
|
|
**Repo:** informed-decision
|
||
|
|
**Workplan task:** `INFD-WP-0001-T06`
|
||
|
|
**Status:** draft, pending review
|
||
|
|
**Companions:** `ProductRequirementsDocument.md`, `UseCaseCatalog.md`,
|
||
|
|
`ArchitectureBlueprint.md` (gated on `INFD-WP-0001-T02`)
|
||
|
|
**Normative algorithm:** `history/20260909-initial-exploration/canonicalize.md`
|
||
|
|
**Governed implementation:** `informed_decision/canonicalize.py`
|
||
|
|
**Tests:** `tests/test_canonicalize.py`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. What this model claims
|
||
|
|
|
||
|
|
> **This person, in this role, was shown this view, and bound this act.**
|
||
|
|
|
||
|
|
Not *"the file was signed"*. The difference is the whole product. A signature
|
||
|
|
over a document proves a key was used on some bytes; it does not establish what
|
||
|
|
the signer was looking at when they decided, which is the question an auditor
|
||
|
|
actually asks and the question no e-signature product answers.
|
||
|
|
|
||
|
|
The mechanism is **dynamic linking**: the human-readable content actually
|
||
|
|
presented is canonicalized and its digest is bound into the act.
|
||
|
|
|
||
|
|
## 2. What this model does not claim
|
||
|
|
|
||
|
|
Stated first, because an evidence model that oversells is worse than none.
|
||
|
|
|
||
|
|
**E-01 — It does not prove the person understood.** It proves what was
|
||
|
|
presented and what was explicitly acknowledged. Comprehension is not
|
||
|
|
observable and this model does not pretend to observe it.
|
||
|
|
|
||
|
|
**E-02 — It does not prove the person read anything.** Deliberately. Evidence of
|
||
|
|
informedness is the hash of the presented view plus explicit acknowledgment —
|
||
|
|
never dwell time, scroll depth, or attention telemetry. That choice trades a
|
||
|
|
weaker claim for the absence of surveillance, and the trade is intentional
|
||
|
|
(`INTENT.md` principle 12, `PR-70`/`PR-71`).
|
||
|
|
|
||
|
|
**E-03 — It does not survive a compromised surface.** `view_hash` is computed by
|
||
|
|
the same component that renders. A compromised surface can present X and attest
|
||
|
|
Y. This is structurally the same residual `approval-engine` names for
|
||
|
|
adversarial omission at a compromised source, and it is not closed here, not
|
||
|
|
closable at this layer, and never to be described as closed.
|
||
|
|
|
||
|
|
**E-04 — A hash chain proves records were not altered or truncated after
|
||
|
|
arrival. It cannot prove a record was never sent.** Carried over from
|
||
|
|
`approval-engine`'s reasoning rather than rediscovered: a suppressed event
|
||
|
|
leaves the chain perfectly intact and verification reports intact. Completeness
|
||
|
|
is a separate obligation met by cadence, not by chaining.
|
||
|
|
|
||
|
|
**E-05 — Stage 1 binding is organizational only.** No AES, no QES. The object
|
||
|
|
carries `binding_level` so higher levels are expressible without a schema
|
||
|
|
change; nothing above organizational is implemented or claimed.
|
||
|
|
|
||
|
|
**E-06 — A Stage 1 escalation is an assertion, not a verified claim.** Without
|
||
|
|
the mandate graph, guard `G_ESC` cannot be fully evaluated. The bundle records
|
||
|
|
what the approver asserted and marks it unverified.
|
||
|
|
|
||
|
|
## 3. The two hashes
|
||
|
|
|
||
|
|
| | `view_hash` | `awareness_hash` |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| Over | the binding document | the awareness document |
|
||
|
|
| Signed | yes, from organizational level up | **no** |
|
||
|
|
| Purpose | what the person committed to | what they were oriented with |
|
||
|
|
| In AES/QES signed attributes | yes | no, unless promotion ran |
|
||
|
|
|
||
|
|
**Binding allow-list:** `memo_id`, `memo_version`, `question`, `requested_act`,
|
||
|
|
`binding_level`, `brief`, `locale`, `ui_release`, normalized `packet`,
|
||
|
|
normalized `highlights`, `binding`, and optionally `awareness_promoted`.
|
||
|
|
|
||
|
|
**Awareness allow-list:** proposed hat and its source, available hats and
|
||
|
|
scopes, last session, situation note.
|
||
|
|
|
||
|
|
Profile: JCS-inspired restricted JSON — UTF-8, keys sorted by code unit, no
|
||
|
|
insignificant whitespace, integers only, unknown keys stripped, nulls dropped.
|
||
|
|
|
||
|
|
```
|
||
|
|
canonical = dumps(document)
|
||
|
|
hash = SHA-256(UTF-8(canonical))
|
||
|
|
```
|
||
|
|
|
||
|
|
### Why the split exists
|
||
|
|
|
||
|
|
The login case forced it and it now applies everywhere. Defaulting a role to
|
||
|
|
last-used is **required** for situational awareness — an operator who cannot see
|
||
|
|
which hat they are about to wear is less safe, not more — and is **forbidden**
|
||
|
|
from silently entering the signed payload. A person must not be bound to
|
||
|
|
orientation material they did not commit to.
|
||
|
|
|
||
|
|
Promotion is the only bridge: moving a field from awareness into binding creates
|
||
|
|
a new memo version, invalidates outstanding presentations, and copies the named
|
||
|
|
fields into `awareness_promoted` where `view_hash` covers them (guard
|
||
|
|
`G_PROMOTE`).
|
||
|
|
|
||
|
|
## 4. The four isolation properties
|
||
|
|
|
||
|
|
These are the model. Each is a test in `tests/test_canonicalize.py` and each
|
||
|
|
must stay green; a failure means `view_hash` no longer means what §1 claims.
|
||
|
|
|
||
|
|
| # | Property | Protects | Test |
|
||
|
|
| --- | --- | --- | --- |
|
||
|
|
| 1 | Shuffling object keys does not change either hash | Any verifier, in any language, agrees with us | `test_key_order_does_not_change_*` |
|
||
|
|
| 2 | Editing an awareness field does not change `view_hash` | The person is bound only to what they committed to | `test_editing_awareness_does_not_change_view_hash` |
|
||
|
|
| 3 | Changing `binding.target` **does** change `view_hash` | Positive control — without it, 2 could pass vacuously | `test_changing_binding_target_changes_view_hash` |
|
||
|
|
| 4 | Selecting a hat after login does not rewrite `view_hash` | Post-bind session state is a different object | `test_post_bind_hat_selection_does_not_change_view_hash` |
|
||
|
|
|
||
|
|
Property 3 is not decorative. Properties 1, 2 and 4 are all *negative* — they
|
||
|
|
assert that things do **not** change the hash. A canonicalizer that returned a
|
||
|
|
constant would pass all three. Property 3, plus the per-field variants over
|
||
|
|
`question`, `requested_act`, `binding_level` and `packet`, is what makes the
|
||
|
|
suite meaningful.
|
||
|
|
|
||
|
|
**Published vectors** reproduce byte for byte and are asserted in CI:
|
||
|
|
|
||
|
|
```
|
||
|
|
login view_hash 492d9d31…1b06b8
|
||
|
|
login awareness_hash 2be77429…4fe05d1
|
||
|
|
ADR-0042 view_hash 1c89ec07…e91f202
|
||
|
|
```
|
||
|
|
|
||
|
|
**Provenance is tested.** `history/20260909-initial-exploration/` is never
|
||
|
|
edited. `test_governed_vectors_match_the_preserved_history_copy` asserts the
|
||
|
|
governed fixtures have not drifted from the founding copies, so a silent edit to
|
||
|
|
a vector to make a failing test pass is itself a test failure.
|
||
|
|
|
||
|
|
## 5. The presentation record
|
||
|
|
|
||
|
|
Every render creates one. It carries:
|
||
|
|
|
||
|
|
- `memo_id`, `memo_version`
|
||
|
|
- `principal` — who it was shown to
|
||
|
|
- `locale`, `ui_release` — both inside `view_hash`
|
||
|
|
- `rendered_at`
|
||
|
|
- `acked_highlight_ids`
|
||
|
|
- `view_hash`, `awareness_hash`
|
||
|
|
- `phase` — `pre_bind` | `bind` | `post_bind`
|
||
|
|
|
||
|
|
A disposition references exactly one presentation, of the same memo version
|
||
|
|
(guard `G_PRES`). A presentation taken at version *n* cannot bind after the memo
|
||
|
|
advances to *n+1* — no silent upgrade (`NC-07`).
|
||
|
|
|
||
|
|
Because `locale` is inside `view_hash`, the same memo read in German and in
|
||
|
|
English produces two different presentations of one binding. Intended, and
|
||
|
|
documented rather than smoothed over (`PR-61`).
|
||
|
|
|
||
|
|
## 6. The evidence bundle
|
||
|
|
|
||
|
|
An **export**, not a screenshot (`INTENT.md` principle 7). Contains the memo and
|
||
|
|
all its versions, every presentation with both hashes, every disposition, the
|
||
|
|
thread, the route as executed, and the append-only hash chain.
|
||
|
|
|
||
|
|
Requirements:
|
||
|
|
|
||
|
|
- **Verifies offline.** No network, no access to this service. A verifier
|
||
|
|
recomputes `view_hash` from the bundle's own binding document and compares.
|
||
|
|
- **Detects mutation.** Altering any covered field breaks verification.
|
||
|
|
- **States its own bound.** The residuals in §2 travel with the bundle as
|
||
|
|
metadata. An auditor must not have to read this document to learn that E-03
|
||
|
|
exists.
|
||
|
|
- **Marks unverified assertions as such** — see E-06.
|
||
|
|
|
||
|
|
## 7. Relationship to `audit-core`
|
||
|
|
|
||
|
|
`audit-core` holds the archive; this repository holds the operative record and
|
||
|
|
produces the export. This surface emits its transitions there and does not
|
||
|
|
duplicate the archive's role.
|
||
|
|
|
||
|
|
Its guarantee is bounded and the bound is inherited, not re-litigated:
|
||
|
|
`audit-core` is not WORM or object-locked, its tamper evidence is conditional on
|
||
|
|
live preconditions, and stronger custody was **decided against** in statute §16.
|
||
|
|
This repository must not plan around a stronger archive, and must not imply the
|
||
|
|
bundle is more durable than the archive it reconciles against.
|
||
|
|
|
||
|
|
## 8. Open — `view_hash` versus the approval binding digest
|
||
|
|
|
||
|
|
**Unresolved.** Filed as `INFD-IN-0001` R3 with `gate-house`.
|
||
|
|
|
||
|
|
`approval-engine`'s claim carries *"a digest over the same canonical binding the
|
||
|
|
decision point already computes."* `view_hash` is also a digest over "the
|
||
|
|
binding". They cover different material: the approval digest exists with no
|
||
|
|
human in the loop; `view_hash` additionally covers brief, packet, highlights,
|
||
|
|
locale and UI release.
|
||
|
|
|
||
|
|
Until ruled, this repository computes `view_hash` and **asserts no relationship**
|
||
|
|
between the two. Three candidate outcomes and their costs are set out in
|
||
|
|
`docs/gate-house-decision-request-layer-placement.md` §2 R3. The outcome to be
|
||
|
|
avoided is both digests shipping with no stated authority rule, leaving the
|
||
|
|
estate with two canonicalizations of one act.
|
||
|
|
|
||
|
|
## 9. Signed attributes (L4+, horizon)
|
||
|
|
|
||
|
|
When AES/QES arrives, the signed attributes carry `memo_id`, `memo_version`,
|
||
|
|
`disposition.verb` and `presentation.view_hash`. They do **not** carry
|
||
|
|
`awareness_hash` unless promotion ran. Recorded here so nothing built at Stage 1
|
||
|
|
forecloses it.
|