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 Scalar zeroizes the moved-from value.

static Scalar zero()
static Scalar one()
static Scalar from_u32(uint32_t v)
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_xmd with SHA-256 followed by wide reduction). Useful for example for deriving Fiat-Shamir challenges. The domain separation tag dst distinguishes different uses of the hash; see RFC 9380 section 3.1 for the requirements. An empty tag is rejected with Invalid_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, negate and invert (the inverse of zero is zero), as well as the usual overloaded operators.

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 G1Affine generator()
static G1Affine identity()
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
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)
G1Affine to_affine() const
static std::vector<G1Affine> to_affine_batch(std::span<const G1Projective> points)

Equivalent to calling to_affine on 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*q in constant time, sharing one doubling chain; notably faster than composing mul and add. 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 than mul2.

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 tag dst distinguishes different uses of the hash; see RFC 9380 section 3.1 for the requirements. An empty tag is rejected with Invalid_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 suite BLS12381G1_XMD:SHA-256_SSWU_NU_. Faster, but the output is distinguishable from a uniformly random group element; use hash_to_curve_ro unless the protocol explicitly calls for the nonuniform encoding.

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_ and BLS12381G2_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.

static Gt identity()
bool is_identity() const
bool operator==(const Gt &other) 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. Throws Invalid_Argument if the spans have different lengths.

Pairing equations of the form e(a, b) == e(c, d) should be checked by testing multi_pairing({a, -c}, {b, d}).is_identity(), which is both faster and the standard formulation.

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;
}