// CAIN-42 TypeScript verifier SDK (E30). Zero dependencies; Node >= 22 (run .ts directly with // `node --experimental-strip-types`, or the tsc-compiled dist/cain-verify.js) or any runtime with WebCrypto Ed25519. // // Verifies, without any CAIN code: canonical JSON digests (CAIN form: sorted keys, no whitespace, ASCII-escaped, // integers only), Ed25519 signatures over domain-separated digests, the E28 governance-receipt hash chain, the E30 // universal-action-receipt chain (including that each receipt's UMA digest matches its published action object), the // E30 autonomy-budget ledger, and RFC 6962 Merkle roots. // // node dist/cain-verify.js artifacts.json (build: tsc -p .) // node --experimental-strip-types cain-verify.ts artifacts.json type Bytes = Uint8Array; // The two Node globals the CLI entry point uses, declared here so the SDK needs no @types/node. declare const process: { argv: string[]; exit(code: number): never }; export type Json = null | boolean | number | string | Json[] | { [k: string]: Json }; const hex = (b: ArrayBuffer | Uint8Array): string => Array.from(b instanceof Uint8Array ? b : new Uint8Array(b)).map((x) => x.toString(16).padStart(2, "0")).join(""); const fromB64 = (s: string): Bytes => Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); function escapeString(s: string): string { let out = '"'; for (const ch of s) { const c = ch.codePointAt(0)!; if (ch === '"') out += '\\"'; else if (ch === "\\") out += "\\\\"; else if (c === 8) out += "\\b"; else if (c === 12) out += "\\f"; else if (c === 10) out += "\\n"; else if (c === 13) out += "\\r"; else if (c === 9) out += "\\t"; else if (c < 0x20 || c > 0x7e) { if (c > 0xffff) { const v = c - 0x10000; out += "\\u" + (0xd800 + (v >> 10)).toString(16).padStart(4, "0"); out += "\\u" + (0xdc00 + (v & 0x3ff)).toString(16).padStart(4, "0"); } else out += "\\u" + c.toString(16).padStart(4, "0"); } else out += ch; } return out + '"'; } export function canonical(o: Json): string { if (o === null) return "null"; if (typeof o === "boolean") return o ? "true" : "false"; if (typeof o === "number") { if (!Number.isInteger(o)) throw new Error("CAIN canonical form refuses floats"); return String(o); } if (typeof o === "string") return escapeString(o); if (Array.isArray(o)) return "[" + o.map(canonical).join(",") + "]"; const keys = Object.keys(o).sort(); return "{" + keys.map((k) => escapeString(k) + ":" + canonical((o as Record)[k])).join(",") + "}"; } export async function sha256hex(data: string | Bytes): Promise { const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data; return hex(await crypto.subtle.digest("SHA-256", bytes)); } export async function digest(domain: string, body: Json): Promise { return sha256hex(canonical({ domain, body })); } export async function verifySig(pubB64: string, sigB64: string, domain: string, body: Json): Promise { try { const key = await crypto.subtle.importKey("raw", fromB64(pubB64), { name: "Ed25519" }, false, ["verify"]); const msg = new TextEncoder().encode(await digest(domain, body)); return await crypto.subtle.verify({ name: "Ed25519" }, key, fromB64(sigB64), msg); } catch { return false; } } async function leaf(b: Bytes): Promise { return new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([0, ...b]))); } async function node(l: Bytes, r: Bytes): Promise { return new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array([1, ...l, ...r]))); } export async function merkleRoot(leaves: Bytes[]): Promise { if (leaves.length === 0) return new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array())); if (leaves.length === 1) return leaves[0]; let k = 1; while (k * 2 < leaves.length) k *= 2; return node(await merkleRoot(leaves.slice(0, k)), await merkleRoot(leaves.slice(k))); } export async function merkleRootOfHashes(hexHashes: string[]): Promise { const ls = await Promise.all(hexHashes.map((h) => leaf(Uint8Array.from(h.match(/../g)!.map((x) => parseInt(x, 16)))))); return hex(await merkleRoot(ls)); } type Chained = { body: Record; signature_b64: string; [k: string]: Json }; async function verifyChain(items: Chained[], pub: string, domain: string, hashKey: string, tag: string, extra?: (it: Chained) => Promise): Promise { const problems: string[] = []; let prev = "0".repeat(64); for (let i = 0; i < items.length; i++) { const it = items[i]; const b = it.body; if (b.seq !== i) problems.push(`${tag}[${i}]: seq`); if (b.prev !== prev) problems.push(`${tag}[${i}]: link`); if ((await digest(domain, b)) !== it[hashKey]) problems.push(`${tag}[${i}]: hash`); if (!(await verifySig(pub, it.signature_b64, domain, b))) problems.push(`${tag}[${i}]: signature`); if (extra) { const e = await extra(it); if (e) problems.push(`${tag}[${i}]: ${e}`); } prev = it[hashKey] as string; } return problems; } export const D = { receipt: "CAIN42/E28-GOVERNANCE-RECEIPT/v1", uar: "CAIN42/E30-UNIVERSAL-ACTION-RECEIPT/v1", uma: "CAIN42/E30-UNIVERSAL-MACHINE-ACTION/v1", budget: "CAIN42/E30-AUTONOMY-BUDGET-ENTRY/v1", }; export async function verifyGovernanceReceipts(exp: { signer_pub: string; receipts: Chained[] }): Promise { return verifyChain(exp.receipts, exp.signer_pub, D.receipt, "receipt_hash", "receipt"); } export async function verifyUniversalReceipts(exp: { pub: string; receipts: Chained[] }): Promise { return verifyChain(exp.receipts, exp.pub, D.uar, "hash", "uar", async (it) => (await digest(D.uma, it.uma as Json)) === it.body.uma_digest ? null : "uma digest"); } export async function verifyBudgetLedger(exp: { pub: string; entries: Chained[] }): Promise { return verifyChain(exp.entries, exp.pub, D.budget, "hash", "budget"); } async function main(): Promise { const fsModule: string = "node:fs"; const fs = (await import(fsModule)) as { readFileSync(p: string, enc: "utf8"): string }; const path = process.argv[2]; if (!path) { console.error("usage: node --experimental-strip-types cain-verify.ts artifacts.json"); return 2; } const a = JSON.parse(fs.readFileSync(path, "utf8")); const problems = [ ...(await verifyGovernanceReceipts(a.governance_receipts)), ...(await verifyUniversalReceipts(a.universal_receipts)), ...(await verifyBudgetLedger(a.budget_ledger)), ]; const checks = a.governance_receipts.receipts.length + a.universal_receipts.receipts.length + a.budget_ledger.entries.length; console.log(JSON.stringify({ verifier: "cain-verify.ts", language: "TypeScript", checks, problems, result: problems.length === 0 ? "INTACT" : "BROKEN" })); return problems.length === 0 ? 0 : 1; } if (import.meta.url === `file://${process.argv[1]}`) { main().then((c) => process.exit(c)); }