noise-blake2s-kat.test.ts

5.5 KB · raw

1import { beforeEach, describe, expect, it, vi } from "vitest";2import { generateKeyPairFromSeed } from "@stablelib/x25519";3import { initHash as xxInitHash, hkdf2 as xxHkdf2 } from "./noise-xx";4import { initHash as nkInitHash, hkdf2 as nkHkdf2 } from "./noise-js";56// PCH-01-002: the Noise handshakes derive `h` (protocol-name init hash) and the7// HKDF in MixKey/Split over BLAKE2s, matching the declared pattern name. These8// are KNOWN-ANSWER tests over a fully deterministic handshake (fixed statics +9// seeded ephemerals): a revert to SHA-256 changes every derived byte, so the10// pinned handshakeHash below stops matching and the test fails. That failure is11// the intended signal of an accidental hash-algorithm change.1213// Mock ONLY ephemeral generation so the handshake is deterministic; sharedKey14// and the seeded keygen stay real. File-scoped, so other noise tests are15// unaffected. The counter lives on globalThis and is reset per test.16vi.mock("@stablelib/x25519", async (importOriginal) => {17  const actual = await importOriginal<typeof import("@stablelib/x25519")>();18  return {19    ...actual,20    generateKeyPair: () => {21      const g = globalThis as { __kpCounter?: number };22      g.__kpCounter = (g.__kpCounter ?? 0) + 1;23      return actual.generateKeyPairFromSeed(new Uint8Array(32).fill(g.__kpCounter));24    },25  };26});2728function memoryPair() {29  const a: Uint8Array[] = [];30  const b: Uint8Array[] = [];31  const aR: Array<(v: Uint8Array) => void> = [];32  const bR: Array<(v: Uint8Array) => void> = [];33  const tA = {34    async send(m: Uint8Array) { const r = bR.shift(); if (r) r(m); else b.push(m); },35    receive(): Promise<Uint8Array> {36      const v = a.shift();37      if (v) return Promise.resolve(v);38      return new Promise<Uint8Array>((resolve) => aR.push(resolve));39    },40  };41  const tB = {42    async send(m: Uint8Array) { const r = aR.shift(); if (r) r(m); else a.push(m); },43    receive(): Promise<Uint8Array> {44      const v = b.shift();45      if (v) return Promise.resolve(v);46      return new Promise<Uint8Array>((resolve) => bR.push(resolve));47    },48  };49  return [tA, tB] as const;50}5152function hex(b: Uint8Array): string {53  return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");54}5556// Cross-repo frozen vectors for the two changed primitives. These exact hex57// values are ALSO pinned in peach-android's CrossPlatformKATTest.kt — a mismatch58// on either side means the BLAKE2s wire derivations have diverged.59describe("PCH-01-002 — BLAKE2s primitive vectors (shared with peach-android)", () => {60  const CK = new Uint8Array(32).map((_, i) => i); // 0x00..0x1f61  const IKM = new Uint8Array(32).map((_, i) => i + 32); // 0x20..0x3f6263  it("initHash is BLAKE2s-256 of the XX protocol name", () => {64    expect(hex(xxInitHash())).toBe(65      "1ceedd81c5f458b225923dc2507787bf156f9251fc17c45af63263a929fc1ed2",66    );67  });6869  it("initHash is BLAKE2s-256 of the NK protocol name", () => {70    expect(hex(nkInitHash())).toBe(71      "e01688d376ef280a27a8af5c559024a02017585b3763763e4f3c7118a979b09e",72    );73  });7475  it("hkdf2 (HKDF-BLAKE2s) matches the frozen vector and is identical XX vs NK", () => {76    const [k1, k2] = xxHkdf2(CK, IKM);77    expect(hex(k1)).toBe("6a96444e20e8d4c1cee974416acae1c10b3c92886010e54ed94dafb2c3b80ea0");78    expect(hex(k2)).toBe("57af120b0de7acbe7907ec149c5ae870a2dbb74232b65777ba4123f1f7f888f5");79    // The hkdf2 logic is duplicated in noise-xx.ts and noise-js.ts — they must agree.80    const [n1, n2] = nkHkdf2(CK, IKM);81    expect(hex(n1)).toBe(hex(k1));82    expect(hex(n2)).toBe(hex(k2));83  });84});8586describe("PCH-01-002 — Noise handshakes derive over BLAKE2s", () => {87  beforeEach(() => {88    (globalThis as { __kpCounter?: number }).__kpCounter = 0;89  });9091  it("noise_xx_handshake_hash_is_blake2s_derived", async () => {92    const { noiseXXInitiator, noiseXXResponder } = await import("./noise-xx");93    const alice = generateKeyPairFromSeed(new Uint8Array(32).fill(0xa1));94    const bob = generateKeyPairFromSeed(new Uint8Array(32).fill(0xb2));95    const [tA, tB] = memoryPair();9697    const [a, b] = await Promise.all([98      noiseXXInitiator(alice.secretKey, alice.publicKey, tA.send, tA.receive),99      noiseXXResponder(bob.secretKey, bob.publicKey, tB.send, tB.receive),100    ]);101102    // Both directions agree (consistency) ...103    expect(hex(a.handshakeHash)).toBe(hex(b.handshakeHash));104    // ... and equal the pinned BLAKE2s + canonical msg1-empty-hash vector.105    expect(hex(a.handshakeHash)).toBe(106      "505b6dcb59838e5d163f4fd4897cfff87c20ecca23aa993b416206aba25ee71c",107    );108109    // Transport keys are also BLAKE2s-derived; a frame must still round-trip.110    const ct = a.encrypt(new TextEncoder().encode("hello"));111    expect(new TextDecoder().decode(b.decrypt(ct))).toBe("hello");112  });113114  it("noise_nk_handshake_hash_is_blake2s_derived", async () => {115    const { noiseNKInitiator, noiseNKResponder } = await import("./noise-js");116    const responder = generateKeyPairFromSeed(new Uint8Array(32).fill(0xc3));117    const [tA, tB] = memoryPair();118119    const [a, b] = await Promise.all([120      noiseNKInitiator(responder.publicKey, tA.send, tA.receive),121      noiseNKResponder(responder.secretKey, responder.publicKey, tB.send, tB.receive),122    ]);123124    expect(hex(a.handshakeHash)).toBe(hex(b.handshakeHash));125    expect(hex(a.handshakeHash)).toBe(126      "0fbab8cd1782bf4b17d69beb69f5b80b0bf24d93da4924959c76ee701c32e655",127    );128129    const ct = a.encrypt(new TextEncoder().encode("ping"));130    expect(new TextDecoder().decode(b.decrypt(ct))).toBe("ping");131  });132});