From 35444249855f8cb386c1a971411bb9956f1f31b3 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Tue, 22 Mar 2022 20:40:22 -0700 Subject: [PATCH] Do real secp256k1 point->curve checking * This is a breaking change, as it requires the JS environment to have BigInt (all supported versions of JavaScript engines appear to). * This check may prevent loss of funds by eliminating a category of unspendable addresses from being created. * Performance is almost as fast as tiny-secp256k1 39-42us vs 33-35us. * Added `isXOnlyPoint` to types, expecting it to be used for Taproot. --- src/types.d.ts | 1 + src/types.js | 79 +++++++++++++++++++++++++++++------- test/fixtures/crypto.json | 2 +- test/types.spec.ts | 29 +++++++++++++ ts_src/types.ts | 85 ++++++++++++++++++++++++++++++++------- 5 files changed, 166 insertions(+), 30 deletions(-) diff --git a/src/types.d.ts b/src/types.d.ts index 5a8505d346..7c52906bc6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,6 +1,7 @@ /// export declare const typeforce: any; export declare function isPoint(p: Buffer | number | undefined | null): boolean; +export declare function isXOnlyPoint(p: Buffer | number | undefined | null): boolean; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; export declare namespace BIP32Path { diff --git a/src/types.js b/src/types.js index a6d1efa167..d6d8417412 100644 --- a/src/types.js +++ b/src/types.js @@ -1,30 +1,79 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0; +exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isXOnlyPoint = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); exports.typeforce = require('typeforce'); -const ZERO32 = buffer_1.Buffer.alloc(32, 0); -const EC_P = buffer_1.Buffer.from( - 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', - 'hex', +const BN_ZERO = BigInt(0); +// Bitcoin uses the secp256k1 curve, whose parameters can be found on +// page 13, section 2.4.1, of https://www.secg.org/sec2-v2.pdf +const EC_P = BigInt( + `0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`, ); +// The short Weierstrass form curve equation simplifes to y^2 = x^3 + 7. +function secp256k1Right(x) { + const EC_B = BigInt(7); + const x2 = (x * x) % EC_P; + const x3 = (x2 * x) % EC_P; + return (x3 + EC_B) % EC_P; +} +// For prime P, the Jacobi Symbol of 'a' is 1 if and only if 'a' is a quadratic +// residue mod P, ie. there exists a value 'x' for whom x^2 = a. +function jacobiSymbol(a) { + // Idea from noble-secp256k1, to be nice to bad JS parsers + const _1n = BigInt(1); + const _2n = BigInt(2); + const _3n = BigInt(3); + const _5n = BigInt(5); + const _7n = BigInt(7); + if (a === BN_ZERO) return 0; + let p = EC_P; + let sign = 1; + // This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking + for (;;) { + let and3; + // Handle runs of zeros efficiently w/o flipping sign each time + for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n); + // If there's one more zero, shift it off and flip the sign + if (and3 === _2n) { + a >>= _1n; + const pand7 = p & _7n; + if (pand7 === _3n || pand7 === _5n) sign = -sign; + } + if (a === _1n) break; + if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign; + [a, p] = [p % a, a]; + } + return sign > 0 ? 1 : -1; +} function isPoint(p) { if (!buffer_1.Buffer.isBuffer(p)) return false; if (p.length < 33) return false; const t = p[0]; - const x = p.slice(1, 33); - if (x.compare(ZERO32) === 0) return false; - if (x.compare(EC_P) >= 0) return false; - if ((t === 0x02 || t === 0x03) && p.length === 33) { - return true; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); } - const y = p.slice(33); - if (y.compare(ZERO32) === 0) return false; - if (y.compare(EC_P) >= 0) return false; - if (t === 0x04 && p.length === 65) return true; - return false; + if (t !== 0x04 || p.length !== 65) return false; + const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`); + if (x === BN_ZERO) return false; + if (x >= EC_P) return false; + const y = BigInt(`0x${p.slice(33).toString('hex')}`); + if (y === BN_ZERO) return false; + if (y >= EC_P) return false; + const left = (y * y) % EC_P; + const right = secp256k1Right(x); + return left === right; } exports.isPoint = isPoint; +function isXOnlyPoint(p) { + if (!buffer_1.Buffer.isBuffer(p)) return false; + if (p.length !== 32) return false; + const x = BigInt(`0x${p.toString('hex')}`); + if (x === BN_ZERO) return false; + if (x >= EC_P) return false; + const y2 = secp256k1Right(x); + return jacobiSymbol(y2) === 1; // If sqrt(y^2) exists, x is on the curve. +} +exports.isXOnlyPoint = isXOnlyPoint; const UINT31_MAX = Math.pow(2, 31) - 1; function UInt31(value) { return exports.typeforce.UInt32(value) && value <= UINT31_MAX; diff --git a/test/fixtures/crypto.json b/test/fixtures/crypto.json index 1d1976b5e9..53b23fe172 100644 --- a/test/fixtures/crypto.json +++ b/test/fixtures/crypto.json @@ -40,4 +40,4 @@ "result": "71ae15bad52efcecf4c9f672bfbded68a4adb8258f1b95f0d06aefdb5ebd14e9" } ] -} \ No newline at end of file +} diff --git a/test/types.spec.ts b/test/types.spec.ts index 478fd997e5..c423af9fee 100644 --- a/test/types.spec.ts +++ b/test/types.spec.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; import * as types from '../src/types'; const typeforce = require('typeforce'); +import * as fixtures from './fixtures/types.json'; describe('types', () => { describe('Buffer Hash160/Hash256', () => { @@ -91,4 +92,32 @@ describe('types', () => { assert.equal(toJsonValue, '"BIP32 derivation path"'); }); }); + + describe('isPoint (uncompressed)', () => { + fixtures.isPoint.forEach(f => { + it(`returns ${f.expected} for isPoint(${f.hex})`, () => { + const bytes = Buffer.from(f.hex, 'hex'); + assert.strictEqual(types.isPoint(bytes), f.expected); + }); + }); + }); + + describe('isPoint (compressed) + isXOnlyPoint', () => { + fixtures.isXOnlyPoint.forEach(f => { + it(`returns ${f.expected} for isPoint(02${f.hex})`, () => { + const bytes = Buffer.from(`02${f.hex}`, 'hex'); + assert.strictEqual(types.isPoint(bytes), f.expected); + }); + + it(`returns ${f.expected} for isPoint(03${f.hex})`, () => { + const bytes = Buffer.from(`03${f.hex}`, 'hex'); + assert.strictEqual(types.isPoint(bytes), f.expected); + }); + + it(`returns ${f.expected} for isXOnlyPoint(${f.hex})`, () => { + const bytes = Buffer.from(f.hex, 'hex'); + assert.strictEqual(types.isXOnlyPoint(bytes), f.expected); + }); + }); + }); }); diff --git a/ts_src/types.ts b/ts_src/types.ts index c035b40082..f44c25a2a5 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,28 +1,85 @@ import { Buffer as NBuffer } from 'buffer'; export const typeforce = require('typeforce'); -const ZERO32 = NBuffer.alloc(32, 0); -const EC_P = NBuffer.from( - 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', - 'hex', +const BN_ZERO = BigInt(0); +// Bitcoin uses the secp256k1 curve, whose parameters can be found on +// page 13, section 2.4.1, of https://www.secg.org/sec2-v2.pdf +const EC_P = BigInt( + `0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`, ); + +// The short Weierstrass form curve equation simplifes to y^2 = x^3 + 7. +function secp256k1Right(x: bigint): bigint { + const EC_B = BigInt(7); + const x2 = (x * x) % EC_P; + const x3 = (x2 * x) % EC_P; + return (x3 + EC_B) % EC_P; +} + +// For prime P, the Jacobi Symbol of 'a' is 1 if and only if 'a' is a quadratic +// residue mod P, ie. there exists a value 'x' for whom x^2 = a. +function jacobiSymbol(a: bigint): -1 | 0 | 1 { + // Idea from noble-secp256k1, to be nice to bad JS parsers + const _1n = BigInt(1); + const _2n = BigInt(2); + const _3n = BigInt(3); + const _5n = BigInt(5); + const _7n = BigInt(7); + + if (a === BN_ZERO) return 0; + + let p = EC_P; + let sign = 1; + // This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking + for (;;) { + let and3; + // Handle runs of zeros efficiently w/o flipping sign each time + for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n); + // If there's one more zero, shift it off and flip the sign + if (and3 === _2n) { + a >>= _1n; + const pand7 = p & _7n; + if (pand7 === _3n || pand7 === _5n) sign = -sign; + } + if (a === _1n) break; + if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign; + [a, p] = [p % a, a]; + } + return sign > 0 ? 1 : -1; +} + export function isPoint(p: Buffer | number | undefined | null): boolean { if (!NBuffer.isBuffer(p)) return false; if (p.length < 33) return false; const t = p[0]; - const x = p.slice(1, 33); - if (x.compare(ZERO32) === 0) return false; - if (x.compare(EC_P) >= 0) return false; - if ((t === 0x02 || t === 0x03) && p.length === 33) { - return true; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); } - const y = p.slice(33); - if (y.compare(ZERO32) === 0) return false; - if (y.compare(EC_P) >= 0) return false; - if (t === 0x04 && p.length === 65) return true; - return false; + if (t !== 0x04 || p.length !== 65) return false; + + const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`); + if (x === BN_ZERO) return false; + if (x >= EC_P) return false; + + const y = BigInt(`0x${p.slice(33).toString('hex')}`); + if (y === BN_ZERO) return false; + if (y >= EC_P) return false; + + const left = (y * y) % EC_P; + const right = secp256k1Right(x); + return left === right; +} + +export function isXOnlyPoint(p: Buffer | number | undefined | null): boolean { + if (!NBuffer.isBuffer(p)) return false; + if (p.length !== 32) return false; + const x = BigInt(`0x${p.toString('hex')}`); + if (x === BN_ZERO) return false; + if (x >= EC_P) return false; + const y2 = secp256k1Right(x); + return jacobiSymbol(y2) === 1; // If sqrt(y^2) exists, x is on the curve. } const UINT31_MAX: number = Math.pow(2, 31) - 1;