From 6c9ef3e01d07b4d15ba9f5f3c7dbbf3c913250b5 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Sun, 4 Aug 2024 19:28:19 +0000 Subject: [PATCH] DANGER: Refactor range checks for private keys and others. --- README.md | 12 +++---- src/abstract/bls.ts | 4 +-- src/abstract/edwards.ts | 61 +++++++++++++++++------------------ src/abstract/montgomery.ts | 21 +++++++------ src/abstract/utils.ts | 41 ++++++++++++++++++------ src/abstract/weierstrass.ts | 63 ++++++++++++++++++------------------- src/ed25519.ts | 7 +++-- src/secp256k1.ts | 36 ++++++++++++++------- 8 files changed, 141 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 9234a17..497e533 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ import { secp256k1 } from '@noble/curves/secp256k1'; // ESM and Common.js ``` - [Implementations](#implementations) - - [ECDSA signature scheme](#ecdsa-signature-scheme) + - [ECDSA signatures over secp256k1 and others](#ecdsa-signatures-over-secp256k1-and-others) - [ECDSA public key recovery & extra entropy](#ecdsa-public-key-recovery--extra-entropy) - [ECDH: Elliptic Curve Diffie-Hellman](#ecdh-elliptic-curve-diffie-hellman) - [Schnorr signatures over secp256k1, BIP340](#schnorr-signatures-over-secp256k1-bip340) @@ -80,24 +80,24 @@ import { secp256k1 } from '@noble/curves/secp256k1'; // ESM and Common.js Implementations use [noble-hashes](https://github.com/paulmillr/noble-hashes). If you want to use a different hashing library, [abstract API](#abstract-api) doesn't depend on them. -#### ECDSA signature scheme - -Generic example that works for all curves, shown for secp256k1: +#### ECDSA signatures over secp256k1 and others ```ts import { secp256k1 } from '@noble/curves/secp256k1'; +// import { p256 } from '@noble/curves/p256'; // or p384 / p521 + const priv = secp256k1.utils.randomPrivateKey(); const pub = secp256k1.getPublicKey(priv); const msg = new Uint8Array(32).fill(1); // message hash (not message) in ecdsa const sig = secp256k1.sign(msg, priv); // `{prehash: true}` option is available const isValid = secp256k1.verify(sig, msg, pub) === true; -// hex strings are also supported besides Uint8Arrays: +// hex strings are also supported besides Uint8Array-s: const privHex = '46c930bc7bb4db7f55da20798697421b98c4175a52c630294d75a84b9c126236'; const pub2 = secp256k1.getPublicKey(privHex); ``` -We support P256 (secp256r1), P384 (secp384r1), P521 (secp521r1). +The same code would work for NIST P256 (secp256r1), P384 (secp384r1) & P521 (secp521r1). #### ECDSA public key recovery & extra entropy diff --git a/src/abstract/bls.ts b/src/abstract/bls.ts index 8e87661..f71c44f 100644 --- a/src/abstract/bls.ts +++ b/src/abstract/bls.ts @@ -2,7 +2,7 @@ // BLS (Barreto-Lynn-Scott) family of pairing-friendly curves. // TODO: import { AffinePoint } from './curve.js'; import { IField, getMinHashLength, mapHashToField } from './modular.js'; -import { Hex, PrivKey, CHash, ensureBytes, cached } from './utils.js'; +import { Hex, PrivKey, CHash, ensureBytes, memoized } from './utils.js'; // prettier-ignore import { MapToCurve, Opts as HTFOpts, H2CPointConstructor, htfBasicOpts, @@ -261,7 +261,7 @@ export function bls(CURVE: CurveType): CurveFn { // add + double in windowed precomputes here, otherwise it would be single op (since X is static) const ATE_NAF = NAfDecomposition(CURVE.params.ateLoopSize); - const calcPairingPrecomputes = cached((point: G2) => { + const calcPairingPrecomputes = memoized((point: G2) => { const p = point; const { x, y } = p.toAffine(); // prettier-ignore diff --git a/src/abstract/edwards.ts b/src/abstract/edwards.ts index cabd4ff..0ca283a 100644 --- a/src/abstract/edwards.ts +++ b/src/abstract/edwards.ts @@ -3,7 +3,7 @@ import { AffinePoint, BasicCurve, Group, GroupConstructor, validateBasic, wNAF } from './curve.js'; import { mod } from './modular.js'; import * as ut from './utils.js'; -import { ensureBytes, FHash, Hex, cached, abool } from './utils.js'; +import { ensureBytes, FHash, Hex, memoized, abool } from './utils.js'; // Be friendly to bad ECMAScript parsers by not using bigint literals // prettier-ignore @@ -46,16 +46,6 @@ function validateOpts(curve: CurveType) { return Object.freeze({ ...opts } as const); } -const isBig = (n: bigint) => typeof n === 'bigint' && _0n <= n; // n in [1..] - -/** - * Asserts min <= n < max - */ -function assertInRange(title: string, n: bigint, min: bigint, max: bigint) { - if (isBig(n) && isBig(min) && isBig(max) && min <= n && n < max) return; - throw new Error(`expected valid ${title}: ${min} <= coord < ${max}, got ${typeof n} ${n}`); -} - // Instance of Extended Point with coordinates in X, Y, Z, T export interface ExtPointType extends Group { readonly ex: bigint; @@ -82,6 +72,10 @@ export interface ExtPointConstructor extends GroupConstructor { fromPrivateKey(privateKey: Hex): ExtPointType; } +/** + * Edwards Curve interface. + * Main methods: `getPublicKey(priv)`, `sign(msg, priv)`, `verify(sig, msg, pub)`. + */ export type CurveFn = { CURVE: ReturnType; getPublicKey: (privateKey: Hex) => Uint8Array; @@ -105,7 +99,13 @@ export type CurveFn = { }; }; -// It is not generic twisted curve for now, but ed25519/ed448 generic implementation +/** + * Creates Twisted Edwards curve with EdDSA signatures. + * @example + * import { Field } from '@noble/curves/abstract/modular'; + * // Before that, define BigInt-s: a, d, p, n, Gx, Gy, h + * const curve = twistedEdwards({ a, d, Fp: Field(p), n, Gx, Gy, h }) + */ export function twistedEdwards(curveDef: CurveType): CurveFn { const CURVE = validateOpts(curveDef) as ReturnType; const { @@ -140,8 +140,8 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { }); // NOOP // 0 <= n < MASK // Coordinates larger than Fp.ORDER are allowed for zip215 - function assertCoordinate(title: string, n: bigint) { - assertInRange('coordinate ' + title, n, _0n, MASK); + function aCoordinate(title: string, n: bigint) { + ut.aInRange('coordinate ' + title, n, _0n, MASK); } function assertPoint(other: unknown) { @@ -149,7 +149,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } // Converts Extended point to default (x, y) coordinates. // Can accept precomputed Z^-1 - for example, from invertBatch. - const cachedAffine = cached((p: Point, iz?: bigint): AffinePoint => { + const toAffineMemo = memoized((p: Point, iz?: bigint): AffinePoint => { const { ex: x, ey: y, ez: z } = p; const is0 = p.is0(); if (iz == null) iz = is0 ? _8n : (Fp.inv(z) as bigint); // 8 was chosen arbitrarily @@ -160,7 +160,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { if (zz !== _1n) throw new Error('invZ was invalid'); return { x: ax, y: ay }; }); - const cachedValidity = cached((p: Point) => { + const assertValidMemo = memoized((p: Point) => { const { a, d } = CURVE; if (p.is0()) throw new Error('bad point: ZERO'); // TODO: optimize, with vars below? // Equation in affine coordinates: ax² + y² = 1 + dx²y² @@ -180,6 +180,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { if (XY !== ZT) throw new Error('bad point: equation left != right (2)'); return true; }); + // Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy). // https://en.wikipedia.org/wiki/Twisted_Edwards_curve#Extended_coordinates class Point implements ExtPointType { @@ -192,10 +193,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { readonly ez: bigint, readonly et: bigint ) { - assertCoordinate('x', ex); - assertCoordinate('y', ey); - assertCoordinate('z', ez); - assertCoordinate('t', et); + aCoordinate('x', ex); + aCoordinate('y', ey); + aCoordinate('z', ez); + aCoordinate('t', et); Object.freeze(this); } @@ -209,8 +210,8 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { static fromAffine(p: AffinePoint): Point { if (p instanceof Point) throw new Error('extended point not allowed'); const { x, y } = p || {}; - assertCoordinate('x', x); - assertCoordinate('y', y); + aCoordinate('x', x); + aCoordinate('y', y); return new Point(x, y, _1n, modP(x * y)); } static normalizeZ(points: Point[]): Point[] { @@ -225,7 +226,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Not required for fromHex(), which always creates valid points. // Could be useful for fromAffine(). assertValidity(): void { - cachedValidity(this); + assertValidMemo(this); } // Compare one point to another. @@ -326,7 +327,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Constant-time multiplication. multiply(scalar: bigint): Point { const n = scalar; - assertInRange('scalar', n, _1n, CURVE_ORDER); + ut.aInRange('scalar', n, _1n, CURVE_ORDER); // 1 <= scalar < L const { p, f } = this.wNAF(n); return Point.normalizeZ([p, f])[0]; } @@ -337,7 +338,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Does NOT allow scalars higher than CURVE.n. multiplyUnsafe(scalar: bigint): Point { const n = scalar; - assertInRange('scalar', n, _0n, CURVE_ORDER); // 0 <= scalar < l + ut.aInRange('scalar', n, _0n, CURVE_ORDER); // 0 <= scalar < L if (n === _0n) return I; if (this.equals(I) || n === _1n) return this; if (this.equals(G)) return this.wNAF(n).p; @@ -361,7 +362,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Converts Extended point to default (x, y) coordinates. // Can accept precomputed Z^-1 - for example, from invertBatch. toAffine(iz?: bigint): AffinePoint { - return cachedAffine(this, iz); + return toAffineMemo(this, iz); } clearCofactor(): Point { @@ -383,10 +384,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { const y = ut.bytesToNumberLE(normed); // RFC8032 prohibits >= p, but ZIP215 doesn't - // zip215=true: 1 <= y < MASK (2^256 for ed25519) - // zip215=false: 1 <= y < P (2^255-19 for ed25519) + // zip215=true: 0 <= y < MASK (2^256 for ed25519) + // zip215=false: 0 <= y < P (2^255-19 for ed25519) const max = zip215 ? MASK : Fp.ORDER; - assertInRange('pointHex.y', y, _0n, max); + ut.aInRange('pointHex.y', y, _0n, max); // Ed25519: x² = (y²-1)/(dy²+1) mod p. Ed448: x² = (y²-1)/(dy²-1) mod p. Generic case: // ax²+y²=1+dx²y² => y²-1=dx²y²-ax² => y²-1=x²(dy²-a) => x²=(y²-1)/(dy²-a) @@ -462,7 +463,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { const R = G.multiply(r).toRawBytes(); // R = rG const k = hashDomainToScalar(options.context, R, pointBytes, msg); // R || A || PH(M) const s = modN(r + k * scalar); // S = (r + k * s) mod L - assertInRange('signature.s', s, _0n, CURVE_ORDER); // 0 <= s < l + ut.aInRange('signature.s', s, _0n, CURVE_ORDER); // 0 <= s < l const res = ut.concatBytes(R, ut.numberToBytesLE(s, Fp.BYTES)); return ensureBytes('result', res, nByteLength * 2); // 64-byte signature } diff --git a/src/abstract/montgomery.ts b/src/abstract/montgomery.ts index 6461096..dda9ae6 100644 --- a/src/abstract/montgomery.ts +++ b/src/abstract/montgomery.ts @@ -1,6 +1,12 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ import { mod, pow } from './modular.js'; -import { bytesToNumberLE, ensureBytes, numberToBytesLE, validateObject } from './utils.js'; +import { + aInRange, + bytesToNumberLE, + ensureBytes, + numberToBytesLE, + validateObject, +} from './utils.js'; const _0n = BigInt(0); const _1n = BigInt(1); @@ -75,12 +81,6 @@ export function montgomery(curveDef: CurveType): CurveFn { return [x_2, x_3]; } - // Accepts 0 as well - function assertFieldElement(n: bigint): bigint { - if (typeof n === 'bigint' && _0n <= n && n < P) return n; - throw new Error('Expected valid scalar 0 < scalar < CURVE.P'); - } - // x25519 from 4 // The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519 const a24 = (CURVE.a - BigInt(2)) / BigInt(4); @@ -90,11 +90,12 @@ export function montgomery(curveDef: CurveType): CurveFn { * @param scalar by which the point would be multiplied * @returns new Point on Montgomery curve */ - function montgomeryLadder(pointU: bigint, scalar: bigint): bigint { - const u = assertFieldElement(pointU); + function montgomeryLadder(u: bigint, scalar: bigint): bigint { + aInRange('u', u, _0n, P); + aInRange('scalar', scalar, _0n, P); // Section 5: Implementations MUST accept non-canonical values and process them as // if they had been reduced modulo the field prime. - const k = assertFieldElement(scalar); + const k = scalar; const x_1 = u; let x_2 = _1n; let z_2 = _0n; diff --git a/src/abstract/utils.ts b/src/abstract/utils.ts index 74d8bee..26a223b 100644 --- a/src/abstract/utils.ts +++ b/src/abstract/utils.ts @@ -179,6 +179,28 @@ export function utf8ToBytes(str: string): Uint8Array { return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809 } +// Is positive bigint +const isPosBig = (n: bigint) => typeof n === 'bigint' && _0n <= n; + +export function inRange(n: bigint, min: bigint, max: bigint) { + return isPosBig(n) && isPosBig(min) && isPosBig(max) && min <= n && n < max; +} + +/** + * Asserts min <= n < max. NOTE: It's < max and not <= max. + * @example + * aInRange('x', x, 1n, 256n); // would assume x is in (1n..255n) + */ +export function aInRange(title: string, n: bigint, min: bigint, max: bigint) { + // Why min <= n < max and not a (min < n < max) OR b (min <= n <= max)? + // consider P=256n, min=0n, max=P + // - a for min=0 would require -1: `inRange('x', x, -1n, P)` + // - b would commonly require subtraction: `inRange('x', x, 0n, P - 1n)` + // - our way is the cleanest: `inRange('x', x, 0n, P) + if (!inRange(n, min, max)) + throw new Error(`expected valid ${title}: ${min} <= n < ${max}, got ${typeof n} ${n}`); +} + // Bit operations /** @@ -330,16 +352,17 @@ export const notImplemented = () => { throw new Error('not implemented'); }; -// Caches computation result in weakmap using first arg. -// Other args are optional and should not affect result (but can be used for performance improvements) -// This for internal use only. -export function cached(fn: (arg: T, ...args: O) => R) { +/** + * Memoizes (caches) computation result. + * Uses WeakMap: the value is going auto-cleaned by GC after last reference is removed. + */ +export function memoized(fn: (arg: T, ...args: O) => R) { const map = new WeakMap(); return (arg: T, ...args: O): R => { - let res = map.get(arg); - if (res !== undefined) return res; - res = fn(arg, ...args); - map.set(arg, res); - return res; + const val = map.get(arg); + if (val !== undefined) return val; + const computed = fn(arg, ...args); + map.set(arg, computed); + return computed; }; } diff --git a/src/abstract/weierstrass.ts b/src/abstract/weierstrass.ts index 362a366..0f6d930 100644 --- a/src/abstract/weierstrass.ts +++ b/src/abstract/weierstrass.ts @@ -3,7 +3,7 @@ import { AffinePoint, BasicCurve, Group, GroupConstructor, validateBasic, wNAF } from './curve.js'; import * as mod from './modular.js'; import * as ut from './utils.js'; -import { CHash, Hex, PrivKey, ensureBytes, cached, abool } from './utils.js'; +import { CHash, Hex, PrivKey, ensureBytes, memoized, abool } from './utils.js'; export type { AffinePoint }; type HmacFnSync = (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array; @@ -233,15 +233,12 @@ export function weierstrassPoints(opts: CurvePointsType): CurvePointsRes(opts: CurvePointsType): CurvePointsRes(opts: CurvePointsType): CurvePointsRes => { + const toAffineMemo = memoized((p: Point, iz?: T): AffinePoint => { const { px: x, py: y, pz: z } = p; // Fast-path for normalized points if (Fp.eql(z, Fp.ONE)) return { x, y }; @@ -288,7 +285,7 @@ export function weierstrassPoints(opts: CurvePointsType): CurvePointsRes { + const assertValidMemo = memoized((p: Point) => { if (p.is0()) { // (0, 1, 0) aka ZERO is invalid in most contexts. // In BLS, ZERO can be serialized, so we allow it. @@ -379,7 +376,7 @@ export function weierstrassPoints(opts: CurvePointsType): CurvePointsRes(opts: CurvePointsType): CurvePointsRes(opts: CurvePointsType): CurvePointsRes(opts: CurvePointsType): CurvePointsRes(opts: CurvePointsType): CurvePointsRes { - return cachedAffine(this, iz); + return toAffineMemo(this, iz); } isTorsionFree(): boolean { const { h: cofactor, isTorsionFree } = CURVE; @@ -708,15 +704,19 @@ export type CurveFn = { }; }; +/** + * Creates short weierstrass curve and ECDSA signature methods for it. + * @example + * import { Field } from '@noble/curves/abstract/modular'; + * // Before that, define BigInt-s: a, b, p, n, Gx, Gy + * const curve = weierstrass({ a, b, Fp: Field(p), n, Gx, Gy, h: 1n }) + */ export function weierstrass(curveDef: CurveType): CurveFn { const CURVE = validateOpts(curveDef) as ReturnType; const { Fp, n: CURVE_ORDER } = CURVE; const compressedLen = Fp.BYTES + 1; // e.g. 33 for 32 const uncompressedLen = 2 * Fp.BYTES + 1; // e.g. 65 for 32 - 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); } @@ -749,7 +749,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { // this.assertValidity() is done inside of fromHex if (len === compressedLen && (head === 0x02 || head === 0x03)) { const x = ut.bytesToNumberBE(tail); - if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); + if (!ut.inRange(x, _1n, Fp.ORDER)) throw new Error('Point is not on curve'); const y2 = weierstrassEquation(x); // y² = x³ + ax + b let y: bigint; try { @@ -815,9 +815,8 @@ export function weierstrass(curveDef: CurveType): CurveFn { } assertValidity(): void { - // can use assertGE here - if (!isWithinCurveOrder(this.r)) throw new Error('r must be 0 < r < CURVE.n'); - if (!isWithinCurveOrder(this.s)) throw new Error('s must be 0 < s < CURVE.n'); + ut.aInRange('r', this.r, _1n, CURVE_ORDER); // r in [1..N] + ut.aInRange('s', this.s, _1n, CURVE_ORDER); // s in [1..N] } addRecoveryBit(recovery: number): RecoveredSignature { @@ -967,9 +966,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { * Converts to bytes. Checks if num in `[0..ORDER_MASK-1]` e.g.: `[0..2^256-1]`. */ function int2octets(num: bigint): Uint8Array { - if (typeof num !== 'bigint') throw new Error('bigint expected'); - if (!(_0n <= num && num < ORDER_MASK)) - throw new Error(`bigint expected < 2^${CURVE.nBitLength}`); + ut.aInRange(`num < 2^${CURVE.nBitLength}`, num, _0n, ORDER_MASK); // works with order, can have different size than numToField! return ut.numberToBytesBE(num, CURVE.nByteLength); } diff --git a/src/ed25519.ts b/src/ed25519.ts index 8f530a8..f7e30ec 100644 --- a/src/ed25519.ts +++ b/src/ed25519.ts @@ -2,7 +2,7 @@ import { sha512 } from '@noble/hashes/sha512'; import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; import { AffinePoint, Group } from './abstract/curve.js'; -import { ExtPointType, twistedEdwards } from './abstract/edwards.js'; +import { CurveFn, ExtPointType, twistedEdwards } from './abstract/edwards.js'; import { createHasher, expand_message_xmd, htfBasicOpts } from './abstract/hash-to-curve.js'; import { Field, FpSqrtEven, isNegativeLE, mod, pow2 } from './abstract/modular.js'; import { montgomery } from './abstract/montgomery.js'; @@ -126,7 +126,10 @@ const ed25519Defaults = /* @__PURE__ */ (() => uvRatio, }) as const)(); -export const ed25519 = /* @__PURE__ */ (() => twistedEdwards(ed25519Defaults))(); +/** + * ed25519 curve with EdDSA signatures. + */ +export const ed25519: CurveFn = /* @__PURE__ */ (() => twistedEdwards(ed25519Defaults))(); function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) { if (ctx.length > 255) throw new Error('Context is too big'); diff --git a/src/secp256k1.ts b/src/secp256k1.ts index 1b42405..1bafaeb 100644 --- a/src/secp256k1.ts +++ b/src/secp256k1.ts @@ -5,7 +5,14 @@ import { createCurve } from './_shortw_utils.js'; import { createHasher, isogenyMap } from './abstract/hash-to-curve.js'; import { Field, mod, pow2 } from './abstract/modular.js'; import type { Hex, PrivKey } from './abstract/utils.js'; -import { bytesToNumberBE, concatBytes, ensureBytes, numberToBytesBE } from './abstract/utils.js'; +import { + inRange, + aInRange, + bytesToNumberBE, + concatBytes, + ensureBytes, + numberToBytesBE, +} from './abstract/utils.js'; import { ProjPointType as PointType, mapToCurveSimpleSWU } from './abstract/weierstrass.js'; const secp256k1P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); @@ -44,6 +51,9 @@ function sqrtMod(y: bigint): bigint { const Fp = Field(secp256k1P, undefined, undefined, { sqrt: sqrtMod }); +/** + * secp256k1 short weierstrass curve and ECDSA signatures over it. + */ export const secp256k1 = createCurve( { a: BigInt(0), // equation params: a, b @@ -92,8 +102,6 @@ export const secp256k1 = createCurve( // Schnorr signatures are superior to ECDSA from above. Below is Schnorr-specific BIP0340 code. // https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki const _0n = BigInt(0); -const fe = (x: bigint) => typeof x === 'bigint' && _0n < x && x < secp256k1P; -const ge = (x: bigint) => typeof x === 'bigint' && _0n < x && x < secp256k1N; /** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */ const TAGGED_HASH_PREFIXES: { [tag: string]: Uint8Array } = {}; function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array { @@ -127,7 +135,7 @@ function schnorrGetExtPubKey(priv: PrivKey) { * @returns valid point checked for being on-curve */ function lift_x(x: bigint): PointType { - if (!fe(x)) throw new Error('bad x: need 0 < x < p'); // Fail if x ≥ p. + aInRange('x', x, _1n, secp256k1P); // Fail if x ≥ p. const xx = modP(x * x); const c = modP(xx * x + BigInt(7)); // Let c = x³ + 7 mod p. let y = sqrtMod(c); // Let y = c^(p+1)/4 mod p. @@ -136,11 +144,12 @@ function lift_x(x: bigint): PointType { p.assertValidity(); return p; } +const num = bytesToNumberBE; /** * Create tagged hash, convert it to bigint, reduce modulo-n. */ function challenge(...args: Uint8Array[]): bigint { - return modN(bytesToNumberBE(taggedHash('BIP0340/challenge', ...args))); + return modN(num(taggedHash('BIP0340/challenge', ...args))); } /** @@ -162,9 +171,9 @@ function schnorrSign( const m = ensureBytes('message', message); const { bytes: px, scalar: d } = schnorrGetExtPubKey(privateKey); // checks for isWithinCurveOrder const a = ensureBytes('auxRand', auxRand, 32); // Auxiliary random data a: a 32-byte array - const t = numTo32b(d ^ bytesToNumberBE(taggedHash('BIP0340/aux', a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a) + const t = numTo32b(d ^ num(taggedHash('BIP0340/aux', a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a) const rand = taggedHash('BIP0340/nonce', t, px, m); // Let rand = hash/nonce(t || bytes(P) || m) - const k_ = modN(bytesToNumberBE(rand)); // Let k' = int(rand) mod n + const k_ = modN(num(rand)); // Let k' = int(rand) mod n if (k_ === _0n) throw new Error('sign failed: k is zero'); // Fail if k' = 0. const { bytes: rx, scalar: k } = schnorrGetExtPubKey(k_); // Let R = k'⋅G. const e = challenge(rx, px, m); // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n. @@ -185,11 +194,11 @@ function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean { const m = ensureBytes('message', message); const pub = ensureBytes('publicKey', publicKey, 32); try { - const P = lift_x(bytesToNumberBE(pub)); // P = lift_x(int(pk)); fail if that fails - const r = bytesToNumberBE(sig.subarray(0, 32)); // Let r = int(sig[0:32]); fail if r ≥ p. - if (!fe(r)) return false; - const s = bytesToNumberBE(sig.subarray(32, 64)); // Let s = int(sig[32:64]); fail if s ≥ n. - if (!ge(s)) return false; + const P = lift_x(num(pub)); // P = lift_x(int(pk)); fail if that fails + const r = num(sig.subarray(0, 32)); // Let r = int(sig[0:32]); fail if r ≥ p. + if (!inRange(r, _1n, secp256k1P)) return false; + const s = num(sig.subarray(32, 64)); // Let s = int(sig[32:64]); fail if s ≥ n. + if (!inRange(s, _1n, secp256k1N)) return false; const e = challenge(numTo32b(r), pointToBytes(P), m); // int(challenge(bytes(r)||bytes(P)||m))%n const R = GmulAdd(P, s, modN(-e)); // R = s⋅G - e⋅P if (!R || !R.hasEvenY() || R.toAffine().x !== r) return false; // -eP == (n-e)P @@ -199,6 +208,9 @@ function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean { } } +/** + * Schnorr signatures over secp256k1. + */ export const schnorr = /* @__PURE__ */ (() => ({ getPublicKey: schnorrGetPublicKey, sign: schnorrSign,