Skip to content

Commit

Permalink
Do real secp256k1 point->curve checking
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
reardencode committed Mar 24, 2022
1 parent 24e4d6f commit b176d36
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 40 deletions.
2 changes: 2 additions & 0 deletions src/crypto.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/// <reference types="node" />
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;
Expand Down
67 changes: 66 additions & 1 deletion src/crypto.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
1 change: 1 addition & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="node" />
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 {
Expand Down
27 changes: 8 additions & 19 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
28 changes: 28 additions & 0 deletions test/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
58 changes: 57 additions & 1 deletion test/fixtures/crypto.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
}
75 changes: 75 additions & 0 deletions ts_src/crypto.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
25 changes: 6 additions & 19 deletions ts_src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down

0 comments on commit b176d36

Please sign in to comment.