From 40530eae0c4b5c5023fce171a3daaa1408dbc60d Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Sat, 21 Jan 2023 18:02:45 +0000 Subject: [PATCH] hash-to-curve: decrease coupling, improve tree shaking support --- src/abstract/bls.ts | 83 +++++++++++++++++++++-------------- src/abstract/edwards.ts | 25 ----------- src/abstract/hash-to-curve.ts | 73 ++++++++++++++++++++++++------ src/abstract/weierstrass.ts | 34 ++------------ src/bls12-381.ts | 1 + src/ed25519.ts | 25 +++++++---- src/ed448.ts | 25 +++++++---- src/p256.ts | 25 +++++++---- src/p384.ts | 25 +++++++---- src/p521.ts | 19 +++++--- src/secp256k1.ts | 34 ++++++++------ test/bls12-381.test.js | 32 +++++++------- test/hash-to-curve.test.js | 20 ++++----- test/stark/stark.test.js | 2 +- 14 files changed, 237 insertions(+), 186 deletions(-) diff --git a/src/abstract/bls.ts b/src/abstract/bls.ts index e20c0d2..b078cf7 100644 --- a/src/abstract/bls.ts +++ b/src/abstract/bls.ts @@ -15,12 +15,7 @@ import * as mod from './modular.js'; import * as ut from './utils.js'; // Types require separate import import { Hex, PrivKey } from './utils.js'; -import { - htfOpts, - stringToBytes, - hash_to_field as hashToField, - expand_message_xmd as expandMessageXMD, -} from './hash-to-curve.js'; +import * as htf from './hash-to-curve.js'; import { CurvePointsType, PointType, CurvePointsRes, weierstrassPoints } from './weierstrass.js'; type Fp = bigint; // Can be different field? @@ -32,9 +27,14 @@ export type SignatureCoder = { export type CurveType = { r: bigint; - G1: Omit, 'n'>; + G1: Omit, 'n'> & { + mapToCurve: htf.MapToCurve; + htfDefaults: htf.Opts; + }; G2: Omit, 'n'> & { Signature: SignatureCoder; + mapToCurve: htf.MapToCurve; + htfDefaults: htf.Opts; }; x: bigint; Fp: mod.Field; @@ -51,7 +51,7 @@ export type CurveType = { conjugate(num: Fp12): Fp12; finalExponentiate(num: Fp12): Fp12; }; - htfDefaults: htfOpts; + htfDefaults: htf.Opts; hash: ut.CHash; // Because we need outputLen for DRBG randomBytes: (bytesLength?: number) => Uint8Array; }; @@ -68,6 +68,11 @@ export type CurveFn = { Signature: SignatureCoder; millerLoop: (ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]) => Fp12; calcPairingPrecomputes: (x: Fp2, y: Fp2) => [Fp2, Fp2, Fp2][]; + // prettier-ignore + hashToCurve: { + G1: ReturnType<(typeof htf.hashToCurve)>, + G2: ReturnType<(typeof htf.hashToCurve)>, + }, pairing: (P: PointType, Q: PointType, withFinalExponent?: boolean) => Fp12; getPublicKey: (privateKey: PrivKey) => Uint8Array; sign: { @@ -93,11 +98,9 @@ export type CurveFn = { publicKeys: (Hex | PointType)[] ) => boolean; utils: { - stringToBytes: typeof stringToBytes; - hashToField: typeof hashToField; - expandMessageXMD: typeof expandMessageXMD; - getDSTLabel: () => string; - setDSTLabel(newLabel: string): void; + stringToBytes: typeof htf.stringToBytes; + hashToField: typeof htf.hash_to_field; + expandMessageXMD: typeof htf.expand_message_xmd; }; }; @@ -174,26 +177,18 @@ export function bls( const utils = { hexToBytes: ut.hexToBytes, bytesToHex: ut.bytesToHex, - stringToBytes: stringToBytes, + stringToBytes: htf.stringToBytes, // TODO: do we need to export it here? hashToField: ( msg: Uint8Array, count: number, options: Partial = {} - ) => hashToField(msg, count, { ...CURVE.htfDefaults, ...options }), + ) => htf.hash_to_field(msg, count, { ...CURVE.htfDefaults, ...options }), expandMessageXMD: (msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H = CURVE.hash) => - expandMessageXMD(msg, DST, lenInBytes, H), + htf.expand_message_xmd(msg, DST, lenInBytes, H), hashToPrivateKey: (hash: Hex): Uint8Array => Fr.toBytes(ut.hashToPrivateScalar(hash, CURVE.r)), randomBytes: (bytesLength: number = groupLen): Uint8Array => CURVE.randomBytes(bytesLength), randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(groupLen + 8)), - getDSTLabel: () => CURVE.htfDefaults.DST, - setDSTLabel(newLabel: string) { - // 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) { - throw new TypeError('Invalid DST'); - } - CURVE.htfDefaults.DST = newLabel; - }, }; // Point on G1 curve: (x, y) @@ -201,6 +196,10 @@ export function bls( n: Fr.ORDER, ...CURVE.G1, }); + const G1HashToCurve = htf.hashToCurve(G1.Point, CURVE.G1.mapToCurve, { + ...CURVE.htfDefaults, + ...CURVE.G1.htfDefaults, + }); // Sparse multiplication against precomputed coefficients // TODO: replace with weakmap? @@ -227,6 +226,11 @@ export function bls( n: Fr.ORDER, ...CURVE.G2, }); + const G2HashToCurve = htf.hashToCurve(G2.Point, CURVE.G2.mapToCurve, { + ...CURVE.htfDefaults, + ...CURVE.G2.htfDefaults, + }); + const { Signature } = CURVE.G2; // Calculates bilinear pairing @@ -250,8 +254,8 @@ export function bls( function normP2(point: G2Hex): G2 { return point instanceof G2.Point ? point : Signature.decode(point); } - function normP2Hash(point: G2Hex): G2 { - return point instanceof G2.Point ? point : G2.Point.hashToCurve(point); + function normP2Hash(point: G2Hex, htfOpts?: htf.htfBasicOpts): G2 { + return point instanceof G2.Point ? point : (G2HashToCurve.hashToCurve(point, htfOpts) as G2); } // Multiplies generator by private key. @@ -262,10 +266,10 @@ export function bls( // Executes `hashToCurve` on the message and then multiplies the result by private key. // S = pk x H(m) - function sign(message: Hex, privateKey: PrivKey): Uint8Array; - function sign(message: G2, privateKey: PrivKey): G2; - function sign(message: G2Hex, privateKey: PrivKey): Uint8Array | G2 { - const msgPoint = normP2Hash(message); + function sign(message: Hex, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): Uint8Array; + function sign(message: G2, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): G2; + function sign(message: G2Hex, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): Uint8Array | G2 { + const msgPoint = normP2Hash(message, htfOpts); msgPoint.assertValidity(); const sigPoint = msgPoint.multiply(G1.normalizePrivateKey(privateKey)); if (message instanceof G2.Point) return sigPoint; @@ -274,9 +278,14 @@ export function bls( // Checks if pairing of public key & hash is equal to pairing of generator & signature. // e(P, H(m)) == e(G, S) - function verify(signature: G2Hex, message: G2Hex, publicKey: G1Hex): boolean { + function verify( + signature: G2Hex, + message: G2Hex, + publicKey: G1Hex, + htfOpts?: htf.htfBasicOpts + ): boolean { const P = normP1(publicKey); - const Hm = normP2Hash(message); + const Hm = normP2Hash(message, htfOpts); const G = G1.Point.BASE; const S = normP2(signature); // Instead of doing 2 exponentiations, we use property of billinear maps @@ -323,12 +332,17 @@ export function bls( // https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407 // e(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si)) - function verifyBatch(signature: G2Hex, messages: G2Hex[], publicKeys: G1Hex[]): boolean { + function verifyBatch( + signature: G2Hex, + messages: G2Hex[], + publicKeys: G1Hex[], + htfOpts?: htf.htfBasicOpts + ): boolean { if (!messages.length) throw new Error('Expected non-empty messages array'); if (publicKeys.length !== messages.length) throw new Error('Pubkey count should equal msg count'); const sig = normP2(signature); - const nMessages = messages.map(normP2Hash); + const nMessages = messages.map((i) => normP2Hash(i, htfOpts)); const nPublicKeys = publicKeys.map(normP1); try { const paired = []; @@ -365,6 +379,7 @@ export function bls( Signature, millerLoop, calcPairingPrecomputes, + hashToCurve: { G1: G1HashToCurve, G2: G2HashToCurve }, pairing, getPublicKey, sign, diff --git a/src/abstract/edwards.ts b/src/abstract/edwards.ts index d71fcdc..0289b71 100644 --- a/src/abstract/edwards.ts +++ b/src/abstract/edwards.ts @@ -13,7 +13,6 @@ import * as mod from './modular.js'; import * as ut from './utils.js'; import { ensureBytes, Hex, PrivKey } from './utils.js'; import { Group, GroupConstructor, wNAF } from './group.js'; -import { hash_to_field as hashToField, htfOpts, validateHTFOpts } from './hash-to-curve.js'; // Be friendly to bad ECMAScript parsers by not using bigint literals like 123n const _0n = BigInt(0); @@ -39,8 +38,6 @@ export type CurveType = ut.BasicCurve & { uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; // RFC 8032 pre-hashing of messages to sign() / verify() preHash?: ut.CHash; - // Hash to field options - htfDefaults?: htfOpts; mapToCurve?: (scalar: bigint[]) => { x: bigint; y: bigint }; }; @@ -59,7 +56,6 @@ function validateOpts(curve: CurveType) { if (opts[fn] === undefined) continue; // Optional if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`); } - if (opts.htfDefaults !== undefined) validateHTFOpts(opts.htfDefaults); // Set defaults return Object.freeze({ ...opts } as const); } @@ -114,8 +110,6 @@ export interface PointConstructor extends GroupConstructor { new (x: bigint, y: bigint): PointType; fromHex(hex: Hex): PointType; fromPrivateKey(privateKey: PrivKey): PointType; - hashToCurve(msg: Hex, options?: Partial): PointType; - encodeToCurve(msg: Hex, options?: Partial): PointType; } export type PubKey = Hex | PointType; @@ -484,25 +478,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { clearCofactor() { return ExtendedPoint.fromAffine(this).clearCofactor().toAffine(); } - // Encodes byte string to elliptic curve - // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3 - static hashToCurve(msg: Hex, options?: Partial) { - const { mapToCurve, htfDefaults } = CURVE; - if (!mapToCurve) throw new Error('No mapToCurve defined for curve'); - const u = hashToField(ensureBytes(msg), 2, { ...htfDefaults, ...options } as htfOpts); - const { x: x0, y: y0 } = mapToCurve(u[0]); - const { x: x1, y: y1 } = mapToCurve(u[1]); - const p = new Point(x0, y0).add(new Point(x1, y1)).clearCofactor(); - return p; - } - // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3 - static encodeToCurve(msg: Hex, options?: Partial) { - const { mapToCurve, htfDefaults } = CURVE; - if (!mapToCurve) throw new Error('No mapToCurve defined for curve'); - const u = hashToField(ensureBytes(msg), 1, { ...htfDefaults, ...options } as htfOpts); - const { x, y } = mapToCurve(u[0]); - return new Point(x, y).clearCofactor(); - } } /** diff --git a/src/abstract/hash-to-curve.ts b/src/abstract/hash-to-curve.ts index 95685ea..c454f0f 100644 --- a/src/abstract/hash-to-curve.ts +++ b/src/abstract/hash-to-curve.ts @@ -1,11 +1,13 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -import { CHash, concatBytes } from './utils.js'; +import * as ut from './utils.js'; import * as mod from './modular.js'; +import type { Group, GroupConstructor } from './group.js'; -export type htfOpts = { +export type Opts = { // DST: a domain separation tag // defined in section 2.2.5 DST: string; + encodeDST: string; // p: the characteristic of F // where F is a finite field of characteristic p and order q = p^m p: bigint; @@ -22,10 +24,10 @@ export type htfOpts = { // wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others. // BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247 // TODO: verify that hash is shake if expand==='xof' via types - hash: CHash; + hash: ut.CHash; }; -export function validateHTFOpts(opts: htfOpts) { +export function validateOpts(opts: Opts) { if (typeof opts.DST !== 'string') throw new Error('Invalid htf/DST'); if (typeof opts.p !== 'bigint') throw new Error('Invalid htf/p'); if (typeof opts.m !== 'number') throw new Error('Invalid htf/m'); @@ -81,25 +83,25 @@ export function expand_message_xmd( msg: Uint8Array, DST: Uint8Array, lenInBytes: number, - H: CHash + H: ut.CHash ): Uint8Array { // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 - if (DST.length > 255) DST = H(concatBytes(stringToBytes('H2C-OVERSIZE-DST-'), DST)); + if (DST.length > 255) DST = H(ut.concatBytes(stringToBytes('H2C-OVERSIZE-DST-'), DST)); const b_in_bytes = H.outputLen; const r_in_bytes = H.blockLen; const ell = Math.ceil(lenInBytes / b_in_bytes); if (ell > 255) throw new Error('Invalid xmd length'); - const DST_prime = concatBytes(DST, i2osp(DST.length, 1)); + const DST_prime = ut.concatBytes(DST, i2osp(DST.length, 1)); const Z_pad = i2osp(0, r_in_bytes); const l_i_b_str = i2osp(lenInBytes, 2); const b = new Array(ell); - const b_0 = H(concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime)); - b[0] = H(concatBytes(b_0, i2osp(1, 1), DST_prime)); + const b_0 = H(ut.concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime)); + b[0] = H(ut.concatBytes(b_0, i2osp(1, 1), DST_prime)); for (let i = 1; i <= ell; i++) { const args = [strxor(b_0, b[i - 1]), i2osp(i + 1, 1), DST_prime]; - b[i] = H(concatBytes(...args)); + b[i] = H(ut.concatBytes(...args)); } - const pseudo_random_bytes = concatBytes(...b); + const pseudo_random_bytes = ut.concatBytes(...b); return pseudo_random_bytes.slice(0, lenInBytes); } @@ -108,7 +110,7 @@ export function expand_message_xof( DST: Uint8Array, lenInBytes: number, k: number, - H: CHash + H: ut.CHash ): Uint8Array { // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 // DST = H('H2C-OVERSIZE-DST-' || a_very_long_DST, Math.ceil((lenInBytes * k) / 8)); @@ -137,7 +139,7 @@ export function expand_message_xof( * @param options `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}` * @returns [u_0, ..., u_(count - 1)], a list of field elements. */ -export function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][] { +export function hash_to_field(msg: Uint8Array, count: number, options: Opts): bigint[][] { // if options is provided but incomplete, fill any missing fields with the // value in hftDefaults (ie hash to G2). const log2p = options.p.toString(2).length; @@ -175,3 +177,48 @@ export function isogenyMap>(field: F, map: [T[], T[], return { x, y }; }; } + +export interface Point extends Group> { + readonly x: T; + readonly y: T; + clearCofactor(): Point; +} + +export interface PointConstructor extends GroupConstructor> { + new (x: T, y: T): Point; +} + +export type MapToCurve = (scalar: bigint[]) => { x: T; y: T }; + +// Separated from initialization opts, so users won't accidentally change per-curve parameters (changing DST is ok!) +export type htfBasicOpts = { + DST: string; +}; + +export function hashToCurve(Point: PointConstructor, mapToCurve: MapToCurve, def: Opts) { + validateOpts(def); + if (typeof mapToCurve !== 'function') + throw new Error('hashToCurve: mapToCurve() has not been defined'); + + return { + // Encodes byte string to elliptic curve + // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3 + hashToCurve(msg: ut.Hex, options?: htfBasicOpts) { + if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined'); + msg = ut.ensureBytes(msg); + const u = hash_to_field(msg, 2, { ...def, DST: def.DST, ...options } as Opts); + const { x: x0, y: y0 } = mapToCurve(u[0]); + const { x: x1, y: y1 } = mapToCurve(u[1]); + return new Point(x0, y0).add(new Point(x1, y1)).clearCofactor(); + }, + + // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3 + encodeToCurve(msg: ut.Hex, options?: htfBasicOpts) { + if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined'); + msg = ut.ensureBytes(msg); + const u = hash_to_field(msg, 1, { ...def, DST: def.encodeDST, ...options } as Opts); + const { x, y } = mapToCurve(u[0]); + return new Point(x, y).clearCofactor(); + }, + }; +} diff --git a/src/abstract/weierstrass.ts b/src/abstract/weierstrass.ts index bdda6f1..7263d8f 100644 --- a/src/abstract/weierstrass.ts +++ b/src/abstract/weierstrass.ts @@ -11,7 +11,6 @@ import * as mod from './modular.js'; import * as ut from './utils.js'; import { bytesToHex, Hex, PrivKey } from './utils.js'; -import { hash_to_field, htfOpts, validateHTFOpts } from './hash-to-curve.js'; import { Group, GroupConstructor, wNAF } from './group.js'; type HmacFnSync = (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array; @@ -39,9 +38,6 @@ export type BasicCurve = ut.BasicCurve & { c: ProjectiveConstructor, point: ProjectivePointType ) => ProjectivePointType; - // Hash to field options - htfDefaults?: htfOpts; - mapToCurve?: (scalar: bigint[]) => { x: T; y: T }; }; // ASN.1 DER encoding utilities @@ -121,6 +117,7 @@ export interface ProjectivePointType extends Group> { multiply(scalar: number | bigint, affinePoint?: PointType): ProjectivePointType; multiplyUnsafe(scalar: bigint): ProjectivePointType; toAffine(invZ?: T): PointType; + clearCofactor(): ProjectivePointType; } // Static methods for 3d XYZ points export interface ProjectiveConstructor extends GroupConstructor> { @@ -139,14 +136,13 @@ export interface PointType extends Group> { toHex(isCompressed?: boolean): string; assertValidity(): void; multiplyAndAddUnsafe(Q: PointType, a: bigint, b: bigint): PointType | undefined; + clearCofactor(): PointType; } // Static methods for 2d XY points export interface PointConstructor extends GroupConstructor> { new (x: T, y: T): PointType; fromHex(hex: Hex): PointType; fromPrivateKey(privateKey: PrivKey): PointType; - hashToCurve(msg: Hex, options?: Partial): PointType; - encodeToCurve(msg: Hex, options?: Partial): PointType; } export type CurvePointsType = BasicCurve & { @@ -162,7 +158,7 @@ function validatePointOpts(curve: CurvePointsType) { if (!Fp.isValid(curve[i])) throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`); } - for (const i of ['isTorsionFree', 'clearCofactor', 'mapToCurve'] as const) { + for (const i of ['isTorsionFree', 'clearCofactor'] as const) { if (curve[i] === undefined) continue; // Optional if (typeof curve[i] !== 'function') throw new Error(`Invalid ${i} function`); } @@ -181,8 +177,6 @@ function validatePointOpts(curve: CurvePointsType) { } if (typeof opts.fromBytes !== 'function') throw new Error('Invalid fromBytes function'); if (typeof opts.toBytes !== 'function') throw new Error('Invalid fromBytes function'); - // Requires including hashToCurve file - if (opts.htfDefaults !== undefined) validateHTFOpts(opts.htfDefaults); // Set defaults return Object.freeze({ ...opts } as const); } @@ -671,28 +665,6 @@ export function weierstrassPoints(opts: CurvePointsType) { const sum = aP.add(bQ); return sum.equals(ProjectivePoint.ZERO) ? undefined : sum.toAffine(); } - - // Encodes byte string to elliptic curve - // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3 - static hashToCurve(msg: Hex, options?: Partial) { - const { mapToCurve } = CURVE; - if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined'); - msg = ut.ensureBytes(msg); - const u = hash_to_field(msg, 2, { ...CURVE.htfDefaults, ...options } as htfOpts); - const { x: x0, y: y0 } = mapToCurve(u[0]); - const { x: x1, y: y1 } = mapToCurve(u[1]); - return new Point(x0, y0).add(new Point(x1, y1)).clearCofactor(); - } - - // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3 - static encodeToCurve(msg: Hex, options?: Partial) { - const { mapToCurve } = CURVE; - if (!mapToCurve) throw new Error('CURVE.mapToCurve() has not been defined'); - msg = ut.ensureBytes(msg); - const u = hash_to_field(msg, 1, { ...CURVE.htfDefaults, ...options } as htfOpts); - const { x, y } = mapToCurve(u[0]); - return new Point(x, y).clearCofactor(); - } } return { diff --git a/src/bls12-381.ts b/src/bls12-381.ts index bdc4e09..c6a8181 100644 --- a/src/bls12-381.ts +++ b/src/bls12-381.ts @@ -920,6 +920,7 @@ const htfDefaults = { // defined in section 2.2.5 // Use utils.getDSTLabel(), utils.setDSTLabel(value) DST: 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_', + encodeDST: 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_', // p: the characteristic of F // where F is a finite field of characteristic p and order q = p^m p: Fp.ORDER, diff --git a/src/ed25519.ts b/src/ed25519.ts index 26cd6b2..ebf8cd7 100644 --- a/src/ed25519.ts +++ b/src/ed25519.ts @@ -12,6 +12,7 @@ import { numberToBytesLE, Hex, } from './abstract/utils.js'; +import * as htf from './abstract/hash-to-curve.js'; /** * ed25519 Twisted Edwards curve with following addons: @@ -189,15 +190,6 @@ const ED25519_DEF = { // Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3. // Constant-time, u/√v uvRatio, - htfDefaults: { - DST: 'edwards25519_XMD:SHA-512_ELL2_RO_', - p: Fp.ORDER, - m: 1, - k: 128, - expand: 'xmd', - hash: sha512, - }, - mapToCurve: (scalars: bigint[]) => map_to_curve_elligator2_edwards25519(scalars[0]), } as const; export const ed25519 = twistedEdwards(ED25519_DEF); @@ -217,6 +209,21 @@ export const ed25519ph = twistedEdwards({ preHash: sha512, }); +const { hashToCurve, encodeToCurve } = htf.hashToCurve( + ed25519.Point, + (scalars: bigint[]) => map_to_curve_elligator2_edwards25519(scalars[0]), + { + DST: 'edwards25519_XMD:SHA-512_ELL2_RO_', + encodeDST: 'edwards25519_XMD:SHA-512_ELL2_NU_', + p: Fp.ORDER, + m: 1, + k: 128, + expand: 'xmd', + hash: sha512, + } +); +export { hashToCurve, encodeToCurve }; + export const x25519 = montgomery({ P: ED25519_P, a24: BigInt('121665'), diff --git a/src/ed448.ts b/src/ed448.ts index 122c755..20e8ad2 100644 --- a/src/ed448.ts +++ b/src/ed448.ts @@ -4,6 +4,7 @@ import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/h import { twistedEdwards } from './abstract/edwards.js'; import { mod, pow2, Fp as Field } from './abstract/modular.js'; import { montgomery } from './abstract/montgomery.js'; +import * as htf from './abstract/hash-to-curve.js'; /** * Edwards448 (not Ed448-Goldilocks) curve with following addons: @@ -189,21 +190,27 @@ const ED448_DEF = { // square root exists, and the decoding fails. return { isValid: mod(x2 * v, P) === u, value: x }; }, - htfDefaults: { - DST: 'edwards448_XOF:SHAKE256_ELL2_RO_', - p: Fp.ORDER, - m: 1, - k: 224, - expand: 'xof', - hash: shake256, - }, - mapToCurve: (scalars: bigint[]) => map_to_curve_elligator2_edwards448(scalars[0]), } as const; export const ed448 = twistedEdwards(ED448_DEF); // NOTE: there is no ed448ctx, since ed448 supports ctx by default export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 }); +const { hashToCurve, encodeToCurve } = htf.hashToCurve( + ed448.Point, + (scalars: bigint[]) => map_to_curve_elligator2_edwards448(scalars[0]), + { + DST: 'edwards448_XOF:SHAKE256_ELL2_RO_', + encodeDST: 'edwards448_XOF:SHAKE256_ELL2_NU_', + p: Fp.ORDER, + m: 1, + k: 224, + expand: 'xof', + hash: shake256, + } +); +export { hashToCurve, encodeToCurve }; + export const x448 = montgomery({ a24: BigInt(39081), montgomeryBits: 448, diff --git a/src/p256.ts b/src/p256.ts index eb176f6..09e7842 100644 --- a/src/p256.ts +++ b/src/p256.ts @@ -3,6 +3,7 @@ import { createCurve } from './_shortw_utils.js'; import { sha256 } from '@noble/hashes/sha256'; import { Fp as Field } from './abstract/modular.js'; import { mapToCurveSimpleSWU } from './abstract/weierstrass.js'; +import * as htf from './abstract/hash-to-curve.js'; // NIST secp256r1 aka P256 // https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-256 @@ -31,16 +32,22 @@ export const P256 = createCurve( Gy: BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'), h: BigInt(1), lowS: false, - mapToCurve: (scalars: bigint[]) => mapSWU(scalars[0]), - htfDefaults: { - DST: 'P256_XMD:SHA-256_SSWU_RO_', - p: Fp.ORDER, - m: 1, - k: 128, - expand: 'xmd', - hash: sha256, - }, } as const, sha256 ); export const secp256r1 = P256; + +const { hashToCurve, encodeToCurve } = htf.hashToCurve( + secp256r1.Point, + (scalars: bigint[]) => mapSWU(scalars[0]), + { + DST: 'P256_XMD:SHA-256_SSWU_RO_', + encodeDST: 'P256_XMD:SHA-256_SSWU_NU_', + p: Fp.ORDER, + m: 1, + k: 128, + expand: 'xmd', + hash: sha256, + } +); +export { hashToCurve, encodeToCurve }; diff --git a/src/p384.ts b/src/p384.ts index 0cf3ae3..6286711 100644 --- a/src/p384.ts +++ b/src/p384.ts @@ -3,6 +3,7 @@ import { createCurve } from './_shortw_utils.js'; import { sha384 } from '@noble/hashes/sha512'; import { Fp as Field } from './abstract/modular.js'; import { mapToCurveSimpleSWU } from './abstract/weierstrass.js'; +import * as htf from './abstract/hash-to-curve.js'; // NIST secp384r1 aka P384 // https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-384 @@ -35,16 +36,22 @@ export const P384 = createCurve({ Gy: BigInt('0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f'), h: BigInt(1), lowS: false, - mapToCurve: (scalars: bigint[]) => mapSWU(scalars[0]), - htfDefaults: { - DST: 'P384_XMD:SHA-384_SSWU_RO_', - p: Fp.ORDER, - m: 1, - k: 192, - expand: 'xmd', - hash: sha384, - }, } as const, sha384 ); export const secp384r1 = P384; + +const { hashToCurve, encodeToCurve } = htf.hashToCurve( + secp384r1.Point, + (scalars: bigint[]) => mapSWU(scalars[0]), + { + DST: 'P384_XMD:SHA-384_SSWU_RO_', + encodeDST: 'P384_XMD:SHA-384_SSWU_NU_', + p: Fp.ORDER, + m: 1, + k: 192, + expand: 'xmd', + hash: sha384, + } +); +export { hashToCurve, encodeToCurve }; diff --git a/src/p521.ts b/src/p521.ts index 042f2f7..f897a80 100644 --- a/src/p521.ts +++ b/src/p521.ts @@ -4,6 +4,7 @@ import { sha512 } from '@noble/hashes/sha512'; import { bytesToHex, PrivKey } from './abstract/utils.js'; import { Fp as Field } from './abstract/modular.js'; import { mapToCurveSimpleSWU } from './abstract/weierstrass.js'; +import * as htf from './abstract/hash-to-curve.js'; // NIST secp521r1 aka P521 // Note that it's 521, which differs from 512 of its hash function. @@ -48,14 +49,20 @@ export const P521 = createCurve({ } return key.padStart(66 * 2, '0'); }, - mapToCurve: (scalars: bigint[]) => mapSWU(scalars[0]), - htfDefaults: { +} as const, sha512); +export const secp521r1 = P521; + +const { hashToCurve, encodeToCurve } = htf.hashToCurve( + secp521r1.Point, + (scalars: bigint[]) => mapSWU(scalars[0]), + { DST: 'P521_XMD:SHA-512_SSWU_RO_', + encodeDST: 'P521_XMD:SHA-512_SSWU_NU_', p: Fp.ORDER, m: 1, k: 256, - expand: 'xmd', + expand: 'xmd', hash: sha512, - }, -} as const, sha512); -export const secp521r1 = P521; + } +); +export { hashToCurve, encodeToCurve }; diff --git a/src/secp256k1.ts b/src/secp256k1.ts index 7edeb8f..a28e153 100644 --- a/src/secp256k1.ts +++ b/src/secp256k1.ts @@ -12,7 +12,7 @@ import { PrivKey, } from './abstract/utils.js'; import { randomBytes } from '@noble/hashes/utils'; -import { isogenyMap } from './abstract/hash-to-curve.js'; +import * as htf from './abstract/hash-to-curve.js'; /** * secp256k1 belongs to Koblitz curves: it has efficiently computable endomorphism. @@ -62,7 +62,7 @@ function sqrtMod(y: bigint): bigint { const Fp = Field(secp256k1P, undefined, undefined, { sqrt: sqrtMod }); type Fp = bigint; -const isoMap = isogenyMap( +const isoMap = htf.isogenyMap( Fp, [ // xNum @@ -143,22 +143,28 @@ export const secp256k1 = createCurve( return { k1neg, k1, k2neg, k2 }; }, }, - mapToCurve: (scalars: bigint[]) => { - const { x, y } = mapSWU(Fp.create(scalars[0])); - return isoMap(x, y); - }, - htfDefaults: { - DST: 'secp256k1_XMD:SHA-256_SSWU_RO_', - p: Fp.ORDER, - m: 1, - k: 128, - expand: 'xmd', - hash: sha256, - }, }, sha256 ); +const { hashToCurve, encodeToCurve } = htf.hashToCurve( + secp256k1.Point, + (scalars: bigint[]) => { + const { x, y } = mapSWU(Fp.create(scalars[0])); + return isoMap(x, y); + }, + { + DST: 'secp256k1_XMD:SHA-256_SSWU_RO_', + encodeDST: 'secp256k1_XMD:SHA-256_SSWU_NU_', + p: Fp.ORDER, + m: 1, + k: 128, + expand: 'xmd', + hash: sha256, + } +); +export { hashToCurve, encodeToCurve }; + // Schnorr const _0n = BigInt(0); const numTo32b = secp256k1.utils._bigintToBytes; diff --git a/test/bls12-381.test.js b/test/bls12-381.test.js index ef8b6fb..135dac6 100644 --- a/test/bls12-381.test.js +++ b/test/bls12-381.test.js @@ -1459,8 +1459,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_G1.length; i++) { const t = VECTORS_G1[i]; - should(`hashToCurve/G1 Killic (${i})`, async () => { - const p = await bls.G1.Point.hashToCurve(t.msg, { + should(`hashToCurve/G1 Killic (${i})`, () => { + const p = bls.hashToCurve.G1.hashToCurve(t.msg, { DST: 'BLS12381G1_XMD:SHA-256_SSWU_RO_TESTGEN', }); deepStrictEqual(p.toHex(), t.expected); @@ -1514,8 +1514,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_G1_RO.length; i++) { const t = VECTORS_G1_RO[i]; - should(`hashToCurve/G1 (BLS12381G1_XMD:SHA-256_SSWU_RO_) (${i})`, async () => { - const p = await bls.G1.Point.hashToCurve(t.msg, { + should(`hashToCurve/G1 (BLS12381G1_XMD:SHA-256_SSWU_RO_) (${i})`, () => { + const p = bls.hashToCurve.G1.hashToCurve(t.msg, { DST: 'QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_', }); deepStrictEqual(p.toHex(), t.expected); @@ -1560,8 +1560,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_G1_NU.length; i++) { const t = VECTORS_G1_NU[i]; - should(`hashToCurve/G1 (BLS12381G1_XMD:SHA-256_SSWU_NU_) (${i})`, async () => { - const p = await bls.G1.Point.encodeToCurve(t.msg, { + should(`hashToCurve/G1 (BLS12381G1_XMD:SHA-256_SSWU_NU_) (${i})`, () => { + const p = bls.hashToCurve.G1.encodeToCurve(t.msg, { DST: 'QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_NU_', }); deepStrictEqual(p.toHex(), t.expected); @@ -1597,8 +1597,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_ENCODE_G1.length; i++) { const t = VECTORS_ENCODE_G1[i]; - should(`hashToCurve/G1 (Killic, encodeToCurve) (${i})`, async () => { - const p = await bls.G1.Point.encodeToCurve(t.msg, { + should(`hashToCurve/G1 (Killic, encodeToCurve) (${i})`, () => { + const p = bls.hashToCurve.G1.encodeToCurve(t.msg, { DST: 'BLS12381G1_XMD:SHA-256_SSWU_NU_TESTGEN', }); deepStrictEqual(p.toHex(), t.expected); @@ -1643,8 +1643,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_G2.length; i++) { const t = VECTORS_G2[i]; - should(`hashToCurve/G2 Killic (${i})`, async () => { - const p = await bls.G2.Point.hashToCurve(t.msg, { + should(`hashToCurve/G2 Killic (${i})`, () => { + const p = bls.hashToCurve.G2.hashToCurve(t.msg, { DST: 'BLS12381G2_XMD:SHA-256_SSWU_RO_TESTGEN', }); deepStrictEqual(p.toHex(), t.expected); @@ -1708,8 +1708,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_G2_RO.length; i++) { const t = VECTORS_G2_RO[i]; - should(`hashToCurve/G2 (BLS12381G2_XMD:SHA-256_SSWU_RO_) (${i})`, async () => { - const p = await bls.G2.Point.hashToCurve(t.msg, { + should(`hashToCurve/G2 (BLS12381G2_XMD:SHA-256_SSWU_RO_) (${i})`, () => { + const p = bls.hashToCurve.G2.hashToCurve(t.msg, { DST: 'QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_', }); deepStrictEqual(p.toHex(), t.expected); @@ -1773,8 +1773,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_G2_NU.length; i++) { const t = VECTORS_G2_NU[i]; - should(`hashToCurve/G2 (BLS12381G2_XMD:SHA-256_SSWU_NU_) (${i})`, async () => { - const p = await bls.G2.Point.encodeToCurve(t.msg, { + should(`hashToCurve/G2 (BLS12381G2_XMD:SHA-256_SSWU_NU_) (${i})`, () => { + const p = bls.hashToCurve.G2.encodeToCurve(t.msg, { DST: 'QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_NU_', }); deepStrictEqual(p.toHex(), t.expected); @@ -1818,8 +1818,8 @@ describe('hash-to-curve', () => { ]; for (let i = 0; i < VECTORS_ENCODE_G2.length; i++) { const t = VECTORS_ENCODE_G2[i]; - should(`hashToCurve/G2 (Killic, encodeToCurve) (${i})`, async () => { - const p = await bls.G2.Point.encodeToCurve(t.msg, { + should(`hashToCurve/G2 (Killic, encodeToCurve) (${i})`, () => { + const p = bls.hashToCurve.G2.encodeToCurve(t.msg, { DST: 'BLS12381G2_XMD:SHA-256_SSWU_NU_TESTGEN', }); deepStrictEqual(p.toHex(), t.expected); diff --git a/test/hash-to-curve.test.js b/test/hash-to-curve.test.js index 3400767..ed11794 100644 --- a/test/hash-to-curve.test.js +++ b/test/hash-to-curve.test.js @@ -5,12 +5,12 @@ import { bytesToHex } from '@noble/hashes/utils'; import { sha256 } from '@noble/hashes/sha256'; import { sha512 } from '@noble/hashes/sha512'; import { shake128, shake256 } from '@noble/hashes/sha3'; -import { secp256r1 } from '../lib/esm/p256.js'; -import { secp384r1 } from '../lib/esm/p384.js'; -import { secp521r1 } from '../lib/esm/p521.js'; -import { ed25519 } from '../lib/esm/ed25519.js'; -import { ed448 } from '../lib/esm/ed448.js'; -import { secp256k1 } from '../lib/esm/secp256k1.js'; +import * as secp256r1 from '../lib/esm/p256.js'; +import * as secp384r1 from '../lib/esm/p384.js'; +import * as secp521r1 from '../lib/esm/p521.js'; +import * as ed25519 from '../lib/esm/ed25519.js'; +import * as ed448 from '../lib/esm/ed448.js'; +import * as secp256k1 from '../lib/esm/secp256k1.js'; import { bls12_381 } from '../lib/esm/bls12-381.js'; import { stringToBytes, @@ -111,7 +111,7 @@ function testCurve(curve, ro, nu) { for (let i = 0; i < ro.vectors.length; i++) { const t = ro.vectors[i]; should(`(${i})`, () => { - const p = curve.Point.hashToCurve(stringToBytes(t.msg), { + const p = curve.hashToCurve(stringToBytes(t.msg), { DST: ro.dst, }); deepStrictEqual(p.x, stringToFp(t.P.x), 'Px'); @@ -123,7 +123,7 @@ function testCurve(curve, ro, nu) { for (let i = 0; i < nu.vectors.length; i++) { const t = nu.vectors[i]; should(`(${i})`, () => { - const p = curve.Point.encodeToCurve(stringToBytes(t.msg), { + const p = curve.encodeToCurve(stringToBytes(t.msg), { DST: nu.dst, }); deepStrictEqual(p.x, stringToFp(t.P.x), 'Px'); @@ -137,8 +137,8 @@ testCurve(secp256r1, p256_ro, p256_nu); testCurve(secp384r1, p384_ro, p384_nu); testCurve(secp521r1, p521_ro, p521_nu); // TODO: remove same tests from bls12 -testCurve(bls12_381.G1, g1_ro, g1_nu); -testCurve(bls12_381.G2, g2_ro, g2_nu); +testCurve(bls12_381.hashToCurve.G1, g1_ro, g1_nu); +testCurve(bls12_381.hashToCurve.G2, g2_ro, g2_nu); testCurve(secp256k1, secp256k1_ro, secp256k1_nu); testCurve(ed25519, ed25519_ro, ed25519_nu); testCurve(ed448, ed448_ro, ed448_nu); diff --git a/test/stark/stark.test.js b/test/stark/stark.test.js index 31a5be5..ba25328 100644 --- a/test/stark/stark.test.js +++ b/test/stark/stark.test.js @@ -255,7 +255,7 @@ should('Starknet.js cross-tests', () => { ); const msgHash = '0x6d1706bd3d1ba7c517be2a2a335996f63d4738e2f182144d078a1dd9997062e'; const sig = starknet.sign(msgHash, privateKey); - const { r, s } = (sig); + const { r, s } = sig; deepStrictEqual( r.toString(),