diff --git a/package.json b/package.json index ba1a860..983e3eb 100644 --- a/package.json +++ b/package.json @@ -60,11 +60,17 @@ "engines": { "node": ">=20.0.0" }, - "dependencies": { + "peerDependencies": { "ethers": "^6.13.0" }, + "peerDependenciesMeta": { + "ethers": { + "optional": true + } + }, "devDependencies": { "@types/node": "^20.0.0", + "ethers": "^6.13.0", "tsx": "^4.0.0", "typescript": "^5.4.0" }, diff --git a/src/canonicalize.ts b/src/canonicalize.ts index f0c0808..c9f77c0 100644 --- a/src/canonicalize.ts +++ b/src/canonicalize.ts @@ -153,8 +153,8 @@ export const CANONICAL_TEST_VECTORS = [ }, { description: "unicode string", - input: { msg: "hello \u4e16\u754c" }, - expected: '{"msg":"hello \u4e16\u754c"}', + input: { msg: "hello 世界" }, + expected: '{"msg":"hello 世界"}', }, { description: "string with quotes and backslash", @@ -173,4 +173,14 @@ export const CANONICAL_TEST_VECTORS = [ expected: '{"agent":"runtime.commandlayer.eth","payload":{"input":"test","result":"ok"},"timestamp":"2026-05-12T00:00:00.000Z","verb":"verify","version":"1.1.0"}', }, + { + // Mandatory audit test vector — protocol audit requires SHA-256 of this + // canonical form to be computed and tested (see test/canonicalize.test.ts). + // Input key insertion order is intentionally scrambled to verify sorting. + description: "audit protocol vector: verb/family/version", + input: { verb: "verify", family: "trust", version: "1.0.0" }, + expected: '{"family":"trust","verb":"verify","version":"1.0.0"}', + // SHA-256 of the canonical string (UTF-8 encoded): + sha256: "3c3e2e6f63b02c1dc4d0dc0f6429bcef5fe27f11059c856218a52a4f43f90e44", + }, ] as const; diff --git a/test/canonicalize.test.ts b/test/canonicalize.test.ts index 07cae23..930df1e 100644 --- a/test/canonicalize.test.ts +++ b/test/canonicalize.test.ts @@ -7,6 +7,7 @@ */ import { strict as assert } from "node:assert"; +import { createHash } from "node:crypto"; import { describe, it } from "node:test"; import { canonicalize, CANONICAL_TEST_VECTORS } from "../src/canonicalize.js"; @@ -65,3 +66,62 @@ describe("canonicalize — determinism", () => { assert.strictEqual(result, '{"outer":{"a":1,"z":9}}'); }); }); + +describe("canonicalize — SHA-256 audit test vector", () => { + /** + * CRITICAL PATH: The audit protocol mandates that the SHA-256 digest of the + * canonical form of {"verb":"verify","family":"trust","version":"1.0.0"} + * is computed and verified against a known value. + * + * Canonical form (keys sorted): {"family":"trust","verb":"verify","version":"1.0.0"} + * SHA-256 (hex): 3c3e2e6f63b02c1dc4d0dc0f6429bcef5fe27f11059c856218a52a4f43f90e44 + * + * This test locks the canonicalization algorithm to a concrete byte-level + * output and proves the SHA-256 is deterministic across Node.js versions. + */ + it("SHA-256 of canonical audit vector matches known digest", () => { + const input = { verb: "verify", family: "trust", version: "1.0.0" }; + const canonical = canonicalize(input); + + // Verify the canonical string itself first + assert.strictEqual( + canonical, + '{"family":"trust","verb":"verify","version":"1.0.0"}', + "canonical form must have keys sorted lexicographically" + ); + + // Compute SHA-256 of the UTF-8 bytes of the canonical string + const digest = createHash("sha256") + .update(canonical, "utf8") + .digest("hex"); + + // Known SHA-256 digest — locked as the protocol audit test vector. + // If this fails, canonicalization has changed and protocol version must bump. + assert.strictEqual( + digest, + "3c3e2e6f63b02c1dc4d0dc0f6429bcef5fe27f11059c856218a52a4f43f90e44", + "SHA-256 of canonical audit vector must match the known protocol digest" + ); + }); + + it("audit vector SHA-256 matches CANONICAL_TEST_VECTORS entry", () => { + // Cross-check: the sha256 field in CANONICAL_TEST_VECTORS must match + // what we compute at runtime, ensuring the exported constant is correct. + const auditVector = CANONICAL_TEST_VECTORS.find( + (v) => v.description === "audit protocol vector: verb/family/version" + ); + assert.ok(auditVector, "audit vector must exist in CANONICAL_TEST_VECTORS"); + + const canonical = canonicalize(auditVector.input); + assert.strictEqual(canonical, auditVector.expected); + + const digest = createHash("sha256").update(canonical, "utf8").digest("hex"); + // Type assertion needed because not all vectors have sha256 field + const vectorWithHash = auditVector as typeof auditVector & { sha256: string }; + assert.strictEqual( + digest, + vectorWithHash.sha256, + "runtime-computed SHA-256 must match the sha256 field in CANONICAL_TEST_VECTORS" + ); + }); +});