Algorithm Search⌘K

Weil descent attack in Python

Reduces ECDLP over extension fields of small degree to a hyperelliptic DLP.

File
weil_descent_attack.py, 515 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
weil_descent_attack.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/weil-descent-attack/. The authors are listed in prov/83GQX3.jsonl.

import random
from math import isqrt

# Weil descent for n = 2 in odd characteristic. K = F_(q^2) = F_q[t]/(t^2 - nu)
# with q an odd prime and nu the least non-residue; an element u + v t of K is
# the pair (u, v), and its Frobenius conjugate is (u, -v). The curve
#   E: y^2 = (x^2 - s x + t0)(x - beta) over K, with s, t0 in F_q and beta not in F_q,
# is covered by the genus 2 curve
#   C0: Y^2 = H(u) = -(T - 2u) G(u) over F_q, T = beta + beta^q, N = beta^(q+1),
#   G(u) = (N - u^2)^2 - s (N - u^2)(T - 2u) + t0 (T - 2u)^2,
# through phi(u, Y) = ((N - u^2)/(T - 2u), Y (u - beta)/(T - 2u)^2). A point R of
# E(K) goes to psi(R) = D + D^sigma in J(F_q), where D is the class of phi^*(R)
# minus 2 infinity, and the logarithm is then found by index calculus in J(F_q).
# Divisor classes are Mumford pairs (a, b) of coefficient lists, lowest first.


def _is_prime(m: int) -> bool:
    return m >= 2 and all(m % d for d in range(2, isqrt(m) + 1))


def _factor(m: int) -> list[tuple[int, int]]:
    out, d = [], 2
    while d * d <= m:
        if m % d == 0:
            e = 0
            while m % d == 0:
                m //= d
                e += 1
            out.append((d, e))
        d += 1
    if m > 1:
        out.append((m, 1))
    return out


def sqrt_mod(c: int, q: int) -> int | None:
    """A square root of c mod the odd prime q (Tonelli-Shanks), or None."""
    c %= q
    if c == 0:
        return 0
    if pow(c, (q - 1) // 2, q) != 1:
        return None
    m, s = q - 1, 0
    while m % 2 == 0:
        m, s = m // 2, s + 1
    z = next(z for z in range(2, q) if pow(z, (q - 1) // 2, q) == q - 1)
    e, w, t, x = s, pow(z, m, q), pow(c, m, q), pow(c, (m + 1) // 2, q)
    while t != 1:
        i, t2 = 0, t
        while t2 != 1:
            t2, i = t2 * t2 % q, i + 1
        bb = pow(w, 1 << (e - i - 1), q)
        e, w, t, x = i, bb * bb % q, t * bb * bb % q, x * bb % q
    return x


class Fq:
    """The prime field F_q; elements are ints in [0, q)."""

    def __init__(self, q: int):
        self.q, self.zero, self.one = q, 0, 1

    def add(self, x, y):
        return (x + y) % self.q

    def sub(self, x, y):
        return (x - y) % self.q

    def mul(self, x, y):
        return x * y % self.q

    def inv(self, x):
        return pow(x, -1, self.q)

    def lift(self, c: int):
        return c % self.q


class Fq2:
    """K = F_q[t]/(t^2 - nu); elements are pairs (u, v)."""

    def __init__(self, q: int, nu: int):
        self.q, self.nu, self.zero, self.one = q, nu, (0, 0), (1, 0)

    def add(self, x, y):
        return (x[0] + y[0]) % self.q, (x[1] + y[1]) % self.q

    def sub(self, x, y):
        return (x[0] - y[0]) % self.q, (x[1] - y[1]) % self.q

    def mul(self, x, y):
        q = self.q
        return (x[0] * y[0] + self.nu * x[1] * y[1]) % q, (x[0] * y[1] + x[1] * y[0]) % q

    def inv(self, x):
        q = self.q
        n = pow((x[0] * x[0] - self.nu * x[1] * x[1]) % q, -1, q)
        return x[0] * n % q, (-x[1]) * n % q

    def lift(self, c: int):
        return c % self.q, 0

    def conj(self, x):
        return x[0], (-x[1]) % self.q

    def norm(self, x) -> int:
        return (x[0] * x[0] - self.nu * x[1] * x[1]) % self.q

    def sqrt(self, x):
        q = self.q
        u, v = x
        if v == 0:
            s = sqrt_mod(u, q)
            return (s, 0) if s is not None else (0, sqrt_mod(u * pow(self.nu, -1, q), q))
        n = sqrt_mod(self.norm(x), q)
        if n is None:
            return None
        for sg in (n, q - n):
            s = sqrt_mod((u + sg) * pow(2, -1, q), q)
            if s:
                return s, v * pow(2 * s, -1, q) % q
        return None


# polynomials over a field F: lists of elements, lowest degree first, trimmed


def ptrim(F, a):
    a = list(a)
    while a and a[-1] == F.zero:
        a.pop()
    return a


def padd(F, a, b):
    n = max(len(a), len(b))
    return ptrim(F, [F.add(a[i] if i < len(a) else F.zero, b[i] if i < len(b) else F.zero) for i in range(n)])


def psub(F, a, b):
    n = max(len(a), len(b))
    return ptrim(F, [F.sub(a[i] if i < len(a) else F.zero, b[i] if i < len(b) else F.zero) for i in range(n)])


def pmul(F, a, b):
    if not a or not b:
        return []
    out = [F.zero] * (len(a) + len(b) - 1)
    for i, x in enumerate(a):
        for j, y in enumerate(b):
            out[i + j] = F.add(out[i + j], F.mul(x, y))
    return ptrim(F, out)


def pscale(F, a, c):
    return ptrim(F, [F.mul(x, c) for x in a])


def pdivmod(F, a, m):
    r = list(a)
    inv = F.inv(m[-1])
    quo = [F.zero] * max(0, len(a) - len(m) + 1)
    for i in range(len(quo) - 1, -1, -1):
        c = F.mul(r[i + len(m) - 1], inv)
        quo[i] = c
        for j, y in enumerate(m):
            r[i + j] = F.sub(r[i + j], F.mul(c, y))
    return ptrim(F, quo), ptrim(F, r[: len(m) - 1])


def pxgcd(F, a, b):
    """(g, x, y) with x a + y b = g and g monic, for a, b not both zero."""
    r0, r1, x0, x1, y0, y1 = a, b, [F.one], [], [], [F.one]
    while r1:
        qt, r = pdivmod(F, r0, r1)
        r0, r1 = r1, r
        x0, x1 = x1, psub(F, x0, pmul(F, qt, x1))
        y0, y1 = y1, psub(F, y0, pmul(F, qt, y1))
    c = F.inv(r0[-1])
    return pscale(F, r0, c), pscale(F, x0, c), pscale(F, y0, c)


def peval(F, a, x):
    out = F.zero
    for c in reversed(a):
        out = F.add(F.mul(out, x), c)
    return out


# the Jacobian of Y^2 = H(u), deg H = 5, by Cantor's algorithm


def jac_add(F, H, D1, D2):
    (a1, b1), (a2, b2) = D1, D2
    d1, e1, e2 = pxgcd(F, a1, a2)
    if len(d1) == 1:  # coprime supports: d = 1, s1 = e1, s2 = e2, s3 = 0
        a = pmul(F, a1, a2)
        b = pdivmod(F, padd(F, pmul(F, pmul(F, e1, a1), b2), pmul(F, pmul(F, e2, a2), b1)), a)[1]
    else:
        d, c1, c2 = pxgcd(F, d1, padd(F, b1, b2))
        s1, s2 = pmul(F, c1, e1), pmul(F, c1, e2)
        a = pdivmod(F, pmul(F, a1, a2), pmul(F, d, d))[0]
        num = padd(F, padd(F, pmul(F, pmul(F, s1, a1), b2), pmul(F, pmul(F, s2, a2), b1)), pmul(F, c2, padd(F, pmul(F, b1, b2), H)))
        b = pdivmod(F, pdivmod(F, num, d)[0], a)[1]
    while len(a) > 3:
        a = pdivmod(F, psub(F, H, pmul(F, b, b)), a)[0]
        a = pscale(F, a, F.inv(a[-1]))
        b = pdivmod(F, ptrim(F, [F.sub(F.zero, c) for c in b]), a)[1]
    return a, b


def jac_neg(F, D):
    return D[0], ptrim(F, [F.sub(F.zero, c) for c in D[1]])


def jac_mul(F, H, k: int, D):
    R = ([F.one], [])
    while k:
        if k & 1:
            R = jac_add(F, H, R, D)
        D = jac_add(F, H, D, D)
        k >>= 1
    return R


def jac_doublings(F, H, D, m: int) -> list:
    """D, 2 D, 4 D, ... with enough terms for any multiplier below m."""
    out = [D]
    for _ in range(max(1, m.bit_length()) - 1):
        out.append(jac_add(F, H, out[-1], out[-1]))
    return out


def jac_mul_table(F, H, k: int, tab: list):
    R, i = ([F.one], []), 0
    while k:
        if k & 1:
            R = jac_add(F, H, R, tab[i])
        k, i = k >> 1, i + 1
    return R


class WeilCurve:
    """E: y^2 = (x^2 - s x + t0)(x - beta) over F_(q^2) and its genus 2 cover C0 over F_q."""

    def __init__(self, q: int, s: int, t0: int, beta: tuple[int, int]):
        if q < 3 or not _is_prime(q):
            raise ValueError("q must be an odd prime")
        nu = next(c for c in range(2, q) if pow(c, (q - 1) // 2, q) == q - 1) if q > 3 else 2
        self.q, self.F, self.K = q, Fq(q), Fq2(q, nu)
        K, F = self.K, self.F
        self.s, self.t0 = s % q, t0 % q
        self.beta = (beta[0] % q, beta[1] % q)
        if self.beta[1] == 0:
            raise ValueError("beta must lie outside F_q")
        if (self.s * self.s - 4 * self.t0) % q == 0:
            raise ValueError("x^2 - s x + t0 has a double root")
        b = self.beta
        gb = K.add(K.sub(K.mul(b, b), K.mul(K.lift(self.s), b)), K.lift(self.t0))
        if gb == K.zero:
            raise ValueError("beta is a root of x^2 - s x + t0")
        # f(x) = x^3 + a2 x^2 + a4 x + a6 over K
        self.a2 = K.sub(K.lift(-self.s), b)
        self.a4 = K.add(K.lift(self.t0), K.mul(K.lift(self.s), b))
        self.a6 = K.mul(K.lift(-self.t0), b)
        self.T, self.N = 2 * b[0] % q, K.norm(b)
        T, N = self.T, self.N
        m = [N, 0, q - 1]  # N - u^2
        l = [T, q - 2]  # T - 2u
        G = padd(F, padd(F, pmul(F, m, m), pscale(F, pmul(F, m, l), (-self.s) % q)), pscale(F, pmul(F, l, l), self.t0))
        self.H = pscale(F, pmul(F, l, G), q - 1)  # @line L1
        dH = ptrim(F, [i * c % q for i, c in enumerate(self.H)][1:])
        if len(self.H) != 6 or len(pxgcd(F, self.H, dH)[0]) != 1:
            raise ValueError("H is not squarefree of degree 5")
        self.HK = [K.lift(c) for c in self.H]

    # E(K)
    def f(self, x):
        K = self.K
        return K.add(K.mul(K.add(K.mul(K.add(x, self.a2), x), self.a4), x), self.a6)

    def on_curve(self, P) -> bool:
        return P is None or self.K.mul(P[1], P[1]) == self.f(P[0])

    def neg(self, P):
        return None if P is None else (P[0], self.K.sub(self.K.zero, P[1]))

    def add(self, P, Q):
        K = self.K
        if P is None:
            return Q
        if Q is None:
            return P
        (x1, y1), (x2, y2) = P, Q
        if x1 == x2:
            if K.add(y1, y2) == K.zero:
                return None
            num = K.add(K.add(K.mul(K.lift(3), K.mul(x1, x1)), K.mul(K.lift(2), K.mul(self.a2, x1))), self.a4)
            lam = K.mul(num, K.inv(K.add(y1, y1)))
        else:
            lam = K.mul(K.sub(y2, y1), K.inv(K.sub(x2, x1)))
        x3 = K.sub(K.sub(K.sub(K.mul(lam, lam), self.a2), x1), x2)
        return x3, K.sub(K.mul(lam, K.sub(x1, x3)), y1)

    def smul(self, k: int, P):
        R = None
        while k:
            if k & 1:
                R = self.add(R, P)
            P = self.add(P, P)
            k >>= 1
        return R

    def order_e(self) -> int:
        """#E(F_(q^2)) by counting, O(q^2) operations (a toy step)."""
        K, q, n = self.K, self.q, 1
        for u in range(q):
            for v in range(q):
                z = self.f((u, v))
                n += 1 if z == K.zero else (2 if pow(K.norm(z), (q - 1) // 2, q) == 1 else 0)
        return n

    def order_j(self) -> int:
        """#J(F_q) from #C0(F_q) and #C0(F_(q^2)) by counting (a toy step)."""
        F, K, q = self.F, self.K, self.q
        n1 = 1
        for u in range(q):
            z = peval(F, self.H, u)
            n1 += 1 if z == 0 else (2 if pow(z, (q - 1) // 2, q) == 1 else 0)
        n2 = 1
        for u in range(q):
            for v in range(q):
                z = peval(K, self.HK, (u, v))
                n2 += 1 if z == K.zero else (2 if pow(K.norm(z), (q - 1) // 2, q) == 1 else 0)
        a1 = n1 - q - 1
        a2 = (a1 * a1 - (q * q + 1 - n2)) // 2
        return 1 + a1 + a2 + q * a1 + q * q

    def point_order(self, P, n: int) -> int:
        for r, _ in _factor(n):
            while n % r == 0 and self.smul(n // r, P) is None:
                n //= r
        return n

    def random_point(self, rng: random.Random):
        while True:
            x = (rng.randrange(self.q), rng.randrange(self.q))
            y = self.K.sqrt(self.f(x))
            if y is not None:
                return (x, y) if rng.randrange(2) else self.neg((x, y))

    # the descent map psi: E(K) -> J(F_q)
    def phi(self, u, Y):
        """The point of E over the point (u, Y) of C0 with u in K, T - 2u != 0."""
        K = self.K
        d = K.sub(K.lift(self.T), K.mul(K.lift(2), u))
        x = K.mul(K.sub(K.lift(self.N), K.mul(u, u)), K.inv(d))
        return x, K.mul(K.mul(Y, K.sub(u, self.beta)), K.inv(K.mul(d, d)))

    def psi(self, R):
        K, F = self.K, self.F
        if R is None or R[0] == self.beta:  # @line L10
            return [1], []
        xr, yr = R
        A = [K.sub(K.mul(xr, K.lift(self.T)), K.lift(self.N)), K.mul(K.lift(-2), xr), K.one]  # @line L11
        _, inv, _ = pxgcd(K, [K.sub(K.zero, self.beta), K.one], A)
        l = [K.lift(self.T), K.lift(-2)]
        B = pdivmod(K, pmul(K, pscale(K, pmul(K, l, l), yr), inv), A)[1]
        D = (ptrim(K, A), B)
        Ds = ([K.conj(c) for c in D[0]], [K.conj(c) for c in D[1]])
        a, b = jac_add(K, self.HK, D, Ds)  # @line L12
        if any(c[1] for c in a + b):
            raise AssertionError("trace not defined over F_q")
        return [c[0] for c in a], [c[0] for c in b]


def factor_base(W: WeilCurve) -> dict[int, int]:
    """u0 -> Y0 for every u0 in F_q with H(u0) a square, Y0 the smaller root."""
    fb = {}
    for u in range(W.q):
        y = sqrt_mod(peval(W.F, W.H, u), W.q)
        if y is not None:
            fb[u] = min(y, (-y) % W.q)
    return fb


def jac_decompose(W: WeilCurve, D, fb: dict[int, int]) -> dict[int, int] | None:
    """A row e with D = sum of e_u (u - u0, Y0) if the a-polynomial of D splits, or None."""
    q, F = W.q, W.F
    a, b = D
    if len(a) == 1:
        return {}
    if len(a) == 2:
        u0, Y = (-a[0]) % q, peval(F, b, (-a[0]) % q)
        return {u0: 1 if Y == fb[u0] else -1}
    w = sqrt_mod(a[1] * a[1] - 4 * a[0], q)
    if w is None:
        return None
    half = pow(2, -1, q)
    row: dict[int, int] = {}
    for u0 in sorted({(-a[1] - w) * half % q, (-a[1] + w) * half % q}):
        Y = peval(F, b, u0)
        row[u0] = (1 if Y == fb[u0] else -1) * (2 if w == 0 else 1)
    return row


def gaudry_log(W: WeilCurve, D1, D2, r: int, nj: int, fb: dict[int, int], rng: random.Random) -> int:
    """log_D1 D2 mod r in J(F_q) by index calculus on degree one divisors.
    Precondition: D1 has prime order r, r divides nj = #J(F_q) exactly once, D2 in <D1>."""
    F, H = W.F, W.H
    cols = sorted(fb)  # @line L13
    idx = {u: i for i, u in enumerate(cols)}
    m, us = nj // r, cols[0]
    t1, t2 = jac_doublings(F, H, D1, r), jac_doublings(F, H, D2, r)
    tu = jac_doublings(F, H, jac_mul(F, H, r, ([(-us) % W.q, 1], ptrim(F, [fb[us]]))), m)
    piv: dict[int, tuple[list[int], dict[int, int]]] = {}
    av: list[int] = []
    bv: list[int] = []
    while True:  # @line L14
        a, b = rng.randrange(r), rng.randrange(r)  # @line L15
        R = jac_add(F, H, jac_add(F, H, jac_mul_table(F, H, a, t1), jac_mul_table(F, H, b, t2)), jac_mul_table(F, H, rng.randrange(m), tu))
        e = jac_decompose(W, R, fb)  # @line L16
        if e is None:  # @line L17
            continue
        j = len(av)
        av.append(a)
        bv.append(b)
        v = [0] * len(cols)
        for u, c in e.items():
            v[idx[u]] = c % r
        comb = {j: 1}
        for col in range(len(cols)):  # @line L18
            f = v[col]
            if f and col in piv:
                pv, pc = piv[col]
                for k in range(col, len(cols)):
                    if pv[k]:
                        v[k] = (v[k] - f * pv[k]) % r
                for k, c in pc.items():
                    comb[k] = (comb.get(k, 0) - f * c) % r
        lead = next((k for k in range(len(cols)) if v[k]), None)
        if lead is None:
            A = sum(c * av[k] for k, c in comb.items()) % r
            C = sum(c * bv[k] for k, c in comb.items()) % r
            if C:  # @line L19
                return (-A) * pow(C, -1, r) % r
        else:
            inv = pow(v[lead], -1, r)  # @line L20
            piv[lead] = ([c * inv % r for c in v], {k: c * inv % r for k, c in comb.items() if c})


def bsgs(W: WeilCurve, P, Q, m: int) -> int | None:
    """x in [0, m) with x P = Q in E(K) for P of order m, or None."""
    s = isqrt(m - 1) + 1
    table: dict = {}
    T = None
    for i in range(s):
        table.setdefault(T, i)
        T = W.add(T, P)
    step, T = W.neg(W.smul(s, P)), Q
    for j in range(s + 1):
        if T in table:
            return (j * s + table[T]) % m
        T = W.add(T, step)
    return None


def weil_descent_log(W: WeilCurve, P, Q, seed: int = 0, ne: int | None = None, nj: int | None = None) -> int | None:
    """The x in [0, ord P) with x P = Q in E(F_(q^2)), or None if Q is not in <P>.

    The group is E(F_(q^2)), of order about q^2; a group operation costs O(1)
    operations in F_(q^2), O(log(q)^2) bit operations. psi costs O(1)
    operations in F_(q^2). For the prime r > q with r || ord P, r^2 not
    dividing #J(F_q) and psi(P_r) != 0 (there is at most one), the index
    calculus in J(F_q) makes an expected O(q) draws (heuristic: about half of
    the reduced divisors split), each O(log q) Jacobian operations of O(1)
    operations in F_q, and the dense elimination costs O(q^3) operations mod
    r. Other prime powers r^e go to baby-step giant-step, O(r^(e/2)) group
    operations. Counting the two group orders is a toy O(q^2) step.
    """
    if not (W.on_curve(P) and W.on_curve(Q)):
        raise ValueError("points must lie on the curve")
    rng = random.Random(seed)
    if ne is None:
        ne = W.order_e()
    n = W.point_order(P, ne)
    if W.smul(n, Q) is not None:  # @line L2
        return None
    fb: dict[int, int] | None = None
    x, mod = 0, 1
    for r, e in _factor(n):  # @line L3
        re = r**e
        Pr, Qr = W.smul(n // re, P), W.smul(n // re, Q)  # @line L4
        big = e == 1 and r > W.q
        if big:
            if nj is None:
                nj = W.order_j()
            DP = W.psi(Pr)
        if big and nj % (r * r) != 0 and DP != ([1], []):  # @line L5
            if fb is None:
                fb = factor_base(W)
            y = gaudry_log(W, DP, W.psi(Qr), r, nj, fb, rng)  # @line L6
            if W.smul(y, Pr) != Qr:  # @line L7
                return None
            xr = y
        else:
            xr = bsgs(W, Pr, Qr, re)  # @line L8
            if xr is None:
                return None
        x += mod * ((xr - x) * pow(mod, -1, re) % re)  # @line L9
        mod *= re
    return x % n
test_weil_descent_attack.py 333 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/weil-descent-attack/. The authors are listed in prov/83GQX3.jsonl.

import itertools
import random

import pytest

from weil_descent_attack import (
    Fq,
    Fq2,
    WeilCurve,
    _factor,
    bsgs,
    factor_base,
    gaudry_log,
    jac_add,
    jac_decompose,
    jac_mul,
    jac_neg,
    pdivmod,
    peval,
    pmul,
    psub,
    sqrt_mod,
    weil_descent_log,
)

ZERO = ([1], [])


def random_curve(rng: random.Random, primes) -> WeilCurve:
    while True:
        q = rng.choice(primes)
        try:
            return WeilCurve(q, rng.randrange(q), rng.randrange(q), (rng.randrange(q), rng.randrange(1, q)))
        except ValueError:
            pass


def all_points(W: WeilCurve) -> list:
    q, K = W.q, W.K
    out = [None]
    for u in range(q):
        for v in range(q):
            z = W.f((u, v))
            for a in range(q):
                for b in range(q):
                    if K.mul((a, b), (a, b)) == z:
                        out.append(((u, v), (a, b)))
    return out


def brute_log(W: WeilCurve, P, Q) -> int | None:
    R, k = None, 0
    while True:
        if R == Q:
            return k
        R, k = W.add(R, P), k + 1
        if R is None:
            return None


def reduced_divisors(W: WeilCurve) -> list:
    """Every reduced Mumford pair over F_q by brute force."""
    q, F, H = W.q, W.F, W.H
    out = [ZERO]
    for a0 in range(q):
        a = [a0, 1]
        for b0 in range(q):
            b = [b0] if b0 else []
            if not pdivmod(F, psub(F, H, pmul(F, b, b)), a)[1]:
                out.append((a, b))
    for a0, a1 in itertools.product(range(q), repeat=2):
        a = [a0, a1, 1]
        for b0, b1 in itertools.product(range(q), repeat=2):
            b = [b0, b1] if b1 else ([b0] if b0 else [])
            if not pdivmod(F, psub(F, H, pmul(F, b, b)), a)[1]:
                out.append((a, b))
    return out


def test_square_roots():
    for q in (3, 5, 7, 11, 13, 101, 307):
        for c in range(q):
            s = sqrt_mod(c, q)
            squares = {x * x % q for x in range(q)}
            assert (s is not None) == (c in squares)
            if s is not None:
                assert s * s % q == c
        if q in (3, 5, 7, 11):
            K = Fq2(q, next(c for c in range(2, q) if pow(c, (q - 1) // 2, q) == q - 1))
            sq = {K.mul((a, b), (a, b)) for a in range(q) for b in range(q)}
            for u in range(q):
                for v in range(q):
                    s = K.sqrt((u, v))
                    assert (s is not None) == ((u, v) in sq)
                    if s is not None:
                        assert K.mul(s, s) == (u, v)


def test_cantor_against_brute_force():
    rng = random.Random(1)
    for q in (3, 5, 7):
        for _ in range(3):
            W = random_curve(rng, [q])
            divs = reduced_divisors(W)
            assert len(divs) == W.order_j()
            keys = {(tuple(a), tuple(b)) for a, b in divs}
            for _ in range(60):
                A, B, C = rng.choice(divs), rng.choice(divs), rng.choice(divs)
                S = jac_add(W.F, W.H, A, B)
                assert (tuple(S[0]), tuple(S[1])) in keys
                assert S == jac_add(W.F, W.H, B, A)
                assert jac_add(W.F, W.H, S, C) == jac_add(W.F, W.H, A, jac_add(W.F, W.H, B, C))
                assert jac_add(W.F, W.H, A, jac_neg(W.F, A)) == ZERO
                assert jac_add(W.F, W.H, A, ZERO) == A
                assert jac_mul(W.F, W.H, len(divs), A) == ZERO


def test_group_orders_agree():
    rng = random.Random(2)
    for _ in range(25):
        W = random_curve(rng, [3, 5, 7, 11, 13, 17, 19, 23])
        ne = W.order_e()
        assert ne == W.order_j()
        if W.q <= 7:
            assert ne == len(all_points(W))
        for _ in range(5):
            assert W.smul(ne, W.random_point(rng)) is None


def test_cover_map_lands_on_e():
    rng = random.Random(3)
    for _ in range(20):
        W = random_curve(rng, [5, 7, 11, 13, 17])
        for u in range(W.q):
            if (W.T - 2 * u) % W.q == 0:
                continue
            Y = sqrt_mod(peval(W.F, W.H, u), W.q)
            if Y is not None:
                assert W.on_curve(W.phi((u, 0), (Y, 0)))


def test_psi_is_a_homomorphism():
    rng = random.Random(4)
    for _ in range(30):
        W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23, 29])
        assert W.psi(None) == ZERO
        T2 = (W.beta, W.K.zero)
        assert W.on_curve(T2) and W.psi(T2) == ZERO
        for _ in range(10):
            A, B = W.random_point(rng), W.random_point(rng)
            assert W.psi(W.add(A, B)) == jac_add(W.F, W.H, W.psi(A), W.psi(B))
            assert W.psi(W.neg(A)) == jac_neg(W.F, W.psi(A))
            assert W.psi(W.add(A, T2)) == W.psi(A)


def test_worked_example():
    W = WeilCurve(7, 0, 1, (2, 1))
    assert W.K.nu == 3 and (W.T, W.N) == (4, 1)
    assert W.H == [2, 0, 2, 4, 3, 2]
    assert W.order_e() == W.order_j() == 52
    fb = factor_base(W)
    assert fb == {0: 3, 2: 0, 5: 2, 6: 1}
    P = ((0, 0), (3, 1))
    Q = ((0, 1), (0, 4))
    assert W.point_order(P, 52) == 13 and W.smul(6, P) == Q
    D1, D2 = W.psi(P), W.psi(Q)
    assert D1 == ([1, 2, 1], [3, 4]) and D2 == ([5, 6, 1], [3, 2])
    rows = {}
    for j in range(7):
        R = jac_add(W.F, W.H, jac_mul(W.F, W.H, j, D1), D2)
        rows[j] = jac_decompose(W, R, fb)
    assert rows == {0: {2: 1, 6: 1}, 1: {2: 1, 6: -1}, 2: {5: 2}, 3: None, 4: None, 5: {0: -2}, 6: {6: 2}}
    A = (0 - 1 - 6) % 13
    C = (1 - 1 - 1) % 13
    assert (A, C) == (6, 12) and (-A) * pow(C, -1, 13) % 13 == 6
    assert weil_descent_log(W, P, Q) == 6


def test_decompositions_are_relations():
    rng = random.Random(5)
    for _ in range(20):
        W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23])
        fb = factor_base(W)
        for _ in range(20):
            D = W.psi(W.random_point(rng))
            e = jac_decompose(W, D, fb)
            if e is None:
                continue
            S = ZERO
            for u, c in e.items():
                Fu = ([(-u) % W.q, 1], [fb[u]] if fb[u] else [])
                S = jac_add(W.F, W.H, S, jac_mul(W.F, W.H, abs(c), Fu if c > 0 else jac_neg(W.F, Fu)))
            assert S == D


def test_gaudry_log_against_brute_force():
    rng = random.Random(6)
    done = 0
    while done < 25:
        W = random_curve(rng, [7, 11, 13, 17, 19, 23, 29, 31, 37])
        nj = W.order_j()
        for r, e in _factor(nj):
            if e != 1 or r == 2:
                continue
            P = W.smul(nj // r, W.random_point(rng))
            if P is None:
                continue
            k = rng.randrange(r)
            D1, D2 = W.psi(P), W.psi(W.smul(k, P))
            assert gaudry_log(W, D1, D2, r, nj, factor_base(W), rng) == k
            done += 1


def check(W: WeilCurve, P, Q, ne: int, nj: int, seed: int):
    assert weil_descent_log(W, P, Q, seed=seed, ne=ne, nj=nj) == brute_log(W, P, Q)


def test_random_against_brute_force():
    rng = random.Random(7)
    for it in range(40):
        W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23, 29])
        ne, nj = W.order_e(), W.order_j()
        P = W.random_point(rng)
        n = W.point_order(P, ne)
        check(W, P, W.smul(rng.randrange(n), P), ne, nj, it)
        check(W, P, W.random_point(rng), ne, nj, it)


def test_trivial_targets():
    rng = random.Random(8)
    for it in range(15):
        W = random_curve(rng, [5, 7, 11, 13, 17, 19])
        ne, nj = W.order_e(), W.order_j()
        P = W.random_point(rng)
        n = W.point_order(P, ne)
        assert weil_descent_log(W, P, None, ne=ne, nj=nj) == 0
        assert weil_descent_log(W, P, P, ne=ne, nj=nj) == (1 % n)
        assert weil_descent_log(W, P, W.neg(P), ne=ne, nj=nj) == (n - 1) % n
        assert weil_descent_log(W, None, None, ne=ne, nj=nj) == 0
        assert weil_descent_log(W, None, P, ne=ne, nj=nj) is None


def test_generators_and_non_generators():
    # E(F_(q^2)) holds the full 2-torsion, so it is never cyclic; a point of
    # order #E/2 generates a cyclic subgroup of the largest possible order.
    rng = random.Random(9)
    gens = nongens = 0
    while gens < 10 or nongens < 10:
        W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23])
        ne, nj = W.order_e(), W.order_j()
        assert len([P for P in [W.random_point(rng) for _ in range(4)] if W.point_order(P, ne) == ne]) == 0
        P = W.random_point(rng)
        if W.point_order(P, ne) == ne // 2:
            gens += 1
            for _ in range(3):
                check(W, P, W.random_point(rng), ne, nj, gens)
                check(W, P, W.smul(rng.randrange(ne), P), ne, nj, gens)
        else:
            nongens += 1
            for _ in range(3):
                check(W, P, W.random_point(rng), ne, nj, nongens)


def test_prime_and_composite_orders():
    rng = random.Random(10)
    prime = composite = 0
    while prime < 10 or composite < 10:
        W = random_curve(rng, [7, 11, 13, 17, 19, 23, 29, 31])
        ne, nj = W.order_e(), W.order_j()
        R = W.random_point(rng)
        r = max(p for p, _ in _factor(ne))
        if r > 2 and ne % (r * r) and prime < 10:
            P = W.smul(ne // r, R)
            if P is not None:
                prime += 1
                check(W, P, W.smul(rng.randrange(r), P), ne, nj, prime)
                check(W, P, W.random_point(rng), ne, nj, prime)
        n = W.point_order(R, ne)
        if len(_factor(n)) >= 2:
            composite += 1
            check(W, R, W.smul(rng.randrange(n), R), ne, nj, composite)


def test_no_solution_is_reported():
    rng = random.Random(11)
    seen = 0
    while seen < 20:
        W = random_curve(rng, [5, 7, 11, 13, 17, 19, 23])
        ne, nj = W.order_e(), W.order_j()
        P, Q = W.random_point(rng), W.random_point(rng)
        if brute_log(W, P, Q) is None:
            assert weil_descent_log(W, P, Q, ne=ne, nj=nj) is None
            seen += 1


def test_larger_fields():
    rng = random.Random(12)
    for q in (101, 211, 307):
        W = random_curve(rng, [q])
        ne, nj = W.order_e(), W.order_j()
        assert ne == nj
        P = W.random_point(rng)
        n = W.point_order(P, ne)
        k = rng.randrange(n)
        Q = W.smul(k, P)
        assert weil_descent_log(W, P, Q, seed=q, ne=ne, nj=nj) == k
        if q == 101:
            assert bsgs(W, P, Q, n) == k


def test_seed_independence():
    rng = random.Random(13)
    W = random_curve(rng, [23])
    ne, nj = W.order_e(), W.order_j()
    P = W.random_point(rng)
    Q = W.smul(rng.randrange(W.point_order(P, ne)), P)
    assert len({weil_descent_log(W, P, Q, seed=s, ne=ne, nj=nj) for s in range(6)}) == 1


def test_bad_input():
    with pytest.raises(ValueError):
        WeilCurve(9, 1, 1, (0, 1))
    with pytest.raises(ValueError):
        WeilCurve(7, 1, 1, (2, 0))
    with pytest.raises(ValueError):
        WeilCurve(7, 2, 1, (0, 1))
    W = WeilCurve(7, 0, 1, (2, 1))
    with pytest.raises(ValueError):
        weil_descent_log(W, ((0, 0), (0, 0)), None)
    assert Fq(7).inv(3) == 5

What the next level needs

  • the tests did not run in this build

Read the full page about Weil descent attack