proof-aware-crypto-tooling-.../notebooks/10_verified_custody_wallet.ipynb

365 lines
16 KiB
Text
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Lecture 10: The Verified-Custody Wallet (warden)\n",
"\n",
"Everything so far *decided* which cryptographic code to trust.\n",
"This lecture *acts* on the decision: we build a custody boundary\n",
"out of the four proven curve25519-dalek forks and use it to\n",
"guard signatures - inbound and outbound.\n",
"\n",
"The one idea: **inbound acceptance requires a unanimous quorum of\n",
"provably-equivalent verifiers, and every outbound signature must\n",
"pass the same quorum before it is released.**\n",
"\n",
"We keep the course's ratchet rule: every load-bearing idea runs\n",
"twice - napkin scale by hand, then real scale against the live\n",
"system - and both are executable here.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Learning Objectives\n",
"\n",
"- Explain why a *unanimous* quorum of provably-equivalent\n",
" verifiers turns disagreement into evidence of a fault, and why\n",
" majority voting would hide exactly that fault.\n",
"- Classify a quorum divergence as a documented semantic edge\n",
" (note) versus unexplained (tamper -> latch).\n",
"- Describe the outbound signing firewall as verify-after-sign\n",
" with a proven verifier, and state warden's honest asymmetry\n",
" (verify custody-grade, sign trusted base).\n",
"- Recompute a custody card's inclusion proof as a counterparty -\n",
" trust by recomputation, not by assertion.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Why a quorum, when one proof would do?\n",
"\n",
"Each member is *proven* to decide the same predicate,\n",
"`accept(A,m,R,s) \u21d4 decompress(R) = [k](\u2212A) + [s]B`. So on the\n",
"proven domain they cannot disagree about *meaning*. Classic\n",
"N-version programming hopes independent code won't share a bug;\n",
"we do not hope - we know the semantics coincide, so a runtime\n",
"disagreement is not opinion, it is **evidence of a fault**: a\n",
"corrupted build, a memory error, or tampering. The quorum turns\n",
"\"the verifiers differed\" into an alarm with a theorem behind it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Napkin scale: a 3-of-3 quorum with toy verifiers\n",
"\n",
"Forget real curves for a moment. Model three verifiers as\n",
"functions and watch the boundary logic: unanimity accepts,\n",
"any disagreement fails closed and is classified.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def toy_quorum(verdicts):\n",
" kinds = set(verdicts.values())\n",
" if kinds == {\"accept\"}:\n",
" return \"unanimous-accept\", True\n",
" if kinds == {\"reject\"}:\n",
" return \"unanimous-reject\", False\n",
" return \"divergence -> FAIL CLOSED + incident\", False\n",
"\n",
"print(toy_quorum({\"dalek\": \"accept\", \"anza\": \"accept\", \"risc0\": \"accept\"}))\n",
"print(toy_quorum({\"dalek\": \"reject\", \"anza\": \"reject\", \"risc0\": \"reject\"}))\n",
"print(toy_quorum({\"dalek\": \"accept\", \"anza\": \"reject\", \"risc0\": \"accept\"}))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The third line is the whole point: a lone dissenter does not get\n",
"out-voted. Acceptance needs *everyone*; anything else is a\n",
"refusal plus a recorded incident. Majority voting would hide\n",
"exactly the fault we most want to see.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The divergence taxonomy\n",
"\n",
"The forks are *allowed* to differ on documented degenerate\n",
"inputs (anza rejects `A = 0` and a legacy excluded-small-order-R\n",
"list). We still fail closed; the taxonomy only grades the alarm:\n",
"\n",
"- **semantic-edge** - they differ AND a documented edge flag\n",
" applies (small-order R, non-canonical s, zero key): severity\n",
" *note*.\n",
"- **unexplained** - they differ with no documented reason, or a\n",
" member errored: severity *tamper* -> custody **latches**.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib\n",
"for parent in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (parent / \"src\" / \"pacta\").exists():\n",
" sys.path.insert(0, str(parent / \"src\")); ROOT = parent; break\n",
"\n",
"from pacta.quorum import semantic_edge_flags, SMALL_ORDER_ENCODINGS\n",
"\n",
"small_order_R = sorted(SMALL_ORDER_ENCODINGS)[0]\n",
"print(\"edge flags for a small-order R:\",\n",
" semantic_edge_flags(b\"\\x02\" * 32, small_order_R + b\"\\x00\" * 32))\n",
"print(\"edge flags for an ordinary sig:\",\n",
" semantic_edge_flags(b\"\\x02\" * 32, b\"\\x01\" * 64))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A divergence on the first input is a documented edge (note); a\n",
"divergence on the second has no excuse (tamper). Same fail-closed\n",
"verdict, very different alarm.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Real scale: the four proven forks, if built\n",
"\n",
"If you have run `pacta wallet build-quorum`, the next cell drives\n",
"the **real** four-fork quorum: sign a payload with the dogfood\n",
"(attested) signer, then watch all four proven verifiers agree on\n",
"accept, and on reject for a flipped byte. If the binaries are not\n",
"built, we say so and skip - honestly, the way the wallet itself\n",
"fails closed.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pacta.quorum import load_quorum, binary_path\n",
"\n",
"built = [b for b in (\"dalek\", \"anza\", \"risc0\", \"betrusted\") if binary_path(b).exists()]\n",
"if len(built) < 2:\n",
" print(\"quorum not built (need >=2). Run: pacta wallet build-quorum --sources-root <...>\")\n",
"else:\n",
" import tempfile, os\n",
" from pacta.dogfood import locate_verifier, pem_public_key_to_raw, sign_payload_dogfood\n",
" from pacta.signing import generate_ed25519_keypair\n",
" v = locate_verifier()\n",
" if v is None:\n",
" print(\"dogfood signer not built; run pacta dogfood-build\")\n",
" else:\n",
" d = tempfile.mkdtemp()\n",
" key, pub = os.path.join(d, \"k.pem\"), os.path.join(d, \"k.pub\")\n",
" generate_ed25519_keypair(key, pub)\n",
" payload = b\"curriculum lecture 10 payload\"\n",
" sig = sign_payload_dogfood(payload, key, v)\n",
" pk = pem_public_key_to_raw(pub)\n",
" q = load_quorum(min_members=2)\n",
" print(\"members:\", sorted(q.members))\n",
" good = q.verify(payload, sig, pk)\n",
" print(\"valid signature ->\", good.classification, \"accepted =\", good.accepted)\n",
" bad = q.verify(payload, bytes([sig[0] ^ 0xFF]) + sig[1:], pk)\n",
" print(\"one flipped byte ->\", bad.classification, \"accepted =\", bad.accepted)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The signing firewall: verify-after-sign, but proven\n",
"\n",
"Outbound is `intent -> sign -> firewall -> release`. The fresh\n",
"signature faces the same quorum; only unanimity releases it. A\n",
"rejected self-signature is *quarantined, never returned*, and\n",
"custody latches. This is the textbook fault-injection\n",
"countermeasure - verify a signer's output before trusting it -\n",
"with the verifier upgraded to machine-checked code.\n",
"\n",
"Note the honest asymmetry: the *verify* paths are certificate-\n",
"covered (custody-grade), but the *signing* step is trusted base -\n",
"the attested artifact, not a third implementation. The firewall\n",
"is exactly how we fence that weaker edge.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Two voices, one boundary (the domain split, again)\n",
"\n",
"Lecture 06 split provider and agent. warden inherits the split:\n",
"\n",
"- **The operator voice** seals the capsule: it runs the R4 gate,\n",
" pins the attested source commits, and stores the transparency-\n",
" log receipts that authorized each member.\n",
"- **The counterparty (agent) voice** never trusts the operator's\n",
" adjectives. It reads the *custody card* and recomputes the\n",
" inclusion proofs itself - trust by recomputation.\n",
"\n",
"The next cell is the counterparty side: given a card, verify a\n",
"member's inclusion proof with nothing but stdlib hashing.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Counterparty-side check of ONE member's inclusion proof.\n",
"# (Works whenever you have a wallet + its fetched evidence; here\n",
"# we show the primitive the card relies on.)\n",
"from pacta.transparency import verify_inclusion, leaf_bytes_for_attestation\n",
"import json, glob\n",
"\n",
"ev = sorted(glob.glob(str(ROOT / \"examples\" / \"wallet-evidence\" / \"*.attestation.json\")))\n",
"if not ev:\n",
" print(\"no bundled evidence; fetch with `pacta log-fetch` to try live\")\n",
"else:\n",
" att = json.load(open(ev[0]))\n",
" rec = json.load(open(ev[0].replace(\".attestation.\", \".receipt.\")))\n",
" ok = verify_inclusion(\n",
" leaf_bytes_for_attestation(att),\n",
" rec[\"leaf_index\"], rec[\"tree_size\"],\n",
" [bytes.fromhex(h) for h in rec[\"inclusion_proof\"]],\n",
" bytes.fromhex(rec[\"sth\"][\"root_hash\"]),\n",
" )\n",
" print(f\"{att['subject']['component']}: inclusion recomputes ->\", ok)\n",
" print(\"The counterparty believed no adjective; it recomputed a Merkle root.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Corrupt a member, watch the pin catch it (executable)\n",
"\n",
"The wallet seals each member's SHA-256 into its capsule. The\n",
"next cell stages a COPY of a real member binary in a temp\n",
"directory, \"seals\" its hash the way the capsule does, appends\n",
"one byte (a supply-chain attack in miniature), and re-checks.\n",
"Nothing on your machine is modified.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import hashlib, shutil, tempfile, pathlib\n",
"from pacta.quorum import binary_path\n",
"\n",
"member = binary_path(\"dalek\")\n",
"if not member.exists():\n",
" print(\"quorum not built; run pacta wallet build-quorum first\")\n",
"else:\n",
" stage = pathlib.Path(tempfile.mkdtemp()) / member.name\n",
" shutil.copy2(member, stage)\n",
" sealed = hashlib.sha256(stage.read_bytes()).hexdigest() # capsule pin\n",
" print(\"sealed :\", sealed[:24], \"...\")\n",
" with stage.open(\"ab\") as f:\n",
" f.write(b\"\\x00\") # the attack\n",
" current = hashlib.sha256(stage.read_bytes()).hexdigest()\n",
" print(\"current:\", current[:24], \"...\")\n",
" if current != sealed:\n",
" print(\"PIN CAUGHT IT: wallet.quorum() would refuse to assemble ->\")\n",
" print(\" 'quorum member dalek binary hash changed since the capsule was sealed'\")\n",
" else:\n",
" print(\"impossible: SHA-256 collision\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One appended byte and the wallet refuses to even *assemble* the\n",
"quorum - before any verification runs. Note what this control\n",
"is and is not: it stops binary substitution *between* wallet\n",
"sessions; an attacker with live root outranks it (see\n",
"docs/threat-model.md, attacker #7 - that is what the choir and\n",
"the airgap profiles are for).\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Exercises\n",
"\n",
"- Change `toy_quorum` to majority voting and write two sentences\n",
" on exactly which attack that lets through.\n",
"- Extend the corrupt-a-member cell: corrupt the capsule JSON\n",
" itself instead of the binary. What catches that, and when?\n",
" (Hint: nothing does until the ledger genesis is compared -\n",
" write down the exact trust statement the capsule hash in the\n",
" genesis entry provides.)\n",
"- The signing path is trusted base. Write the strongest *true*\n",
" sentence you can about warden's outbound safety, and the\n",
" strongest *false* one a marketer would write - and name the\n",
" word that makes the second one false.\n",
"- Design `warden-treasury`: which member re-verifies Solana\n",
" transactions, and what exactly the RPC provider is still\n",
" trusted for after you do.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The human surface: see this wallet through the cockpit\n",
"\n",
"Everything this notebook built programmatically has a read-only human console:\n",
"\n",
"```\n",
"pacta wallet cockpit --demo # throwaway demo wallet, zero setup\n",
"pacta wallet cockpit --wallet DIR # the wallet you just sealed here\n",
"```\n",
"\n",
"Open `/deck` for all six role stations live in parallel (the quorum bench you built is the\n",
"indigo pane; the ledger you hash-chained is re-verified on every page load), and `/manual`\n",
"for the lab-manual sessions that teach each role \u2014 Session 4's tamper drill breaks a *copy*\n",
"of a ledger exactly like this notebook's and watches two independent surfaces catch it.\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}