BLS, jubjub refactoring

This commit is contained in:
Paul Miller 2023-01-12 19:38:10 +00:00
parent 2e81f31d2e
commit 069452dbe7
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
3 changed files with 60 additions and 59 deletions

@ -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,11 +93,11 @@ export type CurveFn<Fp, Fp2, Fp6, Fp12> = {
publicKeys: (Hex | PointType<Fp>)[] publicKeys: (Hex | PointType<Fp>)[]
) => boolean; ) => boolean;
utils: { utils: {
bytesToHex: typeof utils.bytesToHex; bytesToHex: typeof ut.bytesToHex;
hexToBytes: typeof utils.hexToBytes; hexToBytes: typeof ut.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; mod: typeof mod.mod;
getDSTLabel: () => string; getDSTLabel: () => string;
setDSTLabel(newLabel: string): void; setDSTLabel(newLabel: string): void;
@ -100,7 +113,8 @@ export function bls<Fp2, Fp6, Fp12>(
const Fp2 = CURVE.Fp2; const Fp2 = CURVE.Fp2;
const Fp6 = CURVE.Fp6; const Fp6 = CURVE.Fp6;
const Fp12 = CURVE.Fp12; const Fp12 = CURVE.Fp12;
const BLS_X_LEN = bitLen(CURVE.x); const BLS_X_LEN = ut.bitLen(CURVE.x);
const groupLen = 32; // TODO: calculate; hardcoded for now
// 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 +139,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
@ -153,7 +167,7 @@ export function bls<Fp2, Fp6, Fp12>(
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(CURVE.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,59 +177,41 @@ 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 }; const htfDefaults = { ...CURVE.htfDefaults };
// TODO: not needed?
function isWithinCurveOrder(num: bigint): boolean { function isWithinCurveOrder(num: bigint): boolean {
return 0 < num && num < CURVE.r; return 0 < num && num < CURVE.r;
} }
const utils = { const utils = {
hexToBytes: hexToBytes, hexToBytes: ut.hexToBytes,
bytesToHex: bytesToHex, bytesToHex: ut.bytesToHex,
mod: mod.mod, mod: mod.mod,
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: (msg: Uint8Array, count: number, options: Partial<typeof htfDefaults> = {}) =>
hash_to_field(msg, count, { ...CURVE.htfDefaults, ...options }), 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),
/** /**
* Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF * Creates FIPS 186 B.4.1 compliant private keys without modulo bias.
* and convert them into private key, with the modulo bias being negligible.
* 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 => { hashToPrivateKey: (hash: Hex): Uint8Array => {
hash = ensureBytes(hash); hash = ut.ensureBytes(hash);
if (hash.length < 40 || hash.length > 1024) if (hash.length < 40 || hash.length > 1024)
throw new Error('Expected 40-1024 bytes of private key as per FIPS 186'); throw new Error('Expected 40-1024 bytes of private key as per FIPS 186');
// hashToPrivateScalar(hash, CURVE.r) // hashToPrivateScalar(hash, CURVE.r)
// NOTE: doesn't add +/-1 // NOTE: doesn't add +/-1
const num = mod.mod(bytesToNumberBE(hash), CURVE.r); const num = mod.mod(ut.bytesToNumberBE(hash), CURVE.r);
// This should never happen // This should never happen
if (num === 0n || num === 1n) throw new Error('Invalid private key'); if (num === 0n || num === 1n) throw new Error('Invalid private key');
return numberToBytesBE(num, 32); return ut.numberToBytesBE(num, groupLen);
}, },
randomBytes: (bytesLength: number = 32): Uint8Array => CURVE.randomBytes(bytesLength), randomBytes: (bytesLength: number = groupLen): Uint8Array => CURVE.randomBytes(bytesLength),
// NIST SP 800-56A rev 3, section 5.6.1.2.2 randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(groupLen + 8)),
// 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, 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
@ -226,11 +222,12 @@ export function bls<Fp2, Fp6, Fp12>(
}, },
}; };
// TODO: reuse CURVE.G1.utils.normalizePrivateKey() ?
function normalizePrivKey(key: PrivKey): bigint { function normalizePrivKey(key: PrivKey): bigint {
let int: bigint; let int: bigint;
if (key instanceof Uint8Array && key.length === 32) int = bytesToNumberBE(key); if (key instanceof Uint8Array && key.length === groupLen) int = ut.bytesToNumberBE(key);
else if (typeof key === 'string' && key.length === 64) int = BigInt(`0x${key}`); else if (typeof key === 'string' && key.length === 2 * groupLen) int = BigInt(`0x${key}`);
else if (typeof key === 'number' && key > 0 && Number.isSafeInteger(key)) int = BigInt(key); else if (ut.isPositiveInt(key)) int = BigInt(key);
else if (typeof key === 'bigint' && key > 0n) int = key; else if (typeof key === 'bigint' && key > 0n) int = key;
else throw new TypeError('Expected valid private key'); else throw new TypeError('Expected valid private key');
int = mod.mod(int, CURVE.r); int = mod.mod(int, CURVE.r);

@ -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 =

@ -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';
@ -16,7 +16,7 @@ export const jubjub = twistedEdwards({
d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'), d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'),
// Finite field 𝔽p over which we'll do calculations // Finite field 𝔽p over which we'll do calculations
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; // 2n ** 252n + 27742317777372353535851937790883648493n;
n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'), n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'),
// Cofactor // Cofactor
@ -24,7 +24,7 @@ export const jubjub = twistedEdwards({
// 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);