From a5ea57dc1469a50dc7a243281e457b9c82c88cd0 Mon Sep 17 00:00:00 2001 From: Melvin Carvalho Date: Tue, 19 May 2026 08:29:52 +0200 Subject: [PATCH] test(nostr-cid-vm): make JWK-y corruption provably off-curve (#438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "rejects a JWK with the right x but wrong y" test flipped the last base64url char of y from 'A'→'B' (or x→'A'). Because the final base64url char of a 32-byte field carries only 4 high bits — the bottom 2 are padding — 'A' (000000) and 'B' (000001) decode to the SAME bytes whenever y ended in 'A' (≈1/16 of randomly generated keys), turning the "corruption" into a no-op so the test occasionally accepted the JWK as valid. (A second, far rarer path: the flip landed on -y mod p.) Set y to 32 zero bytes instead. (x, 0) is on secp256k1 iff x³ ≡ -7 mod p, which has exactly 3 solutions in ~2²⁵⁶ — provably off-curve for any practical random x. Verified across 30 consecutive runs. --- test/nostr-cid-vm.test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/nostr-cid-vm.test.js b/test/nostr-cid-vm.test.js index 641fb7c7..25e50316 100644 --- a/test/nostr-cid-vm.test.js +++ b/test/nostr-cid-vm.test.js @@ -210,9 +210,12 @@ describe('NIP-98 + CID verificationMethod lookup (#399)', () => { it('rejects a JWK with the right x but wrong y (curve-point integrity)', async () => { const goodJwk = evenYJwk(pk); - // Flip the y to invalid — same x, different y → not on canonical - // BIP-340 point, so should NOT match the Nostr key. - const badJwk = { ...goodJwk, y: goodJwk.y.slice(0, -1) + (goodJwk.y.endsWith('A') ? 'B' : 'A') }; + // Force y = 0 (43 'A's = 32 zero bytes). (x, 0) is on secp256k1 + // iff x³ ≡ -7 mod p, which has exactly 3 solutions in ~2²⁵⁶ — so + // for any practical x this is provably off-curve. Avoids the flake + // where a single-char flip in y was either a base64url padding-bit + // no-op or, far more rarely, landed on the negation -y mod p. + const badJwk = { ...goodJwk, y: 'A'.repeat(43) }; nextProfile = buildProfile({ pubkey: pk, jwk: badJwk }); const url = `https://${POD_HOST}/private/data.ttl`; const { authHeader } = nip98Authorization({ method: 'GET', url, secretKey: sk });