Skip to content

Do real secp256k1 point->curve checking #1786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
79 changes: 64 additions & 15 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/crypto.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
"result": "71ae15bad52efcecf4c9f672bfbded68a4adb8258f1b95f0d06aefdb5ebd14e9"
}
]
}
}
58 changes: 58 additions & 0 deletions test/fixtures/types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"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
}
]
}
29 changes: 29 additions & 0 deletions test/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
});
85 changes: 71 additions & 14 deletions ts_src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down