From f75d6df12926574204be0fac74c6854317dfffa7 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 02:07:58 -0400 Subject: [PATCH 01/13] =?UTF-8?q?docs(spec):=20the=20Ceremony=20Contract?= =?UTF-8?q?=20v0.1=20draft=20=E2=80=94=20six=20laws,=20wire=20objects,=20c?= =?UTF-8?q?hained=20store=20(#225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keystone spec for the decision-surface seam: language-neutral wire objects (PermissionRequest, DecisionMatrix, Decision, Introduction, chained PinRecord), six normative laws with proof obligations mapped to the Lean/Aeneas track, mechanism section (chained CAS store discharging the L2 no-replay hypothesis, signed loosening verdicts from #226, presence-attested pins), cross-language conformance vectors, and the law-minimalism governance rule (L1+L4 merge is the audit's first candidate). A harness can comply without depending on bridle; the crate is the reference implementation. Refs #225, #220, #226. Companions: agent-mesh#65, newt-agent#1209. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 339 +++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 docs/spec/ceremony-contract.md diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md new file mode 100644 index 0000000..90d72db --- /dev/null +++ b/docs/spec/ceremony-contract.md @@ -0,0 +1,339 @@ +# The Ceremony Contract + +**Status:** DRAFT 0.1.0 (2026-07-15) — for review. Normative once accepted. +**Scope:** the decision-surface and first-contact contract between agent-* +libraries (which own decision *semantics*) and harnesses (which own +*rendering*). Companion to the verdict/policy TOML contract (#220) — that +spec is the non-interactive half of this seam; this spec is the interactive +half plus the laws both halves obey. +**Audience:** implementers of client libraries (Rust, Python, Dart, +TypeScript) and of consuming harnesses (newt, hermes, gila, Claude Code / +Codex plugins). A harness can comply with this spec without depending on +any bridle crate; `agent-bridle` is the reference implementation. + +Positioning, prior art, and the adoption strategy are recorded on +[#225](https://github.com/Gilamonster-Foundation/agent-bridle/issues/225); +this document deliberately restates none of it. + +--- + +## 1. Terms + +| Term | Definition | Already shipped as | +|---|---|---| +| **Fingerprint** | `blake3(pubkey)` — a self-certifying identity name | `agent_mesh_protocol::Fingerprint` | +| **Caveats** | attenuable authority; forms a meet-semilattice | `agent_mesh_protocol::Caveats` (`meet_never_amplifies` is property-tested) | +| **Verdict** | durable disposition: `deny ⊏ passkey ⊏ ask ⊏ approve`, ordered by restrictiveness | `agent_bridle_core::policy::Verdict` (`precedence()`) | +| **Gate** | the enforcement choke-point; mints `ToolContext` only inside `authorize()` | `agent-bridle-core` | +| **Surface** | a consumer-supplied renderer of decisions (TUI, GUI, policy file, API) | this spec, §3.6 | +| **Escalation** | a navigation affordance (e.g. `audit`) — never authority | this spec, §3.2 | +| **Pin** | a durable, provenance-carrying record that an identity's key was accepted | this spec, §3.5 | +| **Ceremony** | the interactive resolution of a decision the laws refuse to default | this spec, §4 L5 | +| **ContentId / MerkleNode** | BLAKE3 CID over canonical DAG-CBOR; parent-linked record | `content-addressable` crate | + +Encodings: **one schema, three encodings.** JSON for interchange (client +libs), TOML at rest (#220 policy files), **canonical DAG-CBOR for anything +hashed or signed**. Signatures and `ContentId`s are computed over canonical +bytes only. + +Time: per the workspace hard rule, **wall-clock is never a coordination +primitive**. Validity keys on generation counters +(`valid_for_generation`); RFC 3339 timestamps appear in records as +provenance *data* supplied by the boundary, never read by the kernel. + +## 2. The seam + +Libraries define the decision **space**; consumers define the **layout**. +A library crate MUST NOT contain rendering components or interactive +prompting. A consumer binds a `DecisionSurface` (§3.6); with none bound, +the laws fail closed (§4 L3). + +``` + agent-* libraries consumer (harness) +┌─────────────────────┐ ┌──────────────────────────┐ +│ decision kernel │ struct │ DecisionSurface impl: │ +│ (pure; provable) │──────► │ newt: matrix + audit │ +│ resolve · meet · │ │ hermes: flat list │ +│ gate · pin store │ ◄──────│ daemon: policy files │ +└─────────────────────┘Decision│ phone: GUI sheet │ + └──────────────────────────┘ +``` + +## 3. Wire objects + +Field names are normative; unknown fields MUST be ignored (forward +compatibility). All objects carry `"v": 1`. + +### 3.1 PermissionRequest + +What a gate hands a surface when a verdict resolves to interaction. + +```json +{ + "v": 1, + "subject": "b3:9f2c…", // Fingerprint — an identity, never a location + "action": { "class": "exec", "display": "run_command: cd " }, + "violation": "outside-granted-allowlist", + "matrix": { … }, // §3.2 + "context": { "session": "…", "rationale": "…", "generation": 41 } +} +``` + +### 3.2 DecisionMatrix + +The decision *space*. Nothing here encodes layout — rows, columns, and +ordering are the consumer's. + +```json +{ + "verbs": ["allow", "deny"], + "scopes": ["once", "session"], + "default": ["allow", "once"], // rendering hint only (the ⬅ cursor) + "escalations": ["audit"] // affordances; MAY be rendered +} +``` + +The scope vocabulary is open: this spec fixes the laws over scopes, not the +set. A durable scope (e.g. `always`) materializes as a **signed loosening +entry** in the policy store and is therefore governed by L2. + +### 3.3 Decision + +```json +{ "v": 1, "grant": { "verb": "allow", "scope": "session" } } +{ "v": 1, "escalate": "audit" } +``` + +`escalate` carries **zero authority** (L4): it navigates the human to a +richer surface; the request remains undecided until a `grant` returns. + +### 3.4 Introduction + +First contact: an unpinned identity proposing itself. + +```json +{ + "v": 1, + "fingerprint": "b3:9f2c…", + "pubkey": "ed25519:…", + "channel": "mdns | dial-back | relay | manual | qr", + "proposed_caveats": [ … ], // Caveats; the requested ceiling + "observed": { "addr_candidates": [ … ] } // candidates, never load-bearing +} +``` + +On receipt, an implementation MUST verify `fingerprint == blake3(pubkey)` +and reject on mismatch **before** any surface renders it (self-certification +is checked by the library, not delegated to the human). + +### 3.5 PinRecord / GrantRecord (the chained store) + +Durable outcomes are payloads of `MerkleNode`: + +```json +{ + "parents": ["cid:…"], // ⌀ only for genesis + "payload": { + "v": 1, + "fingerprint": "b3:9f2c…", + "pubkey": "ed25519:…", + "channel": "qr", + "caveats": [ … ], // the granted meet, not the request + "decision": { "grant": { "verb": "pin", "scope": "always" } }, + "presence": { "kind": "passkey", "discharge": "…" }, // optional; §5.3 + "granted_at": "2026-07-15T21:04:00Z" // provenance data, not validity + }, + "sig": "ed25519:…" // over the node's ContentId +} +``` + +The signature covers the `ContentId`; the `ContentId` covers the parents. +Consequences in §5.1. + +### 3.6 DecisionSurface (the seam) + +Language-idiomatic equivalents of: + +```rust +#[async_trait] +pub trait DecisionSurface { + async fn decide(&self, req: PermissionRequest) -> Decision; +} +``` + +Policy files (#220) are the headless implementation of this trait. Client +libraries in Python/Dart/TypeScript expose the same shape over the JSON +wire objects. + +## 4. The Laws (normative) + +Six laws. Each carries a proof obligation (PO); §6.2 maps POs to the formal +track. Per the governance rule (§7), nothing joins this section without a +proof obligation demanding it. + +### L1 — Resolution is a meet + +Verdicts are totally ordered by restrictiveness +(`deny ⊏ passkey ⊏ ask ⊏ approve`). Resolution of request `q` against rule +set `R`: + +``` +resolve(R, q) = ⨅ { verdict(r) | r ∈ R, r matches q } +``` + +**Consequence:** ⨅ is associative, commutative, idempotent ⇒ resolution is +independent of rule order, file order, and load order. No ordering attack +exists. **PO-1.** + +### L2 — Tamper-monotonicity + +For any mutation `m` of the policy store made **without** the signing key: + +``` +resolve(m(R), q) ⊑ resolve(R, q) +``` + +A disk-write attacker can only narrow authority, never widen it. Forged +restrictive entries are a nuisance; forged loosening entries are dropped at +load (verification is fail-closed). + +**Hypothesis H1 (no-replay):** `m` cannot undetectably reintroduce a +previously-signed loosening entry. H1 is discharged by the chained store +(§5.1), not assumed. **PO-2** (proved under H1; H1's discharge is PO-2a). + +### L3 — Fail-closed totality + +`resolve` is total: every request yields a verdict; no input reaches +"undefined permission." The interactive bottom is `ask`. Absent a bound +surface, interaction-requiring verdicts degrade restrictively: + +``` +headless: ask ↦ deny, passkey ↦ deny (degradation is ⊑-monotone) +``` + +**PO-3.** + +### L4 — Attenuation + +Authority composes by meet and never amplifies: + +``` +effective = granted ⊓ required granted = requested ⊓ ceiling +``` + +Escalations carry no authority: `authority(escalate) = ⊥`. +Already property-tested upstream (`meet_never_amplifies`, +agent-mesh-protocol); this law names the obligation the formal track +re-proves over the kernel. **PO-4.** + +### L5 — The ceremony gate + +``` +association(peer) ⇒ pinned(fingerprint(peer)) +``` + +`fingerprint = blake3(pubkey)` is self-certifying, therefore **re-key ⇒ new +fingerprint ⇒ unpinned ⇒ full re-ceremony**. No silent identity swap is +expressible. A pin is created only by (a) a `Decision::grant` from a bound +surface, or (b) a pre-pinned policy entry — which is a signed loosening +entry and therefore governed by L2. **PO-5.** + +### L6 — Completeness without escalation + +Every `DecisionMatrix` MUST be decidable with all escalations unrendered: +`verbs × scopes` is non-empty and sufficient. (A harness with no audit +surface — hermes — renders a complete chooser by omission.) **PO-6** +(structural validation). + +## 5. Mechanism (below the law line) + +Mechanisms implement or discharge the laws; they add no new ones. + +### 5.1 The chained store (discharges H1) + +Pin/grant records are `MerkleNode` in the `content-addressable` crate +(BLAKE3 `ContentId`, canonical DAG-CBOR, parent links). Because the +signature covers the `ContentId` and the `ContentId` covers the parents, +a replayed-after-deletion entry re-enters with a stale parent set: the head +no longer matches the store's, and verification rejects it loudly. This +retires the documented known-limit of flat signed files (policy.rs; #226). + +### 5.2 Signed loosening verdicts (shipped) + +The exposure is asymmetric — a forged `deny`/`ask` only narrows; a forged +`approve` widens — so signatures are required on loosening entries only. +Shipped in #226/#227 (`ExecEntry::signing_payload`, +`PolicyFile::verified_approves`, fail-closed drop). This spec inherits that +contract unchanged and extends it to pins (§3.5). + +### 5.3 Presence-attested pins + +A pin MAY carry a `presence` discharge: a WebAuthn/passkey step-up bound to +the pin's `ContentId` (the `DischargeVerifier` seam; PR #214 lineage). This +upgrades first contact from "someone at a keyboard clicked" to a +hardware-attested human decision. Optional by law, recommended for pins +whose caveat ceiling is broad. + +## 6. Conformance + +### 6.1 Shared vectors + +`tests/vectors/*.json` (to be populated with the kernel): each vector is +`(policy set, request) → verdict` or `(matrix, decision) → outcome`. All +client libraries — Rust, Python, Dart, TypeScript — MUST produce identical +results. Property suites (proptest here; hypothesis/fast-check in bindings) +check L1, L3, L4 executably. This is the kyln round-trip-law pattern, +cross-language. + +### 6.2 Formal obligations + +The decision kernel (pure `resolve`, precedence, verified-load fold; no IO, +no serde, no wall-clock) is carved for extraction by Charon and proof in +Lean via Aeneas: + +| PO | Law | Statement proved | +|---|---|---| +| PO-1 | L1 | ⨅-fold is order-independent (assoc ∘ comm ∘ idem) | +| PO-2 | L2 | keyless mutation is ⊑-monotone, under H1 | +| PO-2a | L2·H1 | chained store rejects replayed loosening entries | +| PO-3 | L3 | totality + monotone headless degradation | +| PO-4 | L4 | meet never amplifies (kernel restatement) | +| PO-5 | L5 | no association without pin; re-key forces re-ceremony | +| PO-6 | L6 | matrix validity is decidable sans escalations | + +Pilot: PO-1 and PO-2. + +### 6.3 Consumer checklist + +A conforming harness: + +- [ ] binds a `DecisionSurface` (interactive) or policy files (headless) — + or accepts the L3 degradation to deny +- [ ] renders `verbs × scopes` completely; MAY render escalations (L6) +- [ ] treats `default` as a cursor hint, never an auto-grant +- [ ] never persists a loosening outcome without a signature (L2) +- [ ] relies on the library's self-certification check (§3.4) rather than + asking the human to compare key bytes +- [ ] ships no rendering into any agent-* library crate + +## 7. Governance — law minimalism + +A good system has only the laws it absolutely needs. **Nothing enters §4 +without a proof obligation demanding it; everything else is mechanism +(§5).** A standing audit hunts merges the algebra suggests — first +candidate: L1+L4 are one law ("authority composes by meet") on two carriers +(verdict lattice, caveat lattice); if the Lean formulation unifies them +cleanly, six becomes five. + +## 8. Relations + +- #220 — verdict/policy TOML contract (headless half of this seam) +- #225 — design directive, strategy, client-lib matrix (umbrella) +- #226 / #227 — signed loosening entries (shipped mechanism, §5.2) +- PR #214 — presence/WebAuthn lineage (§5.3) +- agent-mesh#65 — `Introduction` struct and mesh decision surfaces +- newt-agent#1209 — first consumer: pinning ceremony (HIGH) +- agent-mesh `docs/decisions/floating_identity.md` — identity doctrine + (law 5 there = L5 here, seen from the transport) +- `content-addressable` crate — `ContentId`, canonical DAG-CBOR, + `MerkleNode` (§5.1) From 2acdb1f0cc744bc7799ae92300729f8a59a43f56 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 07:24:37 -0400 Subject: [PATCH 02/13] =?UTF-8?q?docs(spec):=20v0.1.1=20=E2=80=94=20attest?= =?UTF-8?q?=20verb,=20chain-store,=20L2=20honesty,=20L6=20=E2=86=92=20WF-1?= =?UTF-8?q?=20(five=20laws)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review-1 revision (#229): - Third disposition surfaced in the matrix as 'attest' (né passkey; the verdict renames via #231, term from the attestation literature). Its scope composes with the existing once/session — attest×session is the one-ceremony-per-generation affordance; kernel stays clock-free (generations bump at the boundary). - 'chain-store' recorded as a term of art. Chaining strengthened per review: parents reference the LINE-CID (full predecessor incl. sig), so signature-stripping breaks the chain. Stated assumptions split: deterministic signatures (Ed25519; H(sig|content,key)=0) for reproducibility, collision resistance for tamper-evidence. JSONL formalized as the lossless at-rest view of canonical DAG-CBOR. - L2 honesty: flat files hold for additions only — deleting a durable deny widens undetectably; the chain-store is load-bearing for the full mutation set. H1 generalized to append-only-verifiability. - default = administrator opinion surface; negative pins (signed durable denies w/ provenance) as the enterprise/parental-control story. - New AuditRecord wire object (§3.6): audit = ceremony over the chain head, appended to the same chain. - L6 demoted to well-formedness WF-1: six laws → five. Governance section records the executed cut; L1+L4 merge remains the next candidate. Refs #229 review, #231, #225. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 186 +++++++++++++++++++++++++-------- 1 file changed, 145 insertions(+), 41 deletions(-) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 90d72db..a235d22 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,6 +1,7 @@ # The Ceremony Contract -**Status:** DRAFT 0.1.0 (2026-07-15) — for review. Normative once accepted. +**Status:** DRAFT 0.1.1 (2026-07-15) — revised per first review (#229). +Normative once accepted. **Scope:** the decision-surface and first-contact contract between agent-* libraries (which own decision *semantics*) and harnesses (which own *rendering*). Companion to the verdict/policy TOML contract (#220) — that @@ -23,9 +24,11 @@ this document deliberately restates none of it. |---|---|---| | **Fingerprint** | `blake3(pubkey)` — a self-certifying identity name | `agent_mesh_protocol::Fingerprint` | | **Caveats** | attenuable authority; forms a meet-semilattice | `agent_mesh_protocol::Caveats` (`meet_never_amplifies` is property-tested) | -| **Verdict** | durable disposition: `deny ⊏ passkey ⊏ ask ⊏ approve`, ordered by restrictiveness | `agent_bridle_core::policy::Verdict` (`precedence()`) | +| **Verdict** | durable disposition: `deny ⊏ attest ⊏ ask ⊏ approve`, ordered by restrictiveness | `agent_bridle_core::policy::Verdict` (`precedence()`; code says `Passkey` until #231 lands) | +| **attest** | allowed only via a presence ceremony — the term follows the trusted-computing literature's *attestation* (Parno et al.); renamed from `passkey`, which remains correct for the *hardware mechanism* only | #231 (coordinated pre-1.0 rename) | +| **chain-store** | *term of art:* the append-only-verifiable record log — parent-linked full lines, signed content CIDs | `content-addressable` `MerkleNode` + the conventions of §5.1 | | **Gate** | the enforcement choke-point; mints `ToolContext` only inside `authorize()` | `agent-bridle-core` | -| **Surface** | a consumer-supplied renderer of decisions (TUI, GUI, policy file, API) | this spec, §3.6 | +| **Surface** | a consumer-supplied renderer of decisions (TUI, GUI, policy file, API) | this spec, §3.7 | | **Escalation** | a navigation affordance (e.g. `audit`) — never authority | this spec, §3.2 | | **Pin** | a durable, provenance-carrying record that an identity's key was accepted | this spec, §3.5 | | **Ceremony** | the interactive resolution of a decision the laws refuse to default | this spec, §4 L5 | @@ -45,7 +48,7 @@ provenance *data* supplied by the boundary, never read by the kernel. Libraries define the decision **space**; consumers define the **layout**. A library crate MUST NOT contain rendering components or interactive -prompting. A consumer binds a `DecisionSurface` (§3.6); with none bound, +prompting. A consumer binds a `DecisionSurface` (§3.7); with none bound, the laws fail closed (§4 L3). ``` @@ -86,21 +89,56 @@ ordering are the consumer's. ```json { - "verbs": ["allow", "deny"], + "verbs": ["allow", "attest", "deny"], "scopes": ["once", "session"], - "default": ["allow", "once"], // rendering hint only (the ⬅ cursor) + "default": ["allow", "once"], // rendering hint AND opinion surface "escalations": ["audit"] // affordances; MAY be rendered } ``` +**The third verb, `attest`,** is the disposition "allowed, but only via a +presence ceremony." It is not new structure — it surfaces the existing +`attest` verdict (né `passkey`, #231) in the matrix. Its meaning composes +with the ordinary scopes: + +- `attest × once` — a fresh ceremony for **every** action; +- `attest × session` — **one** ceremony whose discharge covers the current + generation; prompts weaken to plain confirmation until the generation + ends. The pragmatic affordance for humans who tire of ceremony. + +No `attest_deny` exists: deny is the bottom — there is nothing to +discharge. Discharge scopes order by coverage (`once ⊑ session`), so +broader coverage is a loosening; storing one durably is governed by L2. +The kernel never reads a clock: a discharge binds to a **generation**, and +the *boundary* bumps generations for any reason it likes — a timer, a +screen lock, a lid close. Wall-clock lives in consumer-land as one bump +trigger among several; authority math stays clock-free. + +**`default` is an opinion surface.** It is a rendering hint (the ⬅ cursor) +— never an auto-grant — and it is exactly where an *administrator or +packager* expresses judgment: `rm -rf` ships as `["deny", "session"]`. +Pre-populated matrices plus durable **negative pins** (signed deny records +in the chain-store, §5.1, carrying provenance — *forbidden at Gate g, by +Fingerprint admin, during Ceremony c* — and transportable over agent-mesh +or configuration) are the enterprise-affordance / parental-control story. +Mechanism, not law: provenance and non-removability come from the +chain-store; the authority math is unchanged. + The scope vocabulary is open: this spec fixes the laws over scopes, not the set. A durable scope (e.g. `always`) materializes as a **signed loosening entry** in the policy store and is therefore governed by L2. +**Well-formedness (WF-1).** Every matrix MUST be decidable with all +escalations unrendered: `verbs × scopes` is non-empty and sufficient. (A +harness with no audit surface — hermes — renders a complete chooser by +omission.) This is a structural predicate on the wire object, checked in +conformance (§6.2) — deliberately *not* a law; see §7. + ### 3.3 Decision ```json { "v": 1, "grant": { "verb": "allow", "scope": "session" } } +{ "v": 1, "grant": { "verb": "attest", "scope": "session" } } // + presence discharge { "v": 1, "escalate": "audit" } ``` @@ -126,13 +164,13 @@ On receipt, an implementation MUST verify `fingerprint == blake3(pubkey)` and reject on mismatch **before** any surface renders it (self-certification is checked by the library, not delegated to the human). -### 3.5 PinRecord / GrantRecord (the chained store) +### 3.5 PinRecord / GrantRecord (the chain-store) Durable outcomes are payloads of `MerkleNode`: ```json { - "parents": ["cid:…"], // ⌀ only for genesis + "parents": ["cid:…"], // line-CIDs of predecessors, sig INCLUDED (§5.1); ⌀ only for genesis "payload": { "v": 1, "fingerprint": "b3:9f2c…", @@ -143,14 +181,34 @@ Durable outcomes are payloads of `MerkleNode`: "presence": { "kind": "passkey", "discharge": "…" }, // optional; §5.3 "granted_at": "2026-07-15T21:04:00Z" // provenance data, not validity }, - "sig": "ed25519:…" // over the node's ContentId + "sig": "ed25519:…" // over the content-CID (presigned form; §5.1) +} +``` + +Two CIDs per record: the **content-CID** (canonical form *minus* `sig` — +what is signed) and the **line-CID** (canonical form *including* `sig` — +what descendants reference in `parents`). Consequences in §5.1. + +### 3.6 AuditRecord + +An audit is a ceremony whose subject is the chain head: a fingerprint +witnesses the store's state and signs what it saw. + +```json +{ + "v": 1, + "witnessed_head": "cid:…", // line-CID of the head at review time + "fingerprint": "b3:…", // the witness + "presence": { "kind": "passkey", "discharge": "…" }, // literal finger on hardware + "decision": { "grant": { "verb": "attest", "scope": "once" } } } ``` -The signature covers the `ContentId`; the `ContentId` covers the parents. -Consequences in §5.1. +It is appended to the chain-store like any record (its own content-CID, +sig, parents), so audits are themselves tamper-evident and auditable. No +new law — L5 composed with the log; one new record type. -### 3.6 DecisionSurface (the seam) +### 3.7 DecisionSurface (the seam) Language-idiomatic equivalents of: @@ -167,14 +225,14 @@ wire objects. ## 4. The Laws (normative) -Six laws. Each carries a proof obligation (PO); §6.2 maps POs to the formal -track. Per the governance rule (§7), nothing joins this section without a -proof obligation demanding it. +Five laws. Each carries a proof obligation (PO); §6.2 maps POs to the +formal track. Per the governance rule (§7), nothing joins this section +without a proof obligation demanding it. ### L1 — Resolution is a meet Verdicts are totally ordered by restrictiveness -(`deny ⊏ passkey ⊏ ask ⊏ approve`). Resolution of request `q` against rule +(`deny ⊏ attest ⊏ ask ⊏ approve`). Resolution of request `q` against rule set `R`: ``` @@ -197,9 +255,19 @@ A disk-write attacker can only narrow authority, never widen it. Forged restrictive entries are a nuisance; forged loosening entries are dropped at load (verification is fail-closed). -**Hypothesis H1 (no-replay):** `m` cannot undetectably reintroduce a -previously-signed loosening entry. H1 is discharged by the chained store -(§5.1), not assumed. **PO-2** (proved under H1; H1's discharge is PO-2a). +**Mechanism honesty:** under flat policy files this law holds for +**additions only**. *Deleting* a restrictive entry (a durable deny) widens +authority, and flat files cannot detect the deletion — signatures guard +loosening additions, not restrictive removals. Extending L2 to the full +mutation set {add, delete, reorder} **requires the chain-store** (§5.1); +the chain is load-bearing for this law, not an optimization. (Surfaced by +the negative-pins review thread: the teenager with disk access deleting +the deny row is the threat model.) + +**Hypothesis H1 (append-only-verifiability):** `m` cannot undetectably +remove a record or reintroduce a previously-signed one. H1 is discharged by +the chain-store (§5.1), not assumed. **PO-2** (proved under H1; H1's +discharge is PO-2a). ### L3 — Fail-closed totality @@ -208,7 +276,7 @@ previously-signed loosening entry. H1 is discharged by the chained store surface, interaction-requiring verdicts degrade restrictively: ``` -headless: ask ↦ deny, passkey ↦ deny (degradation is ⊑-monotone) +headless: ask ↦ deny, attest ↦ deny (degradation is ⊑-monotone) ``` **PO-3.** @@ -238,25 +306,51 @@ expressible. A pin is created only by (a) a `Decision::grant` from a bound surface, or (b) a pre-pinned policy entry — which is a signed loosening entry and therefore governed by L2. **PO-5.** -### L6 — Completeness without escalation - -Every `DecisionMatrix` MUST be decidable with all escalations unrendered: -`verbs × scopes` is non-empty and sufficient. (A harness with no audit -surface — hermes — renders a complete chooser by omission.) **PO-6** -(structural validation). - ## 5. Mechanism (below the law line) Mechanisms implement or discharge the laws; they add no new ones. -### 5.1 The chained store (discharges H1) +### 5.1 The chain-store (load-bearing for L2) + +Records are `MerkleNode` in the `content-addressable` crate (BLAKE3 +CIDs, canonical DAG-CBOR, parent links), with these conventions: -Pin/grant records are `MerkleNode` in the `content-addressable` crate -(BLAKE3 `ContentId`, canonical DAG-CBOR, parent links). Because the -signature covers the `ContentId` and the `ContentId` covers the parents, -a replayed-after-deletion entry re-enters with a stale parent set: the head -no longer matches the store's, and verification rejects it loudly. This -retires the documented known-limit of flat signed files (policy.rs; #226). +``` +c_i = H(canon(record_i ∖ sig)) content-CID (what is signed) +s_i = Sign(k, c_i) the signature (sig-trim convention) +line_i = record_i ∪ { sig: s_i } the full at-rest line +ℓ_i = H(canon(line_i)) line-CID (what parents reference) +parents(record_{i+1}) ∋ ℓ_i descendants commit to content AND sig +``` + +Parents reference the **line-CID** — the full predecessor *including its +signature* — so stripping or swapping a historical signature breaks the +chain just as surely as editing content. Removing a record orphans the +head; replaying a deleted record re-enters with a stale parent set. Both +verify-fail loudly. This is what discharges H1 and extends L2 to +deletions, retiring the documented known-limit of flat signed files +(policy.rs; #226). + +Two stated assumptions, doing different jobs: + +1. **Deterministic signatures.** `H(sig | content, key) = 0` — given the + content and the key, the signature carries no fresh entropy. Ed25519 + provides exactly this (RFC 8032 derives the nonce from key and + message), so the whole log is a **pure function** of (genesis, payload + sequence, keys). A randomized scheme (ECDSA with random nonce) has + `H(sig | content, key) > 0`: two honest signings of identical content + yield different line-CIDs, and the chain forks on any re-sign. The + entropy identity is the proof obligation that forces a deterministic + scheme — it governs *reproducibility*. +2. **Collision resistance of `H`** governs *tamper-evidence*. The two are + independent: determinism makes the log replayable; collision resistance + makes it unforgeable. + +**At rest:** the log's on-disk representation is **JSONL as a lossless +line-oriented view** of the canonical DAG-CBOR records — one `line_i` per +line, human-auditable, no comments or other affordances to make +canonicalization ambiguous. CIDs and signatures are always computed over +the canonical form, never over the view. ### 5.2 Signed loosening verdicts (shipped) @@ -295,11 +389,11 @@ Lean via Aeneas: |---|---|---| | PO-1 | L1 | ⨅-fold is order-independent (assoc ∘ comm ∘ idem) | | PO-2 | L2 | keyless mutation is ⊑-monotone, under H1 | -| PO-2a | L2·H1 | chained store rejects replayed loosening entries | +| PO-2a | L2·H1 | chain-store rejects removed and replayed records | | PO-3 | L3 | totality + monotone headless degradation | | PO-4 | L4 | meet never amplifies (kernel restatement) | | PO-5 | L5 | no association without pin; re-key forces re-ceremony | -| PO-6 | L6 | matrix validity is decidable sans escalations | +| WF-1 | §3.2 | matrix decidable sans escalations (structural predicate, not a law) | Pilot: PO-1 and PO-2. @@ -309,7 +403,7 @@ A conforming harness: - [ ] binds a `DecisionSurface` (interactive) or policy files (headless) — or accepts the L3 degradation to deny -- [ ] renders `verbs × scopes` completely; MAY render escalations (L6) +- [ ] renders `verbs × scopes` completely; MAY render escalations (WF-1, §3.2) - [ ] treats `default` as a cursor hint, never an auto-grant - [ ] never persists a loosening outcome without a signature (L2) - [ ] relies on the library's self-certification check (§3.4) rather than @@ -320,16 +414,26 @@ A conforming harness: A good system has only the laws it absolutely needs. **Nothing enters §4 without a proof obligation demanding it; everything else is mechanism -(§5).** A standing audit hunts merges the algebra suggests — first -candidate: L1+L4 are one law ("authority composes by meet") on two carriers -(verdict lattice, caveat lattice); if the Lean formulation unifies them -cleanly, six becomes five. +(§5) or well-formedness (§3).** The count is audited ruthlessly: + +- **Executed (review 1, 2026-07-15):** L6 demoted to WF-1 — completeness + without escalation is a structural predicate on a wire object, not an + algebraic invariant of authority. Six laws became five. +- **Next candidate:** L1+L4 are one law ("authority composes by meet") on + two carriers (verdict lattice, caveat lattice); if the Lean formulation + unifies them cleanly, five becomes four. + +Additions from the same review — the `attest` verb, negative pins, the +`AuditRecord` — cost **zero** laws: each collapsed into existing structure +or landed below the line. The algebra decides the count; ambition doesn't. ## 8. Relations - #220 — verdict/policy TOML contract (headless half of this seam) - #225 — design directive, strategy, client-lib matrix (umbrella) - #226 / #227 — signed loosening entries (shipped mechanism, §5.2) +- #231 — coordinated pre-1.0 rename `passkey` → `attest` (code catch-up + for this spec's vocabulary) - PR #214 — presence/WebAuthn lineage (§5.3) - agent-mesh#65 — `Introduction` struct and mesh decision surfaces - newt-agent#1209 — first consumer: pinning ceremony (HIGH) From fb090a688f7ae5ac689dd5999acf5b8dc4b990af Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 09:22:00 -0400 Subject: [PATCH 03/13] =?UTF-8?q?docs(spec):=20v0.1.2=20=E2=80=94=20identi?= =?UTF-8?q?ty=20model:=20roles,=20delegation=20via=20chain-to-principal,?= =?UTF-8?q?=20SAS=20enrollment,=20external=20anchors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every participant is a fingerprint; roles (principal/agent/surface/gate) differ only in what they sign — a monolith newt is the degenerate case of one fingerprint wearing all roles, a no-compute mobile surface the other pole (keypair + renderer). Delegation completes L5's pinned predicate transitively: chains meet at a common pinned principal, root involved at ISSUANCE not per-delegation (headless fleets verify offline; already shipped mechanism — mesh CertChain #39 §9.1, proof-of-possession #40 §9.2). Zero new laws. Mechanism: §5.4 SAS enrollment ceremony — commit-then-reveal bound to the exchange transcript, human comparison as the unspoofable channel (naive phrase-echo is MITM-relayable); paranoia parameterized as (SAS entropy × rounds, witnesses, presence) with minima set by caveat ceiling. §5.5 external anchors (GitHub keys, DNS, org CA) = candidate corroboration channels for a self-sovereign root — never load-bearing, per the floating-identity doctrine; no-GitHub users enroll by ceremony alone. Remote surfaces MUST sign their Decisions (attributable grant provenance); in-process surfaces MAY omit. Refs #229 review discussion, #225; protocol work: agent-mesh companion issue. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 94 +++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index a235d22..48c677f 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -23,6 +23,7 @@ this document deliberately restates none of it. | Term | Definition | Already shipped as | |---|---|---| | **Fingerprint** | `blake3(pubkey)` — a self-certifying identity name | `agent_mesh_protocol::Fingerprint` | +| **Principal** | the root identity a human (or org) controls; agents and surfaces chain to it | `agent_mesh_protocol::UserKey` | | **Caveats** | attenuable authority; forms a meet-semilattice | `agent_mesh_protocol::Caveats` (`meet_never_amplifies` is property-tested) | | **Verdict** | durable disposition: `deny ⊏ attest ⊏ ask ⊏ approve`, ordered by restrictiveness | `agent_bridle_core::policy::Verdict` (`precedence()`; code says `Passkey` until #231 lands) | | **attest** | allowed only via a presence ceremony — the term follows the trusted-computing literature's *attestation* (Parno et al.); renamed from `passkey`, which remains correct for the *hardware mechanism* only | #231 (coordinated pre-1.0 rename) | @@ -62,6 +63,32 @@ the laws fail closed (§4 L3). └──────────────────────────┘ ``` +### 2.1 Who has an identity + +**Every participant is a fingerprint.** Identity is universal; *roles* +differ in which wire objects a key signs: + +| Role | Holds | Signs | +|---|---|---| +| **Principal** | the root keypair (vault/hardware-resident) | issuance of agent & surface certs; durable loosening entries | +| **Agent** | a keypair chained to a principal | envelopes, requests, delegations | +| **Surface** | a keypair chained to a principal — possibly *nothing else* (a no-compute device is a keypair + a renderer) | its `Decision`s and `AuditRecord`s | +| **Gate** | the enforcement identity (usually = its agent) | chain-store appends | + +A monolith (newt-agent carrying bridle + mesh in-process) is the +**degenerate case**: one fingerprint wearing every role. Delegation is the +roles splitting into separate keypairs — nothing else changes. An +in-process surface MAY omit signing its `Decision`s (same trust domain as +the gate); a **remote** surface MUST sign them, so grant provenance in the +chain-store is attributable end to end. + +**Delegation** (agent → agent, e.g. newt to a back-end worker): both +endpoints present chains to a **common pinned principal** (L5). The +principal's private key is involved at *issuance*, not per-delegation — +headless fleets verify chains offline while the root key stays in +hardware; revocation is a generation bump. Cross-principal delegation is +two principals pinning each other's roots — L5 again, one level up. + ## 3. Wire objects Field names are normative; unknown fields MUST be ignored (forward @@ -145,6 +172,10 @@ conformance (§6.2) — deliberately *not* a law; see §7. `escalate` carries **zero authority** (L4): it navigates the human to a richer surface; the request remains undecided until a `grant` returns. +A **remote** surface MUST sign its decisions (append `"by": `, +`"sig"` over the content-CID) so grant provenance is attributable end to +end; an in-process surface MAY omit this (§2.1). + ### 3.4 Introduction First contact: an unpinned identity proposing itself. @@ -304,7 +335,21 @@ association(peer) ⇒ pinned(fingerprint(peer)) fingerprint ⇒ unpinned ⇒ full re-ceremony**. No silent identity swap is expressible. A pin is created only by (a) a `Decision::grant` from a bound surface, or (b) a pre-pinned policy entry — which is a signed loosening -entry and therefore governed by L2. **PO-5.** +entry and therefore governed by L2. + +The pinned predicate is **transitive through certification**: + +``` +pinned(fp) ⇔ fp ∈ PinSet + ∨ ∃ chain: fp →* root, root ∈ PinSet, + proof-of-possession at every link +``` + +so pinning a principal admits the agents and surfaces it issues — this is +delegation (§2.1), and it is already shipped mechanism: mesh `CertChain:: +verify` chains to the user root fail-closed (agent-mesh #39, §9.1) and +certification requires proof-of-possession (agent-mesh #40, §9.2). +**PO-5** (now including chain soundness). ## 5. Mechanism (below the law line) @@ -368,6 +413,53 @@ upgrades first contact from "someone at a keyboard clicked" to a hardware-attested human decision. Optional by law, recommended for pins whose caveat ceiling is broad. +### 5.4 Enrollment ceremony (SAS pairing) + +How a new device or surface — possibly with no compute beyond key storage +and a screen — gets its keypair admitted under a principal (L5, applied to +one's own devices). + +The ritual is a **short-authentication-string comparison**, and its shape +matters: a naive "new device shows a phrase, trusted device sends it back +encrypted" is MITM-relayable — an attacker owning the channel relays the +phrase both ways and each side verifies *the attacker's* key. The sound +construction (per Bluetooth numeric comparison / ZRTP): + +1. both devices **commit** to nonces before revealing anything; +2. both **derive** the SAS from the *entire key-exchange transcript* + (commitments, both pubkeys, reveals); +3. **a human compares the SAS on both screens** — the out-of-band channel + the MITM cannot sit on. The phrase is not a secret to transport; it is + a checksum of the handshake two screens must agree on. + +Commit-before-reveal forces a MITM to *guess* the SAS in advance; one +round of a `xxx-000`-style SAS ≈ 1-in-46k. Paranoia is then a parameter, +not a mood: + +``` +strength(enrollment) = (SAS entropy × rounds, distinct witnesses, presence) +``` + +Policy sets minima **by caveat ceiling**: a broad ceiling demands more +rounds, witnessing from ≥ 2 previously-secured surfaces, and a hardware +presence discharge (the `attest` verdict doing enrollment duty — +thumbprint on the device). Independent witnesses buy more than extra +rounds: rounds shrink the guess probability; witnesses multiply the +channels an attacker must own *simultaneously*. The completed enrollment +is a `PinRecord` in the chain-store whose payload carries the ceremony +parameters — auditable later (§3.6). + +### 5.5 External anchors (corroboration, never the root) + +A principal root is **self-sovereign**. Externally published keys — +GitHub-registered keys (`github.com/.keys`), DNS, an org CA, a +previously-secured device — are **candidate corroboration channels** for +it: independent witnesses that the root you are pinning belongs to the +human you think it does. Per the floating-identity doctrine, no anchor is +load-bearing: GitHub corroborates the root; it never *is* the root. A +user with no GitHub enrolls by ceremony alone (§5.4) with zero degradation +in the algebra — anchors raise corroboration, never gate participation. + ## 6. Conformance ### 6.1 Shared vectors From 031210db15c5d99e7a17941f33226cc7ed811f41 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 09:29:45 -0400 Subject: [PATCH 04/13] =?UTF-8?q?docs(spec):=20v0.1.3=20=E2=80=94=20L2=20t?= =?UTF-8?q?amper-boundedness=20(quorum=20revocation),=20anchor=20blessing,?= =?UTF-8?q?=20MITM=20ledger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 2 + the no-MITM-hole-anywhere sweep: - L2 renamed Tamper-boundedness and given its upward direction: sub-quorum actors can neither widen authority NOR shrink load-bearing identity structure. Reversible narrowing (deny-spam) stays nuisance-bounded; irreversible narrowing (revoking an identity) requires quorum — 'reset mesh' must not be a DoS surface. Availability is a security property. PO-2b added; zero law-count change (absorbed, not appended). - RevocationRecord wire object (k-of-n signers; quorum policy is itself a principal-signed loosening entry; last-root revocation refused without succession or an explicit tombstone). Punting ≥ pinning: revocation strength floors at enrollment strength. - Anchors are blessed, participating identities: AnchorRecord binds channel+location+key under the principal's signature; any public-key display surface qualifies once blessed; revocable via the same quorum path. GitHub participates; nothing depends on it. - SAS transcript MUST include the long-term keys being enrolled (closes key-substitution relay). Decision gains a request-CID binding (closes render-swap: what the human saw is bound to what was granted). - §5.6 MITM ledger: nine channels enumerated — attack / closure / residual — with the unifying rule: the authenticated thing is always the key, never the channel. Refs #229 review round 2, #225; protocol: agent-mesh#66. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 120 ++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 16 deletions(-) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 48c677f..543aac3 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,6 +1,6 @@ # The Ceremony Contract -**Status:** DRAFT 0.1.1 (2026-07-15) — revised per first review (#229). +**Status:** DRAFT 0.1.3 (2026-07-15) — revised per review rounds 1–2 (#229). Normative once accepted. **Scope:** the decision-surface and first-contact contract between agent-* libraries (which own decision *semantics*) and harnesses (which own @@ -172,9 +172,19 @@ conformance (§6.2) — deliberately *not* a law; see §7. `escalate` carries **zero authority** (L4): it navigates the human to a richer surface; the request remains undecided until a `grant` returns. -A **remote** surface MUST sign its decisions (append `"by": `, -`"sig"` over the content-CID) so grant provenance is attributable end to -end; an in-process surface MAY omit this (§2.1). +A **remote** surface MUST sign its decisions — and the decision MUST name +the request it answered: + +```json +{ "v": 1, "request": "cid:…", // content-CID of the PermissionRequest AS RENDERED + "grant": { "verb": "allow", "scope": "once" }, + "by": "b3:…", "sig": "…" } // sig over this record's content-CID +``` + +The gate accepts a decision only if `request` matches the CID of the +request it actually issued — binding *what the human saw* to *what was +granted*, which closes the render-swap MITM (§5.6). An in-process surface +MAY omit `by`/`sig` (§2.1); the `request` binding is unconditional. ### 3.4 Introduction @@ -220,7 +230,7 @@ Two CIDs per record: the **content-CID** (canonical form *minus* `sig` — what is signed) and the **line-CID** (canonical form *including* `sig` — what descendants reference in `parents`). Consequences in §5.1. -### 3.6 AuditRecord +### 3.6 AuditRecord & RevocationRecord (ceremonies over the store) An audit is a ceremony whose subject is the chain head: a fingerprint witnesses the store's state and signs what it saw. @@ -237,7 +247,28 @@ witnesses the store's state and signs what it saw. It is appended to the chain-store like any record (its own content-CID, sig, parents), so audits are themselves tamper-evident and auditable. No -new law — L5 composed with the log; one new record type. +new law — L5 composed with the log; one new record type. Audits double as +**freshness checkpoints**: a peer presenting a head must show it extends +the last head this participant witnessed (rollback resistance, §5.6). + +**RevocationRecord** — punting a load-bearing identity is itself a +ceremony, and per L2 it demands **quorum**: + +```json +{ + "v": 1, + "revoke": "b3:…", // the fingerprint being punted + "reason": "compromise | rotation | retirement", + "succession": "b3:…", // optional successor (root rotation) + "signers": [ { "by": "b3:…", "sig": "…" }, … ] // k-of-n per quorum policy +} +``` + +The quorum policy (k, n, eligible signers) is itself a principal-signed +loosening entry established at setup — defining *who may revoke* is +authority structure, governed by L2 like any other loosening. Revoking the +**last** root without a `succession` ends the mesh; implementations MUST +refuse it unless the record carries an explicit `"tombstone": true`. ### 3.7 DecisionSurface (the seam) @@ -274,17 +305,27 @@ resolve(R, q) = ⨅ { verdict(r) | r ∈ R, r matches q } independent of rule order, file order, and load order. No ordering attack exists. **PO-1.** -### L2 — Tamper-monotonicity +### L2 — Tamper-boundedness -For any mutation `m` of the policy store made **without** the signing key: +For any mutation `m` made by a party holding **fewer than quorum(target)** +of the designated keys: ``` -resolve(m(R), q) ⊑ resolve(R, q) +resolve(m(R), q) ⊑ resolve(R, q) (no widening) +LoadBearing(R) ⊆ LoadBearing(m(R)) (no structural narrowing) ``` -A disk-write attacker can only narrow authority, never widen it. Forged -restrictive entries are a nuisance; forged loosening entries are dropped at -load (verification is fail-closed). +Two directions, one law. **Downward:** a sub-quorum actor can only narrow +*authority*, never widen it — forged restrictive entries are a nuisance; +forged loosening entries are dropped at load (verification is +fail-closed). **Upward:** a sub-quorum actor cannot shrink the +**load-bearing identity structure** — pinned principals, enrolled devices, +blessed anchors. Narrowing splits into two species: *reversible* narrowing +(a spammed deny — the principal can prune it) is nuisance-bounded and +needs no key; **irreversible narrowing — revoking a load-bearing identity +— requires quorum**, because a fail-closed system's own failure mode is an +adversary who can *force* closure. "Reset mesh" must not be a +denial-of-service surface. Availability is a security property. **Mechanism honesty:** under flat policy files this law holds for **additions only**. *Deleting* a restrictive entry (a durable deny) widens @@ -298,7 +339,7 @@ the deny row is the threat model.) **Hypothesis H1 (append-only-verifiability):** `m` cannot undetectably remove a record or reintroduce a previously-signed one. H1 is discharged by the chain-store (§5.1), not assumed. **PO-2** (proved under H1; H1's -discharge is PO-2a). +discharge is PO-2a; quorum soundness for revocation is PO-2b). ### L3 — Fail-closed totality @@ -426,8 +467,13 @@ phrase both ways and each side verifies *the attacker's* key. The sound construction (per Bluetooth numeric comparison / ZRTP): 1. both devices **commit** to nonces before revealing anything; -2. both **derive** the SAS from the *entire key-exchange transcript* - (commitments, both pubkeys, reveals); +2. both **derive** the SAS from the *entire key-exchange transcript* — + commitments, reveals, **and the long-term public keys being enrolled**. + This last inclusion is not optional: an SAS over only the ephemeral + session material lets a MITM relay the handshake honestly while + substituting the long-term keys — the classic key-substitution hole + (§5.6). The SAS must checksum *what is being pinned*, not merely the + channel; 3. **a human compares the SAS on both screens** — the out-of-band channel the MITM cannot sit on. The phrase is not a secret to transport; it is a checksum of the handshake two screens must agree on. @@ -449,6 +495,12 @@ channels an attacker must own *simultaneously*. The completed enrollment is a `PinRecord` in the chain-store whose payload carries the ceremony parameters — auditable later (§3.6). +**Punting ≥ pinning.** Revocation (§3.6) is graded on the same scale: +removing an identity demands at least the ceremony strength that enrolled +it — quorum co-signers being the witness axis. Enrollment strength sets a +floor the revocation ceremony must meet, so the strongest identities are +exactly the ones an adversary finds hardest to destroy. + ### 5.5 External anchors (corroboration, never the root) A principal root is **self-sovereign**. Externally published keys — @@ -460,6 +512,37 @@ load-bearing: GitHub corroborates the root; it never *is* the root. A user with no GitHub enrolls by ceremony alone (§5.4) with zero degradation in the algebra — anchors raise corroboration, never gate participation. +**Anchors are blessed, participating identities.** ANY public-key display +surface qualifies — *provided the key owner blesses it*: an `AnchorRecord` +(principal-signed binding of channel + location + displayed key) appended +to the chain-store. An unblessed anchor is ignored; a blessed one may +*participate* as a signing/corroboration surface in ceremonies (a +GitHub-key signature counting as one enrollment witness). Blessings are +revoked like any load-bearing identity — RevocationRecord, quorum (§3.6) +— so a captured anchor can be cut loose without ceremony-strength loss: +corroboration is k-of-n, and n just shrank by one. + +### 5.6 The MITM ledger — every channel, every closure + +"No MITM hole anywhere" is a claim to *enumerate*, not to feel: + +| Channel | Attack | Closure | Residual | +|---|---|---|---| +| First contact (stranger) | TOFU key swap | L5 ceremony: SAS or out-of-band fingerprint check; or chain to a common pinned principal | the ceremony itself (below) | +| Enrollment handshake | relay + **key substitution** | commit-then-reveal; SAS covers the **long-term keys** (§5.4·2); human comparison is the authentic channel | SAS guess ≈ n⁻ᵏ per round; rounds × witnesses shrink it | +| Post-pin transport | impersonation on any raced path | dial-by-pubkey: QUIC/TLS authenticates the **node key** — a path that answers must hold the private key; paths are hints, never identity | key theft (out of scope: L2 quorum + revocation) | +| Delegation | rogue "delegated" agent inserted | chains verify to a pinned principal; proof-of-possession at issuance (mesh #39/#40) | principal-root compromise (quorum revocation + succession) | +| Remote surface | **render-swap**: human approves X, gate runs Y | `Decision.request` = content-CID of the request as rendered; sig covers it; gate matches CIDs (§3.3) | compromised surface *device* → its grants are attributable + revocable | +| Chain-store sync | forgery in transit | records are self-authenticating (signed + chained); transport can corrupt nothing silently | — | +| Chain-store sync | **rollback** (stale head hides a revocation) | heads are monotonic (L2 structural clause); AuditRecords are witnessed freshness checkpoints (§3.6) | withholding = visible staleness → treat as degraded, fail closed | +| Anchor channel | compromised registry vouches a fake root | anchors are blessed, k-of-n, never sufficient alone (§5.5) | k−1 colluding anchors corroborate nothing | +| The human | prompt fatigue / phishing the ceremony | `attest` for high ceilings; distinct ceremony UI is consumer guidance (newt#1209) | irreducible; parameterized paranoia exists for exactly this | + +The pattern behind every row: **the authenticated thing is always the +key, never the channel** — locations, relays, registries, and rendered +pixels are candidates and hints; signatures and CIDs are what the gate +trusts. One doctrine, applied ten times. + ## 6. Conformance ### 6.1 Shared vectors @@ -480,8 +563,9 @@ Lean via Aeneas: | PO | Law | Statement proved | |---|---|---| | PO-1 | L1 | ⨅-fold is order-independent (assoc ∘ comm ∘ idem) | -| PO-2 | L2 | keyless mutation is ⊑-monotone, under H1 | +| PO-2 | L2 | sub-quorum mutation is ⊑-monotone, under H1 | | PO-2a | L2·H1 | chain-store rejects removed and replayed records | +| PO-2b | L2 | a sub-quorum coalition cannot shrink the load-bearing pin set | | PO-3 | L3 | totality + monotone headless degradation | | PO-4 | L4 | meet never amplifies (kernel restatement) | | PO-5 | L5 | no association without pin; re-key forces re-ceremony | @@ -511,6 +595,10 @@ without a proof obligation demanding it; everything else is mechanism - **Executed (review 1, 2026-07-15):** L6 demoted to WF-1 — completeness without escalation is a structural predicate on a wire object, not an algebraic invariant of authority. Six laws became five. +- **Executed (review 2, 2026-07-15):** the revocation-DoS invariant + ("reset mesh" must not be an attack surface) was absorbed into L2 as its + upward direction — tamper-*monotonicity* became tamper-*boundedness*. + Zero count change; PO-2b added. - **Next candidate:** L1+L4 are one law ("authority composes by meet") on two carriers (verdict lattice, caveat lattice); if the Lean formulation unifies them cleanly, five becomes four. From 7703bacaa32a1f612786b9dccbfe43b997dcd504 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 09:48:57 -0400 Subject: [PATCH 05/13] =?UTF-8?q?docs(spec):=20v0.1.4=20=E2=80=94=20Memo?= =?UTF-8?q?=20discipline=20(WF-2),=20PoP=20introductions,=20gate-signed=20?= =?UTF-8?q?requests,=20multihash=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 3, from the sig-everywhere / Memo / multihash discussion: - Memo discipline (§3 preamble, WF-2): every wire object is a Memo-descendant — content-CID unconditional, by+sig at trust boundaries (remote MUST / in-process MAY omit), parents for durability, Sealed at load (verify at construction, immutable after — the Rust heir of Memo's read-time tamper check). All of the data layer, none of the resource layer. - Two sig gaps closed: Introduction gains proof-of-possession (introduced key signs over transcript-bound freshness — unknown-key-share/replay closure, parity with mesh #40); remote PermissionRequest gains the gate's signature, verified before rendering (phishing-canvas closure). Two new MITM-ledger rows. - Multihash directive: 'BLAKE3 is an implementation detail, not a law.' L5 de-algorithm'd to fingerprint = H(pubkey) with required properties; identifiers self-describing (multihash/multicodec/CIDv1), compared opaque including the code; hash rotation = re-naming ceremony (signed linkage; identity never moves), signature rotation = re-keying (full L5). New §8 Profile v1 (pins + the property each pin must preserve); Relations renumbered to §9. Fingerprint wire-format change tracked on agent-mesh#66. - Governance: review-3 executed line — zero law-count change, still five. Refs #229 review round 3, #225, agent-mesh#66. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 109 ++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 543aac3..20d6536 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,6 +1,6 @@ # The Ceremony Contract -**Status:** DRAFT 0.1.3 (2026-07-15) — revised per review rounds 1–2 (#229). +**Status:** DRAFT 0.1.4 (2026-07-16) — revised per review rounds 1–3 (#229). Normative once accepted. **Scope:** the decision-surface and first-contact contract between agent-* libraries (which own decision *semantics*) and harnesses (which own @@ -22,8 +22,9 @@ this document deliberately restates none of it. | Term | Definition | Already shipped as | |---|---|---| -| **Fingerprint** | `blake3(pubkey)` — a self-certifying identity name | `agent_mesh_protocol::Fingerprint` | +| **Fingerprint** | `H(pubkey)` rendered as a **multihash** — a self-describing, self-certifying *name* for the key. The key is the identity; the fingerprint is its name. Algorithms are profile pins (§8), never law: *BLAKE3 is an implementation detail* | `agent_mesh_protocol::Fingerprint` (raw blake3 today; multihash wire format tracked on agent-mesh#66) | | **Principal** | the root identity a human (or org) controls; agents and surfaces chain to it | `agent_mesh_protocol::UserKey` | +| **Memo** | the ancestor discipline (content-addressable-python `data.py`): every value carries its content-id and **reads verify it**. Its Rust heirs: `ContentId` (naming), `MerkleNode` (chaining+sigs), and `Sealed` (§3 preamble — verify at construction, immutable after) | lineage; `Sealed` to build | | **Caveats** | attenuable authority; forms a meet-semilattice | `agent_mesh_protocol::Caveats` (`meet_never_amplifies` is property-tested) | | **Verdict** | durable disposition: `deny ⊏ attest ⊏ ask ⊏ approve`, ordered by restrictiveness | `agent_bridle_core::policy::Verdict` (`precedence()`; code says `Passkey` until #231 lands) | | **attest** | allowed only via a presence ceremony — the term follows the trusted-computing literature's *attestation* (Parno et al.); renamed from `passkey`, which remains correct for the *hardware mechanism* only | #231 (coordinated pre-1.0 rename) | @@ -33,13 +34,20 @@ this document deliberately restates none of it. | **Escalation** | a navigation affordance (e.g. `audit`) — never authority | this spec, §3.2 | | **Pin** | a durable, provenance-carrying record that an identity's key was accepted | this spec, §3.5 | | **Ceremony** | the interactive resolution of a decision the laws refuse to default | this spec, §4 L5 | -| **ContentId / MerkleNode** | BLAKE3 CID over canonical DAG-CBOR; parent-linked record | `content-addressable` crate | +| **ContentId / MerkleNode** | CID (multihash) over canonical DAG-CBOR; parent-linked record; v1 profile hashes with BLAKE3-256 (§8) | `content-addressable` crate | Encodings: **one schema, three encodings.** JSON for interchange (client libs), TOML at rest (#220 policy files), **canonical DAG-CBOR for anything hashed or signed**. Signatures and `ContentId`s are computed over canonical bytes only. +Identifiers are **self-describing**: fingerprints are multihash, keys and +signatures are multicodec-tagged, links are CIDv1 (multihash-native). +Comparison is over the opaque bytes *including* the code — two hash +algorithms never collide silently. The `b3:` / `ed25519:` / `cid:` +prefixes in this document's examples render the **v1 profile** (§8); the +formats themselves are algorithm-agnostic. + Time: per the workspace hard rule, **wall-clock is never a coordination primitive**. Validity keys on generation counters (`valid_for_generation`); RFC 3339 timestamps appear in records as @@ -94,6 +102,26 @@ two principals pinning each other's roots — L5 again, one level up. Field names are normative; unknown fields MUST be ignored (forward compatibility). All objects carry `"v": 1`. +**The Memo discipline (WF-2).** Every wire object is a Memo-descendant, +with capabilities attached by *mechanical criteria* — a boundary test, not +a quota: + +- **content-CID: unconditional.** Anything serializable and meaningful + beyond this process has canonical bytes and therefore a name. +- **`by` + `sig`: at trust boundaries.** REQUIRED whenever the object + crosses to a different fingerprint (remote surface, delegated agent, + another host); MAY be omitted in-process — same trust domain, nothing to + assert. +- **`parents`: for durability.** Anything appended to a chain-store links. +- **Sealed at load.** Implementations construct wire objects only through + verification (CID recomputed; sig checked when present) — verify once at + the boundary, immutable thereafter. Nothing enters a kernel unverified. + +This is const-correctness for integrity: one unsigned hop breaks the chain +of custody the way one non-const cast breaks the guarantee. The discipline +applies to **all of the data layer and none of the resource layer** — the +same line the pure kernel already draws (§6.2). + ### 3.1 PermissionRequest What a gate hands a surface when a verdict resolves to interaction. @@ -105,10 +133,17 @@ What a gate hands a surface when a verdict resolves to interaction. "action": { "class": "exec", "display": "run_command: cd " }, "violation": "outside-granted-allowlist", "matrix": { … }, // §3.2 - "context": { "session": "…", "rationale": "…", "generation": 41 } + "context": { "session": "…", "rationale": "…", "generation": 41 }, + "by": "b3:…", "sig": "…" // the GATE's signature — required remote (Memo discipline) } ``` +A **remote** surface MUST verify the request's signature and that `by` +chains to a pinned principal **before rendering** — an unauthenticated +prompt is a phishing canvas that trains the human on fake ceremonies +(§5.6), even though its harvested decision is unredeemable (the CID +binding, §3.3). In-process, `by`/`sig` MAY be omitted. + ### 3.2 DecisionMatrix The decision *space*. Nothing here encodes layout — rows, columns, and @@ -197,13 +232,21 @@ First contact: an unpinned identity proposing itself. "pubkey": "ed25519:…", "channel": "mdns | dial-back | relay | manual | qr", "proposed_caveats": [ … ], // Caveats; the requested ceiling - "observed": { "addr_candidates": [ … ] } // candidates, never load-bearing + "observed": { "addr_candidates": [ … ] },// candidates, never load-bearing + "fresh": "…", // transcript-bound challenge/nonce + "sig": "…" // by the INTRODUCED key, over this record — proof of possession } ``` -On receipt, an implementation MUST verify `fingerprint == blake3(pubkey)` -and reject on mismatch **before** any surface renders it (self-certification -is checked by the library, not delegated to the human). +On receipt, an implementation MUST, **before any surface renders it**: + +1. verify the fingerprint is a valid multihash name of `pubkey` — + recomputed under the fingerprint's *own* hash code (self-certification + is checked by the library, never delegated to the human); +2. verify `sig` under `pubkey` over this record including `fresh` — + **proof of possession**, transcript-bound so a bystander cannot replay + *someone else's* introduction as their own (the unknown-key-share + closure, §5.6; parity with mesh #40's PoP-to-certify). ### 3.5 PinRecord / GrantRecord (the chain-store) @@ -372,9 +415,13 @@ re-proves over the kernel. **PO-4.** association(peer) ⇒ pinned(fingerprint(peer)) ``` -`fingerprint = blake3(pubkey)` is self-certifying, therefore **re-key ⇒ new -fingerprint ⇒ unpinned ⇒ full re-ceremony**. No silent identity swap is -expressible. A pin is created only by (a) a `Decision::grant` from a bound +`fingerprint = H(pubkey)`, a multihash name, is **self-certifying** for any +H the profile pins (§8) with two required properties: collision resistance, +and hardness of finding a key matching a given name. Therefore **re-key ⇒ +new fingerprint ⇒ unpinned ⇒ full re-ceremony**. No silent identity swap is +expressible. (Rotating H is *re-naming*, not re-keying: the key signs a +linkage record binding its new name — quorum-free, the identity never +moved.) A pin is created only by (a) a `Decision::grant` from a bound surface, or (b) a pre-pinned policy entry — which is a signed loosening entry and therefore governed by L2. @@ -398,8 +445,8 @@ Mechanisms implement or discharge the laws; they add no new ones. ### 5.1 The chain-store (load-bearing for L2) -Records are `MerkleNode` in the `content-addressable` crate (BLAKE3 -CIDs, canonical DAG-CBOR, parent links), with these conventions: +Records are `MerkleNode` in the `content-addressable` crate (multihash +CIDs — §8, canonical DAG-CBOR, parent links), with these conventions: ``` c_i = H(canon(record_i ∖ sig)) content-CID (what is signed) @@ -529,10 +576,12 @@ corroboration is k-of-n, and n just shrank by one. | Channel | Attack | Closure | Residual | |---|---|---|---| | First contact (stranger) | TOFU key swap | L5 ceremony: SAS or out-of-band fingerprint check; or chain to a common pinned principal | the ceremony itself (below) | +| First contact | **introduction replay / unknown-key-share** | PoP: the introduced key signs the Introduction over a transcript-bound `fresh` (§3.4) | — | | Enrollment handshake | relay + **key substitution** | commit-then-reveal; SAS covers the **long-term keys** (§5.4·2); human comparison is the authentic channel | SAS guess ≈ n⁻ᵏ per round; rounds × witnesses shrink it | | Post-pin transport | impersonation on any raced path | dial-by-pubkey: QUIC/TLS authenticates the **node key** — a path that answers must hold the private key; paths are hints, never identity | key theft (out of scope: L2 quorum + revocation) | | Delegation | rogue "delegated" agent inserted | chains verify to a pinned principal; proof-of-possession at issuance (mesh #39/#40) | principal-root compromise (quorum revocation + succession) | | Remote surface | **render-swap**: human approves X, gate runs Y | `Decision.request` = content-CID of the request as rendered; sig covers it; gate matches CIDs (§3.3) | compromised surface *device* → its grants are attributable + revocable | +| Remote surface | **phishing canvas**: forged/unsolicited requests train the human | requests are gate-signed; surface verifies chain-to-pinned-principal *before rendering* (§3.1) | compromised gate key → quorum revocation | | Chain-store sync | forgery in transit | records are self-authenticating (signed + chained); transport can corrupt nothing silently | — | | Chain-store sync | **rollback** (stale head hides a revocation) | heads are monotonic (L2 structural clause); AuditRecords are witnessed freshness checkpoints (§3.6) | withholding = visible staleness → treat as degraded, fail closed | | Anchor channel | compromised registry vouches a fake root | anchors are blessed, k-of-n, never sufficient alone (§5.5) | k−1 colluding anchors corroborate nothing | @@ -570,6 +619,7 @@ Lean via Aeneas: | PO-4 | L4 | meet never amplifies (kernel restatement) | | PO-5 | L5 | no association without pin; re-key forces re-ceremony | | WF-1 | §3.2 | matrix decidable sans escalations (structural predicate, not a law) | +| WF-2 | §3 | Memo discipline: CID unconditional; sig at trust boundaries; parents for durability; Sealed at load | Pilot: PO-1 and PO-2. @@ -582,8 +632,10 @@ A conforming harness: - [ ] renders `verbs × scopes` completely; MAY render escalations (WF-1, §3.2) - [ ] treats `default` as a cursor hint, never an auto-grant - [ ] never persists a loosening outcome without a signature (L2) -- [ ] relies on the library's self-certification check (§3.4) rather than - asking the human to compare key bytes +- [ ] relies on the library's self-certification + proof-of-possession + checks (§3.4) rather than asking the human to compare key bytes +- [ ] (remote) verifies a request's gate signature before rendering (§3.1) +- [ ] constructs wire objects only through verified load (Sealed; WF-2) - [ ] ships no rendering into any agent-* library crate ## 7. Governance — law minimalism @@ -599,6 +651,10 @@ without a proof obligation demanding it; everything else is mechanism ("reset mesh" must not be an attack surface) was absorbed into L2 as its upward direction — tamper-*monotonicity* became tamper-*boundedness*. Zero count change; PO-2b added. +- **Executed (review 3, 2026-07-16):** the Memo discipline and the + multihash directive landed as wire discipline (WF-2) and profile (§8) — + laws name *properties*; algorithms are pins. L5 de-algorithm'd. Zero + count change. - **Next candidate:** L1+L4 are one law ("authority composes by meet") on two carriers (verdict lattice, caveat lattice); if the Lean formulation unifies them cleanly, five becomes four. @@ -607,7 +663,26 @@ Additions from the same review — the `attest` verb, negative pins, the `AuditRecord` — cost **zero** laws: each collapsed into existing structure or landed below the line. The algebra decides the count; ambition doesn't. -## 8. Relations +## 8. Profile v1 (pins, not laws) + +Algorithms are **implementation details**; each pin states the *property* +any replacement must carry. Identifiers are self-describing (multihash / +multicodec), so profile rotation happens under a running mesh. + +| Pin | v1 value | Required property (the law's interest) | +|---|---|---| +| Content hash `H` | BLAKE3-256 (multihash `0x1e`) | collision resistance; preimage hardness (L5 self-certification) | +| Signature | Ed25519 (RFC 8032) | **deterministic** — load-bearing for chain reproducibility (§5.1·1); existential unforgeability | +| Canonical encoding | DAG-CBOR (codec `0x71`) | injective, canonical serialization (one value ⇒ one byte string) | +| Links | CIDv1 | multihash-native, codec-tagged | + +Rotating `H` is a **re-naming ceremony** (L5): keys sign linkage records +binding their new names; identity never moves. Rotating the signature +scheme is heavier — it is a **re-keying** (full L5 re-ceremony per +identity) because the key *is* the identity. The wire format +(`agent-mesh#66`) carries the codes either way. + +## 9. Relations - #220 — verdict/policy TOML contract (headless half of this seam) - #225 — design directive, strategy, client-lib matrix (umbrella) @@ -616,6 +691,8 @@ or landed below the line. The algebra decides the count; ambition doesn't. for this spec's vocabulary) - PR #214 — presence/WebAuthn lineage (§5.3) - agent-mesh#65 — `Introduction` struct and mesh decision surfaces +- agent-mesh#66 — enrollment/delegation protocol; multihash wire format + for `Fingerprint` - newt-agent#1209 — first consumer: pinning ceremony (HIGH) - agent-mesh `docs/decisions/floating_identity.md` — identity doctrine (law 5 there = L5 here, seen from the transport) From f972613ed14afab7893d67a0cbca612b04e7b715 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 21:20:44 -0400 Subject: [PATCH 06/13] =?UTF-8?q?docs(spec):=20v0.1.5=20=E2=80=94=20close?= =?UTF-8?q?=208=20adversarial-review=20security=20findings=20(GPT-5/Codex?= =?UTF-8?q?=20round=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All eight true-positive findings adjudicated against the security canon (RFC 6962, TUF, Schneier-Kelsey, FssAgg eprint 2008/185, Landrock-Pedersen) via a verified deep-research pass, and closed: F1 rollback/truncation/fork — NEW §5.7 external anti-rollback anchor (independently-stored monotonic head + witness cosigning + fork=proof). L2's H1 was over-claimed (chain alone does NOT resist tail rollback); now honestly split chain(interior)+anchor(tail/fork). PO-2c added. In-chain AuditRecord no longer claimed as a checkpoint (it rolls back with the log). F2 approve-X/execute-Y — action.effect carries the canonical resolved-call content-CID; gate MUST recompute before minting (WYSIWYS). F3 grant∉matrix — §3.3 gate acceptance is a MUST checklist: request-CID, matrix membership, ceiling (L4 enforced on the wire), effect, discharge. F4 attest had no attestation — §3.3.1 wires the shipped Challenge/ DischargeVerifier step-up; grant inert until verified. F5 fresh wasn't fresh — Introduction is now a 2-message challenge-response; the RECIPIENT issues+consumes the challenge (replay-state at challenger). F6 circular quorum sig — RevocationRecord splits into one canonical unsigned payload every signer signs; signers sorted+deduped; chain sig separate. F7 unknown fields vs canonical — verify over received bytes; version dispatch; unknown authority-bearing fields fail closed; one of grant/ escalate. F8 algorithm agility — allowlist-before-dispatch (§3.4, §8): the verifier never lets the object choose its own algorithm. Law count unchanged: FIVE. One law (L2) corrected, not multiplied. The review's lesson — prose masquerading as protocol — is the case for the Lean/Aeneas track. Refs #229 review round 4 (GPT-5/Codex), #225; verification: deep-research pass (19 confirmed claims, RFC/paper-cited). PR-body staleness (F9) fixed separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 316 ++++++++++++++++++++++++++------- 1 file changed, 249 insertions(+), 67 deletions(-) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 20d6536..5e51964 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,7 +1,9 @@ # The Ceremony Contract -**Status:** DRAFT 0.1.4 (2026-07-16) — revised per review rounds 1–3 (#229). -Normative once accepted. +**Status:** DRAFT 0.1.5 (2026-07-16) — revised per review rounds 1–4 (#229); +round 4 = adversarial security review (GPT-5/Codex), findings adjudicated +against the security-engineering canon (RFC 6962, TUF, Schneier-Kelsey, +FssAgg, Landrock-Pedersen; see the PR thread). Normative once accepted. **Scope:** the decision-surface and first-contact contract between agent-* libraries (which own decision *semantics*) and harnesses (which own *rendering*). Companion to the verdict/policy TOML contract (#220) — that @@ -99,8 +101,20 @@ two principals pinning each other's roots — L5 again, one level up. ## 3. Wire objects -Field names are normative; unknown fields MUST be ignored (forward -compatibility). All objects carry `"v": 1`. +Field names are normative. All objects carry `"v": `, and +verification dispatches on it (**version dispatch**, not lenient parsing). + +**Signature verification is over the received canonical bytes, never over +a re-serialization.** Typed deserialization drops unknown fields, so +reserializing a parsed object cannot reproduce the signed digest — verify +the bytes as received, then parse (RFC 8785 canonicalization pitfall; +JWS/COSE practice). **Unknown authority-bearing fields fail closed:** an +object carrying a field the profile version does not define is *rejected*, +not ignored — tolerating it is a silent downgrade / version-confusion +surface (finding #7). Non-authority annotations MAY be preserved verbatim +only when the profile marks them non-critical (COSE critical-header model). +Exactly **one** of `grant` / `escalate` is permitted on a Decision; +zero-or-both is rejected. **The Memo discipline (WF-2).** Every wire object is a Memo-descendant, with capabilities attached by *mechanical criteria* — a boundary test, not @@ -130,7 +144,11 @@ What a gate hands a surface when a verdict resolves to interaction. { "v": 1, "subject": "b3:9f2c…", // Fingerprint — an identity, never a location - "action": { "class": "exec", "display": "run_command: cd " }, + "action": { + "class": "exec", + "display": "run_command: cd ", // human-facing presentation + "effect": "cid:…" // content-CID of the CANONICAL resolved call + }, "violation": "outside-granted-allowlist", "matrix": { … }, // §3.2 "context": { "session": "…", "rationale": "…", "generation": 41 }, @@ -138,6 +156,18 @@ What a gate hands a surface when a verdict resolves to interaction. } ``` +**`effect` binds the signature to what executes, not to what is shown** +(finding #2 — *what you see is what you sign*, Landrock-Pedersen 1998). +`display` is a lossy human rendering; `effect` is the content-CID of the +canonical, fully-resolved call (arguments *and* resolved resources — the +`CallRequest` the tool layer already produces). The gate MUST, before +minting authority, **recompute the canonical effect from the call it is +about to run and check it equals `action.effect`** — otherwise a stale or +lossy `display`→effect mapping approves X and executes Y while the CID +still matches (confused-deputy / TOCTOU). Display and effect are bound +together under the one signature; a surface that cannot faithfully render +`effect`'s meaning MUST refuse rather than show a prettier `display`. + A **remote** surface MUST verify the request's signature and that `by` chains to a pinned principal **before rendering** — an unauthenticated prompt is a phishing canvas that trains the human on fake ceremonies @@ -199,31 +229,71 @@ conformance (§6.2) — deliberately *not* a law; see §7. ### 3.3 Decision ```json -{ "v": 1, "grant": { "verb": "allow", "scope": "session" } } -{ "v": 1, "grant": { "verb": "attest", "scope": "session" } } // + presence discharge -{ "v": 1, "escalate": "audit" } +{ "v": 1, "request": "cid:…", // content-CID of the PermissionRequest AS ISSUED + "grant": { "verb": "allow", "scope": "once" }, + "by": "b3:…", "sig": "…" } // sig over this record's content-CID +{ "v": 1, "request": "cid:…", + "grant": { "verb": "attest", "scope": "session", + "discharge": { "challenge": "cid:…", "attempt": "…" } }, // §3.3.1 + "by": "b3:…", "sig": "…" } +{ "v": 1, "request": "cid:…", "escalate": "audit", "by": "b3:…", "sig": "…" } ``` `escalate` carries **zero authority** (L4): it navigates the human to a -richer surface; the request remains undecided until a `grant` returns. +richer surface; the request remains undecided until a `grant` returns. A +**remote** surface MUST sign its decisions; an in-process surface MAY omit +`by`/`sig` (§2.1), but the `request` binding is unconditional. + +**Gate acceptance is a checklist of MUSTs — the client is never trusted** +(finding #3; never-trust-client authorization / capability monotonicity). +The gate mints authority only when *all* hold: + +1. `request` equals the content-CID of the `PermissionRequest` the gate + itself issued (render-swap closure, §5.6); +2. `grant.verb ∈ matrix.verbs` **and** `grant.scope ∈ matrix.scopes` — + the answer is a member of the *offered* option-set; +3. the resulting authority is `⊑` the request's ceiling (L4) — a + `once/session` request can never be answered `always`; a surface + cannot return more than was asked; +4. the executable effect recomputes equal to `action.effect` (§3.1); +5. if `grant.verb == attest`, the discharge verifies (§3.3.1). + +A buggy or compromised surface that violates any of these is refused at +the gate, not obeyed — the wire enforces L4, it does not merely state it. + +#### 3.3.1 Attest discharge + +An `attest` grant is **inert until a presence proof is verified** +(finding #4 — a verb meaning "prove presence" must carry the proof and a +normative verify step; WebAuthn/FIDO2 challenge-response). It reuses +bridle's shipped step-up contract: + +- the gate issues a **domain-separated, single-use `Challenge`** (its + content-CID is `discharge.challenge`), bound to this request's CID, the + subject, and the current generation; +- the authenticator returns a `DischargeAttempt` (`discharge.attempt`) — + a WebAuthn assertion over that challenge; +- the gate verifies it through `step_up::DischargeVerifier` and marks the + challenge consumed **before** the grant takes effect. `attest × session` + binds the verified discharge to the generation; a new generation voids + it. An unverified or replayed discharge yields **no** authority. -A **remote** surface MUST sign its decisions — and the decision MUST name -the request it answered: +### 3.4 Introduction -```json -{ "v": 1, "request": "cid:…", // content-CID of the PermissionRequest AS RENDERED - "grant": { "verb": "allow", "scope": "once" }, - "by": "b3:…", "sig": "…" } // sig over this record's content-CID -``` +First contact is a **two-message challenge-response**, because freshness +comes from the *recipient*, never from the introducer (finding #5 — a +self-chosen nonce inside a self-signed object is byte-for-byte replayable; +the party seeking assurance must issue and consume the challenge). -The gate accepts a decision only if `request` matches the CID of the -request it actually issued — binding *what the human saw* to *what was -granted*, which closes the render-swap MITM (§5.6). An in-process surface -MAY omit `by`/`sig` (§2.1); the `request` binding is unconditional. +**Message 1 — the recipient issues a challenge** it will remember: -### 3.4 Introduction +```json +{ "v": 1, "challenge": "…", // fresh random, recipient-generated + "issued_by": "b3:…", // the recipient's fingerprint + "for_generation": 41, "expires_at_generation": 42 } +``` -First contact: an unpinned identity proposing itself. +**Message 2 — the introducer answers, binding that challenge:** ```json { @@ -233,20 +303,28 @@ First contact: an unpinned identity proposing itself. "channel": "mdns | dial-back | relay | manual | qr", "proposed_caveats": [ … ], // Caveats; the requested ceiling "observed": { "addr_candidates": [ … ] },// candidates, never load-bearing - "fresh": "…", // transcript-bound challenge/nonce - "sig": "…" // by the INTRODUCED key, over this record — proof of possession + "answers": "…", // the recipient's challenge, echoed + "transcript": "cid:…", // binds both fingerprints + msg 1 + "sig": "…" // by the INTRODUCED key over all of the above } ``` -On receipt, an implementation MUST, **before any surface renders it**: - -1. verify the fingerprint is a valid multihash name of `pubkey` — - recomputed under the fingerprint's *own* hash code (self-certification - is checked by the library, never delegated to the human); -2. verify `sig` under `pubkey` over this record including `fresh` — - **proof of possession**, transcript-bound so a bystander cannot replay - *someone else's* introduction as their own (the unknown-key-share - closure, §5.6; parity with mesh #40's PoP-to-certify). +On receipt of message 2, an implementation MUST, **before any surface +renders it**: + +1. confirm `answers` is a challenge **this recipient issued, still + unconsumed and unexpired**, then mark it consumed — replay-state lives + with the challenger, so a captured introduction cannot be re-presented + (Needham-Schroeder / station-to-station; unknown-key-share closure); +2. confirm the fingerprint's declared hash algorithm is a **member of the + locally-trusted profile allowlist (§8) — checked *before* any hashing** + (finding #8; the `alg:none`/algorithm-confusion class — never let the + object choose its own verifier), then verify the fingerprint is that + algorithm's multihash name of `pubkey` (self-certification checked by + the library, never by the human); +3. verify `sig` under `pubkey` over message 2 including `answers` and + `transcript` — **proof of possession**, transcript-bound so a bystander + cannot relay someone else's introduction as their own. ### 3.5 PinRecord / GrantRecord (the chain-store) @@ -290,9 +368,11 @@ witnesses the store's state and signs what it saw. It is appended to the chain-store like any record (its own content-CID, sig, parents), so audits are themselves tamper-evident and auditable. No -new law — L5 composed with the log; one new record type. Audits double as -**freshness checkpoints**: a peer presenting a head must show it extends -the last head this participant witnessed (rollback resistance, §5.6). +new law — L5 composed with the log; one new record type. An audit's +`witnessed_head`, once **exported to independently-protected storage** +(§5.7), becomes an anti-rollback anchor — but an AuditRecord that lives +*only inside the chain* rolls back with it and anchors nothing (finding +#1). Anti-rollback is §5.7's job; the AuditRecord is its raw material. **RevocationRecord** — punting a load-bearing identity is itself a ceremony, and per L2 it demands **quorum**: @@ -300,18 +380,36 @@ ceremony, and per L2 it demands **quorum**: ```json { "v": 1, - "revoke": "b3:…", // the fingerprint being punted - "reason": "compromise | rotation | retirement", - "succession": "b3:…", // optional successor (root rotation) - "signers": [ { "by": "b3:…", "sig": "…" }, … ] // k-of-n per quorum policy + "payload": { // the CANONICAL UNSIGNED payload — every signer signs THIS + "revoke": "b3:…", // the fingerprint being punted + "reason": "compromise | rotation | retirement", + "succession": "b3:…", // optional successor (root rotation) + "policy": "cid:…" // the quorum policy in force + }, + "signers": [ // detached; each sig is over content-CID(payload) + { "by": "b3:aa…", "sig": "…" }, + { "by": "b3:cc…", "sig": "…" } // sorted by `by`, deduplicated + ] } ``` -The quorum policy (k, n, eligible signers) is itself a principal-signed -loosening entry established at setup — defining *who may revoke* is -authority structure, governed by L2 like any other loosening. Revoking the -**last** root without a `succession` ends the mesh; implementations MUST -refuse it unless the record carries an explicit `"tombstone": true`. +**One payload, many detached signatures** (finding #6 — a signature nested +in the object it signs is circular; each signer would otherwise sign a +different, progressively-grown record). Every signer signs +`content-CID(payload)` — the fixed unsigned inner object — so all +signatures cover *identical* bytes. `signers` is **sorted by `by` and +deduplicated** (one identity cannot count twice toward `k`), and the +signature set is *not* part of what is signed. The chain-append signature +(`sig` over the whole record's content-CID, WF-2) is separate from the +quorum signatures and added last. + +Acceptance: `|distinct valid signers ∩ eligible| ≥ k` under the named +`policy`. The quorum policy (k, n, eligible signers) is itself a +principal-signed loosening entry established at setup — defining *who may +revoke* is authority structure, governed by L2 like any other loosening. +Revoking the **last** root without a `succession` ends the mesh; +implementations MUST refuse it unless the record carries an explicit +`"tombstone": true`. ### 3.7 DecisionSurface (the seam) @@ -370,19 +468,27 @@ needs no key; **irreversible narrowing — revoking a load-bearing identity adversary who can *force* closure. "Reset mesh" must not be a denial-of-service surface. Availability is a security property. -**Mechanism honesty:** under flat policy files this law holds for -**additions only**. *Deleting* a restrictive entry (a durable deny) widens -authority, and flat files cannot detect the deletion — signatures guard -loosening additions, not restrictive removals. Extending L2 to the full -mutation set {add, delete, reorder} **requires the chain-store** (§5.1); -the chain is load-bearing for this law, not an optimization. (Surfaced by -the negative-pins review thread: the teenager with disk access deleting -the deny row is the threat model.) - -**Hypothesis H1 (append-only-verifiability):** `m` cannot undetectably -remove a record or reintroduce a previously-signed one. H1 is discharged by -the chain-store (§5.1), not assumed. **PO-2** (proved under H1; H1's -discharge is PO-2a; quorum soundness for revocation is PO-2b). +**Mechanism honesty (two layers).** Under flat policy files this law holds +for **additions only** — deleting a restrictive entry widens authority and +flat files cannot detect it. The chain-store (§5.1) extends detection to +{add, delete, reorder} of the log's *interior*. But the chain-store **by +itself does not detect rollback/truncation of the log's *tail*, nor a +split-view fork** (finding #1): an attacker who truncates a restrictive +suffix and its head, or presents an older valid fork, leaves a prefix that +still verifies — this is the established limit of every hash-chained log +(Schneier-Kelsey 1999; FssAgg / eprint 2008/185; and the reason +Certificate Transparency requires gossiped Signed Tree Heads, RFC 6962). +Closing it requires an **independently-protected monotonic anchor** (§5.7), +*not* the chain alone. An anti-rollback claim resting on an in-chain +AuditRecord is circular — it rolls back with the log it certifies. + +**Hypothesis H1 (append-only-verifiability + monotone freshness):** `m` +cannot undetectably remove/reintroduce an *interior* record (chain-store), +**nor roll the log back past the last independently-anchored head** (§5.7). +H1 is *discharged by mechanism*, not assumed: interior integrity by the +chain (**PO-2a**), tail/fork integrity by the external anchor (**PO-2c**), +revocation-quorum soundness by §3.6 (**PO-2b**). **PO-2** proves ⊑-monotony +under H1. ### L3 — Fail-closed totality @@ -458,11 +564,18 @@ parents(record_{i+1}) ∋ ℓ_i descendants commit to content AND s Parents reference the **line-CID** — the full predecessor *including its signature* — so stripping or swapping a historical signature breaks the -chain just as surely as editing content. Removing a record orphans the -head; replaying a deleted record re-enters with a stale parent set. Both -verify-fail loudly. This is what discharges H1 and extends L2 to -deletions, retiring the documented known-limit of flat signed files -(policy.rs; #226). +chain just as surely as editing content. Editing or removing an *interior* +record orphans every descendant's parent link — it verify-fails loudly +**against a head the verifier already trusts**. This extends interior +integrity to deletions and retires the flat-file known-limit (policy.rs; +#226). + +**What the chain alone does NOT do (finding #1):** verification is always +*relative to a head*. Against an attacker who also controls the head — +truncating the tail, or presenting a wholly older/forked-but-valid log — +the surviving prefix verifies fine and nothing is orphaned. The chain +gives interior integrity; **tail and fork integrity require the external +anchor of §5.7.** This spec does not claim otherwise. Two stated assumptions, doing different jobs: @@ -583,14 +696,53 @@ corroboration is k-of-n, and n just shrank by one. | Remote surface | **render-swap**: human approves X, gate runs Y | `Decision.request` = content-CID of the request as rendered; sig covers it; gate matches CIDs (§3.3) | compromised surface *device* → its grants are attributable + revocable | | Remote surface | **phishing canvas**: forged/unsolicited requests train the human | requests are gate-signed; surface verifies chain-to-pinned-principal *before rendering* (§3.1) | compromised gate key → quorum revocation | | Chain-store sync | forgery in transit | records are self-authenticating (signed + chained); transport can corrupt nothing silently | — | -| Chain-store sync | **rollback** (stale head hides a revocation) | heads are monotonic (L2 structural clause); AuditRecords are witnessed freshness checkpoints (§3.6) | withholding = visible staleness → treat as degraded, fail closed | +| Chain-store sync | **rollback / truncation / fork** (stale-but-valid head hides a revocation) | external anti-rollback anchor (§5.7): independently-stored monotonic head + witness cosigning + fork = proof-of-misbehavior. The chain alone does NOT close this. | withholding = visible staleness vs. a remembered anchor → fail closed | | Anchor channel | compromised registry vouches a fake root | anchors are blessed, k-of-n, never sufficient alone (§5.5) | k−1 colluding anchors corroborate nothing | | The human | prompt fatigue / phishing the ceremony | `attest` for high ceilings; distinct ceremony UI is consumer guidance (newt#1209) | irreducible; parameterized paranoia exists for exactly this | The pattern behind every row: **the authenticated thing is always the key, never the channel** — locations, relays, registries, and rendered pixels are candidates and hints; signatures and CIDs are what the gate -trusts. One doctrine, applied ten times. +trusts. One doctrine, applied across every row. + +### 5.7 The anti-rollback anchor (external, load-bearing for L2·H1) + +A hash-chained log is verified *relative to a head*. Every such log — +Schneier-Kelsey secure audit logs (1999), the FssAgg truncation analysis +(eprint 2008/185), Certificate Transparency (RFC 6962) — shares one limit: +**an attacker who controls the head can truncate the tail or present an +older/forked-but-internally-valid log, and it still verifies.** The chain +gives interior integrity only. Closing tail-and-fork requires state the +attacker does not control. This spec adopts the three canonical layers, +in ascending assurance: + +1. **Independently-protected monotonic head (required).** Each participant + remembers, in storage separate from the log, the highest + `(generation, length, head-CID)` it has accepted, and **MUST reject any + presented head that is not a consistent forward-extension** of it — + never a shorter length or lower generation (the TUF rule: *"clients + MUST NOT replace metadata with a version number less than the one + currently trusted"*, RFC 6962 monotonicity). This alone defeats + truncation and rollback *for that participant*. +2. **Witness cosigning (recommended for shared stores).** The head is + periodically countersigned by one or more witnesses whose signatures + are the `AuditRecord`s of §3.6 **exported off-chain**. A participant + accepts a head only if it carries a witness cosignature no older than + its freshness policy (CT gossip / STH; witness-cosigning). This defeats + *secret* equivocation: to fool a victim the attacker must fork the + witnesses too. +3. **Fork = proof of misbehavior (required).** Two validly-signed heads of + the same store at the same length with different CIDs are incontestable + evidence of equivocation (RFC 6962). Implementations MUST treat a + detected fork as a security event, halt authority minting from that + store, and escalate — never silently pick one. + +For a solo user (§5.4's n=2 world) the monotonic head lives on each of +their own enrolled devices, and each device is the other's witness — the +same k-of-n substrate as revocation, reused. A quorum/witness set is the +enterprise instance of the identical mechanism. **Nothing here trusts the +storage medium**; the anchor is trusted state a participant carries into +each verification, exactly as `pinned` is. ## 6. Conformance @@ -613,8 +765,9 @@ Lean via Aeneas: |---|---|---| | PO-1 | L1 | ⨅-fold is order-independent (assoc ∘ comm ∘ idem) | | PO-2 | L2 | sub-quorum mutation is ⊑-monotone, under H1 | -| PO-2a | L2·H1 | chain-store rejects removed and replayed records | +| PO-2a | L2·H1 | chain-store rejects removed/replayed *interior* records (vs. a trusted head) | | PO-2b | L2 | a sub-quorum coalition cannot shrink the load-bearing pin set | +| PO-2c | L2·H1 | against the §5.7 anchor, tail truncation and fork are rejected (not merely detected-later) | | PO-3 | L3 | totality + monotone headless degradation | | PO-4 | L4 | meet never amplifies (kernel restatement) | | PO-5 | L5 | no association without pin; re-key forces re-ceremony | @@ -635,7 +788,15 @@ A conforming harness: - [ ] relies on the library's self-certification + proof-of-possession checks (§3.4) rather than asking the human to compare key bytes - [ ] (remote) verifies a request's gate signature before rendering (§3.1) -- [ ] constructs wire objects only through verified load (Sealed; WF-2) +- [ ] constructs wire objects only through verified load (Sealed; WF-2), + verifying signatures over received bytes and rejecting unknown + authority-bearing fields fail-closed (§3) +- [ ] (gate) enforces the §3.3 acceptance checklist — request-CID match, + matrix membership, ceiling, effect recomputation, attest discharge — + never trusting the surface +- [ ] (gate) checks a fingerprint's algorithm against the trusted profile + allowlist *before* dispatch (§3.4, §8) +- [ ] carries a §5.7 anti-rollback anchor for any shared/persisted store - [ ] ships no rendering into any agent-* library crate ## 7. Governance — law minimalism @@ -655,6 +816,17 @@ without a proof obligation demanding it; everything else is mechanism multihash directive landed as wire discipline (WF-2) and profile (§8) — laws name *properties*; algorithms are pins. L5 de-algorithm'd. Zero count change. +- **Executed (review 4, 2026-07-16 — adversarial security review, + GPT-5/Codex):** eight true-positive findings, all adjudicated against + the canon and closed as *enforcement* (gate MUSTs, §3.3), *mechanism* + (external anti-rollback anchor §5.7; recipient-issued challenge §3.4; + canonical quorum payload §3.6; effect-CID binding §3.1), and *wire + discipline* (fail-closed unknown fields §3; algorithm allowlist §3.4, + §8). The one law touched — L2 — was *corrected*, not multiplied: its + H1 was over-claimed (chain alone ⇏ rollback resistance) and is now + honestly split across chain + anchor. **Still five laws.** The review's + meta-lesson — "prose becomes authority-bearing protocol" — is the case + for the formal track, not against the design. - **Next candidate:** L1+L4 are one law ("authority composes by meet") on two carriers (verdict lattice, caveat lattice); if the Lean formulation unifies them cleanly, five becomes four. @@ -669,6 +841,16 @@ Algorithms are **implementation details**; each pin states the *property* any replacement must carry. Identifiers are self-describing (multihash / multicodec), so profile rotation happens under a running mesh. +**Agility needs an allowlist, or it is a downgrade attack** (finding #8). +Self-describing identifiers let the *object* declare its algorithm — so a +verifier that dispatches on the declared code alone lets the attacker pick +a broken hash (the `alg:none` / algorithm-confusion class). Therefore: a +verifier MUST check the declared code against **this locally-trusted +profile table before hashing or verifying**, and reject anything outside +it. Profile *rotation* is a negotiated, principal-signed change to the +allowlist (a loosening entry, L2), never a per-object choice. Agility +lives in the profile, not on the wire. + | Pin | v1 value | Required property (the law's interest) | |---|---|---| | Content hash `H` | BLAKE3-256 (multihash `0x1e`) | collision resistance; preimage hardness (L5 self-certification) | From cb2d3ad3207455074194198a0cb83180ed76e394 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 21:25:22 -0400 Subject: [PATCH 07/13] =?UTF-8?q?docs(spec):=20v0.1.6=20=E2=80=94=20second?= =?UTF-8?q?-review=20refinements=20+=20record=20two=20open=20design=20fork?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second independent LLM review confirmed every round-4 fix and added five clearly-correct refinements (applied) plus two design forks (recorded for the author, not agent-decided): Refinements: - §5.1 signing MUST be deterministic (plain prose), entropy identity demoted to parenthetical 'why'. - §5.4 human SAS comparison is a low-bandwidth authenticated channel with measurable error probability — dropped the 'unspoofable' overclaim (humans are non-deterministic peripherals). - §3.1 rendering faithfulness named as a residual: display MUST derive from effect by a deterministic profile function; signed render transcript is the deferred strengthening path. - §3.6 break-glass + succession made a REQUIRED subsystem (recovery factor in n; quorum-or-timelocked-unilateral recovery) — quorum that defeats hostile revocation also locks out a key-losing owner. - §6.3 anti-rollback head location made explicit ('on disk beside the log' does NOT qualify). Open forks recorded in §7 governance: - attest factorization (effect × assurance × scope vs one verb axis) — L1 survives either way (product of lattices is a lattice); a factoring question, deferred to author. - partition into profiles (Ceremony Contract + Signed-Object + Chain-Store + Enrollment + Identity-Lifecycle + Rendering profiles) — the loosely-coupled/functionally-cohesive doctrine applied to the spec; strongly recommended, author to sequence. Still five laws. Refs #229 reviews 4-5. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 95 ++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 5e51964..3d24107 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,9 +1,11 @@ # The Ceremony Contract -**Status:** DRAFT 0.1.5 (2026-07-16) — revised per review rounds 1–4 (#229); -round 4 = adversarial security review (GPT-5/Codex), findings adjudicated -against the security-engineering canon (RFC 6962, TUF, Schneier-Kelsey, -FssAgg, Landrock-Pedersen; see the PR thread). Normative once accepted. +**Status:** DRAFT 0.1.6 (2026-07-16) — revised per review rounds 1–5 (#229); +rounds 4–5 = two independent adversarial security reviews (GPT-5/Codex and +a second LLM), findings adjudicated against the security-engineering canon +(RFC 6962, TUF, Schneier-Kelsey, FssAgg, Landrock-Pedersen; see the PR +thread). Two open design forks (§7): the `attest` factorization and whether +to partition this document into profiles. Normative once accepted. **Scope:** the decision-surface and first-contact contract between agent-* libraries (which own decision *semantics*) and harnesses (which own *rendering*). Companion to the verdict/policy TOML contract (#220) — that @@ -168,6 +170,17 @@ still matches (confused-deputy / TOCTOU). Display and effect are bound together under the one signature; a surface that cannot faithfully render `effect`'s meaning MUST refuse rather than show a prettier `display`. +**Residual — rendering faithfulness is not cryptographic.** Binding the +signature to `effect` proves the decision is bound to the *data*; it does +**not** prove the UI faithfully showed that data to the human. Truncation, +locale, misleading formatting, hidden arguments, and path abbreviation +remain human-factors surfaces. This spec therefore requires `display` to +be derived from `effect` by a profile-defined, deterministic rendering +function (so display is checkable against effect), and treats a *signed +render transcript* — the surface attesting the exact bytes it presented — +as the strengthening path (a Rendering Security Profile, deferred). Named, +not solved. + A **remote** surface MUST verify the request's signature and that `by` chains to a pinned principal **before rendering** — an unauthenticated prompt is a phishing canvas that trains the human on fake ceremonies @@ -411,6 +424,18 @@ Revoking the **last** root without a `succession` ends the mesh; implementations MUST refuse it unless the record carries an explicit `"tombstone": true`. +**Break-glass and succession are required, not optional.** A quorum strong +enough to defeat a hostile revocation is, by the same math, strong enough +to lock out a legitimate owner who loses `k` keys — availability cuts both +ways. So enrollment MUST also provision recovery: a pre-enrolled recovery +factor (offline hardware key / printed share) counted in `n`, plus a +succession path that transfers a principal root to a new key under quorum +*or* a time-delayed unilateral recovery (a self-revocation that any single +device can start but that only takes effect after a published generation +delay, giving co-signers a window to veto — the "social recovery with +timelock" pattern). This is a **named required subsystem** (candidate: +Identity Lifecycle profile), sketched here, not yet fully specified. + ### 3.7 DecisionSurface (the seam) Language-idiomatic equivalents of: @@ -579,15 +604,16 @@ anchor of §5.7.** This spec does not claim otherwise. Two stated assumptions, doing different jobs: -1. **Deterministic signatures.** `H(sig | content, key) = 0` — given the - content and the key, the signature carries no fresh entropy. Ed25519 - provides exactly this (RFC 8032 derives the nonce from key and - message), so the whole log is a **pure function** of (genesis, payload - sequence, keys). A randomized scheme (ECDSA with random nonce) has - `H(sig | content, key) > 0`: two honest signings of identical content - yield different line-CIDs, and the chain forks on any re-sign. The - entropy identity is the proof obligation that forces a deterministic - scheme — it governs *reproducibility*. +1. **Deterministic signatures.** For Profile v1, signing MUST be + deterministic: `Sign(sk, message)` yields one canonical signature + encoding for a fixed key and message. Ed25519 provides this (RFC 8032 + derives the nonce from key and message), so the whole log is a **pure + function** of (genesis, payload sequence, keys). A randomized scheme + (ECDSA with a random nonce) would produce a different signature — hence + a different line-CID — on every re-sign, forking the chain. (The same + fact stated information-theoretically: the signature adds no entropy + given content and key, `H(sig | content, key) = 0`. The prose rule is + normative; the identity is just why.) This governs *reproducibility*. 2. **Collision resistance of `H`** governs *tamper-evidence*. The two are independent: determinism makes the log replayable; collision resistance makes it unforgeable. @@ -634,9 +660,13 @@ construction (per Bluetooth numeric comparison / ZRTP): substituting the long-term keys — the classic key-substitution hole (§5.6). The SAS must checksum *what is being pinned*, not merely the channel; -3. **a human compares the SAS on both screens** — the out-of-band channel - the MITM cannot sit on. The phrase is not a secret to transport; it is - a checksum of the handshake two screens must agree on. +3. **a human compares the SAS on both screens** — a low-bandwidth + *authenticated* channel the network MITM is not on. It is not + unspoofable: it has a measurable error probability (the SAS entropy) + and rests on human-factors assumptions — humans are non-deterministic + peripherals who skim. The phrase is not a secret to transport; it is a + checksum of the handshake two screens must agree on, and its strength + is exactly the guess probability below. Commit-before-reveal forces a MITM to *guess* the SAS in advance; one round of a `xxx-000`-style SAS ≈ 1-in-46k. Paranoia is then a parameter, @@ -796,7 +826,10 @@ A conforming harness: never trusting the surface - [ ] (gate) checks a fingerprint's algorithm against the trusted profile allowlist *before* dispatch (§3.4, §8) -- [ ] carries a §5.7 anti-rollback anchor for any shared/persisted store +- [ ] carries a §5.7 anti-rollback anchor for any shared/persisted store — + naming *where* the monotonic head lives (device keystore / TPM or + hardware monotonic counter / witness quorum / separately-protected + checkpoint); "on disk beside the log" does NOT qualify - [ ] ships no rendering into any agent-* library crate ## 7. Governance — law minimalism @@ -827,10 +860,38 @@ without a proof obligation demanding it; everything else is mechanism honestly split across chain + anchor. **Still five laws.** The review's meta-lesson — "prose becomes authority-bearing protocol" — is the case for the formal track, not against the design. +- **Refinements (review 5, 2026-07-16 — second LLM):** confirmed every + round-4 fix independently, and sharpened five: deterministic-signature + prose over entropy notation (§5.1); human SAS comparison is a + measurable-error authenticated channel, *not* "unspoofable" (§5.4); + rendering faithfulness named as a residual with a deterministic + display-from-effect rule + deferred render transcript (§3.1); break-glass + + succession made a required subsystem (§3.6); anti-rollback head + location made explicit in conformance (§6.3). No law change. - **Next candidate:** L1+L4 are one law ("authority composes by meet") on two carriers (verdict lattice, caveat lattice); if the Lean formulation unifies them cleanly, five becomes four. +**Open design forks (author's call — not agent-decidable):** + +- **The `attest` factorization.** Review 5 argues `attest` is an *assurance + condition* (none / presence / hardware-backed), not an *outcome* peer to + `allow`/`deny`, and proposes three axes — `effect × assurance × scope` — + over one verb axis. Trade-off: three axes are semantically cleaner and + future-proof; the single total order is structurally minimal and keeps + L1 a plain meet (a product of lattices is still a lattice, so L1 survives + either way — the question is factoring, not soundness). Deferred to the + author. +- **Partition into profiles.** Both reviews note this document has grown + from a decision-surface contract into a full security constitution + (~900 lines: identity, enrollment, revocation, audit, storage, hash + agility, rendering). Recommended split — Ceremony Contract (decision + semantics) · Signed-Object Profile · Chain-Store Profile · Enrollment + Protocol · Identity Lifecycle · Rendering Security Profile — with the + five laws as the narrow waist. This is *exactly* the loosely-coupled / + functionally-cohesive doctrine applied to the spec itself; strongly + recommended, deferred to the author to sequence. + Additions from the same review — the `attest` verb, negative pins, the `AuditRecord` — cost **zero** laws: each collapsed into existing structure or landed below the line. The algebra decides the count; ambition doesn't. From 7f6ab16b27f76ee15cbfe91e692370efa985fa1f Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 21:40:08 -0400 Subject: [PATCH 08/13] =?UTF-8?q?docs(spec):=20v0.1.7=20=E2=80=94=20forwar?= =?UTF-8?q?d-only=20attestation=20ratchet=20+=20causal-transcript=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The author's direction: a presence attestation must not authorize against a regressed history. - §3.3.1 forward-only ratchet: the attest challenge commits to a checkpoint; the signer refuses unless the presented head extends its last-witnessed head, held in the §5.7 anti-rollback anchor (outside the store it validates). Rollback OR fork = CHAIN HISTORY REGRESSION → halt+escalate. Per-causal-thread so concurrent threads don't false-trip. Generation (total order) and DAG ancestry (partial order) must both advance. Every ordinary approval becomes a free freshness checkpoint. NOT a new law — L2.H1's anchor applied at ceremony time. - §3.6 new AttestationRecord: one presence sig, TWO distinct statements — authorization (request_cid, decision_cid) and history witness (observed_head, previous_witnessed_head). attest and audit stay distinct but co-signed. - store restated as a CAUSAL TRANSCRIPT (Merkle DAG, parents is a set, branches/merges first-class); the chain-store is the authority projection of the Conversation Graph (agent-mesh#67; floating_identity law 4). Still five laws. Refs #229, agent-mesh#67. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/ceremony-contract.md | 102 +++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 3d24107..302e856 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,11 +1,12 @@ # The Ceremony Contract -**Status:** DRAFT 0.1.6 (2026-07-16) — revised per review rounds 1–5 (#229); -rounds 4–5 = two independent adversarial security reviews (GPT-5/Codex and -a second LLM), findings adjudicated against the security-engineering canon -(RFC 6962, TUF, Schneier-Kelsey, FssAgg, Landrock-Pedersen; see the PR -thread). Two open design forks (§7): the `attest` factorization and whether -to partition this document into profiles. Normative once accepted. +**Status:** DRAFT 0.1.7 (2026-07-16) — revised per review rounds 1–5 (#229) +plus the author's forward-only-ratchet / causal-transcript direction. +Rounds 4–5 = two independent adversarial security reviews (GPT-5/Codex and +a second LLM), adjudicated against the security-engineering canon (RFC 6962, +TUF, Schneier-Kelsey, FssAgg, Landrock-Pedersen; see the PR thread). Two +open design forks (§7): the `attest` factorization and whether to partition +this document into profiles. Normative once accepted. **Scope:** the decision-surface and first-contact contract between agent-* libraries (which own decision *semantics*) and harnesses (which own *rendering*). Companion to the verdict/policy TOML contract (#220) — that @@ -291,6 +292,44 @@ bridle's shipped step-up contract: binds the verified discharge to the generation; a new generation voids it. An unverified or replayed discharge yields **no** authority. +**The forward-only ratchet: a presence proof also witnesses a +non-regressing history.** A presence-backed signer MUST NOT authorize +against a view of the world that has moved *backward*. So the challenge +the authenticator signs commits not only to `request`/`decision` but to a +**checkpoint** — and the signer refuses if the store it is shown does not +extend the store it last witnessed. This produces an `AttestationRecord` +(§3.6) carrying *two distinct statements in one signature*: + +- **authorization** — "I was present and approved this `request` / + `decision`" (`request_cid`, `decision_cid`); +- **history witness** — "I verified the presented head `observed_head` + descends from `previous_witnessed_head`, the last head I remember" + (`observed_head`, `previous_witnessed_head`). + +The signer's `previous_witnessed_head` MUST live in its **§5.7 +anti-rollback anchor** (device keystore / hardware counter / witness +quorum) — *not* in the same store it is validating, or an attacker rolls +back the chain and the memory of it together and everything verifies. On +success the signer advances the anchor to `observed_head`. The check is +run **per causal thread** (per conversation / subject), so concurrent +threads never false-trip regression. + +Ratchet failures are refused loudly, never silently accepted: + +- `previous_witnessed_head` is **not an ancestor** of `observed_head` → + either a rollback (regressed head) or a **fork** (a sibling branch that + omits the witnessed head). Both are `CHAIN HISTORY REGRESSION` — halt + and escalate; a fork is proof-of-misbehavior (§5.7), never a branch to + silently adopt. +- generation has regressed → refuse. + +Two monotonicities compose here: the **generation** counter (a coarse +total order per subject) and **DAG ancestry** (the fine causal order, +§3.6). An attestation is valid only when *both* advance. This is not a new +law — it is L2·H1's §5.7 anchor applied at the instant of the ceremony; +its payoff is that **every ordinary presence approval becomes a free +freshness checkpoint**, nailing a timestamp-free piton into the history. + ### 3.4 Introduction First contact is a **two-message challenge-response**, because freshness @@ -364,10 +403,18 @@ Two CIDs per record: the **content-CID** (canonical form *minus* `sig` — what is signed) and the **line-CID** (canonical form *including* `sig` — what descendants reference in `parents`). Consequences in §5.1. -### 3.6 AuditRecord & RevocationRecord (ceremonies over the store) +### 3.6 Attestation, Audit & Revocation records (ceremonies over the store) + +The store is a **causal transcript** — a Merkle DAG of content-addressed +records, not a linear ledger. `parents` is a *set* (branches and merges +are first-class); "extends" means *reachable-ancestor*, and the +forward-only checks below are over this partial order, not an integer +index. (The chain-store here is the **authority projection** of the wider +Conversation Graph — same `MerkleNode` structure, different payload +`T`; agent-mesh#67.) -An audit is a ceremony whose subject is the chain head: a fingerprint -witnesses the store's state and signs what it saw. +An **AuditRecord** is a ceremony whose subject is the chain head: a +fingerprint witnesses the store's state and signs what it saw. ```json { @@ -379,6 +426,31 @@ witnesses the store's state and signs what it saw. } ``` +An **AttestationRecord** (§3.3.1) is the AuditRecord's authorization-bearing +sibling: one presence signature carrying both an approval and a history +witness. Authorization and witnessing are *distinct fields*, never +conflated — the same finger-press proves "I approved R" and "the world had +not regressed when I did." + +```json +{ + "v": 1, + "request_cid": "cid:…", // WHAT was approved (§3.1 effect-bound) + "decision_cid": "cid:…", // the Decision answering it + "observed_head": "cid:…", // history the approval was made against + "previous_witnessed_head": "cid:…", // last head this signer remembers (§5.7 anchor) + "generation": 41, + "nonce": "…", // the gate-issued single-use challenge (§3.3.1) + "signer": "b3:…", + "presence": { "kind": "passkey", "discharge": "…" }, + "sig": "…" // over content-CID of all of the above +} +``` + +Acceptance requires `previous_witnessed_head` to be a reachable ancestor of +`observed_head` (else `CHAIN HISTORY REGRESSION`, §3.3.1) — *no +authorization floats free of the exact history that gave it meaning.* + It is appended to the chain-store like any record (its own content-CID, sig, parents), so audits are themselves tamper-evident and auditable. No new law — L5 composed with the log; one new record type. An audit's @@ -868,6 +940,13 @@ without a proof obligation demanding it; everything else is mechanism display-from-effect rule + deferred render transcript (§3.1); break-glass + succession made a required subsystem (§3.6); anti-rollback head location made explicit in conformance (§6.3). No law change. +- **Extension (author, 2026-07-16 — forward-only ratchet):** a presence + attestation now also witnesses a non-regressing history head (§3.3.1, + `AttestationRecord` §3.6). This is *not* a sixth law — it is L2·H1's §5.7 + anchor applied at ceremony time, so every approval doubles as a freshness + checkpoint. The store is restated as a **causal transcript** (Merkle DAG, + authority projection of the Conversation Graph, agent-mesh#67). Still + five laws. - **Next candidate:** L1+L4 are one law ("authority composes by meet") on two carriers (verdict lattice, caveat lattice); if the Lean formulation unifies them cleanly, five becomes four. @@ -936,8 +1015,11 @@ identity) because the key *is* the identity. The wire format - agent-mesh#65 — `Introduction` struct and mesh decision surfaces - agent-mesh#66 — enrollment/delegation protocol; multihash wire format for `Fingerprint` +- agent-mesh#67 — the **Conversation Graph** (distributed content-addressed + causal transcript); the chain-store is its authority projection - newt-agent#1209 — first consumer: pinning ceremony (HIGH) - agent-mesh `docs/decisions/floating_identity.md` — identity doctrine - (law 5 there = L5 here, seen from the transport) + (law 5 there = L5 here; law 4 "conversations outlive locations" is what + the Conversation Graph makes concrete) - `content-addressable` crate — `ContentId`, canonical DAG-CBOR, `MerkleNode` (§5.1) From bd6d2a610f0715ba2f62dbf7a70f24bad678f6b3 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 21:49:28 -0400 Subject: [PATCH 09/13] docs(spec): partition the ceremony contract into a provable profile suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monolith (~1025 lines) becomes a dependency-ordered suite of loosely- coupled, functionally-cohesive profiles — the architecture doctrine applied to the spec itself, and the synthesis of review-5's partition + GPT-5 #232's formal kernel. - README.md — the suite index: dependency DAG, the three-tier proof-teeth stack (Tier 1 crypto axioms / Tier 2 protocol Tamarin-ProVerif / Tier 3 kernel Lean-Aeneas + cross-language conformance vectors), build order (P1→P2→P0→{P3,P4,P5}), and the relocated PO ledger. Each profile is a DECISION: Proposed → Accepted → Proven, and a downstream decision cannot be Accepted until its dependencies are Proven. - P0 ceremony-contract.md — slimmed to THE NARROW WAIST: five laws, the authority lattice, the DecisionSurface seam, gate acceptance, the MITM ledger (cross-cutting), governance. 242 lines. - P1 signed-object-profile.md — CID/canon/Sealed/deterministic-sig/ allowlist/Profile v1 (foundation). - P2 chain-store-profile.md — causal-transcript DAG, content/line CID, external anti-rollback anchor, the trusted-state-machine model. - P3 enrollment-protocol.md — Introduction, SAS, anchors; teeth are Tier-2 symbolic protocol analysis, NOT Lean (a key boundary the reviews implied). - P4 identity-lifecycle.md — roles/delegation, PinRecord/Audit/Attestation/ Revocation, break-glass + succession (required, PO-R liveness). - P5 rendering-security-profile.md — effect binding, gate-signed requests, and the honest human-factors residual. Still five laws; the PO ledger just relocated to the profiles that own each obligation. GPT-5 #232's kernel is adopted as the P0+P1+P2 provable MVP. Refs #229, #232, #225, agent-mesh#67. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/README.md | 151 +++ docs/spec/ceremony-contract.md | 1145 ++++------------------- docs/spec/chain-store-profile.md | 123 +++ docs/spec/enrollment-protocol.md | 104 ++ docs/spec/identity-lifecycle.md | 113 +++ docs/spec/rendering-security-profile.md | 76 ++ docs/spec/signed-object-profile.md | 119 +++ 7 files changed, 867 insertions(+), 964 deletions(-) create mode 100644 docs/spec/README.md create mode 100644 docs/spec/chain-store-profile.md create mode 100644 docs/spec/enrollment-protocol.md create mode 100644 docs/spec/identity-lifecycle.md create mode 100644 docs/spec/rendering-security-profile.md create mode 100644 docs/spec/signed-object-profile.md diff --git a/docs/spec/README.md b/docs/spec/README.md new file mode 100644 index 0000000..83037fc --- /dev/null +++ b/docs/spec/README.md @@ -0,0 +1,151 @@ +# The Ceremony Suite — a dependency-ordered set of provable profiles + +**Status:** DRAFT (2026-07-16). This directory replaces the single +`ceremony-contract.md` monolith with a suite of **loosely-coupled, +functionally-cohesive** profiles: each names one job, owns its own algebra +and proof obligations, and exposes a narrow seam the others depend on. The +five laws are the **narrow waist**; everything else is a profile that either +supplies a mechanism the laws assume or consumes the laws to build a +ceremony. + +This is the project's architecture doctrine applied to its own +specification: *group what works together to do one job into a unit named +for it; expose only the narrow seam.* + +## Why a suite, not a constitution + +The monolith grew to ~1000 lines spanning identity, enrollment, revocation, +audit, storage, hash agility, rendering, and MITM analysis. That made three +things impossible: + +1. **Independent proof.** A rollback theorem should not have to re-import + the render-swap threat model to compile. +2. **Independent evolution.** Rotating the hash profile should not reopen + the enrollment protocol. +3. **Independent reuse.** hermes-agent or an outside harness may want the + Signed-Object and Chain-Store profiles without the ceremony UI. + +Each profile below is a **decision** (an ADR): it is `Proposed`, then +`Accepted`, then `Proven`. A downstream decision **cannot be Accepted until +its dependencies are Proven** — that is the "chain of decisions" made real. + +## The dependency DAG + +``` + ┌─────────────────────────┐ + │ P1 Signed-Object │ foundation: content-addressing, + │ (CID · canon · Sealed) │ canonicalization, signatures, allowlist + └────────────┬────────────┘ + │ + ┌────────────▼────────────┐ + │ P2 Chain-Store │ causal transcript (Merkle DAG), + │ (DAG · anchor · rollbk) │ anti-rollback anchor + └────────────┬────────────┘ + │ + ┌────────────▼────────────┐ + │ P0 Ceremony Contract │ ◄── THE NARROW WAIST + │ five laws · lattice · │ authority algebra + decision seam + │ decision surface · gate │ + gate acceptance + └───┬─────────┬────────┬───┘ + │ │ │ + ┌────────▼───┐ ┌───▼──────┐ ┌▼─────────────────┐ + │ P3 Enroll- │ │ P4 Ident-│ │ P5 Rendering │ + │ ment │ │ ity Life-│ │ Security │ + │ (SAS·PoP) │ │ cycle │ │ (WYSIWYS·transc.)│ + └────────────┘ └──────────┘ └──────────────────┘ + + External fabric: agent-mesh#67 Conversation Graph — the wider causal + transcript of which P2's chain-store is the *authority projection*. +``` + +## The profiles + +| # | Profile | Owns | Depends on | Primary teeth | +|---|---|---|---|---| +| **P0** | [Ceremony Contract](ceremony-contract.md) | the five laws; the authority **product lattice**; the `DecisionSurface` seam; gate acceptance | P1, P2 | Lean (lattice + resolution) → Aeneas (kernel refines model) | +| **P1** | [Signed-Object Profile](signed-object-profile.md) | ContentId (multihash), canonical DAG-CBOR, `Sealed`, deterministic signatures, algorithm allowlist, Profile v1 pins | — | proptest round-trip + Lean canonicalization-injectivity contract | +| **P2** | [Chain-Store Profile](chain-store-profile.md) | the causal-transcript DAG, content-CID/line-CID, `Extends` partial order, external anti-rollback anchor | P1 | Lean trusted-state-machine (`untrusted_step_safe`, checkpoint monotonicity) → Aeneas | +| **P3** | [Enrollment Protocol](enrollment-protocol.md) | SAS pairing, PoP introductions, recipient-issued challenge, external anchors | P0, P1, P2 | **symbolic protocol analysis (Tamarin/ProVerif)** — freshness, MITM, unknown-key-share | +| **P4** | [Identity Lifecycle](identity-lifecycle.md) | roles & delegation, quorum revocation, break-glass & succession | P0, P2 | Lean (quorum k-of-n; non-regression of the load-bearing set) + a liveness/recovery obligation | +| **P5** | [Rendering Security](rendering-security-profile.md) | gate-signed requests, effect binding, display-from-effect, render transcript | P0 | Lean (effect-CID soundness) + a stated human-factors residual (not cryptographic) | + +## The teeth — three tiers, do not confuse them + +Correctness enforcement is layered; each tier assumes the one below and is +verified by a *different* kind of tool. Naming the boundary is the point — +a proof that silently assumes the wrong tier is exactly the "prose becomes +authority-bearing protocol" failure the two security reviews caught. + +``` +Tier 3 Kernel refinement Lean model ⟵Aeneas⟶ pure Rust kernel + (functional correctness) proves the algebra + state machine; Charon + extracts the Rust; a bridge theorem proves + the extraction refines the model. CI gate: + no Rust kernel merges unless the refinement + proof passes. (P0, P2, P4.) + +Tier 2 Protocol safety Tamarin / ProVerif symbolic models prove the + (ceremony soundness) CEREMONIES have no MITM / replay / unknown- + key-share under Dolev-Yao. Algebra proofs do + NOT cover this — a lattice can be flawless + while its handshake leaks. (P3, parts of P5.) + +Tier 1 Cryptographic primitives Ed25519 unforgeability, BLAKE3 collision + (assumed, not proven) resistance, deterministic-nonce property. + Cited to the literature; the trust base. + Rotatable via P1's profile + allowlist. + +Cross-cutting: conformance VECTORS (shared JSON, kyln round-trip pattern) + bind the four client languages (Rust/Python/Dart/TS) to ONE + observable behaviour — the teeth that keep implementations + honest where proofs stop. +``` + +**The refinement boundary (Tier 3), stated once:** the pure kernel is +`resolve`, precedence, the gate acceptance checklist, and the trusted-state +transition — no serde, no IO, no crypto impl, no UI. Crypto and hashing +enter as *abstract injective/one-way contracts* at the kernel edge (P1's +job to satisfy, the kernel's job to assume). This is what lets Aeneas run. + +## Build order (what to prove first) + +The DAG dictates it: **P1 → P2 → P0 → {P3, P4, P5}.** The provable MVP is +**P1 + P2 + P0** — the Signed-Object foundation, the anti-rollback store, +and the authority kernel with its five laws. That triple is GPT-5's #232 +"formal ceremony kernel" almost exactly; adopt it as the P0/P1/P2 slice and +graft the ceremonies (P3–P5) on once the waist is Proven. Nothing in P3–P5 +may be Accepted against an unproven waist. + +## Proof-obligation ledger (moves with the profiles) + +| PO | Profile | Statement | Tier | +|---|---|---|---| +| PO-1 | P0 | ⨅-resolution is order-independent (assoc ∘ comm ∘ idem) | 3 | +| PO-4 | P0 | authority composes by meet, never amplifies | 3 | +| PO-3 | P0 | resolve is total; headless degradation is ⊑-monotone | 3 | +| PO-1c | P1 | canonicalization is injective; verify-over-received-bytes | 3 | +| PO-8 | P1 | algorithm dispatch is allowlist-gated (no downgrade) | 3 | +| PO-2 | P2 | sub-quorum mutation is ⊑-monotone under H1 | 3 | +| PO-2a | P2 | interior deletion/replay rejected vs. a trusted head | 3 | +| PO-2c | P2 | tail truncation + fork rejected vs. the external anchor | 3 | +| PO-5 | P0/P4 | no association without pin; re-key forces re-ceremony | 3 | +| PO-2b | P4 | sub-quorum cannot shrink the load-bearing set | 3 | +| PO-R | P4 | recovery is live: a legitimate owner is never permanently locked out | 3 (liveness) | +| PO-E | P3 | enrollment has no MITM / unknown-key-share under Dolev-Yao | 2 | +| PO-F | P3 | challenge freshness: no self-issued nonce is accepted | 2 | +| PO-W | P5 | effect-CID soundness: an accepted decision's effect = the executed call | 3 | +| WF-1 | P0 | matrix decidable sans escalations (schema predicate) | vector | +| WF-2 | P1 | Memo discipline (CID/sig/parents/Sealed) | vector | + +The waist stays **five laws**; the ledger just relocates each obligation to +the profile that owns it. + +## The rule underneath the whole suite + +> No authorization or claim floats free of the exact history and artifacts +> that gave it meaning. + +P1 gives things names; P2 gives history that cannot silently move; P0 makes +authority an algebra that cannot amplify; P3/P4 bind identity to ceremony; +P5 binds what-was-approved to what-executes. Each is a telescope, not the +sky — the thing being observed is a human's authority, faithfully carried. diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 302e856..a293a13 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,69 +1,27 @@ -# The Ceremony Contract +# P0 — The Ceremony Contract (the narrow waist) -**Status:** DRAFT 0.1.7 (2026-07-16) — revised per review rounds 1–5 (#229) -plus the author's forward-only-ratchet / causal-transcript direction. -Rounds 4–5 = two independent adversarial security reviews (GPT-5/Codex and -a second LLM), adjudicated against the security-engineering canon (RFC 6962, -TUF, Schneier-Kelsey, FssAgg, Landrock-Pedersen; see the PR thread). Two -open design forks (§7): the `attest` factorization and whether to partition -this document into profiles. Normative once accepted. -**Scope:** the decision-surface and first-contact contract between agent-* -libraries (which own decision *semantics*) and harnesses (which own -*rendering*). Companion to the verdict/policy TOML contract (#220) — that -spec is the non-interactive half of this seam; this spec is the interactive -half plus the laws both halves obey. -**Audience:** implementers of client libraries (Rust, Python, Dart, -TypeScript) and of consuming harnesses (newt, hermes, gila, Claude Code / -Codex plugins). A harness can comply with this spec without depending on -any bridle crate; `agent-bridle` is the reference implementation. +**Status:** DRAFT 0.2.0 (2026-07-16) — partitioned from the v0.1.x +monolith into the [Ceremony Suite](README.md). This document is now the +**waist**: the five laws, the authority algebra, and the decision seam. +Everything mechanical it used to contain moved to a profile (P1–P5); this +doc *references* them and states the invariants they must satisfy. +**Depends on:** P1 (Signed-Object), P2 (Chain-Store). +**Depended on by:** P3, P4, P5. +**Teeth:** Lean proof of the lattice laws + resolution, refined to the pure +Rust kernel by Aeneas (Tier 3). See the review history and adjudications on +[#229](https://github.com/Gilamonster-Foundation/agent-bridle/pull/229); +positioning on [#225](https://github.com/Gilamonster-Foundation/agent-bridle/issues/225). -Positioning, prior art, and the adoption strategy are recorded on -[#225](https://github.com/Gilamonster-Foundation/agent-bridle/issues/225); -this document deliberately restates none of it. +**Scope:** the contract between agent-* libraries (decision *semantics*) and +harnesses (*rendering*). A harness can comply without depending on any +bridle crate; `agent-bridle` is the reference implementation. ---- +## 1. The seam -## 1. Terms - -| Term | Definition | Already shipped as | -|---|---|---| -| **Fingerprint** | `H(pubkey)` rendered as a **multihash** — a self-describing, self-certifying *name* for the key. The key is the identity; the fingerprint is its name. Algorithms are profile pins (§8), never law: *BLAKE3 is an implementation detail* | `agent_mesh_protocol::Fingerprint` (raw blake3 today; multihash wire format tracked on agent-mesh#66) | -| **Principal** | the root identity a human (or org) controls; agents and surfaces chain to it | `agent_mesh_protocol::UserKey` | -| **Memo** | the ancestor discipline (content-addressable-python `data.py`): every value carries its content-id and **reads verify it**. Its Rust heirs: `ContentId` (naming), `MerkleNode` (chaining+sigs), and `Sealed` (§3 preamble — verify at construction, immutable after) | lineage; `Sealed` to build | -| **Caveats** | attenuable authority; forms a meet-semilattice | `agent_mesh_protocol::Caveats` (`meet_never_amplifies` is property-tested) | -| **Verdict** | durable disposition: `deny ⊏ attest ⊏ ask ⊏ approve`, ordered by restrictiveness | `agent_bridle_core::policy::Verdict` (`precedence()`; code says `Passkey` until #231 lands) | -| **attest** | allowed only via a presence ceremony — the term follows the trusted-computing literature's *attestation* (Parno et al.); renamed from `passkey`, which remains correct for the *hardware mechanism* only | #231 (coordinated pre-1.0 rename) | -| **chain-store** | *term of art:* the append-only-verifiable record log — parent-linked full lines, signed content CIDs | `content-addressable` `MerkleNode` + the conventions of §5.1 | -| **Gate** | the enforcement choke-point; mints `ToolContext` only inside `authorize()` | `agent-bridle-core` | -| **Surface** | a consumer-supplied renderer of decisions (TUI, GUI, policy file, API) | this spec, §3.7 | -| **Escalation** | a navigation affordance (e.g. `audit`) — never authority | this spec, §3.2 | -| **Pin** | a durable, provenance-carrying record that an identity's key was accepted | this spec, §3.5 | -| **Ceremony** | the interactive resolution of a decision the laws refuse to default | this spec, §4 L5 | -| **ContentId / MerkleNode** | CID (multihash) over canonical DAG-CBOR; parent-linked record; v1 profile hashes with BLAKE3-256 (§8) | `content-addressable` crate | - -Encodings: **one schema, three encodings.** JSON for interchange (client -libs), TOML at rest (#220 policy files), **canonical DAG-CBOR for anything -hashed or signed**. Signatures and `ContentId`s are computed over canonical -bytes only. - -Identifiers are **self-describing**: fingerprints are multihash, keys and -signatures are multicodec-tagged, links are CIDv1 (multihash-native). -Comparison is over the opaque bytes *including* the code — two hash -algorithms never collide silently. The `b3:` / `ed25519:` / `cid:` -prefixes in this document's examples render the **v1 profile** (§8); the -formats themselves are algorithm-agnostic. - -Time: per the workspace hard rule, **wall-clock is never a coordination -primitive**. Validity keys on generation counters -(`valid_for_generation`); RFC 3339 timestamps appear in records as -provenance *data* supplied by the boundary, never read by the kernel. - -## 2. The seam - -Libraries define the decision **space**; consumers define the **layout**. -A library crate MUST NOT contain rendering components or interactive -prompting. A consumer binds a `DecisionSurface` (§3.7); with none bound, -the laws fail closed (§4 L3). +Libraries define the decision **space**; consumers define the **layout**. A +library crate MUST NOT contain rendering or interactive prompting. A +consumer binds a `DecisionSurface`; with none bound, the laws fail closed +(L3). ``` agent-* libraries consumer (harness) @@ -76,950 +34,209 @@ the laws fail closed (§4 L3). └──────────────────────────┘ ``` -### 2.1 Who has an identity - -**Every participant is a fingerprint.** Identity is universal; *roles* -differ in which wire objects a key signs: - -| Role | Holds | Signs | -|---|---|---| -| **Principal** | the root keypair (vault/hardware-resident) | issuance of agent & surface certs; durable loosening entries | -| **Agent** | a keypair chained to a principal | envelopes, requests, delegations | -| **Surface** | a keypair chained to a principal — possibly *nothing else* (a no-compute device is a keypair + a renderer) | its `Decision`s and `AuditRecord`s | -| **Gate** | the enforcement identity (usually = its agent) | chain-store appends | - -A monolith (newt-agent carrying bridle + mesh in-process) is the -**degenerate case**: one fingerprint wearing every role. Delegation is the -roles splitting into separate keypairs — nothing else changes. An -in-process surface MAY omit signing its `Decision`s (same trust domain as -the gate); a **remote** surface MUST sign them, so grant provenance in the -chain-store is attributable end to end. - -**Delegation** (agent → agent, e.g. newt to a back-end worker): both -endpoints present chains to a **common pinned principal** (L5). The -principal's private key is involved at *issuance*, not per-delegation — -headless fleets verify chains offline while the root key stays in -hardware; revocation is a generation bump. Cross-principal delegation is -two principals pinning each other's roots — L5 again, one level up. - -## 3. Wire objects - -Field names are normative. All objects carry `"v": `, and -verification dispatches on it (**version dispatch**, not lenient parsing). - -**Signature verification is over the received canonical bytes, never over -a re-serialization.** Typed deserialization drops unknown fields, so -reserializing a parsed object cannot reproduce the signed digest — verify -the bytes as received, then parse (RFC 8785 canonicalization pitfall; -JWS/COSE practice). **Unknown authority-bearing fields fail closed:** an -object carrying a field the profile version does not define is *rejected*, -not ignored — tolerating it is a silent downgrade / version-confusion -surface (finding #7). Non-authority annotations MAY be preserved verbatim -only when the profile marks them non-critical (COSE critical-header model). -Exactly **one** of `grant` / `escalate` is permitted on a Decision; -zero-or-both is rejected. - -**The Memo discipline (WF-2).** Every wire object is a Memo-descendant, -with capabilities attached by *mechanical criteria* — a boundary test, not -a quota: - -- **content-CID: unconditional.** Anything serializable and meaningful - beyond this process has canonical bytes and therefore a name. -- **`by` + `sig`: at trust boundaries.** REQUIRED whenever the object - crosses to a different fingerprint (remote surface, delegated agent, - another host); MAY be omitted in-process — same trust domain, nothing to - assert. -- **`parents`: for durability.** Anything appended to a chain-store links. -- **Sealed at load.** Implementations construct wire objects only through - verification (CID recomputed; sig checked when present) — verify once at - the boundary, immutable thereafter. Nothing enters a kernel unverified. - -This is const-correctness for integrity: one unsigned hop breaks the chain -of custody the way one non-const cast breaks the guarantee. The discipline -applies to **all of the data layer and none of the resource layer** — the -same line the pure kernel already draws (§6.2). - -### 3.1 PermissionRequest - -What a gate hands a surface when a verdict resolves to interaction. - -```json -{ - "v": 1, - "subject": "b3:9f2c…", // Fingerprint — an identity, never a location - "action": { - "class": "exec", - "display": "run_command: cd ", // human-facing presentation - "effect": "cid:…" // content-CID of the CANONICAL resolved call - }, - "violation": "outside-granted-allowlist", - "matrix": { … }, // §3.2 - "context": { "session": "…", "rationale": "…", "generation": 41 }, - "by": "b3:…", "sig": "…" // the GATE's signature — required remote (Memo discipline) -} -``` - -**`effect` binds the signature to what executes, not to what is shown** -(finding #2 — *what you see is what you sign*, Landrock-Pedersen 1998). -`display` is a lossy human rendering; `effect` is the content-CID of the -canonical, fully-resolved call (arguments *and* resolved resources — the -`CallRequest` the tool layer already produces). The gate MUST, before -minting authority, **recompute the canonical effect from the call it is -about to run and check it equals `action.effect`** — otherwise a stale or -lossy `display`→effect mapping approves X and executes Y while the CID -still matches (confused-deputy / TOCTOU). Display and effect are bound -together under the one signature; a surface that cannot faithfully render -`effect`'s meaning MUST refuse rather than show a prettier `display`. - -**Residual — rendering faithfulness is not cryptographic.** Binding the -signature to `effect` proves the decision is bound to the *data*; it does -**not** prove the UI faithfully showed that data to the human. Truncation, -locale, misleading formatting, hidden arguments, and path abbreviation -remain human-factors surfaces. This spec therefore requires `display` to -be derived from `effect` by a profile-defined, deterministic rendering -function (so display is checkable against effect), and treats a *signed -render transcript* — the surface attesting the exact bytes it presented — -as the strengthening path (a Rendering Security Profile, deferred). Named, -not solved. - -A **remote** surface MUST verify the request's signature and that `by` -chains to a pinned principal **before rendering** — an unauthenticated -prompt is a phishing canvas that trains the human on fake ceremonies -(§5.6), even though its harvested decision is unredeemable (the CID -binding, §3.3). In-process, `by`/`sig` MAY be omitted. - -### 3.2 DecisionMatrix - -The decision *space*. Nothing here encodes layout — rows, columns, and -ordering are the consumer's. - -```json -{ - "verbs": ["allow", "attest", "deny"], - "scopes": ["once", "session"], - "default": ["allow", "once"], // rendering hint AND opinion surface - "escalations": ["audit"] // affordances; MAY be rendered -} -``` - -**The third verb, `attest`,** is the disposition "allowed, but only via a -presence ceremony." It is not new structure — it surfaces the existing -`attest` verdict (né `passkey`, #231) in the matrix. Its meaning composes -with the ordinary scopes: - -- `attest × once` — a fresh ceremony for **every** action; -- `attest × session` — **one** ceremony whose discharge covers the current - generation; prompts weaken to plain confirmation until the generation - ends. The pragmatic affordance for humans who tire of ceremony. - -No `attest_deny` exists: deny is the bottom — there is nothing to -discharge. Discharge scopes order by coverage (`once ⊑ session`), so -broader coverage is a loosening; storing one durably is governed by L2. -The kernel never reads a clock: a discharge binds to a **generation**, and -the *boundary* bumps generations for any reason it likes — a timer, a -screen lock, a lid close. Wall-clock lives in consumer-land as one bump -trigger among several; authority math stays clock-free. - -**`default` is an opinion surface.** It is a rendering hint (the ⬅ cursor) -— never an auto-grant — and it is exactly where an *administrator or -packager* expresses judgment: `rm -rf` ships as `["deny", "session"]`. -Pre-populated matrices plus durable **negative pins** (signed deny records -in the chain-store, §5.1, carrying provenance — *forbidden at Gate g, by -Fingerprint admin, during Ceremony c* — and transportable over agent-mesh -or configuration) are the enterprise-affordance / parental-control story. -Mechanism, not law: provenance and non-removability come from the -chain-store; the authority math is unchanged. - -The scope vocabulary is open: this spec fixes the laws over scopes, not the -set. A durable scope (e.g. `always`) materializes as a **signed loosening -entry** in the policy store and is therefore governed by L2. - -**Well-formedness (WF-1).** Every matrix MUST be decidable with all -escalations unrendered: `verbs × scopes` is non-empty and sufficient. (A -harness with no audit surface — hermes — renders a complete chooser by -omission.) This is a structural predicate on the wire object, checked in -conformance (§6.2) — deliberately *not* a law; see §7. - -### 3.3 Decision - -```json -{ "v": 1, "request": "cid:…", // content-CID of the PermissionRequest AS ISSUED - "grant": { "verb": "allow", "scope": "once" }, - "by": "b3:…", "sig": "…" } // sig over this record's content-CID -{ "v": 1, "request": "cid:…", - "grant": { "verb": "attest", "scope": "session", - "discharge": { "challenge": "cid:…", "attempt": "…" } }, // §3.3.1 - "by": "b3:…", "sig": "…" } -{ "v": 1, "request": "cid:…", "escalate": "audit", "by": "b3:…", "sig": "…" } -``` - -`escalate` carries **zero authority** (L4): it navigates the human to a -richer surface; the request remains undecided until a `grant` returns. A -**remote** surface MUST sign its decisions; an in-process surface MAY omit -`by`/`sig` (§2.1), but the `request` binding is unconditional. - -**Gate acceptance is a checklist of MUSTs — the client is never trusted** -(finding #3; never-trust-client authorization / capability monotonicity). -The gate mints authority only when *all* hold: - -1. `request` equals the content-CID of the `PermissionRequest` the gate - itself issued (render-swap closure, §5.6); -2. `grant.verb ∈ matrix.verbs` **and** `grant.scope ∈ matrix.scopes` — - the answer is a member of the *offered* option-set; -3. the resulting authority is `⊑` the request's ceiling (L4) — a - `once/session` request can never be answered `always`; a surface - cannot return more than was asked; -4. the executable effect recomputes equal to `action.effect` (§3.1); -5. if `grant.verb == attest`, the discharge verifies (§3.3.1). - -A buggy or compromised surface that violates any of these is refused at -the gate, not obeyed — the wire enforces L4, it does not merely state it. - -#### 3.3.1 Attest discharge - -An `attest` grant is **inert until a presence proof is verified** -(finding #4 — a verb meaning "prove presence" must carry the proof and a -normative verify step; WebAuthn/FIDO2 challenge-response). It reuses -bridle's shipped step-up contract: - -- the gate issues a **domain-separated, single-use `Challenge`** (its - content-CID is `discharge.challenge`), bound to this request's CID, the - subject, and the current generation; -- the authenticator returns a `DischargeAttempt` (`discharge.attempt`) — - a WebAuthn assertion over that challenge; -- the gate verifies it through `step_up::DischargeVerifier` and marks the - challenge consumed **before** the grant takes effect. `attest × session` - binds the verified discharge to the generation; a new generation voids - it. An unverified or replayed discharge yields **no** authority. - -**The forward-only ratchet: a presence proof also witnesses a -non-regressing history.** A presence-backed signer MUST NOT authorize -against a view of the world that has moved *backward*. So the challenge -the authenticator signs commits not only to `request`/`decision` but to a -**checkpoint** — and the signer refuses if the store it is shown does not -extend the store it last witnessed. This produces an `AttestationRecord` -(§3.6) carrying *two distinct statements in one signature*: - -- **authorization** — "I was present and approved this `request` / - `decision`" (`request_cid`, `decision_cid`); -- **history witness** — "I verified the presented head `observed_head` - descends from `previous_witnessed_head`, the last head I remember" - (`observed_head`, `previous_witnessed_head`). - -The signer's `previous_witnessed_head` MUST live in its **§5.7 -anti-rollback anchor** (device keystore / hardware counter / witness -quorum) — *not* in the same store it is validating, or an attacker rolls -back the chain and the memory of it together and everything verifies. On -success the signer advances the anchor to `observed_head`. The check is -run **per causal thread** (per conversation / subject), so concurrent -threads never false-trip regression. - -Ratchet failures are refused loudly, never silently accepted: - -- `previous_witnessed_head` is **not an ancestor** of `observed_head` → - either a rollback (regressed head) or a **fork** (a sibling branch that - omits the witnessed head). Both are `CHAIN HISTORY REGRESSION` — halt - and escalate; a fork is proof-of-misbehavior (§5.7), never a branch to - silently adopt. -- generation has regressed → refuse. - -Two monotonicities compose here: the **generation** counter (a coarse -total order per subject) and **DAG ancestry** (the fine causal order, -§3.6). An attestation is valid only when *both* advance. This is not a new -law — it is L2·H1's §5.7 anchor applied at the instant of the ceremony; -its payoff is that **every ordinary presence approval becomes a free -freshness checkpoint**, nailing a timestamp-free piton into the history. - -### 3.4 Introduction - -First contact is a **two-message challenge-response**, because freshness -comes from the *recipient*, never from the introducer (finding #5 — a -self-chosen nonce inside a self-signed object is byte-for-byte replayable; -the party seeking assurance must issue and consume the challenge). - -**Message 1 — the recipient issues a challenge** it will remember: - -```json -{ "v": 1, "challenge": "…", // fresh random, recipient-generated - "issued_by": "b3:…", // the recipient's fingerprint - "for_generation": 41, "expires_at_generation": 42 } -``` - -**Message 2 — the introducer answers, binding that challenge:** - -```json -{ - "v": 1, - "fingerprint": "b3:9f2c…", - "pubkey": "ed25519:…", - "channel": "mdns | dial-back | relay | manual | qr", - "proposed_caveats": [ … ], // Caveats; the requested ceiling - "observed": { "addr_candidates": [ … ] },// candidates, never load-bearing - "answers": "…", // the recipient's challenge, echoed - "transcript": "cid:…", // binds both fingerprints + msg 1 - "sig": "…" // by the INTRODUCED key over all of the above +```rust +#[async_trait] +pub trait DecisionSurface { + async fn decide(&self, req: PermissionRequest) -> Decision; } ``` -On receipt of message 2, an implementation MUST, **before any surface -renders it**: +Policy files (#220) are the headless implementation of this trait. -1. confirm `answers` is a challenge **this recipient issued, still - unconsumed and unexpired**, then mark it consumed — replay-state lives - with the challenger, so a captured introduction cannot be re-presented - (Needham-Schroeder / station-to-station; unknown-key-share closure); -2. confirm the fingerprint's declared hash algorithm is a **member of the - locally-trusted profile allowlist (§8) — checked *before* any hashing** - (finding #8; the `alg:none`/algorithm-confusion class — never let the - object choose its own verifier), then verify the fingerprint is that - algorithm's multihash name of `pubkey` (self-certification checked by - the library, never by the human); -3. verify `sig` under `pubkey` over message 2 including `answers` and - `transcript` — **proof of possession**, transcript-bound so a bystander - cannot relay someone else's introduction as their own. +## 2. Wire objects (the seam's vocabulary) -### 3.5 PinRecord / GrantRecord (the chain-store) +All are Memo-descendants (P1 §3). Full field-level definitions of the +identity/enrollment/store records live in their owning profiles; the three +objects the *seam* itself trades are here. -Durable outcomes are payloads of `MerkleNode`: +### 2.1 PermissionRequest ```json -{ - "parents": ["cid:…"], // line-CIDs of predecessors, sig INCLUDED (§5.1); ⌀ only for genesis - "payload": { - "v": 1, - "fingerprint": "b3:9f2c…", - "pubkey": "ed25519:…", - "channel": "qr", - "caveats": [ … ], // the granted meet, not the request - "decision": { "grant": { "verb": "pin", "scope": "always" } }, - "presence": { "kind": "passkey", "discharge": "…" }, // optional; §5.3 - "granted_at": "2026-07-15T21:04:00Z" // provenance data, not validity - }, - "sig": "ed25519:…" // over the content-CID (presigned form; §5.1) -} +{ "v": 1, "subject": "b3:…", + "action": { "class": "exec", "display": "run_command: cd ", + "effect": "cid:…" }, // canonical resolved call — P5 + "violation": "outside-granted-allowlist", + "matrix": { … }, "context": { "session": "…", "generation": 41 }, + "by": "b3:…", "sig": "…" } // gate signature — required remote (P5) ``` -Two CIDs per record: the **content-CID** (canonical form *minus* `sig` — -what is signed) and the **line-CID** (canonical form *including* `sig` — -what descendants reference in `parents`). Consequences in §5.1. - -### 3.6 Attestation, Audit & Revocation records (ceremonies over the store) - -The store is a **causal transcript** — a Merkle DAG of content-addressed -records, not a linear ledger. `parents` is a *set* (branches and merges -are first-class); "extends" means *reachable-ancestor*, and the -forward-only checks below are over this partial order, not an integer -index. (The chain-store here is the **authority projection** of the wider -Conversation Graph — same `MerkleNode` structure, different payload -`T`; agent-mesh#67.) - -An **AuditRecord** is a ceremony whose subject is the chain head: a -fingerprint witnesses the store's state and signs what it saw. - -```json -{ - "v": 1, - "witnessed_head": "cid:…", // line-CID of the head at review time - "fingerprint": "b3:…", // the witness - "presence": { "kind": "passkey", "discharge": "…" }, // literal finger on hardware - "decision": { "grant": { "verb": "attest", "scope": "once" } } -} -``` +`action.effect` binds the signature to *what executes*, not what is shown +(P5 owns effect-binding and the rendering residual). -An **AttestationRecord** (§3.3.1) is the AuditRecord's authorization-bearing -sibling: one presence signature carrying both an approval and a history -witness. Authorization and witnessing are *distinct fields*, never -conflated — the same finger-press proves "I approved R" and "the world had -not regressed when I did." +### 2.2 DecisionMatrix ```json -{ - "v": 1, - "request_cid": "cid:…", // WHAT was approved (§3.1 effect-bound) - "decision_cid": "cid:…", // the Decision answering it - "observed_head": "cid:…", // history the approval was made against - "previous_witnessed_head": "cid:…", // last head this signer remembers (§5.7 anchor) - "generation": 41, - "nonce": "…", // the gate-issued single-use challenge (§3.3.1) - "signer": "b3:…", - "presence": { "kind": "passkey", "discharge": "…" }, - "sig": "…" // over content-CID of all of the above -} +{ "verbs": ["allow", "attest", "deny"], "scopes": ["once", "session"], + "default": ["allow", "once"], "escalations": ["audit"] } ``` -Acceptance requires `previous_witnessed_head` to be a reachable ancestor of -`observed_head` (else `CHAIN HISTORY REGRESSION`, §3.3.1) — *no -authorization floats free of the exact history that gave it meaning.* +The decision *space*; nothing encodes layout. `attest` is the +presence-required disposition (`attest × once` = ceremony every action; +`attest × session` = one discharge per generation). `default` is the +administrator/packager **opinion surface** (`rm -rf` ships `["deny", +"session"]`), never an auto-grant. Scope vocabulary is open; a durable +scope materializes as a signed loosening entry (L2, via P2). -It is appended to the chain-store like any record (its own content-CID, -sig, parents), so audits are themselves tamper-evident and auditable. No -new law — L5 composed with the log; one new record type. An audit's -`witnessed_head`, once **exported to independently-protected storage** -(§5.7), becomes an anti-rollback anchor — but an AuditRecord that lives -*only inside the chain* rolls back with it and anchors nothing (finding -#1). Anti-rollback is §5.7's job; the AuditRecord is its raw material. +**WF-1 (well-formedness, not a law):** every matrix MUST be decidable with +all escalations unrendered — `verbs × scopes` non-empty and sufficient (a +harness with no audit surface renders a complete chooser by omission). -**RevocationRecord** — punting a load-bearing identity is itself a -ceremony, and per L2 it demands **quorum**: +### 2.3 Decision + gate acceptance ```json -{ - "v": 1, - "payload": { // the CANONICAL UNSIGNED payload — every signer signs THIS - "revoke": "b3:…", // the fingerprint being punted - "reason": "compromise | rotation | retirement", - "succession": "b3:…", // optional successor (root rotation) - "policy": "cid:…" // the quorum policy in force - }, - "signers": [ // detached; each sig is over content-CID(payload) - { "by": "b3:aa…", "sig": "…" }, - { "by": "b3:cc…", "sig": "…" } // sorted by `by`, deduplicated - ] -} -``` - -**One payload, many detached signatures** (finding #6 — a signature nested -in the object it signs is circular; each signer would otherwise sign a -different, progressively-grown record). Every signer signs -`content-CID(payload)` — the fixed unsigned inner object — so all -signatures cover *identical* bytes. `signers` is **sorted by `by` and -deduplicated** (one identity cannot count twice toward `k`), and the -signature set is *not* part of what is signed. The chain-append signature -(`sig` over the whole record's content-CID, WF-2) is separate from the -quorum signatures and added last. - -Acceptance: `|distinct valid signers ∩ eligible| ≥ k` under the named -`policy`. The quorum policy (k, n, eligible signers) is itself a -principal-signed loosening entry established at setup — defining *who may -revoke* is authority structure, governed by L2 like any other loosening. -Revoking the **last** root without a `succession` ends the mesh; -implementations MUST refuse it unless the record carries an explicit -`"tombstone": true`. - -**Break-glass and succession are required, not optional.** A quorum strong -enough to defeat a hostile revocation is, by the same math, strong enough -to lock out a legitimate owner who loses `k` keys — availability cuts both -ways. So enrollment MUST also provision recovery: a pre-enrolled recovery -factor (offline hardware key / printed share) counted in `n`, plus a -succession path that transfers a principal root to a new key under quorum -*or* a time-delayed unilateral recovery (a self-revocation that any single -device can start but that only takes effect after a published generation -delay, giving co-signers a window to veto — the "social recovery with -timelock" pattern). This is a **named required subsystem** (candidate: -Identity Lifecycle profile), sketched here, not yet fully specified. - -### 3.7 DecisionSurface (the seam) - -Language-idiomatic equivalents of: - -```rust -#[async_trait] -pub trait DecisionSurface { - async fn decide(&self, req: PermissionRequest) -> Decision; -} +{ "v": 1, "request": "cid:…", "grant": { "verb": "allow", "scope": "once" }, + "by": "b3:…", "sig": "…" } ``` -Policy files (#220) are the headless implementation of this trait. Client -libraries in Python/Dart/TypeScript expose the same shape over the JSON -wire objects. - -## 4. The Laws (normative) - -Five laws. Each carries a proof obligation (PO); §6.2 maps POs to the -formal track. Per the governance rule (§7), nothing joins this section -without a proof obligation demanding it. +`escalate` carries **zero authority** (L4); exactly one of `grant`/ +`escalate`. **Gate acceptance is a checklist of MUSTs — the client is never +trusted:** + +1. `request` = the content-CID of the request the gate itself issued; +2. `grant.verb ∈ matrix.verbs` and `grant.scope ∈ matrix.scopes`; +3. the result is `⊑` the request ceiling (L4) — never answer `once` with + `always`; +4. the executable effect recomputes equal to `action.effect` (P5); +5. if `verb == attest`, the discharge verifies **and** its history witness + passes the forward-only ratchet (§3). + +A surface violating any is refused at the gate — the wire *enforces* L4, it +does not merely state it. + +## 3. Attest discharge + the forward-only ratchet + +An `attest` grant is **inert until a presence proof is verified** (WebAuthn/ +FIDO2 challenge-response via the shipped `step_up::DischargeVerifier`): the +gate issues a domain-separated, single-use `Challenge` bound to the request +CID, subject, and generation; the authenticator returns a +`DischargeAttempt`; the gate verifies and consumes it **before** the grant +takes effect. + +**A presence proof also witnesses a non-regressing history.** The challenge +commits to a **checkpoint**; the signer refuses unless the store it is +shown *extends* the store it last witnessed. This yields an +`AttestationRecord` (P4) carrying two distinct statements under one +signature — **authorization** ("I approved R") and **history witness** ("I +verified `observed_head` descends from `previous_witnessed_head`"). The +signer's `previous_witnessed_head` MUST live in its **P2 §4 anti-rollback +anchor**, not the store it validates. Rollback *or* fork → `CHAIN HISTORY +REGRESSION` → halt + escalate. Checked **per causal thread**, so concurrent +threads never false-trip. Generation (total order) and DAG ancestry +(partial order) must *both* advance. Payoff: **every ordinary approval is a +free freshness checkpoint.** Not a new law — L2·H1's anchor at ceremony +time. + +## 4. The Laws (normative — the whole waist) + +Five laws. Nothing enters this section without a proof obligation (§5); +everything else is mechanism (a profile) or well-formedness. ### L1 — Resolution is a meet - -Verdicts are totally ordered by restrictiveness -(`deny ⊏ attest ⊏ ask ⊏ approve`). Resolution of request `q` against rule -set `R`: - -``` -resolve(R, q) = ⨅ { verdict(r) | r ∈ R, r matches q } -``` - -**Consequence:** ⨅ is associative, commutative, idempotent ⇒ resolution is -independent of rule order, file order, and load order. No ordering attack -exists. **PO-1.** +Verdicts are ordered by restrictiveness (`deny ⊏ attest ⊏ ask ⊏ approve`). +`resolve(R, q) = ⨅ { verdict(r) | r ∈ R, r matches q }`. ⨅ is associative, +commutative, idempotent ⇒ resolution is independent of rule/file/load order. +No ordering attack exists. **PO-1.** *(The `attest`-factorization fork — +one verb axis vs. `effect × assurance × scope` — is a lattice-shape choice; +L1 survives either, since a product of lattices is a lattice. Author's call; +see README.)* ### L2 — Tamper-boundedness - -For any mutation `m` made by a party holding **fewer than quorum(target)** -of the designated keys: - -``` -resolve(m(R), q) ⊑ resolve(R, q) (no widening) -LoadBearing(R) ⊆ LoadBearing(m(R)) (no structural narrowing) -``` - -Two directions, one law. **Downward:** a sub-quorum actor can only narrow -*authority*, never widen it — forged restrictive entries are a nuisance; -forged loosening entries are dropped at load (verification is -fail-closed). **Upward:** a sub-quorum actor cannot shrink the -**load-bearing identity structure** — pinned principals, enrolled devices, -blessed anchors. Narrowing splits into two species: *reversible* narrowing -(a spammed deny — the principal can prune it) is nuisance-bounded and -needs no key; **irreversible narrowing — revoking a load-bearing identity -— requires quorum**, because a fail-closed system's own failure mode is an -adversary who can *force* closure. "Reset mesh" must not be a -denial-of-service surface. Availability is a security property. - -**Mechanism honesty (two layers).** Under flat policy files this law holds -for **additions only** — deleting a restrictive entry widens authority and -flat files cannot detect it. The chain-store (§5.1) extends detection to -{add, delete, reorder} of the log's *interior*. But the chain-store **by -itself does not detect rollback/truncation of the log's *tail*, nor a -split-view fork** (finding #1): an attacker who truncates a restrictive -suffix and its head, or presents an older valid fork, leaves a prefix that -still verifies — this is the established limit of every hash-chained log -(Schneier-Kelsey 1999; FssAgg / eprint 2008/185; and the reason -Certificate Transparency requires gossiped Signed Tree Heads, RFC 6962). -Closing it requires an **independently-protected monotonic anchor** (§5.7), -*not* the chain alone. An anti-rollback claim resting on an in-chain -AuditRecord is circular — it rolls back with the log it certifies. - -**Hypothesis H1 (append-only-verifiability + monotone freshness):** `m` -cannot undetectably remove/reintroduce an *interior* record (chain-store), -**nor roll the log back past the last independently-anchored head** (§5.7). -H1 is *discharged by mechanism*, not assumed: interior integrity by the -chain (**PO-2a**), tail/fork integrity by the external anchor (**PO-2c**), -revocation-quorum soundness by §3.6 (**PO-2b**). **PO-2** proves ⊑-monotony -under H1. +For any mutation `m` by a party holding **fewer than quorum(target)** keys: +``` +resolve(m(R), q) ⊑ resolve(R, q) (no widening) +LoadBearing(R) ⊆ LoadBearing(m(R)) (no structural narrowing) +``` +Two directions, one law. **Downward:** sub-quorum actors only narrow +authority; forged loosening entries drop at load (fail-closed). **Upward:** +sub-quorum actors cannot shrink the load-bearing identity structure — +reversible narrowing (deny-spam) is a nuisance; **irreversible narrowing +(revoking an identity) requires quorum** (P4), because a fail-closed +system's failure mode is an adversary who can *force* closure. Availability +is a security property. +**H1 (append-only-verifiability + monotone freshness)** is discharged *by +mechanism*, split across P2: interior integrity by the chain (**PO-2a**), +tail/fork by the external anchor (**PO-2c**), revocation-quorum by P4 +(**PO-2b**). **PO-2** proves ⊑-monotony under H1. (H1's chain-alone form was +over-claimed in v0.1.x and corrected — see README teeth Tier 3.) ### L3 — Fail-closed totality - -`resolve` is total: every request yields a verdict; no input reaches -"undefined permission." The interactive bottom is `ask`. Absent a bound -surface, interaction-requiring verdicts degrade restrictively: - -``` -headless: ask ↦ deny, attest ↦ deny (degradation is ⊑-monotone) -``` - -**PO-3.** +`resolve` is total; no input reaches "undefined permission." Interactive +bottom is `ask`; absent a bound surface, `ask ↦ deny`, `attest ↦ deny` +(degradation is ⊑-monotone). **PO-3.** ### L4 — Attenuation - -Authority composes by meet and never amplifies: - -``` -effective = granted ⊓ required granted = requested ⊓ ceiling -``` - -Escalations carry no authority: `authority(escalate) = ⊥`. -Already property-tested upstream (`meet_never_amplifies`, -agent-mesh-protocol); this law names the obligation the formal track -re-proves over the kernel. **PO-4.** +`effective = granted ⊓ required`; `granted = requested ⊓ ceiling`; +`authority(escalate) = ⊥`. Authority composes by meet, never amplifies +(property-tested upstream as `meet_never_amplifies`). **PO-4.** ### L5 — The ceremony gate - -``` -association(peer) ⇒ pinned(fingerprint(peer)) -``` - -`fingerprint = H(pubkey)`, a multihash name, is **self-certifying** for any -H the profile pins (§8) with two required properties: collision resistance, -and hardness of finding a key matching a given name. Therefore **re-key ⇒ -new fingerprint ⇒ unpinned ⇒ full re-ceremony**. No silent identity swap is -expressible. (Rotating H is *re-naming*, not re-keying: the key signs a -linkage record binding its new name — quorum-free, the identity never -moved.) A pin is created only by (a) a `Decision::grant` from a bound -surface, or (b) a pre-pinned policy entry — which is a signed loosening -entry and therefore governed by L2. - -The pinned predicate is **transitive through certification**: - +`association(peer) ⇒ pinned(fingerprint(peer))`. `fingerprint = H(pubkey)` +is self-certifying (P1) — re-key ⇒ new fingerprint ⇒ unpinned ⇒ full +re-ceremony; no silent identity swap is expressible. Pinned is **transitive +through certification**: ``` -pinned(fp) ⇔ fp ∈ PinSet - ∨ ∃ chain: fp →* root, root ∈ PinSet, - proof-of-possession at every link +pinned(fp) ⇔ fp ∈ PinSet ∨ ∃ chain: fp →* root ∈ PinSet, PoP at every link ``` +so pinning a principal admits the agents/surfaces it issues (delegation, P4; +shipped as mesh `CertChain::verify` + PoP, #39/#40). **PO-5** (incl. chain +soundness). -so pinning a principal admits the agents and surfaces it issues — this is -delegation (§2.1), and it is already shipped mechanism: mesh `CertChain:: -verify` chains to the user root fail-closed (agent-mesh #39, §9.1) and -certification requires proof-of-possession (agent-mesh #40, §9.2). -**PO-5** (now including chain soundness). - -## 5. Mechanism (below the law line) - -Mechanisms implement or discharge the laws; they add no new ones. - -### 5.1 The chain-store (load-bearing for L2) - -Records are `MerkleNode` in the `content-addressable` crate (multihash -CIDs — §8, canonical DAG-CBOR, parent links), with these conventions: - -``` -c_i = H(canon(record_i ∖ sig)) content-CID (what is signed) -s_i = Sign(k, c_i) the signature (sig-trim convention) -line_i = record_i ∪ { sig: s_i } the full at-rest line -ℓ_i = H(canon(line_i)) line-CID (what parents reference) -parents(record_{i+1}) ∋ ℓ_i descendants commit to content AND sig -``` - -Parents reference the **line-CID** — the full predecessor *including its -signature* — so stripping or swapping a historical signature breaks the -chain just as surely as editing content. Editing or removing an *interior* -record orphans every descendant's parent link — it verify-fails loudly -**against a head the verifier already trusts**. This extends interior -integrity to deletions and retires the flat-file known-limit (policy.rs; -#226). - -**What the chain alone does NOT do (finding #1):** verification is always -*relative to a head*. Against an attacker who also controls the head — -truncating the tail, or presenting a wholly older/forked-but-valid log — -the surviving prefix verifies fine and nothing is orphaned. The chain -gives interior integrity; **tail and fork integrity require the external -anchor of §5.7.** This spec does not claim otherwise. - -Two stated assumptions, doing different jobs: +## 5. Proof-obligation ledger (owned here; full suite in README) -1. **Deterministic signatures.** For Profile v1, signing MUST be - deterministic: `Sign(sk, message)` yields one canonical signature - encoding for a fixed key and message. Ed25519 provides this (RFC 8032 - derives the nonce from key and message), so the whole log is a **pure - function** of (genesis, payload sequence, keys). A randomized scheme - (ECDSA with a random nonce) would produce a different signature — hence - a different line-CID — on every re-sign, forking the chain. (The same - fact stated information-theoretically: the signature adds no entropy - given content and key, `H(sig | content, key) = 0`. The prose rule is - normative; the identity is just why.) This governs *reproducibility*. -2. **Collision resistance of `H`** governs *tamper-evidence*. The two are - independent: determinism makes the log replayable; collision resistance - makes it unforgeable. - -**At rest:** the log's on-disk representation is **JSONL as a lossless -line-oriented view** of the canonical DAG-CBOR records — one `line_i` per -line, human-auditable, no comments or other affordances to make -canonicalization ambiguous. CIDs and signatures are always computed over -the canonical form, never over the view. - -### 5.2 Signed loosening verdicts (shipped) - -The exposure is asymmetric — a forged `deny`/`ask` only narrows; a forged -`approve` widens — so signatures are required on loosening entries only. -Shipped in #226/#227 (`ExecEntry::signing_payload`, -`PolicyFile::verified_approves`, fail-closed drop). This spec inherits that -contract unchanged and extends it to pins (§3.5). - -### 5.3 Presence-attested pins - -A pin MAY carry a `presence` discharge: a WebAuthn/passkey step-up bound to -the pin's `ContentId` (the `DischargeVerifier` seam; PR #214 lineage). This -upgrades first contact from "someone at a keyboard clicked" to a -hardware-attested human decision. Optional by law, recommended for pins -whose caveat ceiling is broad. - -### 5.4 Enrollment ceremony (SAS pairing) - -How a new device or surface — possibly with no compute beyond key storage -and a screen — gets its keypair admitted under a principal (L5, applied to -one's own devices). - -The ritual is a **short-authentication-string comparison**, and its shape -matters: a naive "new device shows a phrase, trusted device sends it back -encrypted" is MITM-relayable — an attacker owning the channel relays the -phrase both ways and each side verifies *the attacker's* key. The sound -construction (per Bluetooth numeric comparison / ZRTP): - -1. both devices **commit** to nonces before revealing anything; -2. both **derive** the SAS from the *entire key-exchange transcript* — - commitments, reveals, **and the long-term public keys being enrolled**. - This last inclusion is not optional: an SAS over only the ephemeral - session material lets a MITM relay the handshake honestly while - substituting the long-term keys — the classic key-substitution hole - (§5.6). The SAS must checksum *what is being pinned*, not merely the - channel; -3. **a human compares the SAS on both screens** — a low-bandwidth - *authenticated* channel the network MITM is not on. It is not - unspoofable: it has a measurable error probability (the SAS entropy) - and rests on human-factors assumptions — humans are non-deterministic - peripherals who skim. The phrase is not a secret to transport; it is a - checksum of the handshake two screens must agree on, and its strength - is exactly the guess probability below. - -Commit-before-reveal forces a MITM to *guess* the SAS in advance; one -round of a `xxx-000`-style SAS ≈ 1-in-46k. Paranoia is then a parameter, -not a mood: - -``` -strength(enrollment) = (SAS entropy × rounds, distinct witnesses, presence) -``` - -Policy sets minima **by caveat ceiling**: a broad ceiling demands more -rounds, witnessing from ≥ 2 previously-secured surfaces, and a hardware -presence discharge (the `attest` verdict doing enrollment duty — -thumbprint on the device). Independent witnesses buy more than extra -rounds: rounds shrink the guess probability; witnesses multiply the -channels an attacker must own *simultaneously*. The completed enrollment -is a `PinRecord` in the chain-store whose payload carries the ceremony -parameters — auditable later (§3.6). - -**Punting ≥ pinning.** Revocation (§3.6) is graded on the same scale: -removing an identity demands at least the ceremony strength that enrolled -it — quorum co-signers being the witness axis. Enrollment strength sets a -floor the revocation ceremony must meet, so the strongest identities are -exactly the ones an adversary finds hardest to destroy. - -### 5.5 External anchors (corroboration, never the root) - -A principal root is **self-sovereign**. Externally published keys — -GitHub-registered keys (`github.com/.keys`), DNS, an org CA, a -previously-secured device — are **candidate corroboration channels** for -it: independent witnesses that the root you are pinning belongs to the -human you think it does. Per the floating-identity doctrine, no anchor is -load-bearing: GitHub corroborates the root; it never *is* the root. A -user with no GitHub enrolls by ceremony alone (§5.4) with zero degradation -in the algebra — anchors raise corroboration, never gate participation. - -**Anchors are blessed, participating identities.** ANY public-key display -surface qualifies — *provided the key owner blesses it*: an `AnchorRecord` -(principal-signed binding of channel + location + displayed key) appended -to the chain-store. An unblessed anchor is ignored; a blessed one may -*participate* as a signing/corroboration surface in ceremonies (a -GitHub-key signature counting as one enrollment witness). Blessings are -revoked like any load-bearing identity — RevocationRecord, quorum (§3.6) -— so a captured anchor can be cut loose without ceremony-strength loss: -corroboration is k-of-n, and n just shrank by one. - -### 5.6 The MITM ledger — every channel, every closure - -"No MITM hole anywhere" is a claim to *enumerate*, not to feel: - -| Channel | Attack | Closure | Residual | +| PO | Law | Statement | Tier | |---|---|---|---| -| First contact (stranger) | TOFU key swap | L5 ceremony: SAS or out-of-band fingerprint check; or chain to a common pinned principal | the ceremony itself (below) | -| First contact | **introduction replay / unknown-key-share** | PoP: the introduced key signs the Introduction over a transcript-bound `fresh` (§3.4) | — | -| Enrollment handshake | relay + **key substitution** | commit-then-reveal; SAS covers the **long-term keys** (§5.4·2); human comparison is the authentic channel | SAS guess ≈ n⁻ᵏ per round; rounds × witnesses shrink it | -| Post-pin transport | impersonation on any raced path | dial-by-pubkey: QUIC/TLS authenticates the **node key** — a path that answers must hold the private key; paths are hints, never identity | key theft (out of scope: L2 quorum + revocation) | -| Delegation | rogue "delegated" agent inserted | chains verify to a pinned principal; proof-of-possession at issuance (mesh #39/#40) | principal-root compromise (quorum revocation + succession) | -| Remote surface | **render-swap**: human approves X, gate runs Y | `Decision.request` = content-CID of the request as rendered; sig covers it; gate matches CIDs (§3.3) | compromised surface *device* → its grants are attributable + revocable | -| Remote surface | **phishing canvas**: forged/unsolicited requests train the human | requests are gate-signed; surface verifies chain-to-pinned-principal *before rendering* (§3.1) | compromised gate key → quorum revocation | -| Chain-store sync | forgery in transit | records are self-authenticating (signed + chained); transport can corrupt nothing silently | — | -| Chain-store sync | **rollback / truncation / fork** (stale-but-valid head hides a revocation) | external anti-rollback anchor (§5.7): independently-stored monotonic head + witness cosigning + fork = proof-of-misbehavior. The chain alone does NOT close this. | withholding = visible staleness vs. a remembered anchor → fail closed | -| Anchor channel | compromised registry vouches a fake root | anchors are blessed, k-of-n, never sufficient alone (§5.5) | k−1 colluding anchors corroborate nothing | -| The human | prompt fatigue / phishing the ceremony | `attest` for high ceilings; distinct ceremony UI is consumer guidance (newt#1209) | irreducible; parameterized paranoia exists for exactly this | - -The pattern behind every row: **the authenticated thing is always the -key, never the channel** — locations, relays, registries, and rendered -pixels are candidates and hints; signatures and CIDs are what the gate -trusts. One doctrine, applied across every row. +| PO-1 | L1 | ⨅-resolution order-independent | 3 | +| PO-2 | L2 | sub-quorum mutation ⊑-monotone under H1 | 3 | +| PO-3 | L3 | totality + monotone headless degradation | 3 | +| PO-4 | L4 | meet never amplifies | 3 | +| PO-5 | L5 | no association without pin; re-key forces re-ceremony | 3 | -### 5.7 The anti-rollback anchor (external, load-bearing for L2·H1) +Pilot proofs: PO-1, PO-2. The kernel is pure `resolve` + precedence + the +gate checklist + the P2 trusted-state transition — no serde/IO/crypto/UI; +crypto enters as P1's abstract contracts. Conformance **vectors** bind the +four client languages to one observable behaviour. -A hash-chained log is verified *relative to a head*. Every such log — -Schneier-Kelsey secure audit logs (1999), the FssAgg truncation analysis -(eprint 2008/185), Certificate Transparency (RFC 6962) — shares one limit: -**an attacker who controls the head can truncate the tail or present an -older/forked-but-internally-valid log, and it still verifies.** The chain -gives interior integrity only. Closing tail-and-fork requires state the -attacker does not control. This spec adopts the three canonical layers, -in ascending assurance: +## 6. Cross-cutting: the MITM ledger -1. **Independently-protected monotonic head (required).** Each participant - remembers, in storage separate from the log, the highest - `(generation, length, head-CID)` it has accepted, and **MUST reject any - presented head that is not a consistent forward-extension** of it — - never a shorter length or lower generation (the TUF rule: *"clients - MUST NOT replace metadata with a version number less than the one - currently trusted"*, RFC 6962 monotonicity). This alone defeats - truncation and rollback *for that participant*. -2. **Witness cosigning (recommended for shared stores).** The head is - periodically countersigned by one or more witnesses whose signatures - are the `AuditRecord`s of §3.6 **exported off-chain**. A participant - accepts a head only if it carries a witness cosignature no older than - its freshness policy (CT gossip / STH; witness-cosigning). This defeats - *secret* equivocation: to fool a victim the attacker must fork the - witnesses too. -3. **Fork = proof of misbehavior (required).** Two validly-signed heads of - the same store at the same length with different CIDs are incontestable - evidence of equivocation (RFC 6962). Implementations MUST treat a - detected fork as a security event, halt authority minting from that - store, and escalate — never silently pick one. +"No MITM hole anywhere" is a claim to *enumerate*. Each row's closure lives +in the cited profile; the unifying rule is **the authenticated thing is +always the key, never the channel.** -For a solo user (§5.4's n=2 world) the monotonic head lives on each of -their own enrolled devices, and each device is the other's witness — the -same k-of-n substrate as revocation, reused. A quorum/witness set is the -enterprise instance of the identical mechanism. **Nothing here trusts the -storage medium**; the anchor is trusted state a participant carries into -each verification, exactly as `pinned` is. - -## 6. Conformance - -### 6.1 Shared vectors - -`tests/vectors/*.json` (to be populated with the kernel): each vector is -`(policy set, request) → verdict` or `(matrix, decision) → outcome`. All -client libraries — Rust, Python, Dart, TypeScript — MUST produce identical -results. Property suites (proptest here; hypothesis/fast-check in bindings) -check L1, L3, L4 executably. This is the kyln round-trip-law pattern, -cross-language. - -### 6.2 Formal obligations - -The decision kernel (pure `resolve`, precedence, verified-load fold; no IO, -no serde, no wall-clock) is carved for extraction by Charon and proof in -Lean via Aeneas: - -| PO | Law | Statement proved | +| Channel | Attack | Closure (profile) | |---|---|---| -| PO-1 | L1 | ⨅-fold is order-independent (assoc ∘ comm ∘ idem) | -| PO-2 | L2 | sub-quorum mutation is ⊑-monotone, under H1 | -| PO-2a | L2·H1 | chain-store rejects removed/replayed *interior* records (vs. a trusted head) | -| PO-2b | L2 | a sub-quorum coalition cannot shrink the load-bearing pin set | -| PO-2c | L2·H1 | against the §5.7 anchor, tail truncation and fork are rejected (not merely detected-later) | -| PO-3 | L3 | totality + monotone headless degradation | -| PO-4 | L4 | meet never amplifies (kernel restatement) | -| PO-5 | L5 | no association without pin; re-key forces re-ceremony | -| WF-1 | §3.2 | matrix decidable sans escalations (structural predicate, not a law) | -| WF-2 | §3 | Memo discipline: CID unconditional; sig at trust boundaries; parents for durability; Sealed at load | - -Pilot: PO-1 and PO-2. - -### 6.3 Consumer checklist - -A conforming harness: - -- [ ] binds a `DecisionSurface` (interactive) or policy files (headless) — - or accepts the L3 degradation to deny -- [ ] renders `verbs × scopes` completely; MAY render escalations (WF-1, §3.2) -- [ ] treats `default` as a cursor hint, never an auto-grant -- [ ] never persists a loosening outcome without a signature (L2) -- [ ] relies on the library's self-certification + proof-of-possession - checks (§3.4) rather than asking the human to compare key bytes -- [ ] (remote) verifies a request's gate signature before rendering (§3.1) -- [ ] constructs wire objects only through verified load (Sealed; WF-2), - verifying signatures over received bytes and rejecting unknown - authority-bearing fields fail-closed (§3) -- [ ] (gate) enforces the §3.3 acceptance checklist — request-CID match, - matrix membership, ceiling, effect recomputation, attest discharge — - never trusting the surface -- [ ] (gate) checks a fingerprint's algorithm against the trusted profile - allowlist *before* dispatch (§3.4, §8) -- [ ] carries a §5.7 anti-rollback anchor for any shared/persisted store — - naming *where* the monotonic head lives (device keystore / TPM or - hardware monotonic counter / witness quorum / separately-protected - checkpoint); "on disk beside the log" does NOT qualify -- [ ] ships no rendering into any agent-* library crate +| First contact | TOFU key swap | L5 ceremony / chain-to-pinned-principal (P3) | +| First contact | intro replay / unknown-key-share | recipient-issued challenge + PoP (P3) | +| Enrollment | relay + key substitution | commit-reveal SAS over long-term keys (P3) | +| Post-pin transport | path impersonation | dial-by-pubkey; paths are hints (mesh) | +| Delegation | rogue delegated agent | chains verify to a pinned principal (P4) | +| Remote surface | render-swap | `Decision.request` CID binding (P5) | +| Remote surface | phishing canvas | gate-signed requests, verified before render (P5) | +| Store sync | rollback / truncation / fork | external anti-rollback anchor (P2 §4) | +| Anchor channel | fake-root vouch | blessed anchors, k-of-n, never alone (P4) | +| The human | prompt fatigue | `attest` for high ceilings; UI guidance (residual) | ## 7. Governance — law minimalism A good system has only the laws it absolutely needs. **Nothing enters §4 -without a proof obligation demanding it; everything else is mechanism -(§5) or well-formedness (§3).** The count is audited ruthlessly: - -- **Executed (review 1, 2026-07-15):** L6 demoted to WF-1 — completeness - without escalation is a structural predicate on a wire object, not an - algebraic invariant of authority. Six laws became five. -- **Executed (review 2, 2026-07-15):** the revocation-DoS invariant - ("reset mesh" must not be an attack surface) was absorbed into L2 as its - upward direction — tamper-*monotonicity* became tamper-*boundedness*. - Zero count change; PO-2b added. -- **Executed (review 3, 2026-07-16):** the Memo discipline and the - multihash directive landed as wire discipline (WF-2) and profile (§8) — - laws name *properties*; algorithms are pins. L5 de-algorithm'd. Zero - count change. -- **Executed (review 4, 2026-07-16 — adversarial security review, - GPT-5/Codex):** eight true-positive findings, all adjudicated against - the canon and closed as *enforcement* (gate MUSTs, §3.3), *mechanism* - (external anti-rollback anchor §5.7; recipient-issued challenge §3.4; - canonical quorum payload §3.6; effect-CID binding §3.1), and *wire - discipline* (fail-closed unknown fields §3; algorithm allowlist §3.4, - §8). The one law touched — L2 — was *corrected*, not multiplied: its - H1 was over-claimed (chain alone ⇏ rollback resistance) and is now - honestly split across chain + anchor. **Still five laws.** The review's - meta-lesson — "prose becomes authority-bearing protocol" — is the case - for the formal track, not against the design. -- **Refinements (review 5, 2026-07-16 — second LLM):** confirmed every - round-4 fix independently, and sharpened five: deterministic-signature - prose over entropy notation (§5.1); human SAS comparison is a - measurable-error authenticated channel, *not* "unspoofable" (§5.4); - rendering faithfulness named as a residual with a deterministic - display-from-effect rule + deferred render transcript (§3.1); break-glass - + succession made a required subsystem (§3.6); anti-rollback head - location made explicit in conformance (§6.3). No law change. -- **Extension (author, 2026-07-16 — forward-only ratchet):** a presence - attestation now also witnesses a non-regressing history head (§3.3.1, - `AttestationRecord` §3.6). This is *not* a sixth law — it is L2·H1's §5.7 - anchor applied at ceremony time, so every approval doubles as a freshness - checkpoint. The store is restated as a **causal transcript** (Merkle DAG, - authority projection of the Conversation Graph, agent-mesh#67). Still - five laws. -- **Next candidate:** L1+L4 are one law ("authority composes by meet") on - two carriers (verdict lattice, caveat lattice); if the Lean formulation - unifies them cleanly, five becomes four. - -**Open design forks (author's call — not agent-decidable):** - -- **The `attest` factorization.** Review 5 argues `attest` is an *assurance - condition* (none / presence / hardware-backed), not an *outcome* peer to - `allow`/`deny`, and proposes three axes — `effect × assurance × scope` — - over one verb axis. Trade-off: three axes are semantically cleaner and - future-proof; the single total order is structurally minimal and keeps - L1 a plain meet (a product of lattices is still a lattice, so L1 survives - either way — the question is factoring, not soundness). Deferred to the - author. -- **Partition into profiles.** Both reviews note this document has grown - from a decision-surface contract into a full security constitution - (~900 lines: identity, enrollment, revocation, audit, storage, hash - agility, rendering). Recommended split — Ceremony Contract (decision - semantics) · Signed-Object Profile · Chain-Store Profile · Enrollment - Protocol · Identity Lifecycle · Rendering Security Profile — with the - five laws as the narrow waist. This is *exactly* the loosely-coupled / - functionally-cohesive doctrine applied to the spec itself; strongly - recommended, deferred to the author to sequence. - -Additions from the same review — the `attest` verb, negative pins, the -`AuditRecord` — cost **zero** laws: each collapsed into existing structure -or landed below the line. The algebra decides the count; ambition doesn't. - -## 8. Profile v1 (pins, not laws) - -Algorithms are **implementation details**; each pin states the *property* -any replacement must carry. Identifiers are self-describing (multihash / -multicodec), so profile rotation happens under a running mesh. - -**Agility needs an allowlist, or it is a downgrade attack** (finding #8). -Self-describing identifiers let the *object* declare its algorithm — so a -verifier that dispatches on the declared code alone lets the attacker pick -a broken hash (the `alg:none` / algorithm-confusion class). Therefore: a -verifier MUST check the declared code against **this locally-trusted -profile table before hashing or verifying**, and reject anything outside -it. Profile *rotation* is a negotiated, principal-signed change to the -allowlist (a loosening entry, L2), never a per-object choice. Agility -lives in the profile, not on the wire. - -| Pin | v1 value | Required property (the law's interest) | -|---|---|---| -| Content hash `H` | BLAKE3-256 (multihash `0x1e`) | collision resistance; preimage hardness (L5 self-certification) | -| Signature | Ed25519 (RFC 8032) | **deterministic** — load-bearing for chain reproducibility (§5.1·1); existential unforgeability | -| Canonical encoding | DAG-CBOR (codec `0x71`) | injective, canonical serialization (one value ⇒ one byte string) | -| Links | CIDv1 | multihash-native, codec-tagged | - -Rotating `H` is a **re-naming ceremony** (L5): keys sign linkage records -binding their new names; identity never moves. Rotating the signature -scheme is heavier — it is a **re-keying** (full L5 re-ceremony per -identity) because the key *is* the identity. The wire format -(`agent-mesh#66`) carries the codes either way. - -## 9. Relations - -- #220 — verdict/policy TOML contract (headless half of this seam) -- #225 — design directive, strategy, client-lib matrix (umbrella) -- #226 / #227 — signed loosening entries (shipped mechanism, §5.2) -- #231 — coordinated pre-1.0 rename `passkey` → `attest` (code catch-up - for this spec's vocabulary) -- PR #214 — presence/WebAuthn lineage (§5.3) -- agent-mesh#65 — `Introduction` struct and mesh decision surfaces -- agent-mesh#66 — enrollment/delegation protocol; multihash wire format - for `Fingerprint` -- agent-mesh#67 — the **Conversation Graph** (distributed content-addressed - causal transcript); the chain-store is its authority projection -- newt-agent#1209 — first consumer: pinning ceremony (HIGH) -- agent-mesh `docs/decisions/floating_identity.md` — identity doctrine - (law 5 there = L5 here; law 4 "conversations outlive locations" is what - the Conversation Graph makes concrete) -- `content-addressable` crate — `ContentId`, canonical DAG-CBOR, - `MerkleNode` (§5.1) +without a proof obligation; everything else is a profile or a +well-formedness predicate.** The count history: six → five (L6 → WF-1); +then held at five through the revocation-DoS absorption (L2 upward), the +Memo/multihash discipline (WF-2/P1), two adversarial security reviews +(rounds 4–5, eight findings closed as enforcement/mechanism/wire-discipline +— L2's H1 *corrected*, not multiplied), and the forward-only ratchet. +**Next candidate:** L1+L4 unify as one "authority composes by meet" law on +two carriers (verdict + caveat lattices) — five → four if the Lean +formulation collapses them cleanly. + +The algebra decides the count; ambition doesn't. + +## Relations +- Suite index: [README.md](README.md) +- Profiles: P1 [signed-object](signed-object-profile.md) · P2 + [chain-store](chain-store-profile.md) · P3 + [enrollment](enrollment-protocol.md) · P4 + [identity-lifecycle](identity-lifecycle.md) · P5 + [rendering](rendering-security-profile.md) +- #220 verdict/policy TOML · #231 `passkey`→`attest` rename · GPT-5 PR #232 + formal kernel · agent-mesh#67 Conversation Graph · + `docs/decisions/floating_identity.md` (law 5 = L5; law 4 = the graph) diff --git a/docs/spec/chain-store-profile.md b/docs/spec/chain-store-profile.md new file mode 100644 index 0000000..5daaaa8 --- /dev/null +++ b/docs/spec/chain-store-profile.md @@ -0,0 +1,123 @@ +# P2 — Chain-Store Profile + +**Layer:** 1. **Depends on:** P1 (Signed-Object). +**Depended on by:** P0 (L2), P3, P4, P5. +**Status:** DRAFT. **Teeth:** a Lean trusted-state-machine +(`untrusted_step_safe`, checkpoint monotonicity) refined to Rust by Aeneas +(Tier 3) — this is the heart of GPT-5 #232's §8–9. +**Owns:** durable, tamper-evident, **rollback-resistant** history — and the +honest statement of what the hash chain does and does not buy. + +## 1. The store is a causal transcript, not a ledger + +Records are `MerkleNode` (P1). `parents` is a **set** — branches +(parallel agents) and merges (accepted synthesis) are first-class, so the +store is a **Merkle DAG**. "Extends" means *reachable-ancestor*; the +forward-only checks below are over this partial order, **not** an integer +index. This chain-store is the **authority projection** of the wider +Conversation Graph (agent-mesh#67) — same structure, payload `T` = +authority records here, conversation records there. Deliberately *not* a +blockchain: no mining, tokens, leader election, or global total order. + +## 2. Two CIDs per record + +``` +c_i = H(canon(record_i ∖ sig)) content-CID (what is signed) +s_i = Sign(k, c_i) the signature +line_i = record_i ∪ { sig: s_i } the full at-rest line +ℓ_i = H(canon(line_i)) line-CID (what parents reference) +parents(record_{i+1}) ∋ ℓ_i descendants commit to content AND sig +``` + +Parents reference the **line-CID** — the full predecessor *including its +signature* — so stripping or swapping a historical signature breaks the +chain as surely as editing content. + +**At rest:** JSONL as a lossless line-oriented view of the canonical +DAG-CBOR records — one `line_i` per line, human-auditable, no comment +affordances to make canonicalization ambiguous. CIDs/sigs are always over +the canonical form, never the view. + +## 3. What the chain buys — and what it does not + +**Interior integrity (PO-2a).** Editing or removing an *interior* record +orphans every descendant's parent link — it verify-fails loudly **against a +head the verifier already trusts.** This extends detection to +{add, delete, reorder} of the interior and retires the flat-file +known-limit (policy.rs #226). + +**What the chain alone does NOT do (finding #1).** Verification is always +*relative to a head*. An attacker who also controls the head can truncate +the tail, or present a wholly older / forked-but-internally-valid log; the +surviving prefix verifies and nothing is orphaned. This is the established +limit of every hash-chained log — Schneier-Kelsey (1999), FssAgg (eprint +2008/185), and the reason Certificate Transparency needs gossiped Signed +Tree Heads (RFC 6962). **Tail and fork integrity require §4's external +anchor, not the chain.** An anti-rollback claim resting on an in-chain +record is circular — it rolls back with the log it certifies. + +## 4. The anti-rollback anchor (external, load-bearing for L2·H1) + +Closing tail-and-fork requires state the attacker does not control. Three +canonical layers, ascending assurance: + +1. **Independently-protected monotonic head (REQUIRED).** Each participant + remembers — in storage *separate from the log* — the highest + `(generation, length, head-CID)` it has accepted, and MUST reject any + presented head that is not a consistent forward-extension of it (TUF: + *"clients MUST NOT replace metadata with a version less than the one + currently trusted"*; RFC 6962 monotonicity). Defeats truncation and + rollback for that participant. **Where it lives is normative** (§6): + device keystore / hardware monotonic counter (TPM) / witness quorum / + separately-protected checkpoint. *"On disk beside the log" does NOT + qualify.* +2. **Witness cosigning (RECOMMENDED for shared stores).** The head is + periodically countersigned by witnesses (the `AuditRecord`s of P4/P0, + **exported off-chain**). A participant accepts a head only with a witness + cosignature no older than its freshness policy (CT gossip / STH). Defeats + *secret* equivocation: to fool a victim the attacker must fork the + witnesses too. +3. **Fork = proof of misbehavior (REQUIRED).** Two validly-signed heads of + the same store at the same length with different CIDs are incontestable + evidence of equivocation (RFC 6962). Implementations MUST halt authority + minting from that store and escalate — never silently pick one. + +For a solo user (P3's n=2 world) the monotonic head lives on each enrolled +device and each device is the other's witness — the same k-of-n substrate +as revocation, reused. A quorum/witness set is the enterprise instance of +the identical mechanism. **Nothing here trusts the storage medium**; the +anchor is trusted state a participant carries into each verification, +exactly as `pinned` is. + +## 5. The trusted state machine (the Lean model) + +The kernel over this store is a state machine with `trustedHead: +Checkpoint` outside the store. Transitions: + +- **untrusted step** (anything an attacker with disk/network but no keys + can do): MUST NOT widen authority, add/remove trusted identities, or + rewrite the checkpoint — `untrusted_step_safe`. +- **sync/reload**: MAY advance the checkpoint only when the candidate + `Extends` the current one; MUST reject a strict ancestor or unrelated + fork. + +Theorems (Tier 3): accepted checkpoints are monotonic under `Extends`; a +strict ancestor is rejected; rollback detection is conditional on the +checkpoint staying outside the attacker-controlled store. + +## 6. Proof obligations + +| PO | Statement | Tier | +|---|---|---| +| PO-2a | interior deletion/replay rejected vs. a trusted head | 3 | +| PO-2c | tail truncation + fork rejected vs. the external anchor (not merely detected later) | 3 | +| — | (`untrusted_step_safe`; checkpoint monotonicity under `Extends`) | 3 | + +PO-2 (⊑-monotony under H1) and PO-2b (load-bearing set) live in P0/P4 but +*depend on* this profile discharging H1's rollback half. + +## Relations +- P1 (Signed-Object) — CIDs, canonical form, deterministic sig +- agent-mesh#67 — the Conversation Graph this projects from +- GPT-5 PR #232 §8–9 — the Lean trusted-state-machine formulation +- RFC 6962, TUF, Schneier-Kelsey, FssAgg (eprint 2008/185) — the canon diff --git a/docs/spec/enrollment-protocol.md b/docs/spec/enrollment-protocol.md new file mode 100644 index 0000000..289972e --- /dev/null +++ b/docs/spec/enrollment-protocol.md @@ -0,0 +1,104 @@ +# P3 — Enrollment Protocol + +**Layer:** 2. **Depends on:** P0 (L5), P1, P2. +**Status:** DRAFT. **Teeth:** this profile is a **protocol**, not an +algebra — its correctness is *symbolic protocol analysis* (Tamarin / +ProVerif under Dolev-Yao), **Tier 2**, not Lean/Aeneas. A flawless lattice +can sit behind a leaky handshake; this is where that leak is proven absent. +**Owns:** how a key first joins — introductions, SAS pairing, and the +blessing of external corroboration surfaces. + +## 1. Introduction — first contact is challenge-response + +Freshness comes from the **recipient**, never the introducer: a self-chosen +nonce in a self-signed object is byte-for-byte replayable. + +**Message 1 — the recipient issues a challenge it will remember:** +```json +{ "v": 1, "challenge": "…", "issued_by": "b3:…", + "for_generation": 41, "expires_at_generation": 42 } +``` +**Message 2 — the introducer answers, binding that challenge:** +```json +{ "v": 1, "fingerprint": "b3:…", "pubkey": "ed25519:…", + "channel": "mdns | dial-back | relay | manual | qr", + "proposed_caveats": [ … ], "observed": { "addr_candidates": [ … ] }, + "answers": "…", "transcript": "cid:…", "sig": "…" } +``` +On receipt of message 2, **before any surface renders it**: +1. `answers` MUST be a challenge *this recipient issued, unconsumed, + unexpired* — then mark it consumed. Replay-state lives with the + challenger (unknown-key-share closure; station-to-station). +2. the fingerprint's hash algorithm MUST be in the P1 allowlist **before + hashing**, then the fingerprint MUST equal that algorithm's multihash + name of `pubkey` (self-certification, checked by the library not the + human). +3. `sig` MUST verify under `pubkey` over message 2 incl. `answers` and + `transcript` — **proof of possession**, transcript-bound. + +## 2. SAS pairing (enrolling your own devices/surfaces) + +How a new device — possibly no compute beyond key storage and a screen — +gets its keypair pinned under a principal. A naive "new device shows a +phrase, trusted device sends it back" is MITM-relayable. The sound +construction (Bluetooth numeric comparison / ZRTP): + +1. both devices **commit** to nonces before revealing; +2. both **derive** the SAS from the *entire transcript* — commitments, + reveals, **and the long-term public keys being enrolled**. This last is + not optional: an SAS over only ephemeral material lets a MITM relay the + handshake while substituting the long-term keys (key-substitution). The + SAS must checksum *what is being pinned*, not the channel; +3. **a human compares the SAS on both screens** — a low-bandwidth + *authenticated* channel the network MITM is not on. It is **not + unspoofable**: it has a measurable error probability (the SAS entropy) + and human-factors assumptions (humans skim). The phrase is a checksum, + not a secret. + +Commit-before-reveal forces the MITM to *guess* the SAS in advance; +`xxx-000` ≈ 1-in-46k per round. Paranoia is a parameter: +``` +strength(enrollment) = (SAS entropy × rounds, distinct witnesses, presence) +``` +Minima are set **by caveat ceiling**: a broad ceiling demands more rounds, +≥ 2 previously-secured witnessing surfaces, and a hardware presence +discharge (`attest`). Witnesses buy more than rounds — rounds shrink the +guess probability; independent witnesses multiply the channels an attacker +must own *simultaneously*. The completed enrollment is a `PinRecord` (P4) +whose payload carries the ceremony parameters. + +**Punting ≥ pinning:** revocation (P4) is graded on the same scale — +removing an identity demands at least the strength that enrolled it. + +## 3. External anchors (corroboration, never the root) + +A principal root is **self-sovereign**. Externally published keys — GitHub +(`github.com/.keys`), DNS, an org CA, a prior device — are **candidate +corroboration channels**: independent witnesses that the root you pin +belongs to the human you think it does. No anchor is load-bearing (floating- +identity law 1): GitHub corroborates the root, never *is* it. A user with no +GitHub enrolls by ceremony alone with zero degradation. + +**Anchors are blessed, then participate.** Any public-key display surface +qualifies *once the key owner blesses it* — an `AnchorRecord` (principal- +signed binding of channel + location + displayed key) appended to the +chain-store. Unblessed anchors are ignored; a blessed one may *participate* +as a signing/corroboration surface (a GitHub-key signature counting as one +enrollment witness). Blessings revoke like any load-bearing identity (P4 +quorum) — a captured anchor is cut loose and `n` just shrank by one. + +## 4. Proof obligations (Tier 2 — symbolic) + +| PO | Statement | Tool | +|---|---|---| +| PO-E | enrollment has no MITM / unknown-key-share under Dolev-Yao | Tamarin/ProVerif | +| PO-F | challenge freshness: no self-issued or replayed nonce is accepted | Tamarin/ProVerif | +| PO-S | SAS guess advantage ≤ (entropy, rounds, witnesses) bound | analysis + vectors | + +The introduction/pin *outcome* records are P1/P4 objects and inherit their +Tier-3 obligations; the *handshake* is what this profile proves. + +## Relations +- P0 L5 (the ceremony gate this satisfies) · P1 (allowlist, PoP sig) · P2 + (the store the PinRecord lands in) · P4 (PinRecord, RevocationRecord, + AnchorRecord definitions) · agent-mesh#65/#66 (mesh-side structs) diff --git a/docs/spec/identity-lifecycle.md b/docs/spec/identity-lifecycle.md new file mode 100644 index 0000000..5856d2f --- /dev/null +++ b/docs/spec/identity-lifecycle.md @@ -0,0 +1,113 @@ +# P4 — Identity Lifecycle + +**Layer:** 2. **Depends on:** P0 (L2, L5), P2 (chain-store). +**Status:** DRAFT. **Teeth:** Lean (quorum k-of-n soundness; non-regression +of the load-bearing set, PO-2b) + a **liveness** obligation (recovery is +never permanently denied to a legitimate owner, PO-R). Tier 3. +**Owns:** roles and delegation, the durable identity records, quorum +revocation, and — required, not optional — break-glass and succession. + +## 1. Roles (one identity model, four roles) + +Every participant is a fingerprint; roles differ only in *what they sign*: + +| Role | Holds | Signs | +|---|---|---| +| **Principal** | root keypair (vault/hardware) | issuance of agent/surface certs; durable loosening entries | +| **Agent** | keypair chained to a principal | envelopes, requests, delegations | +| **Surface** | keypair chained to a principal — possibly nothing else (a no-compute device is keypair + renderer) | its `Decision`s and `AttestationRecord`s | +| **Gate** | the enforcement identity (usually = its agent) | chain-store appends | + +A monolith (newt carrying bridle + mesh in-process) is the **degenerate +case**: one fingerprint, all roles. Delegation is the roles splitting into +separate keypairs. In-process surfaces MAY omit signing decisions; **remote +surfaces MUST sign** (attributable provenance). + +**Delegation** (agent → agent): both endpoints present chains to a **common +pinned principal** (L5). The root key is involved at **issuance, not +per-delegation** — headless fleets verify chains offline while the root +stays in hardware; revocation is a generation bump. Cross-principal +delegation is two principals pinning each other's roots (L5, one level up). +Shipped as mesh `CertChain::verify` + PoP-to-certify (#39/#40). + +## 2. Durable records + +### PinRecord / GrantRecord +```json +{ "parents": ["cid:…"], // line-CIDs (P2); ⌀ only for genesis + "payload": { "v": 1, "fingerprint": "b3:…", "pubkey": "ed25519:…", + "channel": "qr", "caveats": [ … ], // the granted meet + "decision": { "grant": { "verb": "pin", "scope": "always" } }, + "presence": { "kind": "passkey", "discharge": "…" }, // §5 + "granted_at": "…" }, // provenance, not validity + "sig": "ed25519:…" } +``` +A pin is created only by a bound-surface `grant` or a pre-pinned policy +entry (a signed loosening entry, L2). + +### AuditRecord / AttestationRecord +An **AuditRecord** witnesses the chain head (`witnessed_head`, `fingerprint`, +`presence`); exported off-chain it becomes a P2 §4 anchor — in-chain alone +it anchors nothing. An **AttestationRecord** (P0 §3) is its authorization- +bearing sibling: one presence signature, two distinct fields — +**authorization** (`request_cid`, `decision_cid`) and **history witness** +(`observed_head`, `previous_witnessed_head`). Acceptance requires +`previous_witnessed_head` to be a reachable ancestor of `observed_head` +(else CHAIN HISTORY REGRESSION). Attest and audit stay distinct but +co-signed — *no authorization floats free of the history that gave it +meaning.* + +### RevocationRecord (quorum) +```json +{ "v": 1, + "payload": { "revoke": "b3:…", "reason": "compromise|rotation|retirement", + "succession": "b3:…", "policy": "cid:…" }, // CANONICAL UNSIGNED + "signers": [ { "by": "b3:aa…", "sig": "…" }, + { "by": "b3:cc…", "sig": "…" } ] } // sorted, deduped +``` +**One payload, many detached signatures** — every signer signs +`content-CID(payload)`, so all cover identical bytes (a signature nested in +the object it signs is circular). `signers` is sorted by `by` and +deduplicated (one identity can't count twice toward `k`); the signature set +is *not* signed; the chain-append `sig` (WF-2) is separate and last. +Acceptance: `|distinct valid signers ∩ eligible| ≥ k` under `policy`. The +quorum policy is itself a principal-signed loosening entry (defining *who +may revoke* is authority structure, L2). + +## 3. Break-glass & succession (REQUIRED) + +A quorum strong enough to defeat a hostile revocation can also lock out a +legitimate owner who loses `k` keys — availability cuts both ways. So +enrollment MUST provision recovery: + +- a **pre-enrolled recovery factor** (offline hardware key / printed share) + counted in `n`; +- a **succession path** transferring a principal root to a new key under + quorum, *or* **time-delayed unilateral recovery** — a self-revocation any + single device can start that only takes effect after a published + generation delay, giving co-signers a veto window ("social recovery with + timelock"). + +Revoking the **last** root without a `succession` ends the mesh — +implementations MUST refuse unless the record carries `"tombstone": true`. +This subsystem is required and sketched here; full specification is P4's +open work. **PO-R (liveness):** a legitimate owner is never *permanently* +locked out. + +## 4. Presence-attested pins +A pin MAY carry a `presence` discharge — a WebAuthn/passkey step-up bound to +the pin's `ContentId` (`DischargeVerifier`; PR #214). Upgrades first contact +from "someone clicked" to a hardware-attested human decision. Optional by +law, recommended for broad-ceiling pins. + +## 5. Proof obligations + +| PO | Statement | Tier | +|---|---|---| +| PO-2b | a sub-quorum coalition cannot shrink the load-bearing set | 3 | +| PO-5 | (shared with P0) delegation chain soundness; re-key ⇒ re-ceremony | 3 | +| PO-R | recovery is live — no permanent legitimate lock-out | 3 (liveness) | + +## Relations +- P0 (L2/L5) · P2 (the store) · P3 (enrollment produces PinRecords) · + mesh #39/#40 (CertChain + PoP) · PR #214 (presence) · newt-agent#1209 diff --git a/docs/spec/rendering-security-profile.md b/docs/spec/rendering-security-profile.md new file mode 100644 index 0000000..ebf8012 --- /dev/null +++ b/docs/spec/rendering-security-profile.md @@ -0,0 +1,76 @@ +# P5 — Rendering Security Profile + +**Layer:** 2. **Depends on:** P0 (the decision seam), P1. +**Status:** DRAFT. **Teeth:** Lean effect-CID soundness (Tier 3) for the +cryptographic half; a **stated human-factors residual** for the half that +is *not* cryptographic. Naming the boundary honestly is the whole job. +**Owns:** binding *what the human saw* to *what executes*, and the +obligations a rendering surface carries. + +## 1. Effect binding — what you see is what you sign + +`action` carries both a human `display` and an `effect` — the content-CID of +the **canonical, fully-resolved call** (arguments *and* resolved resources — +the `CallRequest` the tool layer produces): + +```json +"action": { "class": "exec", "display": "run_command: cd ", + "effect": "cid:…" } +``` + +The gate MUST, **before minting authority, recompute the canonical effect +from the call it is about to run and check it equals `action.effect`** — +otherwise a stale or lossy `display`→effect mapping approves X and executes +Y while the CID still matches (confused-deputy / TOCTOU; WYSIWYS, +Landrock-Pedersen 1998). Display and effect are bound under the one +signature. **PO-W (soundness):** an accepted decision's effect equals the +executed call. + +## 2. Gate-signed requests (the phishing-canvas closure) + +A **remote** surface MUST verify the request's signature and that `by` +chains to a pinned principal **before rendering** — an unauthenticated +prompt is a phishing canvas that trains the human on fake ceremonies, even +though its harvested decision is unredeemable (the `Decision.request` CID +binding, P0 §2.3). In-process, `by`/`sig` MAY be omitted. + +## 3. The render-swap closure + +`Decision.request` = the content-CID of the `PermissionRequest` as issued; +the gate accepts only a matching CID (P0 §2.3 step 1). This binds *what was +shown* to *what was granted* — an attacker cannot show request A and apply +the approval to request B. + +## 4. The residual — rendering faithfulness is NOT cryptographic + +Binding the signature to `effect` proves the decision is bound to the +*data*. It does **not** prove the UI faithfully showed that data to the +human. Truncation, locale, misleading formatting, hidden arguments, and path +abbreviation remain human-factors surfaces. This profile therefore: + +- **requires** `display` to be derived from `effect` by a profile-defined, + **deterministic rendering function** — so `display` is checkable against + `effect`, and a surface that cannot faithfully render `effect`'s meaning + MUST refuse rather than show a prettier `display`; +- treats a **signed render transcript** — the surface attesting the exact + bytes it presented — as the strengthening path (its own future work); +- states plainly that the last gap is **irreducible human factors**: + `attest` for high ceilings and distinct ceremony UI (consumer guidance, + newt#1209) mitigate but do not eliminate it. + +Named, not solved. This honesty is the point: a rendering profile that +*claimed* cryptographic faithfulness would be exactly the "prose becomes +authority-bearing protocol" failure the security reviews caught. + +## 5. Proof obligations + +| PO | Statement | Tier | +|---|---|---| +| PO-W | effect-CID soundness: accepted decision's effect = executed call | 3 | +| — | render-swap / phishing-canvas closures | 3 (with P0 gate checklist) | +| residual | display-faithfulness | *not cryptographic — stated, mitigated* | + +## Relations +- P0 §2.1/§2.3 (PermissionRequest, Decision, gate acceptance) · P1 + (canonical CIDs) · newt-agent#1209 (ceremony UI consumer) · + Landrock-Pedersen 1998 (WYSIWYS) diff --git a/docs/spec/signed-object-profile.md b/docs/spec/signed-object-profile.md new file mode 100644 index 0000000..281435e --- /dev/null +++ b/docs/spec/signed-object-profile.md @@ -0,0 +1,119 @@ +# P1 — Signed-Object Profile + +**Layer:** foundation (no dependencies). **Depends on:** —. +**Depended on by:** P0, P2, P3, P4, P5 (everything). +**Status:** DRAFT. **Teeth:** proptest round-trip vectors + a Lean +canonicalization-injectivity contract (Tier 3); the crypto primitives are +Tier-1 assumptions. +**Owns:** how any object gets a name, a canonical form, and a signature — +and how a verifier decides which algorithms it will honour. + +This profile is the alphabet the other five write in. It contains no +authority semantics; it makes objects *nameable, canonical, and +verifiable*. + +## 1. Identifiers are self-describing + +Fingerprints are **multihash**; keys and signatures are **multicodec**- +tagged; links are **CIDv1**. Comparison is over the opaque bytes *including +the algorithm code* — two hash algorithms never collide silently. The key +is the identity; the fingerprint `H(pubkey)` is its self-certifying *name*. +*BLAKE3 is an implementation detail, not a law.* + +## 2. One schema, three encodings + +JSON for interchange (client libs), TOML at rest (#220 policy files), +**canonical DAG-CBOR for anything hashed or signed.** Signatures and +`ContentId`s are computed over canonical bytes only. Wall-clock is never a +coordination primitive — validity keys on generation counters; RFC 3339 +timestamps are provenance *data*, never read by a kernel. + +## 3. The Memo discipline (WF-2) + +Every wire object is a `Memo`-descendant (content-addressable-python +`data.py`: every value carries its content-id and *reads verify it*). +Capabilities attach by **mechanical criteria**, not by quota: + +- **content-CID — unconditional.** Anything serializable and meaningful + beyond this process has canonical bytes, hence a name. +- **`by` + `sig` — at trust boundaries.** REQUIRED when the object crosses + to a different fingerprint (remote surface, delegated agent, another + host); MAY be omitted in-process — same trust domain, nothing to assert. +- **`parents` — for durability.** Anything appended to a chain-store (P2) + links. +- **Sealed at load.** Objects are constructed *only* through verification + (CID recomputed; sig checked when present) — verified once at the + boundary, immutable after. Nothing enters a kernel unverified. `Sealed` + is the Rust heir of Memo's read-time tamper check. + +Const-correctness for integrity: one unsigned hop breaks the chain of +custody like one non-const cast breaks the guarantee. Applies to **all of +the data layer, none of the resource layer.** + +## 4. Verification rules (the sharp edges) + +1. **Verify over received canonical bytes, never a re-serialization.** + Typed deserialization drops unknown fields; reserializing a parsed + object cannot reproduce the signed digest. Verify the bytes as received, + *then* parse (RFC 8785 pitfall; JWS/COSE practice). +2. **Unknown authority-bearing fields fail closed.** A field the profile + version does not define is *rejected*, not ignored — tolerating it is a + silent downgrade / version-confusion surface. Non-authority annotations + MAY be preserved verbatim only when the profile marks them non-critical + (COSE critical-header model). +3. **Version dispatch, not lenient parsing.** All objects carry + `"v": `; verification dispatches on it. +4. **Algorithm allowlist before dispatch (PO-8).** Self-describing + identifiers let the *object* declare its algorithm — so a verifier that + dispatches on the declared code alone lets an attacker pick a broken + hash (`alg:none` / algorithm-confusion). A verifier MUST check the + declared code against the locally-trusted profile table (§6) **before + hashing or verifying**, and reject anything outside it. Agility lives in + the profile, never on the wire. + +## 5. Deterministic signatures (why Ed25519 is pinned) + +For Profile v1, signing MUST be **deterministic**: `Sign(sk, message)` +yields one canonical signature encoding for a fixed key and message. +Ed25519 provides this (RFC 8032 derives the nonce from key and message). A +randomized scheme (ECDSA with a random nonce) would produce a different +signature — hence a different content hash — on every re-sign, forking any +chain built over it (P2). Determinism governs **reproducibility**; +collision resistance of `H` governs **tamper-evidence**; they are +independent properties. (Information-theoretically: `H(sig|content,key)=0`. +The prose rule is normative; the identity is just why.) + +## 6. Profile v1 (pins, not laws) + +Each pin states the *property* a replacement must carry: + +| Pin | v1 value | Required property | +|---|---|---| +| Content hash `H` | BLAKE3-256 (multihash `0x1e`) | collision resistance; preimage hardness (L5 self-certification) | +| Signature | Ed25519 (RFC 8032) | **deterministic**; existential unforgeability | +| Canonical encoding | DAG-CBOR (codec `0x71`) | injective canonical serialization (one value ⇒ one byte string) | +| Links | CIDv1 | multihash-native, codec-tagged | + +Rotating `H` is a **re-naming ceremony** (P0·L5): keys sign linkage records +binding their new names; identity never moves. Rotating the signature +scheme is a **re-keying** (full L5 re-ceremony) because the key *is* the +identity. Profile rotation is a negotiated, principal-signed change to the +allowlist (a loosening entry, P0·L2), never a per-object choice. + +## 7. Proof obligations + +| PO | Statement | Tier | +|---|---|---| +| PO-1c | canonicalization is injective; verify-over-received-bytes is sound | 3 (Lean contract) | +| PO-8 | algorithm dispatch is allowlist-gated — no code outside the profile is honoured | 3 | +| WF-2 | the Memo discipline holds structurally on every wire object | vector | + +The kernel (P0) consumes `H` and `Sign` as **abstract injective / one-way +contracts** — this profile's job is to satisfy them, the kernel's job to +assume them. That boundary is what lets Aeneas run without modelling crypto. + +## Relations +- `content-addressable` crate — `ContentId`, canonical DAG-CBOR, + `MerkleNode`, the `Sealed` to build +- agent-mesh#66 — multihash wire format for `Fingerprint` +- #226/#227 — signed loosening entries (shipped) From 9e7a2451e67a003c817765bc25f62d97fd490b13 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Wed, 15 Jul 2026 22:39:07 -0400 Subject: [PATCH 10/13] docs(spec): track review-6 obligations + fix the prose-cyclic dependency DAG (OB-1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh GPT pass on the partitioned suite confirmed the split closed the prior attacks (architecture/threat-honesty/separation/formal-plan all rated Strong) and surfaced ~11 crisp state-machine gaps. - Corrected the dependency DAG: the prose had P0↔P4 back-edges (P0's attestation + L5 lean on P4; P3/P5 consume P4 records). Fix = dependency inversion: P0 depends on abstract AttestEvidence / ValidAssociationProof contracts that P4 implements. Real order: P1→P2→P0→P4→P3; P5 on {P0,P1,P4}. - Added an 'Open specification obligations' table (OB-1..OB-11, severity- tagged, resolution direction, fix-vs-author-choice) so the next pass is a checklist review, not a re-derivation. Sharpest: OB-9 (resolve(∅)=⊤=approve is fail-OPEN, contradicting L3) and OB-1 (the cyclic prose). OB-2 (linear authority spine vs frontier checkpoint) and the attest factorization stay author's calls. No law or profile-content change yet — this captures the review; the semantic repairs (abstract contracts, canonical-byte envelope, attestation transaction, epoch'd policy) are the next pass. Merge/implement still held. Refs #229 review round 6. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/README.md | 81 +++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/docs/spec/README.md b/docs/spec/README.md index 83037fc..2302968 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -31,33 +31,36 @@ its dependencies are Proven** — that is the "chain of decisions" made real. ## The dependency DAG +Corrected 2026-07-16 per an adversarial review (OB-1): the prose had +back-edges the first diagram hid — P0's attestation and L5 lean on P4, and +P3/P5 consume P4 records. The fix is **dependency inversion**: P0 depends +only on *abstract contracts* (`AttestEvidence`, `ValidAssociationProof`) +that **P4 implements** — so the linear order below is real, not aspirational. + ``` - ┌─────────────────────────┐ - │ P1 Signed-Object │ foundation: content-addressing, - │ (CID · canon · Sealed) │ canonicalization, signatures, allowlist - └────────────┬────────────┘ - │ - ┌────────────▼────────────┐ - │ P2 Chain-Store │ causal transcript (Merkle DAG), - │ (DAG · anchor · rollbk) │ anti-rollback anchor - └────────────┬────────────┘ - │ - ┌────────────▼────────────┐ - │ P0 Ceremony Contract │ ◄── THE NARROW WAIST - │ five laws · lattice · │ authority algebra + decision seam - │ decision surface · gate │ + gate acceptance - └───┬─────────┬────────┬───┘ - │ │ │ - ┌────────▼───┐ ┌───▼──────┐ ┌▼─────────────────┐ - │ P3 Enroll- │ │ P4 Ident-│ │ P5 Rendering │ - │ ment │ │ ity Life-│ │ Security │ - │ (SAS·PoP) │ │ cycle │ │ (WYSIWYS·transc.)│ - └────────────┘ └──────────┘ └──────────────────┘ - - External fabric: agent-mesh#67 Conversation Graph — the wider causal - transcript of which P2's chain-store is the *authority projection*. + P1 Signed-Object (foundation: CID · canon · Sealed · allowlist) + │ + ▼ + P2 Chain-Store (causal transcript DAG · external anti-rollback anchor) + │ + ▼ + P0 Ceremony Contract ◄── THE NARROW WAIST + │ five laws · lattice · seam · gate; + │ depends on P4 only via abstract contracts + ▼ + P4 Identity Lifecycle (roles · records · quorum revocation · recovery; + │ implements P0's AttestEvidence / association proof) + ▼ + P3 Enrollment (SAS · PoP; produces P4 PinRecords) + + P5 Rendering depends on {P0, P1, P4} (effect binding · gate-signed requests) + + External fabric: agent-mesh#67 Conversation Graph — the wider causal + transcript of which P2's chain-store is the *authority projection*. ``` +Build/prove order: **P1 → P2 → P0 → P4 → P3**, with P5 after P0/P1/P4. + ## The profiles | # | Profile | Owns | Depends on | Primary teeth | @@ -109,7 +112,8 @@ job to satisfy, the kernel's job to assume). This is what lets Aeneas run. ## Build order (what to prove first) -The DAG dictates it: **P1 → P2 → P0 → {P3, P4, P5}.** The provable MVP is +The DAG dictates it: **P1 → P2 → P0 → P4 → P3**, P5 after P0/P1/P4 (OB-1). +The provable MVP is **P1 + P2 + P0** — the Signed-Object foundation, the anti-rollback store, and the authority kernel with its five laws. That triple is GPT-5's #232 "formal ceremony kernel" almost exactly; adopt it as the P0/P1/P2 slice and @@ -140,6 +144,33 @@ may be Accepted against an unproven waist. The waist stays **five laws**; the ledger just relocates each obligation to the profile that owns it. +## Open specification obligations + +Tracked from adversarial review round 6 (a fresh GPT pass on the partitioned +suite, 2026-07-16). The review confirmed the split closed the prior attacks +and rated architecture / threat-honesty / separation / formal-plan all +*Strong*; these are the crisp state-machine gaps that remain. **B = blocker +before implementation-complete; H = high; M = medium.** "choice" = a design +decision the author owns, not a mechanical fix. + +| # | Sev | Profile | Gap | Resolution direction | +|---|---|---|---|---| +| OB-1 | B | P0/P4 | declared DAG had back-edges (P0↔P4 cycle) | **dependency inversion** — P0 defines abstract `AttestEvidence` + `ValidAssociationProof`; P4 implements. DAG corrected above. | +| OB-2 | B (choice) | P2 | branching DAG checkpointed with a scalar `(gen,length,head)`; "two heads @ equal length = equivocation" is false for a DAG | **linear authority spine per causal thread** `(store_id, thread_id, sequence)`, one accepted successor per sequence (conversation branches; authority is a railway). Alt: frontier checkpoint `Set`. | +| OB-3 | B | P0/P4 | attestation lacks a canonical challenge preimage + an atomic post-append commit; authenticator ≠ DAG verifier | define one challenge preimage (all bound fields); order: verify-checkpoint → presence → construct → append → **CAS-advance anchor** → then mint. Split roles: witness-verifier (DAG) / WebAuthn authenticator (presence) / surface (signs) / gate (appends+advances+activates). | +| OB-4 | B | P1 | "sign DAG-CBOR + verify received bytes + exchange JSON" is contradictory | **signed-bytes-in-envelope**: `{profile, codec, body: b64(canonical), cid, by, sig}`; JSON/TOML are views, never authority-bearing serializations. | +| OB-9 | B | P0 | `resolve(∅,q) = ⨅(∅) = ⊤ = approve` — **fail-OPEN**, contradicting L3 | seed the meet with a base: `resolve(R,q) = ⨅({ask} ∪ {matching verdicts})`; empty match → ask → (headless) deny. Define `resolve(∅)` for PO-3 totality. + normalize vocabulary (allow vs approve; is `ask` verdict/state/escalation?). | +| OB-5 | H | P3 | challenge consumed *before* signature check → burn-DoS | validate-then-consume (reserve→validate→commit); bind the challenge object CID + recipient + role + context. | +| OB-6 | H | P1/all | domain separation only on the WebAuthn challenge | **universal**: every signature covers (record-type string, `store_id`, thread/principal id, canonical payload). P2 needs a normative, cryptographically-bound `store_id`. | +| OB-7 | H | P4 | RevocationRecord's `policy` CID not pinned to an epoch → replay an older weaker policy | `policy_cid == ActiveRevocationPolicy(target, observed_checkpoint, generation)`. The strength tuple is not totally ordered → enrollment records the **exact required revocation predicate**, not an informal tuple. | +| OB-8 | H | P4 | PO-R "never permanently locked out" is unprovable unconditionally | state it **conditionally** (≥1 uncompromised recovery threshold, eventual quorum comms, fair generation advance, no total material destruction). Timelock recovery must handle attacker-owns-a-device-suppresses-veto. | +| OB-10 | M | P5 | effect-CID binds a call *value*, not the mutable world (symlinks, DNS, container **tags**, repo state, creds) | per-class resource identity in the sealed request (file: path+content-CID/inode; container: image **digest** not tag; repo: commit/tree CID; net: destination+DNS policy). Gate MUST run the exact `Sealed` whose CID was approved; ambient = named residual, not PO-W. | +| OB-11 | M | suite | no statement of which profile versions form one compatible suite | a **compatibility manifest**: `suite_version` + per-profile version requires + `conformance_vectors` CID. | + +Two of these are the review's sharpest: **OB-9** (a latent fail-open in a +fail-closed system) and **OB-1** (the clean DAG was prose-cyclic). OB-2 and +the `attest` factorization remain author's calls. + ## The rule underneath the whole suite > No authorization or claim floats free of the exact history and artifacts From 65bc1ecfb5c181ecf98b37024411129e57178227 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Thu, 16 Jul 2026 07:45:38 -0400 Subject: [PATCH 11/13] =?UTF-8?q?docs(spec):=20v0.3.0=20repair=20cut=20?= =?UTF-8?q?=E2=80=94=20close=20all=2011=20review-6=20obligations=20+=20sur?= =?UTF-8?q?face=20attestation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The next cut Shawn asked for: turn the tracked OB-1..OB-11 into actual spec repairs across the suite. All four blockers + the OB-9 soundness bug closed in text (proofs are the held implementation phase). Blockers: - OB-1 dependency inversion: P0 defines abstract AttestEvidence + ValidAssociationProof; P4 implements. DAG is now truly acyclic (P1→P2→P0→P4→P3; P5 on {P0,P1,P4}). - OB-2 (author's-call, adopted): linear authority spine per causal thread (store_id, thread_id, sequence) — conversation branches, authority is a railway; equivocation = two records at one (store,thread,sequence). - OB-3 attestation transaction: one canonical challenge preimage; CAS commit (verify-checkpoint→presence→construct→append→advance-anchor→mint); four separate roles (the WebAuthn authenticator is NOT the DAG verifier). - OB-4 byte carriage: signed-bytes-in-envelope {profile,codec,body,cid,by, sig}; JSON/TOML are views, never authority-bearing serializations. - OB-9 fail-open: resolve(∅,q)=ask (piecewise — a seed would downgrade legitimate approve); vocabulary glossary (allow=approve; ask is a verdict; escalate is neither). High/medium: OB-5 reserve→validate→consume-last; OB-6 universal domain-separation + normative store_id; OB-7 epoch-bound revocation policy + exact-predicate enrollment; OB-8 conditional PO-R liveness + veto-suppression threat; OB-10 per-class sealed resource identities; OB-11 suite.toml manifest. Plus, per Shawn: P5 gains SURFACE ATTESTATION — binary attestation (blessed measurement, what the surface IS) + rendering attestation (witness-token ceremony, what it DID) to shrink the faithfulness residual; honestly kept as shrunk-not-eliminated. Still five laws. Refs #229 review round 6, GPT-5 #232. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/README.md | 71 ++++++++---- docs/spec/ceremony-contract.md | 144 +++++++++++++++++------- docs/spec/chain-store-profile.md | 77 ++++++++++--- docs/spec/enrollment-protocol.md | 21 +++- docs/spec/identity-lifecycle.md | 73 +++++++++--- docs/spec/rendering-security-profile.md | 128 +++++++++++++++------ docs/spec/signed-object-profile.md | 54 +++++++-- docs/spec/suite.toml | 18 +++ 8 files changed, 446 insertions(+), 140 deletions(-) create mode 100644 docs/spec/suite.toml diff --git a/docs/spec/README.md b/docs/spec/README.md index 2302968..2c80e2e 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -144,32 +144,55 @@ may be Accepted against an unproven waist. The waist stays **five laws**; the ledger just relocates each obligation to the profile that owns it. -## Open specification obligations +## Specification obligations — status -Tracked from adversarial review round 6 (a fresh GPT pass on the partitioned -suite, 2026-07-16). The review confirmed the split closed the prior attacks -and rated architecture / threat-honesty / separation / formal-plan all -*Strong*; these are the crisp state-machine gaps that remain. **B = blocker -before implementation-complete; H = high; M = medium.** "choice" = a design -decision the author owns, not a mechanical fix. +From adversarial review round 6 (a fresh GPT pass, 2026-07-16), which +confirmed the split closed the prior attacks and rated architecture / +threat-honesty / separation / formal-plan all *Strong*. **All eleven are +addressed in the v0.3.0 repair cut** below (spec text changed; the *proofs* +that discharge them are the implementation phase, still held). **B/H/M** = +original severity. -| # | Sev | Profile | Gap | Resolution direction | -|---|---|---|---|---| -| OB-1 | B | P0/P4 | declared DAG had back-edges (P0↔P4 cycle) | **dependency inversion** — P0 defines abstract `AttestEvidence` + `ValidAssociationProof`; P4 implements. DAG corrected above. | -| OB-2 | B (choice) | P2 | branching DAG checkpointed with a scalar `(gen,length,head)`; "two heads @ equal length = equivocation" is false for a DAG | **linear authority spine per causal thread** `(store_id, thread_id, sequence)`, one accepted successor per sequence (conversation branches; authority is a railway). Alt: frontier checkpoint `Set`. | -| OB-3 | B | P0/P4 | attestation lacks a canonical challenge preimage + an atomic post-append commit; authenticator ≠ DAG verifier | define one challenge preimage (all bound fields); order: verify-checkpoint → presence → construct → append → **CAS-advance anchor** → then mint. Split roles: witness-verifier (DAG) / WebAuthn authenticator (presence) / surface (signs) / gate (appends+advances+activates). | -| OB-4 | B | P1 | "sign DAG-CBOR + verify received bytes + exchange JSON" is contradictory | **signed-bytes-in-envelope**: `{profile, codec, body: b64(canonical), cid, by, sig}`; JSON/TOML are views, never authority-bearing serializations. | -| OB-9 | B | P0 | `resolve(∅,q) = ⨅(∅) = ⊤ = approve` — **fail-OPEN**, contradicting L3 | seed the meet with a base: `resolve(R,q) = ⨅({ask} ∪ {matching verdicts})`; empty match → ask → (headless) deny. Define `resolve(∅)` for PO-3 totality. + normalize vocabulary (allow vs approve; is `ask` verdict/state/escalation?). | -| OB-5 | H | P3 | challenge consumed *before* signature check → burn-DoS | validate-then-consume (reserve→validate→commit); bind the challenge object CID + recipient + role + context. | -| OB-6 | H | P1/all | domain separation only on the WebAuthn challenge | **universal**: every signature covers (record-type string, `store_id`, thread/principal id, canonical payload). P2 needs a normative, cryptographically-bound `store_id`. | -| OB-7 | H | P4 | RevocationRecord's `policy` CID not pinned to an epoch → replay an older weaker policy | `policy_cid == ActiveRevocationPolicy(target, observed_checkpoint, generation)`. The strength tuple is not totally ordered → enrollment records the **exact required revocation predicate**, not an informal tuple. | -| OB-8 | H | P4 | PO-R "never permanently locked out" is unprovable unconditionally | state it **conditionally** (≥1 uncompromised recovery threshold, eventual quorum comms, fair generation advance, no total material destruction). Timelock recovery must handle attacker-owns-a-device-suppresses-veto. | -| OB-10 | M | P5 | effect-CID binds a call *value*, not the mutable world (symlinks, DNS, container **tags**, repo state, creds) | per-class resource identity in the sealed request (file: path+content-CID/inode; container: image **digest** not tag; repo: commit/tree CID; net: destination+DNS policy). Gate MUST run the exact `Sealed` whose CID was approved; ambient = named residual, not PO-W. | -| OB-11 | M | suite | no statement of which profile versions form one compatible suite | a **compatibility manifest**: `suite_version` + per-profile version requires + `conformance_vectors` CID. | - -Two of these are the review's sharpest: **OB-9** (a latent fail-open in a -fail-closed system) and **OB-1** (the clean DAG was prose-cyclic). OB-2 and -the `attest` factorization remain author's calls. +| # | Sev | Status | Where fixed | +|---|---|---|---| +| OB-1 | B | ✅ resolved | dependency inversion — P0 §3/L5 define abstract `AttestEvidence` + `ValidAssociationProof`; P4 implements. DAG corrected above (P1→P2→P0→P4→P3). | +| OB-2 | B (choice) | ✅ adopted¹ | **linear authority spine per causal thread** `(store_id, thread_id, sequence)` — P2 §1. Conversation branches; authority is a railway. (Frontier alt rejected as harder to prove.) | +| OB-3 | B | ✅ resolved | P0 §3 — one canonical challenge preimage; CAS-committed transaction (verify→presence→construct→append→advance-anchor→mint); four separate roles (authenticator ≠ DAG verifier). | +| OB-4 | B | ✅ resolved | P1 §2 — signed-bytes-in-envelope `{profile, codec, body, cid, by, sig}`; JSON/TOML are views. | +| OB-9 | B | ✅ resolved | P0 L1 — `resolve(∅,q) = ask` (piecewise, not a seed — a seed would downgrade legitimate `approve`); vocabulary glossary P0 §2.4. | +| OB-5 | H | ✅ resolved | P3 §1 — reserve→validate→**consume-last**; bind challenge object CID + recipient + role + context. | +| OB-6 | H | ✅ resolved | P1 §4·5 — universal domain-separation tuple on every signed `body`; normative `store_id` (P2 §1). | +| OB-7 | H | ✅ resolved | P4 §2 — `policy_cid == ActiveRevocationPolicy(target, observed_checkpoint, generation)`; enrollment records the exact required revocation predicate (tuple isn't totally ordered). | +| OB-8 | H | ✅ resolved | P4 §3 — PO-R stated **conditionally**; timelock-recovery veto-suppression threat answered (multi-channel ack, fail-closed on silence). | +| OB-10 | M | ✅ resolved | P5 §1 — per-class sealed resource identities (container **digest** not tag, repo commit/tree CID, …); ambient = named residual. | +| OB-11 | M | ✅ resolved | `suite.toml` compatibility manifest (below). | + +¹ OB-2 and the `attest` factorization were the two author's-calls; the +linear spine is *adopted pending your veto* (it's the reviewer's +recommendation and the provable option). The factorization stays open — the +lattice works either way (GPT-5 #232 already leans product-lattice). + +The review's own verdict was "approve the split, request-changes on the four +blockers, list the rest." All four blockers (OB-1/2/3/4) plus the OB-9 +soundness bug are now closed in text. + +## Suite compatibility manifest (OB-11) + +`suite.toml` names which profile versions form one compatible suite — one +atomic target for implementers, independent profile evolution preserved: + +```toml +suite = "ceremony-suite" +suite_version = "0.3.0" +[requires] +p0 = "0.2" # ceremony-contract (waist) +p1 = "1" # signed-object +p2 = "0.2" # chain-store +p3 = "0.2" # enrollment +p4 = "0.2" # identity-lifecycle +p5 = "0.2" # rendering-security +conformance_vectors = "cid:…" # populated with the kernel +``` ## The rule underneath the whole suite diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index a293a13..6fe973e 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,12 +1,15 @@ # P0 — The Ceremony Contract (the narrow waist) -**Status:** DRAFT 0.2.0 (2026-07-16) — partitioned from the v0.1.x -monolith into the [Ceremony Suite](README.md). This document is now the -**waist**: the five laws, the authority algebra, and the decision seam. -Everything mechanical it used to contain moved to a profile (P1–P5); this -doc *references* them and states the invariants they must satisfy. -**Depends on:** P1 (Signed-Object), P2 (Chain-Store). -**Depended on by:** P3, P4, P5. +**Status:** DRAFT 0.2.1 (2026-07-16) — partitioned from the v0.1.x +monolith into the [Ceremony Suite](README.md), then repaired against review +round 6 (all eleven obligations closed in text — see README). This document +is the **waist**: the five laws, the authority algebra, and the decision +seam. Everything mechanical moved to a profile (P1–P5); this doc *references* +them and states the invariants they must satisfy. +**Depends on:** P1 (Signed-Object), P2 (Chain-Store); P4 only via the +abstract contracts it defines (`AttestEvidence`, `ValidAssociationProof`) — +no back-edge (OB-1). +**Depended on by:** P4, P3, P5. **Teeth:** Lean proof of the lattice laws + resolution, refined to the pure Rust kernel by Aeneas (Tier 3). See the review history and adjudications on [#229](https://github.com/Gilamonster-Foundation/agent-bridle/pull/229); @@ -103,28 +106,78 @@ trusted:** A surface violating any is refused at the gate — the wire *enforces* L4, it does not merely state it. +### 2.4 Vocabulary (one word, one job — OB-9) + +Three registers were drifting; they are pinned here: + +| Term | Register | Meaning | +|---|---|---| +| `allow` / `attest` / `deny` | **matrix verb** — what a surface *offers* the human | `allow` maps to verdict `approve`; `attest` to `attest`; `deny` to `deny` | +| `deny ⊏ attest ⊏ ask ⊏ approve` | **verdict** — resolution's codomain | the durable disposition `resolve` yields | +| `ask` | **verdict only** (never a matrix verb) | "no durable rule → interact"; the `resolve(∅)` default (L1); fail-closed via L3 | +| `escalate` | **Decision action** (never a verdict) | navigation to a richer surface; `authority(escalate) = ⊥` | + +So `allow` (offer) and `approve` (verdict) are the same disposition seen +from the two sides of the seam; `ask` is a verdict, not an offer; `escalate` +is neither. + ## 3. Attest discharge + the forward-only ratchet -An `attest` grant is **inert until a presence proof is verified** (WebAuthn/ -FIDO2 challenge-response via the shipped `step_up::DischargeVerifier`): the -gate issues a domain-separated, single-use `Challenge` bound to the request -CID, subject, and generation; the authenticator returns a -`DischargeAttempt`; the gate verifies and consumes it **before** the grant -takes effect. - -**A presence proof also witnesses a non-regressing history.** The challenge -commits to a **checkpoint**; the signer refuses unless the store it is -shown *extends* the store it last witnessed. This yields an -`AttestationRecord` (P4) carrying two distinct statements under one -signature — **authorization** ("I approved R") and **history witness** ("I -verified `observed_head` descends from `previous_witnessed_head`"). The -signer's `previous_witnessed_head` MUST live in its **P2 §4 anti-rollback -anchor**, not the store it validates. Rollback *or* fork → `CHAIN HISTORY -REGRESSION` → halt + escalate. Checked **per causal thread**, so concurrent -threads never false-trip. Generation (total order) and DAG ancestry -(partial order) must *both* advance. Payoff: **every ordinary approval is a -free freshness checkpoint.** Not a new law — L2·H1's anchor at ceremony -time. +An `attest` grant is **inert until a presence proof is verified**, and the +proof *also witnesses a non-regressing history*: the same finger-press +attests "I approved R" **and** "the world had not regressed when I did." + +**Four roles, deliberately separate (OB-3).** The WebAuthn authenticator +does not understand a Merkle DAG — it signs over a client-data hash. So do +not say "the hardware witnessed the chain." The roles are: + +| Role | Does | +|---|---| +| **Witness-verifier** | holds the P2 §4 protected checkpoint; verifies DAG extension; **constructs the challenge preimage** | +| **Authenticator** (WebAuthn/FIDO2) | proves user presence/verification over that challenge — nothing more | +| **Surface identity** | signs the resulting `AttestEvidence` record | +| **Gate** | appends, advances the anchor, then activates authority | + +**One canonical challenge preimage** — every field the attestation binds, +domain-separated (P1 §), so nothing is left to prose: +``` +challenge = H("agent-bridle/attest/v1", + store_id, thread_id, request_cid, decision_cid, + previous_checkpoint, observed_checkpoint, generation, nonce) +``` + +**The commit is transactional (compare-and-swap, OB-3).** A crash or +rollback must never leave "authority active but attestation not durable" or +"attestation appended but anchor not advanced": +``` +1. witness-verifier: observed_checkpoint Extends protected_checkpoint (else HALT) +2. authenticator: presence proof over the canonical challenge +3. surface: construct AttestEvidence +4. append it → post_attestation_head +5. CAS-advance the protected anchor: protected := post_attestation_head +6. only now mint/activate authority +``` +Steps 4–6 are one atomic transition (CAS on the anchor); a recoverable +two-phase form is permitted. Rollback *or* fork at step 1 → `CHAIN HISTORY +REGRESSION` → halt + escalate (a fork is P2 proof-of-misbehavior, never a +branch to silently adopt). Checked **per causal thread** (P2 `thread_id`), +so concurrent threads never false-trip; generation (total order) and DAG +ancestry (partial order) must *both* advance. + +**P0 depends only on an abstract evidence contract (OB-1)** — not on P4's +concrete record — so the waist has no back-edge into P4: +```rust +trait AttestEvidence { + fn request(&self) -> ContentId; + fn decision(&self) -> ContentId; + fn previous_checkpoint(&self) -> Checkpoint; + fn observed_checkpoint(&self) -> Checkpoint; + fn presence_proof(&self) -> PresenceProof; +} +``` +P4 supplies `AttestationRecord` as the concrete implementation. Payoff of +the whole ratchet: **every ordinary approval is a free freshness +checkpoint.** Not a new law — L2·H1's anchor applied at ceremony time. ## 4. The Laws (normative — the whole waist) @@ -132,13 +185,24 @@ Five laws. Nothing enters this section without a proof obligation (§5); everything else is mechanism (a profile) or well-formedness. ### L1 — Resolution is a meet -Verdicts are ordered by restrictiveness (`deny ⊏ attest ⊏ ask ⊏ approve`). -`resolve(R, q) = ⨅ { verdict(r) | r ∈ R, r matches q }`. ⨅ is associative, -commutative, idempotent ⇒ resolution is independent of rule/file/load order. -No ordering attack exists. **PO-1.** *(The `attest`-factorization fork — -one verb axis vs. `effect × assurance × scope` — is a lattice-shape choice; -L1 survives either, since a product of lattices is a lattice. Author's call; -see README.)* +Verdicts are ordered by restrictiveness (`deny ⊏ attest ⊏ ask ⊏ approve`); +`deny` is ⊥, `approve` is ⊤. Resolution is the meet of the matching +verdicts, **with the no-match case defined explicitly** so it is total *and* +fail-closed (OB-9): +``` +resolve(R, q) = ⨅ { verdict(r) | r ∈ R, r matches q } if some rule matches q + = ask if none matches q +``` +The explicit `ask` default is load-bearing: the empty meet's mathematical +identity is `⊤ = approve`, so *without this clause an unmatched request +would fail OPEN* — the defect L3 forbids. `ask` (→ prompt; headless → deny, +L3) is the deliberate "unknown ⇒ interactive, never granted" default. ⨅ is +associative, commutative, idempotent ⇒ resolution is independent of +rule/file/load order; no ordering attack exists. **PO-1** (now includes +`resolve(∅,q) = ask`, i.e. no fail-open). *(The `attest`-factorization fork +— one verb axis vs. `effect × assurance × scope` — is a lattice-shape +choice; L1 survives either, since a product of lattices is a lattice. +Author's call; see README.)* ### L2 — Tamper-boundedness For any mutation `m` by a party holding **fewer than quorum(target)** keys: @@ -173,13 +237,15 @@ bottom is `ask`; absent a bound surface, `ask ↦ deny`, `attest ↦ deny` `association(peer) ⇒ pinned(fingerprint(peer))`. `fingerprint = H(pubkey)` is self-certifying (P1) — re-key ⇒ new fingerprint ⇒ unpinned ⇒ full re-ceremony; no silent identity swap is expressible. Pinned is **transitive -through certification**: +through certification** — but the waist states this over an **abstract +predicate** (OB-1), not P4's concrete cert-chain, so L5 has no back-edge: ``` -pinned(fp) ⇔ fp ∈ PinSet ∨ ∃ chain: fp →* root ∈ PinSet, PoP at every link +pinned(fp) ⇔ fp ∈ PinSet ∨ ValidAssociationProof(fp, PinSet) ``` -so pinning a principal admits the agents/surfaces it issues (delegation, P4; -shipped as mesh `CertChain::verify` + PoP, #39/#40). **PO-5** (incl. chain -soundness). +`ValidAssociationProof` is an abstract contract (there exists a +PoP-at-every-link chain `fp →* root ∈ PinSet`); **P4 proves its cert-chains +implement it** (shipped as mesh `CertChain::verify` + PoP, #39/#40). **PO-5** +(incl. chain soundness) is proved by P4 against this predicate. ## 5. Proof-obligation ledger (owned here; full suite in README) diff --git a/docs/spec/chain-store-profile.md b/docs/spec/chain-store-profile.md index 5daaaa8..9bfe94f 100644 --- a/docs/spec/chain-store-profile.md +++ b/docs/spec/chain-store-profile.md @@ -8,16 +8,58 @@ **Owns:** durable, tamper-evident, **rollback-resistant** history — and the honest statement of what the hash chain does and does not buy. -## 1. The store is a causal transcript, not a ledger +## 1. Conversation is a jungle; authority is a railway (OB-2) + +Records are `MerkleNode` (P1). The wider **Conversation Graph** +(agent-mesh#67) is a genuine Merkle DAG — branches (parallel agents) and +merges (accepted synthesis) are first-class. But a branching DAG **cannot** +be checkpointed by a scalar `(length, head)`: two heads at equal depth can +be *legitimate* concurrent descendants, so "different heads at the same +length ⇒ equivocation" is false for a DAG. + +The resolution: **the authority projection is a linear spine per causal +thread.** The conversation may branch and merge freely; branches must +**merge before producing the next authority record**, so the sequence of +authority records along one thread is totally ordered. + +```rust +struct AuthorityCheckpoint { // the protected anchor state (P2 §4) + store_id: StoreId, // normative, cryptographically bound (OB-6) + thread_id: ThreadId, // one causal authority thread + sequence: u64, // dense per (store_id, thread_id) + head: LineCid, +} +``` -Records are `MerkleNode` (P1). `parents` is a **set** — branches -(parallel agents) and merges (accepted synthesis) are first-class, so the -store is a **Merkle DAG**. "Extends" means *reachable-ancestor*; the -forward-only checks below are over this partial order, **not** an integer -index. This chain-store is the **authority projection** of the wider -Conversation Graph (agent-mesh#67) — same structure, payload `T` = -authority records here, conversation records there. Deliberately *not* a -blockchain: no mining, tokens, leader election, or global total order. +Normative definitions: +- **`store_id`** — a content-addressed identifier of the store's genesis + (its root record's CID); every signed record's domain tuple binds it + (P1 §4·5), so a record cannot be replayed into a different store. +- **`thread_id`** — the identifier of one authority thread within a store; a + causal thread is the totally-ordered sequence of authority records a + single principal-scope advances. A thread MAY be forked (declared) and + merged (a record naming multiple thread parents), but **within a thread, + `sequence n` has exactly one accepted successor at `n+1`.** +- **`length`/`sequence`** — the dense per-thread record count, *not* + longest-path depth. + +**Equivocation, now well-defined:** two validly-signed records for the same +`(store_id, thread_id, sequence)` with different CIDs are incontestable +proof of misbehavior (§4). This is the branching-safe restatement of the CT +fork rule. + +"Extends" for the ratchet means: the presented head's thread reaches the +protected `(thread_id, sequence, head)` as a strict-ancestor prefix — a +*forward extension of the same spine*, never a sibling. This chain-store is +the authority projection of the Conversation Graph — same `MerkleNode` +structure, payload `T` = authority records here, conversation records there. +Deliberately *not* a blockchain: no mining, tokens, leader election, or +global total order. + +*(Alternative considered and rejected as harder to reason about: a +frontier-based checkpoint `{store_id, frontier: Set}`. The linear +spine is chosen for provability; revisit only if genuine concurrent +authority within one thread becomes a requirement.)* ## 2. Two CIDs per record @@ -63,8 +105,10 @@ canonical layers, ascending assurance: 1. **Independently-protected monotonic head (REQUIRED).** Each participant remembers — in storage *separate from the log* — the highest - `(generation, length, head-CID)` it has accepted, and MUST reject any - presented head that is not a consistent forward-extension of it (TUF: + `AuthorityCheckpoint` `(store_id, thread_id, sequence, head)` it has + accepted per thread (§1), and MUST reject any presented head that is not + a forward extension of the *same spine* — never a lower `sequence`, a + different store, or a sibling (TUF: *"clients MUST NOT replace metadata with a version less than the one currently trusted"*; RFC 6962 monotonicity). Defeats truncation and rollback for that participant. **Where it lives is normative** (§6): @@ -77,10 +121,13 @@ canonical layers, ascending assurance: cosignature no older than its freshness policy (CT gossip / STH). Defeats *secret* equivocation: to fool a victim the attacker must fork the witnesses too. -3. **Fork = proof of misbehavior (REQUIRED).** Two validly-signed heads of - the same store at the same length with different CIDs are incontestable - evidence of equivocation (RFC 6962). Implementations MUST halt authority - minting from that store and escalate — never silently pick one. +3. **Fork = proof of misbehavior (REQUIRED).** Two validly-signed records + for the same `(store_id, thread_id, sequence)` with different CIDs are + incontestable evidence of equivocation (§1; the branching-safe restatement + of RFC 6962). Implementations MUST halt authority minting from that store + and escalate — never silently pick one. (Legitimate concurrent + *conversation* branches are not equivocation; they merge before the next + authority record, so they never collide on a `sequence`.) For a solo user (P3's n=2 world) the monotonic head lives on each enrolled device and each device is the other's witness — the same k-of-n substrate diff --git a/docs/spec/enrollment-protocol.md b/docs/spec/enrollment-protocol.md index 289972e..14525de 100644 --- a/docs/spec/enrollment-protocol.md +++ b/docs/spec/enrollment-protocol.md @@ -25,16 +25,25 @@ nonce in a self-signed object is byte-for-byte replayable. "proposed_caveats": [ … ], "observed": { "addr_candidates": [ … ] }, "answers": "…", "transcript": "cid:…", "sig": "…" } ``` -On receipt of message 2, **before any surface renders it**: -1. `answers` MUST be a challenge *this recipient issued, unconsumed, - unexpired* — then mark it consumed. Replay-state lives with the - challenger (unknown-key-share closure; station-to-station). +On receipt of message 2, **before any surface renders it**, validate +*first* and **consume last** — an early consume lets an unauthenticated +attacker submit garbage referencing a live challenge and *burn it* before +the signature is checked (OB-5). The order is `reserve → validate → +commit-consume`: +1. `answers` MUST reference a challenge *this recipient issued, unconsumed, + unexpired*, and bind the **challenge object CID** (not merely a nonce), + its intended recipient, protocol role, and store/principal context — + **reserve** it (do not yet consume); 2. the fingerprint's hash algorithm MUST be in the P1 allowlist **before hashing**, then the fingerprint MUST equal that algorithm's multihash name of `pubkey` (self-certification, checked by the library not the - human). + human); 3. `sig` MUST verify under `pubkey` over message 2 incl. `answers` and - `transcript` — **proof of possession**, transcript-bound. + `transcript` — **proof of possession**, transcript-bound; +4. **only now consume** the challenge (commit). A failed step 2/3 releases + the reservation, so a forged message cannot burn a live challenge. +Replay-state lives with the challenger (unknown-key-share closure; +station-to-station). ## 2. SAS pairing (enrolling your own devices/surfaces) diff --git a/docs/spec/identity-lifecycle.md b/docs/spec/identity-lifecycle.md index 5856d2f..1b72cb4 100644 --- a/docs/spec/identity-lifecycle.md +++ b/docs/spec/identity-lifecycle.md @@ -1,9 +1,12 @@ # P4 — Identity Lifecycle -**Layer:** 2. **Depends on:** P0 (L2, L5), P2 (chain-store). +**Layer:** 2. **Depends on:** P1 (Signed-Object), P2 (chain-store), P0 +(L2, L5). **Implements** P0's abstract contracts `AttestEvidence` and +`ValidAssociationProof` (OB-1) — the waist stays acyclic because *P4 +depends on P0*, never the reverse. **Status:** DRAFT. **Teeth:** Lean (quorum k-of-n soundness; non-regression -of the load-bearing set, PO-2b) + a **liveness** obligation (recovery is -never permanently denied to a legitimate owner, PO-R). Tier 3. +of the load-bearing set, PO-2b) + a **conditional liveness** obligation +(PO-R, §3). Tier 3. **Owns:** roles and delegation, the durable identity records, quorum revocation, and — required, not optional — break-glass and succession. @@ -48,13 +51,18 @@ entry (a signed loosening entry, L2). ### AuditRecord / AttestationRecord An **AuditRecord** witnesses the chain head (`witnessed_head`, `fingerprint`, `presence`); exported off-chain it becomes a P2 §4 anchor — in-chain alone -it anchors nothing. An **AttestationRecord** (P0 §3) is its authorization- -bearing sibling: one presence signature, two distinct fields — -**authorization** (`request_cid`, `decision_cid`) and **history witness** -(`observed_head`, `previous_witnessed_head`). Acceptance requires -`previous_witnessed_head` to be a reachable ancestor of `observed_head` -(else CHAIN HISTORY REGRESSION). Attest and audit stay distinct but -co-signed — *no authorization floats free of the history that gave it +it anchors nothing. An **AttestationRecord** is the concrete implementation +of P0's `AttestEvidence` contract (OB-1) — its authorization-bearing +sibling: one presence signature, two distinct statements. **Authorization:** +`request_cid`, `decision_cid`. **History witness:** `previous_checkpoint` +and `observed_checkpoint`, each a P2 `AuthorityCheckpoint` `(store_id, +thread_id, sequence, head)` — not a bare CID, so the witness names *which +spine* it advanced (OB-2). Acceptance requires `observed_checkpoint` to be a +forward extension of `previous_checkpoint` on the same `(store_id, +thread_id)` spine, `sequence` strictly greater (else CHAIN HISTORY +REGRESSION). It binds the canonical challenge preimage of P0 §3 and is +committed by that section's CAS transaction. Attest and audit stay distinct +but co-signed — *no authorization floats free of the history that gave it meaning.* ### RevocationRecord (quorum) @@ -70,9 +78,26 @@ meaning.* the object it signs is circular). `signers` is sorted by `by` and deduplicated (one identity can't count twice toward `k`); the signature set is *not* signed; the chain-append `sig` (WF-2) is separate and last. -Acceptance: `|distinct valid signers ∩ eligible| ≥ k` under `policy`. The -quorum policy is itself a principal-signed loosening entry (defining *who -may revoke* is authority structure, L2). +Acceptance is **epoch-bound (OB-7):** `|distinct valid signers ∩ eligible| +≥ k` under `policy`, **and** `policy` MUST be the policy active at the +record's own checkpoint — +``` +policy_cid == ActiveRevocationPolicy(revoke_target, observed_checkpoint, generation) +``` +— else an attacker replays an older, weaker, validly-signed quorum policy. +Policy *transitions* are themselves authorized under the previously-active +policy (or a separately-defined root-transition rule). The quorum policy is +a principal-signed loosening entry (defining *who may revoke* is authority +structure, L2). + +**Enrollment records the exact required revocation predicate, not a strength +tuple (OB-7).** The enrollment strength tuple `(SAS entropy × rounds, +witnesses, presence)` is **not totally ordered** — "2 rounds + 1 witness + +hardware" is incomparable to "1 round + 3 witnesses + no hardware" — so +"punting ≥ pinning" cannot be derived from it after the fact. Instead each +PinRecord names, at enrollment time, the *precise* `RevocationPolicy` CID +required to later punt this identity. Revocation compares against that +recorded predicate, never an informal ordering. ## 3. Break-glass & succession (REQUIRED) @@ -90,9 +115,25 @@ enrollment MUST provision recovery: Revoking the **last** root without a `succession` ends the mesh — implementations MUST refuse unless the record carries `"tombstone": true`. -This subsystem is required and sketched here; full specification is P4's -open work. **PO-R (liveness):** a legitimate owner is never *permanently* -locked out. + +**PO-R is CONDITIONAL liveness (OB-8).** "Never permanently locked out" +cannot be proven unconditionally — all recovery factors can be destroyed, +all witnesses can vanish, an adversary can block progress forever. State the +theorem with its assumptions: +> **Given** at least one configured recovery threshold remains uncompromised, +> eventual communication among an authorized recovery quorum, fair +> advancement of recovery generations, and no permanent destruction of all +> recovery material — **then** a legitimate owner can eventually install a +> successor root. + +The time-delayed unilateral path has a specific threat to answer: an attacker +who owns *one* device and **suppresses the veto messages** during the delay +window. Mitigations (each recorded, not assumed): require the veto window to +be acknowledged over ≥ 2 independent channels; make a *missing* expected +acknowledgement itself veto-the-recovery (fail-closed on silence); and cap +unilateral recovery to below the quorum needed for high-ceiling authority. +This subsystem is **required and specified conditionally**; the full +state-machine is P4's open work — the suite does not claim it closed. ## 4. Presence-attested pins A pin MAY carry a `presence` discharge — a WebAuthn/passkey step-up bound to diff --git a/docs/spec/rendering-security-profile.md b/docs/spec/rendering-security-profile.md index ebf8012..fcb503f 100644 --- a/docs/spec/rendering-security-profile.md +++ b/docs/spec/rendering-security-profile.md @@ -1,11 +1,14 @@ # P5 — Rendering Security Profile -**Layer:** 2. **Depends on:** P0 (the decision seam), P1. -**Status:** DRAFT. **Teeth:** Lean effect-CID soundness (Tier 3) for the -cryptographic half; a **stated human-factors residual** for the half that -is *not* cryptographic. Naming the boundary honestly is the whole job. -**Owns:** binding *what the human saw* to *what executes*, and the -obligations a rendering surface carries. +**Layer:** 2. **Depends on:** P0 (the decision seam), P1, P4 (surface +identity). **Status:** DRAFT. **Teeth:** Lean effect-CID soundness (Tier 3) +for the cryptographic half; **surface-attestation protocol** obligations +(Tier 2) for binary + rendering attestation; a **stated, shrunk +human-factors residual** for the analog gap that is not cryptographic. +Naming the boundary honestly is the whole job. +**Owns:** binding *what the human saw* to *what executes*, surface +attestation (proving the renderer is faithful), and the obligations a +rendering surface carries. ## 1. Effect binding — what you see is what you sign @@ -23,8 +26,28 @@ from the call it is about to run and check it equals `action.effect`** — otherwise a stale or lossy `display`→effect mapping approves X and executes Y while the CID still matches (confused-deputy / TOCTOU; WYSIWYS, Landrock-Pedersen 1998). Display and effect are bound under the one -signature. **PO-W (soundness):** an accepted decision's effect equals the -executed call. +signature. **PO-W (soundness):** the gate executes the exact +`Sealed` whose CID was approved. + +**Effect binds a value; the value must freeze the mutable world (OB-10).** +`action.effect` is only as strong as what enters the sealed `CallRequest`. +"Resolved resources" is therefore given a **normative per-class boundary** — +each action class names which resource *identities* (not mutable handles) +are sealed: + +| Class | Sealed identity — never a mutable handle | +|---|---| +| `file` | canonical path **+** inode/file-identity or content-CID | +| `container` | image **digest**, never a mutable tag | +| `repo` | repository identity **+** commit/tree CID | +| `network` | destination policy **+** an explicit DNS-resolution policy | +| `env`/`creds` | the resolved values, or a named residual (below) | + +Anything left ambient at approval time — symlink targets, DNS answers, +mutable files, env vars, container tags, credentials resolved *after* +approval — is a **named residual**, never silently folded into PO-W. If a +class cannot freeze a resource, it says so; it does not pretend the +signature covered it. ## 2. Gate-signed requests (the phishing-canvas closure) @@ -41,36 +64,75 @@ the gate accepts only a matching CID (P0 §2.3 step 1). This binds *what was shown* to *what was granted* — an attacker cannot show request A and apply the approval to request B. -## 4. The residual — rendering faithfulness is NOT cryptographic - -Binding the signature to `effect` proves the decision is bound to the -*data*. It does **not** prove the UI faithfully showed that data to the -human. Truncation, locale, misleading formatting, hidden arguments, and path -abbreviation remain human-factors surfaces. This profile therefore: - -- **requires** `display` to be derived from `effect` by a profile-defined, - **deterministic rendering function** — so `display` is checkable against - `effect`, and a surface that cannot faithfully render `effect`'s meaning - MUST refuse rather than show a prettier `display`; -- treats a **signed render transcript** — the surface attesting the exact - bytes it presented — as the strengthening path (its own future work); -- states plainly that the last gap is **irreducible human factors**: - `attest` for high ceilings and distinct ceremony UI (consumer guidance, - newt#1209) mitigate but do not eliminate it. - -Named, not solved. This honesty is the point: a rendering profile that -*claimed* cryptographic faithfulness would be exactly the "prose becomes +## 4. Surface attestation — shrinking the faithfulness gap + +Effect-binding proves the decision is bound to the data; it does not prove +the *surface* is a faithful renderer. Two complementary attestations raise +the bar so **an unfaithful surface has trouble passing** — the surface +becomes a participant that must prove *what it is* and *what it did*. + +**4.1 Binary attestation — what the surface IS.** A surface's identity (P4: +a keypair) is bound to a **measured code identity** — a reproducible-build +hash, TPM/DICE quote, or platform attestation (Play Integrity / App +Attest). The principal blesses surface *measurements* onto a profile +allowlist (the same allowlist discipline as P1 §4·4, applied to code). A +decision from a surface whose measurement is not blessed is **refused for +actions above a policy-set ceiling** — degrading the residual from "any +surface on earth" to "a known-good build." A tampered renderer has a +different measurement and fails the check. This is remote attestation +(Parno et al.); its primitives are a Tier-1 assumption, its handshake a +Tier-2 (protocol) obligation. + +**4.2 Rendering attestation — what the surface DID.** A **signed render +transcript**: the surface signs the exact bytes it presented, bound to +`request_cid` and the deterministic `display`-from-`effect` output (§1). The +faithfulness *ceremony* injects a challenge a faithful renderer passes and +an unfaithful one cannot: the gate derives a **witness token** from +`(effect, nonce)` that MUST appear verbatim in the canonical `display` the +human confirms. A surface that truncated, reformatted, localized-away, or +hid part of the effect cannot produce a `display` containing the correct +token — so the token is absent or wrong, the human sees the mismatch, and +the presence discharge (which covers the token) fails. "Did the surface show +the whole truth?" becomes checkable rather than assumed. + +Both compose with the existing gate checklist: high-ceiling actions MAY +require a blessed measurement (4.1) **and** a token-bearing render transcript +(4.2) as part of the `attest` discharge (P0 §3). + +## 5. The residual — faithfulness is *shrunk*, not eliminated + +Even with §4, honesty is preserved (this is the discipline the reviews +praised): binary attestation proves the code is known-good, **not** that +known-good code has no rendering bug; the render transcript proves what bytes +were emitted, **not** what pixels a compromised display driver painted; the +witness-token ceremony assumes the human actually reads. The final analog +gap — human eyes on a possibly-hostile display — is irreducible. So P5: + +- **requires** `display` to be a deterministic function of `effect` (§1), so + it is checkable, and a surface that cannot render `effect` faithfully MUST + refuse rather than show a prettier `display`; +- **offers** binary + rendering attestation (§4) to shrink the residual by + policy ceiling; +- **names** the remainder as irreducible human factors — `attest` for high + ceilings and distinct ceremony UI (newt#1209) mitigate, never eliminate. + +Shrunk, bounded, named — not "solved." A rendering profile that *claimed* +cryptographic faithfulness would be exactly the "prose becomes authority-bearing protocol" failure the security reviews caught. -## 5. Proof obligations +## 6. Proof obligations | PO | Statement | Tier | |---|---|---| -| PO-W | effect-CID soundness: accepted decision's effect = executed call | 3 | +| PO-W | the gate executes the exact `Sealed` whose CID was approved | 3 | +| PO-RES | each action class seals resource *identities*, not mutable handles (OB-10) | 3 + vectors | | — | render-swap / phishing-canvas closures | 3 (with P0 gate checklist) | -| residual | display-faithfulness | *not cryptographic — stated, mitigated* | +| PO-SB | binary attestation: a decision above ceiling C is honoured only from a blessed surface measurement | 2 (protocol) | +| PO-SR | rendering attestation: the witness token is present in `display` ⇒ the surface showed the whole effect | 2 (protocol) | +| residual | display-faithfulness on a hostile display | *shrunk by §4, not eliminated — stated* | ## Relations -- P0 §2.1/§2.3 (PermissionRequest, Decision, gate acceptance) · P1 - (canonical CIDs) · newt-agent#1209 (ceremony UI consumer) · - Landrock-Pedersen 1998 (WYSIWYS) +- P0 §2.1/§2.3/§3 (request, decision, attest discharge) · P1 (canonical + CIDs, allowlist) · P4 (surface identity the measurement binds to) · + newt-agent#1209 (ceremony UI consumer) · Landrock-Pedersen 1998 (WYSIWYS) + · remote attestation (Parno et al.) for §4.1 diff --git a/docs/spec/signed-object-profile.md b/docs/spec/signed-object-profile.md index 281435e..60e9841 100644 --- a/docs/spec/signed-object-profile.md +++ b/docs/spec/signed-object-profile.md @@ -20,13 +20,39 @@ the algorithm code* — two hash algorithms never collide silently. The key is the identity; the fingerprint `H(pubkey)` is its self-certifying *name*. *BLAKE3 is an implementation detail, not a law.* -## 2. One schema, three encodings - -JSON for interchange (client libs), TOML at rest (#220 policy files), -**canonical DAG-CBOR for anything hashed or signed.** Signatures and -`ContentId`s are computed over canonical bytes only. Wall-clock is never a -coordination primitive — validity keys on generation counters; RFC 3339 -timestamps are provenance *data*, never read by a kernel. +## 2. One schema, one signed byte-string, many views (OB-4) + +An earlier draft said "JSON for interchange, TOML at rest, DAG-CBOR for +signing" *and* "verify over received bytes, never reserialize" — which is +contradictory, because a recipient handed JSON has not received the signed +DAG-CBOR bytes, and converting JSON → DAG-CBOR *is* a reserialization. The +resolution: **the canonical DAG-CBOR bytes are carried, not reconstructed.** + +Every authority-bearing object travels in a **signed envelope** whose +`body` is the exact canonical bytes that were hashed and signed: +```json +{ "profile": "agent-bridle/permission-request/v1", + "codec": "dag-cbor", + "body": "", + "cid": "cid:…", // = H(body) + "by": "b3:…", "sig": "…" } // sig over cid +``` +Verification is then unambiguous and identical in all four client languages: +``` +1. allowlist the codec and algorithms (§4·4) — before touching body +2. verify cid == H(body) — CID over the bytes +3. verify sig over cid under `by` +4. decode body → typed value +5. check schema + critical fields; reject unknown authority-bearing fields +6. construct Sealed +``` +**JSON and TOML are views/containers, never independent authority-bearing +serializations.** A JSON rendering of an object is for humans and +non-authority interchange; the *authority* lives in `body`. TOML policy +files (#220) at rest likewise wrap the signed `body`. Signatures and +`ContentId`s are over `body` only. Wall-clock is never a coordination +primitive — validity keys on generation counters; RFC 3339 timestamps are +provenance *data*, never read by a kernel. ## 3. The Memo discipline (WF-2) @@ -70,6 +96,20 @@ the data layer, none of the resource layer.** declared code against the locally-trusted profile table (§6) **before hashing or verifying**, and reject anything outside it. Agility lives in the profile, never on the wire. +5. **Universal domain separation + context binding (OB-6).** *Every* signed + `body` MUST begin with a domain-separation tuple, not just the WebAuthn + challenge: + ``` + ("agent-bridle///", + store_id, thread_id_or_principal_id, generation, ) + ``` + A signature is valid only for its exact `(record-type, store, thread/ + principal, version)` context. Without this, a validly-signed payload can + be replayed **across stores, principals, causal threads, structurally- + compatible record types, and profile versions.** `store_id` is a + normative, cryptographically-bound identifier (P2 §) — "same store" is a + value the signature covers, not prose. Verifiers reject a signature whose + domain tuple does not match the context in which it is being used. ## 5. Deterministic signatures (why Ed25519 is pinned) diff --git a/docs/spec/suite.toml b/docs/spec/suite.toml new file mode 100644 index 0000000..6c549ec --- /dev/null +++ b/docs/spec/suite.toml @@ -0,0 +1,18 @@ +# Ceremony Suite — compatibility manifest (OB-11). +# One atomic target for implementers; profiles still evolve independently. +# When a profile version bumps, update its pin here and re-run conformance. + +suite = "ceremony-suite" +suite_version = "0.3.0" + +[requires] +p0 = "0.2" # ceremony-contract — the narrow waist (five laws) +p1 = "1" # signed-object — CID/canon/Sealed/allowlist/envelope +p2 = "0.2" # chain-store — causal transcript + anti-rollback anchor +p3 = "0.2" # enrollment — SAS/PoP/anchors +p4 = "0.2" # identity-lifecycle — roles/revocation/recovery +p5 = "0.2" # rendering-security — effect binding + surface attestation + +# Cross-language conformance vectors (Rust/Python/Dart/TS must agree). +# CID is populated once the kernel + vectors exist. +conformance_vectors = "cid:TBD" From 18dea20cc00527b2ffd76d88e1745261508950b7 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Thu, 16 Jul 2026 07:49:45 -0400 Subject: [PATCH 12/13] docs(spec): add the agent onboarding PRIMER A single cold-start doc so any agent we bring online for this job gets productive fast: the one-breath what-it-is, the contribution framing (the enforcement gate nobody ships), the seven load-bearing doctrines, the suite architecture + dependency order, the three proof tiers, the five laws, a glossary, the cross-model adversarial review discipline, current state + what is HELD (revise freely, land nothing), the contribution workflow (incl. the concurrency hazard + no-session-link rule), and a full map of where everything lives. Linked from README as the entry point. Refs #229, #225. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/PRIMER.md | 269 ++++++++++++++++++++++++++++++++++++++++++++ docs/spec/README.md | 4 + 2 files changed, 273 insertions(+) create mode 100644 docs/spec/PRIMER.md diff --git a/docs/spec/PRIMER.md b/docs/spec/PRIMER.md new file mode 100644 index 0000000..bf633d0 --- /dev/null +++ b/docs/spec/PRIMER.md @@ -0,0 +1,269 @@ +# Ceremony Suite — Agent Onboarding Primer + +**Read this first.** You have been brought online to work on the **Ceremony +Suite**: the specification (and eventually the proven implementation) of how +an agent system decides *who may do what*, binds those decisions to a +tamper-evident history, and proves it. This primer gets you from cold to +productive. It is opinionated on purpose — the design has survived multiple +adversarial reviews, and the conventions below are how it stays sound. + +If you read nothing else, read: this file, then +[`README.md`](README.md) (the suite index), then the one profile you were +assigned. The five laws in [`ceremony-contract.md`](ceremony-contract.md) §4 +are the spine of everything. + +--- + +## 1. What this is, in one breath + +Agent harnesses need to enforce authority: run this command? trust this +peer? pin this key? The **Ceremony Contract** is the library-side contract +for those decisions; the **Ceremony Suite** is that contract split into +seven documents, each provable in isolation, with the five laws as a *narrow +waist*. + +The deliverable for end-users is a harness (newt, hermes, gila, a Claude +Code / Codex plugin) that prompts a human at the right moment and enforces +the answer. Everything here is the plumbing that makes those prompts +**honest** — cryptographically bound to what actually executes, and provably +unable to widen authority behind the user's back. + +## 2. Why it matters — the contribution + +A verified research pass established that the *ideas* here are ~30 years of +mainstream consensus (Saltzer's identifier/locator split, Host Identity +Protocol, NIST 800-207 Zero Trust, SPIFFE, iroh's "dial the key, not the +IP"). We are not inventing the loc/ID split. + +**The unique contribution is the enforcement gate.** Nobody ships +*first-contact ceremony enforcement* — fail-closed, consumer-rendered, +presence-attested, caveat-attenuated — as a reusable, harness-agnostic, +formally-verified library. HIP admits the trust-on-first-use "leap of faith" +and ships nothing; SSH/Signal bake a prompt into one client; WebPKI/Tailscale +centralize; SPIRE automates the human away. WebAuthn is the closest precedent +(relying-party library enforces, platform renders) but is user→service, not +peer↔peer agent introduction. + +So the moat is **the contract, not the crate**: publish the spec so any +harness can comply without taking a dependency; `agent-bridle` is the +reference implementation. Client libraries follow in Rust, Python, Dart, and +TypeScript (one Rust enforcement core; the other languages implement the +*consumer* side only — never fork the gate). + +## 3. The doctrine (load-bearing beliefs — internalize these) + +These are not style preferences; violating them has repeatedly produced real +bugs that adversarial reviewers found. + +1. **The authenticated thing is always the key, never the channel.** + Locations, relays, registries, and rendered pixels are candidates and + hints; signatures and content-IDs are what a gate trusts. (This is the + floating-identity doctrine — see agent-mesh `docs/decisions/ + floating_identity.md`. Identity floats; harnesses are fungible substrate.) +2. **No authorization or claim floats free of the exact history and + artifacts that gave it meaning.** A decision binds the request; the + request binds the executable effect; an attestation binds a non-regressing + history checkpoint. +3. **Law minimalism.** A good system has only the laws it absolutely needs. + *Nothing enters the law section without a proof obligation demanding it;* + everything else is mechanism (a profile) or well-formedness. We have gone + 6 → 5 laws and absorbed four review rounds of additions at **zero** net + law cost. The algebra decides the count; ambition does not. +4. **Libraries expose decision STRUCTS and seams; they contain NO TUI.** + `agent-bridle` / `agent-mesh` / `agent-*` are libraries. They expose a + `DecisionSurface` seam that *demands* a consumer-built UI; they never + render. newt draws a matrix chooser, hermes a flat list, a daemon reads a + policy file — one struct, many layouts. (Backlog: agent-bridle#225.) +5. **Algorithms are pins, not laws.** Laws name *properties* (collision + resistance, determinism); a profile pins the algorithm; identifiers are + self-describing (multihash/multicodec). *"BLAKE3 is an implementation + detail."* Agility needs an allowlist checked before dispatch, or it is a + downgrade attack. +6. **Honesty over completeness.** When a mechanism does not fully close a + gap, name the residual (rendering faithfulness is not cryptographic; the + chain alone does not stop rollback). A claim stronger than the machinery + is the exact failure adversarial review punishes: *"prose becomes + authority-bearing protocol."* +7. **Publish over patent.** Default to publishing defensively. + +## 4. The architecture: the suite + +Seven documents. Each is a **decision** with a lifecycle: *Proposed → +Accepted → Proven*. A downstream decision **cannot be Accepted until its +dependencies are Proven** — that is the "chain of decisions." + +| # | Profile | Owns | +|---|---|---| +| **P0** | [`ceremony-contract.md`](ceremony-contract.md) | the five laws, the authority lattice, the `DecisionSurface` seam, gate acceptance — **the narrow waist** | +| **P1** | [`signed-object-profile.md`](signed-object-profile.md) | naming, canonicalization, signatures, the signed-bytes envelope, the algorithm allowlist | +| **P2** | [`chain-store-profile.md`](chain-store-profile.md) | the causal-transcript store, the linear authority spine, the external anti-rollback anchor | +| **P3** | [`enrollment-protocol.md`](enrollment-protocol.md) | introductions, SAS pairing, external anchors | +| **P4** | [`identity-lifecycle.md`](identity-lifecycle.md) | roles & delegation, records, quorum revocation, break-glass/succession | +| **P5** | [`rendering-security-profile.md`](rendering-security-profile.md) | effect binding, gate-signed requests, surface attestation | + +**Dependency order:** `P1 → P2 → P0 → P4 → P3`, with P5 on {P0, P1, P4}. +(P0 depends on P4 only via *abstract contracts P0 itself defines* — +`AttestEvidence`, `ValidAssociationProof` — which P4 implements. That +dependency inversion is what keeps the graph acyclic; do not reintroduce a +concrete P4 type into P0.) + +The provable MVP is **P1 + P2 + P0** — the waist. Ceremonies (P3–P5) graft +on once the waist is Proven. `suite.toml` pins which profile versions form +one compatible suite. + +## 5. The three tiers of "teeth" (do not confuse them) + +Correctness is enforced in layers, each verified by a *different* tool. A +proof that silently assumes the wrong tier is the classic failure mode. + +- **Tier 3 — Lean + Aeneas (kernel refinement).** The authority *algebra* + and the trusted *state machine* (P0/P1/P2/P4). A pure Rust kernel is + extracted by Charon and proven in Lean via Aeneas to refine the model. CI + gate: no Rust kernel merges unless the refinement proof passes. +- **Tier 2 — Tamarin / ProVerif (protocol safety).** The *ceremonies* (P3, + parts of P5). A flawless lattice can sit behind a leaky handshake, so + MITM / replay / unknown-key-share get *symbolic* proof, not algebraic. +- **Tier 1 — cryptographic primitives (assumed).** Ed25519 unforgeability, + BLAKE3 collision resistance, deterministic nonces. Cited, not proven; the + trust base; rotatable via P1's allowlist. +- **Cross-cutting — conformance vectors.** Shared JSON vectors bind the four + client languages to one observable behaviour where proofs stop. + +The pure kernel is `resolve` + precedence + the gate acceptance checklist + +the P2 trusted-state transition — **no serde, no IO, no crypto impl, no UI**. +Crypto enters the kernel as *abstract injective/one-way contracts* (P1's job +to satisfy). Keeping that boundary is what lets Aeneas run. + +Toolchain setup (Lean/elan, Charon nightly, opam/OCaml for Aeneas, per OS): +[`../TOOLCHAIN.md`](../TOOLCHAIN.md). + +## 6. The five laws (memorize these) + +From P0 §4. Each has a proof obligation. + +- **L1 — Resolution is a meet.** Verdicts are ordered `deny ⊏ attest ⊏ ask + ⊏ approve`; `resolve` is the meet of matching verdicts, **with the no-match + case defined explicitly as `ask`** (empty meet = ⊤ = approve would fail + *open* — this is a subtle bug we fixed). Order-independent. (PO-1) +- **L2 — Tamper-boundedness.** A sub-quorum actor can neither *widen* + authority nor *shrink* the load-bearing identity structure. Revoking an + identity requires quorum ("reset mesh" must not be a DoS). Rollback + resistance needs an *external* anchor, not the chain alone. (PO-2/2a/2b/2c) +- **L3 — Fail-closed totality.** `resolve` is total; headless degrades + `ask/attest ↦ deny`. Nothing reaches "undefined permission." (PO-3) +- **L4 — Attenuation.** `effective = granted ⊓ required`; authority composes + by meet, never amplifies; `escalate` carries ⊥ authority. (PO-4) +- **L5 — The ceremony gate.** `association ⇒ pinned`; `fingerprint = + H(pubkey)` is self-certifying, so re-key ⇒ re-ceremony; pinned is + transitive through certification (delegation). (PO-5) + +## 7. Glossary (speak the same language) + +- **Fingerprint** — `H(pubkey)` as a multihash; a self-describing *name* for + a key. The key is the identity; the fingerprint is its name. +- **Verdict** vs **matrix verb** — `deny/attest/ask/approve` are *verdicts* + (resolution's codomain); `allow/attest/deny` are *matrix verbs* (what a + surface offers). `allow` = `approve` seen from the offer side. `ask` is a + verdict, never an offer. `escalate` is a navigation action (⊥ authority), + never a verdict. +- **attest** — the presence-required disposition (a fresh WebAuthn/passkey + ceremony). Renamed from `passkey` (that word now names only the hardware + mechanism; rename tracked in #231). Term follows the attestation + literature (Parno et al.). +- **chain-store** — the append-only-verifiable record log; a Merkle DAG. The + *authority projection* of the wider Conversation Graph (agent-mesh#67). +- **causal transcript / authority spine** — the store is a DAG for + conversation but a **linear spine per causal thread** `(store_id, + thread_id, sequence)` for authority. Conversation is a jungle; authority + is a railway. +- **checkpoint / anchor** — the highest `AuthorityCheckpoint` a participant + has accepted, kept *outside* the store it validates (device keystore / + TPM / witness quorum). Rollback resistance lives here. +- **Sealed** — a wire object constructed only through verification; + immutable after. The Rust heir of the `Memo` discipline. +- **Gate** — the enforcement choke-point; mints authority only after its + acceptance checklist passes. Never trusts the surface. +- **Caveats** — attenuable authority; a meet-semilattice (`meet_never_ + amplifies` is property-tested upstream in agent-mesh-protocol). + +## 8. How we got here — the review discipline + +This spec is the product of **heterogeneous adversarial review**: successive +passes by different model families (Claude, GPT-5/Codex, others), each trying +to break it. That is not incidental — it is the method. A cross-model +adversary finds what four same-model rounds miss. + +- Findings are triaged into an **obligations ledger** (`README.md` → "Spec + obligations"), tagged blocker/high/medium, each mapped to the owning + profile. +- We fix by **naming and binding a mechanism**, not by adding a law. +- We record the *residual* honestly when a fix is partial. +- Every review round to date has *strengthened* the design and held the law + count at five. The last full pass moved the criticism from "the + architecture is confused" to "four interfaces need exact state-machine + definitions" — which is victory, not defeat. + +When you find a gap: add it to the obligations ledger with a resolution +direction before (or instead of) arguing it in prose. + +## 9. Current state & what is HELD + +- **PR #229** carries the whole suite on branch `docs/spec-ceremony-contract`. +- **PR #232** (GPT-5/Codex) is the stacked *formal kernel* design — adopted + as the P0+P1+P2 provable MVP. +- **Status: design draft, actively reviewed.** Spec **revisions are open**; + **merge and implementation are HELD** pending the author's sign-off. Do + not merge to `main`; do not start the Lean/Rust/Aeneas implementation + until told. (When in doubt, the hold is: *revise freely, land nothing.*) +- **Open author's-calls** (do not decide these yourself): (a) the OB-2 + *linear authority spine vs. frontier checkpoint* model — currently adopted + as linear, pending veto; (b) the **`attest` factorization** — one verb + axis vs. `effect × assurance × scope` (GPT-5 #232 leans product-lattice; + L1 survives either). + +## 10. How to contribute (the workflow) + +Follow `AGENTS.md` and `../DESIGN.md` at the repo root; the essentials: + +- **Branch → change → `just check` green → push → PR.** Never push to + `main`. `just check` = fmt + clippy `-D warnings` (both feature configs) + + tests. `just install-hooks` after clone; **never `--no-verify`** in this + repo. +- **Concurrency hazard:** more than one agent writes the spec branch. Always + `git fetch` first; work in a throwaway **worktree**; if a push is rejected, + `git rebase` onto the moved branch (verify tree parity if the history was + rewritten). Remove your worktree when done (disk discipline). +- **Commit hygiene:** author `hartsock@users.noreply.github.com`; trailer + `Co-Authored-By: Claude Opus 4.8 (1M context) `. + **Never** put a Claude session URL / `Claude-Session:` trailer in a commit + or PR body — banned, enforced by hook where present. +- **Spec edits** cross-reference the owning profile and cite the obligation + (`OB-n`) or law they serve. Keep the five-law waist sacred: a new law needs + a proof obligation and an author decision. +- **When implementation opens** (not yet): build order `P1 → P2 → P0`; carve + the pure kernel first; wire Charon→Aeneas→Lean; keep crypto behind abstract + contracts. + +## 11. Map — where everything lives + +- **This suite:** `docs/spec/` (you are here). Index: `README.md`. Manifest: + `suite.toml`. Toolchain: `../TOOLCHAIN.md`. +- **Reference implementation:** `agent-bridle` crates (`agent-bridle-core` + owns the policy/verdict/gate; `step_up::DischargeVerifier` is the shipped + presence seam; #226/#227 shipped signed loosening entries). +- **Mesh side:** `agent-mesh` — `Introduction`/decision surfaces (#65), + enrollment + multihash `Fingerprint` (#66), the **Conversation Graph** + (#67); `CertChain::verify` + proof-of-possession (#39/#40); + `docs/decisions/floating_identity.md` (the doctrine). +- **First consumer:** newt-agent#1209 (the pinning ceremony, HIGH). +- **Umbrella / strategy:** agent-bridle#225 (the no-TUI directive, client-lib + matrix, the contribution framing). Rename: #231 (`passkey`→`attest`). +- **Prior art (cite, don't reargue):** Saltzer RFC 1498; HIP RFC 7401/9063; + NIST SP 800-207; SPIFFE; iroh; RFC 6962 (CT); TUF; Schneier-Kelsey; FssAgg + (eprint 2008/185); Landrock-Pedersen 1998 (WYSIWYS); RFC 8785 (JCS). + +--- + +*The rule underneath all of it: no authorization floats free of the history +and artifacts that gave it meaning. Everything else is how we make that +true and prove it.* diff --git a/docs/spec/README.md b/docs/spec/README.md index 2c80e2e..5e2fc85 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -1,5 +1,9 @@ # The Ceremony Suite — a dependency-ordered set of provable profiles +> **New to this job? Start with [`PRIMER.md`](PRIMER.md)** — the agent +> onboarding doc (doctrine, architecture, conventions, current hold, how to +> contribute). Then this index, then your assigned profile. + **Status:** DRAFT (2026-07-16). This directory replaces the single `ceremony-contract.md` monolith with a suite of **loosely-coupled, functionally-cohesive** profiles: each names one job, owns its own algebra From 60ff13e96323e3f70bae26effe24d11c0939ed4e Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Thu, 16 Jul 2026 19:28:54 -0400 Subject: [PATCH 13/13] =?UTF-8?q?docs(spec):=20v0.3.1=20PROTOCOL=20FREEZE?= =?UTF-8?q?=20=E2=80=94=20close=20review-7's=207=20findings;=20freeze=20au?= =?UTF-8?q?thority=20type;=20CAS=E2=86=92BVP=20roadmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Protocol-freeze pass (review 7). Seven places implementers would diverge, now frozen; conformance vectors are the one remaining implementation-phase item. - OB-12 (type FROZEN): P0 §2.0 Authority = Effect × Assurance × Scope (componentwise meet); ask → NeedsDecision (control-flow, not an authority level); deny = ⊥; explicit signed ; scope = profile-declared closed order. Decides the attest factorization (4-way converged). Laws L1/L4 lift componentwise; still five laws. - OB-13: P1 one signed-object constructor — sig covers a canonical PROTECTED TUPLE (profile, codec, cid, signer, domain-sep), not bare cid; body domain tuple must equal the envelope. - OB-14: genesis STORE_ID_SELF sentinel breaks the store_id=CID(genesis) fixed point. - OB-15 (CAS FROZEN): P2 §1.1 compare-and-swap on expected head; concurrent candidates + CAS-losers are NOT equivocation. Roadmap adopted: CAS → Byzantine Vertical Paxos (wedge/reconfiguration; ref Abraham-Malkhi DCCL / Lamport-Malkhi-Zhou) as a post-waist P2 evolution, own ADR + TLA+/Lean, NOT in this freeze. - OB-16 (law corrected): L2 upward is now equality over authority-generating structure, not ⊆ — a sub-quorum actor can't ADD a trusted issuer either (closes time-delayed privilege escalation). - OB-17: P3/P4 revocation — drop 'punting ≥ pinning' scalar; exact policy-CID predicate (P4); P3 now depends on P4. - OB-18: P5 — token downgraded to attention-aid; faithfulness = byte-compare vs a gate-produced canonical render; RAW SECRETS forbidden in signed records (secret-version refs / one-shot caps / keyed commitments). suite 0.3.1; p0 0.3. Refs #229 review round 7. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/README.md | 47 +++++- docs/spec/ceremony-contract.md | 200 +++++++++++++++--------- docs/spec/chain-store-profile.md | 74 ++++++--- docs/spec/enrollment-protocol.md | 14 +- docs/spec/rendering-security-profile.md | 42 +++-- docs/spec/signed-object-profile.md | 65 +++++--- docs/spec/suite.toml | 4 +- 7 files changed, 305 insertions(+), 141 deletions(-) diff --git a/docs/spec/README.md b/docs/spec/README.md index 5e2fc85..21fc460 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -171,14 +171,45 @@ original severity. | OB-10 | M | ✅ resolved | P5 §1 — per-class sealed resource identities (container **digest** not tag, repo commit/tree CID, …); ambient = named residual. | | OB-11 | M | ✅ resolved | `suite.toml` compatibility manifest (below). | -¹ OB-2 and the `attest` factorization were the two author's-calls; the -linear spine is *adopted pending your veto* (it's the reviewer's -recommendation and the provable option). The factorization stays open — the -lattice works either way (GPT-5 #232 already leans product-lattice). - -The review's own verdict was "approve the split, request-changes on the four -blockers, list the rest." All four blockers (OB-1/2/3/4) plus the OB-9 -soundness bug are now closed in text. +¹ OB-2's linear spine and the `attest` factorization were the two +author's-calls — **both now decided** (see OB-12/OB-15 below). + +### Review 7 — the v0.3.1 PROTOCOL FREEZE (2026-07-16) + +A protocol-freeze review named seven places implementers would otherwise +diverge. All closed in text; the *proofs / conformance vectors* that discharge +them are the (still-held) implementation phase. + +| # | Sev | Status | Where fixed | +|---|---|---|---| +| OB-12 | B | ✅ **type frozen** | P0 §2.0 — `Authority = Effect × Assurance × Scope` (componentwise meet); `ask`→`NeedsDecision` (control-flow, not authority); `deny`=⊥; **explicit signed `ceiling`**; scope is a profile-declared closed order. Resolves the `attest` factorization (4-way converged). | +| OB-13 | B | ✅ resolved | P1 §2 — ONE signed-object constructor; `sig` covers a canonical **protected tuple** (profile, codec, cid, signer, domain-sep), not bare cid; body domain tuple must equal the envelope. | +| OB-14 | B | ✅ resolved | P1 §2 / P2 §1 — genesis `STORE_ID_SELF` sentinel breaks the `store_id = CID(genesis)` fixed point. | +| OB-15 | B | ✅ **CAS frozen** | P2 §1.1 — compare-and-swap on expected head; **concurrent candidates + CAS-losers are NOT equivocation**. Roadmap: CAS → **Byzantine Vertical Paxos** (below). | +| OB-16 | B | ✅ **law corrected** | P0 L2 — upward direction is now `TrustedStructure(m(R)) = TrustedStructure(R)` (equality), not `⊆`: sub-quorum actors can't *add* a trusted issuer either (closes time-delayed privilege escalation). | +| OB-17 | H | ✅ resolved | P3/P4 — dropped "punting ≥ pinning" scalar (tuple isn't totally ordered); revocation is P4's exact policy-CID predicate; P3 now depends on P4. | +| OB-18 | H | ✅ resolved | P5 — token downgraded to attention-aid; faithfulness = **byte-compare vs a gate-produced canonical render**; **raw secrets forbidden** in any signed record (use secret-version refs / one-shot caps / keyed commitments). | + +**Exit gate (v0.3.1) status:** factorization + `ask` ✅ · ceiling + scope +order ✅ · signed-envelope grammar ✅ · genesis non-circular ✅ · append/CAS +normative ✅ · L2 forbids latent additions ✅ · P3/P4 agree ✅ · P5 token + +secrets ✅ · **conformance vectors (positive + negative) — the one +implementation-phase item, tracked with the kernel (`suite.toml` +`conformance_vectors`).** + +### Store roadmap (adopted): CAS → Byzantine Vertical Paxos + +The frozen CAS append is the **threshold-1 steady-state case** of a +vertically-reconfigurable store. **Sequencing (author-adopted):** prove the +`P1 → P2 → P0` waist on CAS first; then evolve P2 to separate a lean +steady-state protocol from a stronger **reconfiguration** mechanism (the +*wedge*: fence config → capture safe closing state → certify next config), +giving 2-full-node deployments safe failover (`f+1` steady / `2f+1` +reconfiguration) and an operation-sensitive partition-authority ceiling on the +§2.0 lattice. **Reference implementation: (Byzantine) Vertical Paxos — +Abraham & Malkhi (IBM Zurich DCCL); VP orig. Lamport-Malkhi-Zhou, PODC 2009.** +The wedge/closing-state correctness gets its own ADR + TLA+/Lean model before +it is normative; it is **not** part of the v0.3.1 freeze. ## Suite compatibility manifest (OB-11) diff --git a/docs/spec/ceremony-contract.md b/docs/spec/ceremony-contract.md index 6fe973e..32a9d05 100644 --- a/docs/spec/ceremony-contract.md +++ b/docs/spec/ceremony-contract.md @@ -1,6 +1,8 @@ # P0 — The Ceremony Contract (the narrow waist) -**Status:** DRAFT 0.2.1 (2026-07-16) — partitioned from the v0.1.x +**Status:** DRAFT 0.3.1 — PROTOCOL FREEZE (2026-07-16). Authority type frozen +(`Effect × Assurance × Scope`; `ask`→`NeedsDecision`; explicit ceiling — the +`attest` factorization, decided). Partitioned from the v0.1.x monolith into the [Ceremony Suite](README.md), then repaired against review round 6 (all eleven obligations closed in text — see README). This document is the **waist**: the five laws, the authority algebra, and the decision @@ -52,6 +54,42 @@ All are Memo-descendants (P1 §3). Full field-level definitions of the identity/enrollment/store records live in their owning profiles; the three objects the *seam* itself trades are here. +### 2.0 The Authority type (FROZEN — OB-12) + +Authority is a **product meet-lattice of three independent axes**, with +componentwise attenuation: + +``` +Authority = Effect × Assurance × Scope + Effect : deny ⊏ allow (deny = ⊥) + Assurance : none ⊏ presence ⊏ hardware (the old `attest` lives here) + Scope : once ⊏ session ⊏ durable (open-EXTENSIBLE, but see below) + (e₁,a₁,s₁) ⊓ (e₂,a₂,s₂) = (e₁⊓e₂, a₁⊓a₂, s₁⊓s₂) + ⊥ = (deny, none, once) ⊤ = (allow, hardware, durable) +``` + +`deny` is the bottom of `Effect`; there is exactly one denied authority +(`⊥`), never several semantically-equal "denied authorities". The former +`attest` disposition is **`Assurance = presence`** (or `hardware`); it is no +longer a verb, so it composes with any Effect/Scope. + +**`ask` is NOT an authority level — it is a control-flow result (OB-1/#1):** + +``` +Resolution = NeedsDecision (was `ask`: must interact) + | Decided(Authority) +``` + +`NeedsDecision` means "no rule settled this; a surface must decide." It has +no place in the lattice; the gate never *grants* `NeedsDecision`. Headless, +it degrades to `deny` (L3). + +**Scope is a *declared, closed-per-profile* order, not arbitrary strings +(#1).** A profile MUST publish its scope set and their total order + meet +(v1: `once ⊏ session ⊏ durable`); an implementation rejects a scope outside +its profile. "Open vocabulary" means *a profile may extend the set*, never +*any string compares somehow*. + ### 2.1 PermissionRequest ```json @@ -59,73 +97,81 @@ objects the *seam* itself trades are here. "action": { "class": "exec", "display": "run_command: cd ", "effect": "cid:…" }, // canonical resolved call — P5 "violation": "outside-granted-allowlist", + "ceiling": { "effect": "allow", "assurance": "presence", "scope": "session" }, "matrix": { … }, "context": { "session": "…", "generation": 41 }, "by": "b3:…", "sig": "…" } // gate signature — required remote (P5) ``` -`action.effect` binds the signature to *what executes*, not what is shown -(P5 owns effect-binding and the rendering residual). +**`ceiling` is an explicit, signed field (#1)** — the maximum Authority this +request may resolve to. No grant may exceed it (L4). It is set by the gate +from policy at issue time; the gate re-derives and re-checks it before +minting (never trusts a surface-supplied ceiling). `action.effect` binds the +signature to *what executes* (P5). ### 2.2 DecisionMatrix ```json -{ "verbs": ["allow", "attest", "deny"], "scopes": ["once", "session"], - "default": ["allow", "once"], "escalations": ["audit"] } +{ "effects": ["allow", "deny"], + "assurances": ["none", "presence"], + "scopes": ["once", "session"], + "default": { "effect": "allow", "assurance": "none", "scope": "once" }, + "escalations": ["audit"] } ``` -The decision *space*; nothing encodes layout. `attest` is the -presence-required disposition (`attest × once` = ceremony every action; -`attest × session` = one discharge per generation). `default` is the -administrator/packager **opinion surface** (`rm -rf` ships `["deny", -"session"]`), never an auto-grant. Scope vocabulary is open; a durable -scope materializes as a signed loosening entry (L2, via P2). +The decision *space* over the three axes; nothing encodes layout. A surface +offers points of `effects × assurances × scopes` bounded by `ceiling`. +Presence-required is `assurance ∈ {presence, hardware}` (§3). `default` is +the administrator/packager **opinion surface** (`rm -rf` ships +`{deny, none, once}`), never an auto-grant. Scopes are drawn from the +profile's declared set (§2.0). **WF-1 (well-formedness, not a law):** every matrix MUST be decidable with -all escalations unrendered — `verbs × scopes` non-empty and sufficient (a -harness with no audit surface renders a complete chooser by omission). +all escalations unrendered — the axis products are non-empty and sufficient. ### 2.3 Decision + gate acceptance ```json -{ "v": 1, "request": "cid:…", "grant": { "verb": "allow", "scope": "once" }, +{ "v": 1, "request": "cid:…", + "grant": { "effect": "allow", "assurance": "presence", "scope": "once", + "discharge": { "challenge": "cid:…", "attempt": "…" } }, // if assurance>none "by": "b3:…", "sig": "…" } +{ "v": 1, "request": "cid:…", "escalate": "audit", "by": "b3:…", "sig": "…" } ``` -`escalate` carries **zero authority** (L4); exactly one of `grant`/ -`escalate`. **Gate acceptance is a checklist of MUSTs — the client is never -trusted:** +`escalate` carries **⊥ authority** (L4); exactly one of `grant`/`escalate`. +A `grant` is a point in the Authority lattice. **Gate acceptance is a +checklist of MUSTs — the client is never trusted:** 1. `request` = the content-CID of the request the gate itself issued; -2. `grant.verb ∈ matrix.verbs` and `grant.scope ∈ matrix.scopes`; -3. the result is `⊑` the request ceiling (L4) — never answer `once` with - `always`; +2. each axis of `grant` is a member of the matrix's declared axis sets; +3. `grant ⊑ ceiling` (L4, componentwise) — no axis exceeds the request ceiling; 4. the executable effect recomputes equal to `action.effect` (P5); -5. if `verb == attest`, the discharge verifies **and** its history witness - passes the forward-only ratchet (§3). - -A surface violating any is refused at the gate — the wire *enforces* L4, it -does not merely state it. +5. if `grant.assurance > none`, the discharge verifies **and** its history + witness passes the forward-only ratchet (§3). -### 2.4 Vocabulary (one word, one job — OB-9) +A surface violating any is refused — the wire *enforces* L4. -Three registers were drifting; they are pinned here: +### 2.4 Vocabulary (one word, one job — OB-9/OB-12) | Term | Register | Meaning | |---|---|---| -| `allow` / `attest` / `deny` | **matrix verb** — what a surface *offers* the human | `allow` maps to verdict `approve`; `attest` to `attest`; `deny` to `deny` | -| `deny ⊏ attest ⊏ ask ⊏ approve` | **verdict** — resolution's codomain | the durable disposition `resolve` yields | -| `ask` | **verdict only** (never a matrix verb) | "no durable rule → interact"; the `resolve(∅)` default (L1); fail-closed via L3 | -| `escalate` | **Decision action** (never a verdict) | navigation to a richer surface; `authority(escalate) = ⊥` | +| `Effect` (`deny`/`allow`) | authority axis | *what* is permitted; `deny = ⊥` | +| `Assurance` (`none`/`presence`/`hardware`) | authority axis | *how strongly proven present*; the old `attest` = `presence` | +| `Scope` (`once`/`session`/`durable`) | authority axis | *how long* the grant covers (profile-declared order) | +| `NeedsDecision` | **control-flow result**, not authority | "no rule settled it → interact"; headless ↦ `deny` (L3); never granted | +| `escalate` | **Decision action**, not authority | navigation to a richer surface; `authority(escalate) = ⊥` | -So `allow` (offer) and `approve` (verdict) are the same disposition seen -from the two sides of the seam; `ask` is a verdict, not an offer; `escalate` -is neither. +There is no `ask` verdict and no `attest` verb any more: `ask` became the +control-flow `NeedsDecision`; `attest` became `Assurance = presence`. -## 3. Attest discharge + the forward-only ratchet +## 3. Presence discharge + the forward-only ratchet -An `attest` grant is **inert until a presence proof is verified**, and the -proof *also witnesses a non-regressing history*: the same finger-press -attests "I approved R" **and** "the world had not regressed when I did." +A grant with **`Assurance > none`** (formerly "attest") is **inert until a +presence proof is verified**, and the proof *also witnesses a non-regressing +history*: the same finger-press attests "I approved R" **and** "the world had +not regressed when I did." (`presence` = a WebAuthn/FIDO2 user-presence +discharge; `hardware` additionally requires a bound hardware/measurement +attestation, P5 §4.1.) **Four roles, deliberately separate (OB-3).** The WebAuthn authenticator does not understand a Merkle DAG — it signs over a client-data hash. So do @@ -185,38 +231,43 @@ Five laws. Nothing enters this section without a proof obligation (§5); everything else is mechanism (a profile) or well-formedness. ### L1 — Resolution is a meet -Verdicts are ordered by restrictiveness (`deny ⊏ attest ⊏ ask ⊏ approve`); -`deny` is ⊥, `approve` is ⊤. Resolution is the meet of the matching -verdicts, **with the no-match case defined explicitly** so it is total *and* -fail-closed (OB-9): +Authority is the **product meet-lattice** `Effect × Assurance × Scope` (§2.0); +`⊓` is componentwise; `⊥ = (deny,none,once)`. Resolution meets the matching +rules, **with the no-match case an explicit control-flow result** so it is +total *and* fail-closed (OB-9): ``` -resolve(R, q) = ⨅ { verdict(r) | r ∈ R, r matches q } if some rule matches q - = ask if none matches q +resolve(R, q) = Decided( ⨅ { authority(r) | r ∈ R, r matches q } ) if some rule matches + = NeedsDecision if none matches ``` -The explicit `ask` default is load-bearing: the empty meet's mathematical -identity is `⊤ = approve`, so *without this clause an unmatched request -would fail OPEN* — the defect L3 forbids. `ask` (→ prompt; headless → deny, -L3) is the deliberate "unknown ⇒ interactive, never granted" default. ⨅ is +The explicit `NeedsDecision` default is load-bearing: the empty meet's +identity is `⊤ = (allow,hardware,durable)`, so *without this clause an +unmatched request would fail OPEN*. `NeedsDecision` is not an authority — it +routes to a surface (headless ↦ `deny`, L3). Componentwise `⊓` is associative, commutative, idempotent ⇒ resolution is independent of -rule/file/load order; no ordering attack exists. **PO-1** (now includes -`resolve(∅,q) = ask`, i.e. no fail-open). *(The `attest`-factorization fork -— one verb axis vs. `effect × assurance × scope` — is a lattice-shape -choice; L1 survives either, since a product of lattices is a lattice. -Author's call; see README.)* +rule/file/load order; no ordering attack. **PO-1** (includes +`resolve(∅,q) = NeedsDecision`, i.e. no fail-open; product-of-lattices is a +lattice, so the meet laws lift componentwise). ### L2 — Tamper-boundedness For any mutation `m` by a party holding **fewer than quorum(target)** keys: ``` -resolve(m(R), q) ⊑ resolve(R, q) (no widening) -LoadBearing(R) ⊆ LoadBearing(m(R)) (no structural narrowing) +resolve(m(R), q) ⊑ resolve(R, q) (no widening, any query) +TrustedStructure(m(R)) = TrustedStructure(R) (no authority-generating change) ``` Two directions, one law. **Downward:** sub-quorum actors only narrow -authority; forged loosening entries drop at load (fail-closed). **Upward:** -sub-quorum actors cannot shrink the load-bearing identity structure — -reversible narrowing (deny-spam) is a nuisance; **irreversible narrowing -(revoking an identity) requires quorum** (P4), because a fail-closed -system's failure mode is an adversary who can *force* closure. Availability -is a security property. +authority; forged loosening entries drop at load (fail-closed). **Structural +(strengthened, OB-16/#5):** the *authority-generating structure* — pinned +principals, enrolled devices, trusted issuers, blessed anchors, delegation +edges — must be **unchanged**, not merely un-shrunk. The old `⊆` only forbade +*removal*; it permitted a sub-quorum actor to *add* a trusted issuer that +widens no query today but **issues authority later** — a time-delayed +privilege escalation that passes the per-query monotonicity test. Equality +closes it: any add *or* remove of authority-generating structure is a +quorum-gated operation (P4). Reversible authority *narrowing* (deny-spam) +stays a nuisance; **irreversible narrowing (revoking an identity) and any +structural change require quorum**, because a fail-closed system's failure +mode is an adversary who can *force* closure. Availability is a security +property. **H1 (append-only-verifiability + monotone freshness)** is discharged *by mechanism*, split across P2: interior integrity by the chain (**PO-2a**), tail/fork by the external anchor (**PO-2c**), revocation-quorum by P4 @@ -224,14 +275,16 @@ tail/fork by the external anchor (**PO-2c**), revocation-quorum by P4 over-claimed in v0.1.x and corrected — see README teeth Tier 3.) ### L3 — Fail-closed totality -`resolve` is total; no input reaches "undefined permission." Interactive -bottom is `ask`; absent a bound surface, `ask ↦ deny`, `attest ↦ deny` -(degradation is ⊑-monotone). **PO-3.** +`resolve` is total; no input reaches "undefined permission." The +non-authority result is `NeedsDecision`; absent a bound surface it degrades +to `deny` (`⊥`-Effect), and any un-discharged `Assurance > none` grant also +degrades to `deny` (degradation is ⊑-monotone). **PO-3.** ### L4 — Attenuation -`effective = granted ⊓ required`; `granted = requested ⊓ ceiling`; -`authority(escalate) = ⊥`. Authority composes by meet, never amplifies -(property-tested upstream as `meet_never_amplifies`). **PO-4.** +`effective = granted ⊓ required`; `granted ⊑ ceiling` (componentwise, §2.0); +`authority(escalate) = ⊥`. Authority composes by componentwise meet, never +amplifies on any axis (property-tested upstream as `meet_never_amplifies`, +lifted to the product). **PO-4.** ### L5 — The ceremony gate `association(peer) ⇒ pinned(fingerprint(peer))`. `fingerprint = H(pubkey)` @@ -288,11 +341,16 @@ without a proof obligation; everything else is a profile or a well-formedness predicate.** The count history: six → five (L6 → WF-1); then held at five through the revocation-DoS absorption (L2 upward), the Memo/multihash discipline (WF-2/P1), two adversarial security reviews -(rounds 4–5, eight findings closed as enforcement/mechanism/wire-discipline -— L2's H1 *corrected*, not multiplied), and the forward-only ratchet. +(rounds 4–5), the forward-only ratchet, and the **v0.3.1 protocol freeze** +(review 7): seven findings closed as *type freeze* (OB-12: `Effect × +Assurance × Scope`, `ask`→control-flow, explicit ceiling), *law correction* +(OB-16: L2 upward now **equality** over authority-generating structure, not +`⊆` — closing latent-injection escalation), and *mechanism/wire discipline* +(OB-13 signed grammar, OB-14 genesis, OB-15 CAS append, OB-17 P3/P4 +revocation, OB-18 P5 token+secrets). **Still five laws** — the frozen product +authority type is a *carrier* change, not a new law; L1/L4 lift componentwise. **Next candidate:** L1+L4 unify as one "authority composes by meet" law on -two carriers (verdict + caveat lattices) — five → four if the Lean -formulation collapses them cleanly. +the product carrier — five → four if the Lean formulation collapses them. The algebra decides the count; ambition doesn't. diff --git a/docs/spec/chain-store-profile.md b/docs/spec/chain-store-profile.md index 9bfe94f..33c94eb 100644 --- a/docs/spec/chain-store-profile.md +++ b/docs/spec/chain-store-profile.md @@ -34,7 +34,10 @@ struct AuthorityCheckpoint { // the protected anchor state (P2 §4) Normative definitions: - **`store_id`** — a content-addressed identifier of the store's genesis (its root record's CID); every signed record's domain tuple binds it - (P1 §4·5), so a record cannot be replayed into a different store. + (P1 §4·5), so a record cannot be replayed into a different store. Genesis + is non-circular via the `STORE_ID_SELF` sentinel (P1 §2, OB-14): the + genesis body binds `0x00`, and its own cid *becomes* the `store_id` every + later record binds. - **`thread_id`** — the identifier of one authority thread within a store; a causal thread is the totally-ordered sequence of authority records a single principal-scope advances. A thread MAY be forked (declared) and @@ -43,23 +46,44 @@ Normative definitions: - **`length`/`sequence`** — the dense per-thread record count, *not* longest-path depth. -**Equivocation, now well-defined:** two validly-signed records for the same -`(store_id, thread_id, sequence)` with different CIDs are incontestable -proof of misbehavior (§4). This is the branching-safe restatement of the CT -fork rule. - -"Extends" for the ratchet means: the presented head's thread reaches the -protected `(thread_id, sequence, head)` as a strict-ancestor prefix — a -*forward extension of the same spine*, never a sibling. This chain-store is -the authority projection of the Conversation Graph — same `MerkleNode` -structure, payload `T` = authority records here, conversation records there. -Deliberately *not* a blockchain: no mining, tokens, leader election, or -global total order. - -*(Alternative considered and rejected as harder to reason about: a -frontier-based checkpoint `{store_id, frontier: Set}`. The linear -spine is chosen for provability; revisit only if genuine concurrent -authority within one thread becomes a requirement.)* +This chain-store is the authority projection of the Conversation Graph — +same `MerkleNode` structure, payload `T` = authority records here, +conversation records there. Deliberately *not* a blockchain: no mining, +tokens, leader election, or global total order. + +## 1.1 Append arbitration (OB-15/#4) + +**FROZEN for v0.3.1 — compare-and-swap on the expected head** (author's +decision). `Append(expected_head, next)` commits `next` iff the thread's head +still equals `expected_head`; a loser **retries**. Single-writer/optimistic; +the simplest model that closes the honest-race hole the review flagged. + +**Refined equivocation (this part is load-bearing regardless of the append +model):** a concurrent **candidate** record is NORMAL, and a **CAS-loser** +(one that lost the race to commit a parent) is **benign** — never evidence of +misbehavior. Equivocation is *two records both **committed** at the same +`(store_id, thread_id, sequence)` with different CIDs* — a genuine +double-commit / fork, not a race a writer lost. An honest device that reads a +head, proposes a successor, and loses the CAS is retrying, not attacking. + +> **Roadmap: CAS now → Byzantine Vertical Paxos next (adopted sequencing).** +> CAS is the threshold-1 steady-state case of a **vertically-reconfigurable** +> store: after the P1/P2/P0 waist is proven on CAS, P2 evolves to separate a +> lean *steady-state* protocol from a stronger *reconfiguration* mechanism — +> the **wedge**: fence the active configuration, capture a safe *closing +> state* (preserving acknowledged-but-partial commands), certify the next +> configuration, resume. This makes concurrent candidates normal, gives a +> 2-full-node deployment safe failover via a reconfiguration participant +> (`f+1` steady / `2f+1` reconfiguration), and makes the partition-authority +> ceiling operation-sensitive on the §2.0 lattice. Key-custody stays a +> *separate* threshold. **Reference implementation: (Byzantine) Vertical +> Paxos — Abraham & Malkhi, IBM Zurich DCCL; VP orig. Lamport-Malkhi-Zhou, +> PODC 2009.** The wedge/closing-state correctness gets its own ADR + TLA+/Lean +> model before it is normative — it is *not* part of the v0.3.1 freeze. + +The forward-only ratchet's "extends" means: the presented committed head is a +forward extension of the protected one on the same `(store_id, thread_id)` +spine, `sequence` strictly greater — never a sibling. ## 2. Two CIDs per record @@ -121,13 +145,13 @@ canonical layers, ascending assurance: cosignature no older than its freshness policy (CT gossip / STH). Defeats *secret* equivocation: to fool a victim the attacker must fork the witnesses too. -3. **Fork = proof of misbehavior (REQUIRED).** Two validly-signed records - for the same `(store_id, thread_id, sequence)` with different CIDs are - incontestable evidence of equivocation (§1; the branching-safe restatement - of RFC 6962). Implementations MUST halt authority minting from that store - and escalate — never silently pick one. (Legitimate concurrent - *conversation* branches are not equivocation; they merge before the next - authority record, so they never collide on a `sequence`.) +3. **Fork = proof of misbehavior (REQUIRED) — at the CERTIFIED layer only + (OB-15).** Two conflicting *quorum-certified* heads for the same + `(store_id, thread_id, config_epoch)`, or one voter's conflicting + certification votes, are incontestable evidence (§1.1; branching-safe + restatement of RFC 6962). Implementations MUST halt authority minting and + escalate — never silently pick one. **Concurrent Candidates and + CAS-losers are NOT forks** — only the certified frontier can equivocate. For a solo user (P3's n=2 world) the monotonic head lives on each enrolled device and each device is the other's witness — the same k-of-n substrate diff --git a/docs/spec/enrollment-protocol.md b/docs/spec/enrollment-protocol.md index 14525de..216d034 100644 --- a/docs/spec/enrollment-protocol.md +++ b/docs/spec/enrollment-protocol.md @@ -1,6 +1,7 @@ # P3 — Enrollment Protocol -**Layer:** 2. **Depends on:** P0 (L5), P1, P2. +**Layer:** 2. **Depends on:** P0 (L5), P1, P2, **P4** (it produces P4 +`PinRecord`s and its revocation predicate is P4's — OB-17). **Status:** DRAFT. **Teeth:** this profile is a **protocol**, not an algebra — its correctness is *symbolic protocol analysis* (Tamarin / ProVerif under Dolev-Yao), **Tier 2**, not Lean/Aeneas. A flawless lattice @@ -76,8 +77,15 @@ guess probability; independent witnesses multiply the channels an attacker must own *simultaneously*. The completed enrollment is a `PinRecord` (P4) whose payload carries the ceremony parameters. -**Punting ≥ pinning:** revocation (P4) is graded on the same scale — -removing an identity demands at least the strength that enrolled it. +**Revocation is an exact predicate, not a scale (OB-17/#6).** An earlier +draft said "punting ≥ pinning" — revocation graded on the enrollment-strength +scale. That contradicts P4, and rightly: the strength tuple `(SAS entropy × +rounds, witnesses, presence)` is **not totally ordered**, so "≥" is +undefined. The normative rule is P4's: **each `PinRecord` names, at enrollment +time, the exact `RevocationPolicy` CID required to later punt that identity**, +and revocation compares against *that* recorded predicate. Enrollment's job +here is only to *record* the required policy CID in the pin; P4 owns its +evaluation. ## 3. External anchors (corroboration, never the root) diff --git a/docs/spec/rendering-security-profile.md b/docs/spec/rendering-security-profile.md index fcb503f..2ae982c 100644 --- a/docs/spec/rendering-security-profile.md +++ b/docs/spec/rendering-security-profile.md @@ -41,7 +41,7 @@ are sealed: | `container` | image **digest**, never a mutable tag | | `repo` | repository identity **+** commit/tree CID | | `network` | destination policy **+** an explicit DNS-resolution policy | -| `env`/`creds` | the resolved values, or a named residual (below) | +| `env`/`creds` | an immutable **secret-*version* reference** / one-shot capability / keyed commitment — **NEVER raw secret material** (OB-18) | Anything left ambient at approval time — symlink targets, DNS answers, mutable files, env vars, container tags, credentials resolved *after* @@ -49,6 +49,15 @@ approval — is a **named residual**, never silently folded into PO-W. If a class cannot freeze a resource, it says so; it does not pretend the signature covered it. +**Never put raw secret material in a signed record (OB-18/#7).** The record +is content-addressed, permanently retained, replicated, and offline-guessable +against its CID — so a raw credential in the `body` is a permanent disclosure +(and lands in logs). An authority record MAY reference a secret only as an +immutable **secret-version id**, a **one-shot capability**, or a **keyed +commitment** the gate later redeems; the material itself resolves at +execution time, outside the record. A verifier rejects a body carrying an +apparent raw secret in a credential field. + ## 2. Gate-signed requests (the phishing-canvas closure) A **remote** surface MUST verify the request's signature and that `by` @@ -83,17 +92,24 @@ different measurement and fails the check. This is remote attestation (Parno et al.); its primitives are a Tier-1 assumption, its handshake a Tier-2 (protocol) obligation. -**4.2 Rendering attestation — what the surface DID.** A **signed render -transcript**: the surface signs the exact bytes it presented, bound to -`request_cid` and the deterministic `display`-from-`effect` output (§1). The -faithfulness *ceremony* injects a challenge a faithful renderer passes and -an unfaithful one cannot: the gate derives a **witness token** from -`(effect, nonce)` that MUST appear verbatim in the canonical `display` the -human confirms. A surface that truncated, reformatted, localized-away, or -hid part of the effect cannot produce a `display` containing the correct -token — so the token is absent or wrong, the human sees the mismatch, and -the presence discharge (which covers the token) fails. "Did the surface show -the whole truth?" becomes checkable rather than assumed. +**4.2 Rendering attestation — what the surface DID (corrected, OB-18/#7).** +The mechanism is a **byte-comparison against a gate-produced canonical +render**, *not* a token that "proves" faithful display. Because `display` is +a deterministic function of `effect` (§1), **the gate produces the canonical +render bytes itself**; the surface's signed render transcript commits to the +bytes it presented; the verifier **byte-compares** the committed render +against the gate's canonical render. Equal ⇒ the surface emitted the right +bytes. + +The **witness token** (a value derived from `(effect, nonce)` that must +appear in `display`) is retained only as an **attention-binding aid** — it +nudges the human to look — and is explicitly *not* proof of faithful +rendering: a malicious renderer can print a misleading partial summary *and* +include the correct token. So the token proves *possession of effect-derived +information*, never *that the human saw the whole effect*. Even the +byte-comparison proves only *what bytes were emitted*, not *what pixels a +possibly-hostile display painted or what the human understood* — §5's +irreducible residual. Both compose with the existing gate checklist: high-ceiling actions MAY require a blessed measurement (4.1) **and** a token-bearing render transcript @@ -128,7 +144,7 @@ authority-bearing protocol" failure the security reviews caught. | PO-RES | each action class seals resource *identities*, not mutable handles (OB-10) | 3 + vectors | | — | render-swap / phishing-canvas closures | 3 (with P0 gate checklist) | | PO-SB | binary attestation: a decision above ceiling C is honoured only from a blessed surface measurement | 2 (protocol) | -| PO-SR | rendering attestation: the witness token is present in `display` ⇒ the surface showed the whole effect | 2 (protocol) | +| PO-SR | rendering attestation: committed render bytes == gate's canonical render (byte-compare) ⇒ correct bytes emitted (NOT faithful pixels; token is only an attention aid) | 2 (protocol) | | residual | display-faithfulness on a hostile display | *shrunk by §4, not eliminated — stated* | ## Relations diff --git a/docs/spec/signed-object-profile.md b/docs/spec/signed-object-profile.md index 60e9841..f443fc5 100644 --- a/docs/spec/signed-object-profile.md +++ b/docs/spec/signed-object-profile.md @@ -28,31 +28,58 @@ contradictory, because a recipient handed JSON has not received the signed DAG-CBOR bytes, and converting JSON → DAG-CBOR *is* a reserialization. The resolution: **the canonical DAG-CBOR bytes are carried, not reconstructed.** -Every authority-bearing object travels in a **signed envelope** whose -`body` is the exact canonical bytes that were hashed and signed: +**The signed-object grammar is ONE normative constructor (OB-13/#2).** A +naive envelope that signs only `cid = H(body)` leaves the interpretation- +bearing fields (`profile`, `codec`, `signer`) *outside* the signature — an +attacker could re-tag the codec or profile without breaking the sig. So the +signature covers a canonical **protected tuple**, not the bare CID: + ```json -{ "profile": "agent-bridle/permission-request/v1", - "codec": "dag-cbor", +{ "profile": "agent-bridle/permission-request/v1", // protected + "codec": "dag-cbor", // protected + "cid": "cid:…", // = H(body); protected + "signer": "b3:…", // protected "body": "", - "cid": "cid:…", // = H(body) - "by": "b3:…", "sig": "…" } // sig over cid + "sig": "…" } ``` -Verification is then unambiguous and identical in all four client languages: ``` -1. allowlist the codec and algorithms (§4·4) — before touching body -2. verify cid == H(body) — CID over the bytes -3. verify sig over cid under `by` -4. decode body → typed value -5. check schema + critical fields; reject unknown authority-bearing fields -6. construct Sealed +protected = canon( "agent-bridle/signed-object/v1", // domain separation + profile, codec, cid, signer ) +sig = Sign(signer, protected) ``` + +Normative bindings (no implementer choice left): +- **`sig` covers `protected`** — profile, codec, cid, signer are all + authenticated; changing any breaks the signature. +- **the body's own domain tuple** (§4·5) MUST equal `(profile, codec)` of the + envelope, and its `signer`/`by` MUST equal the envelope `signer` — an + implementation rejects any mismatch (no split identity across the boundary). +- `signer` is the one canonical location for the key id; `by` in inner-record + examples (P0/P4) is the *same* value surfaced logically, never a second + authority. +- verification (identical in all four languages): + ``` + 1. allowlist codec + algorithms (§4·4) — before touching body + 2. recompute protected; verify sig over it under signer + 3. verify cid == H(body) + 4. decode body; check body domain tuple == (profile, codec, signer) + 5. schema + critical fields; reject unknown authority-bearing fields + 6. construct Sealed + ``` + +**Genesis is non-circular (OB-14/#3).** P2 defines `store_id = H(genesis +record)`, but every body must bind `store_id` (§4·5) — a fixed point at +genesis. Resolution (normative): **the genesis body carries the reserved +sentinel `store_id = 0x00` (`STORE_ID_SELF`); its resulting `cid` becomes the +store's `store_id`**, and every *subsequent* record binds that value. A +verifier resolves `STORE_ID_SELF` to "this record's own cid." The first +ordinary record can therefore be canonically encoded. + **JSON and TOML are views/containers, never independent authority-bearing -serializations.** A JSON rendering of an object is for humans and -non-authority interchange; the *authority* lives in `body`. TOML policy -files (#220) at rest likewise wrap the signed `body`. Signatures and -`ContentId`s are over `body` only. Wall-clock is never a coordination -primitive — validity keys on generation counters; RFC 3339 timestamps are -provenance *data*, never read by a kernel. +serializations.** A JSON rendering is for humans/non-authority interchange; +the *authority* lives in `body` + `protected`. TOML policy files (#220) wrap +the signed object. Wall-clock is never a coordination primitive — validity +keys on generation counters; RFC 3339 timestamps are provenance *data*. ## 3. The Memo discipline (WF-2) diff --git a/docs/spec/suite.toml b/docs/spec/suite.toml index 6c549ec..961507c 100644 --- a/docs/spec/suite.toml +++ b/docs/spec/suite.toml @@ -3,10 +3,10 @@ # When a profile version bumps, update its pin here and re-run conformance. suite = "ceremony-suite" -suite_version = "0.3.0" +suite_version = "0.3.1" # PROTOCOL FREEZE — authority type + wire grammar frozen [requires] -p0 = "0.2" # ceremony-contract — the narrow waist (five laws) +p0 = "0.3" # ceremony-contract — the narrow waist (five laws; Authority = Effect×Assurance×Scope) p1 = "1" # signed-object — CID/canon/Sealed/allowlist/envelope p2 = "0.2" # chain-store — causal transcript + anti-rollback anchor p3 = "0.2" # enrollment — SAS/PoP/anchors