noise-js.test.ts

6.9 KB · raw

1/**2 * Noise NK auth proof regression test.3 *4 * Pins a known input → output for the HMAC-SHA256 proof so any change to the5 * proof format is caught immediately. The pinned vector was captured against6 * the originally-shipped implementation; if the format ever needs to change,7 * regenerate both this vector and any peer that pairs against this build.8 */910import { describe, expect, it } from "vitest";11import { generateKeyPair as x25519GenerateKeyPair } from "@stablelib/x25519";1213import {14  bytesToHex,15  computeAuthProof,16  noiseNKInitiator,17  noiseNKResponder,18  performInitiatorAuth,19  performResponderAuth,20} from "./noise-js";2122// Pinned cross-check vector for HMAC-SHA256 auth proof.23const INITIATOR_PRIVATE = new Uint8Array(32).fill(0x01);24const NONCE = new Uint8Array(32).fill(0x03);2526const EXPECTED_INITIATOR_PUBLIC =27  "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209";28const EXPECTED_RESPONDER_PUBLIC =29  "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59";30const EXPECTED_PROOF =31  "39efee52ccc9d7eaa24fcfec9a4c75eaad75a6bf3319d9794d9227f2b1466752";3233function derivePublic(_privateKey: Uint8Array): Uint8Array {34  const pair = x25519GenerateKeyPair();35  // We can't derive a public from an arbitrary private with @stablelib's36  // generateKeyPair (it samples a fresh secret). For the cross-check, the37  // expected public keys are pinned above so we just use them directly.38  void pair;39  throw new Error("derivePublic should not be called — use pinned constants");40}4142describe("noise-js cross-check", () => {43  it("computes the pinned HMAC-SHA256 proof for the known input vector", () => {44    // Use the pinned public-key constants — they correspond to the X2551945    // public derived from the all-0x01 / all-0x02 private keys. Rederiving46    // them here would require a "derivePublic from raw scalar" helper that47    // @stablelib does not expose, so we trust the pinned values.48    void derivePublic;4950    const initiatorPublic = hexToBytes(EXPECTED_INITIATOR_PUBLIC);51    const responderPublic = hexToBytes(EXPECTED_RESPONDER_PUBLIC);5253    const proof = computeAuthProof(54      INITIATOR_PRIVATE,55      initiatorPublic,56      responderPublic,57      NONCE,58    );5960    expect(bytesToHex(proof)).toBe(EXPECTED_PROOF);61  });62});6364describe("noise NK responder ↔ initiator round trip (pure JS)", () => {65  it("completes handshake, encrypts/decrypts, and verifies auth proof", async () => {66    const initiatorStatic = x25519GenerateKeyPair();67    const responderStatic = x25519GenerateKeyPair();6869    const initToResp: Uint8Array[] = [];70    const respToInit: Uint8Array[] = [];7172    const initSend = async (b: Uint8Array) => { initToResp.push(b); };73    const initRecv = async () => {74      while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));75      return respToInit.shift()!;76    };77    const respSend = async (b: Uint8Array) => { respToInit.push(b); };78    const respRecv = async () => {79      while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));80      return initToResp.shift()!;81    };8283    const [initSession, respSession] = await Promise.all([84      noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),85      noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),86    ]);8788    // Transport phase: bidirectional encrypt/decrypt.89    const initiatorOut = initSession.encrypt(new TextEncoder().encode("hello from initiator"));90    expect(new TextDecoder().decode(respSession.decrypt(initiatorOut))).toBe("hello from initiator");91    const responderOut = respSession.encrypt(new TextEncoder().encode("hello from responder"));92    expect(new TextDecoder().decode(initSession.decrypt(responderOut))).toBe("hello from responder");9394    // Auth round.95    const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };96    const initiatorEncRecv = async () => initSession.decrypt(await initRecv());97    const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };98    const responderEncRecv = async () => respSession.decrypt(await respRecv());99100    const [, authResult] = await Promise.all([101      performInitiatorAuth(102        initSession,103        initiatorStatic.secretKey,104        initiatorStatic.publicKey,105        responderStatic.publicKey,106        initiatorEncSend,107        initiatorEncRecv,108      ),109      performResponderAuth(110        respSession,111        responderStatic.secretKey,112        responderStatic.publicKey,113        responderEncSend,114        responderEncRecv,115      ),116    ]);117118    expect(bytesToHex(authResult.initiatorPublicKey)).toBe(bytesToHex(initiatorStatic.publicKey));119  });120121  it("rejects a forged proof (constant-time MAC compare)", async () => {122    const initiatorStatic = x25519GenerateKeyPair();123    const attackerStatic = x25519GenerateKeyPair();124    const responderStatic = x25519GenerateKeyPair();125126    const initToResp: Uint8Array[] = [];127    const respToInit: Uint8Array[] = [];128    const initSend = async (b: Uint8Array) => { initToResp.push(b); };129    const initRecv = async () => {130      while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));131      return respToInit.shift()!;132    };133    const respSend = async (b: Uint8Array) => { respToInit.push(b); };134    const respRecv = async () => {135      while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));136      return initToResp.shift()!;137    };138139    const [initSession, respSession] = await Promise.all([140      noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),141      noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),142    ]);143144    // Initiator lies — claims initiator's identity but signs with attacker's key.145    const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };146    const initiatorEncRecv = async () => initSession.decrypt(await initRecv());147    const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };148    const responderEncRecv = async () => respSession.decrypt(await respRecv());149150    const initiatorPromise = performInitiatorAuth(151      initSession,152      attackerStatic.secretKey, // wrong private — used to compute proof153      initiatorStatic.publicKey, // claimed identity154      responderStatic.publicKey,155      initiatorEncSend,156      initiatorEncRecv,157    );158    const responderPromise = performResponderAuth(159      respSession,160      responderStatic.secretKey,161      responderStatic.publicKey,162      responderEncSend,163      responderEncRecv,164    );165166    await expect(Promise.all([initiatorPromise, responderPromise])).rejects.toThrow(/proof did not verify/);167  });168});169170function hexToBytes(hex: string): Uint8Array {171  const out = new Uint8Array(hex.length / 2);172  for (let i = 0; i < hex.length; i += 2) {173    out[i / 2] = parseInt(hex.slice(i, i + 2), 16);174  }175  return out;176}