@aguycalled/noble-bls12-381
Version:
Fastest JS implementation of BLS12-381. Auditable, secure, 0-dependency aggregated signatures & pairings
585 lines (584 loc) • 21.2 kB
JavaScript
/*! noble-bls12-381 - MIT License (c) Paul Miller (paulmillr.com) */
import nodeCrypto from 'crypto';
import { Fp, Fr, Fp2, Fp12, CURVE, ProjectivePoint, map_to_curve_simple_swu_9mod16, isogenyMapG2, millerLoop, psi, psi2, calcPairingPrecomputes, mod } from './math.js';
export { Fp, Fr, Fp2, Fp12, CURVE };
const POW_2_381 = BigInt("0x200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
const POW_2_382 = BigInt("0x400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
const POW_2_383 = BigInt("0x800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
const PUBLIC_KEY_LENGTH = 48;
const SHA256_DIGEST_SIZE = 32;
const htfDefaults = {
DST: 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_',
p: CURVE.P,
m: 2,
k: 128,
expand: true,
};
function isWithinCurveOrder(num) {
return 0 < num && num < CURVE.r;
}
const crypto = {
node: nodeCrypto,
web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined,
};
export const utils = {
hashToField: hash_to_field,
bytesToHex,
randomBytes: (bytesLength = 32) => {
if (crypto.web) {
return crypto.web.getRandomValues(new Uint8Array(bytesLength));
}
else if (crypto.node) {
const { randomBytes } = crypto.node;
return new Uint8Array(randomBytes(bytesLength).buffer);
}
else {
throw new Error("The environment doesn't have randomBytes function");
}
},
randomPrivateKey: () => {
let i = 8;
while (i--) {
const b32 = utils.randomBytes(32);
const num = bytesToNumberBE(b32);
if (isWithinCurveOrder(num) && num !== 1n)
return b32;
}
throw new Error('Valid private key was not found in 8 iterations. PRNG is broken');
},
sha256: async (message) => {
if (crypto.web) {
const buffer = await crypto.web.subtle.digest('SHA-256', message.buffer);
return new Uint8Array(buffer);
}
else if (crypto.node) {
return Uint8Array.from(crypto.node.createHash('sha256').update(message).digest());
}
else {
throw new Error("The environment doesn't have sha256 function");
}
},
mod,
getDSTLabel() {
return htfDefaults.DST;
},
setDSTLabel(newLabel) {
if (typeof newLabel !== 'string' || newLabel.length > 2048 || newLabel.length === 0) {
throw new TypeError('Invalid DST');
}
htfDefaults.DST = newLabel;
},
};
function bytesToNumberBE(bytes) {
let value = 0n;
for (let i = bytes.length - 1, j = 0; i >= 0; i--, j++) {
value += (BigInt(bytes[i]) & 255n) << (8n * BigInt(j));
}
return value;
}
const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'));
function bytesToHex(uint8a) {
let hex = '';
for (let i = 0; i < uint8a.length; i++) {
hex += hexes[uint8a[i]];
}
return hex;
}
function hexToBytes(hex) {
if (typeof hex !== 'string') {
throw new TypeError('hexToBytes: expected string, got ' + typeof hex);
}
if (hex.length % 2)
throw new Error('hexToBytes: received invalid unpadded hex');
const array = new Uint8Array(hex.length / 2);
for (let i = 0; i < array.length; i++) {
const j = i * 2;
const hexByte = hex.slice(j, j + 2);
if (hexByte.length !== 2)
throw new Error('Invalid byte sequence');
const byte = Number.parseInt(hexByte, 16);
if (Number.isNaN(byte))
throw new Error('Invalid byte sequence');
array[i] = byte;
}
return array;
}
function toPaddedHex(num, padding) {
if (num < 0n)
throw new Error('Expected valid number');
if (typeof padding !== 'number')
throw new TypeError('Expected valid padding');
return num.toString(16).padStart(padding * 2, '0');
}
function ensureBytes(hex) {
if (hex instanceof Uint8Array)
return hex;
if (typeof hex === 'string')
return hexToBytes(hex);
throw new TypeError('Expected hex string or Uint8Array');
}
function concatBytes(...arrays) {
if (arrays.length === 1)
return arrays[0];
const length = arrays.reduce((a, arr) => a + arr.length, 0);
const result = new Uint8Array(length);
for (let i = 0, pad = 0; i < arrays.length; i++) {
const arr = arrays[i];
result.set(arr, pad);
pad += arr.length;
}
return result;
}
function stringToBytes(str) {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
function os2ip(bytes) {
let result = 0n;
for (let i = 0; i < bytes.length; i++) {
result <<= 8n;
result += BigInt(bytes[i]);
}
return result;
}
function i2osp(value, length) {
if (value < 0 || value >= 1 << (8 * length)) {
throw new Error(`bad I2OSP call: value=${value} length=${length}`);
}
const res = Array.from({ length }).fill(0);
for (let i = length - 1; i >= 0; i--) {
res[i] = value & 0xff;
value >>>= 8;
}
return new Uint8Array(res);
}
function strxor(a, b) {
const arr = new Uint8Array(a.length);
for (let i = 0; i < a.length; i++) {
arr[i] = a[i] ^ b[i];
}
return arr;
}
async function expand_message_xmd(msg, DST, lenInBytes) {
const H = utils.sha256;
const b_in_bytes = SHA256_DIGEST_SIZE;
const r_in_bytes = b_in_bytes * 2;
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 Z_pad = i2osp(0, r_in_bytes);
const l_i_b_str = i2osp(lenInBytes, 2);
const b = new Array(ell);
const b_0 = await H(concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime));
b[0] = await H(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] = await H(concatBytes(...args));
}
const pseudo_random_bytes = concatBytes(...b);
return pseudo_random_bytes.slice(0, lenInBytes);
}
async function hash_to_field(msg, count, options = {}) {
const htfOptions = { ...htfDefaults, ...options };
const log2p = htfOptions.p.toString(2).length;
const L = Math.ceil((log2p + htfOptions.k) / 8);
const len_in_bytes = count * htfOptions.m * L;
const DST = stringToBytes(htfOptions.DST);
let pseudo_random_bytes = msg;
if (htfOptions.expand) {
pseudo_random_bytes = await expand_message_xmd(msg, DST, len_in_bytes);
}
const u = new Array(count);
for (let i = 0; i < count; i++) {
const e = new Array(htfOptions.m);
for (let j = 0; j < htfOptions.m; j++) {
const elm_offset = L * (j + i * htfOptions.m);
const tv = pseudo_random_bytes.slice(elm_offset, elm_offset + L);
e[j] = mod(os2ip(tv), htfOptions.p);
}
u[i] = e;
}
return u;
}
function normalizePrivKey(key) {
let int;
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(int, CURVE.r);
if (!isWithinCurveOrder(int))
throw new Error('Private key must be 0 < key < CURVE.r');
return int;
}
function assertType(item, type) {
if (!(item instanceof type))
throw new Error('Expected Fp* argument, not number/bigint');
}
export class PointG1 extends ProjectivePoint {
constructor(x, y, z = Fp.ONE) {
super(x, y, z, Fp);
assertType(x, Fp);
assertType(y, Fp);
assertType(z, Fp);
}
static fromHex(bytes) {
bytes = ensureBytes(bytes);
const { P } = CURVE;
let point;
if (bytes.length === 48) {
const compressedValue = bytesToNumberBE(bytes);
const bflag = mod(compressedValue, POW_2_383) / POW_2_382;
if (bflag === 1n) {
return this.ZERO;
}
const x = new Fp(mod(compressedValue, POW_2_381));
const right = x.pow(3n).add(new Fp(CURVE.b));
let y = right.sqrt();
if (!y)
throw new Error('Invalid compressed G1 point');
const aflag = mod(compressedValue, POW_2_382) / POW_2_381;
if ((y.value * 2n) / P !== aflag)
y = y.negate();
point = new PointG1(x, y);
}
else if (bytes.length === 96) {
if ((bytes[0] & (1 << 6)) !== 0)
return PointG1.ZERO;
const x = bytesToNumberBE(bytes.slice(0, PUBLIC_KEY_LENGTH));
const y = bytesToNumberBE(bytes.slice(PUBLIC_KEY_LENGTH));
point = new PointG1(new Fp(x), new Fp(y));
}
else {
throw new Error('Invalid point G1, expected 48/96 bytes');
}
point.assertValidity();
return point;
}
static fromPrivateKey(privateKey) {
return this.BASE.multiplyPrecomputed(normalizePrivKey(privateKey));
}
toRawBytes(isCompressed = false) {
return hexToBytes(this.toHex(isCompressed));
}
toHex(isCompressed = false) {
this.assertValidity();
const { P } = CURVE;
if (isCompressed) {
let hex;
if (this.isZero()) {
hex = POW_2_383 + POW_2_382;
}
else {
const [x, y] = this.toAffine();
const flag = (y.value * 2n) / P;
hex = x.value + flag * POW_2_381 + POW_2_383;
}
return toPaddedHex(hex, PUBLIC_KEY_LENGTH);
}
else {
if (this.isZero()) {
return '4'.padEnd(2 * 2 * PUBLIC_KEY_LENGTH, '0');
}
else {
const [x, y] = this.toAffine();
return toPaddedHex(x.value, PUBLIC_KEY_LENGTH) + toPaddedHex(y.value, PUBLIC_KEY_LENGTH);
}
}
}
assertValidity() {
if (this.isZero())
return this;
if (!this.isOnCurve())
throw new Error('Invalid G1 point: not on curve Fp');
if (!this.isTorsionFree())
throw new Error('Invalid G1 point: must be of prime-order subgroup');
return this;
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return this.toString();
}
millerLoop(P) {
return millerLoop(P.pairingPrecomputes(), this.toAffine());
}
clearCofactor() {
const t = this.mulCurveMinusX();
return t.add(this);
}
isOnCurve() {
const b = new Fp(CURVE.b);
const { x, y, z } = this;
const left = y.pow(2n).multiply(z).subtract(x.pow(3n));
const right = b.multiply(z.pow(3n));
return left.subtract(right).isZero();
}
sigma() {
const BETA = 0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaacn;
const [x, y] = this.toAffine();
return new PointG1(x.multiply(BETA), y);
}
phi() {
const cubicRootOfUnityModP = 0x5f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffen;
return new PointG1(this.x.multiply(cubicRootOfUnityModP), this.y, this.z);
}
mulCurveX() {
return this.multiplyUnsafe(CURVE.x).negate();
}
mulCurveMinusX() {
return this.multiplyUnsafe(CURVE.x);
}
isTorsionFree() {
const xP = this.mulCurveX();
const u2P = xP.mulCurveMinusX();
return u2P.equals(this.phi());
}
}
PointG1.BASE = new PointG1(new Fp(CURVE.Gx), new Fp(CURVE.Gy), Fp.ONE);
PointG1.ZERO = new PointG1(Fp.ONE, Fp.ONE, Fp.ZERO);
export class PointG2 extends ProjectivePoint {
constructor(x, y, z = Fp2.ONE) {
super(x, y, z, Fp2);
assertType(x, Fp2);
assertType(y, Fp2);
assertType(z, Fp2);
}
static async hashToCurve(msg) {
msg = ensureBytes(msg);
const u = await hash_to_field(msg, 2);
const Q0 = new PointG2(...isogenyMapG2(map_to_curve_simple_swu_9mod16(u[0])));
const Q1 = new PointG2(...isogenyMapG2(map_to_curve_simple_swu_9mod16(u[1])));
const R = Q0.add(Q1);
const P = R.clearCofactor();
return P;
}
static fromSignature(hex) {
hex = ensureBytes(hex);
const { P } = CURVE;
const half = hex.length / 2;
if (half !== 48 && half !== 96)
throw new Error('Invalid compressed signature length, must be 96 or 192');
const z1 = bytesToNumberBE(hex.slice(0, half));
const z2 = bytesToNumberBE(hex.slice(half));
const bflag1 = mod(z1, POW_2_383) / POW_2_382;
if (bflag1 === 1n)
return this.ZERO;
const x1 = z1 % POW_2_381;
const x2 = z2;
const x = new Fp2([x2, x1]);
const y2 = x.pow(3n).add(new Fp2(CURVE.b2));
let y = y2.sqrt();
if (!y)
throw new Error('Failed to find a square root');
const [y0, y1] = y.values;
const aflag1 = (z1 % POW_2_382) / POW_2_381;
const isGreater = y1 > 0n && (y1 * 2n) / P !== aflag1;
const isZero = y1 === 0n && (y0 * 2n) / P !== aflag1;
if (isGreater || isZero)
y = y.multiply(-1n);
const point = new PointG2(x, y, Fp2.ONE);
point.assertValidity();
return point;
}
static fromHex(bytes) {
bytes = ensureBytes(bytes);
let point;
if (bytes.length === 96) {
throw new Error('Compressed format not supported yet.');
}
else if (bytes.length === 192) {
if ((bytes[0] & (1 << 6)) !== 0) {
return PointG2.ZERO;
}
const x1 = bytesToNumberBE(bytes.slice(0, PUBLIC_KEY_LENGTH));
const x0 = bytesToNumberBE(bytes.slice(PUBLIC_KEY_LENGTH, 2 * PUBLIC_KEY_LENGTH));
const y1 = bytesToNumberBE(bytes.slice(2 * PUBLIC_KEY_LENGTH, 3 * PUBLIC_KEY_LENGTH));
const y0 = bytesToNumberBE(bytes.slice(3 * PUBLIC_KEY_LENGTH));
point = new PointG2(new Fp2([x0, x1]), new Fp2([y0, y1]));
}
else {
throw new Error('Invalid uncompressed point G2, expected 192 bytes');
}
point.assertValidity();
return point;
}
static fromPrivateKey(privateKey) {
return this.BASE.multiplyPrecomputed(normalizePrivKey(privateKey));
}
toSignature() {
if (this.equals(PointG2.ZERO)) {
const sum = POW_2_383 + POW_2_382;
const h = toPaddedHex(sum, PUBLIC_KEY_LENGTH) + toPaddedHex(0n, PUBLIC_KEY_LENGTH);
return hexToBytes(h);
}
const [[x0, x1], [y0, y1]] = this.toAffine().map((a) => a.values);
const tmp = y1 > 0n ? y1 * 2n : y0 * 2n;
const aflag1 = tmp / CURVE.P;
const z1 = x1 + aflag1 * POW_2_381 + POW_2_383;
const z2 = x0;
return hexToBytes(toPaddedHex(z1, PUBLIC_KEY_LENGTH) + toPaddedHex(z2, PUBLIC_KEY_LENGTH));
}
toRawBytes(isCompressed = false) {
return hexToBytes(this.toHex(isCompressed));
}
toHex(isCompressed = false) {
this.assertValidity();
if (isCompressed) {
throw new Error('Point compression has not yet been implemented');
}
else {
if (this.equals(PointG2.ZERO)) {
return '4'.padEnd(2 * 4 * PUBLIC_KEY_LENGTH, '0');
}
const [[x0, x1], [y0, y1]] = this.toAffine().map((a) => a.values);
return (toPaddedHex(x1, PUBLIC_KEY_LENGTH) +
toPaddedHex(x0, PUBLIC_KEY_LENGTH) +
toPaddedHex(y1, PUBLIC_KEY_LENGTH) +
toPaddedHex(y0, PUBLIC_KEY_LENGTH));
}
}
assertValidity() {
if (this.isZero())
return this;
if (!this.isOnCurve())
throw new Error('Invalid G2 point: not on curve Fp2');
if (!this.isTorsionFree())
throw new Error('Invalid G2 point: must be of prime-order subgroup');
return this;
}
psi() {
return this.fromAffineTuple(psi(...this.toAffine()));
}
psi2() {
return this.fromAffineTuple(psi2(...this.toAffine()));
}
mulCurveX() {
return this.multiplyUnsafe(CURVE.x).negate();
}
clearCofactor() {
const P = this;
let t1 = P.mulCurveX();
let t2 = P.psi();
let t3 = P.double();
t3 = t3.psi2();
t3 = t3.subtract(t2);
t2 = t1.add(t2);
t2 = t2.mulCurveX();
t3 = t3.add(t2);
t3 = t3.subtract(t1);
const Q = t3.subtract(P);
return Q;
}
isOnCurve() {
const b = new Fp2(CURVE.b2);
const { x, y, z } = this;
const left = y.pow(2n).multiply(z).subtract(x.pow(3n));
const right = b.multiply(z.pow(3n));
return left.subtract(right).isZero();
}
isTorsionFree() {
const P = this;
return P.mulCurveX().equals(P.psi());
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return this.toString();
}
clearPairingPrecomputes() {
this._PPRECOMPUTES = undefined;
}
pairingPrecomputes() {
if (this._PPRECOMPUTES)
return this._PPRECOMPUTES;
this._PPRECOMPUTES = calcPairingPrecomputes(...this.toAffine());
return this._PPRECOMPUTES;
}
}
PointG2.BASE = new PointG2(new Fp2(CURVE.G2x), new Fp2(CURVE.G2y), Fp2.ONE);
PointG2.ZERO = new PointG2(Fp2.ONE, Fp2.ONE, Fp2.ZERO);
export function pairing(P, Q, withFinalExponent = true) {
if (P.isZero() || Q.isZero())
throw new Error('No pairings at point of Infinity');
P.assertValidity();
Q.assertValidity();
const looped = P.millerLoop(Q);
return withFinalExponent ? looped.finalExponentiate() : looped;
}
function normP1(point) {
return point instanceof PointG1 ? point : PointG1.fromHex(point);
}
function normP2(point) {
return point instanceof PointG2 ? point : PointG2.fromSignature(point);
}
async function normP2Hash(point) {
return point instanceof PointG2 ? point : PointG2.hashToCurve(point);
}
export function getPublicKey(privateKey) {
return PointG1.fromPrivateKey(privateKey).toRawBytes(true);
}
export async function sign(message, privateKey) {
const msgPoint = await normP2Hash(message);
msgPoint.assertValidity();
const sigPoint = msgPoint.multiply(normalizePrivKey(privateKey));
if (message instanceof PointG2)
return sigPoint;
return sigPoint.toSignature();
}
export async function verify(signature, message, publicKey) {
const P = normP1(publicKey);
const Hm = await normP2Hash(message);
const G = PointG1.BASE;
const S = normP2(signature);
const ePHm = pairing(P.negate(), Hm, false);
const eGS = pairing(G, S, false);
const exp = eGS.multiply(ePHm).finalExponentiate();
return exp.equals(Fp12.ONE);
}
export function aggregatePublicKeys(publicKeys) {
if (!publicKeys.length)
throw new Error('Expected non-empty array');
const agg = publicKeys.map(normP1).reduce((sum, p) => sum.add(p), PointG1.ZERO);
if (publicKeys[0] instanceof PointG1)
return agg.assertValidity();
return agg.toRawBytes(true);
}
export function aggregateSignatures(signatures) {
if (!signatures.length)
throw new Error('Expected non-empty array');
const agg = signatures.map(normP2).reduce((sum, s) => sum.add(s), PointG2.ZERO);
if (signatures[0] instanceof PointG2)
return agg.assertValidity();
return agg.toSignature();
}
export async function verifyBatch(signature, messages, publicKeys) {
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 = await Promise.all(messages.map(normP2Hash));
const nPublicKeys = publicKeys.map(normP1);
try {
const paired = [];
for (const message of new Set(nMessages)) {
const groupPublicKey = nMessages.reduce((groupPublicKey, subMessage, i) => subMessage === message ? groupPublicKey.add(nPublicKeys[i]) : groupPublicKey, PointG1.ZERO);
paired.push(pairing(groupPublicKey, message, false));
}
paired.push(pairing(PointG1.BASE.negate(), sig, false));
const product = paired.reduce((a, b) => a.multiply(b), Fp12.ONE);
const exp = product.finalExponentiate();
return exp.equals(Fp12.ONE);
}
catch {
return false;
}
}
PointG1.BASE.calcMultiplyPrecomputes(4);