12345import { describe, expect, it } from "vitest";6import { generateKeyPair } from "@stablelib/x25519";7import { noiseNKInitiator, noiseNKResponder } from "./noise-js";8import { noiseXXInitiator, noiseXXResponder } from "./noise-xx";910const ZERO_POINT = new Uint8Array(32);1112function queuePair() {13 const makeQueue = () => {14 const items: Uint8Array[] = [];15 const waiters: Array<(m: Uint8Array) => void> = [];16 return {17 push(m: Uint8Array) {18 const w = waiters.shift();19 if (w) w(m);20 else items.push(m);21 },22 pull(): Promise<Uint8Array> {23 const m = items.shift();24 return m ? Promise.resolve(m) : new Promise((r) => waiters.push(r));25 },26 };27 };28 const aToB = makeQueue();29 const bToA = makeQueue();30 return {31 a: { send: async (m: Uint8Array) => aToB.push(m), receive: () => bToA.pull() },32 b: { send: async (m: Uint8Array) => bToA.push(m), receive: () => aToB.pull() },33 };34}3536const deadTransport = {37 send: async () => undefined,38 receive: () => new Promise<Uint8Array>(() => undefined),39};4041describe("PCH-02-009 — reject zero X25519 DH outputs", () => {42 it("NK initiator aborts against an all-zero responder static key", async () => {43 await expect(44 noiseNKInitiator(ZERO_POINT, deadTransport.send, deadTransport.receive),45 ).rejects.toThrow(/invalid shared key/);46 });4748 it("NK responder aborts on an all-zero msg-1 ephemeral", async () => {49 const ourStatic = generateKeyPair();50 const msg1 = new Uint8Array(48);51 await expect(52 noiseNKResponder(53 ourStatic.secretKey,54 ourStatic.publicKey,55 deadTransport.send,56 async () => msg1,57 ),58 ).rejects.toThrow(/invalid shared key/);59 });6061 it("XX responder aborts on an all-zero msg-1 ephemeral", async () => {62 const ourStatic = generateKeyPair();63 await expect(64 noiseXXResponder(65 ourStatic.secretKey,66 ourStatic.publicKey,67 deadTransport.send,68 async () => ZERO_POINT.slice(),69 ),70 ).rejects.toThrow(/invalid shared key/);71 });7273 it("honest NK handshake still completes and transports a frame (regression)", async () => {74 const responderStatic = generateKeyPair();75 const { a, b } = queuePair();7677 const [initiator, responder] = await Promise.all([78 noiseNKInitiator(responderStatic.publicKey, a.send, a.receive),79 noiseNKResponder(responderStatic.secretKey, responderStatic.publicKey, b.send, b.receive),80 ]);8182 const frame = new TextEncoder().encode("peach-nk-frame");83 expect(responder.decrypt(initiator.encrypt(frame))).toEqual(frame);84 expect(initiator.handshakeHash).toEqual(responder.handshakeHash);85 });8687 it("honest XX handshake still completes and transports a frame (regression)", async () => {88 const initiatorStatic = generateKeyPair();89 const responderStatic = generateKeyPair();90 const { a, b } = queuePair();9192 const [initiator, responder] = await Promise.all([93 noiseXXInitiator(initiatorStatic.secretKey, initiatorStatic.publicKey, a.send, a.receive),94 noiseXXResponder(responderStatic.secretKey, responderStatic.publicKey, b.send, b.receive),95 ]);9697 expect(initiator.remoteStaticPublicKey).toEqual(responderStatic.publicKey);98 expect(responder.remoteStaticPublicKey).toEqual(initiatorStatic.publicKey);99100 const frame = new TextEncoder().encode("peach-xx-frame");101 expect(responder.decrypt(initiator.encrypt(frame))).toEqual(frame);102 });103});