TKK_E32232028/.venv/lib/python3.10/site-packages/lightecc/commons/pairing.py

458 lines
13 KiB
Python

"""
Pairing implementations using Miller's algorithm for Weierstrass curves.
Supports:
- Weil pairing over F_p (for curves where full r-torsion is in E(F_p))
- Modified Tate pairing with distortion map over F_{p^2}
(for supersingular curves y^2 = x^3 + ax with embedding degree 2)
The pairing e_r(P, Q) maps two r-torsion points P, Q on an elliptic curve
to an r-th root of unity.
Properties:
- Bilinear: e(aP, bQ) = e(P, Q)^(ab)
- Alternating: e(P, P) = 1
- Non-degenerate: if e(P, Q) = 1 for all Q, then P = O
Reference: Washington, "Elliptic Curves: Number Theory and Cryptography", Chapter 11
"""
from typing import Tuple, Union
from lightecc.interfaces.elliptic_curve import EllipticCurvePoint
from lightecc.commons.logger import Logger
logger = Logger(module="lightecc/pairing.py")
# ===========================================================================
# F_{p^2} arithmetic: elements are (a, b) representing a + b*i where i^2 = -1
# Used for supersingular curves with embedding degree 2 and p ≡ 3 (mod 4)
# ===========================================================================
def _fp2_add(x, y, p):
"""Add two F_{p^2} elements."""
return ((x[0] + y[0]) % p, (x[1] + y[1]) % p)
def _fp2_sub(x, y, p):
"""Subtract two F_{p^2} elements."""
return ((x[0] - y[0]) % p, (x[1] - y[1]) % p)
def _fp2_mul(x, y, p):
"""Multiply two F_{p^2} elements. (a+bi)(c+di) = (ac-bd) + (ad+bc)i"""
a, b = x
c, d = y
return ((a * c - b * d) % p, (a * d + b * c) % p)
def _fp2_inv(x, p):
"""Invert an F_{p^2} element. (a+bi)^{-1} = (a-bi)/(a^2+b^2)"""
a, b = x
norm = (a * a + b * b) % p
if norm == 0:
raise ZeroDivisionError("Cannot invert zero in F_{p^2}")
norm_inv = pow(norm, -1, p)
return ((a * norm_inv) % p, ((-b) * norm_inv) % p)
def _fp2_pow(x, n, p):
"""Exponentiate an F_{p^2} element by a non-negative integer n."""
if n == 0:
return (1, 0)
if n < 0:
x = _fp2_inv(x, p)
n = -n
result = (1, 0)
base = x
while n > 0:
if n & 1:
result = _fp2_mul(result, base, p)
base = _fp2_mul(base, base, p)
n >>= 1
return result
# ===========================================================================
# Supersingular curve detection
# ===========================================================================
def _is_supersingular_embedding2(curve) -> bool:
"""
Check if a Weierstrass curve y^2 = x^3 + ax + b is supersingular
with embedding degree 2.
For y^2 = x^3 + ax (b=0, a!=0) with p ≡ 3 (mod 4), the curve is
supersingular with embedding degree 2, meaning p^2 ≡ 1 (mod r)
but p ≢ 1 (mod r) for the relevant torsion orders.
"""
return curve.b == 0 and curve.a != 0 and curve.modulo % 4 == 3
# ===========================================================================
# Standard Weil pairing (F_p) — for curves with full torsion in E(F_p)
# ===========================================================================
def _is_identity(P: EllipticCurvePoint) -> bool:
"""Check if a point is the identity element (point at infinity)."""
return P.x == P.curve.O[0] and P.y == P.curve.O[1]
def _line_eval(
T: EllipticCurvePoint, P: EllipticCurvePoint, R: EllipticCurvePoint
) -> int:
"""
Evaluate the line function g_{T,P}(R) used in Miller's algorithm.
g_{T,P} = l_{T,P} / v_{T+P} where:
l_{T,P} is the line through T and P (tangent if T == P)
v_{T+P} is the vertical line through the sum T + P
Args:
T: first point on the line
P: second point on the line (same as T for tangent/doubling)
R: evaluation point
Returns:
value of g_{T,P}(R) in F_p
"""
if _is_identity(T) or _is_identity(P) or _is_identity(R):
return 1
a = T.curve.a
p = T.curve.modulo
xT, yT = T.x, T.y
xP, yP = P.x, P.y
xR, yR = R.x, R.y
# compute T + P using the library's point arithmetic
sum_point = T + P
if _is_identity(sum_point):
# T + P = O means vertical line (tangent at 2-torsion or T = -P)
return (xR - xT) % p
# slope of line through T and P (tangent if T == P, secant otherwise)
if T == P:
lam = (3 * xT * xT + a) * pow(2 * yT, -1, p) % p
else:
lam = (yP - yT) * pow(xP - xT, -1, p) % p
# Line l evaluated at R: yR - yT - lam * (xR - xT)
num = (yR - yT - lam * (xR - xT)) % p
# Vertical line v through T+P evaluated at R: xR - x_{T+P}
den = (xR - sum_point.x) % p
if den == 0:
return num if num != 0 else 1
return num * pow(den, -1, p) % p
def _miller(P: EllipticCurvePoint, R: EllipticCurvePoint, r: int) -> int:
"""
Miller's algorithm to compute f_{r,P}(R) over F_p.
Args:
P: base point (r-torsion)
R: evaluation point
r: torsion order
Returns:
f_{r,P}(R) in F_p
"""
p = P.curve.modulo
if _is_identity(P) or _is_identity(R):
return 1
f = 1
T = P
bits = bin(r)[2:]
for i in range(1, len(bits)):
# Doubling step
if _is_identity(T):
f = (f * f) % p
else:
g = _line_eval(T, T, R)
f = (f * f * g) % p
T = T + T
if bits[i] == "1":
# Addition step
if _is_identity(T):
T = P
else:
g = _line_eval(T, P, R)
f = (f * g) % p
T = T + P
return f % p
def _find_auxiliary_point(
P: EllipticCurvePoint, Q: EllipticCurvePoint
) -> EllipticCurvePoint:
"""
Find an auxiliary point S on the curve for the Weil pairing computation.
S must avoid degeneracies: S should not be in {O, P, -P, Q, -Q, P-Q, Q-P, P+Q}
and the derived points Q+S, -S, P-S must also not be O.
Args:
P: first pairing input point
Q: second pairing input point
Returns:
a safe auxiliary point S
"""
curve = P.curve
G = EllipticCurvePoint(curve.G[0], curve.G[1], curve)
bad_points = [P, -P, Q, -Q, P - Q, Q - P, P + Q]
for k in range(1, min(curve.n, 1000)):
S = k * G
if _is_identity(S):
continue
if any(S == bp for bp in bad_points):
continue
QpS = Q + S
PmS = P - S
if _is_identity(QpS) or _is_identity(PmS):
continue
return S
raise ValueError(
"Could not find a suitable auxiliary point for the Weil pairing. "
"The curve may have too few points."
)
# ===========================================================================
# Modified Tate pairing with distortion map (F_{p^2})
# For supersingular curves y^2 = x^3 + ax with embedding degree 2
# ===========================================================================
def _line_eval_fp2(T: EllipticCurvePoint, P: EllipticCurvePoint, xR, yR, p, a):
"""
Evaluate the line function g_{T,P} at a point R = (xR, yR) in F_{p^2}.
T and P are points on E(F_p). R is a point in E(F_{p^2}) given by
its coordinates as F_{p^2} elements (tuples).
Returns an F_{p^2} element (tuple).
"""
if _is_identity(T) or _is_identity(P):
return (1, 0)
xT, yT = T.x, T.y
xP, yP = P.x, P.y
sum_point = T + P
if _is_identity(sum_point):
# Vertical line: xR - xT
return _fp2_sub(xR, (xT, 0), p)
# Slope lambda (in F_p, embedded as (lam, 0))
if T == P:
lam = (3 * xT * xT + a) * pow(2 * yT, -1, p) % p
else:
lam = (yP - yT) * pow(xP - xT, -1, p) % p
# Numerator: yR - yT - lam * (xR - xT)
num = _fp2_sub(
_fp2_sub(yR, (yT, 0), p),
_fp2_mul((lam, 0), _fp2_sub(xR, (xT, 0), p), p),
p,
)
# Denominator: xR - x_{T+P}
den = _fp2_sub(xR, (sum_point.x, 0), p)
if den == (0, 0):
return num if num != (0, 0) else (1, 0)
return _fp2_mul(num, _fp2_inv(den, p), p)
def _miller_fp2(P: EllipticCurvePoint, xR, yR, r: int, p: int, a: int):
"""
Miller's algorithm computing f_{r,P}(R) where R is in E(F_{p^2}).
P is a point on E(F_p). R = (xR, yR) with coordinates in F_{p^2}.
Returns an F_{p^2} element.
"""
if _is_identity(P):
return (1, 0)
f = (1, 0)
T = P
bits = bin(r)[2:]
for i in range(1, len(bits)):
# Doubling step
if _is_identity(T):
f = _fp2_mul(f, f, p)
else:
g = _line_eval_fp2(T, T, xR, yR, p, a)
f = _fp2_mul(_fp2_mul(f, f, p), g, p)
T = T + T
if bits[i] == "1":
# Addition step
if _is_identity(T):
T = P
else:
g = _line_eval_fp2(T, P, xR, yR, p, a)
f = _fp2_mul(f, g, p)
T = T + P
return f
def _tate_pairing_supersingular(
P: EllipticCurvePoint, Q: EllipticCurvePoint, r: int
):
"""
Modified Tate pairing for supersingular curves y^2 = x^3 + ax
with embedding degree 2, using the distortion map phi(x, y) = (-x, i*y).
Computes e_r(P, Q) = f_{r,P}(phi(Q))^{(p^2-1)/r} in F_{p^2}.
Args:
P: first r-torsion point on E(F_p)
Q: second r-torsion point on E(F_p)
r: torsion order
Returns:
An F_{p^2} element (a, b) representing a + b*i, which is an r-th root of unity.
"""
p = P.curve.modulo
a = P.curve.a
# Apply distortion map: phi(Q) = (-x_Q, i * y_Q)
# In F_{p^2}: x_phi = (-x_Q mod p, 0), y_phi = (0, y_Q)
x_phi = ((-Q.x) % p, 0)
y_phi = (0, Q.y % p)
# Run Miller's algorithm over F_{p^2}
f = _miller_fp2(P, x_phi, y_phi, r, p, a)
# Final exponentiation: f^{(p^2 - 1) / r}
exp = (p * p - 1) // r
result = _fp2_pow(f, exp, p)
return result
# ===========================================================================
# Public API
# ===========================================================================
def weil_pairing(
P: EllipticCurvePoint, Q: EllipticCurvePoint, r: int
) -> Union[int, Tuple[int, int]]:
"""
Compute the pairing e_r(P, Q).
For supersingular curves with embedding degree 2 (y^2 = x^3 + ax, p ≡ 3 mod 4),
automatically uses the modified Tate pairing with distortion map over F_{p^2}.
For other curves, uses the standard Weil pairing over F_p.
Args:
P: first r-torsion point
Q: second r-torsion point
r: torsion order (r*P = O and r*Q = O must hold)
Returns:
int: an r-th root of unity in F_p (standard curves)
tuple[int, int]: an element (a, b) representing a + b*i
in F_{p^2} (supersingular curves with embedding degree 2)
"""
p = P.curve.modulo
if _is_identity(P) or _is_identity(Q):
return 1
# Use modified Tate pairing for supersingular curves with embedding degree 2,
# but only when the full r-torsion is NOT in E(F_p).
# If r | (p-1), the full r-torsion is in E(F_p) and the standard Weil pairing works.
# Note: P == Q is allowed here because the distortion map phi(Q) != Q,
# so e(P, phi(P)) is non-trivial (the alternating property does not apply).
if _is_supersingular_embedding2(P.curve) and (p - 1) % r != 0:
# Verify P and Q are r-torsion points
rP = r * P
if not _is_identity(rP):
raise ValueError(
f"P = ({P.x}, {P.y}) is not an {r}-torsion point"
f" (r*P = ({rP.x}, {rP.y}))"
)
if P != Q:
rQ = r * Q
if not _is_identity(rQ):
raise ValueError(
f"Q = ({Q.x}, {Q.y}) is not an {r}-torsion point"
f" (r*Q = ({rQ.x}, {rQ.y}))"
)
return _tate_pairing_supersingular(P, Q, r)
if P == Q:
return 1
# Verify P and Q are r-torsion points
rP = r * P
rQ = r * Q
if not _is_identity(rP):
raise ValueError(
f"P = ({P.x}, {P.y}) is not an {r}-torsion point"
f" (r*P = ({rP.x}, {rP.y}))"
)
if not _is_identity(rQ):
raise ValueError(
f"Q = ({Q.x}, {Q.y}) is not an {r}-torsion point"
f" (r*Q = ({rQ.x}, {rQ.y}))"
)
# Standard Weil pairing for curves with full torsion in E(F_p)
S = _find_auxiliary_point(P, Q)
nS = -S
QpS = Q + S
PmS = P - S
fP_QpS = _miller(P, QpS, r)
fP_S = _miller(P, S, r)
fQ_PmS = _miller(Q, PmS, r)
fQ_nS = _miller(Q, nS, r)
if fP_S == 0 or fQ_PmS == 0:
raise ValueError(
"Degenerate case in Weil pairing computation. Try different input points."
)
num = (fP_QpS * fQ_nS) % p
den = (fP_S * fQ_PmS) % p
if den == 0:
raise ValueError("Degenerate case: denominator is zero in Weil pairing.")
return num * pow(den, -1, p) % p