From 9465e60d30a59022b2094f9eb64de96f3608213b Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Thu, 26 Jan 2023 04:24:41 +0000 Subject: [PATCH] More refactoring --- benchmark/index.js | 9 +- src/abstract/bls.ts | 42 +++-- src/abstract/curve.ts | 4 +- src/abstract/edwards.ts | 6 +- src/abstract/hash-to-curve.ts | 34 ++-- src/abstract/montgomery.ts | 6 +- src/abstract/poseidon.ts | 16 +- src/abstract/utils.ts | 6 +- src/abstract/weierstrass.ts | 322 +++++++++++++++------------------- src/secp256k1.ts | 12 +- 10 files changed, 208 insertions(+), 249 deletions(-) diff --git a/benchmark/index.js b/benchmark/index.js index 23082bb..c2fb8a2 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -44,9 +44,8 @@ const G2_VECTORS = readFileSync('../test/bls12-381/bls12-381-g2-test-vectors.txt let p1, p2, oldp1, oldp2; // /BLS -for (let item of [secp256k1, ed25519, ed448, P256, P384, P521, old_secp, noble_ed25519]) { - item.utils.precompute(8); -} +for (let item of [secp256k1, ed25519, ed448, P256, P384, P521]) item.utils.precompute(8); +for (let item of [old_secp, noble_ed25519]) item.utils.precompute(8); const ONLY_NOBLE = process.argv[2] === 'noble'; @@ -76,14 +75,14 @@ export const CURVES = { sign: { samples: 5000, secp256k1_old: ({ msg, priv }) => old_secp.signSync(msg, priv), - secp256k1: ({ msg, priv }) => secp256k1.sign(msg, priv), + secp256k1: ({ msg, priv }) => secp256k1.sign(msg, priv).toCompactRawBytes(), }, verify: { samples: 1000, secp256k1_old: ({ sig, msg, pub }) => { return old_secp.verify(new old_secp.Signature(sig.r, sig.s), msg, pub); }, - secp256k1: ({ sig, msg, pub }) => secp256k1.verify(sig, msg, pub), + secp256k1: ({ sig, msg, pub }) => secp256k1.verify(sig.toCompactRawBytes(), msg, pub), }, getSharedSecret: { samples: 1000, diff --git a/src/abstract/bls.ts b/src/abstract/bls.ts index 607a566..8da95e3 100644 --- a/src/abstract/bls.ts +++ b/src/abstract/bls.ts @@ -11,10 +11,8 @@ * We are using Fp for private keys (shorter) and Fp₂ for signatures (longer). * Some projects may prefer to swap this relation, it is not supported for now. */ -import * as mod from './modular.js'; -import * as ut from './utils.js'; -// Types require separate import -import { Hex, PrivKey } from './utils.js'; +import { Field, hashToPrivateScalar } from './modular.js'; +import { Hex, PrivKey, CHash, bitLen, bitGet, hexToBytes, bytesToHex } from './utils.js'; import * as htf from './hash-to-curve.js'; import { CurvePointsType, @@ -43,32 +41,32 @@ export type CurveType = { htfDefaults: htf.Opts; }; x: bigint; - Fp: mod.Field; - Fr: mod.Field; - Fp2: mod.Field & { + Fp: Field; + Fr: Field; + Fp2: Field & { reim: (num: Fp2) => { re: bigint; im: bigint }; multiplyByB: (num: Fp2) => Fp2; frobeniusMap(num: Fp2, power: number): Fp2; }; - Fp6: mod.Field; - Fp12: mod.Field & { + Fp6: Field; + Fp12: Field & { frobeniusMap(num: Fp12, power: number): Fp12; multiplyBy014(num: Fp12, o0: Fp2, o1: Fp2, o4: Fp2): Fp12; conjugate(num: Fp12): Fp12; finalExponentiate(num: Fp12): Fp12; }; htfDefaults: htf.Opts; - hash: ut.CHash; // Because we need outputLen for DRBG + hash: CHash; // Because we need outputLen for DRBG randomBytes: (bytesLength?: number) => Uint8Array; }; export type CurveFn = { CURVE: CurveType; - Fr: mod.Field; - Fp: mod.Field; - Fp2: mod.Field; - Fp6: mod.Field; - Fp12: mod.Field; + Fr: Field; + Fp: Field; + Fp2: Field; + Fp6: Field; + Fp12: Field; G1: CurvePointsRes; G2: CurvePointsRes; Signature: SignatureCoder; @@ -115,7 +113,7 @@ export function bls( ): CurveFn { // Fields looks pretty specific for curve, so for now we need to pass them with options const { Fp, Fr, Fp2, Fp6, Fp12 } = CURVE; - const BLS_X_LEN = ut.bitLen(CURVE.x); + const BLS_X_LEN = bitLen(CURVE.x); const groupLen = 32; // TODO: calculate; hardcoded for now // Pre-compute coefficients for sparse multiplication @@ -142,7 +140,7 @@ export function bls( Rx = Fp2.div(Fp2.mul(Fp2.mul(Fp2.sub(t0, t3), Rx), Ry), 2n); // ((T0 - T3) * Rx * Ry) / 2 Ry = Fp2.sub(Fp2.sqr(Fp2.div(Fp2.add(t0, t3), 2n)), Fp2.mul(Fp2.sqr(t2), 3n)); // ((T0 + T3) / 2)² - 3 * T2² Rz = Fp2.mul(t0, t4); // T0 * T4 - if (ut.bitGet(CURVE.x, i)) { + if (bitGet(CURVE.x, i)) { // Addition let t0 = Fp2.sub(Ry, Fp2.mul(Qy, Rz)); // Ry - Qy * Rz let t1 = Fp2.sub(Rx, Fp2.mul(Qx, Rz)); // Rx - Qx * Rz @@ -171,7 +169,7 @@ export function bls( for (let j = 0, i = BLS_X_LEN - 2; i >= 0; i--, j++) { const E = ell[j]; f12 = Fp12.multiplyBy014(f12, E[0], Fp2.mul(E[1], Px), Fp2.mul(E[2], Py)); - if (ut.bitGet(x, i)) { + if (bitGet(x, i)) { j += 1; const F = ell[j]; f12 = Fp12.multiplyBy014(f12, F[0], Fp2.mul(F[1], Px), Fp2.mul(F[2], Py)); @@ -182,8 +180,8 @@ export function bls( } const utils = { - hexToBytes: ut.hexToBytes, - bytesToHex: ut.bytesToHex, + hexToBytes: hexToBytes, + bytesToHex: bytesToHex, stringToBytes: htf.stringToBytes, // TODO: do we need to export it here? hashToField: ( @@ -193,7 +191,7 @@ export function bls( ) => htf.hash_to_field(msg, count, { ...CURVE.htfDefaults, ...options }), expandMessageXMD: (msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H = CURVE.hash) => htf.expand_message_xmd(msg, DST, lenInBytes, H), - hashToPrivateKey: (hash: Hex): Uint8Array => Fr.toBytes(mod.hashToPrivateScalar(hash, CURVE.r)), + hashToPrivateKey: (hash: Hex): Uint8Array => Fr.toBytes(hashToPrivateScalar(hash, CURVE.r)), randomBytes: (bytesLength: number = groupLen): Uint8Array => CURVE.randomBytes(bytesLength), randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(groupLen + 8)), }; @@ -342,7 +340,7 @@ export function bls( htfOpts?: htf.htfBasicOpts ): boolean { // @ts-ignore - // console.log('verifyBatch', ut.bytesToHex(signature as any), messages, publicKeys.map(ut.bytesToHex)); + // console.log('verifyBatch', bytesToHex(signature as any), messages, publicKeys.map(bytesToHex)); if (!messages.length) throw new Error('Expected non-empty messages array'); if (publicKeys.length !== messages.length) diff --git a/src/abstract/curve.ts b/src/abstract/curve.ts index 2b15bbb..f92f8ba 100644 --- a/src/abstract/curve.ts +++ b/src/abstract/curve.ts @@ -148,7 +148,7 @@ export function wNAF>(c: GroupConstructor, bits: number) { // Generic BasicCurve interface: works even for polynomial fields (BLS): P, n, h would be ok. // Though generator can be different (Fp2 / Fp6 for BLS). -export type BasicCurve = { +export type AbstractCurve = { Fp: Field; // Field over which we'll do calculations (Fp) n: bigint; // Curve order, total count of valid points in the field nBitLength?: number; // bit length of curve order @@ -161,7 +161,7 @@ export type BasicCurve = { allowInfinityPoint?: boolean; // bls12-381 requires it. ZERO point is valid, but invalid pubkey }; -export function validateBasicCurveOpts(curve: BasicCurve & T) { +export function validateAbsOpts(curve: AbstractCurve & T) { validateField(curve.Fp); for (const i of ['n', 'h'] as const) { const val = curve[i]; diff --git a/src/abstract/edwards.ts b/src/abstract/edwards.ts index 3adba37..96fdec7 100644 --- a/src/abstract/edwards.ts +++ b/src/abstract/edwards.ts @@ -10,7 +10,7 @@ import { Hex, numberToBytesLE, } from './utils.js'; -import { Group, GroupConstructor, wNAF, BasicCurve, validateBasicCurveOpts } from './curve.js'; +import { Group, GroupConstructor, wNAF, AbstractCurve, validateAbsOpts } from './curve.js'; // Be friendly to bad ECMAScript parsers by not using bigint literals like 123n const _0n = BigInt(0); @@ -19,7 +19,7 @@ const _2n = BigInt(2); const _8n = BigInt(8); // Edwards curves must declare params a & d. -export type CurveType = BasicCurve & { +export type CurveType = AbstractCurve & { a: bigint; // curve param a d: bigint; // curve param d hash: FHash; // Hashing @@ -32,7 +32,7 @@ export type CurveType = BasicCurve & { }; function validateOpts(curve: CurveType) { - const opts = validateBasicCurveOpts(curve); + const opts = validateAbsOpts(curve); if (typeof opts.hash !== 'function') throw new Error('Invalid hash function'); for (const i of ['a', 'd'] as const) { const val = opts[i]; diff --git a/src/abstract/hash-to-curve.ts b/src/abstract/hash-to-curve.ts index 0f0f4ad..9acf594 100644 --- a/src/abstract/hash-to-curve.ts +++ b/src/abstract/hash-to-curve.ts @@ -1,7 +1,7 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -import * as ut from './utils.js'; -import * as mod from './modular.js'; import type { Group, GroupConstructor } from './curve.js'; +import { mod, Field } from './modular.js'; +import { CHash, Hex, concatBytes, ensureBytes } from './utils.js'; export type Opts = { // DST: a domain separation tag @@ -24,7 +24,7 @@ export type Opts = { // wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others. // BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247 // TODO: verify that hash is shake if expand==='xof' via types - hash: ut.CHash; + hash: CHash; }; export function validateOpts(opts: Opts) { @@ -87,25 +87,25 @@ export function expand_message_xmd( msg: Uint8Array, DST: Uint8Array, lenInBytes: number, - H: ut.CHash + H: CHash ): Uint8Array { // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 - if (DST.length > 255) DST = H(ut.concatBytes(stringToBytes('H2C-OVERSIZE-DST-'), DST)); + if (DST.length > 255) DST = H(concatBytes(stringToBytes('H2C-OVERSIZE-DST-'), DST)); const b_in_bytes = H.outputLen; const r_in_bytes = H.blockLen; const ell = Math.ceil(lenInBytes / b_in_bytes); if (ell > 255) throw new Error('Invalid xmd length'); - const DST_prime = ut.concatBytes(DST, i2osp(DST.length, 1)); + const DST_prime = concatBytes(DST, i2osp(DST.length, 1)); const Z_pad = i2osp(0, r_in_bytes); const l_i_b_str = i2osp(lenInBytes, 2); const b = new Array(ell); - const b_0 = H(ut.concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime)); - b[0] = H(ut.concatBytes(b_0, i2osp(1, 1), DST_prime)); + const b_0 = H(concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime)); + b[0] = H(concatBytes(b_0, i2osp(1, 1), DST_prime)); for (let i = 1; i <= ell; i++) { const args = [strxor(b_0, b[i - 1]), i2osp(i + 1, 1), DST_prime]; - b[i] = H(ut.concatBytes(...args)); + b[i] = H(concatBytes(...args)); } - const pseudo_random_bytes = ut.concatBytes(...b); + const pseudo_random_bytes = concatBytes(...b); return pseudo_random_bytes.slice(0, lenInBytes); } @@ -114,7 +114,7 @@ export function expand_message_xof( DST: Uint8Array, lenInBytes: number, k: number, - H: ut.CHash + H: CHash ): Uint8Array { // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 // DST = H('H2C-OVERSIZE-DST-' || a_very_long_DST, Math.ceil((lenInBytes * k) / 8)); @@ -162,14 +162,14 @@ export function hash_to_field(msg: Uint8Array, count: number, options: Opts): bi for (let j = 0; j < options.m; j++) { const elm_offset = L * (j + i * options.m); const tv = pseudo_random_bytes.subarray(elm_offset, elm_offset + L); - e[j] = mod.mod(os2ip(tv), options.p); + e[j] = mod(os2ip(tv), options.p); } u[i] = e; } return u; } -export function isogenyMap>(field: F, map: [T[], T[], T[], T[]]) { +export function isogenyMap>(field: F, map: [T[], T[], T[], T[]]) { // Make same order as in spec const COEFF = map.map((i) => Array.from(i).reverse()); return (x: T, y: T) => { @@ -212,9 +212,9 @@ export function hashToCurve( return { // Encodes byte string to elliptic curve // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3 - hashToCurve(msg: ut.Hex, options?: htfBasicOpts) { + hashToCurve(msg: Hex, options?: htfBasicOpts) { if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined'); - msg = ut.ensureBytes(msg); + msg = ensureBytes(msg); const u = hash_to_field(msg, 2, { ...def, DST: def.DST, ...options } as Opts); return Point.fromAffine(mapToCurve(u[0])) .add(Point.fromAffine(mapToCurve(u[1]))) @@ -222,9 +222,9 @@ export function hashToCurve( }, // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3 - encodeToCurve(msg: ut.Hex, options?: htfBasicOpts) { + encodeToCurve(msg: Hex, options?: htfBasicOpts) { if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined'); - msg = ut.ensureBytes(msg); + msg = ensureBytes(msg); const u = hash_to_field(msg, 1, { ...def, DST: def.encodeDST, ...options } as Opts); return Point.fromAffine(mapToCurve(u[0])).clearCofactor(); }, diff --git a/src/abstract/montgomery.ts b/src/abstract/montgomery.ts index 999192f..b658d87 100644 --- a/src/abstract/montgomery.ts +++ b/src/abstract/montgomery.ts @@ -1,5 +1,5 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -import * as mod from './modular.js'; +import { mod, pow } from './modular.js'; import { ensureBytes, numberToBytesLE, bytesToNumberLE } from './utils.js'; const _0n = BigInt(0); @@ -54,12 +54,12 @@ function validateOpts(curve: CurveType) { export function montgomery(curveDef: CurveType): CurveFn { const CURVE = validateOpts(curveDef); const { P } = CURVE; - const modP = (a: bigint) => mod.mod(a, P); + const modP = (a: bigint) => mod(a, P); const montgomeryBits = CURVE.montgomeryBits; const montgomeryBytes = Math.ceil(montgomeryBits / 8); const fieldLen = CURVE.nByteLength; const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes); - const powPminus2 = CURVE.powPminus2 || ((x: bigint) => mod.pow(x, P - BigInt(2), P)); + const powPminus2 = CURVE.powPminus2 || ((x: bigint) => pow(x, P - BigInt(2), P)); /** * Checks for num to be in range: diff --git a/src/abstract/poseidon.ts b/src/abstract/poseidon.ts index c170d94..7e5052d 100644 --- a/src/abstract/poseidon.ts +++ b/src/abstract/poseidon.ts @@ -1,12 +1,10 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -// Poseidon Hash (https://eprint.iacr.org/2019/458.pdf) -// Website: https://www.poseidon-hash.info - -import * as mod from './modular.js'; -// NOTE: we currently don't provide any constants, since different implementations use diffferent constants -// For reference constants see './test/poseidon.test.js' +// Poseidon Hash: https://eprint.iacr.org/2019/458.pdf, https://www.poseidon-hash.info +import { Field, validateField, FpPow } from './modular.js'; +// We don't provide any constants, since different implementations use different constants. +// For reference constants see './test/poseidon.test.js'. export type PoseidonOpts = { - Fp: mod.Field; + Fp: Field; t: number; roundsFull: number; roundsPartial: number; @@ -18,7 +16,7 @@ export type PoseidonOpts = { export function validateOpts(opts: PoseidonOpts) { const { Fp } = opts; - mod.validateField(Fp); + validateField(Fp); for (const i of ['t', 'roundsFull', 'roundsPartial'] as const) { if (typeof opts[i] !== 'number' || !Number.isSafeInteger(opts[i])) throw new Error(`Poseidon: invalid param ${i}=${opts[i]} (${typeof opts[i]})`); @@ -32,7 +30,7 @@ export function validateOpts(opts: PoseidonOpts) { throw new Error(`Poseidon wrong sboxPower=${sboxPower}`); const _sboxPower = BigInt(sboxPower); - let sboxFn = (n: bigint) => mod.FpPow(Fp, n, _sboxPower); + let sboxFn = (n: bigint) => FpPow(Fp, n, _sboxPower); // Unwrapped sbox power for common cases (195->142μs) if (sboxPower === 3) sboxFn = (n: bigint) => Fp.mul(Fp.sqrN(n), n); else if (sboxPower === 5) sboxFn = (n: bigint) => Fp.mul(Fp.sqrN(Fp.sqrN(n)), n); diff --git a/src/abstract/utils.ts b/src/abstract/utils.ts index 4101193..1fb207f 100644 --- a/src/abstract/utils.ts +++ b/src/abstract/utils.ts @@ -2,8 +2,6 @@ const _0n = BigInt(0); const _1n = BigInt(1); const _2n = BigInt(2); - -const str = (a: any): a is string => typeof a === 'string'; const u8a = (a: any): a is Uint8Array => a instanceof Uint8Array; // We accept hex strings besides Uint8Array for simplicity @@ -35,14 +33,14 @@ export function numberToHexUnpadded(num: number | bigint): string { } export function hexToNumber(hex: string): bigint { - if (!str(hex)) throw new Error('hexToNumber: expected string, got ' + typeof hex); + if (typeof hex !== 'string') throw new Error('hexToNumber: expected string, got ' + typeof hex); // Big Endian return BigInt(`0x${hex}`); } // Caching slows it down 2-3x export function hexToBytes(hex: string): Uint8Array { - if (!str(hex)) throw new Error('hexToBytes: expected string, got ' + typeof hex); + if (typeof hex !== 'string') throw new Error('hexToBytes: expected string, got ' + typeof hex); if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex ' + hex.length); const array = new Uint8Array(hex.length / 2); for (let i = 0; i < array.length; i++) { diff --git a/src/abstract/weierstrass.ts b/src/abstract/weierstrass.ts index 12b8444..2dd3e8b 100644 --- a/src/abstract/weierstrass.ts +++ b/src/abstract/weierstrass.ts @@ -1,34 +1,16 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ // Short Weierstrass curve. The formula is: y² = x³ + ax + b import * as mod from './modular.js'; -// import * as ut from './utils.js'; -import { - Hex, - PrivKey, - bytesToHex, - bytesToNumberBE, - ensureBytes, - hexToBytes, - concatBytes, - bitMask, - numberToBytesBE, - CHash, - numberToHexUnpadded, -} from './utils.js'; -import { - Group, - GroupConstructor, - wNAF, - BasicCurve as CBasicCurve, - validateBasicCurveOpts, -} from './curve.js'; +import * as ut from './utils.js'; +import { Hex, PrivKey, ensureBytes, CHash } from './utils.js'; +import { Group, GroupConstructor, wNAF, AbstractCurve, validateAbsOpts } from './curve.js'; type HmacFnSync = (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array; type EndomorphismOpts = { beta: bigint; splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint }; }; -export type BasicCurve = CBasicCurve & { +export type BasicCurve = AbstractCurve & { // Params: a, b a: T; b: T; @@ -47,50 +29,6 @@ export type BasicCurve = CBasicCurve & { clearCofactor?: (c: ProjConstructor, point: ProjPointType) => ProjPointType; }; -// ASN.1 DER encoding utilities -class DERError extends Error { - constructor(message: string) { - super(message); - } -} - -const DER = { - slice(s: string): string { - // Proof: any([(i>=0x80) == (int(hex(i).replace('0x', '').zfill(2)[0], 16)>=8) for i in range(0, 256)]) - // Padding done by numberToHex - return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s; - }, - parseInt(data: Uint8Array): { data: bigint; left: Uint8Array } { - if (data.length < 2 || data[0] !== 0x02) { - throw new DERError(`Invalid signature integer tag: ${bytesToHex(data)}`); - } - const len = data[1]; - const res = data.subarray(2, len + 2); - if (!len || res.length !== len) { - throw new DERError(`Invalid signature integer: wrong length`); - } - // Strange condition, its not about length, but about first bytes of number. - if (res[0] === 0x00 && res[1] <= 0x7f) { - throw new DERError('Invalid signature integer: trailing length'); - } - return { data: bytesToNumberBE(res), left: data.subarray(len + 2) }; - }, - parseSig(data: Uint8Array): { r: bigint; s: bigint } { - if (data.length < 2 || data[0] != 0x30) { - throw new DERError(`Invalid signature tag: ${bytesToHex(data)}`); - } - if (data[1] !== data.length - 2) { - throw new DERError('Invalid signature: incorrect length'); - } - const { data: r, left: sBytes } = DER.parseInt(data.subarray(2)); - const { data: s, left: rBytesLeft } = DER.parseInt(sBytes); - if (rBytesLeft.length) { - throw new DERError(`Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`); - } - return { r, s }; - }, -}; - type Entropy = Hex | true; export type SignOpts = { lowS?: boolean; extraEntropy?: Entropy; prehash?: boolean }; export type VerOpts = { lowS?: boolean; prehash?: boolean }; @@ -154,7 +92,7 @@ export type CurvePointsType = BasicCurve & { }; function validatePointOpts(curve: CurvePointsType) { - const opts = validateBasicCurveOpts(curve); + const opts = validateAbsOpts(curve); const Fp = opts.Fp; for (const i of ['a', 'b'] as const) { if (!Fp.isValid(curve[i])) @@ -190,6 +128,55 @@ export type CurvePointsRes = { isWithinCurveOrder: (num: bigint) => boolean; }; +// ASN.1 DER encoding utilities +const { bytesToNumberBE: b2n, hexToBytes: h2b } = ut; +const DER = { + // asn.1 DER encoding utils + Err: class DERErr extends Error { + constructor(m = '') { + super(m); + } + }, + _parseInt(data: Uint8Array): { d: bigint; l: Uint8Array } { + const { Err: E } = DER; + if (data.length < 2 || data[0] !== 0x02) throw new E('Invalid signature integer tag'); + const len = data[1]; + const res = data.subarray(2, len + 2); + if (!len || res.length !== len) throw new E('Invalid signature integer: wrong length'); + if (res[0] === 0x00 && res[1] <= 0x7f) + throw new E('Invalid signature integer: trailing length'); + // ^ Weird condition: not about length, but about first bytes of number. + return { d: b2n(res), l: data.subarray(len + 2) }; // d is data, l is left + }, + toSig(hex: string | Uint8Array): { r: bigint; s: bigint } { + // parse DER signature + const { Err: E } = DER; + const data = typeof hex === 'string' ? h2b(hex) : hex; + if (!(data instanceof Uint8Array)) throw new Error('ui8a expected'); + let l = data.length; + if (l < 2 || data[0] != 0x30) throw new E('Invalid signature tag'); + if (data[1] !== l - 2) throw new E('Invalid signature: incorrect length'); + const { d: r, l: sBytes } = DER._parseInt(data.subarray(2)); + const { d: s, l: rBytesLeft } = DER._parseInt(sBytes); + if (rBytesLeft.length) throw new E('Invalid signature: left bytes after parsing'); + return { r, s }; + }, + hexFromSig(sig: { r: bigint; s: bigint }): string { + const slice = (s: string): string => (Number.parseInt(s[0], 16) >= 8 ? '00' + s : s); // slice DER + const h = (num: number | bigint) => { + const hex = num.toString(16); + return hex.length & 1 ? `0${hex}` : hex; + }; + const s = slice(h(sig.s)); + const r = slice(h(sig.r)); + const shl = s.length / 2; + const rhl = r.length / 2; + const sl = h(shl); + const rl = h(rhl); + return `30${h(rhl + shl + 4)}02${rl}${r}02${sl}${s}`; + }, +}; + // Be friendly to bad ECMAScript parsers by not using bigint literals like 123n const _0n = BigInt(0); const _1n = BigInt(1); @@ -232,10 +219,10 @@ export function weierstrassPoints(opts: CurvePointsType) { } else if (typeof key === 'string') { if (key.length !== 2 * groupLen) throw new Error(`must be ${groupLen} bytes`); // Validates individual octets - num = bytesToNumberBE(ensureBytes(key)); + num = ut.bytesToNumberBE(ensureBytes(key)); } else if (key instanceof Uint8Array) { if (key.length !== groupLen) throw new Error(`must be ${groupLen} bytes`); - num = bytesToNumberBE(key); + num = ut.bytesToNumberBE(key); } else { throw new Error('private key must be bytes, hex or bigint, not ' + typeof key); } @@ -245,18 +232,18 @@ export function weierstrassPoints(opts: CurvePointsType) { return num; } - const pointPrecomputes = new Map(); + const pointPrecomputes = new Map(); function assertPrjPoint(other: unknown) { - if (!(other instanceof ProjectivePoint)) throw new Error('ProjectivePoint expected'); + if (!(other instanceof Point)) throw new Error('ProjectivePoint expected'); } /** * Projective Point works in 3d / projective (homogeneous) coordinates: (x, y, z) ∋ (x=x/z, y=y/z) * Default Point works in 2d / affine coordinates: (x, y) * We're doing calculations in projective, because its operations don't require costly inversion. */ - class ProjectivePoint implements ProjPointType { - static readonly BASE = new ProjectivePoint(CURVE.Gx, CURVE.Gy, Fp.ONE); - static readonly ZERO = new ProjectivePoint(Fp.ZERO, Fp.ONE, Fp.ZERO); + class Point implements ProjPointType { + static readonly BASE = new Point(CURVE.Gx, CURVE.Gy, Fp.ONE); + static readonly ZERO = new Point(Fp.ZERO, Fp.ONE, Fp.ZERO); constructor(readonly px: T, readonly py: T, readonly pz: T) { if (px == null || !Fp.isValid(px)) throw new Error('x required'); @@ -264,14 +251,14 @@ export function weierstrassPoints(opts: CurvePointsType) { if (pz == null || !Fp.isValid(pz)) throw new Error('z required'); } - static fromAffine(p: AffinePoint): ProjectivePoint { + static fromAffine(p: AffinePoint): Point { const { x, y } = p || {}; if (!p || !Fp.isValid(x) || !Fp.isValid(y)) throw new Error('invalid affine point'); - if (p instanceof ProjectivePoint) throw new Error('projective point not allowed'); + if (p instanceof Point) throw new Error('projective point not allowed'); const is0 = (i: T) => Fp.eql(i, Fp.ZERO); // fromAffine(x:0, y:0) would produce (x:0, y:0, z:1), but we need (x:0, y:1, z:0) - if (is0(x) && is0(y)) return ProjectivePoint.ZERO; - return new ProjectivePoint(x, y, Fp.ONE); + if (is0(x) && is0(y)) return Point.ZERO; + return new Point(x, y, Fp.ONE); } get x(): T { @@ -287,24 +274,24 @@ export function weierstrassPoints(opts: CurvePointsType) { * so this improves performance massively. * Optimization: converts a list of projective points to a list of identical points with Z=1. */ - static normalizeZ(points: ProjectivePoint[]): ProjectivePoint[] { + static normalizeZ(points: Point[]): Point[] { const toInv = Fp.invertBatch(points.map((p) => p.pz)); - return points.map((p, i) => p.toAffine(toInv[i])).map(ProjectivePoint.fromAffine); + return points.map((p, i) => p.toAffine(toInv[i])).map(Point.fromAffine); } /** * Converts hash string or Uint8Array to Point. * @param hex short/long ECDSA hex */ - static fromHex(hex: Hex): ProjectivePoint { - const P = ProjectivePoint.fromAffine(CURVE.fromBytes(ensureBytes(hex))); + static fromHex(hex: Hex): Point { + const P = Point.fromAffine(CURVE.fromBytes(ensureBytes(hex))); P.assertValidity(); return P; } // Multiplies generator point by privateKey. static fromPrivateKey(privateKey: PrivKey) { - return ProjectivePoint.BASE.multiply(normalizePrivateKey(privateKey)); + return Point.BASE.multiply(normalizePrivateKey(privateKey)); } // We calculate precomputes for elliptic curve point multiplication @@ -343,7 +330,7 @@ export function weierstrassPoints(opts: CurvePointsType) { /** * Compare one point to another. */ - equals(other: ProjectivePoint): boolean { + equals(other: Point): boolean { assertPrjPoint(other); const { px: X1, py: Y1, pz: Z1 } = this; const { px: X2, py: Y2, pz: Z2 } = other; @@ -355,8 +342,8 @@ export function weierstrassPoints(opts: CurvePointsType) { /** * Flips point to one corresponding to (x, -y) in Affine coordinates. */ - negate(): ProjectivePoint { - return new ProjectivePoint(this.px, Fp.neg(this.py), this.pz); + negate(): Point { + return new Point(this.px, Fp.neg(this.py), this.pz); } // Renes-Costello-Batina exception-free doubling formula. @@ -399,14 +386,14 @@ export function weierstrassPoints(opts: CurvePointsType) { Z3 = Fp.mul(t2, t1); Z3 = Fp.add(Z3, Z3); // step 30 Z3 = Fp.add(Z3, Z3); - return new ProjectivePoint(X3, Y3, Z3); + return new Point(X3, Y3, Z3); } // Renes-Costello-Batina exception-free addition formula. // There is 30% faster Jacobian formula, but it is not complete. // https://eprint.iacr.org/2015/1060, algorithm 1 // Cost: 12M + 0S + 3*a + 3*b3 + 23add. - add(other: ProjectivePoint): ProjectivePoint { + add(other: Point): Point { assertPrjPoint(other); const { px: X1, py: Y1, pz: Z1 } = this; const { px: X2, py: Y2, pz: Z2 } = other; @@ -453,20 +440,20 @@ export function weierstrassPoints(opts: CurvePointsType) { t0 = Fp.mul(t3, t1); Z3 = Fp.mul(t5, Z3); Z3 = Fp.add(Z3, t0); // step 40 - return new ProjectivePoint(X3, Y3, Z3); + return new Point(X3, Y3, Z3); } - subtract(other: ProjectivePoint) { + subtract(other: Point) { return this.add(other.negate()); } private is0() { - return this.equals(ProjectivePoint.ZERO); + return this.equals(Point.ZERO); } - private wNAF(n: bigint): { p: ProjectivePoint; f: ProjectivePoint } { - return wnaf.wNAFCached(this, pointPrecomputes, n, (comp: ProjectivePoint[]) => { + private wNAF(n: bigint): { p: Point; f: Point } { + return wnaf.wNAFCached(this, pointPrecomputes, n, (comp: Point[]) => { const toInv = Fp.invertBatch(comp.map((p) => p.pz)); - return comp.map((p, i) => p.toAffine(toInv[i])).map(ProjectivePoint.fromAffine); + return comp.map((p, i) => p.toAffine(toInv[i])).map(Point.fromAffine); }); } @@ -475,8 +462,8 @@ export function weierstrassPoints(opts: CurvePointsType) { * It's faster, but should only be used when you don't care about * an exposed private key e.g. sig verification, which works over *public* keys. */ - multiplyUnsafe(n: bigint): ProjectivePoint { - const I = ProjectivePoint.ZERO; + multiplyUnsafe(n: bigint): Point { + const I = Point.ZERO; if (n === _0n) return I; assertGE(n); // Will throw on 0 if (n === _1n) return this; @@ -487,7 +474,7 @@ export function weierstrassPoints(opts: CurvePointsType) { let { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); let k1p = I; let k2p = I; - let d: ProjectivePoint = this; + let d: Point = this; while (k1 > _0n || k2 > _0n) { if (k1 & _1n) k1p = k1p.add(d); if (k2 & _1n) k2p = k2p.add(d); @@ -497,7 +484,7 @@ export function weierstrassPoints(opts: CurvePointsType) { } if (k1neg) k1p = k1p.negate(); if (k2neg) k2p = k2p.negate(); - k2p = new ProjectivePoint(Fp.mul(k2p.px, endo.beta), k2p.py, k2p.pz); + k2p = new Point(Fp.mul(k2p.px, endo.beta), k2p.py, k2p.pz); return k1p.add(k2p); } @@ -509,10 +496,10 @@ export function weierstrassPoints(opts: CurvePointsType) { * @param affinePoint optional point ot save cached precompute windows on it * @returns New point */ - multiply(scalar: bigint): ProjectivePoint { + multiply(scalar: bigint): Point { assertGE(scalar); let n = scalar; - let point: ProjectivePoint, fake: ProjectivePoint; // Fake point is used to const-time mult + let point: Point, fake: Point; // Fake point is used to const-time mult const { endo } = CURVE; if (endo) { const { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); @@ -520,7 +507,7 @@ export function weierstrassPoints(opts: CurvePointsType) { let { p: k2p, f: f2p } = this.wNAF(k2); k1p = wnaf.constTimeNegate(k1neg, k1p); k2p = wnaf.constTimeNegate(k2neg, k2p); - k2p = new ProjectivePoint(Fp.mul(k2p.px, endo.beta), k2p.py, k2p.pz); + k2p = new Point(Fp.mul(k2p.px, endo.beta), k2p.py, k2p.pz); point = k1p.add(k2p); fake = f1p.add(f2p); } else { @@ -529,17 +516,17 @@ export function weierstrassPoints(opts: CurvePointsType) { fake = f; } // Normalize `z` for both points, but return only real one - return ProjectivePoint.normalizeZ([point, fake])[0]; + return Point.normalizeZ([point, fake])[0]; } /** * Efficiently calculate `aP + bQ`. Unsafe, can expose private key, if used incorrectly. * @returns non-zero affine point */ - multiplyAndAddUnsafe(Q: ProjectivePoint, a: bigint, b: bigint): ProjectivePoint | undefined { - const G = ProjectivePoint.BASE; // No Strauss-Shamir trick: we have 10% faster G precomputes + multiplyAndAddUnsafe(Q: Point, a: bigint, b: bigint): Point | undefined { + const G = Point.BASE; // No Strauss-Shamir trick: we have 10% faster G precomputes const mul = ( - P: ProjectivePoint, + P: Point, a: bigint // Select faster multiply() method ) => (a === _0n || a === _1n || !P.equals(G) ? P.multiplyUnsafe(a) : P.multiply(a)); const sum = mul(this, a).add(mul(Q, b)); @@ -565,30 +552,30 @@ export function weierstrassPoints(opts: CurvePointsType) { isTorsionFree(): boolean { const { h: cofactor, isTorsionFree } = CURVE; if (cofactor === _1n) return true; // No subgroups, always torsion-free - if (isTorsionFree) return isTorsionFree(ProjectivePoint, this); + if (isTorsionFree) return isTorsionFree(Point, this); throw new Error('isTorsionFree() has not been declared for the elliptic curve'); } - clearCofactor(): ProjectivePoint { + clearCofactor(): Point { const { h: cofactor, clearCofactor } = CURVE; if (cofactor === _1n) return this; // Fast-path - if (clearCofactor) return clearCofactor(ProjectivePoint, this) as ProjectivePoint; + if (clearCofactor) return clearCofactor(Point, this) as Point; return this.multiplyUnsafe(CURVE.h); } toRawBytes(isCompressed = true): Uint8Array { this.assertValidity(); - return CURVE.toBytes(ProjectivePoint, this, isCompressed); + return CURVE.toBytes(Point, this, isCompressed); } toHex(isCompressed = true): string { - return bytesToHex(this.toRawBytes(isCompressed)); + return ut.bytesToHex(this.toRawBytes(isCompressed)); } } const _bits = CURVE.nBitLength; - const wnaf = wNAF(ProjectivePoint, CURVE.endo ? Math.ceil(_bits / 2) : _bits); + const wnaf = wNAF(Point, CURVE.endo ? Math.ceil(_bits / 2) : _bits); return { - ProjectivePoint: ProjectivePoint as ProjConstructor, + ProjectivePoint: Point as ProjConstructor, normalizePrivateKey, weierstrassEquation, isWithinCurveOrder, @@ -605,11 +592,11 @@ export interface SignatureType { hasHighS(): boolean; normalizeS(): SignatureType; recoverPublicKey(msgHash: Hex): ProjPointType; + toCompactRawBytes(): Uint8Array; + toCompactHex(): string; // DER-encoded toDERRawBytes(isCompressed?: boolean): Uint8Array; toDERHex(isCompressed?: boolean): string; - toCompactRawBytes(): Uint8Array; - toCompactHex(): string; } // Static methods export type SignatureConstructor = { @@ -633,7 +620,7 @@ export type CurveType = BasicCurve & { }; function validateOpts(curve: CurveType) { - const opts = validateBasicCurveOpts(curve); + const opts = validateAbsOpts(curve); if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen)) throw new Error('Invalid hash function'); if (typeof opts.hmac !== 'function') throw new Error('Invalid hmac function'); @@ -699,7 +686,7 @@ function hmacDrbg( out.push(sl); len += v.length; } - return concatBytes(...out); + return ut.concatBytes(...out); }; const genUntil = (seed: Uint8Array, pred: Pred): T => { reset(); @@ -721,6 +708,12 @@ export function weierstrass(curveDef: CurveType): CurveFn { function isValidFieldElement(num: bigint): boolean { return _0n < num && num < Fp.ORDER; // 0 is banned since it's not invertible FE } + function modN(a: bigint) { + return mod.mod(a, CURVE_ORDER); + } + function invN(a: bigint) { + return mod.invert(a, CURVE_ORDER); + } const { ProjectivePoint: Point, @@ -732,7 +725,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { toBytes(c, point, isCompressed: boolean): Uint8Array { const a = point.toAffine(); const x = Fp.toBytes(a.x); - const cat = concatBytes; + const cat = ut.concatBytes; if (isCompressed) { // TODO: hasEvenY return cat(Uint8Array.from([point.hasEvenY() ? 0x02 : 0x03]), x); @@ -746,7 +739,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { const tail = bytes.subarray(1); // this.assertValidity() is done inside of fromHex if (len === compressedLen && (head === 0x02 || head === 0x03)) { - const x = bytesToNumberBE(tail); + const x = ut.bytesToNumberBE(tail); if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); const y2 = weierstrassEquation(x); // y² = x³ + ax + b let y = Fp.sqrt(y2); // y = y² ^ (p+1)/4 @@ -767,7 +760,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { }, }); const numToNByteStr = (num: bigint): string => - bytesToHex(numberToBytesBE(num, CURVE.nByteLength)); + ut.bytesToHex(ut.numberToBytesBE(num, CURVE.nByteLength)); function isBiggerThanHalfOrder(number: bigint) { const HALF = CURVE_ORDER >> _1n; @@ -775,10 +768,10 @@ export function weierstrass(curveDef: CurveType): CurveFn { } function normalizeS(s: bigint) { - return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE_ORDER) : s; + return isBiggerThanHalfOrder(s) ? modN(-s) : s; } // slice bytes num - const slcNum = (b: Uint8Array, from: number, to: number) => bytesToNumberBE(b.slice(from, to)); + const slcNum = (b: Uint8Array, from: number, to: number) => ut.bytesToNumberBE(b.slice(from, to)); /** * ECDSA signature with its (r, s) properties. Supports DER & compact representations. @@ -798,10 +791,9 @@ export function weierstrass(curveDef: CurveType): CurveFn { // DER encoded ECDSA signature // https://bitcoin.stackexchange.com/questions/57644/what-are-the-parts-of-a-bitcoin-transaction-input-script static fromDER(hex: Hex) { - const arr = hex instanceof Uint8Array; - if (typeof hex !== 'string' && !arr) + if (typeof hex !== 'string' && !(hex instanceof Uint8Array)) throw new Error(`Signature.fromDER: Expected string or Uint8Array`); - const { r, s } = DER.parseSig(ensureBytes(hex)); + const { r, s } = DER.toSig(ensureBytes(hex)); return new Signature(r, s); } @@ -824,10 +816,9 @@ export function weierstrass(curveDef: CurveType): CurveFn { if (radj >= Fp.ORDER) throw new Error('recovery id 2 or 3 invalid'); const prefix = (rec & 1) === 0 ? '02' : '03'; const R = Point.fromHex(prefix + numToNByteStr(radj)); - const Fn = mod.Fp(N); - const ir = Fn.inv(radj); // r^-1 - const u1 = Fn.mul(-h, ir); // -hr^-1 - const u2 = Fn.mul(s, ir); // sr^-1 + const ir = invN(radj); // r^-1 + const u1 = modN(-h * ir); // -hr^-1 + const u2 = modN(s * ir); // sr^-1 const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) if (!Q) throw new Error('point at infinify'); // unsafe is fine: no priv data leaked Q.assertValidity(); @@ -840,38 +831,22 @@ export function weierstrass(curveDef: CurveType): CurveFn { } normalizeS() { - return this.hasHighS() - ? new Signature(this.r, mod.mod(-this.s, CURVE_ORDER), this.recovery) - : this; + return this.hasHighS() ? new Signature(this.r, modN(-this.s), this.recovery) : this; } // DER-encoded toDERRawBytes() { - return hexToBytes(this.toDERHex()); + return ut.hexToBytes(this.toDERHex()); } toDERHex() { - const toHex = numberToHexUnpadded; - const sHex = DER.slice(toHex(this.s)); - const rHex = DER.slice(toHex(this.r)); - const sHexL = sHex.length / 2; - const rHexL = rHex.length / 2; - const sLen = toHex(sHexL); - const rLen = toHex(rHexL); - const length = toHex(rHexL + sHexL + 4); - return `30${length}02${rLen}${rHex}02${sLen}${sHex}`; + return DER.hexFromSig({ r: this.r, s: this.s }); } // padded bytes of r, then padded bytes of s toCompactRawBytes() { - // const l = CURVE.nByteLength; - // const a = new Uint8Array(2 * l); - // a.set(numberToBytesBE(this.r, l), 0); - // a.set(numberToBytesBE(this.s, l), l); - // return a; - return hexToBytes(this.toCompactHex()); + return ut.hexToBytes(this.toCompactHex()); } toCompactHex() { - // return bytesToHex(this.toCompactRawBytes()); return numToNByteStr(this.r) + numToNByteStr(this.s); } } @@ -891,7 +866,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { * Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes. */ hashToPrivateKey: (hash: Hex): Uint8Array => - numberToBytesBE(mod.hashToPrivateScalar(hash, CURVE_ORDER), CURVE.nByteLength), + ut.numberToBytesBE(mod.hashToPrivateScalar(hash, CURVE_ORDER), CURVE.nByteLength), /** * Produces cryptographically secure private key from random of size (nBitLength+64) @@ -963,22 +938,22 @@ export function weierstrass(curveDef: CurveType): CurveFn { // For curves with nBitLength % 8 !== 0: bits2octets(bits2octets(m)) !== bits2octets(m) // for some cases, since bytes.length * 8 is not actual bitLength. const delta = bytes.length * 8 - CURVE.nBitLength; // truncate to nBitLength leftmost bits - const num = bytesToNumberBE(bytes); // check for == u8 done here + const num = ut.bytesToNumberBE(bytes); // check for == u8 done here return delta > 0 ? num >> BigInt(delta) : num; }; const bits2int_modN = CURVE.bits2int_modN || function (bytes: Uint8Array): bigint { - return mod.mod(bits2int(bytes), CURVE_ORDER); // can't use bytesToNumberBE here + return modN(bits2int(bytes)); // can't use bytesToNumberBE here }; // NOTE: pads output with zero as per spec - const ORDER_MASK = bitMask(CURVE.nBitLength); + const ORDER_MASK = ut.bitMask(CURVE.nBitLength); function int2octets(num: bigint): Uint8Array { if (typeof num !== 'bigint') throw new Error('Expected bigint'); if (!(_0n <= num && num < ORDER_MASK)) throw new Error(`Expected number < 2^${CURVE.nBitLength}`); // works with order, can have different size than numToField! - return numberToBytesBE(num, CURVE.nByteLength); + return ut.numberToBytesBE(num, CURVE.nByteLength); } // Steps A, D of RFC6979 3.2 // Creates RFC6979 seed; converts msg/privKey to numbers. @@ -1013,31 +988,24 @@ export function weierstrass(curveDef: CurveType): CurveFn { if (e.length !== Fp.BYTES) throw new Error(`sign: Expected ${Fp.BYTES} bytes of extra data`); seedArgs.push(e); } - // seed is constructed from private key and message - // Step D - // V, 0x00 are done in HmacDRBG constructor. - const seed = concatBytes(...seedArgs); + const seed = ut.concatBytes(...seedArgs); // Step D of RFC6979 3.2 const m = h1int; // NOTE: no need to call bits2int second time here, it is inside truncateHash! // Converts signature params into point w r/s, checks result for validity. function k2sig(kBytes: Uint8Array): Signature | undefined { - // const { n } = CURVE; // RFC 6979 Section 3.2, step 3: k = bits2int(T) const k = bits2int(kBytes); // Cannot use fields methods, since it is group element - if (!isWithinCurveOrder(k)) return; - // Important: all mod() calls in the function must be done over `n` - const Fn = mod.Fp(CURVE.n); - const ik = Fn.inv(k); // k^-1 mod n + if (!isWithinCurveOrder(k)) return; // Important: all mod() calls here must be done over N + const ik = invN(k); // k^-1 mod n const q = Point.BASE.multiply(k).toAffine(); // q = Gk - const r = Fn.create(q.x); // r = q.x mod n - if (r === _0n) return; // r=0 invalid - const s = Fn.mul(ik, Fn.add(m, Fn.mul(d, r))); // s = k^-1(m + rd) mod n - if (s === _0n) return; // s=0 invalid + const r = modN(q.x); // r = q.x mod n + if (r === _0n) return; + const s = modN(ik * modN(m + modN(d * r))); // s = k^-1(m + rd) mod n + if (s === _0n) return; let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n); // recovery bit (2 or 3, when q.x > n) let normS = s; if (lowS && isBiggerThanHalfOrder(s)) { - // if lowS was passed, ensure s is always - normS = normalizeS(s); // in the bottom half of CURVE.n - recovery ^= 1; + normS = normalizeS(s); // if lowS was passed, ensure s is always + recovery ^= 1; // // in the bottom half of N } return new Signature(r, normS, recovery); // use normS, not s } @@ -1093,12 +1061,12 @@ export function weierstrass(curveDef: CurveType): CurveFn { signature.assertValidity(); _sig = signature; } else { - // Signature can be represented in 2 ways: compact (64-byte) & DER (variable-length). - // Since DER can also be 64 bytes, we check for it first. + // Signature can be represented in 2 ways: compact (2*nByteLength) & DER (variable-length). + // Since DER can also be 2*nByteLength bytes, we check for it first. try { _sig = Signature.fromDER(signature as Hex); } catch (derError) { - if (!(derError instanceof DERError)) throw derError; + if (!(derError instanceof DER.Err)) throw derError; _sig = Signature.fromCompact(signature as Hex); } } @@ -1109,16 +1077,14 @@ export function weierstrass(curveDef: CurveType): CurveFn { } if (opts.lowS && _sig.hasHighS()) return false; if (opts.prehash) msgHash = CURVE.hash(msgHash); - const { n: N } = CURVE; const { r, s } = _sig; const h = bits2int_modN(msgHash); // Cannot use fields methods, since it is group element - const Fn = mod.Fp(N); - const is = Fn.inv(s); // s^-1 - const u1 = Fn.mul(h, is); // u1 = hs^-1 mod n - const u2 = Fn.mul(r, is); // u2 = rs^-1 mod n + const is = invN(s); // s^-1 + const u1 = modN(h * is); // u1 = hs^-1 mod n + const u2 = modN(r * is); // u2 = rs^-1 mod n const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2)?.toAffine(); // R = u1⋅G + u2⋅P if (!R) return false; - const v = Fn.create(R.x); + const v = modN(R.x); return v === r; } return { diff --git a/src/secp256k1.ts b/src/secp256k1.ts index 90ed701..476c7b3 100644 --- a/src/secp256k1.ts +++ b/src/secp256k1.ts @@ -120,7 +120,7 @@ const TAGS = { /** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */ const TAGGED_HASH_PREFIXES: { [tag: string]: Uint8Array } = {}; -export function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array { +function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array { let tagP = TAGGED_HASH_PREFIXES[tag]; if (tagP === undefined) { const tagH = sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0))); @@ -130,7 +130,6 @@ export function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array { return sha256(concatBytes(tagP, ...messages)); } -const tag = taggedHash; const toRawX = (point: PointType) => point.toRawBytes(true).slice(1); const numTo32b = (n: bigint) => numberToBytesBE(n, 32); const modN = (x: bigint) => mod(x, secp256k1N); @@ -148,7 +147,7 @@ function schnorrGetScalar(priv: bigint) { return { point, scalar, x: toRawX(point) }; } function lift_x(x: bigint): PointType { - if (!fe(x)) throw new Error('not fe'); // Fail if x ≥ p. + if (!fe(x)) throw new Error('bad x: need 0 < x < p'); // Fail if x ≥ p. const c = mod(x * x * x + BigInt(7), secp256k1P); // Let c = x³ + 7 mod p. let y = sqrtMod(c); // Let y = c^(p+1)/4 mod p. if (y % 2n !== 0n) y = mod(-y, secp256k1P); // Return the unique point P such that x(P) = x and @@ -157,7 +156,7 @@ function lift_x(x: bigint): PointType { return p; } function challenge(...args: Uint8Array[]): bigint { - return modN(bytesToNum(tag(TAGS.challenge, ...args))); + return modN(bytesToNum(taggedHash(TAGS.challenge, ...args))); } function schnorrGetPublicKey(privateKey: PrivKey): Uint8Array { return toRawX(Gmul(privateKey)); // Let d' = int(sk). Fail if d' = 0 or d' ≥ n. Return bytes(d'⋅G) @@ -177,8 +176,8 @@ function schnorrSign(message: Hex, privateKey: Hex, auxRand: Hex = randomBytes(3 const { x: px, scalar: d } = schnorrGetScalar(bytesToNum(ensureBytes(privateKey, 32))); const a = ensureBytes(auxRand, 32); // Auxiliary random data a: a 32-byte array // TODO: replace with proper xor? - const t = numTo32b(d ^ bytesToNum(tag(TAGS.aux, a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a) - const rand = tag(TAGS.nonce, t, px, m); // Let rand = hash/nonce(t || bytes(P) || m) + const t = numTo32b(d ^ bytesToNum(taggedHash(TAGS.aux, a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a) + const rand = taggedHash(TAGS.nonce, t, px, m); // Let rand = hash/nonce(t || bytes(P) || m) const k_ = modN(bytesToNum(rand)); // Let k' = int(rand) mod n if (k_ === _0n) throw new Error('sign failed: k is zero'); // Fail if k' = 0. const { point: R, x: rx, scalar: k } = schnorrGetScalar(k_); // Let R = k'⋅G. @@ -217,6 +216,7 @@ export const schnorr = { getPublicKey: schnorrGetPublicKey, sign: schnorrSign, verify: schnorrVerify, + utils: { lift_x, int: bytesToNum, taggedHash }, }; const isoMap = htf.isogenyMap(