edwards, montgomery, weierstrass: refactor

This commit is contained in:
Paul Miller 2023-01-12 19:40:16 +00:00
parent e45d7c2d25
commit 23cc2aa5d1
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
4 changed files with 251 additions and 286 deletions

@ -2,28 +2,18 @@
// Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
// Differences from @noble/ed25519 1.7:
// 1. Different field element lengths in ed448:
// 1. Variable field element lengths between EDDSA/ECDH:
// EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes
// 2. Different addition formula (doubling is same)
// 3. uvRatio differs between curves (half-expected, not only pow fn changes)
// 4. Point decompression code is different too (unexpected), now using generalized formula
// 4. Point decompression code is different (unexpected), now using generalized formula
// 5. Domain function was no-op for ed25519, but adds some data even with empty context for ed448
import * as mod from './modular.js';
import {
bytesToHex,
concatBytes,
ensureBytes,
numberToBytesLE,
bytesToNumberLE,
hashToPrivateScalar,
BasicCurve,
validateOpts as utilOpts,
Hex,
PrivKey,
} from './utils.js'; // TODO: import * as u from './utils.js'?
import * as ut from './utils.js';
import { ensureBytes, Hex, PrivKey } from './utils.js';
import { Group, GroupConstructor, wNAF } from './group.js';
import { hash_to_field, htfOpts, validateHTFOpts } from './hash-to-curve.js';
import { hash_to_field as hashToField, htfOpts, validateHTFOpts } from './hash-to-curve.js';
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0);
@ -31,49 +21,41 @@ const _1n = BigInt(1);
const _2n = BigInt(2);
const _8n = BigInt(8);
export type CHash = {
(message: Uint8Array | string): Uint8Array;
blockLen: number;
outputLen: number;
create(): any;
};
export type CurveType = BasicCurve<bigint> & {
// Edwards curves must declare params a & d.
export type CurveType = ut.BasicCurve<bigint> & {
// Params: a, d
a: bigint;
d: bigint;
// Hashes
hash: CHash; // Because we need outputLen for DRBG
// The interface, because we need outputLen for DRBG
hash: ut.CHash;
// CSPRNG
randomBytes: (bytesLength?: number) => Uint8Array;
// Probably clears bits in a byte array to produce a valid field element
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
// Used during hashing
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
// Ratio √(u/v)
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
preHash?: CHash;
clearCofactor?: (c: ExtendedPointConstructor, point: ExtendedPointType) => ExtendedPointType;
// Hash to field opts
// RFC 8032 pre-hashing of messages to sign() / verify()
preHash?: ut.CHash;
// Hash to field options
htfDefaults?: htfOpts;
mapToCurve?: (scalar: bigint[]) => { x: bigint; y: bigint };
};
// Should be separate from overrides, since overrides can use information about curve (for example nBits)
function validateOpts(curve: CurveType) {
const opts = utilOpts(curve);
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen))
const opts = ut.validateOpts(curve);
if (typeof opts.hash !== 'function' || !ut.isPositiveInt(opts.hash.outputLen))
throw new Error('Invalid hash function');
for (const i of ['a', 'd'] as const) {
if (typeof opts[i] !== 'bigint')
throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`);
const val = opts[i];
if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
}
for (const fn of ['randomBytes'] as const) {
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
}
for (const fn of [
'adjustScalarBytes',
'domain',
'uvRatio',
'mapToCurve',
'clearCofactor',
] as const) {
for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio', 'mapToCurve'] as const) {
if (opts[fn] === undefined) continue; // Optional
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
}
@ -96,7 +78,7 @@ export type SignatureConstructor = {
fromHex(hex: Hex): SignatureType;
};
// Instance
// Instance of Extended Point with coordinates in X, Y, Z, T
export interface ExtendedPointType extends Group<ExtendedPointType> {
readonly x: bigint;
readonly y: bigint;
@ -109,7 +91,7 @@ export interface ExtendedPointType extends Group<ExtendedPointType> {
toAffine(invZ?: bigint): PointType;
clearCofactor(): ExtendedPointType;
}
// Static methods
// Static methods of Extended Point with coordinates in X, Y, Z, T
export interface ExtendedPointConstructor extends GroupConstructor<ExtendedPointType> {
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtendedPointType;
fromAffine(p: PointType): ExtendedPointType;
@ -117,7 +99,7 @@ export interface ExtendedPointConstructor extends GroupConstructor<ExtendedPoint
normalizeZ(points: ExtendedPointType[]): ExtendedPointType[];
}
// Instance
// Instance of Affine Point with coordinates in X, Y
export interface PointType extends Group<PointType> {
readonly x: bigint;
readonly y: bigint;
@ -127,7 +109,7 @@ export interface PointType extends Group<PointType> {
isTorsionFree(): boolean;
clearCofactor(): PointType;
}
// Static methods
// Static methods of Affine Point with coordinates in X, Y
export interface PointConstructor extends GroupConstructor<PointType> {
new (x: bigint, y: bigint): PointType;
fromHex(hex: Hex): PointType;
@ -166,34 +148,29 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
const Fp = CURVE.Fp as mod.Field<bigint>;
const CURVE_ORDER = CURVE.n;
const fieldLen = Fp.BYTES; // 32 (length of one field element)
if (fieldLen > 2048) throw new Error('Field lengths over 2048 are not supported');
const groupLen = CURVE.nByteLength;
// (2n ** 256n).toString(16);
const maxGroupElement = _2n ** BigInt(groupLen * 8); // previous POW_2_256
const maxGroupElement = _2n ** BigInt(CURVE.nByteLength * 8);
// Function overrides
const { randomBytes } = CURVE;
const modP = Fp.create;
// sqrt(u/v)
function _uvRatio(u: bigint, v: bigint) {
const uvRatio =
CURVE.uvRatio ||
((u: bigint, v: bigint) => {
try {
const value = Fp.sqrt(u * Fp.invert(v));
return { isValid: true, value };
return { isValid: true, value: Fp.sqrt(u * Fp.invert(v)) };
} catch (e) {
return { isValid: false, value: _0n };
}
}
const uvRatio = CURVE.uvRatio || _uvRatio;
const _adjustScalarBytes = (bytes: Uint8Array) => bytes; // NOOP
const adjustScalarBytes = CURVE.adjustScalarBytes || _adjustScalarBytes;
function _domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
});
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes); // NOOP
const domain =
CURVE.domain ||
((data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
if (ctx.length || phflag) throw new Error('Contexts/pre-hash are not supported');
return data;
}
const domain = CURVE.domain || _domain; // NOOP
}); // NOOP
/**
* Extended Point works in extended coordinates: (x, y, z, t) (x=x/z, y=y/z, t=xy).
@ -370,15 +347,16 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
if (zz !== _1n) throw new Error('invZ was invalid');
return new Point(ax, ay);
}
// Custom functions are unsupported for now: no effective cofactor clearing formulas
// This only clears low-torsion component: the point could still be "unsafe".
// To "fix" the point fully, it needs to be multiplied by expensive curve order CURVE.n
clearCofactor(): ExtendedPoint {
if (CURVE.h === _1n) return this; // Fast-path
// clear_cofactor(P) := h_eff * P
// hEff = h for ed25519/ed448. Maybe worth moving to params?
if (CURVE.clearCofactor) return CURVE.clearCofactor(ExtendedPoint, this) as ExtendedPoint;
return this.multiplyUnsafe(CURVE.h);
const { h: cofactor } = CURVE;
if (cofactor === _1n) return this;
return this.multiplyUnsafe(cofactor);
}
}
const wnaf = wNAF(ExtendedPoint, groupLen * 8);
const wnaf = wNAF(ExtendedPoint, CURVE.nByteLength * 8);
function assertExtPoint(other: unknown) {
if (!(other instanceof ExtendedPoint)) throw new TypeError('ExtendedPoint expected');
@ -413,19 +391,20 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// Uses algo from RFC8032 5.1.3.
static fromHex(hex: Hex, strict = true) {
const { d, a } = CURVE;
hex = ensureBytes(hex, fieldLen);
const len = Fp.BYTES;
hex = ensureBytes(hex, len);
// 1. First, interpret the string as an integer in little-endian
// representation. Bit 255 of this number is the least significant
// bit of the x-coordinate and denote this value x_0. The
// y-coordinate is recovered simply by clearing this bit. If the
// resulting value is >= p, decoding fails.
const normed = hex.slice();
const lastByte = hex[fieldLen - 1];
normed[fieldLen - 1] = lastByte & ~0x80;
const y = bytesToNumberLE(normed);
const lastByte = hex[len - 1];
normed[len - 1] = lastByte & ~0x80;
const y = ut.bytesToNumberLE(normed);
if (strict && y >= Fp.ORDER) throw new Error('Expected 0 < hex < P');
if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < 2**256');
if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < CURVE.n');
// 2. To recover the x-coordinate, the curve equation implies
// Ed25519: x² = (y² - 1) / (d y² + 1) (mod p).
@ -459,14 +438,14 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// When compressing point, it's enough to only store its y coordinate
// and use the last byte to encode sign of x.
toRawBytes(): Uint8Array {
const bytes = numberToBytesLE(this.y, fieldLen);
bytes[fieldLen - 1] |= this.x & _1n ? 0x80 : 0;
const bytes = ut.numberToBytesLE(this.y, Fp.BYTES);
bytes[Fp.BYTES - 1] |= this.x & _1n ? 0x80 : 0;
return bytes;
}
// Same as toRawBytes, but returns string.
toHex(): string {
return bytesToHex(this.toRawBytes());
return ut.bytesToHex(this.toRawBytes());
}
isTorsionFree(): boolean {
@ -509,20 +488,20 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// Encodes byte string to elliptic curve
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3
static hashToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve');
msg = ensureBytes(msg);
const u = hash_to_field(msg, 2, { ...CURVE.htfDefaults, ...options } as htfOpts);
const { x: x0, y: y0 } = CURVE.mapToCurve(u[0]);
const { x: x1, y: y1 } = CURVE.mapToCurve(u[1]);
const { mapToCurve, htfDefaults } = CURVE;
if (!mapToCurve) throw new Error('No mapToCurve defined for curve');
const u = hashToField(ensureBytes(msg), 2, { ...htfDefaults, ...options } as htfOpts);
const { x: x0, y: y0 } = mapToCurve(u[0]);
const { x: x1, y: y1 } = mapToCurve(u[1]);
const p = new Point(x0, y0).add(new Point(x1, y1)).clearCofactor();
return p;
}
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3
static encodeToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve');
msg = ensureBytes(msg);
const u = hash_to_field(msg, 1, { ...CURVE.htfDefaults, ...options } as htfOpts);
const { x, y } = CURVE.mapToCurve(u[0]);
const { mapToCurve, htfDefaults } = CURVE;
if (!mapToCurve) throw new Error('No mapToCurve defined for curve');
const u = hashToField(ensureBytes(msg), 1, { ...htfDefaults, ...options } as htfOpts);
const { x, y } = mapToCurve(u[0]);
return new Point(x, y).clearCofactor();
}
}
@ -536,9 +515,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
}
static fromHex(hex: Hex) {
const bytes = ensureBytes(hex, 2 * fieldLen);
const r = Point.fromHex(bytes.slice(0, fieldLen), false);
const s = bytesToNumberLE(bytes.slice(fieldLen, 2 * fieldLen));
const len = Fp.BYTES;
const bytes = ensureBytes(hex, 2 * len);
const r = Point.fromHex(bytes.slice(0, len), false);
const s = ut.bytesToNumberLE(bytes.slice(len, 2 * len));
return new Signature(r, s);
}
@ -551,17 +531,17 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
}
toRawBytes() {
return concatBytes(this.r.toRawBytes(), numberToBytesLE(this.s, fieldLen));
return ut.concatBytes(this.r.toRawBytes(), ut.numberToBytesLE(this.s, Fp.BYTES));
}
toHex() {
return bytesToHex(this.toRawBytes());
return ut.bytesToHex(this.toRawBytes());
}
}
// Little-endian SHA512 with modulo n
function modlLE(hash: Uint8Array): bigint {
return mod.mod(bytesToNumberLE(hash), CURVE_ORDER);
function modnLE(hash: Uint8Array): bigint {
return mod.mod(ut.bytesToNumberLE(hash), CURVE_ORDER);
}
/**
@ -572,6 +552,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
*/
function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint {
if (!max) throw new TypeError('Specify max value');
// No > 0 check: done in bigint case
if (typeof num === 'number' && Number.isSafeInteger(num)) num = BigInt(num);
if (typeof num === 'bigint' && num < max) {
if (strict) {
@ -583,34 +564,29 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
throw new TypeError('Expected valid scalar: 0 < scalar < max');
}
function checkPrivateKey(key: PrivKey) {
// Normalize bigint / number / string to Uint8Array
key =
typeof key === 'bigint' || typeof key === 'number'
? numberToBytesLE(normalizeScalar(key, maxGroupElement), groupLen)
: ensureBytes(key);
if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes, got ${key.length}`);
return key;
}
// Takes 64 bytes
function getKeyFromHash(hashed: Uint8Array) {
// First 32 bytes of 64b uniformingly random input are taken,
// clears 3 bits of it to produce a random field element.
const head = adjustScalarBytes(hashed.slice(0, groupLen));
// Second 32 bytes is called key prefix (5.1.6)
const prefix = hashed.slice(groupLen, 2 * groupLen);
// The actual private scalar
const scalar = modlLE(head);
// Point on Edwards curve aka public key
const point = Point.BASE.multiply(scalar);
const pointBytes = point.toRawBytes();
return { head, prefix, scalar, point, pointBytes };
}
/** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */
function getExtendedPublicKey(key: PrivKey) {
return getKeyFromHash(CURVE.hash(checkPrivateKey(key)));
const groupLen = CURVE.nByteLength;
// Normalize bigint / number / string to Uint8Array
const keyb =
typeof key === 'bigint' || typeof key === 'number'
? ut.numberToBytesLE(normalizeScalar(key, maxGroupElement), groupLen)
: key;
// Hash private key with curve's hash function to produce uniformingly random input
// We check byte lengths e.g.: ensureBytes(64, hash(ensureBytes(32, key)))
const hashed = ensureBytes(CURVE.hash(ensureBytes(keyb, groupLen)), 2 * groupLen);
// First half's bits are cleared to produce a random field element.
const head = adjustScalarBytes(hashed.slice(0, groupLen));
// Second half is called key prefix (5.1.6)
const prefix = hashed.slice(groupLen, 2 * groupLen);
// The actual private scalar
const scalar = modnLE(head);
// Point on Edwards curve aka public key
const point = Point.BASE.multiply(scalar);
// Uint8Array representation
const pointBytes = point.toRawBytes();
return { head, prefix, scalar, point, pointBytes };
}
/**
@ -625,7 +601,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const EMPTY = new Uint8Array();
function hashDomainToScalar(message: Uint8Array, context: Hex = EMPTY) {
context = ensureBytes(context);
return modlLE(CURVE.hash(domain(message, context, !!CURVE.preHash)));
return modnLE(CURVE.hash(domain(message, context, !!CURVE.preHash)));
}
/** Signs message with privateKey. RFC8032 5.1.6 */
@ -633,9 +609,9 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
message = ensureBytes(message);
if (CURVE.preHash) message = CURVE.preHash(message);
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privateKey);
const r = hashDomainToScalar(concatBytes(prefix, message), context);
const r = hashDomainToScalar(ut.concatBytes(prefix, message), context);
const R = Point.BASE.multiply(r); // R = rG
const k = hashDomainToScalar(concatBytes(R.toRawBytes(), pointBytes, message), context); // k = hash(R+P+msg)
const k = hashDomainToScalar(ut.concatBytes(R.toRawBytes(), pointBytes, message), context); // k = hash(R+P+msg)
const s = mod.mod(r + k * scalar, CURVE_ORDER); // s = r + kp
return new Signature(R, s).toRawBytes();
}
@ -672,7 +648,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const { r, s } = sig;
const SB = ExtendedPoint.BASE.multiplyUnsafe(s);
const k = hashDomainToScalar(
concatBytes(r.toRawBytes(), publicKey.toRawBytes(), message),
ut.concatBytes(r.toRawBytes(), publicKey.toRawBytes(), message),
context
);
const kA = ExtendedPoint.fromAffine(publicKey).multiplyUnsafe(k);
@ -692,13 +668,13 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
/**
* Not needed for ed25519 private keys. Needed if you use scalars directly (rare).
*/
hashToPrivateScalar: (hash: Hex): bigint => hashToPrivateScalar(hash, CURVE_ORDER, true),
hashToPrivateScalar: (hash: Hex): bigint => ut.hashToPrivateScalar(hash, CURVE_ORDER, true),
/**
* ed25519 private keys are uniform 32-bit strings. We do not need to check for
* modulo bias like we do in secp256k1 randomPrivateKey()
*/
randomPrivateKey: (): Uint8Array => randomBytes(fieldLen),
randomPrivateKey: (): Uint8Array => randomBytes(Fp.BYTES),
/**
* We're doing scalar multiplication (used in getPublicKey etc) with precomputed BASE_POINT

@ -1,11 +1,6 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import * as mod from './modular.js';
import {
ensureBytes,
numberToBytesLE,
bytesToNumberLE,
// nLength,
} from './utils.js';
import { ensureBytes, numberToBytesLE, bytesToNumberLE, isPositiveInt } from './utils.js';
const _0n = BigInt(0);
const _1n = BigInt(1);
@ -38,7 +33,7 @@ function validateOpts(curve: CurveType) {
}
for (const i of ['montgomeryBits', 'nByteLength'] as const) {
if (curve[i] === undefined) continue; // Optional
if (!Number.isSafeInteger(curve[i]))
if (!isPositiveInt(curve[i]))
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
}
for (const fn of ['adjustScalarBytes', 'domain', 'powPminus2'] as const) {

@ -1,8 +1,6 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Short Weierstrass curve. The formula is: y² = x³ + ax + b
// TODO: sync vs async naming
// TODO: default randomBytes
// Differences from @noble/secp256k1 1.7:
// 1. Different double() formula (but same addition)
// 2. Different sqrt() function
@ -11,19 +9,8 @@
// 5. Support for different hash functions
import * as mod from './modular.js';
import {
bytesToHex,
bytesToNumberBE,
concatBytes,
ensureBytes,
hexToBytes,
hexToNumber,
numberToHexUnpadded,
hashToPrivateScalar,
Hex,
PrivKey,
} from './utils.js';
import * as utils from './utils.js';
import * as ut from './utils.js';
import { bytesToHex, Hex, PrivKey } from './utils.js';
import { hash_to_field, htfOpts, validateHTFOpts } from './hash-to-curve.js';
import { Group, GroupConstructor, wNAF } from './group.js';
@ -32,39 +19,45 @@ type EndomorphismOpts = {
beta: bigint;
splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint };
};
export type BasicCurve<T> = utils.BasicCurve<T> & {
export type BasicCurve<T> = ut.BasicCurve<T> & {
// Params: a, b
a: T;
b: T;
// TODO: move into options?
// Optional params
// Executed before privkey validation. Useful for P521 with var-length priv key
normalizePrivateKey?: (key: PrivKey) => PrivKey;
// Whether to execute modular division on a private key, useful for bls curves with cofactor > 1
wrapPrivateKey?: boolean;
// Endomorphism options for Koblitz curves
endo?: EndomorphismOpts;
// Torsions, can be optimized via endomorphisms
// When a cofactor != 1, there can be an effective methods to:
// 1. Determine whether a point is torsion-free
isTorsionFree?: (c: ProjectiveConstructor<T>, point: ProjectivePointType<T>) => boolean;
// 2. Clear torsion component
clearCofactor?: (
c: ProjectiveConstructor<T>,
point: ProjectivePointType<T>
) => ProjectivePointType<T>;
// Hash to field opts
// Hash to field options
htfDefaults?: htfOpts;
mapToCurve?: (scalar: bigint[]) => { x: T; y: T };
};
// DER encoding utilities
// ASN.1 DER encoding utilities
class DERError extends Error {
constructor(message: string) {
super(message);
}
}
function sliceDER(s: string): string {
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;
}
function parseDERInt(data: Uint8Array) {
},
parseInt(data: Uint8Array): { data: bigint; left: Uint8Array } {
if (data.length < 2 || data[0] !== 0x02) {
throw new DERError(`Invalid signature integer tag: ${bytesToHex(data)}`);
}
@ -77,31 +70,26 @@ function parseDERInt(data: Uint8Array) {
if (res[0] === 0x00 && res[1] <= 0x7f) {
throw new DERError('Invalid signature integer: trailing length');
}
return { data: bytesToNumberBE(res), left: data.subarray(len + 2) };
}
function parseDERSignature(data: Uint8Array) {
return { data: ut.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 } = parseDERInt(data.subarray(2));
const { data: s, left: rBytesLeft } = parseDERInt(sBytes);
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 };
}
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0);
const _1n = BigInt(1);
const _3n = BigInt(3);
},
};
type Entropy = Hex | true;
type SignOpts = { lowS?: boolean; extraEntropy?: Entropy };
export type SignOpts = { lowS?: boolean; extraEntropy?: Entropy };
/**
* ### Design rationale for types
@ -125,7 +113,7 @@ type SignOpts = { lowS?: boolean; extraEntropy?: Entropy };
* TODO: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#unique-symbol
*/
// Instance
// Instance for 3d XYZ points
export interface ProjectivePointType<T> extends Group<ProjectivePointType<T>> {
readonly x: T;
readonly y: T;
@ -134,14 +122,14 @@ export interface ProjectivePointType<T> extends Group<ProjectivePointType<T>> {
multiplyUnsafe(scalar: bigint): ProjectivePointType<T>;
toAffine(invZ?: T): PointType<T>;
}
// Static methods
// Static methods for 3d XYZ points
export interface ProjectiveConstructor<T> extends GroupConstructor<ProjectivePointType<T>> {
new (x: T, y: T, z: T): ProjectivePointType<T>;
fromAffine(p: PointType<T>): ProjectivePointType<T>;
toAffineBatch(points: ProjectivePointType<T>[]): PointType<T>[];
normalizeZ(points: ProjectivePointType<T>[]): ProjectivePointType<T>[];
}
// Instance
// Instance for 2d XY points
export interface PointType<T> extends Group<PointType<T>> {
readonly x: T;
readonly y: T;
@ -152,7 +140,7 @@ export interface PointType<T> extends Group<PointType<T>> {
assertValidity(): void;
multiplyAndAddUnsafe(Q: PointType<T>, a: bigint, b: bigint): PointType<T> | undefined;
}
// Static methods
// Static methods for 2d XY points
export interface PointConstructor<T> extends GroupConstructor<PointType<T>> {
new (x: T, y: T): PointType<T>;
fromHex(hex: Hex): PointType<T>;
@ -168,7 +156,7 @@ export type CurvePointsType<T> = BasicCurve<T> & {
};
function validatePointOpts<T>(curve: CurvePointsType<T>) {
const opts = utils.validateOpts(curve);
const opts = ut.validateOpts(curve);
const Fp = opts.Fp;
for (const i of ['a', 'b'] as const) {
if (!Fp.isValid(curve[i]))
@ -207,22 +195,14 @@ export type CurvePointsRes<T> = {
isWithinCurveOrder: (num: bigint) => boolean;
};
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0);
const _1n = BigInt(1);
const _3n = BigInt(3);
export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
const CURVE = validatePointOpts(opts);
const Fp = CURVE.Fp;
// Lengths
// All curves has same field / group length as for now, but it can be different for other curves
const { nByteLength, nBitLength } = CURVE;
const groupLen = nByteLength;
// Not using ** operator with bigints for old engines.
// 2n ** (8n * 32n) == 2n << (8n * 32n - 1n)
//const FIELD_MASK = _2n << (_8n * BigInt(fieldLen) - _1n);
// function numToFieldStr(num: bigint): string {
// if (typeof num !== 'bigint') throw new Error('Expected bigint');
// if (!(_0n <= num && num < FIELD_MASK)) throw new Error(`Expected number < 2^${fieldLen * 8}`);
// return num.toString(16).padStart(2 * fieldLen, '0');
// }
const { Fp } = CURVE; // All curves has same field / group length as for now, but they can differ
/**
* y² = x³ + ax + b: Short weierstrass curve formula
@ -235,6 +215,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return Fp.add(Fp.add(x3, Fp.mul(x, a)), b); // x3 + a * x + b
}
// Valid group elements reside in range 1..n-1
function isWithinCurveOrder(num: bigint): boolean {
return _0n < num && num < CURVE.n;
}
@ -242,28 +223,30 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
/**
* Validates if a private key is valid and converts it to bigint form.
* Supports two options, that are passed when CURVE is initialized:
* - `normalizePrivateKey()` executed before all checks. Useful for P521 with var-length priv key
* - `wrapPrivateKey()` executed after most checks, but before `0 < key < n` check. Useful for BLS12-381 with cofactor higher than 1.
* - `normalizePrivateKey()` executed before all checks
* - `wrapPrivateKey` when true, executed after most checks, but before `0 < key < n`
*/
function normalizePrivateKey(key: PrivKey): bigint {
if (typeof CURVE.normalizePrivateKey === 'function') {
key = CURVE.normalizePrivateKey(key);
}
const { normalizePrivateKey: custom, nByteLength: groupLen, wrapPrivateKey, n: order } = CURVE;
if (typeof custom === 'function') key = custom(key);
let num: bigint;
if (typeof key === 'bigint') {
// Curve order check is done below
num = key;
} else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) {
} else if (ut.isPositiveInt(key)) {
num = BigInt(key);
} else if (typeof key === 'string') {
if (key.length !== 2 * groupLen) throw new Error(`Expected ${groupLen} bytes of private key`);
num = hexToNumber(key);
// Validates individual octets
num = ut.hexToNumber(key);
} else if (key instanceof Uint8Array) {
if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes of private key`);
num = bytesToNumberBE(key);
num = ut.bytesToNumberBE(key);
} else {
throw new TypeError('Expected valid private key');
}
if (CURVE.wrapPrivateKey) num = mod.mod(num, CURVE.n);
// Useful for curves with cofactor != 1
if (wrapPrivateKey) num = mod.mod(num, order);
if (!isWithinCurveOrder(num)) throw new Error('Expected private key: 0 < key < n');
return num;
}
@ -273,7 +256,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
* Scalars are valid only if they are less than curve order.
*/
function normalizeScalar(num: number | bigint): bigint {
if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) return BigInt(num);
if (ut.isPositiveInt(num)) return BigInt(num);
if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num;
throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n');
}
@ -535,24 +518,25 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return new Point(ax, ay);
}
isTorsionFree(): boolean {
if (CURVE.h === _1n) return true; // No subgroups, always torsion fee
if (CURVE.isTorsionFree) return CURVE.isTorsionFree(ProjectivePoint, this);
// is multiplyUnsafe(CURVE.n) is always ok, same as for edwards?
throw new Error('Unsupported!');
const { h: cofactor, isTorsionFree } = CURVE;
if (cofactor === _1n) return true; // No subgroups, always torsion-free
if (isTorsionFree) return isTorsionFree(ProjectivePoint, this);
throw new Error('isTorsionFree() has not been declared for the elliptic curve');
}
// Clear cofactor of G1
// https://eprint.iacr.org/2019/403
clearCofactor(): ProjectivePoint {
if (CURVE.h === _1n) return this; // Fast-path
if (CURVE.clearCofactor) return CURVE.clearCofactor(ProjectivePoint, this) as ProjectivePoint;
const { h: cofactor, clearCofactor } = CURVE;
if (cofactor === _1n) return this; // Fast-path
if (clearCofactor) return clearCofactor(ProjectivePoint, this) as ProjectivePoint;
return this.multiplyUnsafe(CURVE.h);
}
}
const wnaf = wNAF(ProjectivePoint, CURVE.endo ? nBitLength / 2 : nBitLength);
const _bits = CURVE.nBitLength;
const wnaf = wNAF(ProjectivePoint, CURVE.endo ? Math.ceil(_bits / 2) : _bits);
function assertPrjPoint(other: unknown) {
if (!(other instanceof ProjectivePoint)) throw new TypeError('ProjectivePoint expected');
}
// Stores precomputed values for points.
const pointPrecomputes = new WeakMap<Point, ProjectivePoint[]>();
@ -593,7 +577,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
* @param hex short/long ECDSA hex
*/
static fromHex(hex: Hex): Point {
const { x, y } = CURVE.fromBytes(ensureBytes(hex));
const { x, y } = CURVE.fromBytes(ut.ensureBytes(hex));
const point = new Point(x, y);
point.assertValidity();
return point;
@ -640,34 +624,37 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return new Point(this.x, Fp.negate(this.y));
}
protected toProj() {
return ProjectivePoint.fromAffine(this);
}
// Adds point to itself
double() {
return ProjectivePoint.fromAffine(this).double().toAffine();
return this.toProj().double().toAffine();
}
// Adds point to other point
add(other: Point) {
return ProjectivePoint.fromAffine(this).add(ProjectivePoint.fromAffine(other)).toAffine();
return this.toProj().add(ProjectivePoint.fromAffine(other)).toAffine();
}
// Subtracts other point from the point
subtract(other: Point) {
return this.add(other.negate());
}
multiply(scalar: number | bigint) {
return ProjectivePoint.fromAffine(this).multiply(scalar, this).toAffine();
return this.toProj().multiply(scalar, this).toAffine();
}
multiplyUnsafe(scalar: bigint) {
return ProjectivePoint.fromAffine(this).multiplyUnsafe(scalar).toAffine();
return this.toProj().multiplyUnsafe(scalar).toAffine();
}
clearCofactor() {
return ProjectivePoint.fromAffine(this).clearCofactor().toAffine();
return this.toProj().clearCofactor().toAffine();
}
isTorsionFree(): boolean {
return ProjectivePoint.fromAffine(this).isTorsionFree();
return this.toProj().isTorsionFree();
}
/**
@ -677,7 +664,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
* @returns non-zero affine point
*/
multiplyAndAddUnsafe(Q: Point, a: bigint, b: bigint): Point | undefined {
const P = ProjectivePoint.fromAffine(this);
const P = this.toProj();
const aP =
a === _0n || a === _1n || this !== Point.BASE ? P.multiplyUnsafe(a) : P.multiply(a);
const bQ = ProjectivePoint.fromAffine(Q).multiplyUnsafe(b);
@ -688,23 +675,26 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
// Encodes byte string to elliptic curve
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3
static hashToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve');
msg = ensureBytes(msg);
const { mapToCurve } = CURVE;
if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined');
msg = ut.ensureBytes(msg);
const u = hash_to_field(msg, 2, { ...CURVE.htfDefaults, ...options } as htfOpts);
const { x: x0, y: y0 } = CURVE.mapToCurve(u[0]);
const { x: x1, y: y1 } = CURVE.mapToCurve(u[1]);
const p = new Point(x0, y0).add(new Point(x1, y1)).clearCofactor();
return p;
const { x: x0, y: y0 } = mapToCurve(u[0]);
const { x: x1, y: y1 } = mapToCurve(u[1]);
return new Point(x0, y0).add(new Point(x1, y1)).clearCofactor();
}
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3
static encodeToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve');
msg = ensureBytes(msg);
const { mapToCurve } = CURVE;
if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined');
msg = ut.ensureBytes(msg);
const u = hash_to_field(msg, 1, { ...CURVE.htfDefaults, ...options } as htfOpts);
const { x, y } = CURVE.mapToCurve(u[0]);
const { x, y } = mapToCurve(u[0]);
return new Point(x, y).clearCofactor();
}
}
return {
Point: Point as PointConstructor<T>,
ProjectivePoint: ProjectivePoint as ProjectiveConstructor<T>,
@ -743,15 +733,15 @@ export type CurveType = BasicCurve<bigint> & {
// Default options
lowS?: boolean;
// Hashes
hash: utils.CHash; // Because we need outputLen for DRBG
hash: ut.CHash; // Because we need outputLen for DRBG
hmac: HmacFnSync;
randomBytes: (bytesLength?: number) => Uint8Array;
truncateHash?: (hash: Uint8Array, truncateOnly?: boolean) => bigint;
};
function validateOpts(curve: CurveType) {
const opts = utils.validateOpts(curve);
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen))
const opts = ut.validateOpts(curve);
if (typeof opts.hash !== 'function' || !ut.isPositiveInt(opts.hash.outputLen))
throw new Error('Invalid hash function');
if (typeof opts.hmac !== 'function') throw new Error('Invalid hmac function');
if (typeof opts.randomBytes !== 'function') throw new Error('Invalid randomBytes function');
@ -835,7 +825,7 @@ class HmacDrbg {
out.push(sl);
len += this.v.length;
}
return concatBytes(...out);
return ut.concatBytes(...out);
}
// There is no need in clean() method
// It's useless, there are no guarantees with JS GC
@ -858,10 +848,12 @@ export function weierstrass(curveDef: CurveType): CurveFn {
weierstrassPoints({
...CURVE,
toBytes(c, point, isCompressed: boolean): Uint8Array {
const x = Fp.toBytes(point.x);
const cat = ut.concatBytes;
if (isCompressed) {
return concatBytes(new Uint8Array([point.hasEvenY() ? 0x02 : 0x03]), Fp.toBytes(point.x));
return cat(Uint8Array.from([point.hasEvenY() ? 0x02 : 0x03]), x);
} else {
return concatBytes(new Uint8Array([0x04]), Fp.toBytes(point.x), Fp.toBytes(point.y));
return cat(Uint8Array.from([0x04]), x, Fp.toBytes(point.y));
}
},
fromBytes(bytes: Uint8Array) {
@ -869,7 +861,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const header = bytes[0];
// this.assertValidity() is done inside of fromHex
if (len === compressedLen && (header === 0x02 || header === 0x03)) {
const x = bytesToNumberBE(bytes.subarray(1));
const x = ut.bytesToNumberBE(bytes.subarray(1));
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
@ -923,7 +915,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
function bits2int_2(bytes: Uint8Array): bigint {
const delta = bytes.length * 8 - CURVE.nBitLength;
const num = bytesToNumberBE(bytes);
const num = ut.bytesToNumberBE(bytes);
return delta > 0 ? num >> BigInt(delta) : num;
}
@ -944,15 +936,17 @@ export function weierstrass(curveDef: CurveType): CurveFn {
this.assertValidity();
}
// pair (32 bytes of r, 32 bytes of s)
// pair (bytes of r, bytes of s)
static fromCompact(hex: Hex) {
const arr = hex instanceof Uint8Array;
const name = 'Signature.fromCompact';
if (typeof hex !== 'string' && !arr)
throw new TypeError(`${name}: Expected string or Uint8Array`);
const str = arr ? bytesToHex(hex) : hex;
if (str.length !== 128) throw new Error(`${name}: Expected 64-byte hex`);
return new Signature(hexToNumber(str.slice(0, 64)), hexToNumber(str.slice(64, 128)));
const gl = CURVE.nByteLength * 2; // group length in hex, not ui8a
if (str.length !== 2 * gl) throw new Error(`${name}: Expected ${gl / 2}-byte hex`);
const slice = (from: number, to: number) => ut.hexToNumber(str.slice(from, to));
return new Signature(slice(0, gl), slice(gl, 2 * gl));
}
// DER encoded ECDSA signature
@ -961,7 +955,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const arr = hex instanceof Uint8Array;
if (typeof hex !== 'string' && !arr)
throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`);
const { r, s } = parseDERSignature(arr ? hex : hexToBytes(hex));
const { r, s } = DER.parseSig(arr ? hex : ut.hexToBytes(hex));
return new Signature(r, s);
}
@ -994,7 +988,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const { r, s, recovery } = this;
if (recovery == null) throw new Error('Cannot recover: recovery bit is not present');
if (![0, 1, 2, 3].includes(recovery)) throw new Error('Cannot recover: invalid recovery bit');
const h = truncateHash(ensureBytes(msgHash));
const h = truncateHash(ut.ensureBytes(msgHash));
const { n } = CURVE;
const radj = recovery === 2 || recovery === 3 ? r + n : r;
if (radj >= Fp.ORDER) throw new Error('Cannot recover: bit 2/3 is invalid with current r');
@ -1027,22 +1021,23 @@ export function weierstrass(curveDef: CurveType): CurveFn {
// DER-encoded
toDERRawBytes() {
return hexToBytes(this.toDERHex());
return ut.hexToBytes(this.toDERHex());
}
toDERHex() {
const sHex = sliceDER(numberToHexUnpadded(this.s));
const rHex = sliceDER(numberToHexUnpadded(this.r));
const { numberToHexUnpadded: toHex } = ut;
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 = numberToHexUnpadded(sHexL);
const rLen = numberToHexUnpadded(rHexL);
const length = numberToHexUnpadded(rHexL + sHexL + 4);
const sLen = toHex(sHexL);
const rLen = toHex(rHexL);
const length = toHex(rHexL + sHexL + 4);
return `30${length}02${rLen}${rHex}02${sLen}${sHex}`;
}
// 32 bytes of r, then 32 bytes of s
// padded bytes of r, then padded bytes of s
toCompactRawBytes() {
return hexToBytes(this.toCompactHex());
return ut.hexToBytes(this.toCompactHex());
}
toCompactHex() {
return numToFieldStr(this.r) + numToFieldStr(this.s);
@ -1071,7 +1066,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
/**
* Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes.
*/
hashToPrivateKey: (hash: Hex): Uint8Array => numToField(hashToPrivateScalar(hash, CURVE_ORDER)),
hashToPrivateKey: (hash: Hex): Uint8Array =>
numToField(ut.hashToPrivateScalar(hash, CURVE_ORDER)),
/**
* Produces cryptographically secure private key from random of size (nBitLength+64)
@ -1141,7 +1137,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
if (!(bytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
const slice = bytes.length > nByteLength ? bytes.slice(0, nByteLength) : bytes;
// const slice = bytes; nByteLength; nBitLength;
let num = bytesToNumberBE(slice);
let num = ut.bytesToNumberBE(slice);
// const { nBitLength } = CURVE;
// const delta = (bytes.length * 8) - nBitLength;
// if (delta > 0) {
@ -1158,28 +1154,28 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return int2octets(z2 < _0n ? z1 : z2);
}
function int2octets(num: bigint): Uint8Array {
return numToField(num); // prohibits >32 bytes
return numToField(num); // prohibits >nByteLength bytes
}
// Steps A, D of RFC6979 3.2
// Creates RFC6979 seed; converts msg/privKey to numbers.
function initSigArgs(msgHash: Hex, privateKey: PrivKey, extraEntropy?: Entropy) {
if (msgHash == null) throw new Error(`sign: expected valid message hash, not "${msgHash}"`);
// Step A is ignored, since we already provide hash instead of msg
const h1 = numToField(truncateHash(ensureBytes(msgHash)));
const h1 = numToField(truncateHash(ut.ensureBytes(msgHash)));
const d = normalizePrivateKey(privateKey);
// K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
const seedArgs = [int2octets(d), bits2octets(h1)];
// RFC6979 3.6: additional k' could be provided
if (extraEntropy != null) {
if (extraEntropy === true) extraEntropy = CURVE.randomBytes(Fp.BYTES);
const e = ensureBytes(extraEntropy);
const e = ut.ensureBytes(extraEntropy);
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);
const m = bits2int(h1);
return { seed, m, d };
}
@ -1243,7 +1239,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
* Signs a message (not message hash).
*/
function signUnhashed(msg: Uint8Array, privKey: PrivKey, opts = defaultSigOpts): Signature {
return sign(CURVE.hash(ensureBytes(msg)), privKey, opts);
return sign(CURVE.hash(ut.ensureBytes(msg)), privKey, opts);
}
// Enable precomputes. Slows down first publicKey computation by 20ms.
@ -1281,7 +1277,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
signature = Signature.fromCompact(signature as Hex);
}
}
msgHash = ensureBytes(msgHash);
msgHash = ut.ensureBytes(msgHash);
} catch (error) {
return false;
}

@ -15,10 +15,8 @@ import { randomBytes } from '@noble/hashes/utils';
import { isogenyMap } from './abstract/hash-to-curve.js';
/**
* secp256k1 belongs to Koblitz curves: it has
* efficiently computable Frobenius endomorphism.
* Endomorphism improves efficiency:
* Uses 2x less RAM, speeds up precomputation by 2x and ECDH / sign key recovery by 20%.
* secp256k1 belongs to Koblitz curves: it has efficiently computable endomorphism.
* Endomorphism uses 2x less RAM, speeds up precomputation by 2x and ECDH / key recovery by 20%.
* Should always be used for Projective's double-and-add multiplication.
* For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit.
* https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066