Add twisted edwards curve.

This commit is contained in:
Paul Miller 2022-12-09 19:58:53 +00:00
parent 16ae76d185
commit 211c887a57
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
7 changed files with 912 additions and 131 deletions

@ -40,8 +40,8 @@ npm install @noble/curves
```
```ts
// Short Weierstrass curve
import shortw from '@noble/curves/shortw';
import shortw from '@noble/curves/shortw'; // Short Weierstrass curve
import twistede from '@noble/curves/twistede'; // Twisted Edwards curve
import { sha256 } from '@noble/hashes/sha256';
import { hmac } from '@noble/hashes/hmac';
import { concatBytes, randomBytes } from '@noble/hashes/utils';

@ -42,6 +42,11 @@
"import": "./lib/esm/shortw.js",
"default": "./lib/shortw.js"
},
"./twisted": {
"types": "./lib/twisted.d.ts",
"import": "./lib/esm/twisted.js",
"default": "./lib/twisted.js"
},
"./utils": {
"types": "./lib/utils.d.ts",
"import": "./lib/esm/utils.js",
@ -72,4 +77,4 @@
"url": "https://paulmillr.com/funding/"
}
]
}
}

124
src/group.ts Normal file

@ -0,0 +1,124 @@
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Default group related functions
const _0n = BigInt(0);
const _1n = BigInt(1);
export interface Group<T extends Group<T>> {
double(): T;
add(other: T): T;
negate(): T;
}
export type GroupConstructor<T> = {
BASE: T;
ZERO: T;
};
// Not big, but pretty complex and it is easy to break stuff. To avoid too much copy paste
export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) {
const constTimeNegate = (condition: boolean, item: T): T => {
const neg = item.negate();
return condition ? neg : item;
};
const opts = (W: number) => {
if (256 % W) throw new Error('Invalid precomputation window, must be power of 2');
const windows = Math.ceil(bits / W) + 1; // +1, because
const windowSize = 2 ** (W - 1); // -1 because we skip zero
return { windows, windowSize };
};
return {
constTimeNegate,
// non-const time multiplication ladder
unsafeLadder(elm: T, n: bigint) {
let p = c.ZERO;
let d: T = elm;
while (n > _0n) {
if (n & _1n) p = p.add(d);
d = d.double();
n >>= _1n;
}
return p;
},
/**
* Creates a wNAF precomputation window. Used for caching.
* Default window size is set by `utils.precompute()` and is equal to 8.
* Which means we are caching 65536 points: 256 points for every bit from 0 to 256.
* @returns 65K precomputed points, depending on W
*/
precomputeWindow(elm: T, W: number): Group<T>[] {
const { windows, windowSize } = opts(W);
const points: T[] = [];
let p: T = elm;
let base = p;
for (let window = 0; window < windows; window++) {
base = p;
points.push(base);
// =1, because we skip zero
for (let i = 1; i < windowSize; i++) {
base = base.add(p);
points.push(base);
}
p = base.double();
}
return points;
},
/**
* Implements w-ary non-adjacent form for calculating ec multiplication.
* @param n
* @param affinePoint optional 2d point to save cached precompute windows on it.
* @returns real and fake (for const-time) points
*/
wNAF(W: number, precomputes: T[], n: bigint): { p: T; f: T } {
const { windows, windowSize } = opts(W);
let p = c.ZERO;
let f = c.BASE;
const mask = BigInt(2 ** W - 1); // Create mask with W ones: 0b1111 for W=4 etc.
const maxNumber = 2 ** W;
const shiftBy = BigInt(W);
for (let window = 0; window < windows; window++) {
const offset = window * windowSize;
// Extract W bits.
let wbits = Number(n & mask);
// Shift number by W bits.
n >>= shiftBy;
// If the bits are bigger than max size, we'll split those.
// +224 => 256 - 32
if (wbits > windowSize) {
wbits -= maxNumber;
n += _1n;
}
// This code was first written with assumption that 'f' and 'p' will never be infinity point:
// since each addition is multiplied by 2 ** W, it cannot cancel each other. However,
// there is negate now: it is possible that negated element from low value
// would be the same as high element, which will create carry into next window.
// It's not obvious how this can fail, but still worth investigating later.
// Check if we're onto Zero point.
// Add random point inside current window to f.
const offset1 = offset;
const offset2 = offset + Math.abs(wbits) - 1; // -1 because we skip zero
const cond1 = window % 2 !== 0;
const cond2 = wbits < 0;
if (wbits === 0) {
// The most important part for const-time getPublicKey
f = f.add(constTimeNegate(cond1, precomputes[offset1]));
} else {
p = p.add(constTimeNegate(cond2, precomputes[offset2]));
}
}
// JIT-compiler should not eliminate f here, since it will later be used in normalizeZ()
// Even if the variable is still unused, there are some checks which will
// throw an exception, so compiler needs to prove they won't happen, which is hard.
// At this point there is a way to F be infinity-point even if p is not,
// which makes it less const-time: around 1 bigint multiply.
return { p, f };
},
};
}

@ -1,4 +1,6 @@
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Utilities for modular arithmetics
const _0n = BigInt(0);
const _1n = BigInt(1);
const _2n = BigInt(2);
@ -144,3 +146,14 @@ export function sqrt(number: bigint, modulo: bigint): bigint {
}
return r;
}
// Little-endian check for first LE bit (last BE bit);
export const isNegativeLE = (num: bigint, modulo: bigint) => (mod(num, modulo) & _1n) === _1n;
// An idea on modular arithmetic for bls12-381:
// const FIELD = {add, pow, sqrt, mul};
// Functions will take field elements, no need for an additional class
// Could be faster. 1 bigint field will just do operations and mod later:
// instead of 'r = mod(r * b, P)' we will write r = mul(r, b);
// Could be insecure without shape check, so it needs to be done.
// Functions could be inlined by JIT.

@ -1,8 +1,14 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Implementation of Short weierstrass curve. The formula is: y² = x³ + ax + b
// Implementation of Short Weierstrass curve. The formula is: y² = x³ + ax + b
// TODO: sync vs async naming
// TODO: default randomBytes
// Differences from noble/secp256k1:
// 1. Different double() formula (but same addition)
// 2. Different sqrt() function
// 3. truncateHash() truncateOnly mode
// 4. DRBG supports outputLen bigger than outputLen of hmac
import * as mod from './modular.js';
import {
bytesToHex,
@ -12,7 +18,9 @@ import {
hexToBytes,
hexToNumber,
numberToHexUnpadded,
nLength,
} from './utils.js';
import { wNAF } from './group.js';
export type CHash = {
(message: Uint8Array | string): Uint8Array;
@ -91,10 +99,8 @@ function validateOpts(curve: CurveType) {
throw new Error('Expected endomorphism with beta: bigint and splitScalar: function');
}
}
const nBitLength = curve.n.toString(2).length; // Bit size of CURVE.n
const nByteLength = Math.ceil(nBitLength / 8); // Byte size of CURVE.n
// Set defaults
return Object.freeze({ lowS: true, nBitLength, nByteLength, ...curve } as const);
return Object.freeze({ lowS: true, ...nLength(curve.n, curve.nBitLength), ...curve } as const);
}
// TODO: convert bits to bytes aligned to 32 bits? (224 for example)
@ -287,8 +293,8 @@ class HmacDrbg {
if (typeof qByteLen !== 'number' || qByteLen < 2) throw new Error('qByteLen must be a number');
if (typeof hmacFn !== 'function') throw new Error('hmacFn must be a function');
// Step B, Step C: set hashLen to 8*ceil(hlen/8)
this.v = new Uint8Array(this.hashLen).fill(1);
this.k = new Uint8Array(this.hashLen).fill(0);
this.v = new Uint8Array(hashLen).fill(1);
this.k = new Uint8Array(hashLen).fill(0);
this.counter = 0;
}
private hmacSync(...values: Uint8Array[]) {
@ -327,8 +333,11 @@ class HmacDrbg {
// Use only input from curveOpts!
export function weierstrass(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
const CURVE_ORDER = CURVE.n;
// Lengths
const fieldLen = CURVE.nByteLength!; // 32 (length of one field element)
// All curves has same field / group length as for now, but it can be different for other curves
const groupLen = CURVE.nByteLength;
const fieldLen = CURVE.nByteLength; // 32 (length of one field element)
if (fieldLen > 2048) throw new Error('Field lengths over 2048 are not supported');
const compressedLen = fieldLen + 1; // 33
@ -378,11 +387,11 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) {
num = BigInt(key);
} else if (typeof key === 'string') {
key = key.padStart(2 * fieldLen, '0'); // Eth-like hexes
if (key.length !== 2 * fieldLen) throw new Error(`Expected ${fieldLen} bytes of private key`);
key = key.padStart(2 * groupLen, '0'); // Eth-like hexes
if (key.length !== 2 * groupLen) throw new Error(`Expected ${groupLen} bytes of private key`);
num = hexToNumber(key);
} else if (key instanceof Uint8Array) {
if (key.length !== fieldLen) throw new Error(`Expected ${fieldLen} bytes of private key`);
if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes of private key`);
num = bytesToNumber(key);
} else {
throw new TypeError('Expected valid private key');
@ -405,12 +414,12 @@ export function weierstrass(curveDef: CurveType): CurveFn {
}
function isBiggerThanHalfOrder(number: bigint) {
const HALF = CURVE.n >> _1n;
const HALF = CURVE_ORDER >> _1n;
return number > HALF;
}
function normalizeS(s: bigint) {
return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE.n) : s;
return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE_ORDER) : s;
}
function normalizeScalar(num: number | bigint): bigint {
@ -499,6 +508,23 @@ export function weierstrass(curveDef: CurveType): CurveFn {
double(): JacobianPoint {
const { x: X1, y: Y1, z: Z1 } = this;
const { a } = CURVE;
// // Faster algorithm: when a=0
// // From: https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l
// // Cost: 2M + 5S + 6add + 3*2 + 1*3 + 1*8.
if (a === _0n) {
const A = modP(X1 * X1);
const B = modP(Y1 * Y1);
const C = modP(B * B);
const x1b = X1 + B;
const D = modP(_2n * (modP(x1b * x1b) - A - C));
const E = modP(_3n * A);
const F = modP(E * E);
const X3 = modP(F - _2n * D);
const Y3 = modP(E * (D - X3) - _8n * C);
const Z3 = modP(_2n * Y1 * Z1);
return new JacobianPoint(X3, Y3, Z3);
}
const XX = modP(X1 * X1);
const YY = modP(Y1 * Y1);
const YYYY = modP(YY * YY);
@ -520,8 +546,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
// Note: 2007 Bernstein-Lange (11M + 5S + 9add + 4*2) is actually 10% slower.
add(other: JacobianPoint): JacobianPoint {
if (!(other instanceof JacobianPoint)) throw new TypeError('JacobianPoint expected');
// TODO: remove
if (this.equals(JacobianPoint.ZERO)) return other;
const { x: X1, y: Y1, z: Z1 } = this;
const { x: X2, y: Y2, z: Z2 } = other;
if (X2 === _0n || Y2 === _0n) return this;
@ -562,16 +586,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
let n = normalizeScalar(scalar);
if (n === _1n) return this;
if (!CURVE.endo) {
let p = P0;
let d: JacobianPoint = this;
while (n > _0n) {
if (n & _1n) p = p.add(d);
d = d.double();
n >>= _1n;
}
return p;
}
if (!CURVE.endo) return wnaf.unsafeLadder(this, n);
// Apply endomorphism
let { k1neg, k1, k2neg, k2 } = CURVE.endo.splitScalar(n);
@ -591,106 +606,22 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return k1p.add(k2p);
}
/**
* Creates a wNAF precomputation window. Used for caching.
* Default window size is set by `utils.precompute()` and is equal to 8.
* Which means we are caching 65536 points: 256 points for every bit from 0 to 256.
* @returns 65K precomputed points, depending on W
*/
private precomputeWindow(W: number): JacobianPoint[] {
const windows = CURVE.endo
? Math.ceil(CURVE.nBitLength / 2) / W + 1
: CURVE.nBitLength / W + 1;
const points: JacobianPoint[] = [];
let p: JacobianPoint = this;
let base = p;
for (let window = 0; window < windows; window++) {
base = p;
points.push(base);
for (let i = 1; i < 2 ** (W - 1); i++) {
base = base.add(p);
points.push(base);
}
p = base.double();
}
return points;
}
/**
* Implements w-ary non-adjacent form for calculating ec multiplication.
* @param n
* @param affinePoint optional 2d point to save cached precompute windows on it.
* @returns real and fake (for const-time) points
*/
private wNAF(n: bigint, affinePoint?: Point): { p: JacobianPoint; f: JacobianPoint } {
if (!affinePoint && this.equals(JacobianPoint.BASE)) affinePoint = Point.BASE;
const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1;
if (256 % W) {
throw new Error('Point#wNAF: Invalid precomputation window, must be power of 2');
}
// Calculate precomputes on a first run, reuse them after
let precomputes = affinePoint && pointPrecomputes.get(affinePoint);
if (!precomputes) {
precomputes = this.precomputeWindow(W);
precomputes = wnaf.precomputeWindow(this, W) as JacobianPoint[];
if (affinePoint && W !== 1) {
precomputes = JacobianPoint.normalizeZ(precomputes);
pointPrecomputes.set(affinePoint, precomputes);
}
}
// Initialize real and fake points for const-time
let p = JacobianPoint.ZERO;
// Should be G (base) point, since otherwise f can be infinity point in the end
let f = JacobianPoint.BASE;
const nBits = CURVE.endo ? CURVE.nBitLength / 2 : CURVE.nBitLength;
const windows = 1 + Math.ceil(nBits / W); // W=8 17
const windowSize = 2 ** (W - 1); // W=8 128
const mask = BigInt(2 ** W - 1); // Create mask with W ones: 0b11111111 for W=8
const maxNumber = 2 ** W; // W=8 256
const shiftBy = BigInt(W); // W=8 8
for (let window = 0; window < windows; window++) {
const offset = window * windowSize;
// Extract W bits.
let wbits = Number(n & mask);
// Shift number by W bits.
n >>= shiftBy;
// If the bits are bigger than max size, we'll split those.
// +224 => 256 - 32
if (wbits > windowSize) {
wbits -= maxNumber;
n += _1n;
}
// This code was first written with assumption that 'f' and 'p' will never be infinity point:
// since each addition is multiplied by 2 ** W, it cannot cancel each other. However,
// there is negate now: it is possible that negated element from low value
// would be the same as high element, which will create carry into next window.
// It's not obvious how this can fail, but still worth investigating later.
// Check if we're onto Zero point.
// Add random point inside current window to f.
const offset1 = offset;
const offset2 = offset + Math.abs(wbits) - 1;
const cond1 = window % 2 !== 0;
const cond2 = wbits < 0;
if (wbits === 0) {
// The most important part for const-time getPublicKey
f = f.add(constTimeNegate(cond1, precomputes[offset1]));
} else {
p = p.add(constTimeNegate(cond2, precomputes[offset2]));
}
}
// JIT-compiler should not eliminate f here, since it will later be used in normalizeZ()
// Even if the variable is still unused, there are some checks which will
// throw an exception, so compiler needs to prove they won't happen, which is hard.
// At this point there is a way to F be infinity-point even if p is not,
// which makes it less const-time: around 1 bigint multiply.
return { p, f };
return wnaf.wNAF(W, precomputes, n);
}
/**
@ -712,8 +643,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const { k1neg, k1, k2neg, k2 } = CURVE.endo.splitScalar(n);
let { p: k1p, f: f1p } = this.wNAF(k1, affinePoint);
let { p: k2p, f: f2p } = this.wNAF(k2, affinePoint);
k1p = constTimeNegate(k1neg, k1p);
k2p = constTimeNegate(k2neg, k2p);
k1p = wnaf.constTimeNegate(k1neg, k1p);
k2p = wnaf.constTimeNegate(k2neg, k2p);
k2p = new JacobianPoint(modP(k2p.x * CURVE.endo.beta), k2p.y, k2p.z);
point = k1p.add(k2p);
fake = f1p.add(f2p);
@ -747,11 +678,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return new Point(ax, ay);
}
}
// Const-time utility for wNAF
function constTimeNegate(condition: boolean, item: JacobianPoint) {
const neg = item.negate();
return condition ? neg : item;
}
const wnaf = wNAF(JacobianPoint, CURVE.endo ? CURVE.nBitLength / 2 : CURVE.nBitLength);
// Stores precomputed values for points.
const pointPrecomputes = new WeakMap<Point, JacobianPoint[]>();
@ -787,8 +715,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
}
/**
* Supports compressed ECDSA (33-byte) points
* @param bytes 33 bytes
* Supports compressed ECDSA points
* @returns Point instance
*/
private static fromCompressedHex(bytes: Uint8Array) {
@ -816,7 +743,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
/**
* Converts hash string or Uint8Array to Point.
* @param hex 33/65-byte (ECDSA) hex
* @param hex short/long ECDSA hex
*/
static fromHex(hex: Hex): Point {
const bytes = ensureBytes(hex);
@ -986,7 +913,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
normalizeS() {
return this.hasHighS()
? new Signature(this.r, mod.mod(-this.s, CURVE.n), this.recovery)
? new Signature(this.r, mod.mod(-this.s, CURVE_ORDER), this.recovery)
: this;
}
@ -1041,7 +968,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
if (hash.length < minLen || hash.length > 1024) {
throw new Error(`Expected ${minLen}-1024 bytes of private key as per FIPS 186`);
}
const num = mod.mod(bytesToNumber(hash), CURVE.n - _1n) + _1n;
const num = mod.mod(bytesToNumber(hash), CURVE_ORDER - _1n) + _1n;
return numToField(num);
},
@ -1067,8 +994,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
/**
* Computes public key for a private key.
* @param privateKey 32-byte private key
* @param isCompressed whether to return compact (33-byte), or full (65-byte) key
* @param privateKey private key
* @param isCompressed whether to return compact, or full key
* @returns Public key, full by default; short when isCompressed=true
*/
function getPublicKey(privateKey: PrivKey, isCompressed = false): Uint8Array {
@ -1112,7 +1039,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
}
function bits2octets(bytes: Uint8Array): Uint8Array {
const z1 = bits2int(bytes);
const z2 = mod.mod(z1, CURVE.n);
const z2 = mod.mod(z1, CURVE_ORDER);
return int2octets(z2 < _0n ? z1 : z2);
}
function int2octets(num: bigint): Uint8Array {
@ -1253,7 +1180,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
getSharedSecret,
sign,
verify,
Point,
JacobianPoint,
Signature,

698
src/twistede.ts Normal file

@ -0,0 +1,698 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Implementation of Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
// Differences with @noble/ed25519:
// 1. EDDSA & ECDH have different field element lengths (for ed448/x448 only)
// https://www.rfc-editor.org/rfc/rfc8032.html says bitLength is 456 (57 bytes)
// https://www.rfc-editor.org/rfc/rfc7748 says bitLength is 448 (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
// 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, nLength } from './utils.js';
import { wNAF } from './group.js';
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0);
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 = {
// Params: a, d
a: bigint;
d: bigint;
// Field over which we'll do calculations. Verify with:
P: bigint;
// Subgroup order: how many points ed25519 has
n: bigint; // in rfc8032 called l
// Cofactor
h: bigint;
nBitLength?: number;
nByteLength?: number;
// Base point (x, y) aka generator point
Gx: bigint;
Gy: bigint;
// Other constants
a24: bigint;
// ECDH bits (can be different from N bits)
scalarBits: number;
// Hashes
hash: CHash; // Because we need outputLen for DRBG
randomBytes: (bytesLength?: number) => Uint8Array;
adjustScalarBytes: (bytes: Uint8Array) => Uint8Array;
domain: (data: Uint8Array, ctx: Uint8Array, hflag: boolean) => Uint8Array;
uvRatio: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
};
// We accept hex strings besides Uint8Array for simplicity
type Hex = Uint8Array | string;
// Very few implementations accept numbers, we do it to ease learning curve
type PrivKey = Hex | bigint | number;
// Should be separate from overrides, since overrides can use information about curve (for example nBits)
function validateOpts(curve: CurveType) {
if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen))
throw new Error('Invalid hash function');
for (const i of ['a', 'd', 'P', 'n', 'h', 'Gx', 'Gy', 'a24'] as const) {
if (typeof curve[i] !== 'bigint')
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
}
for (const i of ['scalarBits'] as const) {
if (typeof curve[i] !== 'number')
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
}
for (const i of ['nBitLength', 'nByteLength'] as const) {
if (curve[i] === undefined) continue; // Optional
if (!Number.isSafeInteger(curve[i]))
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
}
for (const fn of ['randomBytes', 'adjustScalarBytes', 'domain', 'uvRatio'] as const) {
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
}
// Set defaults
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
}
// Instance
export interface SignatureType {
readonly r: PointType;
readonly s: bigint;
assertValidity(): SignatureType;
toRawBytes(): Uint8Array;
toHex(): string;
}
// Static methods
export type SignatureConstructor = {
new (r: PointType, s: bigint): SignatureType;
fromHex(hex: Hex): SignatureType;
};
// Instance
export interface ExtendedPointType {
readonly x: bigint;
readonly y: bigint;
readonly z: bigint;
readonly t: bigint;
equals(other: ExtendedPointType): boolean;
negate(): ExtendedPointType;
double(): ExtendedPointType;
add(other: ExtendedPointType): ExtendedPointType;
subtract(other: ExtendedPointType): ExtendedPointType;
multiply(scalar: number | bigint, affinePoint?: PointType): ExtendedPointType;
multiplyUnsafe(scalar: number | bigint): ExtendedPointType;
isSmallOrder(): boolean;
isTorsionFree(): boolean;
toAffine(invZ?: bigint): PointType;
}
// Static methods
export type ExtendedPointConstructor = {
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtendedPointType;
BASE: ExtendedPointType;
ZERO: ExtendedPointType;
fromAffine(p: PointType): ExtendedPointType;
toAffineBatch(points: ExtendedPointType[]): PointType[];
normalizeZ(points: ExtendedPointType[]): ExtendedPointType[];
};
// Instance
export interface PointType {
readonly x: bigint;
readonly y: bigint;
_setWindowSize(windowSize: number): void;
toRawBytes(isCompressed?: boolean): Uint8Array;
toHex(isCompressed?: boolean): string;
// toX25519(): Uint8Array;
isTorsionFree(): boolean;
equals(other: PointType): boolean;
negate(): PointType;
add(other: PointType): PointType;
subtract(other: PointType): PointType;
multiply(scalar: number | bigint): PointType;
}
// Static methods
export type PointConstructor = {
BASE: PointType;
ZERO: PointType;
new (x: bigint, y: bigint): PointType;
fromHex(hex: Hex): PointType;
fromPrivateKey(privateKey: PrivKey): PointType;
};
export type PubKey = Hex | PointType;
export type SigType = Hex | SignatureType;
export type CurveFn = {
CURVE: ReturnType<typeof validateOpts>;
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
sign: (message: Hex, privateKey: Hex) => Uint8Array;
verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean;
Point: PointConstructor;
ExtendedPoint: ExtendedPointConstructor;
Signature: SignatureConstructor;
utils: {
mod: (a: bigint, b?: bigint) => bigint;
invert: (number: bigint, modulo?: bigint) => bigint;
randomPrivateKey: () => Uint8Array;
getExtendedPublicKey: (key: PrivKey) => {
head: Uint8Array;
prefix: Uint8Array;
scalar: bigint;
point: PointType;
pointBytes: Uint8Array;
};
};
};
// NOTE: it is not generic twisted curve for now, but ed25519/ed448 generic implementation
export function twistedEdwards(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
const CURVE_ORDER = CURVE.n;
const fieldLen = CURVE.nByteLength; // 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
// Function overrides
const { adjustScalarBytes, randomBytes, uvRatio } = CURVE;
function modP(a: bigint) {
return mod.mod(a, CURVE.P);
}
/**
* Extended Point works in extended coordinates: (x, y, z, t) (x=x/z, y=y/z, t=xy).
* Default Point works in affine coordinates: (x, y)
* https://en.wikipedia.org/wiki/Twisted_Edwards_curve#Extended_coordinates
*/
class ExtendedPoint implements ExtendedPointType {
constructor(readonly x: bigint, readonly y: bigint, readonly z: bigint, readonly t: bigint) {}
static BASE = new ExtendedPoint(CURVE.Gx, CURVE.Gy, _1n, modP(CURVE.Gx * CURVE.Gy));
static ZERO = new ExtendedPoint(_0n, _1n, _1n, _0n);
static fromAffine(p: Point): ExtendedPoint {
if (!(p instanceof Point)) {
throw new TypeError('ExtendedPoint#fromAffine: expected Point');
}
if (p.equals(Point.ZERO)) return ExtendedPoint.ZERO;
return new ExtendedPoint(p.x, p.y, _1n, modP(p.x * p.y));
}
// Takes a bunch of Jacobian Points but executes only one
// invert on all of them. invert is very slow operation,
// so this improves performance massively.
static toAffineBatch(points: ExtendedPoint[]): Point[] {
const toInv = mod.invertBatch(points.map((p) => p.z), CURVE.P);
return points.map((p, i) => p.toAffine(toInv[i]));
}
static normalizeZ(points: ExtendedPoint[]): ExtendedPoint[] {
return this.toAffineBatch(points).map(this.fromAffine);
}
// Compare one point to another.
equals(other: ExtendedPoint): boolean {
assertExtPoint(other);
const { x: X1, y: Y1, z: Z1 } = this;
const { x: X2, y: Y2, z: Z2 } = other;
const X1Z2 = modP(X1 * Z2);
const X2Z1 = modP(X2 * Z1);
const Y1Z2 = modP(Y1 * Z2);
const Y2Z1 = modP(Y2 * Z1);
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
}
// Inverses point to one corresponding to (x, -y) in Affine coordinates.
negate(): ExtendedPoint {
return new ExtendedPoint(modP(-this.x), this.y, this.z, modP(-this.t));
}
// Fast algo for doubling Extended Point.
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd
// Cost: 4M + 4S + 1*a + 6add + 1*2.
double(): ExtendedPoint {
const { a } = CURVE;
const { x: X1, y: Y1, z: Z1 } = this;
const A = modP(X1 * X1); // A = X12
const B = modP(Y1 * Y1); // B = Y12
const C = modP(_2n * modP(Z1 * Z1)); // C = 2*Z12
const D = modP(a * A); // D = a*A
const x1y1 = X1 + Y1;
const E = modP(modP(x1y1 * x1y1) - A - B); // E = (X1+Y1)2-A-B
const G = D + B; // G = D+B
const F = G - C; // F = G-C
const H = D - B; // H = D-B
const X3 = modP(E * F); // X3 = E*F
const Y3 = modP(G * H); // Y3 = G*H
const T3 = modP(E * H); // T3 = E*H
const Z3 = modP(F * G); // Z3 = F*G
return new ExtendedPoint(X3, Y3, Z3, T3);
}
// Fast algo for adding 2 Extended Points.
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#addition-add-2008-hwcd
// Cost: 9M + 1*a + 1*d + 7add.
add(other: ExtendedPoint) {
assertExtPoint(other);
const { a, d } = CURVE;
const { x: X1, y: Y1, z: Z1, t: T1 } = this;
const { x: X2, y: Y2, z: Z2, t: T2 } = other;
const A = modP(X1 * X2); // A = X1*X2
const B = modP(Y1 * Y2); // B = Y1*Y2
const C = modP(T1 * d * T2); // C = T1*d*T2
const D = modP(Z1 * Z2); // D = Z1*Z2
const E = modP((X1 + Y1) * (X2 + Y2) - A - B); // E = (X1+Y1)*(X2+Y2)-A-B
// TODO: do we need to check for same point here? Looks like working without it
const F = D - C; // F = D-C
const G = D + C; // G = D+C
const H = modP(B - a * A); // H = B-a*A
const X3 = modP(E * F); // X3 = E*F
const Y3 = modP(G * H); // Y3 = G*H
const T3 = modP(E * H); // T3 = E*H
const Z3 = modP(F * G); // Z3 = F*G
return new ExtendedPoint(X3, Y3, Z3, T3);
}
subtract(other: ExtendedPoint): ExtendedPoint {
return this.add(other.negate());
}
private wNAF(n: bigint, affinePoint?: Point): ExtendedPoint {
if (!affinePoint && this.equals(ExtendedPoint.BASE)) affinePoint = Point.BASE;
const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1;
let precomputes = affinePoint && pointPrecomputes.get(affinePoint);
if (!precomputes) {
precomputes = wnaf.precomputeWindow(this, W) as ExtendedPoint[];
if (affinePoint && W !== 1) {
precomputes = ExtendedPoint.normalizeZ(precomputes);
pointPrecomputes.set(affinePoint, precomputes);
}
}
const { p, f } = wnaf.wNAF(W, precomputes, n);
return ExtendedPoint.normalizeZ([p, f])[0];
}
// Constant time multiplication.
// Uses wNAF method. Windowed method may be 10% faster,
// but takes 2x longer to generate and consumes 2x memory.
multiply(scalar: number | bigint, affinePoint?: Point): ExtendedPoint {
return this.wNAF(normalizeScalar(scalar, CURVE_ORDER), affinePoint);
}
// Non-constant-time multiplication. Uses double-and-add algorithm.
// It's faster, but should only be used when you don't care about
// an exposed private key e.g. sig verification.
// Allows scalar bigger than curve order, but less than 2^256
multiplyUnsafe(scalar: number | bigint): ExtendedPoint {
let n = normalizeScalar(scalar, CURVE_ORDER, false);
const G = ExtendedPoint.BASE;
const P0 = ExtendedPoint.ZERO;
if (n === _0n) return P0;
if (this.equals(P0) || n === _1n) return this;
if (this.equals(G)) return this.wNAF(n);
return wnaf.unsafeLadder(this, n);
}
// Multiplies point by cofactor and checks if the result is 0.
isSmallOrder(): boolean {
return this.multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO);
}
// Multiplies point by a very big scalar n and checks if the result is 0.
isTorsionFree(): boolean {
return this.multiplyUnsafe(CURVE_ORDER).equals(ExtendedPoint.ZERO);
}
// Converts Extended point to default (x, y) coordinates.
// Can accept precomputed Z^-1 - for example, from invertBatch.
toAffine(invZ?: bigint): Point {
const { x, y, z } = this;
const is0 = this.equals(ExtendedPoint.ZERO);
if (invZ == null) invZ = is0 ? _8n : mod.invert(z, CURVE.P); // 8 was chosen arbitrarily
const ax = modP(x * invZ);
const ay = modP(y * invZ);
const zz = modP(z * invZ);
if (is0) return Point.ZERO;
if (zz !== _1n) throw new Error('invZ was invalid');
return new Point(ax, ay);
}
}
const wnaf = wNAF(ExtendedPoint, groupLen * 8);
function assertExtPoint(other: unknown) {
if (!(other instanceof ExtendedPoint)) throw new TypeError('ExtendedPoint expected');
}
// Stores precomputed values for points.
const pointPrecomputes = new WeakMap<Point, ExtendedPoint[]>();
/**
* Default Point works in affine coordinates: (x, y)
*/
class Point implements PointType {
// Base point aka generator
// public_key = Point.BASE * private_key
static BASE: Point = new Point(CURVE.Gx, CURVE.Gy);
// Identity point aka point at infinity
// point = point + zero_point
static ZERO: Point = new Point(_0n, _1n);
// We calculate precomputes for elliptic curve point multiplication
// using windowed method. This specifies window size and
// stores precomputed values. Usually only base point would be precomputed.
_WINDOW_SIZE?: number;
constructor(readonly x: bigint, readonly y: bigint) {}
// "Private method", don't use it directly.
_setWindowSize(windowSize: number) {
this._WINDOW_SIZE = windowSize;
pointPrecomputes.delete(this);
}
// Converts hash string or Uint8Array to Point.
// Uses algo from RFC8032 5.1.3.
static fromHex(hex: Hex, strict = true) {
const { d, P, a } = CURVE;
hex = ensureBytes(hex, fieldLen);
// 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);
if (strict && y >= P) throw new Error('Expected 0 < hex < P');
if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < 2**256');
// 2. To recover the x-coordinate, the curve equation implies
// Ed25519: x² = (y² - 1) / (d y² + 1) (mod p).
// Ed448: x² = (y² - 1) / (d y² - 1) (mod p).
// For generic case:
// a*x²+y²=1+d*x²*y²
// -> y²-1 = d*x²*y²-a*x²
// -> y²-1 = x² (d*y²-a)
// -> x² = (y²-1) / (d*y²-a)
// The denominator is always non-zero mod p. Let u = y² - 1 and v = d y² + 1.
const y2 = modP(y * y);
const u = modP(y2 - _1n);
const v = modP(d * y2 - a);
let { isValid, value: x } = uvRatio(u, v);
if (!isValid) throw new Error('Point.fromHex: invalid y coordinate');
// 4. Finally, use the x_0 bit to select the right square root. If
// x = 0, and x_0 = 1, decoding fails. Otherwise, if x_0 != x mod
// 2, set x <-- p - x. Return the decoded point (x,y).
const isXOdd = (x & _1n) === _1n;
const isLastByteOdd = (lastByte & 0x80) !== 0;
if (isLastByteOdd !== isXOdd) {
x = modP(-x);
}
return new Point(x, y);
}
static fromPrivateKey(privateKey: PrivKey) {
return getExtendedPublicKey(privateKey).point;
}
// There can always be only two x values (x, -x) for any y
// 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;
return bytes;
}
// Same as toRawBytes, but returns string.
toHex(): string {
return bytesToHex(this.toRawBytes());
}
isTorsionFree(): boolean {
return ExtendedPoint.fromAffine(this).isTorsionFree();
}
equals(other: Point): boolean {
return this.x === other.x && this.y === other.y;
}
negate() {
return new Point(modP(-this.x), this.y);
}
add(other: Point) {
return ExtendedPoint.fromAffine(this).add(ExtendedPoint.fromAffine(other)).toAffine();
}
subtract(other: Point) {
return this.add(other.negate());
}
/**
* Constant time multiplication.
* @param scalar Big-Endian number
* @returns new point
*/
multiply(scalar: number | bigint): Point {
return ExtendedPoint.fromAffine(this).multiply(scalar, this).toAffine();
}
}
/**
* EDDSA signature.
*/
class Signature implements SignatureType {
constructor(readonly r: Point, readonly s: bigint) {
this.assertValidity();
}
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));
return new Signature(r, s);
}
assertValidity() {
const { r, s } = this;
if (!(r instanceof Point)) throw new Error('Expected Point instance');
// 0 <= s < l
normalizeScalar(s, CURVE_ORDER, false);
return this;
}
toRawBytes() {
return concatBytes(this.r.toRawBytes(), numberToBytesLE(this.s, fieldLen));
}
toHex() {
return bytesToHex(this.toRawBytes());
}
}
// Little Endian
function bytesToNumberLE(uint8a: Uint8Array): bigint {
if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array');
return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse()));
}
// -------------------------
// Little-endian SHA512 with modulo n
function modlLE(hash: Uint8Array): bigint {
return mod.mod(bytesToNumberLE(hash), CURVE_ORDER);
}
/**
* Checks for num to be in range:
* For strict == true: `0 < num < max`.
* For strict == false: `0 <= num < max`.
* Converts non-float safe numbers to bigints.
*/
function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint {
if (!max) throw new TypeError('Specify max value');
if (typeof num === 'number' && Number.isSafeInteger(num)) num = BigInt(num);
if (typeof num === 'bigint' && num < max) {
if (strict) {
if (_0n < num) return num;
} else {
if (_0n <= num) return num;
}
}
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)));
}
/**
* Calculates ed25519 public key. RFC8032 5.1.5
* 1. private key is hashed with sha512, then first 32 bytes are taken from the hash
* 2. 3 least significant bits of the first byte are cleared
*/
function getPublicKey(privateKey: PrivKey): Uint8Array {
return getExtendedPublicKey(privateKey).pointBytes;
}
/** Signs message with privateKey. RFC8032 5.1.6 */
function sign(message: Hex, privateKey: Hex): Uint8Array {
message = ensureBytes(message);
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privateKey);
const rDomain = CURVE.domain(concatBytes(prefix, message), new Uint8Array(), false);
const r = modlLE(CURVE.hash(rDomain)); // r = hash(prefix + msg)
const R = Point.BASE.multiply(r); // R = rG
const kDomain = CURVE.domain(
concatBytes(R.toRawBytes(), pointBytes, message),
new Uint8Array(),
false
);
const k = modlLE(CURVE.hash(kDomain)); // k = hash(R+P+msg)
const s = mod.mod(r + k * scalar, CURVE_ORDER); // s = r + kp
return new Signature(R, s).toRawBytes();
}
// Helper functions because we have async and sync methods.
function prepareVerification(sig: SigType, message: Hex, publicKey: PubKey) {
message = ensureBytes(message);
// When hex is passed, we check public key fully.
// When Point instance is passed, we assume it has already been checked, for performance.
// If user passes Point/Sig instance, we assume it has been already verified.
// We don't check its equations for performance. We do check for valid bounds for s though
// We always check for: a) s bounds. b) hex validity
if (publicKey instanceof Point) {
// ignore
} else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') {
publicKey = Point.fromHex(publicKey, false);
} else {
throw new Error(`Invalid publicKey: ${publicKey}`);
}
if (sig instanceof Signature) sig.assertValidity();
else if (sig instanceof Uint8Array || typeof sig === 'string') sig = Signature.fromHex(sig);
else throw new Error(`Wrong signature: ${sig}`);
const { r, s } = sig;
const SB = ExtendedPoint.BASE.multiplyUnsafe(s);
return { r, s, SB, pub: publicKey, msg: message };
}
function finishVerification(publicKey: Point, r: Point, SB: ExtendedPoint, hashed: Uint8Array) {
const k = modlLE(hashed);
const kA = ExtendedPoint.fromAffine(publicKey).multiplyUnsafe(k);
const RkA = ExtendedPoint.fromAffine(r).add(kA);
// [8][S]B = [8]R + [8][k]A'
return RkA.subtract(SB).multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO);
}
/**
* Verifies EdDSA signature against message and public key.
* An extended group equation is checked.
* RFC8032 5.1.7
* Compliant with ZIP215:
* 0 <= sig.R/publicKey < 2**256 (can be >= curve.P)
* 0 <= sig.s < l
* Not compliant with RFC8032: it's not possible to comply to both ZIP & RFC at the same time.
*/
function verify(sig: SigType, message: Hex, publicKey: PubKey): boolean {
const { r, SB, msg, pub } = prepareVerification(sig, message, publicKey);
const domain = CURVE.domain(
concatBytes(r.toRawBytes(), pub.toRawBytes(), msg),
new Uint8Array([]),
false
);
const hashed = CURVE.hash(domain);
return finishVerification(pub, r, SB, hashed);
}
// Enable precomputes. Slows down first publicKey computation by 20ms.
Point.BASE._setWindowSize(8);
const utils = {
getExtendedPublicKey,
mod: modP,
invert: (a: bigint, m = CURVE.P) => mod.invert(a, m),
/**
* Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF
* and convert them into private scalar, with the modulo bias being neglible.
* As per FIPS 186 B.4.1.
* Not needed for ed25519 private keys. Needed if you use scalars directly (rare).
* @param hash hash output from sha512, or a similar function
* @returns valid private scalar
*/
hashToPrivateScalar: (hash: Hex): bigint => {
hash = ensureBytes(hash);
if (hash.length < 40 || hash.length > 1024)
throw new Error('Expected 40-1024 bytes of private key as per FIPS 186');
return mod.mod(bytesToNumberLE(hash), CURVE_ORDER - _1n) + _1n;
},
/**
* ed25519 private keys are uniform 32-bit strings. We do not need to check for
* modulo bias like we do in noble-secp256k1 randomPrivateKey()
*/
randomPrivateKey: (): Uint8Array => randomBytes(fieldLen),
/**
* We're doing scalar multiplication (used in getPublicKey etc) with precomputed BASE_POINT
* values. This slows down first getPublicKey() by milliseconds (see Speed section),
* but allows to speed-up subsequent getPublicKey() calls up to 20x.
* @param windowSize 2, 4, 8, 16
*/
precompute(windowSize = 8, point = Point.BASE): Point {
const cached = point.equals(Point.BASE) ? point : new Point(point.x, point.y);
cached._setWindowSize(windowSize);
cached.multiply(_2n);
return cached;
},
};
return {
CURVE,
ExtendedPoint,
Point,
Signature,
getPublicKey,
utils,
sign,
verify,
};
}

@ -48,10 +48,17 @@ export function bytesToNumber(bytes: Uint8Array): bigint {
return hexToNumber(bytesToHex(bytes));
}
export function ensureBytes(hex: string | Uint8Array): Uint8Array {
export const numberToBytesBE = (n: bigint, len: number) =>
hexToBytes(n.toString(16).padStart(len * 2, '0'));
export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse();
export function ensureBytes(hex: string | Uint8Array, expectedLength?: number): Uint8Array {
// Uint8Array.from() instead of hash.slice() because node.js Buffer
// is instance of Uint8Array, and its slice() creates **mutable** copy
return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex);
const bytes = hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex);
if (typeof expectedLength === 'number' && bytes.length !== expectedLength)
throw new Error(`Expected ${expectedLength} bytes`);
return bytes;
}
// Copies several Uint8Arrays into one.
@ -67,3 +74,11 @@ export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
}
return result;
}
// CURVE.n lengths
export function nLength(n: bigint, nBitLength?: number) {
// Bit size, byte size of CURVE.n
const _nBitLength = nBitLength !== undefined ? nBitLength : n.toString(2).length;
const nByteLength = Math.ceil(_nBitLength / 8);
return { nBitLength: _nBitLength, nByteLength };
}