Elliptic Curve Pairings: Mathematical Foundations and BLS Signature Implementation

Mamone Tarsia
14 min read

Introduction

Bilinear pairings on elliptic curves revolutionized modern cryptography by enabling novel protocols impossible with traditional discrete logarithm primitives. From identity-based encryption (Boneh-Franklin, 2001) to compact BLS signatures now securing Ethereum 2.0’s proof-of-stake consensus, pairings provide unique mathematical structure exploited across blockchain, zero-knowledge proofs, and post-quantum transitional cryptography.

This article provides rigorous treatment of pairing mathematics, efficient implementation techniques, and security considerations for pairing-based cryptographic systems.

Mathematical Foundations

Elliptic Curve Preliminaries

An elliptic curve E over finite field 𝔽_q is defined by the Weierstrass equation:

E: y² = x³ + ax + b (mod q)

where 4a³ + 27b² ≠ 0 (non-singular condition).

Group structure: Points on E(𝔽_q) form an abelian group under point addition with identity element 𝒪 (point at infinity).

Order: By Hasse’s theorem, the number of points satisfies:

|E(𝔽_q)| = q + 1 - t
where |t| ≤ 2√q

Torsion Points and Embedding Degree

Definition: For prime r dividing |E(𝔽_q)|, the r-torsion subgroup is:

E[r] = {P ∈ E(𝔽̄_q) : [r]P = 𝒪}

Embedding degree: The smallest integer k such that r | (q^k - 1). This determines the smallest extension field 𝔽_{q^k} containing E[r].

Security implication: The discrete logarithm problem (DLP) on E(𝔽_q) can be reduced to DLP in 𝔽_{q^k} via pairing. Security requires k large enough that 𝔽_{q^k} DLP is intractable.

Bilinear Pairings

A pairing is a bilinear map:

e: G₁ × G₂ → G_T

where G₁, G₂ are cyclic groups of prime order r on E, and G_T ⊆ 𝔽*_{q^k}.

Bilinearity: For all P, P’ ∈ G₁, Q, Q’ ∈ G₂, and a, b ∈ ℤ:

e([a]P, [b]Q) = e(P, Q)^{ab}
e(P + P', Q) = e(P, Q) · e(P', Q)
e(P, Q + Q') = e(P, Q) · e(P, Q')

Non-degeneracy: If e(P, Q) = 1 for all Q ∈ G₂, then P = 𝒪.

Pairing types:

  1. Type-1 (symmetric): G₁ = G₂
  2. Type-2 (asymmetric): G₁ ≠ G₂, efficient homomorphism φ: G₂ → G₁
  3. Type-3 (asymmetric): G₁ ≠ G₂, no efficient homomorphism

Modern protocols prefer Type-3 for optimal performance.

Weil and Tate Pairings

Weil Pairing

Definition: For points P, Q ∈ E[r], let f_P, f_Q be functions with divisors:

div(f_P) = r(P) - r(𝒪)
div(f_Q) = r(Q) - r(𝒪)

The Weil pairing is:

e_r(P, Q) = (-1)^r · f_P(Q + S) / f_P(S) · f_Q(S) / f_Q(P + S)

for random S ∈ E(𝔽_{q^k}).

Properties:

  • Bilinearity: e_r([a]P, [b]Q) = e_r(P, Q)^{ab}
  • Alternating: e_r(P, P) = 1
  • Non-degenerate: e_r(P, Q) is a primitive r-th root of unity

Tate Pairing

Definition: For P ∈ E[r] and Q ∈ E(𝔽_{q^k}):

t_r(P, Q) = f_P(Q)^{(q^k - 1)/r}

where f_P has divisor div(f_P) = r(P) - r(𝒪).

Advantages over Weil:

  • ~2× faster (single Miller loop vs. two)
  • No point subtraction required
  • Preferred for implementations

Reduced Tate pairing (ate pairing): Further optimization using Frobenius endomorphism.

Miller’s Algorithm

The core computational primitive for pairings is Miller’s algorithm, computing rational functions f_P efficiently.

Algorithm Derivation

Given point P of order r, construct f_P via double-and-add:

f_P = ∏_{i=0}^{log₂(r)-1} l_{iP, iP} / v_{2iP}  if bit i of r is 0
                          · l_{T, P} / v_{T+P}    if bit i of r is 1

where:

  • l_{A, B}: Line through A and B
  • v_C: Vertical line through C

Python Implementation

def miller_algorithm(P, Q, r, E):
    """
    Compute f_r,P(Q) using Miller's algorithm

    Args:
        P: Point in G₁
        Q: Point in G₂
        r: Order (scalar)
        E: Elliptic curve

    Returns:
        f_r,P(Q) ∈ 𝔽_{q^k}
    """
    T = P
    f = E.base_field().one()

    # Binary expansion of r
    r_bits = bin(r)[2:]  # Remove '0b' prefix

    for i in range(1, len(r_bits)):
        # Line through T and T (tangent)
        l = line_function(T, T, Q, E)
        # Vertical through 2T
        v = vertical_function(2*T, Q, E)

        # Update f: f ← f² · (l / v)
        f = f**2 * l / v

        # Double T
        T = 2*T

        if r_bits[i] == '1':
            # Line through T and P
            l = line_function(T, P, Q, E)
            # Vertical through T + P
            v = vertical_function(T + P, Q, E)

            # Update f: f ← f · (l / v)
            f = f * l / v

            # Add P
            T = T + P

    return f

def line_function(P, Q, R, E):
    """
    Evaluate line through P and Q at point R

    Returns:
        l_{P,Q}(R)
    """
    if P == Q:
        # Tangent line: y - y_P = λ(x - x_P)
        # where λ = (3x_P² + a) / (2y_P)
        x_P, y_P = P.xy()
        a = E.a4()

        λ = (3 * x_P**2 + a) / (2 * y_P)
    else:
        # Secant line: λ = (y_Q - y_P) / (x_Q - x_P)
        x_P, y_P = P.xy()
        x_Q, y_Q = Q.xy()

        λ = (y_Q - y_P) / (x_Q - x_P)

    # Evaluate at R: l(R) = y_R - y_P - λ(x_R - x_P)
    x_R, y_R = R.xy()
    return y_R - y_P - λ * (x_R - x_P)

def vertical_function(P, R, E):
    """
    Evaluate vertical line through P at point R

    Returns:
        v_P(R) = x_R - x_P
    """
    x_P, _ = P.xy()
    x_R, _ = R.xy()
    return x_R - x_P

Complexity: O(log r) curve operations and field multiplications.

Optimizations

  1. Sparse multiplication: Line evaluation produces sparse elements in 𝔽_{q^k}
  2. Denominator elimination: For well-chosen curves, vertical lines can be omitted
  3. Twisted curves: Map G₂ to E’(𝔽_{q^{k/d}}) for smaller field operations
  4. Precomputation: Cache line parameters for fixed P

BLS12-381: Pairing-Friendly Curve Construction

Design Criteria

Barreto-Lynn-Scott (BLS) curves optimize pairing-friendly parameters:

BLS12-381 parameters:

  • Field modulus (381 bits):

    q = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
  • Curve equation: E: y² = x³ + 4 over 𝔽_q

  • Embedding degree: k = 12

  • Subgroup order (255 bits):

    r = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001
  • Cofactor: h = 0x396c8c005555e1568c00aaab0000aaab

Security level: ~128-bit (comparable to AES-128)

Subgroup Construction

G₁: Prime-order subgroup of E(𝔽_q)

  • Points: Compressed 48-byte representation
  • Operations: Standard elliptic curve arithmetic in 𝔽_q

G₂: Prime-order subgroup of E’(𝔽_{q²}) (sextic twist)

  • Twist curve: E’: y² = x³ + 4(1 + i)
  • Points: Compressed 96-byte representation
  • Operations: Arithmetic in 𝔽_{q²}

G_T: Subgroup of 𝔽*_{q^{12}}

  • Elements: 576-byte representation (can be compressed)
  • Operations: Multiplication in extension field

Twisted Curve Mathematics

Using sextic twist reduces G₂ operations to 𝔽_{q²} instead of 𝔽_{q^{12}}:

# Define 𝔽_{q²} = 𝔽_q[i] / (i² + 1)
F_q2 = GF(q^2, modulus=x^2 + 1, name='i')

# Twisted curve E' over 𝔽_{q²}
E_twist = EllipticCurve(F_q2, [0, 4*(1 + F_q2.gen())])

# Isomorphism ψ: E'(𝔽_{q²}) → E(𝔽_{q^{12}})
def twist_isomorphism(P):
    """
    Map point from twist curve to base curve
    """
    x, y = P.xy()
    # ψ(x, y) = (x · w², y · w³) where w ∈ 𝔽_{q^{12}}
    # (implementation details omitted for brevity)
    return E_base_12([x * w**2, y * w**3])

BLS Signature Scheme

Construction

BLS (Boneh-Lynn-Shacham) signatures exploit pairing bilinearity for compact, aggregatable signatures.

Setup:

  • Public parameters: (G₁, G₂, G_T, e, P, H)
    • P: Generator of G₁
    • H: Hash function H: {0,1}* → G₁

Key generation:

def keygen():
    """
    Generate BLS keypair

    Returns:
        (sk, pk): Secret key sk ∈ ℤ_r, public key pk ∈ G₂
    """
    # Secret key: random scalar
    sk = random_scalar(r)

    # Public key: pk = [sk]P₂ where P₂ is generator of G₂
    pk = sk * P2

    return (sk, pk)

Signing:

def sign(sk, message):
    """
    Sign message

    Args:
        sk: Secret key
        message: Message bytes

    Returns:
        σ ∈ G₁: Signature
    """
    # Hash message to curve point
    H_m = hash_to_curve_G1(message)

    # Signature: σ = [sk]H(m)
    σ = sk * H_m

    return σ

Verification:

def verify(pk, message, σ):
    """
    Verify BLS signature

    Args:
        pk: Public key (G₂ element)
        message: Message bytes
        σ: Signature (G₁ element)

    Returns:
        bool: True if signature valid
    """
    # Hash message to curve
    H_m = hash_to_curve_G1(message)

    # Check pairing equation: e(σ, P₂) = e(H(m), pk)
    # Equivalently: e(σ, P₂) · e(H(m), -pk) = 1
    lhs = pairing(σ, P2)
    rhs = pairing(H_m, pk)

    return lhs == rhs

Correctness:

e(σ, P₂) = e([sk]H(m), P₂)
         = e(H(m), P₂)^{sk}      (bilinearity)
         = e(H(m), [sk]P₂)       (bilinearity)
         = e(H(m), pk)           (by definition)

Signature Aggregation

Multiple signatures on different messages can be aggregated:

def aggregate_signatures(signatures):
    """
    Aggregate multiple signatures

    Args:
        signatures: List of σ_i ∈ G₁

    Returns:
        σ_agg: Aggregated signature
    """
    # Simply sum the signatures
    σ_agg = sum(signatures)
    return σ_agg

def verify_aggregate(public_keys, messages, σ_agg):
    """
    Verify aggregated signature

    Args:
        public_keys: List of pk_i ∈ G₂
        messages: List of messages m_i
        σ_agg: Aggregated signature

    Returns:
        bool: True if valid
    """
    # Check: e(σ_agg, P₂) = ∏ᵢ e(H(mᵢ), pkᵢ)

    # Left side
    lhs = pairing(σ_agg, P2)

    # Right side: product of pairings
    rhs = G_T.one()
    for pk, m in zip(public_keys, messages):
        H_m = hash_to_curve_G1(m)
        rhs *= pairing(H_m, pk)

    return lhs == rhs

Advantage: n signatures → 1 signature (48 bytes instead of 48n bytes)

Application: Ethereum 2.0 uses BLS aggregation for validator attestations, reducing bandwidth by >95%.

Hash-to-Curve

Secure BLS signatures require deterministic, uniform hashing to G₁:

def hash_to_curve_G1(message):
    """
    Hash message to elliptic curve point (RFC 9380)

    Args:
        message: Arbitrary bytes

    Returns:
        Point in G₁
    """
    # Domain separation tag
    DST = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_"

    # Expand message to field elements
    u = hash_to_field(message, count=2, DST=DST)

    # Map to curve using Simplified SWU
    Q0 = map_to_curve_SSWU(u[0])
    Q1 = map_to_curve_SSWU(u[1])

    # Sum and clear cofactor
    P = clear_cofactor(Q0 + Q1)

    return P

def map_to_curve_SSWU(u):
    """
    Simplified Shallue-van de Woestijne-Ulas (SSWU) mapping

    Maps field element to curve point
    """
    # Constants for BLS12-381
    A = 0x144698a3b8e9433d693a02c96d4982b0ea985383ee66a8d8e8981aefd881ac98936f8da0e0f97f5cf428082d584c1d
    B = 0x12e2908d11688030018b12e8753eee3b2016c1f0f24f4070a0b9c14fcef35ef55a23215a316ceaa5d1cc48e98e172be0

    # SSWU algorithm (simplified):
    # 1. tv1 ← u²
    # 2. tv3 ← Z · tv1
    # ... (complete algorithm per RFC 9380)

    # Returns point on E(𝔽_q)
    return (x, y)

Security: RFC 9380 specifies “hash_to_curve” to prevent related-message attacks and ensure indifferentiability from random oracle.

Security Analysis

Computational Assumptions

BLS security relies on:

1. Computational Co-Diffie-Hellman (Co-CDH): Given (P, [a]P, [b]Q) ∈ G₁ × G₁ × G₂, compute [ab]P ∈ G₁.

2. Decisional Bilinear Diffie-Hellman (DBDH): Distinguish (P, [a]P, [b]P, [c]P, e(P, P)^{abc}) from (P, [a]P, [b]P, [c]P, e(P, P)^z) for random z.

BLS signature security:

  • Existential unforgeability under Co-CDH in random oracle model
  • Aggregation security requires distinct messages (prevented by message augmentation)

Rogue Public Key Attack

Threat: Attacker chooses pk* = [s]P₂ - ∑ pkᵢ to forge aggregate signature.

Mitigation:

  1. Proof of possession: Require signature on pk before accepting
  2. Message augmentation: Hash pk with message: σ = [sk]H(pk || m)
  3. Distinct messages: Prevent aggregating signatures on same message

Pairing Inversions and Small Subgroup Attacks

Vulnerability: If point has small-order component, pairing may leak information.

Defense:

  • Subgroup checks: Verify [r]P = 𝒪 for all inputs
  • Cofactor multiplication: Multiply by cofactor h to clear small components
def is_in_G1(P):
    """
    Verify point is in prime-order subgroup G₁
    """
    # Check 1: Point on curve
    if not E.is_on_curve(P):
        return False

    # Check 2: Prime order
    if r * P != E.point_at_infinity():
        return False

    return True

Pairing-Based Cryptographic Protocols

Identity-Based Encryption (IBE)

Boneh-Franklin IBE eliminates public key certificates by deriving public keys from identities.

Setup:

def ibe_setup():
    """
    Generate IBE system parameters

    Returns:
        (params, msk): Public params and master secret key
    """
    # Master secret key
    msk = random_scalar(r)

    # Public parameters
    P_pub = msk * P2  # Master public key
    params = (P, P_pub, H1, H2)  # H1: {0,1}* → G₁, H2: G_T → {0,1}^n

    return (params, msk)

Key extraction:

def ibe_extract(params, msk, identity):
    """
    Extract private key for identity

    Args:
        identity: Email, domain, etc.

    Returns:
        sk_id ∈ G₁
    """
    # Hash identity to curve
    Q_id = H1(identity)

    # Private key: sk_id = [msk]Q_id
    sk_id = msk * Q_id

    return sk_id

Encryption:

def ibe_encrypt(params, identity, message):
    """
    Encrypt message for identity
    """
    P, P_pub, H1, H2 = params

    # Hash identity
    Q_id = H1(identity)

    # Random scalar
    σ = random_scalar(r)

    # Compute pairing: g_id = e(Q_id, P_pub)
    g_id = pairing(Q_id, P_pub)

    # Ciphertext
    C1 = σ * P
    C2 = message ⊕ H2(g_id^σ)

    return (C1, C2)

Decryption:

def ibe_decrypt(params, sk_id, ciphertext):
    """
    Decrypt ciphertext with identity private key
    """
    C1, C2 = ciphertext

    # Compute e(sk_id, C1) = e([msk]Q_id, [σ]P)
    #                       = e(Q_id, P)^{msk·σ}
    #                       = e(Q_id, [msk]P)^σ
    #                       = e(Q_id, P_pub)^σ
    #                       = g_id^σ
    shared_secret = pairing(sk_id, C1)

    # Decrypt
    message = C2 ⊕ H2(shared_secret)

    return message

Short Randomizable Signatures

BLS enables efficient blind signatures and threshold signatures:

Threshold BLS (t-of-n):

def threshold_keygen(t, n):
    """
    Generate threshold BLS keys

    Args:
        t: Threshold
        n: Total participants

    Returns:
        shares: Key shares for each participant
    """
    # Master secret polynomial: s(x) = s₀ + s₁x + ... + s_{t-1}x^{t-1}
    coeffs = [random_scalar(r) for _ in range(t)]
    s_0 = coeffs[0]  # Master secret key

    # Evaluate polynomial at participant indices
    shares = []
    for i in range(1, n+1):
        share = sum(coeffs[j] * (i**j) for j in range(t)) % r
        shares.append((i, share))

    # Master public key
    pk = s_0 * P2

    return (shares, pk)

def threshold_sign(share, message):
    """
    Generate signature share
    """
    i, sk_i = share
    H_m = hash_to_curve_G1(message)
    σ_i = sk_i * H_m
    return (i, σ_i)

def threshold_combine(sig_shares, t):
    """
    Combine t signature shares into full signature
    """
    # Lagrange interpolation at x=0
    indices, sigs = zip(*sig_shares[:t])

    σ = G1.identity()
    for j in range(t):
        # Lagrange coefficient
        λ_j = 1
        for m in range(t):
            if m != j:
                λ_j *= indices[m] / (indices[m] - indices[j])

        σ += int(λ_j) * sigs[j]

    return σ

Implementation Considerations

Constant-Time Operations

Prevent timing attacks in pairing computations:

def constant_time_pairing(P, Q):
    """
    Constant-time Miller algorithm

    - No secret-dependent branches
    - Uniform memory access patterns
    - Constant-time field arithmetic
    """
    # Ensure all code paths take same time
    # regardless of secret values
    # ... (implementation uses dummy operations)

Performance Optimization

Benchmark (BLS12-381 on Intel i7-10710U):

OperationTimeNotes
G₁ scalar mult50 μsVariable-base
G₂ scalar mult180 μsVariable-base
Pairing (single)1.2 msFull Miller + final exp
Pairing (batch 10)8.5 msAmortized 0.85 ms each
Signature verify1.7 ms2 pairings
Aggregate verify (10 sigs)11 msMulti-pairing optimization

Optimizations:

  1. Multi-pairing: Compute ∏ e(Pᵢ, Qᵢ) faster than individual pairings
  2. Precomputation: Cache Miller loop values for fixed points
  3. Batch verification: Use randomized checks for multiple signatures
  4. Assembly: Hand-optimize field arithmetic (10×+ speedup)

Comparison with Other Signature Schemes

SchemeSignature SizeVerificationAggregationSecurity Basis
BLS48 bytes2 pairingsYes (linear)Co-CDH
ECDSA64 bytes1 scalar multNoECDLP
EdDSA64 bytes1 double-base multPartialECDLP
RSA-2048256 bytesFast (e=65537)NoFactoring
Schnorr64 bytes1 double-base multYes (interactive)ECDLP

BLS advantages:

  • Smallest signatures
  • Non-interactive aggregation
  • Deterministic (no nonce vulnerabilities)

BLS disadvantages:

  • Slower verification (~10× ECDSA)
  • Larger public keys (96 bytes vs. 33 for ECDSA)
  • Requires pairing-friendly curves

Conclusion

Elliptic curve pairings provide powerful cryptographic primitives enabling protocols impossible with traditional discrete logarithm systems. BLS signatures, leveraging bilinear pairings on BLS12-381, achieve unmatched compactness and aggregation properties critical for blockchain scalability (Ethereum 2.0) and distributed systems.

Understanding pairing mathematics—from Weil/Tate definitions through Miller’s algorithm to secure hash-to-curve—is essential for implementing pairing-based cryptography securely and efficiently. As post-quantum migration continues, pairings remain relevant for hybrid constructions and transitional protocols.

References

  1. Boneh, D., Lynn, B., & Shacham, H. (2001). “Short signatures from the Weil pairing.” ASIACRYPT 2001.
  2. Boneh, D., & Franklin, M. (2001). “Identity-based encryption from the Weil pairing.” CRYPTO 2001.
  3. Barreto, P., & Naehrig, M. (2006). “Pairing-friendly elliptic curves of prime order.” SAC 2005.
  4. Bowe, S. (2017). “BLS12-381: New zk-SNARK Elliptic Curve Construction.” Zcash blog.
  5. Miller, V. (1986). “The Weil pairing, and its efficient calculation.” Journal of Cryptology.
  6. Faz-Hernández, A., et al. (2023). “Hashing to Elliptic Curves.” RFC 9380.
  7. Silverman, J. (2009). “The Arithmetic of Elliptic Curves.” Springer GTM 106.
  8. Galbraith, S., Paterson, K., & Smart, N. (2008). “Pairings for cryptographers.” Discrete Applied Mathematics.