Skip to content

Commit

Permalink
DANGER: Refactor range checks for private keys and others.
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Aug 4, 2024
1 parent 43b2923 commit 6c9ef3e
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 104 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/abstract/bls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
61 changes: 31 additions & 30 deletions src/abstract/edwards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ExtPointType> {
readonly ex: bigint;
Expand All @@ -82,6 +72,10 @@ export interface ExtPointConstructor extends GroupConstructor<ExtPointType> {
fromPrivateKey(privateKey: Hex): ExtPointType;
}

/**
* Edwards Curve interface.
* Main methods: `getPublicKey(priv)`, `sign(msg, priv)`, `verify(sig, msg, pub)`.
*/
export type CurveFn = {
CURVE: ReturnType<typeof validateOpts>;
getPublicKey: (privateKey: Hex) => Uint8Array;
Expand All @@ -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<typeof validateOpts>;
const {
Expand Down Expand Up @@ -140,16 +140,16 @@ 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) {
if (!(other instanceof Point)) throw new Error('ExtendedPoint expected');
}
// 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<bigint> => {
const toAffineMemo = memoized((p: Point, iz?: bigint): AffinePoint<bigint> => {
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
Expand All @@ -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²
Expand All @@ -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 {
Expand All @@ -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);
}

Expand All @@ -209,8 +210,8 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
static fromAffine(p: AffinePoint<bigint>): 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[] {
Expand All @@ -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.
Expand Down Expand Up @@ -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];
}
Expand All @@ -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;
Expand All @@ -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<bigint> {
return cachedAffine(this, iz);
return toAffineMemo(this, iz);
}

clearCofactor(): Point {
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
21 changes: 11 additions & 10 deletions src/abstract/montgomery.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
41 changes: 32 additions & 9 deletions src/abstract/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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<T extends object, R, O extends any[]>(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<T extends object, R, O extends any[]>(fn: (arg: T, ...args: O) => R) {
const map = new WeakMap<T, R>();
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;
};
}
Loading

0 comments on commit 6c9ef3e

Please sign in to comment.