BLS12-381¶
Added in version 3.14.0.
BLS12-381 is a pairing-friendly elliptic curve widely used for BLS signatures, verifiable random functions, threshold cryptography, and zero knowledge proof systems.
Warning
This is a low level mathematical primitive intended for implementing higher level protocols. It does not by itself implement any signature or encryption scheme. Protocol level requirements, such as rejecting identity public keys or defending against rogue key attacks in aggregate settings, remain the caller’s responsibility.
The interface consists of value types for the scalar field, the two source groups G1 and
G2, and the pairing target group Gt, all declared in bls12_381.h within namespace
Botan::BLS12_381. The base field and extension tower arithmetic is internal to the
library.
All operations are constant time unless otherwise documented; in particular scalar
multiplication via mul is safe for secret scalars, and hash-to-curve is safe for
secret inputs. Operations documented as variable time (such as msm_vartime), the
pairing, and deserialization assume their inputs are public values, which is the case
in the standard usages (signature verification, decoding transmitted points).
Serialization Formats¶
Group elements use the ZCash compressed point encoding: 48 bytes for G1, 96 bytes for G2.
The three high bits of the first byte are flag bits indicating (in order) compressed
encoding, the point at infinity, and the lexicographically larger of the two possible y
coordinates. The x coordinate follows as a big-endian integer; for G2 the c1
coefficient of the extension field element is encoded first. Deserialization rejects
any encoding that is non-canonical, not on the curve, or outside the prime order
subgroup. The canonical encoding of the identity element is accepted; protocols for
which the identity is not a legal value must check for it explicitly.
Scalars are encoded as 32 byte big-endian integers, field elements as 48 byte big-endian integers; deserialization of values not fully reduced modulo the respective prime is rejected.
Note
Some implementations of BLS12-381 default to using little-endian encoding of scalars.
Note
There is no standardized serialization for elements of Gt. Gt::serialize
encodes the twelve field coefficients of the underlying extension field
element in a fixed order. This encoding is stable across versions of the
library but is not guaranteed to match the encoding of any other
implementation. Note also that while all correct implementations will agree
on whether a pairing product equals the identity, the exact value of that
pairing output may vary depending on implementation choices (eg using
an alternative field tower)
Scalars¶
-
class Botan::BLS12_381::Scalar¶
An integer modulo the group order r. Default constructed as zero. Since scalars are commonly secret keys, the value is zeroized on destruction, and moving from a
Scalarzeroizes the moved-from value.-
static std::optional<Scalar> deserialize(std::span<const uint8_t> bytes)¶
Accepts exactly 32 bytes encoding a big-endian integer in the range [1, r). The zero scalar is rejected, since a zero scalar is almost always a mistake.
-
static Scalar from_bytes_wide(std::span<const uint8_t, 64> bytes)¶
Reduces a 64 byte big-endian integer modulo r, suitable for deriving uniformly distributed scalars from an RNG or hash.
-
static Scalar hash(std::span<const uint8_t> input, std::span<const uint8_t> dst)¶
Hash an input to a uniformly distributed scalar, using the hash_to_field construction of RFC 9380 (
expand_message_xmdwith SHA-256 followed by wide reduction). Useful for example for deriving Fiat-Shamir challenges. The domain separation tagdstdistinguishes different uses of the hash; see RFC 9380 section 3.1 for the requirements. An empty tag is rejected withInvalid_Argument; tags longer than 255 bytes are replaced by an intermediate hash, following RFC 9380 section 5.3.3.
-
std::array<uint8_t, 32> serialize() const¶
Arithmetic is available via
add,sub,mul,square,negateandinvert(the inverse of zero is zero), as well as the usual overloaded operators.-
static std::optional<Scalar> deserialize(std::span<const uint8_t> bytes)¶
Group Elements¶
-
class Botan::BLS12_381::G1Affine¶
A point of the prime order group G1 in affine form, used for serialization and as pairing input.
-
static std::optional<G1Affine> deserialize(std::span<const uint8_t> bytes)¶
Only the 48 byte compressed encoding is accepted. Points outside the prime order subgroup are rejected.
-
std::array<uint8_t, 48> serialize() const¶
-
bool is_identity() const¶
-
static std::optional<G1Affine> deserialize(std::span<const uint8_t> bytes)¶
-
class Botan::BLS12_381::G1Projective¶
A point of G1 in projective form, used for group arithmetic. Default constructed as the identity element.
-
static G1Projective generator()¶
-
static G1Projective identity()¶
-
static G1Projective from_affine(const G1Affine &affine)¶
-
static std::vector<G1Affine> to_affine_batch(std::span<const G1Projective> points)¶
Equivalent to calling
to_affineon each point, but much faster, since a single field inversion is shared across the batch; always prefer this when converting more than one point.
-
G1Projective add(const G1Projective &other) const¶
-
G1Projective add_mixed(const G1Affine &other) const¶
-
G1Projective negate() const¶
-
G1Projective mul(const Scalar &scalar) const¶
Constant time scalar multiplication, safe for secret scalars.
-
static G1Projective mul2(const G1Projective &p, const Scalar &a, const G1Projective &q, const Scalar &b)¶
Compute
a*p + b*qin constant time, sharing one doubling chain; notably faster than composingmulandadd. Being constant time it is safe for secret scalars, for example when computing a Pedersen commitment.
-
static G1Projective mul2_vartime(const G1Projective &p, const Scalar &a, const G1Projective &q, const Scalar &b)¶
Compute
a*p + b*q, faster thanmul2.Warning
Runs in variable time and must only be used with public inputs, such as verification equations.
-
static G1Projective msm_vartime(std::span<const G1Affine> points, std::span<const Scalar> scalars)¶
Multiscalar multiplication, returning the sum of
scalars[i] * points[i]. Above a certain number of terms this will use Pippenger’s algorithm. Below that size, instead a chain of 2-ary multiplications will be used. The algorithm choice will in any event always be superior to chaining individual multiplications and additions.Warning
Runs in variable time and must only be used with public inputs.
-
static G1Projective hash_to_curve_ro(std::span<const uint8_t> input, std::span<const uint8_t> dst)¶
Hash to curve following RFC 9380, using the suite
BLS12381G1_XMD:SHA-256_SSWU_RO_. The domain separation tagdstdistinguishes different uses of the hash; see RFC 9380 section 3.1 for the requirements. An empty tag is rejected withInvalid_Argument; tags longer than 255 bytes are replaced by an intermediate hash, following RFC 9380 section 5.3.3.
-
static G1Projective hash_to_curve_nu(std::span<const uint8_t> input, std::span<const uint8_t> dst)¶
The nonuniform (
encode_to_curve) variant, using the suiteBLS12381G1_XMD:SHA-256_SSWU_NU_. Faster, but the output is distinguishable from a uniformly random group element; usehash_to_curve_rounless the protocol explicitly calls for the nonuniform encoding.
-
static G1Projective generator()¶
-
class Botan::BLS12_381::G2Affine¶
-
class Botan::BLS12_381::G2Projective¶
The group G2, defined over the quadratic extension field, with the same interface as the G1 types. Compressed points are 96 bytes, and the hash to curve functions implement the suites
BLS12381G2_XMD:SHA-256_SSWU_RO_andBLS12381G2_XMD:SHA-256_SSWU_NU_.
The Pairing¶
-
class Botan::BLS12_381::Gt¶
An element of the pairing target group, a prime order subgroup of the multiplicative group of Fp12. Elements are created only by the pairing functions; there is no deserialization.
-
bool is_identity() const¶
-
std::array<uint8_t, 576> serialize() const¶
Note that the serialization format of Gt is not standardized for BLS12-381, and the encoding may differ between implementations.
-
static Gt pairing(const G1Affine &p, const G2Affine &q)¶
Compute the optimal ate pairing
e(p, q). If either input is the identity element the result is the identity.
-
Gt multi_pairing(std::span<const G1Affine> p, std::span<const G2Affine> q)¶
Compute the product of pairings
e(p[0], q[0]) * e(p[1], q[1]) * ..., sharing one final exponentiation; always prefer this over multiplying individual pairing results. The empty product is the identity. ThrowsInvalid_Argumentif the spans have different lengths.Pairing equations of the form
e(a, b) == e(c, d)should be checked by testingmulti_pairing({a, -c}, {b, d}).is_identity(), which is both faster and the standard formulation.
-
bool is_identity() const¶
Example¶
The following example implements the operations of a BLS signature (the minimal-pubkey-size variant of draft-irtf-cfrg-bls-signature, with public keys in G1 and signatures in G2): key generation, signing, verification, and signature aggregation.
/*
* BLS signatures built from the low level BLS12-381 interface.
*
* This implements the scheme of draft-irtf-cfrg-bls-signature in the
* minimal-pubkey-size setting (public keys in G1, signatures in G2),
* specifically the "basic" ciphersuite
* BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_.
*
* The basic scheme is only safe for aggregation if all messages in an
* aggregate are distinct; see section 3.1 of the draft. The
* message-augmentation and proof-of-possession variants build on the
* same operations shown here.
*/
#include <botan/auto_rng.h>
#include <botan/bls12_381.h>
#include <algorithm>
#include <iostream>
#include <optional>
#include <span>
#include <string_view>
#include <vector>
namespace {
using namespace Botan::BLS12_381;
const std::string_view BLS_SIG_DST = "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";
std::span<const uint8_t> as_bytes(std::string_view s) {
return {reinterpret_cast<const uint8_t*>(s.data()), s.size()};
}
G2Projective hash_message(std::string_view msg) {
return G2Projective::hash_to_curve_ro(as_bytes(msg), as_bytes(BLS_SIG_DST));
}
class BlsPrivateKey {
public:
/*
* Generate a signing key.
*
* Note that section 2.3 of the draft specifies a deterministic
* HKDF based derivation from a seed; if interoperable key
* derivation matters for your application, implement KeyGen with
* Botan's HKDF. Here fresh randomness is used directly.
*/
static BlsPrivateKey generate(Botan::RandomNumberGenerator& rng) {
for(;;) {
std::array<uint8_t, 64> seed{};
rng.randomize(seed);
const auto sk = Scalar::from_bytes_wide(seed);
// The secret key must be nonzero (probability 2^-255)
if(sk.serialize() != Scalar::zero().serialize()) {
return BlsPrivateKey(sk);
}
}
}
// SkToPk: the public key is sk*g1, serialized to 48 bytes
std::array<uint8_t, G1Affine::BYTES> public_key() const {
return G1Projective::generator().mul(m_sk).to_affine().serialize();
}
// CoreSign: the signature is sk*H(msg), serialized to 96 bytes
std::array<uint8_t, G2Affine::BYTES> sign(std::string_view msg) const {
return hash_message(msg).mul(m_sk).to_affine().serialize();
}
private:
explicit BlsPrivateKey(const Scalar& sk) : m_sk(sk) {}
Scalar m_sk;
};
/*
* CoreVerify: check that e(pk, H(msg)) == e(g1, sig).
*
* Deserialization rejects any point outside the prime order subgroup,
* which the scheme requires. The pairing equation is rearranged as
* e(pk, H(msg)) * e(-g1, sig) == 1 so that a single product of pairings
* (with its one shared final exponentiation) suffices.
*/
bool bls_verify(std::span<const uint8_t> pk_bytes, std::string_view msg, std::span<const uint8_t> sig_bytes) {
const auto pk = G1Affine::deserialize(pk_bytes);
const auto sig = G2Affine::deserialize(sig_bytes);
if(!pk || !sig || pk->is_identity()) {
return false;
}
const auto neg_g1 = G1Projective::generator().negate().to_affine();
const std::vector<G1Affine> ps{*pk, neg_g1};
const std::vector<G2Affine> qs{hash_message(msg).to_affine(), *sig};
return Gt::multi_pairing(ps, qs).is_identity();
}
// Aggregate: signatures combine by group addition in G2
std::optional<std::array<uint8_t, G2Affine::BYTES>> bls_aggregate(
std::span<const std::array<uint8_t, G2Affine::BYTES>> sigs) {
if(sigs.empty()) {
return std::nullopt;
}
auto agg = G2Projective::identity();
for(const auto& sig_bytes : sigs) {
const auto sig = G2Affine::deserialize(sig_bytes);
if(!sig) {
return std::nullopt;
}
agg = agg.add_mixed(*sig);
}
return agg.to_affine().serialize();
}
/*
* AggregateVerify: check e(pk_1, H(msg_1)) * ... * e(pk_n, H(msg_n)) == e(g1, sig)
* via one product of n+1 pairings.
*
* Section 3.1 of the draft: "In a basic scheme, rogue key attacks are
* handled by requiring all messages signed by an aggregate signature
* to be distinct."
*/
bool bls_aggregate_verify(std::span<const std::vector<uint8_t>> pks,
std::span<const std::string_view> msgs,
std::span<const uint8_t> agg_sig_bytes) {
// "Precondition: n >= 1, otherwise return INVALID."
if(pks.size() != msgs.size() || pks.empty()) {
return false;
}
// "If any two input messages are equal, return INVALID."
std::vector<std::string_view> sorted_msgs(msgs.begin(), msgs.end());
std::ranges::sort(sorted_msgs);
if(std::ranges::adjacent_find(sorted_msgs) != sorted_msgs.end()) {
return false;
}
const auto agg_sig = G2Affine::deserialize(agg_sig_bytes);
if(!agg_sig) {
return false;
}
std::vector<G1Affine> ps;
std::vector<G2Projective> hashed;
for(size_t i = 0; i != pks.size(); ++i) {
const auto pk = G1Affine::deserialize(pks[i]);
if(!pk || pk->is_identity()) {
return false;
}
ps.push_back(*pk);
hashed.push_back(hash_message(msgs[i]));
}
ps.push_back(G1Projective::generator().negate().to_affine());
// A single shared inversion converts all the hashed points at once
auto qs = G2Projective::to_affine_batch(hashed);
qs.push_back(*agg_sig);
return Gt::multi_pairing(ps, qs).is_identity();
}
} // namespace
int main() {
Botan::AutoSeeded_RNG rng;
bool all_ok = true;
auto check = [&](const char* what, bool ok, bool expected) {
std::cout << what << ": " << (ok ? "valid" : "invalid") << "\n";
all_ok = all_ok && (ok == expected);
};
// Single signer
const auto key = BlsPrivateKey::generate(rng);
const auto pk = key.public_key();
const std::string_view message = "This is a tasty burger!";
const auto sig = key.sign(message);
check("Signature", bls_verify(pk, message, sig), true);
check("Signature on modified message", bls_verify(pk, "This is a nasty burger!", sig), false);
// Aggregation: three signers, three (distinct!) messages, one signature
const std::vector<std::string_view> messages{
"message to the first signer", "message to the second signer", "message to the third signer"};
std::vector<std::vector<uint8_t>> pks;
std::vector<std::array<uint8_t, G2Affine::BYTES>> sigs;
for(const auto msg : messages) {
const auto k = BlsPrivateKey::generate(rng);
const auto k_pk = k.public_key();
pks.emplace_back(k_pk.begin(), k_pk.end());
sigs.push_back(k.sign(msg));
}
const auto agg_sig = bls_aggregate(sigs);
if(!agg_sig) {
std::cout << "Aggregation failed\n";
return 1;
}
check("Aggregate signature", bls_aggregate_verify(pks, messages, *agg_sig), true);
auto swapped = messages;
std::swap(swapped[0], swapped[1]);
check("Aggregate with swapped messages", bls_aggregate_verify(pks, swapped, *agg_sig), false);
// Even a correctly signed aggregate over repeated messages must be
// rejected in the basic scheme
const std::vector<std::string_view> dup_msgs{"same message", "same message"};
std::vector<std::vector<uint8_t>> dup_pks;
std::vector<std::array<uint8_t, G2Affine::BYTES>> dup_sigs;
for(const auto msg : dup_msgs) {
const auto k = BlsPrivateKey::generate(rng);
const auto k_pk = k.public_key();
dup_pks.emplace_back(k_pk.begin(), k_pk.end());
dup_sigs.push_back(k.sign(msg));
}
const auto dup_agg = bls_aggregate(dup_sigs);
if(!dup_agg) {
std::cout << "Aggregation failed\n";
return 1;
}
check("Aggregate with repeated messages", bls_aggregate_verify(dup_pks, dup_msgs, *dup_agg), false);
return all_ok ? 0 : 1;
}