15 Commits
0.5.1 ... 0.5.2

Author SHA1 Message Date
Paul Miller
b9482bb17d Release 0.5.2. 2023-01-13 16:23:52 +01:00
Paul Miller
74475dca68 Fix lint 2023-01-13 16:02:07 +01:00
Paul Miller
f4cf21b9c8 tests: Use describe() 2023-01-13 16:00:13 +01:00
Paul Miller
5312d92b2c edwards: Fix isTorsionFree() 2023-01-13 15:58:04 +01:00
Paul Miller
d1770c0ac7 Rename test 2023-01-13 01:29:54 +01:00
Paul Miller
2d37edf7d1 Remove utils.mod(), utils.invert() 2023-01-13 01:26:00 +01:00
Paul Miller
36998fede8 Fix sqrt 2023-01-13 01:21:51 +01:00
Paul Miller
83960d445d Refactor: weierstrass assertValidity and others 2023-01-12 21:18:51 +01:00
Paul Miller
23cc2aa5d1 edwards, montgomery, weierstrass: refactor 2023-01-12 20:40:16 +01:00
Paul Miller
e45d7c2d25 utils: new util; ed448: small adjustment 2023-01-12 20:39:43 +01:00
Paul Miller
bfe929aac3 modular: Tonneli-Shanks refactoring 2023-01-12 20:38:42 +01:00
Paul Miller
069452dbe7 BLS, jubjub refactoring 2023-01-12 20:38:10 +01:00
Paul Miller
2e81f31d2e ECDSA: signUnhashed(), support for key recovery from bits 2/3 2023-01-08 20:02:04 +01:00
Paul Miller
9f7df0f13b ECDSA adjustments 2023-01-08 18:46:55 +01:00
Paul Miller
5600629bca Refactor 2023-01-08 18:02:54 +01:00
22 changed files with 3513 additions and 3404 deletions

View File

@@ -201,8 +201,6 @@ export type CurveFn = {
ExtendedPoint: ExtendedPointConstructor; ExtendedPoint: ExtendedPointConstructor;
Signature: SignatureConstructor; Signature: SignatureConstructor;
utils: { utils: {
mod: (a: bigint, b?: bigint) => bigint;
invert: (number: bigint, modulo?: bigint) => bigint;
randomPrivateKey: () => Uint8Array; randomPrivateKey: () => Uint8Array;
getExtendedPublicKey: (key: PrivKey) => { getExtendedPublicKey: (key: PrivKey) => {
head: Uint8Array; head: Uint8Array;
@@ -306,6 +304,7 @@ export type CurveFn = {
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
getSharedSecret: (privateA: PrivKey, publicB: PubKey, isCompressed?: boolean) => Uint8Array; getSharedSecret: (privateA: PrivKey, publicB: PubKey, isCompressed?: boolean) => Uint8Array;
sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => SignatureType; sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => SignatureType;
signUnhashed: (msg: Uint8Array, privKey: PrivKey, opts?: SignOpts) => SignatureType;
verify: ( verify: (
signature: Hex | SignatureType, signature: Hex | SignatureType,
msgHash: Hex, msgHash: Hex,
@@ -316,8 +315,6 @@ export type CurveFn = {
ProjectivePoint: ProjectivePointConstructor; ProjectivePoint: ProjectivePointConstructor;
Signature: SignatureConstructor; Signature: SignatureConstructor;
utils: { utils: {
mod: (a: bigint) => bigint;
invert: (number: bigint) => bigint;
isValidPrivateKey(privateKey: PrivKey): boolean; isValidPrivateKey(privateKey: PrivKey): boolean;
hashToPrivateKey: (hash: Hex) => Uint8Array; hashToPrivateKey: (hash: Hex) => Uint8Array;
randomPrivateKey: () => Uint8Array; randomPrivateKey: () => Uint8Array;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@noble/curves", "name": "@noble/curves",
"version": "0.5.1", "version": "0.5.2",
"description": "Minimal, auditable JS implementation of elliptic curve cryptography", "description": "Minimal, auditable JS implementation of elliptic curve cryptography",
"files": [ "files": [
"lib" "lib"
@@ -31,7 +31,7 @@
"@types/node": "18.11.3", "@types/node": "18.11.3",
"fast-check": "3.0.0", "fast-check": "3.0.0",
"micro-bmark": "0.2.0", "micro-bmark": "0.2.0",
"micro-should": "0.2.0", "micro-should": "0.3.0",
"prettier": "2.6.2", "prettier": "2.6.2",
"rollup": "2.75.5", "rollup": "2.75.5",
"typescript": "4.7.3" "typescript": "4.7.3"

View File

@@ -1,13 +1,26 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Barreto-Lynn-Scott Curves. A family of pairing friendly curves, with embedding degree = 12 or 24 /**
// NOTE: only 12 supported for now * BLS (Barreto-Lynn-Scott) family of pairing-friendly curves.
// Constructed from pair of weierstrass curves, based pairing logic * Implements BLS (Boneh-Lynn-Shacham) signatures.
* Consists of two curves: G1 and G2:
* - G1 is a subgroup of (x, y) E(Fq) over y² = x³ + 4.
* - G2 is a subgroup of ((x₁, x₂+i), (y₁, y₂+i)) E(Fq²) over y² = x³ + 4(1 + i) where i is √-1
* - Gt, created by bilinear (ate) pairing e(G1, G2), consists of p-th roots of unity in
* Fq^k where k is embedding degree. Only degree 12 is currently supported, 24 is not.
* Pairing is used to aggregate and verify signatures.
* We are using Fp for private keys (shorter) and Fp₂ for signatures (longer).
* Some projects may prefer to swap this relation, it is not supported for now.
*/
import * as mod from './modular.js'; import * as mod from './modular.js';
import { ensureBytes, numberToBytesBE, bytesToNumberBE, bitLen, bitGet } from './utils.js'; import * as ut from './utils.js';
import * as utils from './utils.js'; // Types require separate import
// Types import { Hex, PrivKey } from './utils.js';
import { hexToBytes, bytesToHex, Hex, PrivKey } from './utils.js'; import {
import { htfOpts, stringToBytes, hash_to_field, expand_message_xmd } from './hash-to-curve.js'; htfOpts,
stringToBytes,
hash_to_field as hashToField,
expand_message_xmd as expandMessageXMD,
} from './hash-to-curve.js';
import { CurvePointsType, PointType, CurvePointsRes, weierstrassPoints } from './weierstrass.js'; import { CurvePointsType, PointType, CurvePointsRes, weierstrassPoints } from './weierstrass.js';
type Fp = bigint; // Can be different field? type Fp = bigint; // Can be different field?
@@ -39,7 +52,7 @@ export type CurveType<Fp, Fp2, Fp6, Fp12> = {
finalExponentiate(num: Fp12): Fp12; finalExponentiate(num: Fp12): Fp12;
}; };
htfDefaults: htfOpts; htfDefaults: htfOpts;
hash: utils.CHash; // Because we need outputLen for DRBG hash: ut.CHash; // Because we need outputLen for DRBG
randomBytes: (bytesLength?: number) => Uint8Array; randomBytes: (bytesLength?: number) => Uint8Array;
}; };
@@ -80,12 +93,9 @@ export type CurveFn<Fp, Fp2, Fp6, Fp12> = {
publicKeys: (Hex | PointType<Fp>)[] publicKeys: (Hex | PointType<Fp>)[]
) => boolean; ) => boolean;
utils: { utils: {
bytesToHex: typeof utils.bytesToHex;
hexToBytes: typeof utils.hexToBytes;
stringToBytes: typeof stringToBytes; stringToBytes: typeof stringToBytes;
hashToField: typeof hash_to_field; hashToField: typeof hashToField;
expandMessageXMD: typeof expand_message_xmd; expandMessageXMD: typeof expandMessageXMD;
mod: typeof mod.mod;
getDSTLabel: () => string; getDSTLabel: () => string;
setDSTLabel(newLabel: string): void; setDSTLabel(newLabel: string): void;
}; };
@@ -95,12 +105,9 @@ export function bls<Fp2, Fp6, Fp12>(
CURVE: CurveType<Fp, Fp2, Fp6, Fp12> CURVE: CurveType<Fp, Fp2, Fp6, Fp12>
): CurveFn<Fp, Fp2, Fp6, Fp12> { ): CurveFn<Fp, Fp2, Fp6, Fp12> {
// Fields looks pretty specific for curve, so for now we need to pass them with options // Fields looks pretty specific for curve, so for now we need to pass them with options
const Fp = CURVE.Fp; const { Fp, Fr, Fp2, Fp6, Fp12 } = CURVE;
const Fr = CURVE.Fr; const BLS_X_LEN = ut.bitLen(CURVE.x);
const Fp2 = CURVE.Fp2; const groupLen = 32; // TODO: calculate; hardcoded for now
const Fp6 = CURVE.Fp6;
const Fp12 = CURVE.Fp12;
const BLS_X_LEN = bitLen(CURVE.x);
// Pre-compute coefficients for sparse multiplication // Pre-compute coefficients for sparse multiplication
// Point addition and point double calculations is reused for coefficients // Point addition and point double calculations is reused for coefficients
@@ -125,7 +132,7 @@ export function bls<Fp2, Fp6, Fp12>(
Rx = Fp2.div(Fp2.mul(Fp2.mul(Fp2.sub(t0, t3), Rx), Ry), 2n); // ((T0 - T3) * Rx * Ry) / 2 Rx = Fp2.div(Fp2.mul(Fp2.mul(Fp2.sub(t0, t3), Rx), Ry), 2n); // ((T0 - T3) * Rx * Ry) / 2
Ry = Fp2.sub(Fp2.square(Fp2.div(Fp2.add(t0, t3), 2n)), Fp2.mul(Fp2.square(t2), 3n)); // ((T0 + T3) / 2)² - 3 * T2² Ry = Fp2.sub(Fp2.square(Fp2.div(Fp2.add(t0, t3), 2n)), Fp2.mul(Fp2.square(t2), 3n)); // ((T0 + T3) / 2)² - 3 * T2²
Rz = Fp2.mul(t0, t4); // T0 * T4 Rz = Fp2.mul(t0, t4); // T0 * T4
if (bitGet(CURVE.x, i)) { if (ut.bitGet(CURVE.x, i)) {
// Addition // Addition
let t0 = Fp2.sub(Ry, Fp2.mul(Qy, Rz)); // Ry - Qy * Rz let t0 = Fp2.sub(Ry, Fp2.mul(Qy, Rz)); // Ry - Qy * Rz
let t1 = Fp2.sub(Rx, Fp2.mul(Qx, Rz)); // Rx - Qx * Rz let t1 = Fp2.sub(Rx, Fp2.mul(Qx, Rz)); // Rx - Qx * Rz
@@ -147,13 +154,14 @@ export function bls<Fp2, Fp6, Fp12>(
} }
function millerLoop(ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]): Fp12 { function millerLoop(ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]): Fp12 {
const { x } = CURVE;
const Px = g1[0]; const Px = g1[0];
const Py = g1[1]; const Py = g1[1];
let f12 = Fp12.ONE; let f12 = Fp12.ONE;
for (let j = 0, i = BLS_X_LEN - 2; i >= 0; i--, j++) { for (let j = 0, i = BLS_X_LEN - 2; i >= 0; i--, j++) {
const E = ell[j]; const E = ell[j];
f12 = Fp12.multiplyBy014(f12, E[0], Fp2.mul(E[1], Px), Fp2.mul(E[2], Py)); f12 = Fp12.multiplyBy014(f12, E[0], Fp2.mul(E[1], Px), Fp2.mul(E[2], Py));
if (bitGet(CURVE.x, i)) { if (ut.bitGet(x, i)) {
j += 1; j += 1;
const F = ell[j]; const F = ell[j];
f12 = Fp12.multiplyBy014(f12, F[0], Fp2.mul(F[1], Px), Fp2.mul(F[2], Py)); f12 = Fp12.multiplyBy014(f12, F[0], Fp2.mul(F[1], Px), Fp2.mul(F[2], Py));
@@ -163,81 +171,31 @@ export function bls<Fp2, Fp6, Fp12>(
return Fp12.conjugate(f12); return Fp12.conjugate(f12);
} }
// bls12-381 is a construction of two curves:
// 1. Fp: (x, y)
// 2. Fp₂: ((x₁, x₂+i), (y₁, y₂+i)) - (complex numbers)
//
// Bilinear Pairing (ate pairing) is used to combine both elements into a paired one:
// Fp₁₂ = e(Fp, Fp2)
// where Fp₁₂ = 12-degree polynomial
// Pairing is used to verify signatures.
//
// We are using Fp for private keys (shorter) and Fp2 for signatures (longer).
// Some projects may prefer to swap this relation, it is not supported for now.
const htfDefaults = { ...CURVE.htfDefaults };
function isWithinCurveOrder(num: bigint): boolean {
return 0 < num && num < CURVE.r;
}
const utils = { const utils = {
hexToBytes: hexToBytes, hexToBytes: ut.hexToBytes,
bytesToHex: bytesToHex, bytesToHex: ut.bytesToHex,
mod: mod.mod, stringToBytes: stringToBytes,
stringToBytes,
// TODO: do we need to export it here? // TODO: do we need to export it here?
hashToField: (msg: Uint8Array, count: number, options: Partial<typeof htfDefaults> = {}) => hashToField: (
hash_to_field(msg, count, { ...CURVE.htfDefaults, ...options }), msg: Uint8Array,
count: number,
options: Partial<typeof CURVE.htfDefaults> = {}
) => hashToField(msg, count, { ...CURVE.htfDefaults, ...options }),
expandMessageXMD: (msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H = CURVE.hash) => expandMessageXMD: (msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H = CURVE.hash) =>
expand_message_xmd(msg, DST, lenInBytes, H), expandMessageXMD(msg, DST, lenInBytes, H),
hashToPrivateKey: (hash: Hex): Uint8Array => Fr.toBytes(ut.hashToPrivateScalar(hash, CURVE.r)),
/** randomBytes: (bytesLength: number = groupLen): Uint8Array => CURVE.randomBytes(bytesLength),
* Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(groupLen + 8)),
* and convert them into private key, with the modulo bias being negligible. getDSTLabel: () => CURVE.htfDefaults.DST,
* As per FIPS 186 B.1.1.
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
* @param hash hash output from sha512, or a similar function
* @returns valid private key
*/
hashToPrivateKey: (hash: Hex): Uint8Array => {
hash = ensureBytes(hash);
if (hash.length < 40 || hash.length > 1024)
throw new Error('Expected 40-1024 bytes of private key as per FIPS 186');
// hashToPrivateScalar(hash, CURVE.r)
// NOTE: doesn't add +/-1
const num = mod.mod(bytesToNumberBE(hash), CURVE.r);
// This should never happen
if (num === 0n || num === 1n) throw new Error('Invalid private key');
return numberToBytesBE(num, 32);
},
randomBytes: (bytesLength: number = 32): Uint8Array => CURVE.randomBytes(bytesLength),
// NIST SP 800-56A rev 3, section 5.6.1.2.2
// https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(40)),
getDSTLabel: () => htfDefaults.DST,
setDSTLabel(newLabel: string) { setDSTLabel(newLabel: string) {
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
if (typeof newLabel !== 'string' || newLabel.length > 2048 || newLabel.length === 0) { if (typeof newLabel !== 'string' || newLabel.length > 2048 || newLabel.length === 0) {
throw new TypeError('Invalid DST'); throw new TypeError('Invalid DST');
} }
htfDefaults.DST = newLabel; CURVE.htfDefaults.DST = newLabel;
}, },
}; };
function normalizePrivKey(key: PrivKey): bigint {
let int: bigint;
if (key instanceof Uint8Array && key.length === 32) int = bytesToNumberBE(key);
else if (typeof key === 'string' && key.length === 64) int = BigInt(`0x${key}`);
else if (typeof key === 'number' && key > 0 && Number.isSafeInteger(key)) int = BigInt(key);
else if (typeof key === 'bigint' && key > 0n) int = key;
else throw new TypeError('Expected valid private key');
int = mod.mod(int, CURVE.r);
if (!isWithinCurveOrder(int)) throw new Error('Private key must be 0 < key < CURVE.r');
return int;
}
// Point on G1 curve: (x, y) // Point on G1 curve: (x, y)
const G1 = weierstrassPoints({ const G1 = weierstrassPoints({
n: Fr.ORDER, n: Fr.ORDER,
@@ -309,7 +267,7 @@ export function bls<Fp2, Fp6, Fp12>(
function sign(message: G2Hex, privateKey: PrivKey): Uint8Array | G2 { function sign(message: G2Hex, privateKey: PrivKey): Uint8Array | G2 {
const msgPoint = normP2Hash(message); const msgPoint = normP2Hash(message);
msgPoint.assertValidity(); msgPoint.assertValidity();
const sigPoint = msgPoint.multiply(normalizePrivKey(privateKey)); const sigPoint = msgPoint.multiply(G1.normalizePrivateKey(privateKey));
if (message instanceof G2.Point) return sigPoint; if (message instanceof G2.Point) return sigPoint;
return Signature.encode(sigPoint); return Signature.encode(sigPoint);
} }

View File

@@ -2,28 +2,18 @@
// Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y² // Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
// Differences from @noble/ed25519 1.7: // Differences from @noble/ed25519 1.7:
// 1. Different field element lengths in ed448: // 1. Variable field element lengths between EDDSA/ECDH:
// EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes // EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes
// 2. Different addition formula (doubling is same) // 2. Different addition formula (doubling is same)
// 3. uvRatio differs between curves (half-expected, not only pow fn changes) // 3. uvRatio differs between curves (half-expected, not only pow fn changes)
// 4. Point decompression code is different too (unexpected), now using generalized formula // 4. Point decompression code is different (unexpected), now using generalized formula
// 5. Domain function was no-op for ed25519, but adds some data even with empty context for ed448 // 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 * as mod from './modular.js';
import { import * as ut from './utils.js';
bytesToHex, import { ensureBytes, Hex, PrivKey } from './utils.js';
concatBytes,
ensureBytes,
numberToBytesLE,
bytesToNumberLE,
hashToPrivateScalar,
BasicCurve,
validateOpts as utilOpts,
Hex,
PrivKey,
} from './utils.js'; // TODO: import * as u from './utils.js'?
import { Group, GroupConstructor, wNAF } from './group.js'; import { Group, GroupConstructor, wNAF } from './group.js';
import { hash_to_field, htfOpts, validateHTFOpts } from './hash-to-curve.js'; import { hash_to_field as hashToField, htfOpts, validateHTFOpts } from './hash-to-curve.js';
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n // Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0); const _0n = BigInt(0);
@@ -31,49 +21,41 @@ const _1n = BigInt(1);
const _2n = BigInt(2); const _2n = BigInt(2);
const _8n = BigInt(8); const _8n = BigInt(8);
export type CHash = { // Edwards curves must declare params a & d.
(message: Uint8Array | string): Uint8Array; export type CurveType = ut.BasicCurve<bigint> & {
blockLen: number;
outputLen: number;
create(): any;
};
export type CurveType = BasicCurve<bigint> & {
// Params: a, d // Params: a, d
a: bigint; a: bigint;
d: bigint; d: bigint;
// Hashes // Hashes
hash: CHash; // Because we need outputLen for DRBG // The interface, because we need outputLen for DRBG
hash: ut.CHash;
// CSPRNG
randomBytes: (bytesLength?: number) => Uint8Array; randomBytes: (bytesLength?: number) => Uint8Array;
// Probably clears bits in a byte array to produce a valid field element
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array; adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
// Used during hashing
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
// Ratio √(u/v)
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
preHash?: CHash; // RFC 8032 pre-hashing of messages to sign() / verify()
clearCofactor?: (c: ExtendedPointConstructor, point: ExtendedPointType) => ExtendedPointType; preHash?: ut.CHash;
// Hash to field opts // Hash to field options
htfDefaults?: htfOpts; htfDefaults?: htfOpts;
mapToCurve?: (scalar: bigint[]) => { x: bigint; y: bigint }; mapToCurve?: (scalar: bigint[]) => { x: bigint; y: bigint };
}; };
// Should be separate from overrides, since overrides can use information about curve (for example nBits)
function validateOpts(curve: CurveType) { function validateOpts(curve: CurveType) {
const opts = utilOpts(curve); const opts = ut.validateOpts(curve);
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen)) if (typeof opts.hash !== 'function' || !ut.isPositiveInt(opts.hash.outputLen))
throw new Error('Invalid hash function'); throw new Error('Invalid hash function');
for (const i of ['a', 'd'] as const) { for (const i of ['a', 'd'] as const) {
if (typeof opts[i] !== 'bigint') const val = opts[i];
throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`); if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
} }
for (const fn of ['randomBytes'] as const) { for (const fn of ['randomBytes'] as const) {
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`); if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
} }
for (const fn of [ for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio', 'mapToCurve'] as const) {
'adjustScalarBytes',
'domain',
'uvRatio',
'mapToCurve',
'clearCofactor',
] as const) {
if (opts[fn] === undefined) continue; // Optional if (opts[fn] === undefined) continue; // Optional
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`); if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
} }
@@ -96,7 +78,7 @@ export type SignatureConstructor = {
fromHex(hex: Hex): SignatureType; fromHex(hex: Hex): SignatureType;
}; };
// Instance // Instance of Extended Point with coordinates in X, Y, Z, T
export interface ExtendedPointType extends Group<ExtendedPointType> { export interface ExtendedPointType extends Group<ExtendedPointType> {
readonly x: bigint; readonly x: bigint;
readonly y: bigint; readonly y: bigint;
@@ -109,7 +91,7 @@ export interface ExtendedPointType extends Group<ExtendedPointType> {
toAffine(invZ?: bigint): PointType; toAffine(invZ?: bigint): PointType;
clearCofactor(): ExtendedPointType; clearCofactor(): ExtendedPointType;
} }
// Static methods // Static methods of Extended Point with coordinates in X, Y, Z, T
export interface ExtendedPointConstructor extends GroupConstructor<ExtendedPointType> { export interface ExtendedPointConstructor extends GroupConstructor<ExtendedPointType> {
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtendedPointType; new (x: bigint, y: bigint, z: bigint, t: bigint): ExtendedPointType;
fromAffine(p: PointType): ExtendedPointType; fromAffine(p: PointType): ExtendedPointType;
@@ -117,7 +99,7 @@ export interface ExtendedPointConstructor extends GroupConstructor<ExtendedPoint
normalizeZ(points: ExtendedPointType[]): ExtendedPointType[]; normalizeZ(points: ExtendedPointType[]): ExtendedPointType[];
} }
// Instance // Instance of Affine Point with coordinates in X, Y
export interface PointType extends Group<PointType> { export interface PointType extends Group<PointType> {
readonly x: bigint; readonly x: bigint;
readonly y: bigint; readonly y: bigint;
@@ -127,7 +109,7 @@ export interface PointType extends Group<PointType> {
isTorsionFree(): boolean; isTorsionFree(): boolean;
clearCofactor(): PointType; clearCofactor(): PointType;
} }
// Static methods // Static methods of Affine Point with coordinates in X, Y
export interface PointConstructor extends GroupConstructor<PointType> { export interface PointConstructor extends GroupConstructor<PointType> {
new (x: bigint, y: bigint): PointType; new (x: bigint, y: bigint): PointType;
fromHex(hex: Hex): PointType; fromHex(hex: Hex): PointType;
@@ -148,8 +130,6 @@ export type CurveFn = {
ExtendedPoint: ExtendedPointConstructor; ExtendedPoint: ExtendedPointConstructor;
Signature: SignatureConstructor; Signature: SignatureConstructor;
utils: { utils: {
mod: (a: bigint) => bigint;
invert: (number: bigint) => bigint;
randomPrivateKey: () => Uint8Array; randomPrivateKey: () => Uint8Array;
getExtendedPublicKey: (key: PrivKey) => { getExtendedPublicKey: (key: PrivKey) => {
head: Uint8Array; head: Uint8Array;
@@ -164,36 +144,31 @@ export type CurveFn = {
// NOTE: it is not generic twisted curve for now, but ed25519/ed448 generic implementation // NOTE: it is not generic twisted curve for now, but ed25519/ed448 generic implementation
export function twistedEdwards(curveDef: CurveType): CurveFn { export function twistedEdwards(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>; const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
const Fp = CURVE.Fp as mod.Field<bigint>; const Fp = CURVE.Fp;
const CURVE_ORDER = CURVE.n; const CURVE_ORDER = CURVE.n;
const fieldLen = Fp.BYTES; // 32 (length of one field element) const maxGroupElement = _2n ** BigInt(CURVE.nByteLength * 8);
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 // Function overrides
const { randomBytes } = CURVE; const { randomBytes } = CURVE;
const modP = Fp.create; const modP = Fp.create;
// sqrt(u/v) // sqrt(u/v)
function _uvRatio(u: bigint, v: bigint) { const uvRatio =
try { CURVE.uvRatio ||
const value = Fp.sqrt(u * Fp.invert(v)); ((u: bigint, v: bigint) => {
return { isValid: true, value }; try {
} catch (e) { return { isValid: true, value: Fp.sqrt(u * Fp.invert(v)) };
return { isValid: false, value: _0n }; } catch (e) {
} return { isValid: false, value: _0n };
} }
const uvRatio = CURVE.uvRatio || _uvRatio; });
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes); // NOOP
const _adjustScalarBytes = (bytes: Uint8Array) => bytes; // NOOP const domain =
const adjustScalarBytes = CURVE.adjustScalarBytes || _adjustScalarBytes; CURVE.domain ||
function _domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) { ((data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
if (ctx.length || phflag) throw new Error('Contexts/pre-hash are not supported'); if (ctx.length || phflag) throw new Error('Contexts/pre-hash are not supported');
return data; return data;
} }); // NOOP
const domain = CURVE.domain || _domain; // NOOP
/** /**
* Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy). * Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy).
@@ -336,25 +311,27 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// Non-constant-time multiplication. Uses double-and-add algorithm. // Non-constant-time multiplication. Uses double-and-add algorithm.
// It's faster, but should only be used when you don't care about // It's faster, but should only be used when you don't care about
// an exposed private key e.g. sig verification. // an exposed private key e.g. sig verification.
// Allows scalar bigger than curve order, but less than 2^256
multiplyUnsafe(scalar: number | bigint): ExtendedPoint { multiplyUnsafe(scalar: number | bigint): ExtendedPoint {
let n = normalizeScalar(scalar, CURVE_ORDER, false); let n = normalizeScalar(scalar, CURVE_ORDER, false);
const G = ExtendedPoint.BASE;
const P0 = ExtendedPoint.ZERO; const P0 = ExtendedPoint.ZERO;
if (n === _0n) return P0; if (n === _0n) return P0;
if (this.equals(P0) || n === _1n) return this; if (this.equals(P0) || n === _1n) return this;
if (this.equals(G)) return this.wNAF(n); if (this.equals(ExtendedPoint.BASE)) return this.wNAF(n);
return wnaf.unsafeLadder(this, n); return wnaf.unsafeLadder(this, n);
} }
// Checks if point is of small order.
// If you add something to small order point, you will have "dirty"
// point with torsion component.
// Multiplies point by cofactor and checks if the result is 0. // Multiplies point by cofactor and checks if the result is 0.
isSmallOrder(): boolean { isSmallOrder(): boolean {
return this.multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO); return this.multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO);
} }
// Multiplies point by a very big scalar n and checks if the result is 0. // Multiplies point by curve order (very big scalar CURVE.n) and checks if the result is 0.
// Returns `false` is the point is dirty.
isTorsionFree(): boolean { isTorsionFree(): boolean {
return this.multiplyUnsafe(CURVE_ORDER).equals(ExtendedPoint.ZERO); return wnaf.unsafeLadder(this, CURVE_ORDER).equals(ExtendedPoint.ZERO);
} }
// Converts Extended point to default (x, y) coordinates. // Converts Extended point to default (x, y) coordinates.
@@ -371,14 +348,12 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
return new Point(ax, ay); return new Point(ax, ay);
} }
clearCofactor(): ExtendedPoint { clearCofactor(): ExtendedPoint {
if (CURVE.h === _1n) return this; // Fast-path const { h: cofactor } = CURVE;
// clear_cofactor(P) := h_eff * P if (cofactor === _1n) return this;
// hEff = h for ed25519/ed448. Maybe worth moving to params? return this.multiplyUnsafe(cofactor);
if (CURVE.clearCofactor) return CURVE.clearCofactor(ExtendedPoint, this) as ExtendedPoint;
return this.multiplyUnsafe(CURVE.h);
} }
} }
const wnaf = wNAF(ExtendedPoint, groupLen * 8); const wnaf = wNAF(ExtendedPoint, CURVE.nByteLength * 8);
function assertExtPoint(other: unknown) { function assertExtPoint(other: unknown) {
if (!(other instanceof ExtendedPoint)) throw new TypeError('ExtendedPoint expected'); if (!(other instanceof ExtendedPoint)) throw new TypeError('ExtendedPoint expected');
@@ -413,19 +388,20 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// Uses algo from RFC8032 5.1.3. // Uses algo from RFC8032 5.1.3.
static fromHex(hex: Hex, strict = true) { static fromHex(hex: Hex, strict = true) {
const { d, a } = CURVE; const { d, a } = CURVE;
hex = ensureBytes(hex, fieldLen); const len = Fp.BYTES;
hex = ensureBytes(hex, len);
// 1. First, interpret the string as an integer in little-endian // 1. First, interpret the string as an integer in little-endian
// representation. Bit 255 of this number is the least significant // representation. Bit 255 of this number is the least significant
// bit of the x-coordinate and denote this value x_0. The // bit of the x-coordinate and denote this value x_0. The
// y-coordinate is recovered simply by clearing this bit. If the // y-coordinate is recovered simply by clearing this bit. If the
// resulting value is >= p, decoding fails. // resulting value is >= p, decoding fails.
const normed = hex.slice(); const normed = hex.slice();
const lastByte = hex[fieldLen - 1]; const lastByte = hex[len - 1];
normed[fieldLen - 1] = lastByte & ~0x80; normed[len - 1] = lastByte & ~0x80;
const y = bytesToNumberLE(normed); const y = ut.bytesToNumberLE(normed);
if (strict && y >= Fp.ORDER) throw new Error('Expected 0 < hex < P'); if (strict && y >= Fp.ORDER) throw new Error('Expected 0 < hex < P');
if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < 2**256'); if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < CURVE.n');
// 2. To recover the x-coordinate, the curve equation implies // 2. To recover the x-coordinate, the curve equation implies
// Ed25519: x² = (y² - 1) / (d y² + 1) (mod p). // Ed25519: x² = (y² - 1) / (d y² + 1) (mod p).
@@ -459,16 +435,18 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// When compressing point, it's enough to only store its y coordinate // When compressing point, it's enough to only store its y coordinate
// and use the last byte to encode sign of x. // and use the last byte to encode sign of x.
toRawBytes(): Uint8Array { toRawBytes(): Uint8Array {
const bytes = numberToBytesLE(this.y, fieldLen); const bytes = ut.numberToBytesLE(this.y, Fp.BYTES);
bytes[fieldLen - 1] |= this.x & _1n ? 0x80 : 0; bytes[Fp.BYTES - 1] |= this.x & _1n ? 0x80 : 0;
return bytes; return bytes;
} }
// Same as toRawBytes, but returns string. // Same as toRawBytes, but returns string.
toHex(): string { toHex(): string {
return bytesToHex(this.toRawBytes()); return ut.bytesToHex(this.toRawBytes());
} }
// Determines if point is in prime-order subgroup.
// Returns `false` is the point is dirty.
isTorsionFree(): boolean { isTorsionFree(): boolean {
return ExtendedPoint.fromAffine(this).isTorsionFree(); return ExtendedPoint.fromAffine(this).isTorsionFree();
} }
@@ -509,20 +487,20 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// Encodes byte string to elliptic curve // Encodes byte string to elliptic curve
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3
static hashToCurve(msg: Hex, options?: Partial<htfOpts>) { static hashToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve'); const { mapToCurve, htfDefaults } = CURVE;
msg = ensureBytes(msg); if (!mapToCurve) throw new Error('No mapToCurve defined for curve');
const u = hash_to_field(msg, 2, { ...CURVE.htfDefaults, ...options } as htfOpts); const u = hashToField(ensureBytes(msg), 2, { ...htfDefaults, ...options } as htfOpts);
const { x: x0, y: y0 } = CURVE.mapToCurve(u[0]); const { x: x0, y: y0 } = mapToCurve(u[0]);
const { x: x1, y: y1 } = CURVE.mapToCurve(u[1]); const { x: x1, y: y1 } = mapToCurve(u[1]);
const p = new Point(x0, y0).add(new Point(x1, y1)).clearCofactor(); const p = new Point(x0, y0).add(new Point(x1, y1)).clearCofactor();
return p; return p;
} }
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3
static encodeToCurve(msg: Hex, options?: Partial<htfOpts>) { static encodeToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve'); const { mapToCurve, htfDefaults } = CURVE;
msg = ensureBytes(msg); if (!mapToCurve) throw new Error('No mapToCurve defined for curve');
const u = hash_to_field(msg, 1, { ...CURVE.htfDefaults, ...options } as htfOpts); const u = hashToField(ensureBytes(msg), 1, { ...htfDefaults, ...options } as htfOpts);
const { x, y } = CURVE.mapToCurve(u[0]); const { x, y } = mapToCurve(u[0]);
return new Point(x, y).clearCofactor(); return new Point(x, y).clearCofactor();
} }
} }
@@ -536,9 +514,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
} }
static fromHex(hex: Hex) { static fromHex(hex: Hex) {
const bytes = ensureBytes(hex, 2 * fieldLen); const len = Fp.BYTES;
const r = Point.fromHex(bytes.slice(0, fieldLen), false); const bytes = ensureBytes(hex, 2 * len);
const s = bytesToNumberLE(bytes.slice(fieldLen, 2 * fieldLen)); const r = Point.fromHex(bytes.slice(0, len), false);
const s = ut.bytesToNumberLE(bytes.slice(len, 2 * len));
return new Signature(r, s); return new Signature(r, s);
} }
@@ -551,17 +530,17 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
} }
toRawBytes() { toRawBytes() {
return concatBytes(this.r.toRawBytes(), numberToBytesLE(this.s, fieldLen)); return ut.concatBytes(this.r.toRawBytes(), ut.numberToBytesLE(this.s, Fp.BYTES));
} }
toHex() { toHex() {
return bytesToHex(this.toRawBytes()); return ut.bytesToHex(this.toRawBytes());
} }
} }
// Little-endian SHA512 with modulo n // Little-endian SHA512 with modulo n
function modlLE(hash: Uint8Array): bigint { function modnLE(hash: Uint8Array): bigint {
return mod.mod(bytesToNumberLE(hash), CURVE_ORDER); return mod.mod(ut.bytesToNumberLE(hash), CURVE_ORDER);
} }
/** /**
@@ -572,7 +551,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
*/ */
function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint { function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint {
if (!max) throw new TypeError('Specify max value'); if (!max) throw new TypeError('Specify max value');
if (typeof num === 'number' && Number.isSafeInteger(num)) num = BigInt(num); if (ut.isPositiveInt(num)) num = BigInt(num);
if (typeof num === 'bigint' && num < max) { if (typeof num === 'bigint' && num < max) {
if (strict) { if (strict) {
if (_0n < num) return num; if (_0n < num) return num;
@@ -580,37 +559,32 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
if (_0n <= num) return num; if (_0n <= num) return num;
} }
} }
throw new TypeError('Expected valid scalar: 0 < scalar < max'); 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 */ /** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */
function getExtendedPublicKey(key: PrivKey) { function getExtendedPublicKey(key: PrivKey) {
return getKeyFromHash(CURVE.hash(checkPrivateKey(key))); const groupLen = CURVE.nByteLength;
// Normalize bigint / number / string to Uint8Array
const keyb =
typeof key === 'bigint' || typeof key === 'number'
? ut.numberToBytesLE(normalizeScalar(key, maxGroupElement), groupLen)
: key;
// Hash private key with curve's hash function to produce uniformingly random input
// We check byte lengths e.g.: ensureBytes(64, hash(ensureBytes(32, key)))
const hashed = ensureBytes(CURVE.hash(ensureBytes(keyb, groupLen)), 2 * groupLen);
// First half's bits are cleared to produce a random field element.
const head = adjustScalarBytes(hashed.slice(0, groupLen));
// Second half is called key prefix (5.1.6)
const prefix = hashed.slice(groupLen, 2 * groupLen);
// The actual private scalar
const scalar = modnLE(head);
// Point on Edwards curve aka public key
const point = Point.BASE.multiply(scalar);
// Uint8Array representation
const pointBytes = point.toRawBytes();
return { head, prefix, scalar, point, pointBytes };
} }
/** /**
@@ -625,7 +599,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const EMPTY = new Uint8Array(); const EMPTY = new Uint8Array();
function hashDomainToScalar(message: Uint8Array, context: Hex = EMPTY) { function hashDomainToScalar(message: Uint8Array, context: Hex = EMPTY) {
context = ensureBytes(context); context = ensureBytes(context);
return modlLE(CURVE.hash(domain(message, context, !!CURVE.preHash))); return modnLE(CURVE.hash(domain(message, context, !!CURVE.preHash)));
} }
/** Signs message with privateKey. RFC8032 5.1.6 */ /** Signs message with privateKey. RFC8032 5.1.6 */
@@ -633,9 +607,9 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
message = ensureBytes(message); message = ensureBytes(message);
if (CURVE.preHash) message = CURVE.preHash(message); if (CURVE.preHash) message = CURVE.preHash(message);
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privateKey); const { prefix, scalar, pointBytes } = getExtendedPublicKey(privateKey);
const r = hashDomainToScalar(concatBytes(prefix, message), context); const r = hashDomainToScalar(ut.concatBytes(prefix, message), context);
const R = Point.BASE.multiply(r); // R = rG const R = Point.BASE.multiply(r); // R = rG
const k = hashDomainToScalar(concatBytes(R.toRawBytes(), pointBytes, message), context); // k = hash(R+P+msg) const k = hashDomainToScalar(ut.concatBytes(R.toRawBytes(), pointBytes, message), context); // k = hash(R+P+msg)
const s = mod.mod(r + k * scalar, CURVE_ORDER); // s = r + kp const s = mod.mod(r + k * scalar, CURVE_ORDER); // s = r + kp
return new Signature(R, s).toRawBytes(); return new Signature(R, s).toRawBytes();
} }
@@ -672,13 +646,13 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const { r, s } = sig; const { r, s } = sig;
const SB = ExtendedPoint.BASE.multiplyUnsafe(s); const SB = ExtendedPoint.BASE.multiplyUnsafe(s);
const k = hashDomainToScalar( const k = hashDomainToScalar(
concatBytes(r.toRawBytes(), publicKey.toRawBytes(), message), ut.concatBytes(r.toRawBytes(), publicKey.toRawBytes(), message),
context context
); );
const kA = ExtendedPoint.fromAffine(publicKey).multiplyUnsafe(k); const kA = ExtendedPoint.fromAffine(publicKey).multiplyUnsafe(k);
const RkA = ExtendedPoint.fromAffine(r).add(kA); const RkA = ExtendedPoint.fromAffine(r).add(kA);
// [8][S]B = [8]R + [8][k]A' // [8][S]B = [8]R + [8][k]A'
return RkA.subtract(SB).multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO); return RkA.subtract(SB).clearCofactor().equals(ExtendedPoint.ZERO);
} }
// Enable precomputes. Slows down first publicKey computation by 20ms. // Enable precomputes. Slows down first publicKey computation by 20ms.
@@ -686,19 +660,16 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const utils = { const utils = {
getExtendedPublicKey, getExtendedPublicKey,
mod: modP,
invert: Fp.invert,
/** /**
* Not needed for ed25519 private keys. Needed if you use scalars directly (rare). * Not needed for ed25519 private keys. Needed if you use scalars directly (rare).
*/ */
hashToPrivateScalar: (hash: Hex): bigint => hashToPrivateScalar(hash, CURVE_ORDER, true), hashToPrivateScalar: (hash: Hex): bigint => ut.hashToPrivateScalar(hash, CURVE_ORDER, true),
/** /**
* ed25519 private keys are uniform 32-bit strings. We do not need to check for * ed25519 private keys are uniform 32-bit strings. We do not need to check for
* modulo bias like we do in secp256k1 randomPrivateKey() * modulo bias like we do in secp256k1 randomPrivateKey()
*/ */
randomPrivateKey: (): Uint8Array => randomBytes(fieldLen), randomPrivateKey: (): Uint8Array => randomBytes(Fp.BYTES),
/** /**
* We're doing scalar multiplication (used in getPublicKey etc) with precomputed BASE_POINT * We're doing scalar multiplication (used in getPublicKey etc) with precomputed BASE_POINT

View File

@@ -55,6 +55,7 @@ export function invert(number: bigint, modulo: bigint): bigint {
// prettier-ignore // prettier-ignore
let x = _0n, y = _1n, u = _1n, v = _0n; let x = _0n, y = _1n, u = _1n, v = _0n;
while (a !== _0n) { while (a !== _0n) {
// JIT applies optimization if those two lines follow each other
const q = b / a; const q = b / a;
const r = b % a; const r = b % a;
const m = x - u * q; const m = x - u * q;
@@ -68,7 +69,8 @@ export function invert(number: bigint, modulo: bigint): bigint {
} }
// Tonelli-Shanks algorithm // Tonelli-Shanks algorithm
// https://eprint.iacr.org/2012/685.pdf (page 12) // Paper 1: https://eprint.iacr.org/2012/685.pdf (page 12)
// Paper 2: Square Roots from 1; 24, 51, 10 to Dan Shanks
export function tonelliShanks(P: bigint) { export function tonelliShanks(P: bigint) {
// Legendre constant: used to calculate Legendre symbol (a | p), // Legendre constant: used to calculate Legendre symbol (a | p),
// which denotes the value of a^((p-1)/2) (mod p). // which denotes the value of a^((p-1)/2) (mod p).
@@ -79,7 +81,7 @@ export function tonelliShanks(P: bigint) {
let Q: bigint, S: number, Z: bigint; let Q: bigint, S: number, Z: bigint;
// Step 1: By factoring out powers of 2 from p - 1, // Step 1: By factoring out powers of 2 from p - 1,
// find q and s such that p - 1 = q2s with q odd // find q and s such that p - 1 = q*(2^s) with q odd
for (Q = P - _1n, S = 0; Q % _2n === _0n; Q /= _2n, S++); for (Q = P - _1n, S = 0; Q % _2n === _0n; Q /= _2n, S++);
// Step 2: Select a non-square z such that (z | p) ≡ -1 and set c ≡ zq // Step 2: Select a non-square z such that (z | p) ≡ -1 and set c ≡ zq
@@ -98,31 +100,30 @@ export function tonelliShanks(P: bigint) {
// Slow-path // Slow-path
const Q1div2 = (Q + _1n) / _2n; const Q1div2 = (Q + _1n) / _2n;
return function tonelliSlow<T>(Fp: Field<T>, n: T): T { return function tonelliSlow<T>(Fp: Field<T>, n: T): T {
// Step 0: Check that n is indeed a square: (n | p) must be ≡ 1 // Step 0: Check that n is indeed a square: (n | p) should not be ≡ -1
if (Fp.pow(n, legendreC) !== Fp.ONE) throw new Error('Cannot find square root'); if (Fp.pow(n, legendreC) === Fp.negate(Fp.ONE)) throw new Error('Cannot find square root');
let s = S; let r = S;
// TODO: will fail at Fp2/etc
let g = Fp.pow(Fp.mul(Fp.ONE, Z), Q); // will update both x and b
let x = Fp.pow(n, Q1div2); // first guess at the square root
let b = Fp.pow(n, Q); // first guess at the fudge factor
let c = pow(Z, Q, P); while (!Fp.equals(b, Fp.ONE)) {
let r = Fp.pow(n, Q1div2); if (Fp.equals(b, Fp.ZERO)) return Fp.ZERO; // https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm (4. If t = 0, return r = 0)
let t = Fp.pow(n, Q); // Find m such b^(2^m)==1
let m = 1;
let t2 = Fp.ZERO; for (let t2 = Fp.square(b); m < r; m++) {
while (!Fp.equals(Fp.sub(t, Fp.ONE), Fp.ZERO)) { if (Fp.equals(t2, Fp.ONE)) break;
t2 = Fp.square(t); t2 = Fp.square(t2); // t2 *= t2
let i;
for (i = 1; i < s; i++) {
// stop if t2-1 == 0
if (Fp.equals(Fp.sub(t2, Fp.ONE), Fp.ZERO)) break;
// t2 *= t2
t2 = Fp.square(t2);
} }
let b = pow(c, BigInt(1 << (s - i - 1)), P); // NOTE: r-m-1 can be bigger than 32, need to convert to bigint before shift, otherwise there will be overflow
r = Fp.mul(r, b); const ge = Fp.pow(g, _1n << BigInt(r - m - 1)); // ge = 2^(r-m-1)
c = mod(b * b, P); g = Fp.square(ge); // g = ge * ge
t = Fp.mul(t, c); x = Fp.mul(x, ge); // x *= ge
s = i; b = Fp.mul(b, g); // b *= g
r = m;
} }
return r; return x;
}; };
} }

View File

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

View File

@@ -40,19 +40,24 @@ export type BasicCurve<T> = {
allowInfinityPoint?: boolean; allowInfinityPoint?: boolean;
}; };
// Bans floats and integers above 2^53-1
export function isPositiveInt(num: any): num is number {
return typeof num === 'number' && Number.isSafeInteger(num) && num > 0;
}
export function validateOpts<FP, T>(curve: BasicCurve<FP> & T) { export function validateOpts<FP, T>(curve: BasicCurve<FP> & T) {
mod.validateField(curve.Fp); mod.validateField(curve.Fp);
for (const i of ['n', 'h'] as const) { for (const i of ['n', 'h'] as const) {
if (typeof curve[i] !== 'bigint') const val = curve[i];
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
} }
if (!curve.Fp.isValid(curve.Gx)) throw new Error('Invalid generator X coordinate Fp element'); if (!curve.Fp.isValid(curve.Gx)) throw new Error('Invalid generator X coordinate Fp element');
if (!curve.Fp.isValid(curve.Gy)) throw new Error('Invalid generator Y coordinate Fp element'); if (!curve.Fp.isValid(curve.Gy)) throw new Error('Invalid generator Y coordinate Fp element');
for (const i of ['nBitLength', 'nByteLength'] as const) { for (const i of ['nBitLength', 'nByteLength'] as const) {
if (curve[i] === undefined) continue; // Optional const val = curve[i];
if (!Number.isSafeInteger(curve[i])) if (val === undefined) continue; // Optional
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); if (!isPositiveInt(val)) throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
} }
// Set defaults // Set defaults
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const); return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
@@ -144,21 +149,22 @@ export function nLength(n: bigint, nBitLength?: number) {
} }
/** /**
* FIPS 186 B.4.1-compliant "constant-time" private key generation utility.
* Can take (n+8) or more bytes of uniform input e.g. from CSPRNG or KDF * Can take (n+8) or more bytes of uniform input e.g. from CSPRNG or KDF
* and convert them into private scalar, with the modulo bias being neglible. * and convert them into private scalar, with the modulo bias being neglible.
* As per FIPS 186 B.4.1. * Needs at least 40 bytes of input for 32-byte private key.
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/ * https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
* @param hash hash output from sha512, or a similar function * @param hash hash output from SHA3 or a similar function
* @returns valid private scalar * @returns valid private scalar
*/ */
export function hashToPrivateScalar(hash: Hex, CURVE_ORDER: bigint, isLE = false): bigint { export function hashToPrivateScalar(hash: Hex, groupOrder: bigint, isLE = false): bigint {
hash = ensureBytes(hash); hash = ensureBytes(hash);
const orderLen = nLength(CURVE_ORDER).nByteLength; const hashLen = hash.length;
const minLen = orderLen + 8; const minLen = nLength(groupOrder).nByteLength + 8;
if (orderLen < 16 || hash.length < minLen || hash.length > 1024) if (minLen < 24 || hashLen < minLen || hashLen > 1024)
throw new Error('Expected valid bytes of private key as per FIPS 186'); throw new Error(`hashToPrivateScalar: expected ${minLen}-1024 bytes of input, got ${hashLen}`);
const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash); const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash);
return mod.mod(num, CURVE_ORDER - _1n) + _1n; return mod.mod(num, groupOrder - _1n) + _1n;
} }
export function equalBytes(b1: Uint8Array, b2: Uint8Array) { export function equalBytes(b1: Uint8Array, b2: Uint8Array) {

View File

@@ -1,28 +1,16 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Short Weierstrass curve. The formula is: y² = x³ + ax + b // Short Weierstrass curve. The formula is: y² = x³ + ax + b
// TODO: sync vs async naming
// TODO: default randomBytes
// Differences from @noble/secp256k1 1.7: // Differences from @noble/secp256k1 1.7:
// 1. Different double() formula (but same addition) // 1. Different double() formula (but same addition)
// 2. Different sqrt() function // 2. Different sqrt() function
// 3. truncateHash() truncateOnly mode // 3. truncateHash() truncateOnly mode
// 4. DRBG supports outputLen bigger than outputLen of hmac // 4. DRBG supports outputLen bigger than outputLen of hmac
// 5. Support for different hash functions
import * as mod from './modular.js'; import * as mod from './modular.js';
import { import * as ut from './utils.js';
bytesToHex, import { bytesToHex, Hex, PrivKey } from './utils.js';
bytesToNumberBE,
concatBytes,
ensureBytes,
hexToBytes,
hexToNumber,
numberToHexUnpadded,
hashToPrivateScalar,
Hex,
PrivKey,
} from './utils.js';
import * as utils from './utils.js';
import { hash_to_field, htfOpts, validateHTFOpts } from './hash-to-curve.js'; import { hash_to_field, htfOpts, validateHTFOpts } from './hash-to-curve.js';
import { Group, GroupConstructor, wNAF } from './group.js'; import { Group, GroupConstructor, wNAF } from './group.js';
@@ -31,76 +19,77 @@ type EndomorphismOpts = {
beta: bigint; beta: bigint;
splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint }; splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint };
}; };
export type BasicCurve<T> = utils.BasicCurve<T> & { export type BasicCurve<T> = ut.BasicCurve<T> & {
// Params: a, b // Params: a, b
a: T; a: T;
b: T; b: T;
// TODO: move into options?
// Optional params
// Executed before privkey validation. Useful for P521 with var-length priv key
normalizePrivateKey?: (key: PrivKey) => PrivKey; normalizePrivateKey?: (key: PrivKey) => PrivKey;
// Whether to execute modular division on a private key, useful for bls curves with cofactor > 1
wrapPrivateKey?: boolean;
// Endomorphism options for Koblitz curves // Endomorphism options for Koblitz curves
endo?: EndomorphismOpts; endo?: EndomorphismOpts;
// Torsions, can be optimized via endomorphisms // When a cofactor != 1, there can be an effective methods to:
// 1. Determine whether a point is torsion-free
isTorsionFree?: (c: ProjectiveConstructor<T>, point: ProjectivePointType<T>) => boolean; isTorsionFree?: (c: ProjectiveConstructor<T>, point: ProjectivePointType<T>) => boolean;
// 2. Clear torsion component
clearCofactor?: ( clearCofactor?: (
c: ProjectiveConstructor<T>, c: ProjectiveConstructor<T>,
point: ProjectivePointType<T> point: ProjectivePointType<T>
) => ProjectivePointType<T>; ) => ProjectivePointType<T>;
// Hash to field opts // Hash to field options
htfDefaults?: htfOpts; htfDefaults?: htfOpts;
mapToCurve?: (scalar: bigint[]) => { x: T; y: T }; mapToCurve?: (scalar: bigint[]) => { x: T; y: T };
}; };
// DER encoding utilities
// ASN.1 DER encoding utilities
class DERError extends Error { class DERError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
} }
} }
function sliceDER(s: string): string { const DER = {
// Proof: any([(i>=0x80) == (int(hex(i).replace('0x', '').zfill(2)[0], 16)>=8) for i in range(0, 256)]) slice(s: string): string {
// Padding done by numberToHex // Proof: any([(i>=0x80) == (int(hex(i).replace('0x', '').zfill(2)[0], 16)>=8) for i in range(0, 256)])
return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s; // Padding done by numberToHex
} return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s;
},
function parseDERInt(data: Uint8Array) { parseInt(data: Uint8Array): { data: bigint; left: Uint8Array } {
if (data.length < 2 || data[0] !== 0x02) { if (data.length < 2 || data[0] !== 0x02) {
throw new DERError(`Invalid signature integer tag: ${bytesToHex(data)}`); throw new DERError(`Invalid signature integer tag: ${bytesToHex(data)}`);
} }
const len = data[1]; const len = data[1];
const res = data.subarray(2, len + 2); const res = data.subarray(2, len + 2);
if (!len || res.length !== len) { if (!len || res.length !== len) {
throw new DERError(`Invalid signature integer: wrong length`); throw new DERError(`Invalid signature integer: wrong length`);
} }
// Strange condition, its not about length, but about first bytes of number. // Strange condition, its not about length, but about first bytes of number.
if (res[0] === 0x00 && res[1] <= 0x7f) { if (res[0] === 0x00 && res[1] <= 0x7f) {
throw new DERError('Invalid signature integer: trailing length'); throw new DERError('Invalid signature integer: trailing length');
} }
return { data: bytesToNumberBE(res), left: data.subarray(len + 2) }; return { data: ut.bytesToNumberBE(res), left: data.subarray(len + 2) };
} },
parseSig(data: Uint8Array): { r: bigint; s: bigint } {
function parseDERSignature(data: Uint8Array) { if (data.length < 2 || data[0] != 0x30) {
if (data.length < 2 || data[0] != 0x30) { throw new DERError(`Invalid signature tag: ${bytesToHex(data)}`);
throw new DERError(`Invalid signature tag: ${bytesToHex(data)}`); }
} if (data[1] !== data.length - 2) {
if (data[1] !== data.length - 2) { throw new DERError('Invalid signature: incorrect length');
throw new DERError('Invalid signature: incorrect length'); }
} const { data: r, left: sBytes } = DER.parseInt(data.subarray(2));
const { data: r, left: sBytes } = parseDERInt(data.subarray(2)); const { data: s, left: rBytesLeft } = DER.parseInt(sBytes);
const { data: s, left: rBytesLeft } = parseDERInt(sBytes); if (rBytesLeft.length) {
if (rBytesLeft.length) { throw new DERError(`Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`);
throw new DERError(`Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`); }
} return { r, s };
return { r, s }; },
} };
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0);
const _1n = BigInt(1);
const _3n = BigInt(3);
type Entropy = Hex | true; type Entropy = Hex | true;
type SignOpts = { lowS?: boolean; extraEntropy?: Entropy }; export type SignOpts = { lowS?: boolean; extraEntropy?: Entropy };
/** /**
* ### Design rationale for types * ### Design rationale for types
@@ -124,7 +113,7 @@ type SignOpts = { lowS?: boolean; extraEntropy?: Entropy };
* TODO: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#unique-symbol * TODO: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#unique-symbol
*/ */
// Instance // Instance for 3d XYZ points
export interface ProjectivePointType<T> extends Group<ProjectivePointType<T>> { export interface ProjectivePointType<T> extends Group<ProjectivePointType<T>> {
readonly x: T; readonly x: T;
readonly y: T; readonly y: T;
@@ -133,14 +122,14 @@ export interface ProjectivePointType<T> extends Group<ProjectivePointType<T>> {
multiplyUnsafe(scalar: bigint): ProjectivePointType<T>; multiplyUnsafe(scalar: bigint): ProjectivePointType<T>;
toAffine(invZ?: T): PointType<T>; toAffine(invZ?: T): PointType<T>;
} }
// Static methods // Static methods for 3d XYZ points
export interface ProjectiveConstructor<T> extends GroupConstructor<ProjectivePointType<T>> { export interface ProjectiveConstructor<T> extends GroupConstructor<ProjectivePointType<T>> {
new (x: T, y: T, z: T): ProjectivePointType<T>; new (x: T, y: T, z: T): ProjectivePointType<T>;
fromAffine(p: PointType<T>): ProjectivePointType<T>; fromAffine(p: PointType<T>): ProjectivePointType<T>;
toAffineBatch(points: ProjectivePointType<T>[]): PointType<T>[]; toAffineBatch(points: ProjectivePointType<T>[]): PointType<T>[];
normalizeZ(points: ProjectivePointType<T>[]): ProjectivePointType<T>[]; normalizeZ(points: ProjectivePointType<T>[]): ProjectivePointType<T>[];
} }
// Instance // Instance for 2d XY points
export interface PointType<T> extends Group<PointType<T>> { export interface PointType<T> extends Group<PointType<T>> {
readonly x: T; readonly x: T;
readonly y: T; readonly y: T;
@@ -151,7 +140,7 @@ export interface PointType<T> extends Group<PointType<T>> {
assertValidity(): void; assertValidity(): void;
multiplyAndAddUnsafe(Q: PointType<T>, a: bigint, b: bigint): PointType<T> | undefined; multiplyAndAddUnsafe(Q: PointType<T>, a: bigint, b: bigint): PointType<T> | undefined;
} }
// Static methods // Static methods for 2d XY points
export interface PointConstructor<T> extends GroupConstructor<PointType<T>> { export interface PointConstructor<T> extends GroupConstructor<PointType<T>> {
new (x: T, y: T): PointType<T>; new (x: T, y: T): PointType<T>;
fromHex(hex: Hex): PointType<T>; fromHex(hex: Hex): PointType<T>;
@@ -167,7 +156,7 @@ export type CurvePointsType<T> = BasicCurve<T> & {
}; };
function validatePointOpts<T>(curve: CurvePointsType<T>) { function validatePointOpts<T>(curve: CurvePointsType<T>) {
const opts = utils.validateOpts(curve); const opts = ut.validateOpts(curve);
const Fp = opts.Fp; const Fp = opts.Fp;
for (const i of ['a', 'b'] as const) { for (const i of ['a', 'b'] as const) {
if (!Fp.isValid(curve[i])) if (!Fp.isValid(curve[i]))
@@ -206,22 +195,14 @@ export type CurvePointsRes<T> = {
isWithinCurveOrder: (num: bigint) => boolean; isWithinCurveOrder: (num: bigint) => boolean;
}; };
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
const _0n = BigInt(0);
const _1n = BigInt(1);
const _3n = BigInt(3);
export function weierstrassPoints<T>(opts: CurvePointsType<T>) { export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
const CURVE = validatePointOpts(opts); const CURVE = validatePointOpts(opts);
const Fp = CURVE.Fp; const { Fp } = CURVE; // All curves has same field / group length as for now, but they can differ
// Lengths
// All curves has same field / group length as for now, but it can be different for other curves
const { nByteLength, nBitLength } = CURVE;
const groupLen = nByteLength;
// Not using ** operator with bigints for old engines.
// 2n ** (8n * 32n) == 2n << (8n * 32n - 1n)
//const FIELD_MASK = _2n << (_8n * BigInt(fieldLen) - _1n);
// function numToFieldStr(num: bigint): string {
// if (typeof num !== 'bigint') throw new Error('Expected bigint');
// if (!(_0n <= num && num < FIELD_MASK)) throw new Error(`Expected number < 2^${fieldLen * 8}`);
// return num.toString(16).padStart(2 * fieldLen, '0');
// }
/** /**
* y² = x³ + ax + b: Short weierstrass curve formula * y² = x³ + ax + b: Short weierstrass curve formula
@@ -234,35 +215,48 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return Fp.add(Fp.add(x3, Fp.mul(x, a)), b); // x3 + a * x + b return Fp.add(Fp.add(x3, Fp.mul(x, a)), b); // x3 + a * x + b
} }
// Valid group elements reside in range 1..n-1
function isWithinCurveOrder(num: bigint): boolean { function isWithinCurveOrder(num: bigint): boolean {
return _0n < num && num < CURVE.n; return _0n < num && num < CURVE.n;
} }
/**
* Validates if a private key is valid and converts it to bigint form.
* Supports two options, that are passed when CURVE is initialized:
* - `normalizePrivateKey()` executed before all checks
* - `wrapPrivateKey` when true, executed after most checks, but before `0 < key < n`
*/
function normalizePrivateKey(key: PrivKey): bigint { function normalizePrivateKey(key: PrivKey): bigint {
if (typeof CURVE.normalizePrivateKey === 'function') { const { normalizePrivateKey: custom, nByteLength: groupLen, wrapPrivateKey, n: order } = CURVE;
key = CURVE.normalizePrivateKey(key); if (typeof custom === 'function') key = custom(key);
}
let num: bigint; let num: bigint;
if (typeof key === 'bigint') { if (typeof key === 'bigint') {
// Curve order check is done below
num = key; num = key;
} else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) { } else if (ut.isPositiveInt(key)) {
num = BigInt(key); num = BigInt(key);
} else if (typeof key === 'string') { } else if (typeof key === 'string') {
if (key.length !== 2 * groupLen) throw new Error(`Expected ${groupLen} bytes of private key`); if (key.length !== 2 * groupLen) throw new Error(`Expected ${groupLen} bytes of private key`);
num = hexToNumber(key); // Validates individual octets
num = ut.hexToNumber(key);
} else if (key instanceof Uint8Array) { } else if (key instanceof Uint8Array) {
if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes of private key`); if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes of private key`);
num = bytesToNumberBE(key); num = ut.bytesToNumberBE(key);
} else { } else {
throw new TypeError('Expected valid private key'); throw new TypeError('Expected valid private key');
} }
if (CURVE.wrapPrivateKey) num = mod.mod(num, CURVE.n); // Useful for curves with cofactor != 1
if (wrapPrivateKey) num = mod.mod(num, order);
if (!isWithinCurveOrder(num)) throw new Error('Expected private key: 0 < key < n'); if (!isWithinCurveOrder(num)) throw new Error('Expected private key: 0 < key < n');
return num; return num;
} }
/**
* Validates if a scalar ("private number") is valid.
* Scalars are valid only if they are less than curve order.
*/
function normalizeScalar(num: number | bigint): bigint { function normalizeScalar(num: number | bigint): bigint {
if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) return BigInt(num); if (ut.isPositiveInt(num)) return BigInt(num);
if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num; if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num;
throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n'); throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n');
} }
@@ -289,7 +283,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
/** /**
* Takes a bunch of Projective Points but executes only one * Takes a bunch of Projective Points but executes only one
* invert on all of them. invert is very slow operation, * inversion on all of them. Inversion is very slow operation,
* so this improves performance massively. * so this improves performance massively.
*/ */
static toAffineBatch(points: ProjectivePoint[]): Point[] { static toAffineBatch(points: ProjectivePoint[]): Point[] {
@@ -297,6 +291,9 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return points.map((p, i) => p.toAffine(toInv[i])); return points.map((p, i) => p.toAffine(toInv[i]));
} }
/**
* Optimization: converts a list of projective points to a list of identical points with Z=1.
*/
static normalizeZ(points: ProjectivePoint[]): ProjectivePoint[] { static normalizeZ(points: ProjectivePoint[]): ProjectivePoint[] {
return ProjectivePoint.toAffineBatch(points).map(ProjectivePoint.fromAffine); return ProjectivePoint.toAffineBatch(points).map(ProjectivePoint.fromAffine);
} }
@@ -320,10 +317,6 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return new ProjectivePoint(this.x, Fp.negate(this.y), this.z); return new ProjectivePoint(this.x, Fp.negate(this.y), this.z);
} }
doubleAdd(): ProjectivePoint {
return this.add(this);
}
// Renes-Costello-Batina exception-free doubling formula. // Renes-Costello-Batina exception-free doubling formula.
// There is 30% faster Jacobian formula, but it is not complete. // There is 30% faster Jacobian formula, but it is not complete.
// https://eprint.iacr.org/2015/1060, algorithm 3 // https://eprint.iacr.org/2015/1060, algorithm 3
@@ -525,24 +518,25 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return new Point(ax, ay); return new Point(ax, ay);
} }
isTorsionFree(): boolean { isTorsionFree(): boolean {
if (CURVE.h === _1n) return true; // No subgroups, always torsion fee const { h: cofactor, isTorsionFree } = CURVE;
if (CURVE.isTorsionFree) return CURVE.isTorsionFree(ProjectivePoint, this); if (cofactor === _1n) return true; // No subgroups, always torsion-free
// is multiplyUnsafe(CURVE.n) is always ok, same as for edwards? if (isTorsionFree) return isTorsionFree(ProjectivePoint, this);
throw new Error('Unsupported!'); throw new Error('isTorsionFree() has not been declared for the elliptic curve');
} }
// Clear cofactor of G1
// https://eprint.iacr.org/2019/403
clearCofactor(): ProjectivePoint { clearCofactor(): ProjectivePoint {
if (CURVE.h === _1n) return this; // Fast-path const { h: cofactor, clearCofactor } = CURVE;
if (CURVE.clearCofactor) return CURVE.clearCofactor(ProjectivePoint, this) as ProjectivePoint; if (cofactor === _1n) return this; // Fast-path
if (clearCofactor) return clearCofactor(ProjectivePoint, this) as ProjectivePoint;
return this.multiplyUnsafe(CURVE.h); return this.multiplyUnsafe(CURVE.h);
} }
} }
const wnaf = wNAF(ProjectivePoint, CURVE.endo ? nBitLength / 2 : nBitLength); const _bits = CURVE.nBitLength;
const wnaf = wNAF(ProjectivePoint, CURVE.endo ? Math.ceil(_bits / 2) : _bits);
function assertPrjPoint(other: unknown) { function assertPrjPoint(other: unknown) {
if (!(other instanceof ProjectivePoint)) throw new TypeError('ProjectivePoint expected'); if (!(other instanceof ProjectivePoint)) throw new TypeError('ProjectivePoint expected');
} }
// Stores precomputed values for points. // Stores precomputed values for points.
const pointPrecomputes = new WeakMap<Point, ProjectivePoint[]>(); const pointPrecomputes = new WeakMap<Point, ProjectivePoint[]>();
@@ -551,11 +545,11 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
*/ */
class Point implements PointType<T> { class Point implements PointType<T> {
/** /**
* Base point aka generator. public_key = Point.BASE * private_key * Base point aka generator. Any public_key = Point.BASE * private_key
*/ */
static BASE: Point = new Point(CURVE.Gx, CURVE.Gy); static BASE: Point = new Point(CURVE.Gx, CURVE.Gy);
/** /**
* Identity point aka point at infinity. point = point + zero_point * Identity point aka point at infinity. p - p = zero_p; p + zero_p = p
*/ */
static ZERO: Point = new Point(Fp.ZERO, Fp.ZERO); static ZERO: Point = new Point(Fp.ZERO, Fp.ZERO);
@@ -583,7 +577,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
* @param hex short/long ECDSA hex * @param hex short/long ECDSA hex
*/ */
static fromHex(hex: Hex): Point { static fromHex(hex: Hex): Point {
const { x, y } = CURVE.fromBytes(ensureBytes(hex)); const { x, y } = CURVE.fromBytes(ut.ensureBytes(hex));
const point = new Point(x, y); const point = new Point(x, y);
point.assertValidity(); point.assertValidity();
return point; return point;
@@ -607,16 +601,16 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
// Zero is valid point too! // Zero is valid point too!
if (this.equals(Point.ZERO)) { if (this.equals(Point.ZERO)) {
if (CURVE.allowInfinityPoint) return; if (CURVE.allowInfinityPoint) return;
throw new Error('Point is infinity'); throw new Error('Point at infinity');
} }
// Some 3rd-party test vectors require different wording between here & `fromCompressedHex` // Some 3rd-party test vectors require different wording between here & `fromCompressedHex`
const msg = 'Point is not on elliptic curve'; const msg = 'Point is not on elliptic curve';
const { x, y } = this; const { x, y } = this;
// Check if x, y are valid field elements
if (!Fp.isValid(x) || !Fp.isValid(y)) throw new Error(msg); if (!Fp.isValid(x) || !Fp.isValid(y)) throw new Error(msg);
const left = Fp.square(y); const left = Fp.square(y); // y²
const right = weierstrassEquation(x); const right = weierstrassEquation(x); // x³ + ax + b
if (!Fp.equals(left, right)) throw new Error(msg); if (!Fp.equals(left, right)) throw new Error(msg);
// TODO: flag to disable this?
if (!this.isTorsionFree()) throw new Error('Point must be of prime-order subgroup'); if (!this.isTorsionFree()) throw new Error('Point must be of prime-order subgroup');
} }
@@ -630,34 +624,37 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
return new Point(this.x, Fp.negate(this.y)); return new Point(this.x, Fp.negate(this.y));
} }
protected toProj() {
return ProjectivePoint.fromAffine(this);
}
// Adds point to itself // Adds point to itself
double() { double() {
return ProjectivePoint.fromAffine(this).double().toAffine(); return this.toProj().double().toAffine();
} }
// Adds point to other point
add(other: Point) { add(other: Point) {
return ProjectivePoint.fromAffine(this).add(ProjectivePoint.fromAffine(other)).toAffine(); return this.toProj().add(ProjectivePoint.fromAffine(other)).toAffine();
} }
// Subtracts other point from the point
subtract(other: Point) { subtract(other: Point) {
return this.add(other.negate()); return this.add(other.negate());
} }
multiply(scalar: number | bigint) { multiply(scalar: number | bigint) {
return ProjectivePoint.fromAffine(this).multiply(scalar, this).toAffine(); return this.toProj().multiply(scalar, this).toAffine();
} }
multiplyUnsafe(scalar: bigint) { multiplyUnsafe(scalar: bigint) {
return ProjectivePoint.fromAffine(this).multiplyUnsafe(scalar).toAffine(); return this.toProj().multiplyUnsafe(scalar).toAffine();
} }
clearCofactor() { clearCofactor() {
return ProjectivePoint.fromAffine(this).clearCofactor().toAffine(); return this.toProj().clearCofactor().toAffine();
} }
isTorsionFree(): boolean { isTorsionFree(): boolean {
return ProjectivePoint.fromAffine(this).isTorsionFree(); return this.toProj().isTorsionFree();
} }
/** /**
@@ -667,7 +664,7 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
* @returns non-zero affine point * @returns non-zero affine point
*/ */
multiplyAndAddUnsafe(Q: Point, a: bigint, b: bigint): Point | undefined { multiplyAndAddUnsafe(Q: Point, a: bigint, b: bigint): Point | undefined {
const P = ProjectivePoint.fromAffine(this); const P = this.toProj();
const aP = const aP =
a === _0n || a === _1n || this !== Point.BASE ? P.multiplyUnsafe(a) : P.multiply(a); a === _0n || a === _1n || this !== Point.BASE ? P.multiplyUnsafe(a) : P.multiply(a);
const bQ = ProjectivePoint.fromAffine(Q).multiplyUnsafe(b); const bQ = ProjectivePoint.fromAffine(Q).multiplyUnsafe(b);
@@ -678,23 +675,26 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
// Encodes byte string to elliptic curve // Encodes byte string to elliptic curve
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3
static hashToCurve(msg: Hex, options?: Partial<htfOpts>) { static hashToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve'); const { mapToCurve } = CURVE;
msg = ensureBytes(msg); if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined');
msg = ut.ensureBytes(msg);
const u = hash_to_field(msg, 2, { ...CURVE.htfDefaults, ...options } as htfOpts); const u = hash_to_field(msg, 2, { ...CURVE.htfDefaults, ...options } as htfOpts);
const { x: x0, y: y0 } = CURVE.mapToCurve(u[0]); const { x: x0, y: y0 } = mapToCurve(u[0]);
const { x: x1, y: y1 } = CURVE.mapToCurve(u[1]); const { x: x1, y: y1 } = mapToCurve(u[1]);
const p = new Point(x0, y0).add(new Point(x1, y1)).clearCofactor(); return new Point(x0, y0).add(new Point(x1, y1)).clearCofactor();
return p;
} }
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3
static encodeToCurve(msg: Hex, options?: Partial<htfOpts>) { static encodeToCurve(msg: Hex, options?: Partial<htfOpts>) {
if (!CURVE.mapToCurve) throw new Error('No mapToCurve defined for curve'); const { mapToCurve } = CURVE;
msg = ensureBytes(msg); if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined');
msg = ut.ensureBytes(msg);
const u = hash_to_field(msg, 1, { ...CURVE.htfDefaults, ...options } as htfOpts); const u = hash_to_field(msg, 1, { ...CURVE.htfDefaults, ...options } as htfOpts);
const { x, y } = CURVE.mapToCurve(u[0]); const { x, y } = mapToCurve(u[0]);
return new Point(x, y).clearCofactor(); return new Point(x, y).clearCofactor();
} }
} }
return { return {
Point: Point as PointConstructor<T>, Point: Point as PointConstructor<T>,
ProjectivePoint: ProjectivePoint as ProjectiveConstructor<T>, ProjectivePoint: ProjectivePoint as ProjectiveConstructor<T>,
@@ -733,15 +733,15 @@ export type CurveType = BasicCurve<bigint> & {
// Default options // Default options
lowS?: boolean; lowS?: boolean;
// Hashes // Hashes
hash: utils.CHash; // Because we need outputLen for DRBG hash: ut.CHash; // Because we need outputLen for DRBG
hmac: HmacFnSync; hmac: HmacFnSync;
randomBytes: (bytesLength?: number) => Uint8Array; randomBytes: (bytesLength?: number) => Uint8Array;
truncateHash?: (hash: Uint8Array, truncateOnly?: boolean) => bigint; truncateHash?: (hash: Uint8Array, truncateOnly?: boolean) => bigint;
}; };
function validateOpts(curve: CurveType) { function validateOpts(curve: CurveType) {
const opts = utils.validateOpts(curve); const opts = ut.validateOpts(curve);
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen)) if (typeof opts.hash !== 'function' || !ut.isPositiveInt(opts.hash.outputLen))
throw new Error('Invalid hash function'); throw new Error('Invalid hash function');
if (typeof opts.hmac !== 'function') throw new Error('Invalid hmac function'); if (typeof opts.hmac !== 'function') throw new Error('Invalid hmac function');
if (typeof opts.randomBytes !== 'function') throw new Error('Invalid randomBytes function'); if (typeof opts.randomBytes !== 'function') throw new Error('Invalid randomBytes function');
@@ -754,6 +754,7 @@ export type CurveFn = {
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
getSharedSecret: (privateA: PrivKey, publicB: PubKey, isCompressed?: boolean) => Uint8Array; getSharedSecret: (privateA: PrivKey, publicB: PubKey, isCompressed?: boolean) => Uint8Array;
sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => SignatureType; sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => SignatureType;
signUnhashed: (msg: Uint8Array, privKey: PrivKey, opts?: SignOpts) => SignatureType;
verify: ( verify: (
signature: Hex | SignatureType, signature: Hex | SignatureType,
msgHash: Hex, msgHash: Hex,
@@ -766,8 +767,6 @@ export type CurveFn = {
ProjectivePoint: ProjectiveConstructor<bigint>; ProjectivePoint: ProjectiveConstructor<bigint>;
Signature: SignatureConstructor; Signature: SignatureConstructor;
utils: { utils: {
mod: (a: bigint, b?: bigint) => bigint;
invert: (number: bigint, modulo?: bigint) => bigint;
_bigintToBytes: (num: bigint) => Uint8Array; _bigintToBytes: (num: bigint) => Uint8Array;
_bigintToString: (num: bigint) => string; _bigintToString: (num: bigint) => string;
_normalizePrivateKey: (key: PrivKey) => bigint; _normalizePrivateKey: (key: PrivKey) => bigint;
@@ -824,19 +823,17 @@ class HmacDrbg {
out.push(sl); out.push(sl);
len += this.v.length; len += this.v.length;
} }
return concatBytes(...out); return ut.concatBytes(...out);
} }
// There is no need in clean() method // There are no guarantees with JS GC whether bigints are removed even if you clean Uint8Arrays.
// It's useless, there are no guarantees with JS GC
// whether bigints are removed even if you clean Uint8Arrays.
} }
export function weierstrass(curveDef: CurveType): CurveFn { export function weierstrass(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>; const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
const CURVE_ORDER = CURVE.n; const CURVE_ORDER = CURVE.n;
const Fp = CURVE.Fp; const Fp = CURVE.Fp;
const compressedLen = Fp.BYTES + 1; // 33 const compressedLen = Fp.BYTES + 1; // e.g. 33 for 32
const uncompressedLen = 2 * Fp.BYTES + 1; // 65 const uncompressedLen = 2 * Fp.BYTES + 1; // e.g. 65 for 32
function isValidFieldElement(num: bigint): boolean { function isValidFieldElement(num: bigint): boolean {
// 0 is disallowed by arbitrary reasons. Probably because infinity point? // 0 is disallowed by arbitrary reasons. Probably because infinity point?
@@ -847,10 +844,12 @@ export function weierstrass(curveDef: CurveType): CurveFn {
weierstrassPoints({ weierstrassPoints({
...CURVE, ...CURVE,
toBytes(c, point, isCompressed: boolean): Uint8Array { toBytes(c, point, isCompressed: boolean): Uint8Array {
const x = Fp.toBytes(point.x);
const cat = ut.concatBytes;
if (isCompressed) { if (isCompressed) {
return concatBytes(new Uint8Array([point.hasEvenY() ? 0x02 : 0x03]), Fp.toBytes(point.x)); return cat(Uint8Array.from([point.hasEvenY() ? 0x02 : 0x03]), x);
} else { } else {
return concatBytes(new Uint8Array([0x04]), Fp.toBytes(point.x), Fp.toBytes(point.y)); return cat(Uint8Array.from([0x04]), x, Fp.toBytes(point.y));
} }
}, },
fromBytes(bytes: Uint8Array) { fromBytes(bytes: Uint8Array) {
@@ -858,7 +857,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const header = bytes[0]; const header = bytes[0];
// this.assertValidity() is done inside of fromHex // this.assertValidity() is done inside of fromHex
if (len === compressedLen && (header === 0x02 || header === 0x03)) { if (len === compressedLen && (header === 0x02 || header === 0x03)) {
const x = bytesToNumberBE(bytes.subarray(1)); const x = ut.bytesToNumberBE(bytes.subarray(1));
if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); if (!isValidFieldElement(x)) throw new Error('Point is not on curve');
const y2 = weierstrassEquation(x); // y² = x³ + ax + b const y2 = weierstrassEquation(x); // y² = x³ + ax + b
let y = Fp.sqrt(y2); // y = y² ^ (p+1)/4 let y = Fp.sqrt(y2); // y = y² ^ (p+1)/4
@@ -910,15 +909,18 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE_ORDER) : s; return isBiggerThanHalfOrder(s) ? mod.mod(-s, CURVE_ORDER) : s;
} }
function bits2int_2(bytes: Uint8Array): bigint {
const delta = bytes.length * 8 - CURVE.nBitLength;
const num = ut.bytesToNumberBE(bytes);
return delta > 0 ? num >> BigInt(delta) : num;
}
// Ensures ECDSA message hashes are 32 bytes and < curve order // Ensures ECDSA message hashes are 32 bytes and < curve order
function _truncateHash(hash: Uint8Array, truncateOnly = false): bigint { function _truncateHash(hash: Uint8Array, truncateOnly = false): bigint {
const { n, nBitLength } = CURVE; const h = bits2int_2(hash);
const byteLength = hash.length; if (truncateOnly) return h;
const delta = byteLength * 8 - nBitLength; // size of curve.n (252 bits) const { n } = CURVE;
let h = bytesToNumberBE(hash); return h >= n ? h - n : h;
if (delta > 0) h = h >> BigInt(delta);
if (!truncateOnly && h >= n) h -= n;
return h;
} }
const truncateHash = CURVE.truncateHash || _truncateHash; const truncateHash = CURVE.truncateHash || _truncateHash;
@@ -930,15 +932,17 @@ export function weierstrass(curveDef: CurveType): CurveFn {
this.assertValidity(); this.assertValidity();
} }
// pair (32 bytes of r, 32 bytes of s) // pair (bytes of r, bytes of s)
static fromCompact(hex: Hex) { static fromCompact(hex: Hex) {
const arr = hex instanceof Uint8Array; const arr = hex instanceof Uint8Array;
const name = 'Signature.fromCompact'; const name = 'Signature.fromCompact';
if (typeof hex !== 'string' && !arr) if (typeof hex !== 'string' && !arr)
throw new TypeError(`${name}: Expected string or Uint8Array`); throw new TypeError(`${name}: Expected string or Uint8Array`);
const str = arr ? bytesToHex(hex) : hex; const str = arr ? bytesToHex(hex) : hex;
if (str.length !== 128) throw new Error(`${name}: Expected 64-byte hex`); const gl = CURVE.nByteLength * 2; // group length in hex, not ui8a
return new Signature(hexToNumber(str.slice(0, 64)), hexToNumber(str.slice(64, 128))); if (str.length !== 2 * gl) throw new Error(`${name}: Expected ${gl / 2}-byte hex`);
const slice = (from: number, to: number) => ut.hexToNumber(str.slice(from, to));
return new Signature(slice(0, gl), slice(gl, 2 * gl));
} }
// DER encoded ECDSA signature // DER encoded ECDSA signature
@@ -947,7 +951,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const arr = hex instanceof Uint8Array; const arr = hex instanceof Uint8Array;
if (typeof hex !== 'string' && !arr) if (typeof hex !== 'string' && !arr)
throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`); throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`);
const { r, s } = parseDERSignature(arr ? hex : hexToBytes(hex)); const { r, s } = DER.parseSig(arr ? hex : ut.hexToBytes(hex));
return new Signature(r, s); return new Signature(r, s);
} }
@@ -964,6 +968,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
/** /**
* Recovers public key from signature with recovery bit. Throws on invalid hash. * Recovers public key from signature with recovery bit. Throws on invalid hash.
* https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery * https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery
* It's also possible to recover key without bit: try all 4 bit values and check for sig match.
* *
* ``` * ```
* recover(r, s, h) where * recover(r, s, h) where
@@ -978,16 +983,18 @@ export function weierstrass(curveDef: CurveType): CurveFn {
recoverPublicKey(msgHash: Hex): Point { recoverPublicKey(msgHash: Hex): Point {
const { r, s, recovery } = this; const { r, s, recovery } = this;
if (recovery == null) throw new Error('Cannot recover: recovery bit is not present'); if (recovery == null) throw new Error('Cannot recover: recovery bit is not present');
if (recovery !== 0 && recovery !== 1) throw new Error('Cannot recover: invalid recovery bit'); if (![0, 1, 2, 3].includes(recovery)) throw new Error('Cannot recover: invalid recovery bit');
const h = truncateHash(ensureBytes(msgHash)); const h = truncateHash(ut.ensureBytes(msgHash));
const { n } = CURVE; const { n } = CURVE;
const rinv = mod.invert(r, n); const radj = recovery === 2 || recovery === 3 ? r + n : r;
if (radj >= Fp.ORDER) throw new Error('Cannot recover: bit 2/3 is invalid with current r');
const rinv = mod.invert(radj, n);
// Q = u1⋅G + u2⋅R // Q = u1⋅G + u2⋅R
const u1 = mod.mod(-h * rinv, n); const u1 = mod.mod(-h * rinv, n);
const u2 = mod.mod(s * rinv, n); const u2 = mod.mod(s * rinv, n);
const prefix = recovery & 1 ? '03' : '02'; const prefix = recovery & 1 ? '03' : '02';
const R = Point.fromHex(prefix + numToFieldStr(r)); const R = Point.fromHex(prefix + numToFieldStr(radj));
const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); // unsafe is fine: no priv data leaked
if (!Q) throw new Error('Cannot recover: point at infinify'); if (!Q) throw new Error('Cannot recover: point at infinify');
Q.assertValidity(); Q.assertValidity();
return Q; return Q;
@@ -1009,22 +1016,24 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
// DER-encoded // DER-encoded
toDERRawBytes(isCompressed = false) { toDERRawBytes() {
return hexToBytes(this.toDERHex(isCompressed)); return ut.hexToBytes(this.toDERHex());
} }
toDERHex(isCompressed = false) { toDERHex() {
const sHex = sliceDER(numberToHexUnpadded(this.s)); const { numberToHexUnpadded: toHex } = ut;
if (isCompressed) return sHex; const sHex = DER.slice(toHex(this.s));
const rHex = sliceDER(numberToHexUnpadded(this.r)); const rHex = DER.slice(toHex(this.r));
const rLen = numberToHexUnpadded(rHex.length / 2); const sHexL = sHex.length / 2;
const sLen = numberToHexUnpadded(sHex.length / 2); const rHexL = rHex.length / 2;
const length = numberToHexUnpadded(rHex.length / 2 + sHex.length / 2 + 4); const sLen = toHex(sHexL);
const rLen = toHex(rHexL);
const length = toHex(rHexL + sHexL + 4);
return `30${length}02${rLen}${rHex}02${sLen}${sHex}`; return `30${length}02${rLen}${rHex}02${sLen}${sHex}`;
} }
// 32 bytes of r, then 32 bytes of s // padded bytes of r, then padded bytes of s
toCompactRawBytes() { toCompactRawBytes() {
return hexToBytes(this.toCompactHex()); return ut.hexToBytes(this.toCompactHex());
} }
toCompactHex() { toCompactHex() {
return numToFieldStr(this.r) + numToFieldStr(this.s); return numToFieldStr(this.r) + numToFieldStr(this.s);
@@ -1032,8 +1041,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
const utils = { const utils = {
mod: (n: bigint, modulo = Fp.ORDER) => mod.mod(n, modulo),
invert: Fp.invert,
isValidPrivateKey(privateKey: PrivKey) { isValidPrivateKey(privateKey: PrivKey) {
try { try {
normalizePrivateKey(privateKey); normalizePrivateKey(privateKey);
@@ -1053,7 +1060,8 @@ export function weierstrass(curveDef: CurveType): CurveFn {
/** /**
* Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes. * Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes.
*/ */
hashToPrivateKey: (hash: Hex): Uint8Array => numToField(hashToPrivateScalar(hash, CURVE_ORDER)), hashToPrivateKey: (hash: Hex): Uint8Array =>
numToField(ut.hashToPrivateScalar(hash, CURVE_ORDER)),
/** /**
* Produces cryptographically secure private key from random of size (nBitLength+64) * Produces cryptographically secure private key from random of size (nBitLength+64)
@@ -1078,10 +1086,10 @@ export function weierstrass(curveDef: CurveType): CurveFn {
}; };
/** /**
* Computes public key for a private key. * Computes public key for a private key. Checks for validity of the private key.
* @param privateKey private key * @param privateKey private key
* @param isCompressed whether to return compact, or full key * @param isCompressed whether to return compact (default), or full key
* @returns Public key, full by default; short when isCompressed=true * @returns Public key, full when isCompressed=false; short when isCompressed=true
*/ */
function getPublicKey(privateKey: PrivKey, isCompressed = false): Uint8Array { function getPublicKey(privateKey: PrivKey, isCompressed = false): Uint8Array {
return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed); return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed);
@@ -1101,12 +1109,12 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
/** /**
* ECDH (Elliptic Curve Diffie Hellman) implementation. * ECDH (Elliptic Curve Diffie Hellman).
* 1. Checks for validity of private key * Computes shared public key from private key and public key.
* 2. Checks for the public key of being on-curve * Checks: 1) private key validity 2) shared key is on-curve
* @param privateA private key * @param privateA private key
* @param publicB different public key * @param publicB different public key
* @param isCompressed whether to return compact (33-byte), or full (65-byte) key * @param isCompressed whether to return compact (default), or full key
* @returns shared public key * @returns shared public key
*/ */
function getSharedSecret(privateA: PrivKey, publicB: PubKey, isCompressed = false): Uint8Array { function getSharedSecret(privateA: PrivKey, publicB: PubKey, isCompressed = false): Uint8Array {
@@ -1118,9 +1126,21 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
// RFC6979 methods // RFC6979 methods
function bits2int(bytes: Uint8Array) { function bits2int(bytes: Uint8Array): bigint {
const slice = bytes.length > Fp.BYTES ? bytes.slice(0, Fp.BYTES) : bytes; const { nByteLength } = CURVE;
return bytesToNumberBE(slice); if (!(bytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
const slice = bytes.length > nByteLength ? bytes.slice(0, nByteLength) : bytes;
// const slice = bytes; nByteLength; nBitLength;
let num = ut.bytesToNumberBE(slice);
// const { nBitLength } = CURVE;
// const delta = (bytes.length * 8) - nBitLength;
// if (delta > 0) {
// // console.log('bits=', bytes.length*8, 'CURVE n=', nBitLength, 'delta=', delta);
// // console.log(bytes.length, nBitLength, delta);
// // console.log(bytes, new Error().stack);
// num >>= BigInt(delta);
// }
return num;
} }
function bits2octets(bytes: Uint8Array): Uint8Array { function bits2octets(bytes: Uint8Array): Uint8Array {
const z1 = bits2int(bytes); const z1 = bits2int(bytes);
@@ -1128,28 +1148,28 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return int2octets(z2 < _0n ? z1 : z2); return int2octets(z2 < _0n ? z1 : z2);
} }
function int2octets(num: bigint): Uint8Array { function int2octets(num: bigint): Uint8Array {
return numToField(num); // prohibits >32 bytes return numToField(num); // prohibits >nByteLength bytes
} }
// Steps A, D of RFC6979 3.2 // Steps A, D of RFC6979 3.2
// Creates RFC6979 seed; converts msg/privKey to numbers. // Creates RFC6979 seed; converts msg/privKey to numbers.
function initSigArgs(msgHash: Hex, privateKey: PrivKey, extraEntropy?: Entropy) { function initSigArgs(msgHash: Hex, privateKey: PrivKey, extraEntropy?: Entropy) {
if (msgHash == null) throw new Error(`sign: expected valid message hash, not "${msgHash}"`); if (msgHash == null) throw new Error(`sign: expected valid message hash, not "${msgHash}"`);
// Step A is ignored, since we already provide hash instead of msg // Step A is ignored, since we already provide hash instead of msg
const h1 = numToField(truncateHash(ensureBytes(msgHash))); const h1 = numToField(truncateHash(ut.ensureBytes(msgHash)));
const d = normalizePrivateKey(privateKey); const d = normalizePrivateKey(privateKey);
// K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k') // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
const seedArgs = [int2octets(d), bits2octets(h1)]; const seedArgs = [int2octets(d), bits2octets(h1)];
// RFC6979 3.6: additional k' could be provided // RFC6979 3.6: additional k' could be provided
if (extraEntropy != null) { if (extraEntropy != null) {
if (extraEntropy === true) extraEntropy = CURVE.randomBytes(Fp.BYTES); if (extraEntropy === true) extraEntropy = CURVE.randomBytes(Fp.BYTES);
const e = ensureBytes(extraEntropy); const e = ut.ensureBytes(extraEntropy);
if (e.length !== Fp.BYTES) throw new Error(`sign: Expected ${Fp.BYTES} bytes of extra data`); if (e.length !== Fp.BYTES) throw new Error(`sign: Expected ${Fp.BYTES} bytes of extra data`);
seedArgs.push(e); seedArgs.push(e);
} }
// seed is constructed from private key and message // seed is constructed from private key and message
// Step D // Step D
// V, 0x00 are done in HmacDRBG constructor. // V, 0x00 are done in HmacDRBG constructor.
const seed = concatBytes(...seedArgs); const seed = ut.concatBytes(...seedArgs);
const m = bits2int(h1); const m = bits2int(h1);
return { seed, m, d }; return { seed, m, d };
} }
@@ -1172,9 +1192,10 @@ export function weierstrass(curveDef: CurveType): CurveFn {
// r = x mod n // r = x mod n
const r = mod.mod(q.x, n); const r = mod.mod(q.x, n);
if (r === _0n) return; if (r === _0n) return;
// s = (1/k * (m + dr) mod n // s = (m + dr)/k mod n where x/k == x*inv(k)
const s = mod.mod(kinv * mod.mod(m + mod.mod(d * r, n), n), n); const s = mod.mod(kinv * mod.mod(m + mod.mod(d * r, n), n), n);
if (s === _0n) return; if (s === _0n) return;
// recovery bit is usually 0 or 1; rarely it's 2 or 3, when q.x > n
let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n); let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n);
let normS = s; let normS = s;
if (lowS && isBiggerThanHalfOrder(s)) { if (lowS && isBiggerThanHalfOrder(s)) {
@@ -1184,11 +1205,19 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return new Signature(r, normS, recovery); return new Signature(r, normS, recovery);
} }
const defaultSigOpts: SignOpts = { lowS: CURVE.lowS };
/** /**
* Signs message hash (not message: you need to hash it by yourself). * Signs message hash (not message: you need to hash it by yourself).
* ```
* sign(m, d, k) where
* (x, y) = G × k
* r = x mod n
* s = (m + dr)/k mod n
* ```
* @param opts `lowS, extraEntropy` * @param opts `lowS, extraEntropy`
*/ */
function sign(msgHash: Hex, privKey: PrivKey, opts: SignOpts = { lowS: CURVE.lowS }): Signature { function sign(msgHash: Hex, privKey: PrivKey, opts = defaultSigOpts): Signature {
// Steps A, D of RFC6979 3.2. // Steps A, D of RFC6979 3.2.
const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy); const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy);
// Steps B, C, D, E, F, G // Steps B, C, D, E, F, G
@@ -1199,6 +1228,14 @@ export function weierstrass(curveDef: CurveType): CurveFn {
while (!(sig = kmdToSig(drbg.generateSync(), m, d, opts.lowS))) drbg.reseedSync(); while (!(sig = kmdToSig(drbg.generateSync(), m, d, opts.lowS))) drbg.reseedSync();
return sig; return sig;
} }
/**
* Signs a message (not message hash).
*/
function signUnhashed(msg: Uint8Array, privKey: PrivKey, opts = defaultSigOpts): Signature {
return sign(CURVE.hash(ut.ensureBytes(msg)), privKey, opts);
}
// Enable precomputes. Slows down first publicKey computation by 20ms. // Enable precomputes. Slows down first publicKey computation by 20ms.
Point.BASE._setWindowSize(8); Point.BASE._setWindowSize(8);
@@ -1234,7 +1271,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
signature = Signature.fromCompact(signature as Hex); signature = Signature.fromCompact(signature as Hex);
} }
} }
msgHash = ensureBytes(msgHash); msgHash = ut.ensureBytes(msgHash);
} catch (error) { } catch (error) {
return false; return false;
} }
@@ -1265,6 +1302,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
getPublicKey, getPublicKey,
getSharedSecret, getSharedSecret,
sign, sign,
signUnhashed,
verify, verify,
Point, Point,
ProjectivePoint, ProjectivePoint,

View File

@@ -1,4 +1,16 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// The pairing-friendly Barreto-Lynn-Scott elliptic curve construction allows to:
// - Construct zk-SNARKs at the 128-bit security
// - Use threshold signatures, which allows a user to sign lots of messages with one signature and verify them swiftly in a batch, using Boneh-Lynn-Shacham signature scheme.
// Differences from @noble/bls12-381 1.4:
// - PointG1 -> G1.Point
// - PointG2 -> G2.Point
// - PointG2.fromSignature -> Signature.decode
// - PointG2.toSignature -> Signature.encode
// - Fixed Fp2 ORDER
// - Points now have only two coordinates
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
import { randomBytes } from '@noble/hashes/utils'; import { randomBytes } from '@noble/hashes/utils';
import { bls, CurveFn } from './abstract/bls.js'; import { bls, CurveFn } from './abstract/bls.js';
@@ -23,14 +35,6 @@ import {
} from './abstract/weierstrass.js'; } from './abstract/weierstrass.js';
import { isogenyMap } from './abstract/hash-to-curve.js'; import { isogenyMap } from './abstract/hash-to-curve.js';
// Differences from bls12-381:
// - PointG1 -> G1.Point
// - PointG2 -> G2.Point
// - PointG2.fromSignature -> Signature.decode
// - PointG2.toSignature -> Signature.encode
// - Fixed Fp2 ORDER
// Points now have only two coordinates
// CURVE FIELDS // CURVE FIELDS
// Finite field over p. // Finite field over p.
const Fp = const Fp =
@@ -131,6 +135,7 @@ const Fp2: mod.Field<Fp2> & Fp2Utils = {
return { c0: Fp.mul(factor, Fp.create(a)), c1: Fp.mul(factor, Fp.create(-b)) }; return { c0: Fp.mul(factor, Fp.create(a)), c1: Fp.mul(factor, Fp.create(-b)) };
}, },
sqrt: (num) => { sqrt: (num) => {
if (Fp2.equals(num, Fp2.ZERO)) return Fp2.ZERO; // Algo doesn't handles this case
// TODO: Optimize this line. It's extremely slow. // TODO: Optimize this line. It's extremely slow.
// Speeding this up would boost aggregateSignatures. // Speeding this up would boost aggregateSignatures.
// https://eprint.iacr.org/2012/685.pdf applicable? // https://eprint.iacr.org/2012/685.pdf applicable?

View File

@@ -260,7 +260,7 @@ const invertSqrt = (number: bigint) => uvRatio(_1n, number);
const MAX_255B = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); const MAX_255B = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const bytes255ToNumberLE = (bytes: Uint8Array) => const bytes255ToNumberLE = (bytes: Uint8Array) =>
ed25519.utils.mod(bytesToNumberLE(bytes) & MAX_255B); ed25519.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_255B);
type ExtendedPoint = ExtendedPointType; type ExtendedPoint = ExtendedPointType;
@@ -269,7 +269,7 @@ type ExtendedPoint = ExtendedPointType;
function calcElligatorRistrettoMap(r0: bigint): ExtendedPoint { function calcElligatorRistrettoMap(r0: bigint): ExtendedPoint {
const { d } = ed25519.CURVE; const { d } = ed25519.CURVE;
const P = ed25519.CURVE.Fp.ORDER; const P = ed25519.CURVE.Fp.ORDER;
const { mod } = ed25519.utils; const mod = ed25519.CURVE.Fp.create;
const r = mod(SQRT_M1 * r0 * r0); // 1 const r = mod(SQRT_M1 * r0 * r0); // 1
const Ns = mod((r + _1n) * ONE_MINUS_D_SQ); // 2 const Ns = mod((r + _1n) * ONE_MINUS_D_SQ); // 2
let c = BigInt(-1); // 3 let c = BigInt(-1); // 3
@@ -327,7 +327,7 @@ export class RistrettoPoint {
hex = ensureBytes(hex, 32); hex = ensureBytes(hex, 32);
const { a, d } = ed25519.CURVE; const { a, d } = ed25519.CURVE;
const P = ed25519.CURVE.Fp.ORDER; const P = ed25519.CURVE.Fp.ORDER;
const { mod } = ed25519.utils; const mod = ed25519.CURVE.Fp.create;
const emsg = 'RistrettoPoint.fromHex: the hex is not valid encoding of RistrettoPoint'; const emsg = 'RistrettoPoint.fromHex: the hex is not valid encoding of RistrettoPoint';
const s = bytes255ToNumberLE(hex); const s = bytes255ToNumberLE(hex);
// 1. Check that s_bytes is the canonical encoding of a field element, or else abort. // 1. Check that s_bytes is the canonical encoding of a field element, or else abort.
@@ -357,7 +357,7 @@ export class RistrettoPoint {
toRawBytes(): Uint8Array { toRawBytes(): Uint8Array {
let { x, y, z, t } = this.ep; let { x, y, z, t } = this.ep;
const P = ed25519.CURVE.Fp.ORDER; const P = ed25519.CURVE.Fp.ORDER;
const { mod } = ed25519.utils; const mod = ed25519.CURVE.Fp.create;
const u1 = mod(mod(z + y) * mod(z - y)); // 1 const u1 = mod(mod(z + y) * mod(z - y)); // 1
const u2 = mod(x * y); // 2 const u2 = mod(x * y); // 2
// Square root always exists // Square root always exists
@@ -395,7 +395,7 @@ export class RistrettoPoint {
assertRstPoint(other); assertRstPoint(other);
const a = this.ep; const a = this.ep;
const b = other.ep; const b = other.ep;
const { mod } = ed25519.utils; const mod = ed25519.CURVE.Fp.create;
// (x1 * y2 == y1 * x2) | (y1 * y2 == x1 * x2) // (x1 * y2 == y1 * x2) | (y1 * y2 == x1 * x2)
const one = mod(a.x * b.y) === mod(a.y * b.x); const one = mod(a.x * b.y) === mod(a.y * b.x);
const two = mod(a.y * b.y) === mod(a.x * b.x); const two = mod(a.y * b.y) === mod(a.x * b.x);

View File

@@ -138,7 +138,8 @@ const ED448_DEF = {
), ),
// Finite field 𝔽p over which we'll do calculations; 2n ** 448n - 2n ** 224n - 1n // Finite field 𝔽p over which we'll do calculations; 2n ** 448n - 2n ** 224n - 1n
Fp, Fp,
// Subgroup order: how many points ed448 has; 2n**446n - 13818066809895115352007386748515426880336692474882178609894547503885n // Subgroup order: how many points curve has;
// 2n**446n - 13818066809895115352007386748515426880336692474882178609894547503885n
n: BigInt( n: BigInt(
'181709681073901722637330951972001133588410340171829515070372549795146003961539585716195755291692375963310293709091662304773755859649779' '181709681073901722637330951972001133588410340171829515070372549795146003961539585716195755291692375963310293709091662304773755859649779'
), ),

View File

@@ -1,5 +1,5 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { sha256 } from '@noble/hashes/sha256'; import { sha512 } from '@noble/hashes/sha512';
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
import { twistedEdwards } from './abstract/edwards.js'; import { twistedEdwards } from './abstract/edwards.js';
import { blake2s } from '@noble/hashes/blake2s'; import { blake2s } from '@noble/hashes/blake2s';
@@ -8,6 +8,7 @@ import { Fp } from './abstract/modular.js';
/** /**
* jubjub Twisted Edwards curve. * jubjub Twisted Edwards curve.
* https://neuromancer.sk/std/other/JubJub * https://neuromancer.sk/std/other/JubJub
* jubjub does not use EdDSA, so `hash`/sha512 params are passed because interface expects them.
*/ */
export const jubjub = twistedEdwards({ export const jubjub = twistedEdwards({
@@ -15,16 +16,16 @@ export const jubjub = twistedEdwards({
a: BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000'), a: BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000'),
d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'), d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'),
// Finite field 𝔽p over which we'll do calculations // Finite field 𝔽p over which we'll do calculations
// Same value as bls12-381 Fr (not Fp)
Fp: Fp(BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001')), Fp: Fp(BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001')),
// Subgroup order: how many points ed25519 has // Subgroup order: how many points curve has
// 2n ** 252n + 27742317777372353535851937790883648493n;
n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'), n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'),
// Cofactor // Cofactor
h: BigInt(8), h: BigInt(8),
// Base point (x, y) aka generator point // Base point (x, y) aka generator point
Gx: BigInt('0x11dafe5d23e1218086a365b99fbf3d3be72f6afd7d1f72623e6b071492d1122b'), Gx: BigInt('0x11dafe5d23e1218086a365b99fbf3d3be72f6afd7d1f72623e6b071492d1122b'),
Gy: BigInt('0x1d523cf1ddab1a1793132e78c866c0c33e26ba5cc220fed7cc3f870e59d292aa'), Gy: BigInt('0x1d523cf1ddab1a1793132e78c866c0c33e26ba5cc220fed7cc3f870e59d292aa'),
hash: sha256, hash: sha512,
randomBytes, randomBytes,
} as const); } as const);

View File

@@ -15,10 +15,8 @@ import { randomBytes } from '@noble/hashes/utils';
import { isogenyMap } from './abstract/hash-to-curve.js'; import { isogenyMap } from './abstract/hash-to-curve.js';
/** /**
* secp256k1 belongs to Koblitz curves: it has * secp256k1 belongs to Koblitz curves: it has efficiently computable endomorphism.
* efficiently computable Frobenius endomorphism. * Endomorphism uses 2x less RAM, speeds up precomputation by 2x and ECDH / key recovery by 20%.
* Endomorphism improves efficiency:
* Uses 2x less RAM, speeds up precomputation by 2x and ECDH / sign key recovery by 20%.
* Should always be used for Projective's double-and-add multiplication. * Should always be used for Projective's double-and-add multiplication.
* For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit. * For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit.
* https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 * https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066
@@ -129,7 +127,7 @@ export const secp256k1 = createCurve(
const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3'); const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3');
const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8'); const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8');
const b2 = a1; const b2 = a1;
const POW_2_128 = BigInt('0x100000000000000000000000000000000'); const POW_2_128 = BigInt('0x100000000000000000000000000000000'); // (2n**128n).toString(16)
const c1 = divNearest(b2 * k, n); const c1 = divNearest(b2 * k, n);
const c2 = divNearest(-b1 * k, n); const c2 = divNearest(-b1 * k, n);
@@ -175,20 +173,17 @@ function normalizePublicKey(publicKey: Hex | PointType<bigint>): PointType<bigin
} else { } else {
const bytes = ensureBytes(publicKey); const bytes = ensureBytes(publicKey);
// Schnorr is 32 bytes // Schnorr is 32 bytes
if (bytes.length === 32) { if (bytes.length !== 32) throw new Error('Schnorr pubkeys must be 32 bytes');
const x = bytesToNumberBE(bytes); const x = bytesToNumberBE(bytes);
if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); if (!isValidFieldElement(x)) throw new Error('Point is not on curve');
const y2 = secp256k1.utils._weierstrassEquation(x); // y² = x³ + ax + b const y2 = secp256k1.utils._weierstrassEquation(x); // y² = x³ + ax + b
let y = sqrtMod(y2); // y = y² ^ (p+1)/4 let y = sqrtMod(y2); // y = y² ^ (p+1)/4
const isYOdd = (y & _1n) === _1n; const isYOdd = (y & _1n) === _1n;
// Schnorr // Schnorr
if (isYOdd) y = secp256k1.CURVE.Fp.negate(y); if (isYOdd) y = secp256k1.CURVE.Fp.negate(y);
const point = new secp256k1.Point(x, y); const point = new secp256k1.Point(x, y);
point.assertValidity(); point.assertValidity();
return point; return point;
}
// Do we need that in schnorr at all?
return secp256k1.Point.fromHex(publicKey);
} }
} }
@@ -227,10 +222,13 @@ class SchnorrSignature {
} }
static fromHex(hex: Hex) { static fromHex(hex: Hex) {
const bytes = ensureBytes(hex); const bytes = ensureBytes(hex);
if (bytes.length !== 64) const len = 32; // group length
throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`); if (bytes.length !== 2 * len)
const r = bytesToNumberBE(bytes.subarray(0, 32)); throw new TypeError(
const s = bytesToNumberBE(bytes.subarray(32, 64)); `SchnorrSignature.fromHex: expected ${2 * len} bytes, not ${bytes.length}`
);
const r = bytesToNumberBE(bytes.subarray(0, len));
const s = bytesToNumberBE(bytes.subarray(len, 2 * len));
return new SchnorrSignature(r, s); return new SchnorrSignature(r, s);
} }
assertValidity() { assertValidity() {

View File

@@ -138,11 +138,12 @@ function hashKeyWithIndex(key: Uint8Array, index: number) {
export function grindKey(seed: Hex) { export function grindKey(seed: Hex) {
const _seed = ensureBytes0x(seed); const _seed = ensureBytes0x(seed);
const sha256mask = 2n ** 256n; const sha256mask = 2n ** 256n;
const limit = sha256mask - starkCurve.utils.mod(sha256mask, starkCurve.CURVE.n); const Fn = Fp(CURVE.n);
const limit = sha256mask - Fn.create(sha256mask);
for (let i = 0; ; i++) { for (let i = 0; ; i++) {
const key = hashKeyWithIndex(_seed, i); const key = hashKeyWithIndex(_seed, i);
// key should be in [0, limit) // key should be in [0, limit)
if (key < limit) return starkCurve.utils.mod(key, starkCurve.CURVE.n).toString(16); if (key < limit) return Fn.create(key).toString(16);
} }
} }

View File

@@ -1,7 +1,8 @@
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should'; import { should, describe } from 'micro-should';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import * as mod from '../lib/esm/abstract/modular.js'; import * as mod from '../lib/esm/abstract/modular.js';
import { bytesToHex as toHex } from '../lib/esm/abstract/utils.js';
// Generic tests for all curves in package // Generic tests for all curves in package
import { secp192r1 } from '../lib/esm/p192.js'; import { secp192r1 } from '../lib/esm/p192.js';
import { secp224r1 } from '../lib/esm/p224.js'; import { secp224r1 } from '../lib/esm/p224.js';
@@ -61,201 +62,235 @@ for (const c in FIELDS) {
const FC_BIGINT = curve[f][1] ? curve[f][1] : fc.bigInt(1n, Fp.ORDER - 1n); const FC_BIGINT = curve[f][1] ? curve[f][1] : fc.bigInt(1n, Fp.ORDER - 1n);
const create = curve[f][2] ? curve[f][2].bind(null, Fp) : (num) => Fp.create(num); const create = curve[f][2] ? curve[f][2].bind(null, Fp) : (num) => Fp.create(num);
should(`${name} equality`, () => { describe(name, () => {
fc.assert( should('equality', () => {
fc.property(FC_BIGINT, (num) => { fc.assert(
const a = create(num); fc.property(FC_BIGINT, (num) => {
const b = create(num); const a = create(num);
deepStrictEqual(Fp.equals(a, b), true); const b = create(num);
deepStrictEqual(Fp.equals(b, a), true); deepStrictEqual(Fp.equals(a, b), true);
}) deepStrictEqual(Fp.equals(b, a), true);
); })
}); );
should(`${name} non-equality`, () => { });
fc.assert( should('non-equality', () => {
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => { fc.assert(
const a = create(num1); fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
const b = create(num2); const a = create(num1);
deepStrictEqual(Fp.equals(a, b), num1 === num2); const b = create(num2);
deepStrictEqual(Fp.equals(b, a), num1 === num2); deepStrictEqual(Fp.equals(a, b), num1 === num2);
}) deepStrictEqual(Fp.equals(b, a), num1 === num2);
); })
}); );
should(`${name} add/subtract/commutativity`, () => { });
fc.assert( should('add/subtract/commutativity', () => {
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => { fc.assert(
const a = create(num1); fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
const b = create(num2); const a = create(num1);
deepStrictEqual(Fp.add(a, b), Fp.add(b, a)); const b = create(num2);
}) deepStrictEqual(Fp.add(a, b), Fp.add(b, a));
); })
}); );
should(`${name} add/subtract/associativity`, () => { });
fc.assert( should('add/subtract/associativity', () => {
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => { fc.assert(
const a = create(num1); fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
const b = create(num2); const a = create(num1);
const c = create(num3); const b = create(num2);
deepStrictEqual(Fp.add(a, Fp.add(b, c)), Fp.add(Fp.add(a, b), c)); const c = create(num3);
}) deepStrictEqual(Fp.add(a, Fp.add(b, c)), Fp.add(Fp.add(a, b), c));
); })
}); );
should(`${name} add/subtract/x+0=x`, () => { });
fc.assert( should('add/subtract/x+0=x', () => {
fc.property(FC_BIGINT, (num) => { fc.assert(
const a = create(num); fc.property(FC_BIGINT, (num) => {
deepStrictEqual(Fp.add(a, Fp.ZERO), a); const a = create(num);
}) deepStrictEqual(Fp.add(a, Fp.ZERO), a);
); })
}); );
should(`${name} add/subtract/x-0=x`, () => { });
fc.assert( should('add/subtract/x-0=x', () => {
fc.property(FC_BIGINT, (num) => { fc.assert(
const a = create(num); fc.property(FC_BIGINT, (num) => {
deepStrictEqual(Fp.sub(a, Fp.ZERO), a); const a = create(num);
deepStrictEqual(Fp.sub(a, a), Fp.ZERO); deepStrictEqual(Fp.sub(a, Fp.ZERO), a);
}) deepStrictEqual(Fp.sub(a, a), Fp.ZERO);
); })
}); );
should(`${name} add/subtract/negate equality`, () => { });
fc.assert( should('add/subtract/negate equality', () => {
fc.property(FC_BIGINT, (num1) => { fc.assert(
const a = create(num1); fc.property(FC_BIGINT, (num1) => {
const b = create(num1); const a = create(num1);
deepStrictEqual(Fp.sub(Fp.ZERO, a), Fp.negate(a)); const b = create(num1);
deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.negate(b))); deepStrictEqual(Fp.sub(Fp.ZERO, a), Fp.negate(a));
deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.mul(b, Fp.create(-1n)))); deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.negate(b)));
}) deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.mul(b, Fp.create(-1n))));
); })
}); );
should(`${name} add/subtract/negate`, () => { });
fc.assert( should('add/subtract/negate', () => {
fc.property(FC_BIGINT, (num) => { fc.assert(
const a = create(num); fc.property(FC_BIGINT, (num) => {
deepStrictEqual(Fp.negate(a), Fp.sub(Fp.ZERO, a)); const a = create(num);
deepStrictEqual(Fp.negate(a), Fp.mul(a, Fp.create(-1n))); deepStrictEqual(Fp.negate(a), Fp.sub(Fp.ZERO, a));
}) deepStrictEqual(Fp.negate(a), Fp.mul(a, Fp.create(-1n)));
); })
}); );
});
should('negate(0)', () => {
deepStrictEqual(Fp.negate(Fp.ZERO), Fp.ZERO);
});
should(`${name} multiply/commutativity`, () => { should('multiply/commutativity', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => { fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
const a = create(num1); const a = create(num1);
const b = create(num2); const b = create(num2);
deepStrictEqual(Fp.mul(a, b), Fp.mul(b, a)); deepStrictEqual(Fp.mul(a, b), Fp.mul(b, a));
}) })
); );
}); });
should(`${name} multiply/associativity`, () => { should('multiply/associativity', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => { fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
const a = create(num1); const a = create(num1);
const b = create(num2); const b = create(num2);
const c = create(num3); const c = create(num3);
deepStrictEqual(Fp.mul(a, Fp.mul(b, c)), Fp.mul(Fp.mul(a, b), c)); deepStrictEqual(Fp.mul(a, Fp.mul(b, c)), Fp.mul(Fp.mul(a, b), c));
}) })
); );
}); });
should(`${name} multiply/distributivity`, () => { should('multiply/distributivity', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => { fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
const a = create(num1); const a = create(num1);
const b = create(num2); const b = create(num2);
const c = create(num3); const c = create(num3);
deepStrictEqual(Fp.mul(a, Fp.add(b, c)), Fp.add(Fp.mul(b, a), Fp.mul(c, a))); deepStrictEqual(Fp.mul(a, Fp.add(b, c)), Fp.add(Fp.mul(b, a), Fp.mul(c, a)));
}) })
); );
}); });
should(`${name} multiply/add equality`, () => { should('multiply/add equality', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
deepStrictEqual(Fp.mul(a, 0n), Fp.ZERO); deepStrictEqual(Fp.mul(a, 0n), Fp.ZERO);
deepStrictEqual(Fp.mul(a, Fp.ZERO), Fp.ZERO); deepStrictEqual(Fp.mul(a, Fp.ZERO), Fp.ZERO);
deepStrictEqual(Fp.mul(a, 1n), a); deepStrictEqual(Fp.mul(a, 1n), a);
deepStrictEqual(Fp.mul(a, Fp.ONE), a); deepStrictEqual(Fp.mul(a, Fp.ONE), a);
deepStrictEqual(Fp.mul(a, 2n), Fp.add(a, a)); deepStrictEqual(Fp.mul(a, 2n), Fp.add(a, a));
deepStrictEqual(Fp.mul(a, 3n), Fp.add(Fp.add(a, a), a)); deepStrictEqual(Fp.mul(a, 3n), Fp.add(Fp.add(a, a), a));
deepStrictEqual(Fp.mul(a, 4n), Fp.add(Fp.add(Fp.add(a, a), a), a)); deepStrictEqual(Fp.mul(a, 4n), Fp.add(Fp.add(Fp.add(a, a), a), a));
}) })
); );
}); });
should(`${name} multiply/square equality`, () => { should('multiply/square equality', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
deepStrictEqual(Fp.square(a), Fp.mul(a, a)); deepStrictEqual(Fp.square(a), Fp.mul(a, a));
}) })
); );
}); });
should(`${name} multiply/pow equality`, () => { should('multiply/pow equality', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
deepStrictEqual(Fp.pow(a, 0n), Fp.ONE); deepStrictEqual(Fp.pow(a, 0n), Fp.ONE);
deepStrictEqual(Fp.pow(a, 1n), a); deepStrictEqual(Fp.pow(a, 1n), a);
deepStrictEqual(Fp.pow(a, 2n), Fp.mul(a, a)); deepStrictEqual(Fp.pow(a, 2n), Fp.mul(a, a));
deepStrictEqual(Fp.pow(a, 3n), Fp.mul(Fp.mul(a, a), a)); deepStrictEqual(Fp.pow(a, 3n), Fp.mul(Fp.mul(a, a), a));
}) })
); );
}); });
const isSquare = mod.FpIsSquare(Fp);
should(`${name} multiply/sqrt`, () => {
if (Fp === bls12_381.CURVE.Fp12) return; // Not implemented
fc.assert(
fc.property(FC_BIGINT, (num) => {
const a = create(num);
let root;
try {
root = Fp.sqrt(a);
} catch (e) {
deepStrictEqual(isSquare(a), false);
return;
}
deepStrictEqual(isSquare(a), true);
deepStrictEqual(Fp.equals(Fp.square(root), a), true, 'sqrt(a)^2 == a');
deepStrictEqual(Fp.equals(Fp.square(Fp.negate(root)), a), true, '(-sqrt(a))^2 == a');
})
);
});
should(`${name} div/division by one equality`, () => { should('square(0)', () => {
fc.assert( deepStrictEqual(Fp.square(Fp.ZERO), Fp.ZERO);
fc.property(FC_BIGINT, (num) => { deepStrictEqual(Fp.mul(Fp.ZERO, Fp.ZERO), Fp.ZERO);
const a = create(num); });
if (Fp.equals(a, Fp.ZERO)) return; // No division by zero
deepStrictEqual(Fp.div(a, Fp.ONE), a); should('square(1)', () => {
deepStrictEqual(Fp.div(a, a), Fp.ONE); deepStrictEqual(Fp.square(Fp.ONE), Fp.ONE);
}) deepStrictEqual(Fp.mul(Fp.ONE, Fp.ONE), Fp.ONE);
); });
});
should(`${name} zero division equality`, () => { should('square(-1)', () => {
fc.assert( const minus1 = Fp.negate(Fp.ONE);
fc.property(FC_BIGINT, (num) => { deepStrictEqual(Fp.square(minus1), Fp.ONE);
const a = create(num); deepStrictEqual(Fp.mul(minus1, minus1), Fp.ONE);
deepStrictEqual(Fp.div(Fp.ZERO, a), Fp.ZERO); });
})
); const isSquare = mod.FpIsSquare(Fp);
}); // Not implemented
should(`${name} div/division distributivity`, () => { if (Fp !== bls12_381.CURVE.Fp12) {
fc.assert( should('multiply/sqrt', () => {
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => { fc.assert(
const a = create(num1); fc.property(FC_BIGINT, (num) => {
const b = create(num2); const a = create(num);
const c = create(num3); let root;
deepStrictEqual(Fp.div(Fp.add(a, b), c), Fp.add(Fp.div(a, c), Fp.div(b, c))); try {
}) root = Fp.sqrt(a);
); } catch (e) {
}); deepStrictEqual(isSquare(a), false);
should(`${name} div/division and multiplication equality`, () => { return;
fc.assert( }
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => { deepStrictEqual(isSquare(a), true);
const a = create(num1); deepStrictEqual(Fp.equals(Fp.square(root), a), true, 'sqrt(a)^2 == a');
const b = create(num2); deepStrictEqual(Fp.equals(Fp.square(Fp.negate(root)), a), true, '(-sqrt(a))^2 == a');
deepStrictEqual(Fp.div(a, b), Fp.mul(a, Fp.invert(b))); })
}) );
); });
should('sqrt(0)', () => {
deepStrictEqual(Fp.sqrt(Fp.ZERO), Fp.ZERO);
const sqrt1 = Fp.sqrt(Fp.ONE);
deepStrictEqual(
Fp.equals(sqrt1, Fp.ONE) || Fp.equals(sqrt1, Fp.negate(Fp.ONE)),
true,
'sqrt(1) = 1 or -1'
);
});
}
should('div/division by one equality', () => {
fc.assert(
fc.property(FC_BIGINT, (num) => {
const a = create(num);
if (Fp.equals(a, Fp.ZERO)) return; // No division by zero
deepStrictEqual(Fp.div(a, Fp.ONE), a);
deepStrictEqual(Fp.div(a, a), Fp.ONE);
})
);
});
should('zero division equality', () => {
fc.assert(
fc.property(FC_BIGINT, (num) => {
const a = create(num);
deepStrictEqual(Fp.div(Fp.ZERO, a), Fp.ZERO);
})
);
});
should('div/division distributivity', () => {
fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
const a = create(num1);
const b = create(num2);
const c = create(num3);
deepStrictEqual(Fp.div(Fp.add(a, b), c), Fp.add(Fp.div(a, c), Fp.div(b, c)));
})
);
});
should('div/division and multiplication equality', () => {
fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
const a = create(num1);
const b = create(num2);
deepStrictEqual(Fp.div(a, b), Fp.mul(a, Fp.invert(b)));
})
);
});
}); });
} }
} }
@@ -278,12 +313,12 @@ const NUM_RUNS = 5;
const getXY = (p) => ({ x: p.x, y: p.y }); const getXY = (p) => ({ x: p.x, y: p.y });
function equal(a, b, comment) { function equal(a, b, comment) {
deepStrictEqual(a.equals(b), true, `eq(${comment})`); deepStrictEqual(a.equals(b), true, 'eq(${comment})');
if (a.toAffine && b.toAffine) { if (a.toAffine && b.toAffine) {
deepStrictEqual(getXY(a.toAffine()), getXY(b.toAffine()), `eqToAffine(${comment})`); deepStrictEqual(getXY(a.toAffine()), getXY(b.toAffine()), 'eqToAffine(${comment})');
} else if (!a.toAffine && !b.toAffine) { } else if (!a.toAffine && !b.toAffine) {
// Already affine // Already affine
deepStrictEqual(getXY(a), getXY(b), `eqAffine(${comment})`); deepStrictEqual(getXY(a), getXY(b), 'eqAffine(${comment})');
} else throw new Error('Different point types'); } else throw new Error('Different point types');
} }
@@ -308,253 +343,287 @@ for (const name in CURVES) {
const G = [p.ZERO, p.BASE]; const G = [p.ZERO, p.BASE];
for (let i = 2; i < 10; i++) G.push(G[1].multiply(i)); for (let i = 2; i < 10; i++) G.push(G[1].multiply(i));
// Here we check basic group laws, to verify that points works as group const title = `${name}/${pointName}`;
should(`${name}/${pointName}/Basic group laws (zero)`, () => { describe(title, () => {
equal(G[0].double(), G[0], '(0*G).double() = 0'); describe('basic group laws', () => {
equal(G[0].add(G[0]), G[0], '0*G + 0*G = 0'); // Here we check basic group laws, to verify that points works as group
equal(G[0].subtract(G[0]), G[0], '0*G - 0*G = 0'); should('(zero)', () => {
equal(G[0].negate(), G[0], '-0 = 0'); equal(G[0].double(), G[0], '(0*G).double() = 0');
for (let i = 0; i < G.length; i++) { equal(G[0].add(G[0]), G[0], '0*G + 0*G = 0');
const p = G[i]; equal(G[0].subtract(G[0]), G[0], '0*G - 0*G = 0');
equal(p, p.add(G[0]), `${i}*G + 0 = ${i}*G`); equal(G[0].negate(), G[0], '-0 = 0');
equal(G[0].multiply(i + 1), G[0], `${i + 1}*0 = 0`); for (let i = 0; i < G.length; i++) {
} const p = G[i];
}); equal(p, p.add(G[0]), '${i}*G + 0 = ${i}*G');
should(`${name}/${pointName}/Basic group laws (one)`, () => { equal(G[0].multiply(i + 1), G[0], '${i + 1}*0 = 0');
equal(G[1].double(), G[2], '(1*G).double() = 2*G'); }
equal(G[1].subtract(G[1]), G[0], '1*G - 1*G = 0'); });
equal(G[1].add(G[1]), G[2], '1*G + 1*G = 2*G'); should('(one)', () => {
}); equal(G[1].double(), G[2], '(1*G).double() = 2*G');
should(`${name}/${pointName}/Basic group laws (sanity tests)`, () => { equal(G[1].subtract(G[1]), G[0], '1*G - 1*G = 0');
equal(G[2].double(), G[4], `(2*G).double() = 4*G`); equal(G[1].add(G[1]), G[2], '1*G + 1*G = 2*G');
equal(G[2].add(G[2]), G[4], `2*G + 2*G = 4*G`); });
equal(G[7].add(G[3].negate()), G[4], `7*G - 3*G = 4*G`); should('(sanity tests)', () => {
}); equal(G[2].double(), G[4], '(2*G).double() = 4*G');
should(`${name}/${pointName}/Basic group laws (addition commutativity)`, () => { equal(G[2].add(G[2]), G[4], '2*G + 2*G = 4*G');
equal(G[4].add(G[3]), G[3].add(G[4]), `4*G + 3*G = 3*G + 4*G`); equal(G[7].add(G[3].negate()), G[4], '7*G - 3*G = 4*G');
equal(G[4].add(G[3]), G[3].add(G[2]).add(G[2]), `4*G + 3*G = 3*G + 2*G + 2*G`); });
}); should('(addition commutativity)', () => {
should(`${name}/${pointName}/Basic group laws (double)`, () => { equal(G[4].add(G[3]), G[3].add(G[4]), '4*G + 3*G = 3*G + 4*G');
equal(G[3].double(), G[6], '(3*G).double() = 6*G'); equal(G[4].add(G[3]), G[3].add(G[2]).add(G[2]), '4*G + 3*G = 3*G + 2*G + 2*G');
}); });
should(`${name}/${pointName}/Basic group laws (multiply)`, () => { should('(double)', () => {
equal(G[2].multiply(3), G[6], '(2*G).multiply(3) = 6*G'); equal(G[3].double(), G[6], '(3*G).double() = 6*G');
}); });
should(`${name}/${pointName}/Basic group laws (same point addition)`, () => { should('(multiply)', () => {
equal(G[3].add(G[3]), G[6], `3*G + 3*G = 6*G`); equal(G[2].multiply(3), G[6], '(2*G).multiply(3) = 6*G');
}); });
should(`${name}/${pointName}/Basic group laws (same point (negative) addition)`, () => { should('(same point addition)', () => {
equal(G[3].add(G[3].negate()), G[0], '3*G + (- 3*G) = 0*G'); equal(G[3].add(G[3]), G[6], '3*G + 3*G = 6*G');
equal(G[3].subtract(G[3]), G[0], '3*G - 3*G = 0*G'); });
}); should('(same point (negative) addition)', () => {
should(`${name}/${pointName}/Basic group laws (curve order)`, () => { equal(G[3].add(G[3].negate()), G[0], '3*G + (- 3*G) = 0*G');
equal(G[1].multiply(CURVE_ORDER - 1n).add(G[1]), G[0], '(N-1)*G + G = 0'); equal(G[3].subtract(G[3]), G[0], '3*G - 3*G = 0*G');
equal(G[1].multiply(CURVE_ORDER - 1n).add(G[2]), G[1], '(N-1)*G + 2*G = 1*G'); });
equal(G[1].multiply(CURVE_ORDER - 2n).add(G[2]), G[0], '(N-2)*G + 2*G = 0'); should('(curve order)', () => {
const half = CURVE_ORDER / 2n; equal(G[1].multiply(CURVE_ORDER - 1n).add(G[1]), G[0], '(N-1)*G + G = 0');
const carry = CURVE_ORDER % 2n === 1n ? G[1] : G[0]; equal(G[1].multiply(CURVE_ORDER - 1n).add(G[2]), G[1], '(N-1)*G + 2*G = 1*G');
equal(G[1].multiply(half).double().add(carry), G[0], '((N/2) * G).double() = 0'); equal(G[1].multiply(CURVE_ORDER - 2n).add(G[2]), G[0], '(N-2)*G + 2*G = 0');
}); const half = CURVE_ORDER / 2n;
should(`${name}/${pointName}/Basic group laws (inversion)`, () => { const carry = CURVE_ORDER % 2n === 1n ? G[1] : G[0];
const a = 1234n; equal(G[1].multiply(half).double().add(carry), G[0], '((N/2) * G).double() = 0');
const b = 5678n; });
const c = a * b; should('(inversion)', () => {
equal(G[1].multiply(a).multiply(b), G[1].multiply(c), 'a*b*G = c*G'); const a = 1234n;
const inv = mod.invert(b, CURVE_ORDER); const b = 5678n;
equal(G[1].multiply(c).multiply(inv), G[1].multiply(a), 'c*G * (1/b)*G = a*G'); const c = a * b;
}); equal(G[1].multiply(a).multiply(b), G[1].multiply(c), 'a*b*G = c*G');
should(`${name}/${pointName}/Basic group laws (multiply, rand)`, () => const inv = mod.invert(b, CURVE_ORDER);
fc.assert( equal(G[1].multiply(c).multiply(inv), G[1].multiply(a), 'c*G * (1/b)*G = a*G');
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { });
const c = mod.mod(a + b, CURVE_ORDER); should('(multiply, rand)', () =>
if (c === CURVE_ORDER || c < 1n) return; fc.assert(
const pA = G[1].multiply(a); fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
const pB = G[1].multiply(b); const c = mod.mod(a + b, CURVE_ORDER);
const pC = G[1].multiply(c); if (c === CURVE_ORDER || c < 1n) return;
equal(pA.add(pB), pB.add(pA), `pA + pB = pB + pA`); const pA = G[1].multiply(a);
equal(pA.add(pB), pC, `pA + pB = pC`); const pB = G[1].multiply(b);
}), const pC = G[1].multiply(c);
{ numRuns: NUM_RUNS } equal(pA.add(pB), pB.add(pA), 'pA + pB = pB + pA');
) equal(pA.add(pB), pC, 'pA + pB = pC');
); }),
should(`${name}/${pointName}/Basic group laws (multiply2, rand)`, () => { numRuns: NUM_RUNS }
fc.assert( )
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { );
const c = mod.mod(a * b, CURVE_ORDER); should('(multiply2, rand)', () =>
const pA = G[1].multiply(a); fc.assert(
const pB = G[1].multiply(b); fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
equal(pA.multiply(b), pB.multiply(a), `b*pA = a*pB`); const c = mod.mod(a * b, CURVE_ORDER);
equal(pA.multiply(b), G[1].multiply(c), `b*pA = c*G`); const pA = G[1].multiply(a);
}), const pB = G[1].multiply(b);
{ numRuns: NUM_RUNS } equal(pA.multiply(b), pB.multiply(a), 'b*pA = a*pB');
) equal(pA.multiply(b), G[1].multiply(c), 'b*pA = c*G');
); }),
{ numRuns: NUM_RUNS }
for (const op of ['add', 'subtract']) { )
should(`${name}/${pointName}/${op} type check`, () => {
throws(() => G[1][op](0), '0');
throws(() => G[1][op](0n), '0n');
G[1][op](G[2]);
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
throws(() => G[1][op](123.456), '123.456');
throws(() => G[1][op](true), 'true');
throws(() => G[1][op]('1'), "'1'");
throws(() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }');
throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
if (G[1].toAffine) throws(() => G[1][op](C.Point.BASE), `Point ${op} ${pointName}`);
throws(() => G[1][op](o.BASE), `${op}/other curve point`);
});
}
should(`${name}/${pointName}/equals type check`, () => {
throws(() => G[1].equals(0), '0');
throws(() => G[1].equals(0n), '0n');
deepStrictEqual(G[1].equals(G[2]), false, '1*G != 2*G');
deepStrictEqual(G[1].equals(G[1]), true, '1*G == 1*G');
deepStrictEqual(G[2].equals(G[2]), true, '2*G == 2*G');
throws(() => G[1].equals(CURVE_ORDER), 'CURVE_ORDER');
throws(() => G[1].equals(123.456), '123.456');
throws(() => G[1].equals(true), 'true');
throws(() => G[1].equals('1'), "'1'");
throws(() => G[1].equals({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }');
throws(() => G[1].equals(new Uint8Array([])), 'ui8a([])');
throws(() => G[1].equals(new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1].equals(new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
if (G[1].toAffine) throws(() => G[1].equals(C.Point.BASE), `Point.equals(${pointName})`);
throws(() => G[1].equals(o.BASE), 'other curve point');
});
for (const op of ['multiply', 'multiplyUnsafe']) {
if (!p.BASE[op]) continue;
should(`${name}/${pointName}/${op} type check`, () => {
if (op !== 'multiplyUnsafe') {
throws(() => G[1][op](0), '0');
throws(() => G[1][op](0n), '0n');
}
G[1][op](1n);
G[1][op](CURVE_ORDER - 1n);
throws(() => G[1][op](G[2]), 'G[2]');
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
throws(() => G[1][op](CURVE_ORDER + 1n), 'CURVE_ORDER+1');
throws(() => G[1][op](123.456), '123.456');
throws(() => G[1][op](true), 'true');
throws(() => G[1][op]('1'), '1');
throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
throws(() => G[1][op](o.BASE), 'other curve point');
});
}
// Complex point (Extended/Jacobian/Projective?)
if (p.BASE.toAffine) {
should(`${name}/${pointName}/toAffine()`, () => {
equal(p.ZERO.toAffine(), C.Point.ZERO, `0 = 0`);
equal(p.BASE.toAffine(), C.Point.BASE, `1 = 1`);
});
}
if (p.fromAffine) {
should(`${name}/${pointName}/fromAffine()`, () => {
equal(p.ZERO, p.fromAffine(C.Point.ZERO), `0 = 0`);
equal(p.BASE, p.fromAffine(C.Point.BASE), `1 = 1`);
});
}
// toHex/fromHex (if available)
if (p.fromHex && p.BASE.toHex) {
should(`${name}/${pointName}/fromHex(toHex()) roundtrip`, () => {
fc.assert(
fc.property(FC_BIGINT, (x) => {
const hex = p.BASE.multiply(x).toHex();
deepStrictEqual(p.fromHex(hex).toHex(), hex);
})
); );
}); });
}
}
// Generic complex things (getPublicKey/sign/verify/getSharedSecret)
should(`${name}/getPublicKey type check`, () => {
throws(() => C.getPublicKey(0), '0');
throws(() => C.getPublicKey(0n), '0n');
throws(() => C.getPublicKey(false), 'false');
throws(() => C.getPublicKey(123.456), '123.456');
throws(() => C.getPublicKey(true), 'true');
throws(() => C.getPublicKey(''), "''");
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
//throws(() => C.getPublicKey('1'), "'1'");
throws(() => C.getPublicKey('key'), "'key'");
throws(() => C.getPublicKey(new Uint8Array([])));
throws(() => C.getPublicKey(new Uint8Array([0])));
throws(() => C.getPublicKey(new Uint8Array([1])));
throws(() => C.getPublicKey(new Uint8Array(4096).fill(1)));
});
should(`${name}.verify()/should verify random signatures`, () =>
fc.assert(
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
const priv = C.utils.randomPrivateKey();
const pub = C.getPublicKey(priv);
const sig = C.sign(msg, priv);
deepStrictEqual(C.verify(sig, msg, pub), true);
}),
{ numRuns: NUM_RUNS }
)
);
should(`${name}.sign()/edge cases`, () => {
throws(() => C.sign());
throws(() => C.sign(''));
});
should(`${name}.verify()/should not verify signature with wrong hash`, () => { for (const op of ['add', 'subtract']) {
const MSG = '01'.repeat(32); describe(op, () => {
const PRIV_KEY = 0x2n; should('type check', () => {
const WRONG_MSG = '11'.repeat(32); throws(() => G[1][op](0), '0');
const signature = C.sign(MSG, PRIV_KEY); throws(() => G[1][op](0n), '0n');
const publicKey = C.getPublicKey(PRIV_KEY); G[1][op](G[2]);
deepStrictEqual(C.verify(signature, WRONG_MSG, publicKey), false); throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
}); throws(() => G[1][op](123.456), '123.456');
// NOTE: fails for ed, because of empty message. Since we convert it to scalar, throws(() => G[1][op](true), 'true');
// need to check what other implementations do. Empty message != new Uint8Array([0]), but what scalar should be in that case? throws(() => G[1][op]('1'), "'1'");
// should(`${name}/should not verify signature with wrong message`, () => { throws(
// fc.assert( () => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }),
// fc.property( '{ x: 1n, y: 1n, z: 1n, t: 1n }'
// fc.array(fc.integer({ min: 0x00, max: 0xff })), );
// fc.array(fc.integer({ min: 0x00, max: 0xff })), throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
// (bytes, wrongBytes) => { throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
// const privKey = C.utils.randomPrivateKey(); throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])');
// const message = new Uint8Array(bytes); throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
// const wrongMessage = new Uint8Array(wrongBytes); if (G[1].toAffine) throws(() => G[1][op](C.Point.BASE), 'Point ${op} ${pointName}');
// const publicKey = C.getPublicKey(privKey); throws(() => G[1][op](o.BASE), '${op}/other curve point');
// const signature = C.sign(message, privKey); });
// deepStrictEqual( });
// C.verify(signature, wrongMessage, publicKey), }
// bytes.toString() === wrongBytes.toString()
// );
// }
// ),
// { numRuns: NUM_RUNS }
// );
// });
if (C.getSharedSecret) { should('equals type check', () => {
should(`${name}/getSharedSecret() should be commutative`, () => { throws(() => G[1].equals(0), '0');
for (let i = 0; i < NUM_RUNS; i++) { throws(() => G[1].equals(0n), '0n');
const asec = C.utils.randomPrivateKey(); deepStrictEqual(G[1].equals(G[2]), false, '1*G != 2*G');
const apub = C.getPublicKey(asec); deepStrictEqual(G[1].equals(G[1]), true, '1*G == 1*G');
const bsec = C.utils.randomPrivateKey(); deepStrictEqual(G[2].equals(G[2]), true, '2*G == 2*G');
const bpub = C.getPublicKey(bsec); throws(() => G[1].equals(CURVE_ORDER), 'CURVE_ORDER');
try { throws(() => G[1].equals(123.456), '123.456');
deepStrictEqual(C.getSharedSecret(asec, bpub), C.getSharedSecret(bsec, apub)); throws(() => G[1].equals(true), 'true');
} catch (error) { throws(() => G[1].equals('1'), "'1'");
console.error('not commutative', { asec, apub, bsec, bpub }); throws(() => G[1].equals({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }');
throw error; throws(() => G[1].equals(new Uint8Array([])), 'ui8a([])');
} throws(() => G[1].equals(new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1].equals(new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
if (G[1].toAffine) throws(() => G[1].equals(C.Point.BASE), 'Point.equals(${pointName})');
throws(() => G[1].equals(o.BASE), 'other curve point');
});
for (const op of ['multiply', 'multiplyUnsafe']) {
if (!p.BASE[op]) continue;
describe(op, () => {
should('type check', () => {
if (op !== 'multiplyUnsafe') {
throws(() => G[1][op](0), '0');
throws(() => G[1][op](0n), '0n');
}
G[1][op](1n);
G[1][op](CURVE_ORDER - 1n);
throws(() => G[1][op](G[2]), 'G[2]');
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
throws(() => G[1][op](CURVE_ORDER + 1n), 'CURVE_ORDER+1');
throws(() => G[1][op](123.456), '123.456');
throws(() => G[1][op](true), 'true');
throws(() => G[1][op]('1'), '1');
throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
throws(() => G[1][op](o.BASE), 'other curve point');
});
});
}
// Complex point (Extended/Jacobian/Projective?)
if (p.BASE.toAffine) {
should('toAffine()', () => {
equal(p.ZERO.toAffine(), C.Point.ZERO, '0 = 0');
equal(p.BASE.toAffine(), C.Point.BASE, '1 = 1');
});
}
if (p.fromAffine) {
should('fromAffine()', () => {
equal(p.ZERO, p.fromAffine(C.Point.ZERO), '0 = 0');
equal(p.BASE, p.fromAffine(C.Point.BASE), '1 = 1');
});
}
// toHex/fromHex (if available)
if (p.fromHex && p.BASE.toHex) {
should('fromHex(toHex()) roundtrip', () => {
fc.assert(
fc.property(FC_BIGINT, (x) => {
const hex = p.BASE.multiply(x).toHex();
deepStrictEqual(p.fromHex(hex).toHex(), hex);
})
);
});
} }
}); });
} }
describe(name, () => {
// Generic complex things (getPublicKey/sign/verify/getSharedSecret)
should('getPublicKey type check', () => {
throws(() => C.getPublicKey(0), '0');
throws(() => C.getPublicKey(0n), '0n');
throws(() => C.getPublicKey(false), 'false');
throws(() => C.getPublicKey(123.456), '123.456');
throws(() => C.getPublicKey(true), 'true');
throws(() => C.getPublicKey(''), "''");
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
//throws(() => C.getPublicKey('1'), "'1'");
throws(() => C.getPublicKey('key'), "'key'");
throws(() => C.getPublicKey(new Uint8Array([])));
throws(() => C.getPublicKey(new Uint8Array([0])));
throws(() => C.getPublicKey(new Uint8Array([1])));
throws(() => C.getPublicKey(new Uint8Array(4096).fill(1)));
});
should('.verify() should verify random signatures', () =>
fc.assert(
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
const priv = C.utils.randomPrivateKey();
const pub = C.getPublicKey(priv);
const sig = C.sign(msg, priv);
deepStrictEqual(
C.verify(sig, msg, pub),
true,
'priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}'
);
}),
{ numRuns: NUM_RUNS }
)
);
should('.sign() edge cases', () => {
throws(() => C.sign());
throws(() => C.sign(''));
});
should('.verify() should not verify signature with wrong hash', () => {
const MSG = '01'.repeat(32);
const PRIV_KEY = 0x2n;
const WRONG_MSG = '11'.repeat(32);
const signature = C.sign(MSG, PRIV_KEY);
const publicKey = C.getPublicKey(PRIV_KEY);
deepStrictEqual(C.verify(signature, WRONG_MSG, publicKey), false);
});
// NOTE: fails for ed, because of empty message. Since we convert it to scalar,
// need to check what other implementations do. Empty message != new Uint8Array([0]), but what scalar should be in that case?
// should('should not verify signature with wrong message', () => {
// fc.assert(
// fc.property(
// fc.array(fc.integer({ min: 0x00, max: 0xff })),
// fc.array(fc.integer({ min: 0x00, max: 0xff })),
// (bytes, wrongBytes) => {
// const privKey = C.utils.randomPrivateKey();
// const message = new Uint8Array(bytes);
// const wrongMessage = new Uint8Array(wrongBytes);
// const publicKey = C.getPublicKey(privKey);
// const signature = C.sign(message, privKey);
// deepStrictEqual(
// C.verify(signature, wrongMessage, publicKey),
// bytes.toString() === wrongBytes.toString()
// );
// }
// ),
// { numRuns: NUM_RUNS }
// );
// });
if (C.getSharedSecret) {
should('getSharedSecret() should be commutative', () => {
for (let i = 0; i < NUM_RUNS; i++) {
const asec = C.utils.randomPrivateKey();
const apub = C.getPublicKey(asec);
const bsec = C.utils.randomPrivateKey();
const bpub = C.getPublicKey(bsec);
try {
deepStrictEqual(C.getSharedSecret(asec, bpub), C.getSharedSecret(bsec, apub));
} catch (error) {
console.error('not commutative', { asec, apub, bsec, bpub });
throw error;
}
}
});
}
});
} }
should('secp224k1 sqrt bug', () => {
const { Fp } = secp224r1.CURVE;
const sqrtMinus1 = Fp.sqrt(-1n);
// Verified against sage
deepStrictEqual(
sqrtMinus1,
23621584063597419797792593680131996961517196803742576047493035507225n
);
deepStrictEqual(
Fp.negate(sqrtMinus1),
3338362603553219996874421406887633712040719456283732096017030791656n
);
deepStrictEqual(Fp.square(sqrtMinus1), Fp.create(-1n));
});
// ESM is broken. // ESM is broken.
import url from 'url'; import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { deepStrictEqual } from 'assert'; import { deepStrictEqual } from 'assert';
import { should } from 'micro-should'; import { describe, should } from 'micro-should';
import { bytesToHex } from '@noble/hashes/utils'; import { bytesToHex } from '@noble/hashes/utils';
// Generic tests for all curves in package // Generic tests for all curves in package
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
@@ -51,43 +51,51 @@ import { default as ed448_ro } from './hash-to-curve/edwards448_XOF:SHAKE256_ELL
import { default as ed448_nu } from './hash-to-curve/edwards448_XOF:SHAKE256_ELL2_NU_.json' assert { type: 'json' }; import { default as ed448_nu } from './hash-to-curve/edwards448_XOF:SHAKE256_ELL2_NU_.json' assert { type: 'json' };
function testExpandXMD(hash, vectors) { function testExpandXMD(hash, vectors) {
for (let i = 0; i < vectors.tests.length; i++) { describe(`${vectors.hash}/${vectors.DST.length}`, () => {
const t = vectors.tests[i]; for (let i = 0; i < vectors.tests.length; i++) {
should(`expand_message_xmd/${vectors.hash}/${vectors.DST.length}/${i}`, () => { const t = vectors.tests[i];
const p = expand_message_xmd( should(`${vectors.hash}/${vectors.DST.length}/${i}`, () => {
stringToBytes(t.msg), const p = expand_message_xmd(
stringToBytes(vectors.DST), stringToBytes(t.msg),
t.len_in_bytes, stringToBytes(vectors.DST),
hash t.len_in_bytes,
); hash
deepStrictEqual(bytesToHex(p), t.uniform_bytes); );
}); deepStrictEqual(bytesToHex(p), t.uniform_bytes);
} });
}
});
} }
testExpandXMD(sha256, xmd_sha256_38); describe('expand_message_xmd', () => {
testExpandXMD(sha256, xmd_sha256_256); testExpandXMD(sha256, xmd_sha256_38);
testExpandXMD(sha512, xmd_sha512_38); testExpandXMD(sha256, xmd_sha256_256);
testExpandXMD(sha512, xmd_sha512_38);
});
function testExpandXOF(hash, vectors) { function testExpandXOF(hash, vectors) {
for (let i = 0; i < vectors.tests.length; i++) { describe(`${vectors.hash}/${vectors.DST.length}`, () => {
const t = vectors.tests[i]; for (let i = 0; i < vectors.tests.length; i++) {
should(`expand_message_xof/${vectors.hash}/${vectors.DST.length}/${i}`, () => { const t = vectors.tests[i];
const p = expand_message_xof( should(`${i}`, () => {
stringToBytes(t.msg), const p = expand_message_xof(
stringToBytes(vectors.DST), stringToBytes(t.msg),
+t.len_in_bytes, stringToBytes(vectors.DST),
vectors.k, +t.len_in_bytes,
hash vectors.k,
); hash
deepStrictEqual(bytesToHex(p), t.uniform_bytes); );
}); deepStrictEqual(bytesToHex(p), t.uniform_bytes);
} });
}
});
} }
testExpandXOF(shake128, xof_shake128_36); describe('expand_message_xof', () => {
testExpandXOF(shake128, xof_shake128_256); testExpandXOF(shake128, xof_shake128_36);
testExpandXOF(shake256, xof_shake256_36); testExpandXOF(shake128, xof_shake128_256);
testExpandXOF(shake256, xof_shake256_36);
});
function stringToFp(s) { function stringToFp(s) {
// bls-G2 support // bls-G2 support
@@ -99,26 +107,30 @@ function stringToFp(s) {
} }
function testCurve(curve, ro, nu) { function testCurve(curve, ro, nu) {
for (let i = 0; i < ro.vectors.length; i++) { describe(`${ro.curve}/${ro.ciphersuite}`, () => {
const t = ro.vectors[i]; for (let i = 0; i < ro.vectors.length; i++) {
should(`${ro.curve}/${ro.ciphersuite}(${i})`, () => { const t = ro.vectors[i];
const p = curve.Point.hashToCurve(stringToBytes(t.msg), { should(`(${i})`, () => {
DST: ro.dst, const p = curve.Point.hashToCurve(stringToBytes(t.msg), {
DST: ro.dst,
});
deepStrictEqual(p.x, stringToFp(t.P.x), 'Px');
deepStrictEqual(p.y, stringToFp(t.P.y), 'Py');
}); });
deepStrictEqual(p.x, stringToFp(t.P.x), 'Px'); }
deepStrictEqual(p.y, stringToFp(t.P.y), 'Py'); });
}); describe(`${nu.curve}/${nu.ciphersuite}`, () => {
} for (let i = 0; i < nu.vectors.length; i++) {
for (let i = 0; i < nu.vectors.length; i++) { const t = nu.vectors[i];
const t = nu.vectors[i]; should(`(${i})`, () => {
should(`${nu.curve}/${nu.ciphersuite}(${i})`, () => { const p = curve.Point.encodeToCurve(stringToBytes(t.msg), {
const p = curve.Point.encodeToCurve(stringToBytes(t.msg), { DST: nu.dst,
DST: nu.dst, });
deepStrictEqual(p.x, stringToFp(t.P.x), 'Px');
deepStrictEqual(p.y, stringToFp(t.P.y), 'Py');
}); });
deepStrictEqual(p.x, stringToFp(t.P.x), 'Px'); }
deepStrictEqual(p.y, stringToFp(t.P.y), 'Py'); });
});
}
} }
testCurve(secp256r1, p256_ro, p256_nu); testCurve(secp256r1, p256_ro, p256_nu);

View File

@@ -1,5 +1,5 @@
import { jubjub, findGroupHash } from '../lib/esm/jubjub.js'; import { jubjub, findGroupHash } from '../lib/esm/jubjub.js';
import { should } from 'micro-should'; import { describe, should } from 'micro-should';
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
@@ -18,53 +18,61 @@ const G_PROOF = new jubjub.ExtendedPoint(
const getXY = (p) => ({ x: p.x, y: p.y }); const getXY = (p) => ({ x: p.x, y: p.y });
should('toHex/fromHex', () => { describe('jubjub', () => {
// More than field should('toHex/fromHex', () => {
throws(() => // More than field
jubjub.Point.fromHex( throws(() =>
jubjub.Point.fromHex(
new Uint8Array([
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
])
)
);
// Multiplicative generator (sqrt == null), not on curve.
throws(() =>
jubjub.Point.fromHex(
new Uint8Array([
7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0,
])
)
);
const tmp = jubjub.Point.fromHex(
new Uint8Array([ new Uint8Array([
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
])
)
);
// Multiplicative generator (sqrt == null), not on curve.
throws(() =>
jubjub.Point.fromHex(
new Uint8Array([
7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0,
]) ])
) );
); deepStrictEqual(tmp.x, 0x8d51ccce760304d0ec030002760300000001000000000000n);
const tmp = jubjub.Point.fromHex( deepStrictEqual(tmp.y, 0n);
new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0,
])
);
deepStrictEqual(tmp.x, 0x8d51ccce760304d0ec030002760300000001000000000000n);
deepStrictEqual(tmp.y, 0n);
const S = G_SPEND.toAffine().toRawBytes(); const S = G_SPEND.toAffine().toRawBytes();
const S2 = G_SPEND.double().toAffine().toRawBytes(); const S2 = G_SPEND.double().toAffine().toRawBytes();
const P = G_PROOF.toAffine().toRawBytes(); const P = G_PROOF.toAffine().toRawBytes();
const P2 = G_PROOF.double().toAffine().toRawBytes(); const P2 = G_PROOF.double().toAffine().toRawBytes();
const S_exp = jubjub.Point.fromHex(S); const S_exp = jubjub.Point.fromHex(S);
const S2_exp = jubjub.Point.fromHex(S2); const S2_exp = jubjub.Point.fromHex(S2);
const P_exp = jubjub.Point.fromHex(P); const P_exp = jubjub.Point.fromHex(P);
const P2_exp = jubjub.Point.fromHex(P2); const P2_exp = jubjub.Point.fromHex(P2);
deepStrictEqual(getXY(G_SPEND.toAffine()), getXY(S_exp)); deepStrictEqual(getXY(G_SPEND.toAffine()), getXY(S_exp));
deepStrictEqual(getXY(G_SPEND.double().toAffine()), getXY(S2_exp)); deepStrictEqual(getXY(G_SPEND.double().toAffine()), getXY(S2_exp));
deepStrictEqual(getXY(G_PROOF.toAffine()), getXY(P_exp)); deepStrictEqual(getXY(G_PROOF.toAffine()), getXY(P_exp));
deepStrictEqual(getXY(G_PROOF.double().toAffine()), getXY(P2_exp)); deepStrictEqual(getXY(G_PROOF.double().toAffine()), getXY(P2_exp));
}); });
should('Find generators', () => { should('Find generators', () => {
const spend = findGroupHash(new Uint8Array(), new Uint8Array([90, 99, 97, 115, 104, 95, 71, 95])); const spend = findGroupHash(
const proof = findGroupHash(new Uint8Array(), new Uint8Array([90, 99, 97, 115, 104, 95, 72, 95])); new Uint8Array(),
deepStrictEqual(getXY(spend.toAffine()), getXY(G_SPEND.toAffine())); new Uint8Array([90, 99, 97, 115, 104, 95, 71, 95])
deepStrictEqual(getXY(proof.toAffine()), getXY(G_PROOF.toAffine())); );
const proof = findGroupHash(
new Uint8Array(),
new Uint8Array([90, 99, 97, 115, 104, 95, 72, 95])
);
deepStrictEqual(getXY(spend.toAffine()), getXY(G_SPEND.toAffine()));
deepStrictEqual(getXY(proof.toAffine()), getXY(G_PROOF.toAffine()));
});
}); });
// ESM is broken. // ESM is broken.

View File

@@ -1,5 +1,5 @@
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should'; import { describe, should } from 'micro-should';
import { secp192r1, P192 } from '../lib/esm/p192.js'; import { secp192r1, P192 } from '../lib/esm/p192.js';
import { secp224r1, P224 } from '../lib/esm/p224.js'; import { secp224r1, P224 } from '../lib/esm/p224.js';
import { secp256r1, P256 } from '../lib/esm/p256.js'; import { secp256r1, P256 } from '../lib/esm/p256.js';
@@ -344,19 +344,21 @@ function runWycheproof(name, CURVE, group, index) {
for (const name in WYCHEPROOF_ECDSA) { for (const name in WYCHEPROOF_ECDSA) {
const { curve, hashes } = WYCHEPROOF_ECDSA[name]; const { curve, hashes } = WYCHEPROOF_ECDSA[name];
for (const hName in hashes) { describe('Wycheproof/WYCHEPROOF_ECDSA', () => {
const { hash, tests } = hashes[hName]; for (const hName in hashes) {
const CURVE = curve.create(hash); const { hash, tests } = hashes[hName];
should(`Wycheproof/WYCHEPROOF_ECDSA ${name}/${hName}`, () => { const CURVE = curve.create(hash);
for (let i = 0; i < tests.length; i++) { should(`${name}/${hName}`, () => {
const groups = tests[i].testGroups; for (let i = 0; i < tests.length; i++) {
for (let j = 0; j < groups.length; j++) { const groups = tests[i].testGroups;
const group = groups[j]; for (let j = 0; j < groups.length; j++) {
runWycheproof(name, CURVE, group, `${i}/${j}`); const group = groups[j];
runWycheproof(name, CURVE, group, `${i}/${j}`);
}
} }
} });
}); }
} });
} }
const hexToBigint = (hex) => BigInt(`0x${hex}`); const hexToBigint = (hex) => BigInt(`0x${hex}`);

File diff suppressed because it is too large Load Diff