import { beforeEach, describe, expect, it, vi } from "vitest"; import { generateKeyPairFromSeed } from "@stablelib/x25519"; // PCH-01-002: the Noise handshakes derive `h` (protocol-name init hash) and the // HKDF in MixKey/Split over BLAKE2s, matching the declared pattern name. These // are KNOWN-ANSWER tests over a fully deterministic handshake (fixed statics + // seeded ephemerals): a revert to SHA-256 changes every derived byte, so the // pinned handshakeHash below stops matching and the test fails. That failure is // the intended signal of an accidental hash-algorithm change. // Mock ONLY ephemeral generation so the handshake is deterministic; sharedKey // and the seeded keygen stay real. File-scoped, so other noise tests are // unaffected. The counter lives on globalThis and is reset per test. vi.mock("@stablelib/x25519", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, generateKeyPair: () => { const g = globalThis as { __kpCounter?: number }; g.__kpCounter = (g.__kpCounter ?? 0) + 1; return actual.generateKeyPairFromSeed(new Uint8Array(32).fill(g.__kpCounter)); }, }; }); function memoryPair() { const a: Uint8Array[] = []; const b: Uint8Array[] = []; const aR: Array<(v: Uint8Array) => void> = []; const bR: Array<(v: Uint8Array) => void> = []; const tA = { async send(m: Uint8Array) { const r = bR.shift(); if (r) r(m); else b.push(m); }, receive(): Promise { const v = a.shift(); if (v) return Promise.resolve(v); return new Promise((resolve) => aR.push(resolve)); }, }; const tB = { async send(m: Uint8Array) { const r = aR.shift(); if (r) r(m); else a.push(m); }, receive(): Promise { const v = b.shift(); if (v) return Promise.resolve(v); return new Promise((resolve) => bR.push(resolve)); }, }; return [tA, tB] as const; } function hex(b: Uint8Array): string { return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); } describe("PCH-01-002 — Noise handshakes derive over BLAKE2s", () => { beforeEach(() => { (globalThis as { __kpCounter?: number }).__kpCounter = 0; }); it("noise_xx_handshake_hash_is_blake2s_derived", async () => { const { noiseXXInitiator, noiseXXResponder } = await import("./noise-xx"); const alice = generateKeyPairFromSeed(new Uint8Array(32).fill(0xa1)); const bob = generateKeyPairFromSeed(new Uint8Array(32).fill(0xb2)); const [tA, tB] = memoryPair(); const [a, b] = await Promise.all([ noiseXXInitiator(alice.secretKey, alice.publicKey, tA.send, tA.receive), noiseXXResponder(bob.secretKey, bob.publicKey, tB.send, tB.receive), ]); // Both directions agree (consistency) ... expect(hex(a.handshakeHash)).toBe(hex(b.handshakeHash)); // ... and equal the pinned BLAKE2s vector (a SHA-256 revert changes this). expect(hex(a.handshakeHash)).toBe( "11c8bf7fcefad1f00ff8573c7bca66891626d053addae6d2b78b2f195a9ffd8a", ); // Transport keys are also BLAKE2s-derived; a frame must still round-trip. const ct = a.encrypt(new TextEncoder().encode("hello")); expect(new TextDecoder().decode(b.decrypt(ct))).toBe("hello"); }); it("noise_nk_handshake_hash_is_blake2s_derived", async () => { const { noiseNKInitiator, noiseNKResponder } = await import("./noise-js"); const responder = generateKeyPairFromSeed(new Uint8Array(32).fill(0xc3)); const [tA, tB] = memoryPair(); const [a, b] = await Promise.all([ noiseNKInitiator(responder.publicKey, tA.send, tA.receive), noiseNKResponder(responder.secretKey, responder.publicKey, tB.send, tB.receive), ]); expect(hex(a.handshakeHash)).toBe(hex(b.handshakeHash)); expect(hex(a.handshakeHash)).toBe( "0fbab8cd1782bf4b17d69beb69f5b80b0bf24d93da4924959c76ee701c32e655", ); const ct = a.encrypt(new TextEncoder().encode("ping")); expect(new TextDecoder().decode(b.decrypt(ct))).toBe("ping"); }); });