Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
14 changes: 12 additions & 2 deletions src/canonicalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;
60 changes: 60 additions & 0 deletions test/canonicalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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"
);
});
});
Loading