From 34d796356a8ff02e232fc5135ab493271aa26afc Mon Sep 17 00:00:00 2001 From: Thomas Connally Date: Tue, 4 Aug 2026 04:43:41 +0000 Subject: [PATCH] spec: separate dataplane Merkle-root integrity from authority-layer signed receipts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New spec doc (docs/spec-two-layer-integrity.md) canonizing the two-layer model: the dataplane layer (per-org event hash chain + retained checkpoints, plutus verify) proves the store did not silently change; the authority layer (AAR receipts binding actor, boundary, evidence, action, result) proves who authorized what. Each layer verifies independently of the other, neither implies the other, and the two failure cases — tampered store with valid receipts, and valid store with revoked/absent signature — are distinguishable outcomes. ledger-integrity.md gets a layer-boundary callout pointing to the spec (plus one pre-existing MD049 emphasis-style fix so the touched file lints clean). Closes #206 --- docs/ledger-integrity.md | 10 +++- docs/spec-two-layer-integrity.md | 94 ++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 docs/spec-two-layer-integrity.md diff --git a/docs/ledger-integrity.md b/docs/ledger-integrity.md index 5db8a48..50f7b3a 100644 --- a/docs/ledger-integrity.md +++ b/docs/ledger-integrity.md @@ -1,5 +1,13 @@ # Ledger integrity — usage-event tamper-evidence (#108) +> **Layer boundary:** this document covers the **dataplane** layer — storage +> integrity over the append-only event store. Authority-layer attestation +> (signed receipts binding actor, boundary, evidence, action, result) is a +> separate, independently verifiable claim; see +> [Two-layer integrity: dataplane storage root and authority-layer receipts](spec-two-layer-integrity.md) +> for the split, and [Authorized Action Receipts](authorized-action-receipts.md) +> for the authority-layer mechanism. + Plutus stores usage in an integer-exact, independently re-queryable ledger (`SUM(usage_events.cost_micros)`). That makes the dollars *reproducible* — but, on its own, the table was append-only **by convention** only. Nothing stopped an @@ -176,7 +184,7 @@ corruption and casual tampering, not a motivated operator with DB access. ## Guardrail -**No public document may claim Plutus is "tamper-evident" until this ships _and_ +**No public document may claim Plutus is "tamper-evident" until this ships *and* an external cryptographic review covers it** (the SOW drafted for the Perseus Vault audit-chain review). Until then, savings statements carry the caveat the harness/one-pager already print: the ledger is *re-queryable* and now diff --git a/docs/spec-two-layer-integrity.md b/docs/spec-two-layer-integrity.md new file mode 100644 index 0000000..907ff4f --- /dev/null +++ b/docs/spec-two-layer-integrity.md @@ -0,0 +1,94 @@ +# Two-layer integrity: dataplane storage root and authority-layer receipts + +Status: spec +Date: 2026-08-04 +Resolves: #206 +Related: [ledger-integrity](ledger-integrity.md) (dataplane) · +[authorized-action-receipts](authorized-action-receipts.md) (authority layer) · +[evidence-receipts](evidence-receipts.md) (task-scoped receipt view) · +[continuous-attestation](continuous-attestation.md) (#201 semantics) + +Ledger separates two guarantees that are enforced by different mechanisms. +They are composable, verifiable independently, and **neither implies the +other**. This is the same split the ecosystem documents elsewhere: a dataplane +layer seals security-relevant events into a hash root, and a hosting/authority +layer signs receipts. Storage integrity and authority attestation are distinct +claims; mixing them weakens the audit story because one failing (or one +passing) says nothing about the other. + +## Layer 1 — dataplane: storage integrity over the append-only event store + +The dataplane layer proves the store did not silently change. It is +implemented by the per-org event hash chain and out-of-band checkpoints +(`docs/ledger-integrity.md`): + +- every `usage_events` row carries `prev_hash`/`row_hash` over canonical, + column-tagged immutable fields; SHA-256 by default, HMAC-SHA256 when a key + is set; +- verification (`plutus verify`) replays the chain from genesis and fails at + the first divergence — exit 0 = intact, exit 2 = tampered; +- retained checkpoints (`plutus checkpoint` / `verify-checkpoints`) pin a + chain head out of band so a rewritten history cannot be re-chained into + agreement with a head someone else already holds. + +The chain head (or a Merkle-style root over it) is a **storage-root claim +only**: it answers "did the store change?" and nothing else. + +## Layer 2 — authority layer: receipts signed/attested by the control plane + +The authority layer proves who authorized what, under which evidence. It is +implemented by Authorized Action Receipts (AAR, `docs/authorized-action-receipts.md`): + +- a receipt binds actor, boundary (org/workspace/scope), evidence references, + action, and result; +- Vault is the authoritative control plane; Ledger commits only opaque, + validated references and hashes — never raw prompts, secrets, tool output, + or policy bodies; +- the receipt's authority bindings are checkable against the control plane's + manifests and approval state independently of any storage root. + +A receipt is an **authority claim only**: it answers "who authorized what" +and nothing about whether the store has since changed. + +## Independence + +Each layer has its own verifier, and neither verifier replays the other: + +| Claim to check | Verifier | Does not require | +|---|---|---| +| Storage did not change | `plutus verify` / `verify-checkpoints` (chain + retained heads) | Replaying receipt signatures; any receipt at all | +| Receipt is genuine and authorized | AAR authority checks (manifest state, approval state, opaque reference validity) | Replaying the storage chain; the chain root | + +The two verifiers may run in any order, on different machines, at different +times. A receipt can be verified long after the storage root has rotated +away, and the storage root can be verified with no authority material +present. + +## Failure cases are distinguishable + +| Case | Dataplane verdict | Authority verdict | Distinguishable outcome | +|---|---|---|---| +| Tampered store, valid receipts | **broken** — chain/checkpoint divergence (`plutus verify` exit 2) | **intact** — receipts still verify against manifests | Storage compromised; authority trail intact. Do not conflate "receipts check out" with "store intact". | +| Valid store, revoked/absent signature | **intact** — chain verifies | **failed** — signature missing or manifest revoked | Store intact; authority compromised/expired. Do not conflate "chain verifies" with "action was authorized". | +| Both intact | intact | intact | The only case where "what happened" and "it was authorized" both hold. | +| Both failed | broken | failed | Either layer independently broken; both must be remediated. | + +The receipt record keeps the two verdicts separate: `verify` output never +silently "passes" a receipt-bearing org whose chain is broken, and a receipt's +authority fields are never inferred from chain position. + +## Composition + +Where both layers apply to the same event, they compose in the obvious way: +the dataplane root proves the event is the event, the authority receipt proves +the event was authorized. Neither claim upgrades the other — in particular, a +valid signature does not repair a broken chain, and an intact chain does not +authorize an unsigned event. + +## Non-goals + +- No new cryptographic primitive or schema change is required by this spec; + it canonizes the existing split (chain/checkpoints vs. AAR receipts). +- The two-layer model is not a claim that every recorded event needs an + authority receipt; receipts attach where authority applies (authorized + actions), while the dataplane layer covers the whole append-only stream.