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 RESPONDER_PRIVATE = new Uint8Array(32).fill(0x02);25const NONCE = new Uint8Array(32).fill(0x03);2627const EXPECTED_INITIATOR_PUBLIC =28  "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209";29const EXPECTED_RESPONDER_PUBLIC =30  "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59";31const EXPECTED_PROOF =32  "39efee52ccc9d7eaa24fcfec9a4c75eaad75a6bf3319d9794d9227f2b1466752";3334function derivePublic(privateKey: Uint8Array): Uint8Array {35  const pair = x25519GenerateKeyPair();36  // We can't derive a public from an arbitrary private with @stablelib's37  // generateKeyPair (it samples a fresh secret). For the cross-check, the38  // expected public keys are pinned above so we just use them directly.39  void pair;40  throw new Error("derivePublic should not be called — use pinned constants");41}4243describe("noise-js cross-check", () => {44  it("computes the pinned HMAC-SHA256 proof for the known input vector", () => {45    // Use the pinned public-key constants — they correspond to the X2551946    // public derived from the all-0x01 / all-0x02 private keys. Rederiving47    // them here would require a "derivePublic from raw scalar" helper that48    // @stablelib does not expose, so we trust the pinned values.49    void derivePublic;5051    const initiatorPublic = hexToBytes(EXPECTED_INITIATOR_PUBLIC);52    const responderPublic = hexToBytes(EXPECTED_RESPONDER_PUBLIC);5354    const proof = computeAuthProof(55      INITIATOR_PRIVATE,56      initiatorPublic,57      responderPublic,58      NONCE,59    );6061    expect(bytesToHex(proof)).toBe(EXPECTED_PROOF);62  });63});6465describe("noise NK responder ↔ initiator round trip (pure JS)", () => {66  it("completes handshake, encrypts/decrypts, and verifies auth proof", async () => {67    const initiatorStatic = x25519GenerateKeyPair();68    const responderStatic = x25519GenerateKeyPair();6970    const initToResp: Uint8Array[] = [];71    const respToInit: Uint8Array[] = [];7273    const initSend = async (b: Uint8Array) => { initToResp.push(b); };74    const initRecv = async () => {75      while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));76      return respToInit.shift()!;77    };78    const respSend = async (b: Uint8Array) => { respToInit.push(b); };79    const respRecv = async () => {80      while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));81      return initToResp.shift()!;82    };8384    const [initSession, respSession] = await Promise.all([85      noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),86      noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),87    ]);8889    // Transport phase: bidirectional encrypt/decrypt.90    const initiatorOut = initSession.encrypt(new TextEncoder().encode("hello from initiator"));91    expect(new TextDecoder().decode(respSession.decrypt(initiatorOut))).toBe("hello from initiator");92    const responderOut = respSession.encrypt(new TextEncoder().encode("hello from responder"));93    expect(new TextDecoder().decode(initSession.decrypt(responderOut))).toBe("hello from responder");9495    // Auth round.96    const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };97    const initiatorEncRecv = async () => initSession.decrypt(await initRecv());98    const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };99    const responderEncRecv = async () => respSession.decrypt(await respRecv());100101    const [, authResult] = await Promise.all([102      performInitiatorAuth(103        initSession,104        initiatorStatic.secretKey,105        initiatorStatic.publicKey,106        responderStatic.publicKey,107        initiatorEncSend,108        initiatorEncRecv,109      ),110      performResponderAuth(111        respSession,112        responderStatic.secretKey,113        responderStatic.publicKey,114        responderEncSend,115        responderEncRecv,116      ),117    ]);118119    expect(bytesToHex(authResult.initiatorPublicKey)).toBe(bytesToHex(initiatorStatic.publicKey));120  });121122  it("rejects a forged proof (constant-time MAC compare)", async () => {123    const initiatorStatic = x25519GenerateKeyPair();124    const attackerStatic = x25519GenerateKeyPair();125    const responderStatic = x25519GenerateKeyPair();126127    const initToResp: Uint8Array[] = [];128    const respToInit: Uint8Array[] = [];129    const initSend = async (b: Uint8Array) => { initToResp.push(b); };130    const initRecv = async () => {131      while (respToInit.length === 0) await new Promise<void>((r) => setTimeout(r, 0));132      return respToInit.shift()!;133    };134    const respSend = async (b: Uint8Array) => { respToInit.push(b); };135    const respRecv = async () => {136      while (initToResp.length === 0) await new Promise<void>((r) => setTimeout(r, 0));137      return initToResp.shift()!;138    };139140    const [initSession, respSession] = await Promise.all([141      noiseNKInitiator(responderStatic.publicKey, initSend, initRecv),142      noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, respSend, respRecv),143    ]);144145    // Initiator lies — claims initiator's identity but signs with attacker's key.146    const initiatorEncSend = async (data: Uint8Array) => { initToResp.push(initSession.encrypt(data)); };147    const initiatorEncRecv = async () => initSession.decrypt(await initRecv());148    const responderEncSend = async (data: Uint8Array) => { respToInit.push(respSession.encrypt(data)); };149    const responderEncRecv = async () => respSession.decrypt(await respRecv());150151    const initiatorPromise = performInitiatorAuth(152      initSession,153      attackerStatic.secretKey, // wrong private — used to compute proof154      initiatorStatic.publicKey, // claimed identity155      responderStatic.publicKey,156      initiatorEncSend,157      initiatorEncRecv,158    );159    const responderPromise = performResponderAuth(160      respSession,161      responderStatic.secretKey,162      responderStatic.publicKey,163      responderEncSend,164      responderEncRecv,165    );166167    await expect(Promise.all([initiatorPromise, responderPromise])).rejects.toThrow(/proof did not verify/);168  });169});170171function hexToBytes(hex: string): Uint8Array {172  const out = new Uint8Array(hex.length / 2);173  for (let i = 0; i < hex.length; i += 2) {174    out[i / 2] = parseInt(hex.slice(i, i + 2), 16);175  }176  return out;177}