1136f6f812bd
drive thirteen matrix scenarios plus the manifest adversarial cases
- time
- 2026-09-11 00:17 UTC · 1 month ago
- machine
- koh2
- agent
- deepseek/deepseek-v4-flash
- session
- cross-client matrix
- parent
- 4c9c7d9d496d
↓ peach-browser-1136f6f8.tar.gzatlas at this save
files changed1 file
scripts/check-cross-client-sync.mjs+182 −12scripts/check-cross-client-sync.mjs+182 −12
@@ -1,7 +1,8 @@11 import { existsSync, readFileSync, mkdtempSync } from "node:fs";22 import { dirname, resolve } from "node:path";33 import { fileURLToPath } from "node:url";4-import { spawn } from "node:child_process";4+import { execFileSync, spawn } from "node:child_process";5+import { createPrivateKey, createPublicKey, sign as signEd25519Node } from "node:crypto";56 import { tmpdir } from "node:os";67 78 const browserRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");@@ -115,6 +116,11 @@115116 // the live vault (PB-26-112 C).116117 locked: process.env.PEACH_SYNC_HARNESS_LOCKED === "1",117118 stopAfter: process.env.PEACH_SYNC_HARNESS_STOP_AFTER ?? "",119+ // A manifest the harness signed: the wire shape IS the scenario, and one120+ // signer (the harness, in Node) keeps both clients verifying the same bytes121+ // rather than each signing its own.122+ signedManifestJson: process.env.PEACH_SYNC_HARNESS_SIGNED_MANIFEST ?? "",123+ trustedPubkeyHex: process.env.PEACH_SYNC_HARNESS_TRUSTED_PUBKEY ?? "",118124 };119125 120126 const id = mode === "initiator" ? "browser" : "android";@@ -141,6 +147,7 @@141147 const typeOf = (message) => message.type;142148 let outcome = null;143149 let counted = { sent: 0, received: 0, applied: 0, staged: 0 };150+let manifestVerdict = null;144151 const fs = require("node:fs");145152 const writeTranscript = (status) => fs.writeFileSync(transcriptFile, JSON.stringify({146153 status,@@ -149,6 +156,7 @@149156 entriesReceived: counted.received,150157 applied: counted.applied,151158 staged: counted.staged,159+ manifestAccepted: manifestVerdict === null ? null : manifestVerdict === "accepted",152160 events,153161 }));154162 const session = {@@ -178,6 +186,10 @@178186 // is newer for a given index: the same parity on both sides means equal data179187 // versions (nothing to send), opposite parities means each side is newer for180188 // half the vault (entries flow both ways).189+const manifestApi = await import(root + "/src/shared/device-manifest.ts");190+const hexBytes = (hex) => Uint8Array.from(hex.match(/../g).map((pair) => parseInt(pair, 16)));191+const crafted = knobs.signedManifestJson === "" ? null : JSON.parse(knobs.signedManifestJson);192+181193 const ids = Array.from({ length: knobs.entryCount }, (_, index) => "shared-entry-" + index);182194 const mtimeFor = (index) =>183195 knobs.parity === "" ? 10 : (index % 2 === Number(knobs.parity) ? 15 : 10);@@ -208,9 +220,18 @@208220 ...(knobs.manifestSupport && !knobs.locked209221 ? {210222 async buildOutgoingDeviceManifest() {211- return { body: { manifestVersion: syncTypes.DEVICE_MANIFEST_VERSION, issuerDeviceId: id, issuedAt: 1700000000000, devices: [], revoked: [] }, signatureHex: "11".repeat(64) };223+ return crafted ?? { body: { manifestVersion: syncTypes.DEVICE_MANIFEST_VERSION, issuerDeviceId: id, issuedAt: Date.now(), devices: [], revoked: [] }, signatureHex: "11".repeat(64) };212224 },213- async applyIncomingDeviceManifest() {},225+ async applyIncomingDeviceManifest(incoming) {226+ if (!incoming) return;227+ if (knobs.trustedPubkeyHex === "") { manifestVerdict = "not-checked"; return; }228+ try {229+ await manifestApi.verifyDeviceManifest(incoming, hexBytes(knobs.trustedPubkeyHex), {});230+ manifestVerdict = "accepted";231+ } catch {232+ manifestVerdict = "rejected";233+ }234+ },214235 }215236 : {}),216237 };@@ -239,6 +260,68 @@239260 }240261 `;241262 263+/**264+ * Manifest fixtures for the adversarial cases.265+ *266+ * The canonical bytes come from the Browser's *production* canonicaliser (so267+ * the harness cannot drift from the wire contract), and the Ed25519 signature268+ * comes from Node — one signer, both clients verifying the identical bytes.269+ */270+const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");271+const trustedKey = createPrivateKey({272+ key: Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from("11".repeat(32), "hex")]),273+ format: "der",274+ type: "pkcs8",275+});276+const TRUSTED_PUBKEY_HEX = createPublicKey(trustedKey)277+ .export({ format: "der", type: "spki" })278+ .subarray(-32)279+ .toString("hex");280+const UNTRUSTED_PUBKEY_HEX = createPublicKey(createPrivateKey({281+ key: Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from("22".repeat(32), "hex")]),282+ format: "der",283+ type: "pkcs8",284+})).export({ format: "der", type: "spki" }).subarray(-32).toString("hex");285+286+function canonicalBody(body) {287+ return execFileSync("bun", [288+ "--eval",289+ `import { canonicalise } from ${JSON.stringify(resolve(browserRoot, "src/shared/device-manifest.ts"))}; process.stdout.write(canonicalise(JSON.parse(process.env.PEACH_MANIFEST_SPEC)));`,290+ ], { env: { ...process.env, PEACH_MANIFEST_SPEC: JSON.stringify(body) } }).toString();291+}292+293+/** Sign a crafted body; `seed` picks the key, so a bad signature is one argument. */294+function signManifest({ issuerDeviceId = "crafted-issuer", nowOffsetMs = 0, devices = [], revoked = [], seed = "11".repeat(32) } = {}) {295+ const body = {296+ // Read, never assumed: a hard-coded version makes every case reject for the297+ // same reason, and the whole group proves nothing. The control case is what298+ // catches that.299+ manifestVersion: numericConstant(browserSyncTypes, "DEVICE_MANIFEST_VERSION"),300+ issuerDeviceId,301+ issuedAt: Date.now() + nowOffsetMs,302+ devices,303+ revoked,304+ };305+ const key = createPrivateKey({306+ key: Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from(seed, "hex")]),307+ format: "der",308+ type: "pkcs8",309+ });310+ const signature = signEd25519Node(null, Buffer.from(canonicalBody(body), "utf8"), key);311+ return JSON.stringify({ body, signatureHex: signature.toString("hex") });312+}313+314+const deviceEntry = (overrides = {}) => ({315+ deviceId: "crafted-device",316+ deviceName: "Crafted",317+ deviceType: "android",318+ longTermPublicKeyHex: "aa".repeat(32),319+ signingPublicKeyHex: "bb".repeat(32),320+ pairedAt: 1_700_000_000_000,321+ vouchDepth: 0,322+ ...overrides,323+});324+242325 const MAX_DIAGNOSTIC_BYTES = 12_000;243326 244327 function spawnProcess(command, args, env) {@@ -299,7 +382,15 @@299382 * non-zero exit means "my side did not behave as the matrix says it should" and300383 * the scenario is free to describe a failure outcome without special-casing.301384 */302-async function runRound({ initiator, android = {}, browser = {}, timeoutMs = 180_000, label = "", expectFailure = [] }) {385+async function runRound({ initiator, android = {}, browser = {}, timeoutMs = 180_000, label = "", expectFailure = [], manifest = null }) {386+ // A crafted manifest goes to both sides: each sends it and each verdicts the387+ // peer's, so one scenario exercises both clients against identical bytes.388+ // Signed per round, not at module load: a fixture built once drifts out of389+ // its own freshness window while earlier rounds run, and a "+10 minutes in the390+ // future" case silently stops being in the future.391+ const manifestKnobs = manifest392+ ? { SIGNED_MANIFEST: typeof manifest === "function" ? manifest() : manifest, TRUSTED_PUBKEY: TRUSTED_PUBKEY_HEX }393+ : {};303394 const dir = mkdtempSync(resolve(tmpdir(), "peach-cross-sync-"));304395 const androidTranscript = resolve(dir, "android.json");305396 const browserTranscript = resolve(dir, "browser.json");@@ -308,7 +399,7 @@308399 const common = { PEACH_BROWSER_ROOT: browserRoot };309400 const launchBrowser = (mode, port, transcript, portFile) => spawnProcess("bun", ["--eval", browserEndpoint], {310401 ...common,311- ...knobEnv(browser),402+ ...knobEnv({ ...manifestKnobs, ...browser }),312403 PEACH_SYNC_HARNESS_MODE: mode,313404 PEACH_SYNC_HARNESS_PORT: port ?? "",314405 PEACH_SYNC_HARNESS_PORT_FILE: portFile ?? "",@@ -319,7 +410,7 @@319410 ...androidTask,320411 ], {321412 ...common,322- ...knobEnv(android),413+ ...knobEnv({ ...manifestKnobs, ...android }),323414 PEACH_PROCESS_ROOT: androidRoot,324415 PEACH_SYNC_HARNESS_MODE: mode,325416 PEACH_SYNC_HARNESS_PORT: port ?? "",@@ -572,6 +663,31 @@572663 },573664 },574665 {666+ id: 12,667+ name: "manifest with a device vouched too deeply is rejected",668+ initiator: "android",669+ manifest: () => signManifest({ devices: [deviceEntry({ vouchDepth: 99 })] }),670+ android: { MTIME_PARITY: "0", ENTRY_COUNT: "2", EXPECT_STATUS: "COMPLETED" },671+ browser: { MTIME_PARITY: "0", ENTRY_COUNT: "2", EXPECT_STATUS: "completed" },672+ check(round, { completed, expect, manifestVerdict }) {673+ completed(round, { flow: false, manifests: true, label: "scenario 12" });674+ manifestVerdict(round, "scenario 12", false);675+ },676+ },677+ {678+ id: 14,679+ name: "manifest issued outside the freshness window is rejected",680+ initiator: "android",681+ // Ten minutes stale, against a five-minute window.682+ manifest: () => signManifest({ nowOffsetMs: -10 * 60_000 }),683+ android: { MTIME_PARITY: "0", ENTRY_COUNT: "2", EXPECT_STATUS: "COMPLETED" },684+ browser: { MTIME_PARITY: "0", ENTRY_COUNT: "2", EXPECT_STATUS: "completed" },685+ check(round, { completed, expect, manifestVerdict }) {686+ completed(round, { flow: false, manifests: true, label: "scenario 14" });687+ manifestVerdict(round, "scenario 14", false);688+ },689+ },690+ {575691 id: 15,576692 name: "10,000-entry vault with padded frames",577693 initiator: "android",@@ -610,6 +726,14 @@610726 if (!predicate(round[side])) throw new Error(`${label}: unexpected transcript for ${side}: ${JSON.stringify(round[side].events)}`);611727 }612728 },729+ manifestVerdict(round, label, expected) {730+ for (const side of ["android", "browser"]) {731+ const verdict = round[side].manifestAccepted;732+ if (verdict !== expected) {733+ throw new Error(`${label}: ${side} verdict was ${verdict}, expected ${expected}`);734+ }735+ }736+ },613737 side(round, side, label, expected) {614738 const got = JSON.stringify(round[side].events);615739 const want = JSON.stringify(expected);@@ -617,6 +741,23 @@617741 },618742 };619743 744+/**745+ * The §16.3 manifest-v5 adversarial cases: the same bytes offered to both746+ * clients, each verdicting with its own production verification. The last case747+ * is the control — without it, seven rejections would prove only that the748+ * verifier says no to everything.749+ */750+const MANIFEST_CASES = [751+ ["stale issuedAt", () => signManifest({ nowOffsetMs: -10 * 60_000 }), false],752+ ["future issuedAt", () => signManifest({ nowOffsetMs: 10 * 60_000 }), false],753+ ["vouchDepth above the absolute bound", () => signManifest({ devices: [deviceEntry({ vouchDepth: 99 })] }), false],754+ ["device count above the bound", () => signManifest({ devices: Array.from({ length: 257 }, (_, i) => deviceEntry({ deviceId: `d-${i}` })) }), false],755+ ["malformed device key", () => signManifest({ devices: [deviceEntry({ longTermPublicKeyHex: "aa".repeat(31) + "a" })] }), false],756+ ["oversized issuer id", () => signManifest({ issuerDeviceId: "x".repeat(300) }), false],757+ ["signature from an untrusted key", () => signManifest({ seed: "22".repeat(32) }), false],758+ ["valid manifest (control)", () => signManifest({ devices: [deviceEntry()] }), true],759+];760+620761 // PEACH_SYNC_HARNESS_ONLY=13,15 runs a subset. Debugging the matrix one full621762 // pass at a time is otherwise a four-minute loop.622763 const only = (process.env.PEACH_SYNC_HARNESS_ONLY ?? "").split(",").filter(Boolean).map(Number);@@ -634,16 +775,45 @@634775 console.log(` ✓ ${String(scenario.id).padStart(2)} ${scenario.name} (${(elapsed / 1000).toFixed(1)}s)`);635776 }636777 778+// The §16.3 adversarial group. Each case is a full round, so this is the slow779+// half of the gate; it is a release gate, not a pre-commit hook.780+const manifestResults = [];781+if (only.length === 0 || only.includes(-1)) {782+ let index = 0;783+ for (const [name, manifest, expected] of MANIFEST_CASES) {784+ index += 1;785+ const started = Date.now();786+ const round = await runRound({787+ initiator: "android",788+ manifest,789+ label: `§16.3 manifest case ${index} (${name})`,790+ android: { MTIME_PARITY: "0", ENTRY_COUNT: "1", EXPECT_STATUS: "COMPLETED" },791+ browser: { MTIME_PARITY: "0", ENTRY_COUNT: "1", EXPECT_STATUS: "completed" },792+ });793+ round.initiatedBy = "android";794+ helpers.completed(round, { flow: false, manifests: true, label: `§16.3 case ${index}` });795+ helpers.manifestVerdict(round, `§16.3 case ${index} (${name})`, expected);796+ console.log(` ✓ 16.3.${index} ${name} → ${expected ? "accepted" : "rejected"} (${((Date.now() - started) / 1000).toFixed(1)}s)`);797+ manifestResults.push({ name, expected });798+ }799+}800+637801 console.log("");638-console.log(`Cross-client sync process conformance passed: ${results.length} of 15 §7.1 scenarios.`);802+const scope = only.length > 0 ? ` (filtered to ${only.join(",")})` : "";803+console.log(`Cross-client sync process conformance passed: ${results.length} of 15 §7.1 scenarios${scope}.`);639804 for (const result of results) {640805 const timing = result.id === 15 ? ` — 10,000-entry round ${(result.elapsed / 1000).toFixed(1)}s wall clock` : "";641806 console.log(` #${result.id} ${result.name}${timing}`);642807 }643808 console.log("");809+for (const result of manifestResults) { /* listed above with their verdicts */ }810+console.log("");644811 console.log("Not yet covered by this gate (see the campaign §7.1 table):");645-console.log(" #7 android cold-locked — evidence class is unit + real-device, not a process transcript");646-console.log(" #11 revocation then re-vouch, #12 excessive vouch depth, #14 freshness window —");647-console.log(" these need a correctly signed adversarial manifest on the wire; the");648-console.log(" signed-manifest fixture is not built yet");649-console.log(" the adoption-v2 group, and the eight §16.3 manifest-v5 adversarial cases");812+console.log(" #7 android cold-locked — evidence class is unit + real-device, NOT a process");813+console.log(" transcript; the unit half is LockedSyncSpoolModelTest. Do not fake it here.");814+console.log(" #11 revocation then re-vouch — the rule is entry ADMISSION, not manifest");815+console.log(" validity: the Browser enforces it in sync-vault-adapter and Android in");816+console.log(" PairedDeviceStore, both of which need real storage this endpoint has no");817+console.log(" JVM-reachable path to. Its unit coverage exists on both clients; the");818+console.log(" process class needs an adapter-level harness.");819+console.log(" the adoption-v2 group — a separate transcript, not yet driven across the boundary.");