From b176d36fe974144162382857019f7a84236a2c4b 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/crypto.d.ts | 2 ++ src/crypto.js | 67 +++++++++++++++++++++++++++++++++- src/types.d.ts | 1 + src/types.js | 27 +++++--------- test/crypto.spec.ts | 28 +++++++++++++++ test/fixtures/crypto.json | 58 +++++++++++++++++++++++++++++- ts_src/crypto.ts | 75 +++++++++++++++++++++++++++++++++++++++ ts_src/types.ts | 25 ++++--------- 8 files changed, 243 insertions(+), 40 deletions(-) diff --git a/src/crypto.d.ts b/src/crypto.d.ts index ec088f3e3b..209dbb16e5 100644 --- a/src/crypto.d.ts +++ b/src/crypto.d.ts @@ -1,4 +1,6 @@ /// +export declare function isPoint(p: Buffer): boolean; +export declare function isXOnlyPoint(p: Buffer): boolean; export declare function ripemd160(buffer: Buffer): Buffer; export declare function sha1(buffer: Buffer): Buffer; export declare function sha256(buffer: Buffer): Buffer; diff --git a/src/crypto.js b/src/crypto.js index 3c308da110..75356475b2 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -1,7 +1,72 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.taggedHash = exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = void 0; +exports.taggedHash = exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = exports.isXOnlyPoint = exports.isPoint = void 0; const createHash = require('create-hash'); +const EC_P = BigInt( + `0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`, +); +const EC_B = BigInt(7); +const BN_ZERO = BigInt(0); +function weierstrass(x) { + const x2 = (x * x) % EC_P; + const x3 = (x2 * x) % EC_P; + return (x3 /* + a=0 a*x */ + EC_B) % EC_P; +} +// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P +// This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking +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; + 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 (p.length < 33) return false; + const t = p[0]; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); + } + 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 = weierstrass(x); + return (left - right) % EC_P === BN_ZERO; +} +exports.isPoint = isPoint; +function isXOnlyPoint(p) { + 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 = weierstrass(x); + return jacobiSymbol(y2) === 1; +} +exports.isXOnlyPoint = isXOnlyPoint; function ripemd160(buffer) { try { return createHash('rmd160') 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..ff553315b0 100644 --- a/src/types.js +++ b/src/types.js @@ -1,30 +1,19 @@ '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'); +const bcrypto = require('./crypto'); exports.typeforce = require('typeforce'); -const ZERO32 = buffer_1.Buffer.alloc(32, 0); -const EC_P = buffer_1.Buffer.from( - 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', - 'hex', -); 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; - } - 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; + return bcrypto.isPoint(p); } exports.isPoint = isPoint; +function isXOnlyPoint(p) { + if (!buffer_1.Buffer.isBuffer(p)) return false; + return bcrypto.isXOnlyPoint(p); +} +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/crypto.spec.ts b/test/crypto.spec.ts index 0482ec9ecb..fe989d1f6f 100644 --- a/test/crypto.spec.ts +++ b/test/crypto.spec.ts @@ -30,4 +30,32 @@ describe('crypto', () => { }); }); }); + + describe('isPoint (uncompressed)', () => { + fixtures.isPoint.forEach(f => { + it(`returns ${f.expected} for isPoint(${f.hex})`, () => { + const bytes = Buffer.from(f.hex, 'hex'); + assert.strictEqual(bcrypto.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(bcrypto.isPoint(bytes), f.expected); + }); + + it(`returns ${f.expected} for isPoint(03${f.hex})`, () => { + const bytes = Buffer.from(`03${f.hex}`, 'hex'); + assert.strictEqual(bcrypto.isPoint(bytes), f.expected); + }); + + it(`returns ${f.expected} for isXOnlyPoint(${f.hex})`, () => { + const bytes = Buffer.from(f.hex, 'hex'); + assert.strictEqual(bcrypto.isXOnlyPoint(bytes), f.expected); + }); + }); + }); }); diff --git a/test/fixtures/crypto.json b/test/fixtures/crypto.json index 1d1976b5e9..0862016149 100644 --- a/test/fixtures/crypto.json +++ b/test/fixtures/crypto.json @@ -39,5 +39,61 @@ "hex": "0101010101010101", "result": "71ae15bad52efcecf4c9f672bfbded68a4adb8258f1b95f0d06aefdb5ebd14e9" } + ], + "isPoint": [ + { + "hex": "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected": false + }, + { + "hex": "04ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "expected": false + }, + { + "hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6", + "expected": true + }, + { + "hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded0", + "expected": false + }, + { + "hex": "04ff", + "expected": false + } + ], + "isXOnlyPoint": [ + { + "hex": "ff", + "expected": false + }, + { + "hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800", + "expected": false + }, + { + "hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "expected": true + }, + { + "hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e", + "expected": true + }, + { + "hex": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "expected": true + }, + { + "hex": "0000000000000000000000000000000000000000000000000000000000000001", + "expected": true + }, + { + "hex": "0000000000000000000000000000000000000000000000000000000000000000", + "expected": false + }, + { + "hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + "expected": false + } ] -} \ No newline at end of file +} diff --git a/ts_src/crypto.ts b/ts_src/crypto.ts index b7c355a736..e5147ba80d 100644 --- a/ts_src/crypto.ts +++ b/ts_src/crypto.ts @@ -1,5 +1,80 @@ import * as createHash from 'create-hash'; +const EC_P = BigInt( + `0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`, +); +const EC_B = BigInt(7); +const BN_ZERO = BigInt(0); + +function weierstrass(x: bigint): bigint { + const x2 = (x * x) % EC_P; + const x3 = (x2 * x) % EC_P; + return (x3 /* + a=0 a*x */ + EC_B) % EC_P; +} + +// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P +// This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking +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; + 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): boolean { + if (p.length < 33) return false; + + const t = p[0]; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); + } + + 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 = weierstrass(x); + return (left - right) % EC_P === BN_ZERO; +} + +export function isXOnlyPoint(p: Buffer): boolean { + 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 = weierstrass(x); + return jacobiSymbol(y2) === 1; +} + export function ripemd160(buffer: Buffer): Buffer { try { return createHash('rmd160') diff --git a/ts_src/types.ts b/ts_src/types.ts index c035b40082..560e164b51 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,28 +1,15 @@ import { Buffer as NBuffer } from 'buffer'; +import * as bcrypto from './crypto'; export const typeforce = require('typeforce'); -const ZERO32 = NBuffer.alloc(32, 0); -const EC_P = NBuffer.from( - 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', - 'hex', -); 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; - } + return bcrypto.isPoint(p); +} - 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; +export function isXOnlyPoint(p: Buffer | number | undefined | null): boolean { + if (!NBuffer.isBuffer(p)) return false; + return bcrypto.isXOnlyPoint(p); } const UINT31_MAX: number = Math.pow(2, 31) - 1;