noise.reject-zero-dh.test.ts

4.2 KB · raw

1// PCH-02-009 regression tests — every Noise XX/NK DH must reject an all-zero2// shared secret (low-order / identity peer point) instead of feeding it into3// the key schedule. Relies on `sharedKey(…, …, true)` from the PCH-02-0094// patch, which throws "X25519: invalid shared key".5//6// DESTINATION: src/shared/noise.reject-zero-dh.test.ts7// Imports are written in destination form; this file is excluded from the8// suite while it lives under src/drafts/.910import { describe, expect, it } from "vitest";11import { generateKeyPair } from "@stablelib/x25519";12import { noiseNKInitiator, noiseNKResponder } from "./noise-js";13import { noiseXXInitiator, noiseXXResponder } from "./noise-xx";1415// The identity element: X25519 with an all-zero public key yields an all-zero16// shared secret — the canonical low-order input the fix must reject.17const ZERO_POINT = new Uint8Array(32);1819function queuePair() {20  const makeQueue = () => {21    const items: Uint8Array[] = [];22    const waiters: Array<(m: Uint8Array) => void> = [];23    return {24      push(m: Uint8Array) {25        const w = waiters.shift();26        if (w) w(m);27        else items.push(m);28      },29      pull(): Promise<Uint8Array> {30        const m = items.shift();31        return m ? Promise.resolve(m) : new Promise((r) => waiters.push(r));32      },33    };34  };35  const aToB = makeQueue();36  const bToA = makeQueue();37  return {38    a: { send: async (m: Uint8Array) => aToB.push(m), receive: () => bToA.pull() },39    b: { send: async (m: Uint8Array) => bToA.push(m), receive: () => aToB.pull() },40  };41}4243const deadTransport = {44  send: async () => undefined,45  receive: () => new Promise<Uint8Array>(() => undefined), // never resolves46};4748describe("PCH-02-009 — reject zero X25519 DH outputs", () => {49  it("NK initiator aborts against an all-zero responder static key", async () => {50    // es = DH(ephemeral, ZERO_POINT) is computed before msg1 is sent, so the51    // handshake must die before anything touches the transport.52    await expect(53      noiseNKInitiator(ZERO_POINT, deadTransport.send, deadTransport.receive),54    ).rejects.toThrow(/invalid shared key/);55  });5657  it("NK responder aborts on an all-zero msg-1 ephemeral", async () => {58    const ourStatic = generateKeyPair();59    // 32 zero bytes of "ephemeral" + 16 bytes of fake AEAD tag = minimum frame.60    const msg1 = new Uint8Array(48);61    await expect(62      noiseNKResponder(63        ourStatic.secretKey,64        ourStatic.publicKey,65        deadTransport.send,66        async () => msg1,67      ),68    ).rejects.toThrow(/invalid shared key/);69  });7071  it("XX responder aborts on an all-zero msg-1 ephemeral", async () => {72    const ourStatic = generateKeyPair();73    await expect(74      noiseXXResponder(75        ourStatic.secretKey,76        ourStatic.publicKey,77        deadTransport.send,78        async () => ZERO_POINT.slice(),79      ),80    ).rejects.toThrow(/invalid shared key/);81  });8283  it("honest NK handshake still completes and transports a frame (regression)", async () => {84    const responderStatic = generateKeyPair();85    const { a, b } = queuePair();8687    const [initiator, responder] = await Promise.all([88      noiseNKInitiator(responderStatic.publicKey, a.send, a.receive),89      noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, b.send, b.receive),90    ]);9192    const frame = new TextEncoder().encode("peach-nk-frame");93    expect(responder.decrypt(initiator.encrypt(frame))).toEqual(frame);94    expect(initiator.handshakeHash).toEqual(responder.handshakeHash);95  });9697  it("honest XX handshake still completes and transports a frame (regression)", async () => {98    const initiatorStatic = generateKeyPair();99    const responderStatic = generateKeyPair();100    const { a, b } = queuePair();101102    const [initiator, responder] = await Promise.all([103      noiseXXInitiator(initiatorStatic.secretKey, initiatorStatic.publicKey, a.send, a.receive),104      noiseXXResponder(responderStatic.secretKey, responderStatic.publicKey, b.send, b.receive),105    ]);106107    expect(initiator.remoteStaticPublicKey).toEqual(responderStatic.publicKey);108    expect(responder.remoteStaticPublicKey).toEqual(initiatorStatic.publicKey);109110    const frame = new TextEncoder().encode("peach-xx-frame");111    expect(responder.decrypt(initiator.encrypt(frame))).toEqual(frame);112  });113});