// PB-26-014 follow-up — the JS-side DH boundaries in noise-js.ts mirror the // Rust core's small-order X25519 blocklist (libhydrogen's contributory-behaviour // list). The WASM core rejects these before every handshake DH; the TypeScript // sharedKey calls must too, before doing the DH. import { describe, expect, it } from "vitest"; import { generateKeyPair } from "@stablelib/x25519"; import { computeAuthProof, hexToBytes, isSmallOrderPubkey, noiseKKInitiator, noiseKKResponder, performResponderAuth, } from "./noise-js"; import type { NoiseNKSession } from "./noise-js"; const SMALL_ORDER_PUBKEY_HEX = [ "00".repeat(32), "01" + "00".repeat(31), "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880", "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7", "ec" + "ff".repeat(30) + "7f", "ed" + "ff".repeat(30) + "7f", "ee" + "ff".repeat(30) + "7f", "cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880", "4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7", ]; const deadTransport = { send: async () => undefined, receive: () => new Promise(() => undefined), }; describe("PB-26-014 follow-up — JS-side small-order X25519 rejection", () => { it("flags all nine small-order u-coordinates; a real generated key passes", () => { for (const hex of SMALL_ORDER_PUBKEY_HEX) { expect(isSmallOrderPubkey(hexToBytes(hex))).toBe(true); } expect(isSmallOrderPubkey(generateKeyPair().publicKey)).toBe(false); }); it("KK initiator aborts against a small-order responder static", async () => { const initiator = generateKeyPair(); for (const hex of SMALL_ORDER_PUBKEY_HEX) { await expect( noiseKKInitiator( initiator.secretKey, initiator.publicKey, hexToBytes(hex), deadTransport.send, deadTransport.receive, ), ).rejects.toThrow(/small-order/i); } }); it("PB-26-601: KK responder aborts against a small-order initiator static", async () => { const responder = generateKeyPair(); for (const hex of SMALL_ORDER_PUBKEY_HEX) { // A transport that reports if the WASM core is reached: the guard must // throw first, so neither callback may ever run. const touched = { send: 0, receive: 0 }; await expect( noiseKKResponder( responder.secretKey, responder.publicKey, hexToBytes(hex), async () => { touched.send += 1; }, () => { touched.receive += 1; return Promise.reject(new Error("reached the WASM core")); }, ), ).rejects.toThrow(/small-order/i); expect(touched).toEqual({ send: 0, receive: 0 }); } }); it("computeAuthProof refuses a small-order responder key", () => { const initiator = generateKeyPair(); const nonce = new Uint8Array(32); const handshakeHash = new Uint8Array(32); expect(() => computeAuthProof( initiator.secretKey, initiator.publicKey, hexToBytes(SMALL_ORDER_PUBKEY_HEX[3]!), nonce, handshakeHash, ), ).toThrow(/small-order/i); }); it("performResponderAuth refuses a PROOF frame with a small-order initiator key", async () => { const responder = generateKeyPair(); const session = { handshakeHash: new Uint8Array(32) } as NoiseNKSession; const proofFrame = new TextEncoder().encode( JSON.stringify({ type: "PROOF", initiator_public_key_hex: SMALL_ORDER_PUBKEY_HEX[0], proof_hex: "00".repeat(32), }), ); await expect( performResponderAuth( session, responder.secretKey, responder.publicKey, async () => undefined, async () => proofFrame, ), ).rejects.toThrow(/small-order/i); }); });