bls-sign
v0.13.23
Published
BLS threshold signatures creation and verification
Maintainers
Readme
bls-sign
npm: bls-sign
This package is still being downloaded ~10 times a week after 6 years! I decided to publish some small updates using new AI features. I am going to split 2 curves to 2 packages.
Boneh–Lynn–Shacham signature scheme
The Boneh–Lynn–Shacham (BLS) signature scheme allows a user to verify that a signer is authentic. The scheme uses a bilinear pairing for verification, and signatures are elements of an elliptic curve group. Working in an elliptic curve group provides some defense against index calculus attacks, allowing shorter signatures than FDH signatures for a similar level of security. Signatures produced by the BLS signature scheme are often referred to as short signatures, BLS short signatures, or simply BLS signatures. The signature scheme is provably secure (it is existentially unforgeable under adaptive chosen-message attacks), assuming both the existence of random oracles and the intractability of the computational Diffie–Hellman problem in a gap Diffie–Hellman group.
Usage
npm install bls-signconst { BLSSigner, BLSSecretKey, BLSPublicKey } = require('bls-sign')
const signer = new BLSSigner(256)
// Q is a fixed global generator on G1; H is the message hashed onto G2
const Q = signer.G.multiply(4n)
const H = signer.getRandomPointOnEt()
const secretKey = new BLSSecretKey()
const publicKey = new BLSPublicKey(secretKey, Q)
const signature = secretKey.sign(H)
signer.verify(Q, H, publicKey, signature) // trueIn the browser
A self-contained UMD build (with alg-field/alg-bn inlined and no Node crypto dependency) is published alongside the package, so it can be loaded straight from a CDN — no build step:
<script src="https://unpkg.com/bls-sign"></script>
<script>
const { BLSSigner, BLSSecretKey, BLSPublicKey } = window['bls-sign']
const signer = new BLSSigner(256)
const Q = signer.G.multiply(4n)
const H = signer.getRandomPointOnEt()
const secretKey = new BLSSecretKey()
const publicKey = new BLSPublicKey(secretKey, Q)
console.log(signer.verify(Q, H, publicKey, secretKey.sign(H))) // true
</script>Pin a version for production (https://unpkg.com/[email protected]/dist-web/index.js); jsDelivr serves the same file. Bundlers pick this build up automatically through the browser field, which also avoids the Node crypto import that the main entry point uses.
Using the BLS12-381 curve
The default curve is alt_bn128 (BN254), matching Ethereum's precompiles. A signer over BLS12-381 (the curve used by Ethereum consensus, Zcash, Chia, and filecoin) works identically:
const signer = BLSSigner.bls12381(256)
const Q = signer.G.multiply(4n)
const H = signer.getRandomPointOnEt()
const secretKey = new BLSSecretKey()
const publicKey = new BLSPublicKey(secretKey, Q)
signer.verify(Q, H, publicKey, secretKey.sign(H)) // trueThe final exponentiation uses the standard optimized easy-part/hard-part split (Hayashida–Hayasaka–Teruya decomposition over the cyclotomic subgroup), and verify runs a single merged product check, so a full verify takes on the order of 110 ms (see the Performance section below). The computed value is the cube of the textbook pairing — a bijection on the result subgroup, so all equality and verification semantics are identical; only raw pairing outputs compared against other libraries would differ.
Threshold signatures
A secret key can be split into n shares, any k of which are enough to reconstruct it (Shamir's Secret Sharing):
// split secretKey into 5 shares, any 3 of which reconstruct it
const shares = secretKey.share(5, 3)
const recovered = new BLSSecretKey()
recovered.recover(shares.slice(0, 3))
recovered.s === secretKey.s // truePerformance
Benchmarked against the actual packages published on npm, installed fresh with each version's own declared dependencies. Environment: Node v24.11.0, Apple M2 Pro. Times are the mean over repeated iterations after warmup. (0.13.12 was never published to npm and was code-identical to 0.13.11, so 0.13.11 stands in for it below.)
BN254 (alt_bn128, default curve)
| Operation | 0.13.11 | 0.13.13 | 0.13.14 | 0.13.15 |
| -------------------------------- | ------- | ------- | ------- | ---------- |
| sign | 0.76 ms | 0.70 ms | 0.72 ms | 0.71 ms |
| verify | 176 ms | 175 ms | 170 ms | 111 ms |
| getRandomPointOnEt | 0.99 ms | 1.1 ms | 1.0 ms | 1.0 ms |
| share(5, 3) | 12 µs | 11 µs | 12 µs | 11 µs |
| secret recover (3 shares) | 2.5 µs | 3.3 µs | 2.6 µs | 2.4 µs |
| signature aggregation (3 shares) | 0.65 ms | 0.67 ms | 0.65 ms | 0.64 ms |
BN254 performance was flat from 0.13.11 through 0.13.14 — in particular, the alg-field/alg-bn 0.1.x → 0.2.x upgrade (which rewrote the field tower to be curve-parameterized) cost nothing on the default-curve path. 0.13.15 restructured verify into a single product check e(−sQ, H) · e(Q, sH) == 1 over a merged multi-Miller loop, sharing one squaring chain and one final exponentiation across both pairings instead of running two independent pairing computations.
BLS12-381 (BLSSigner.bls12381(), added in 0.13.13)
| Operation | 0.13.13 | 0.13.14 | 0.13.15 | 0.13.16 |
| ---------------------------------- | ------- | ------- | ---------- | --------- |
| first bls12381() call (one-time) | 0.91 s | 0.95 s | 0.94 s | ~2 ms |
| sign | 0.73 ms | 0.77 ms | 0.75 ms | 0.75 ms |
| verify | 3.36 s | 183 ms | 113 ms | 112 ms |
| signature aggregation (3 shares) | 0.69 ms | 0.68 ms | 0.69 ms | 0.69 ms |
The 18x verify speedup in 0.13.14 comes from replacing the generic 4314-bit final exponentiation with the optimized easy-part/hard-part split (Hayashida–Hayasaka–Teruya decomposition over the cyclotomic subgroup); 0.13.15 adds the same merged single-product verify as BN254. Verification on the two curves is now equally fast (~110 ms). Signing and aggregation are scalar-multiplication-bound and unaffected.
In 0.13.16 the extension-tower parameters (non-residue and all Frobenius coefficients) are hardcoded rather than derived by brute-force search on first use, eliminating the ~0.9 s one-time bls12381() cost; a unit test re-runs the generic derivation and asserts it still matches the hardcoded constants.
The sign figures above predate 0.13.23. Secret keys were one byte then, so scalar multiplication only had 8 bits to walk; with full-width 254-bit keys sign costs ~33 ms on both curves. That is the real price of the key size, not a regression — verify is unchanged, since the pairing dominates it.
API
BLSSigner
Holds the curve's fixed generator points and provides the top-level sign/verify helpers.
new BLSSigner(bitLength, G, G2, PairingCheckImpl, fieldPrime)— createsG(a generator point on G1) andG2(a generator point on G2); both default to alt_bn128's standard generators and can be overridden, along with the pairing implementation and base-field prime.bitLengthis currently unused.BLSSigner.bls12381(bitLength)— factory returning a signer over the BLS12-381 curve (standard generators, M-type-twist optimal ate pairing) instead of the default alt_bn128..G,.G2— the fixed generator points..getRandomPointOnE()— a random scalar multiple ofG(a random point on G1)..getRandomPointOnEt()— a random scalar multiple ofG2(a random point on G2); typically used as the "message" pointH..sign(H, s)— low-level signing: returnsH.multiply(s), a raw point (not aBLSSignature). For normal use preferBLSSecretKey.sign(H)below..verify(Q, H, sQ, sH)— checkse(sQ, H) === e(Q, sH).sQmust be aBLSPublicKey,sHaBLSSignature..getPairing(),.getParameters()— currently always returnundefined; unimplemented.
BLSSecretKey
new BLSSecretKey(s, order)— wraps a secret scalar. Ifsis omitted, generates a uniformly random secret in[1, order-1], whereorderdefaults to the BN254 subgroup order (~254 bits, and a valid scalar on either supported curve). Before 0.13.23 this drew a single random byte, giving a keyspace of 256 — see the security note below..toString().getPublicKey(Q)— derives this key'sBLSPublicKeyfor generatorQ..sign(H)— signs pointH, returning aBLSSignature..getMasterSecretKey(k)— builds thekcoefficients of a degreek-1sharing polynomial withthisas the constant term (the secret). Throws ifk <= 1..share(n, k)— Shamir's Secret Sharing: splits the secret intonshares (ids1..n) drawn from a degreek-1polynomial; anykof the returned shares can reconstruct the secret. Throws ifk > n..recover(vec)— reconstructs the secret from an array of shares via Lagrange interpolation and setsthis.s(this.idbecomes0). Passing fewer thankshares does not throw — it silently produces a different, wrong secret, so callers are responsible for gathering enough shares.
BLSPublicKey
new BLSPublicKey(secretKey, Q)— computessQ = Q.multiply(secretKey.s)..toString()
BLSSignature
.toString().recover(signVec)— combines an array of partial signatures (each produced by a key share's.sign()) into one valid signature via Lagrange interpolation, without ever reconstructing the underlying secret key. Same "fewer thankshares silently gives a wrong result" caveat asBLSSecretKey.recover.
BLSSignature instances are normally produced by BLSSecretKey.sign() / BLSSignature.recover(), not constructed directly.
BLSPolynomial
Internal helper backing the threshold-sharing methods above:
.eval(msk, x)— evaluates the sharing polynomial (coefficientsmsk, each with an.s) atx..calcDelta(ids, modulus)— computes the Lagrange basis coefficients atx = 0for an array of share ids. These are fractions in general (for ids1,2,4the first is8/3), so pass the group order asmodulusto get them as exact modular values; without a modulus they are only returned when every coefficient happens to be an integer, and an inexact set throws rather than silently truncating. Throws if fewer than 2 ids are given, or if two ids are equal..lagrange(vec)— reconstructs either a secret scalar (if entries have.s, via exact rational interpolation) or an aggregated signature point (if entries have.sH, via coefficients reduced modulo the curve's group order) atx = 0. Throws if given fewer than two shares.
Parameters
Re-exported from alg-field: the curve's field parameters — Parameters.p (the base field prime) and Parameters.n (the curve/group order).
Security notes
Key generation. Up to and including 0.13.22, new BLSSecretKey() drew a single random byte, so there were only 256 possible keys — an attacker could derive the public key for every candidate and recover any secret instantly. Since 0.13.23 keys are drawn uniformly from [1, order-1] (~254 bits), as are the sharing polynomial's coefficients. If you generated keys with an earlier version, treat them as compromised and reissue them.
Message encoding. getRandomPointOnEt() is a convenience for demos and tests, not a hash-to-curve. Mapping a message to a curve point safely requires a proper hash-to-curve construction (RFC 9380); this package does not provide one, so signatures over attacker-influenced messages are not covered by the usual security argument.
No encryption. BLS is a signature scheme: it authenticates messages, it does not hide them. There is no encrypt/decrypt here, and the pairing should not be improvised into one.
Not audited. This is a from-scratch implementation maintained for learning and experimentation. It has had no third-party cryptographic review, and the pairing and threshold code have both carried real correctness bugs found only recently. For production use prefer an audited library such as @noble/curves or blst.
The Scheme
e : G1 x G2 -> Fp12 ; ate pairing over BN curve
Q in G1 ; fixed global parameter
H : {str} -> G2
s in Fr: secret key
sQ in G1; public key
s H(m) in G2; signature of m
verify ; e(sQ, H(m)) = e(Q, s H(m))Shamir Secret Sharing and Lagrange Interpolation
Shamir's Secret Sharing is an algorithm in cryptography created by Adi Shamir. It is a form of secret sharing where a secret is divided into parts, giving each participant its own unique part. Some or all of the parts are needed in order to reconstruct the secret.
Counting on all participants to combine the secret might be impractical, so the threshold scheme is sometimes used instead, where any k of the parts is sufficient to reconstruct the original secret.

Interactive demo
Live: wa1one.github.io/bls-sign
demo.html is a self-contained browser playground for the whole API: pick a curve (alt_bn128 or BLS12-381), generate or set a secret key, sign a text message (SHA-256 → G2 point), verify — including against a tampered message or the wrong key — and split the secret into k-of-n shares, then aggregate any subset of partial signatures and see whether the result verifies. Every operation shows its wall-clock time.
To run it locally instead (it loads the locally built browser bundle):
npm install
npm run build
open demo.html # or serve the repo root with any static file serverThe demo is repository-only — it is not part of the published npm package.
Build
npm run buildRun tests
npm run testCurves are compatible with Ethereum.
