forked from tornado-packages/noble-curves
Add twisted edwards curve.
This commit is contained in:
parent
16ae76d185
commit
211c887a57
@ -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",
|
||||
|
124
src/group.ts
Normal file
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.
|
||||
|
178
src/shortw.ts
178
src/shortw.ts
@ -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
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,
|
||||
};
|
||||
}
|
19
src/utils.ts
19
src/utils.ts
@ -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 };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user