noble-bls12-381
Fastest JS implementation of BLS12-381. Auditable, secure, 0-dependency aggregated signatures & pairings.
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.
Compatible with Algorand, Chia, Dfinity, Ethereum, FIL, Zcash. Matches specs pairing-curves-10, bls-sigs-04, hash-to-curve-12. To learn more about internals, navigate to
utilities section.
This library belongs to noble crypto
noble-crypto — high-security, easily auditable set of contained cryptographic libraries and tools.
- Just two files
- No dependencies
- Easily auditable TypeScript/JS code
- Supported in all major browsers and stable node.js versions
- All releases are signed with PGP keys
- Check out homepage & all libraries:
secp256k1,
ed25519,
bls12-381,
hashes
Usage
Use NPM in node.js / browser, or include single file from
GitHub's releases page:
npm install @noble/bls12-381
const bls = require('@noble/bls12-381');
(async () => {
const privateKey = '67d53f170b908cabb9eb326c3c337762d59289a8fec79f7bc9254b584b73265c';
const message = '64726e3da8';
const publicKey = bls.getPublicKey(privateKey);
const signature = await bls.sign(message, privateKey);
const isValid = await bls.verify(signature, message, publicKey);
console.log({ publicKey, signature, isValid });
const privateKeys = [
'18f020b98eb798752a50ed0563b079c125b0db5dd0b1060d1c1b47d4a193e1e4',
'ed69a8c50cf8c9836be3b67c7eeff416612d45ba39a5c099d48fa668bf558c9c',
'16ae669f3be7a2121e17d0c68c05a8f3d6bef21ec0f2315f1d7aec12484e4cf5'
];
const messages = ['d2', '0d98', '05caf3'];
const publicKeys = privateKeys.map(bls.getPublicKey);
const signatures2 = await Promise.all(privateKeys.map(p => bls.sign(message, p)));
const aggPubKey2 = bls.aggregatePublicKeys(publicKeys);
const aggSignature2 = bls.aggregateSignatures(signatures2);
const isValid2 = await bls.verify(aggSignature2, message, aggPubKey2);
console.log({ signatures2, aggSignature2, isValid2 });
const signatures3 = await Promise.all(privateKeys.map((p, i) => bls.sign(messages[i], p)));
const aggSignature3 = bls.aggregateSignatures(signatures3);
const isValid3 = await bls.verifyBatch(aggSignature3, messages, publicKeys);
console.log({ publicKeys, signatures3, aggSignature3, isValid3 });
})();
To use the module with Deno,
you will need import map:
deno run --import-map=imports.json app.ts
- app.ts:
import * as bls from "https://deno.land/x/bls12_381/mod.ts";
- imports.json:
{"imports": {"crypto": "https://deno.land/std@0.119.0/node/crypto.ts"}}
API
getPublicKey(privateKey)
function getPublicKey(privateKey: Uint8Array | string | bigint): Uint8Array;
privateKey: Uint8Array | string | bigint
will be used to generate public key.
Public key is generated by executing scalar multiplication of a base Point(x, y) by a fixed
integer. The result is another Point(x, y)
which we will by default encode to hex Uint8Array.- Returns
Uint8Array
: encoded publicKey for signature verification
Note: if you need EIP2333-compliant KeyGen
(eth2/fil), use paulmillr/bls12-381-keygen.
sign(message, privateKey)
function sign(message: Uint8Array | string, privateKey: Uint8Array | string): Promise<Uint8Array>;
function sign(message: PointG2, privateKey: Uint8Array | string | bigint): Promise<PointG2>;
message: Uint8Array | string
- message which would be hashed & signedprivateKey: Uint8Array | string | bigint
- private key which will sign the hash- Returns
Uint8Array | string | PointG2
: encoded signature
Check out Utilities section on instructions about domain separation tag (DST).
verify(signature, message, publicKey)
function verify(
signature: Uint8Array | string | PointG2,
message: Uint8Array | string | PointG2,
publicKey: Uint8Array | string | PointG1
): Promise<boolean>
signature: Uint8Array | string
- object returned by the sign
or aggregateSignatures
functionmessage: Uint8Array | string
- message hash that needs to be verifiedpublicKey: Uint8Array | string
- e.g. that was generated from privateKey
by getPublicKey
- Returns
Promise<boolean>
: true
/ false
whether the signature matches hash
aggregatePublicKeys(publicKeys)
function aggregatePublicKeys(publicKeys: (Uint8Array | string)[]): Uint8Array;
function aggregatePublicKeys(publicKeys: PointG1[]): PointG1;
publicKeys: (Uint8Array | string | PointG1)[]
- e.g. that have been generated from privateKey
by getPublicKey
- Returns
Uint8Array | PointG1
: one aggregated public key which calculated from public keys
aggregateSignatures(signatures)
function aggregateSignatures(signatures: (Uint8Array | string)[]): Uint8Array;
function aggregateSignatures(signatures: PointG2[]): PointG2;
signatures: (Uint8Array | string | PointG2)[]
- e.g. that have been generated by sign
- Returns
Uint8Array | PointG2
: one aggregated signature which calculated from signatures
verifyBatch(signature, messages, publicKeys)
function verifyBatch(
signature: Uint8Array | string | PointG2,
messages: (Uint8Array | string | PointG2)[],
publicKeys: (Uint8Array | string | PointG1)[]
): Promise<boolean>
signature: Uint8Array | string | PointG2
- object returned by the aggregateSignatures
functionmessages: (Uint8Array | string | PointG2)[]
- messages hashes that needs to be verifiedpublicKeys: (Uint8Array | string | PointG1)[]
- e.g. that were generated from privateKeys
by getPublicKey
- Returns
Promise<boolean>
: true
/ false
whether the signature matches hashes
pairing(pointG1, pointG2)
function pairing(
pointG1: PointG1,
pointG2: PointG2,
withFinalExponent: boolean = true
): Fp12
pointG1: PointG1
- simple point, x, y
are bigintspointG2: PointG2
- point over curve with complex numbers ((x₁, x₂+i), (y₁, y₂+i)
) - pairs of bigintswithFinalExponent: boolean
- should the result be powered by curve order; very slow- Returns
Fp12
: paired point over 12-degree extension field.
Utilities
Resources that help to understand bls12-381:
The library uses G1 for public keys and G2 for signatures. Adding support for G1 signatures is planned.
- BLS Relies on Bilinear Pairing (expensive)
- Private Keys: 32 bytes
- Public Keys: 48 bytes: 381 bit affine x coordinate, encoded into 48 big-endian bytes.
- Signatures: 96 bytes: two 381 bit integers (affine x coordinate), encoded into two 48 big-endian byte arrays.
- The signature is a point on the G2 subgroup, which is defined over a finite field
with elements twice as big as the G1 curve (G2 is over Fp2 rather than Fp. Fp2 is analogous to the complex numbers).
- The 12 stands for the Embedding degree.
Formulas:
P = pk x G
- public keysS = pk x H(m)
- signinge(P, H(m)) == e(G, S)
- verification using pairingse(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si))
- signature aggregation
The BLS parameters for the library are:
PK_IN
G1
HASH_OR_ENCODE
true
DST
BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_
- use bls.utils.getDSTLabel()
& bls.utils.setDSTLabel("...")
to read/change the Domain Separation Tag labelRAND_BITS
64
Filecoin uses little endian byte arrays for private keys - so ensure to reverse byte order if you'll use it with FIL.
const utils: {
hashToField(msg: Uint8Array, count: number, options = {}): Promise<bigint[][]>;
bytesToHex: (bytes: Uint8Array): string;
randomBytes: (bytesLength?: number) => Uint8Array;
randomPrivateKey: () => Uint8Array;
sha256: (message: Uint8Array) => Promise<Uint8Array>;
mod: (a: bigint, b = CURVE.P): bigint;
getDSTLabel(): string;
setDSTLabel(newLabel: string): void;
};
CURVE.P
CURVE.r
curve.h
CURVE.Gx, CURVE.Gy
CURVE.G2x, CURVE.G2y
bls.Fp
bls.Fp2
bls.Fp12
declare const htfDefaults: {
DST: string;
p: bigint;
m: number;
k: number;
expand: boolean;
hash: Hash;
};
class PointG1 extends ProjectivePoint<Fp> {
constructor(x: Fp, y: Fp, z?: Fp);
static BASE: PointG1;
static ZERO: PointG1;
static fromHex(bytes: Bytes): PointG1;
static fromPrivateKey(privateKey: PrivateKey): PointG1;
static hashToCurve(msg: Hex, options?: Partial<typeof htfDefaults>): Promise<PointG1>;
toRawBytes(isCompressed?: boolean): Uint8Array;
toHex(isCompressed?: boolean): string;
assertValidity(): this;
millerLoop(P: PointG2): Fp12;
clearCofactor(): PointG1;
}
class PointG2 extends ProjectivePoint<Fp2> {
constructor(x: Fp2, y: Fp2, z?: Fp2);
static BASE: PointG2;
static ZERO: PointG2;
static hashToCurve(msg: Hex, options?: Partial<typeof htfDefaults>): Promise<PointG2>;
static fromSignature(hex: Bytes): PointG2;
static fromHex(bytes: Bytes): PointG2;
static fromPrivateKey(privateKey: PrivateKey): PointG2;
toSignature(): Uint8Array;
toRawBytes(isCompressed?: boolean): Uint8Array;
toHex(isCompressed?: boolean): string;
assertValidity(): this;
clearCofactor(): PointG2;
}
Speed
To achieve the best speed out of all JS / Python implementations, the library employs different optimizations:
- cyclotomic exponentation
- endomorphism for clearing cofactor
- pairing precomputation
Benchmarks measured with Apple M2 on macOS 12.5 with node.js 18.8:
getPublicKey x 818 ops/sec @ 1ms/op
sign x 44 ops/sec @ 22ms/op
verify x 34 ops/sec @ 29ms/op
pairing x 84 ops/sec @ 11ms/op
aggregatePublicKeys/8 x 117 ops/sec @ 8ms/op
aggregateSignatures/8 x 45 ops/sec @ 21ms/op
with compression / decompression disabled:
sign/nc x 60 ops/sec @ 16ms/op
verify/nc x 57 ops/sec @ 17ms/op ± 1.05% (min: 17ms, max: 19ms)
aggregatePublicKeys/32 x 1,163 ops/sec @ 859μs/op
aggregatePublicKeys/128 x 758 ops/sec @ 1ms/op
aggregatePublicKeys/512 x 318 ops/sec @ 3ms/op
aggregatePublicKeys/2048 x 96 ops/sec @ 10ms/op
aggregateSignatures/32 x 516 ops/sec @ 1ms/op
aggregateSignatures/128 x 269 ops/sec @ 3ms/op
aggregateSignatures/512 x 93 ops/sec @ 10ms/op
aggregateSignatures/2048 x 25 ops/sec @ 38ms/op
Security
Noble is production-ready.
- No public audits have been done yet. Our goal is to crowdfund the audit.
- It was developed in a similar fashion to
noble-secp256k1, which was audited by a third-party firm.
- It was fuzzed by Guido Vranken's cryptofuzz,
no serious issues have been found. You can run the fuzzer by yourself to check it.
We're using built-in JS BigInt
, which is potentially vulnerable to timing attacks as per official spec. But, JIT-compiler and Garbage Collector make "constant time" extremely hard to achieve in a scripting language. Which means any other JS library doesn't use constant-time bigints. Including bn.js or anything else. Even statically typed Rust, a language without GC, makes it harder to achieve constant-time for some cases. If your goal is absolute security, don't use any JS lib — including bindings to native ones. Use low-level libraries & languages.
We however consider infrastructure attacks like rogue NPM modules very important; that's why it's crucial to minimize the amount of 3rd-party dependencies & native bindings. If your app uses 500 dependencies, any dep could get hacked and you'll be downloading malware with every npm install
. Our goal is to minimize this attack vector.
Contributing
- Clone the repository.
npm install
to install build dependencies like TypeScriptnpm run build
to compile TypeScript codenpm run test
to run jest on test/index.ts
Special thanks to Roman Koblov, who have helped to improve pairing speed.
License
MIT (c) 2019 Paul Miller (https://paulmillr.com), see LICENSE file.