Ryan Rueger

ryan@rueg.re / picture / key / home
aboutsummaryrefslogtreecommitdiffhomepage
path: root/theta_lib/utilities
diff options
context:
space:
mode:
Diffstat (limited to 'theta_lib/utilities')
-rw-r--r--theta_lib/utilities/discrete_log.py273
-rw-r--r--theta_lib/utilities/fast_sqrt.py55
-rw-r--r--theta_lib/utilities/order.py117
-rw-r--r--theta_lib/utilities/strategy.py229
-rw-r--r--theta_lib/utilities/supersingular.py413
5 files changed, 0 insertions, 1087 deletions
diff --git a/theta_lib/utilities/discrete_log.py b/theta_lib/utilities/discrete_log.py
deleted file mode 100644
index dff04dc..0000000
--- a/theta_lib/utilities/discrete_log.py
+++ /dev/null
@@ -1,273 +0,0 @@
-# ===================================== #
-# Fast DLP solving using Weil pairing #
-# ===================================== #
-
-"""
-This code has been taken from:
-https://github.com/FESTA-PKE/FESTA-SageMath
-
-Copyright (c) 2023 Andrea Basso, Luciano Maino and Giacomo Pope.
-"""
-
-
-
-# Sage imports
-from sage.all import (
- ZZ
-)
-
-# import pari for fast dlog
-import cypari2
-
-# Make instance of Pari
-pari = cypari2.Pari()
-
-def discrete_log_pari(a, base, order):
- """
- Wrapper around pari discrete log. Works like a.log(b),
- but allows us to use the optional argument order. This
- is important as we skip Pari attempting to factor the
- full order of Fp^k, which is slow.
- """
- x = pari.fflog(a, base, order)
- return ZZ(x)
-
-def ell_discrete_log_pari(E,P,Base,order):
- """
- Wrapper around pari elllog function to compute discrete
- logarithms in elliptic curves over finite fields.
- Works like P.log(Base).
- """
- x = pari.elllog(E,P,Base,order)
- return ZZ(x)
-
-def weil_pairing_pari(P, Q, D, check=False):
- """
- Wrapper around Pari's implementation of the Weil pairing
- Allows the check of whether P,Q are in E[D] to be optional
- """
- if check:
- nP, nQ = D*P, D*Q
- if nP.is_zero() or nQ.is_zero():
- raise ValueError("points must both be n-torsion")
-
- return pari.ellweilpairing(P.curve(), P, Q, D)
-
-def tate_pairing_pari(P, Q, D):
- """
- Wrapper around Pari's implementation of the Tate pairing
- NOTE: this is not the reduced Tate pairing, so to make this
- match with SageMath you need
-
- P.tate_pairing(Q, D, k) == pari.elltatepairing(E, P, Q, D)**((p^k - 1) / D)
- """
- E = P.curve()
- return pari.elltatepairing(E, P, Q, D)
-
-def _precompute_baby_steps(base, step, e):
- """
- Helper function to compute the baby steps for
- pohlig_hellman_base and windowed_pohlig_hellman.
- """
- baby_steps = [base]
- for _ in range(e):
- base = base**step
- baby_steps.append(base)
- return baby_steps
-
-def pohlig_hellman_base(a, base, e):
- """
- Solve the discrete log for a = base^x for
- elements base,a of order 2^e using the
- Pohlig-Hellman algorithm.
- """
- baby_steps = _precompute_baby_steps(base, 2, e)
-
- dlog = 0
- exp = 2**(e-1)
-
- # Solve the discrete log mod 2, only 2 choices
- # for each digit!
- for i in range(e):
- if a**exp != 1:
- a /= baby_steps[i]
- dlog += (2**i)
-
- if a == 1:
- break
-
- exp //= 2
-
- return dlog
-
-def windowed_pohlig_hellman(a, base, e, window):
- """
- Solve the discrete log for a = base^x for
- elements base,a of order 2^e using the
- windowed Pohlig-Hellman algorithm following
- https://ia.cr/2016/963.
-
- Algorithm runs recursively, computing in windows
- l^wi for window=[w1, w2, w3, ...].
-
- Runs the base case when window = []
- """
- # Base case when all windows have been used
- if not window:
- return pohlig_hellman_base(a, base, e)
-
- # Collect the next window
- w, window = window[0], window[1:]
- step = 2**w
-
- # When the window is not a divisor of e, we compute
- # e mod w and solve for both the lower e - e mod w
- # bits and then the e mod w bits at the end.
- e_div_w, e_rem = divmod(e, w)
- e_prime = e - e_rem
-
- # First force elements to have order e - e_rem
- a_prime = a**(2**e_rem)
- base_prime = base**(2**e_rem)
-
- # Compute base^(2^w*i) for i in (0, ..., e/w-1)
- baby_steps = _precompute_baby_steps(base_prime, step, e_div_w)
-
- # Initialise some pieces
- dlog = 0
- if e_prime:
- exp = 2**(e_prime - w)
-
- # Work in subgroup of size 2^w
- s = base_prime**(exp)
-
- # Windowed Pohlig-Hellman to recover dlog as
- # alpha = sum l^(i*w) * alpha_i
- for i in range(e_div_w):
- # Solve the dlog in 2^w
- ri = a_prime**exp
- alpha_i = windowed_pohlig_hellman(ri, s, w, window)
-
- # Update a value and dlog computation
- a_prime /= baby_steps[i]**(alpha_i)
- dlog += alpha_i * step**i
-
- if a_prime == 1:
- break
-
- exp //= step
-
- # TODO:
- # I don't know if this is a nice way to do
- # this last step... Works well enough but could
- # be improved I imagine...
- exp = 2**e_prime
- if e_rem:
- base_last = base**exp
- a_last = a / base**dlog
- dlog_last = pohlig_hellman_base(a_last, base_last, e_rem)
- dlog += exp * dlog_last
-
- return dlog
-
-def BiDLP(R, P, Q, D, ePQ=None):
- """
- Given a basis P,Q of E[D] finds
- a,b such that R = [a]P + [b]Q.
-
- Uses the fact that
- e([a]P + [b]Q, [c]P + [d]Q) = e(P,Q)^(ad-bc)
-
- Optional: include the pairing e(P,Q) which can be precomputed
- which is helpful when running multiple BiDLP problems with P,Q
- as input. This happens, for example, during compression.
- """
- # e(P,Q)
- if ePQ:
- pair_PQ = ePQ
- else:
- pair_PQ = weil_pairing_pari(P, Q, D)
-
- # Write R = aP + bQ for unknown a,b
- # e(R, Q) = e(P, Q)^a
- pair_a = weil_pairing_pari(R, Q, D)
-
- # e(R,-P) = e(P, Q)^b
- pair_b = weil_pairing_pari(R, -P, D)
-
- # Now solve the dlog in Fq
- a = discrete_log_pari(pair_a, pair_PQ, D)
- b = discrete_log_pari(pair_b, pair_PQ, D)
-
- return a, b
-
-def BiDLP_power_two(R, P, Q, e, window, ePQ=None):
- r"""
- Same as the above, but uses optimisations using that
- D = 2^e.
-
- First, rather than use the Weil pairing, we can use
- the Tate pairing which is approx 2x faster.
-
- Secondly, rather than solve the discrete log naively,
- we use an optimised windowed Pohlig-Hellman.
-
- NOTE: this could be optimised further, following the
- SIKE key-compression algorithms and for the Tate pairing
- we could reuse Miller loops to save even more time, but
- that seemed a little overkill for a SageMath PoC
-
- Finally, as the Tate pairing produces elements in \mu_n
- we also have fast inversion from conjugation, but SageMath
- has slow conjugation, so this doesn't help for now.
- """
- p = R.curve().base_ring().characteristic()
- D = 2**e
- exp = (p**2 - 1) // D
-
- # e(P,Q)
- if ePQ:
- pair_PQ = ePQ
- else:
- pair_PQ = tate_pairing_pari(P, Q, D)**exp
-
- # Write R = aP + bQ for unknown a,b
- # e(R, Q) = e(P, Q)^a
- pair_a = tate_pairing_pari(Q, -R, D)**exp
-
- # e(R,-P) = e(P, Q)^b
- pair_b = tate_pairing_pari(P, R, D)**exp
-
- # Now solve the dlog in Fq
- a = windowed_pohlig_hellman(pair_a, pair_PQ, e, window)
- b = windowed_pohlig_hellman(pair_b, pair_PQ, e, window)
-
- return a, b
-
-def DLP_power_two(R, P, Q, e, window, ePQ=None, first=True):
- r"""
- This is the same as BiDLP but it only returns either a or b
- depending on whether first is true or false.
- This is used in compression, where we only send 3 of the 4
- scalars from BiDLP
- """
- p = R.curve().base_ring().characteristic()
- D = 2**e
- exp = (p**2 - 1) // D
-
- # e(P,Q)
- if ePQ:
- pair_PQ = ePQ
- else:
- pair_PQ = tate_pairing_pari(P, Q, D)**exp
-
- if first:
- pair_a = tate_pairing_pari(Q, -R, D)**exp
- x = windowed_pohlig_hellman(pair_a, pair_PQ, e, window)
-
- else:
- pair_b = tate_pairing_pari(P, R, D)**exp
- x = windowed_pohlig_hellman(pair_b, pair_PQ, e, window)
-
- return x
-
diff --git a/theta_lib/utilities/fast_sqrt.py b/theta_lib/utilities/fast_sqrt.py
deleted file mode 100644
index 0609fe5..0000000
--- a/theta_lib/utilities/fast_sqrt.py
+++ /dev/null
@@ -1,55 +0,0 @@
-
-# ============================================ #
-# Fast square root and quadratic roots #
-# ============================================ #
-
-"""
-Most of this code has been taken from:
-https://github.com/FESTA-PKE/FESTA-SageMath
-
-Copyright (c) 2023 Andrea Basso, Luciano Maino and Giacomo Pope.
-
-Functions with another Copyright mention are not from the above authors.
-"""
-
-def sqrt_Fp2(a):
- """
- Efficiently computes the sqrt
- of an element in Fp2 using that
- we always have a prime p such that
- p ≡ 3 mod 4.
- """
- Fp2 = a.parent()
- p = Fp2.characteristic()
- i = Fp2.gen() # i = √-1
-
- a1 = a ** ((p - 3) // 4)
- x0 = a1 * a
- alpha = a1 * x0
-
- if alpha == -1:
- x = i * x0
- else:
- b = (1 + alpha) ** ((p - 1) // 2)
- x = b * x0
-
- return x
-
-def n_sqrt(a, n):
- for _ in range(n):
- a = sqrt_Fp2(a)
- return a
-
-def sqrt_Fp(a):
- """
- Efficiently computes the sqrt
- of an element in Fp using that
- we always have a prime p such that
- p ≡ 3 mod 4.
-
- Copyright (c) Pierrick Dartois 2025.
- """
- Fp = a.parent()
- p = Fp.characteristic()
-
- return a**((p+1)//4)
diff --git a/theta_lib/utilities/order.py b/theta_lib/utilities/order.py
deleted file mode 100644
index 223f568..0000000
--- a/theta_lib/utilities/order.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# ================================================== #
-# Code to check whether a group element has order D #
-# ================================================== #
-
-"""
-This code has been taken from:
-https://github.com/FESTA-PKE/FESTA-SageMath
-
-Copyright (c) 2023 Andrea Basso, Luciano Maino and Giacomo Pope.
-"""
-
-from sage.all import(
- ZZ,
- prod,
- cached_function
-)
-
-def batch_cofactor_mul_generic(G_list, pis, group_action, lower, upper):
- """
- Input: A list of elements `G_list`, such that
- G is the first entry and the rest is empty
- in the sublist G_list[lower:upper]
- A list `pis` of primes p such that
- their product is D
- The `group_action` of the group
- Indices lower and upper
- Output: None
-
- NOTE: G_list is created in place
- """
-
- # check that indices are valid
- if lower > upper:
- raise ValueError(f"Wrong input to cofactor_multiples()")
-
- # last recursion step does not need any further splitting
- if upper - lower == 1:
- return
-
- # Split list in two parts,
- # multiply to get new start points for the two sublists,
- # and call the function recursively for both sublists.
- mid = lower + (upper - lower + 1) // 2
- cl, cu = 1, 1
- for i in range(lower, mid):
- cu = cu * pis[i]
- for i in range(mid, upper):
- cl = cl * pis[i]
- # cl = prod(pis[lower:mid])
- # cu = prod(pis[mid:upper])
-
- G_list[mid] = group_action(G_list[lower], cu)
- G_list[lower] = group_action(G_list[lower], cl)
-
- batch_cofactor_mul_generic(G_list, pis, group_action, lower, mid)
- batch_cofactor_mul_generic(G_list, pis, group_action, mid, upper)
-
-
-@cached_function
-def has_order_constants(D):
- """
- Helper function, finds constants to
- help with has_order_D
- """
- D = ZZ(D)
- pis = [p for p, _ in D.factor()]
- D_radical = prod(pis)
- Dtop = D // D_radical
- return Dtop, pis
-
-
-def has_order_D(G, D, multiplicative=False):
- """
- Given an element G in a group, checks if the
- element has order exactly D. This is much faster
- than determining its order, and is enough for
- many checks we need when computing the torsion
- basis.
-
- We allow both additive and multiplicative groups
- which means we can use this when computing the order
- of points and elements in Fp^k when checking the
- multiplicative order of the Weil pairing output
- """
- # For the case when we work with elements of Fp^k
- if multiplicative:
- group_action = lambda a, k: a**k
- is_identity = lambda a: a == 1
- identity = 1
- # For the case when we work with elements of E / Fp^k
- else:
- group_action = lambda a, k: k * a
- is_identity = lambda a: a.is_zero()
- identity = G.curve()(0)
-
- if is_identity(G):
- return False
-
- D_top, pis = has_order_constants(D)
-
- # If G is the identity after clearing the top
- # factors, we can abort early
- Gtop = group_action(G, D_top)
- if is_identity(Gtop):
- return False
-
- G_list = [identity for _ in range(len(pis))]
- G_list[0] = Gtop
-
- # Lastly we have to determine whether removing any prime
- # factors of the order gives the identity of the group
- if len(pis) > 1:
- batch_cofactor_mul_generic(G_list, pis, group_action, 0, len(pis))
- if not all([not is_identity(G) for G in G_list]):
- return False
-
- return True \ No newline at end of file
diff --git a/theta_lib/utilities/strategy.py b/theta_lib/utilities/strategy.py
deleted file mode 100644
index 550ef09..0000000
--- a/theta_lib/utilities/strategy.py
+++ /dev/null
@@ -1,229 +0,0 @@
-# ============================================================================ #
-# Compute optimised strategy for 2-isogeny chains (in dimensions 2 and 4) #
-# ============================================================================ #
-
-"""
-The function optimised_strategy has been taken from:
-https://github.com/FESTA-PKE/FESTA-SageMath
-
-Copyright (c) 2023 Andrea Basso, Luciano Maino and Giacomo Pope.
-
-Other functions are original work.
-"""
-
-from sage.all import log, cached_function
-import logging
-logger = logging.getLogger(__name__)
-#logger.setLevel("DEBUG")
-
-@cached_function
-def optimised_strategy(n, mul_c=1):
- """
- Algorithm 60: https://sike.org/files/SIDH-spec.pdf
- Shown to be appropriate for (l,l)-chains in
- https://ia.cr/2023/508
-
- Note: the costs we consider are:
- eval_c: the cost of one isogeny evaluation
- mul_c: the cost of one element doubling
- """
-
- eval_c = 1.000
- mul_c = mul_c
-
- S = {1:[]}
- C = {1:0 }
- for i in range(2, n+1):
- b, cost = min(((b, C[i-b] + C[b] + b*mul_c + (i-b)*eval_c) for b in range(1,i)), key=lambda t: t[1])
- S[i] = [b] + S[i-b] + S[b]
- C[i] = cost
-
- return S[n]
-
-@cached_function
-def optimised_strategy_with_first_eval(n,mul_c=1,first_eval_c=1):
- r"""
- Adapted from optimised_strategy when the fist isogeny evaluation is more costly.
- This is well suited to gluing comptations. Computes optimal strategies with constraint
- at the beginning. This takes into account the fact that doublings on the codomain of
- the first isogeny are impossible (because of zero dual theta constants).
-
- CAUTION: When splittings are involved, do not use this function. Use
- optimised_strategy_with_first_eval_and_splitting instead.
-
- INPUT:
- - n: number of leaves of the strategy (length of the isogeny).
- - mul_c: relative cost of one doubling compared to one generic 2-isogeny evaluation.
- - first_eval_c: relative cost of an evaluation of the first 2-isogeny (gluing)
- compared to one generic 2-isogeny evaluation.
-
- OUTPUT:
- - S_left[n]: an optimal strategy of depth n with constraint at the beginning
- represented as a sequence [s_0,...,s_{t-2}], where there is an index i for every
- internal node of the strategy, where indices are ordered depth-first left-first
- (as the way we move on the strategy) and s_i is the number of leaves to the right
- of internal node i (see https://sike.org/files/SIDH-spec.pdf, pp. 16-17).
- """
-
- S_left, _, _, _ = optimised_strategies_with_first_eval(n,mul_c,first_eval_c)
-
- return S_left[n]
-
-@cached_function
-def optimised_strategies_with_first_eval(n,mul_c=1,first_eval_c=1):
- r"""
- Adapted from optimised_strategy when the fist isogeny evaluation is more costly.
- This is well suited to gluing comptations. Computes optimal strategies with constraint
- at the beginning. This takes into account the fact that doublings on the codomain of
- the first isogeny are impossible (because of zero dual theta constants).
-
- CAUTION: When splittings are involved, do not use this function. Use
- optimised_strategy_with_first_eval_and_splitting instead.
-
- INPUT:
- - n: number of leaves of the strategy (length of the isogeny).
- - mul_c: relative cost of one doubling compared to one generic 2-isogeny evaluation.
- - first_eval_c: relative cost of an evaluation of the first 2-isogeny (gluing)
- compared to one generic 2-isogeny evaluation.
-
- OUTPUT:
- - S_left: Optimal strategies "on the left"/with constraint at the beginning i.e. meeting the
- first left edge that do not contain any left edge on the line y=sqrt(3)*(x-1).
- - S_right: Optimal strategies "on the right" i.e. not meeting the fisrt left edge (no constraint).
- """
-
- # print(f"Strategy eval: n={n}, mul_c={mul_c}, first_eval_c={first_eval_c}")
-
- eval_c = 1.000
- first_eval_c = first_eval_c
- mul_c = mul_c
-
- S_left = {1:[], 2:[1]} # Optimal strategies "on the left" i.e. meeting the first left edge
- S_right = {1:[]} # Optimal strategies "on the right" i.e. not meeting the first left edge
- C_left = {1:0, 2:mul_c+first_eval_c } # Cost of strategies on the left
- C_right = {1:0 } # Cost of strategies on the right
- for i in range(2, n+1):
- # Optimisation on the right
- b, cost = min(((b, C_right[i-b] + C_right[b] + b*mul_c + (i-b)*eval_c) for b in range(1,i)), key=lambda t: t[1])
- S_right[i] = [b] + S_right[i-b] + S_right[b]
- C_right[i] = cost
-
- for i in range(3,n+1):
- # Optimisation on the left (b<i-1 to avoid doublings on the codomain of the first isogeny)
- b, cost = min(((b, C_left[i-b] + C_right[b] + b*mul_c + (i-1-b)*eval_c+first_eval_c) for b in range(1,i-1)), key=lambda t: t[1])
- S_left[i] = [b] + S_left[i-b] + S_right[b]
- C_left[i] = cost
-
- return S_left, S_right, C_left, C_right
-
-@cached_function
-def optimised_strategy_with_first_eval_and_splitting(n,m,mul_c=1,first_eval_c=1):
- eval_c = 1.000
- first_eval_c = first_eval_c
- mul_c = mul_c
-
- S_left, S_middle, C_left, C_middle = optimised_strategies_with_first_eval(n-m,mul_c,first_eval_c)
-
- ## Optimization of the right part (with constraint at the end only)
-
- # Dictionnary of dictionnaries of translated strategies "on the right".
- # trans_S_right[d][i] is an optimal strategy of depth i
- # without left edge on the line y=sqrt(3)*(x-(i-1-d))
- trans_S_right={}
- trans_C_right={}
-
- for d in range(1,m+1):
- trans_S_right[d]={1:[]}
- trans_C_right[d]={1:0}
- if d==1:
- for i in range(3,n-m+d):
- b, cost = min(((b, C_middle[i-b] + trans_C_right[1][b] + b*mul_c + (i-b)*eval_c) for b in [1]+list(range(3,i))), key=lambda t: t[1])
- trans_S_right[1][i] = [b] + S_middle[i-b] + trans_S_right[1][b]
- trans_C_right[1][i] = cost
- else:
- for i in range(2,n-m+d):
- if i!=d+1:
- b = 1
- cost = trans_C_right[d-b][i-b] + C_middle[b] + b*mul_c + (i-b)*eval_c
- for k in range(2,min(i,d)):
- cost_k = trans_C_right[d-k][i-k] + C_middle[k] + k*mul_c + (i-k)*eval_c
- if cost_k<cost:
- b = k
- cost = cost_k
- # k=d
- if i>d:
- cost_k = C_middle[i-d] + C_middle[d] + d*mul_c + (i-d)*eval_c
- if cost_k<cost:
- b = d
- cost = cost_k
- for k in range(d+2,i):
- #print(d,i,k)
- cost_k = C_middle[i-k] + trans_C_right[d][k] + k*mul_c + (i-k)*eval_c
- if cost_k<cost:
- b = k
- cost = cost_k
- if b<d:
- trans_S_right[d][i] = [b] + trans_S_right[d-b][i-b] + S_middle[b]
- trans_C_right[d][i] = cost
- else:
- trans_S_right[d][i] = [b] + S_middle[i-b] + trans_S_right[d][b]
- trans_C_right[d][i] = cost
-
- ## Optimization on the left (last part) taking into account the constraints at the beginning and at the end
- for i in range(n-m+1,n+1):
- d = i-(n-m)
- b = 1
- cost = C_left[i-b] + trans_C_right[d][b] + b*mul_c + (i-1-b)*eval_c + first_eval_c
- for k in range(2,i):
- if k!=d+1 and k!=n-1:
- cost_k = C_left[i-k] + trans_C_right[d][k] + k*mul_c + (i-1-k)*eval_c + first_eval_c
- if cost_k<cost:
- b = k
- cost = cost_k
-
- S_left[i] = [b] + S_left[i-b] + trans_S_right[d][b]
- C_left[i] = cost
-
- return S_left[n]
-
-@cached_function
-def precompute_strategy_with_first_eval(e,m,M=1,S=0.8,I=100):
- r"""
- INPUT:
- - e: isogeny chain length.
- - m: length of the chain in dimension 2 before gluing in dimension 4.
- - M: multiplication cost.
- - S: squaring cost.
- - I: inversion cost.
-
- OUTPUT: Optimal strategy to compute an isogeny chain without splitting of
- length e with m steps in dimension 2 before gluing in dimension 4.
- """
- n = e - m
- eval_c = 4*(16*M+16*S)
- mul_c = 4*(32*M+32*S)+(48*M+I)/log(n*1.0)
- first_eval_c = 4*(2*I+(244+6*m)*M+(56+8*m)*S)
-
- return optimised_strategy_with_first_eval(n, mul_c = mul_c/eval_c, first_eval_c = first_eval_c/eval_c)
-
-@cached_function
-def precompute_strategy_with_first_eval_and_splitting(e,m,M=1,S=0.8,I=100):
- r"""
- INPUT:
- - e: isogeny chain length.
- - m: length of the chain in dimension 2 before gluing in dimension 4.
- - M: multiplication cost.
- - S: squaring cost.
- - I: inversion cost.
-
- OUTPUT: Optimal strategy to compute an isogeny chain of length e
- with m steps in dimension 2 before gluing in dimension 4 and
- with splitting m steps before the end.
- """
- logger.debug(f"Strategy eval split: e={e}, m={m}")
- n = e - m
- eval_c = 4*(16*M+16*S)
- mul_c = 4*(32*M+32*S)+(48*M+I)/log(n*1.0)
- first_eval_c = 4*(2*I+(244+6*m)*M+(56+8*m)*S)
-
- return optimised_strategy_with_first_eval_and_splitting(n, m, mul_c = mul_c/eval_c, first_eval_c = first_eval_c/eval_c)
diff --git a/theta_lib/utilities/supersingular.py b/theta_lib/utilities/supersingular.py
deleted file mode 100644
index e760c1b..0000000
--- a/theta_lib/utilities/supersingular.py
+++ /dev/null
@@ -1,413 +0,0 @@
-"""
-Helper functions for the supersingular elliptic curve computations in FESTA
-"""
-
-# =========================================== #
-# Compute points of order D and Torsion Bases #
-# =========================================== #
-
-"""
-Most of this code has been taken from:
-https://github.com/FESTA-PKE/FESTA-SageMath
-
-Copyright (c) 2023 Andrea Basso, Luciano Maino and Giacomo Pope.
-
-Functions with another Copyright mention are not from the above authors.
-"""
-
-# Sage Imports
-from sage.all import ZZ, GF, Integers
-
-# Local imports
-from .order import has_order_D
-from .discrete_log import weil_pairing_pari, ell_discrete_log_pari, discrete_log_pari
-from .fast_sqrt import sqrt_Fp2, sqrt_Fp
-
-
-
-
-def random_point(E):
- """
- Returns a random point on the elliptic curve E
- assumed to be in Montgomery form with a base
- field which characteristic p = 3 mod 4
- """
- A = E.a_invariants()[1]
- if E.a_invariants() != (0,A,0,1,0):
- raise ValueError("Function `generate_point` assumes the curve E is in the Montgomery model")
-
- # Try 10000 times then give up, just protection
- # for infinite loops
- F = E.base_ring()
- for _ in range(10000):
- x = F.random_element()
- y2 = x*(x**2 + A*x + 1)
- if y2.is_square():
- y = sqrt_Fp2(y2)
- return E(x, y)
-
- raise ValueError("Generated 10000 points, something is probably going wrong somewhere.")
-
-def generate_point(E, x_start=0):
- """
- Generate points on a curve E with x-coordinate
- i + x for x in Fp and i is the generator of Fp^2
- such that i^2 = -1.
- """
- F = E.base_ring()
- one = F.one()
-
- if x_start:
- x = x_start + one
- else:
- x = F.gen() + one
-
- A = E.a_invariants()[1]
- if E.a_invariants() != (0,A,0,1,0):
- raise ValueError("Function `generate_point` assumes the curve E is in the Montgomery model")
-
- # Try 10000 times then give up, just protection
- # for infinite loops
- for _ in range(10000):
- y2 = x*(x**2 + A*x + 1)
- if y2.is_square():
- y = sqrt_Fp2(y2)
- yield E(x, y)
- x += one
-
- raise ValueError("Generated 10000 points, something is probably going wrong somewhere.")
-
-def generate_Fp_point(E):
- """
- Returns a random Fp-rational point on the
- elliptic curve E assumed to be in Montgomery
- form with a base field which characteristic
- p = 3 mod 4 and coefficients defined over Fp.
-
- Copyright (c) Pierrick Dartois 2025.
- """
-
- A = E.a_invariants()[1]
- if E.a_invariants() != (0,A,0,1,0):
- raise ValueError("Function `generate_Fp_point` assumes the curve E is in the Montgomery model")
- if A[1] != 0:
- raise ValueError("The curve E should be Fp-rational")
-
- p=E.base_field().characteristic()
- Fp=GF(p,proof=False)
- one=Fp(1)
- x=one
-
- # Try 10000 times then give up, just protection
- # for infinite loops
- for _ in range(10000):
- y2 = x*(x**2 + A*x + 1)
- if y2.is_square():
- y = sqrt_Fp(y2)
- yield E(x, y)
- x += one
-
- raise ValueError("Generated 10000 points, something is probably going wrong somewhere.")
-
-def generate_point_order_D(E, D, x_start=0):
- """
- Input: An elliptic curve E / Fp2
- An integer D dividing (p +1)
- Output: A point P of order D.
- """
- p = E.base().characteristic()
- n = (p + 1) // D
-
- Ps = generate_point(E, x_start=x_start)
- for G in Ps:
- P = n * G
-
- # Case when we randomly picked
- # a point in the n-torsion
- if P.is_zero():
- continue
-
- # Check that P has order exactly D
- if has_order_D(P, D):
- P._order = ZZ(D)
- yield P
-
- raise ValueError(f"Never found a point P of order D.")
-
-def generate_Fp_point_order_D(E, D):
- """
- Input: An elliptic curve E / Fp2
- An integer D dividing (p +1)
- Output: A point P of order D.
-
- Copyright (c) Pierrick Dartois 2025.
- """
- p = E.base().characteristic()
- n = (p + 1) // D
- if D%2==0:
- n=n//2
-
- Ps = generate_Fp_point(E)
- for G in Ps:
- P = n * G
-
- # Case when we randomly picked
- # a point in the n-torsion
- if P.is_zero():
- continue
-
- # Check that P has order exactly D
- if has_order_D(P, D):
- P._order = ZZ(D)
- yield P
-
- raise ValueError(f"Never found a point P of order D.")
-
-def compute_point_order_D(E, D, x_start=0):
- """
- Wrapper function around a generator which returns the first
- point of order D
- """
- return generate_point_order_D(E, D, x_start=x_start).__next__()
-
-def compute_Fp_point_order_D(E, D):
- """
- Wrapper function around a generator which returns the first
- point of order D
-
- Copyright (c) Pierrick Dartois 2025.
- """
- return generate_Fp_point_order_D(E, D).__next__()
-
-def compute_linearly_independent_point_with_pairing(E, P, D, x_start=0):
- """
- Input: An elliptic curve E / Fp2
- A point P ∈ E[D]
- An integer D dividing (p +1)
- Output: A point Q such that E[D] = <P, Q>
- The Weil pairing e(P,Q)
- """
- Qs = generate_point_order_D(E, D, x_start=x_start)
- for Q in Qs:
- # Make sure the point is linearly independent
- pair = weil_pairing_pari(P, Q, D)
- if has_order_D(pair, D, multiplicative=True):
- Q._order = ZZ(D)
- return Q, pair
- raise ValueError("Never found a point Q linearly independent to P")
-
-def compute_linearly_independent_point(E, P, D, x_start=0):
- """
- Wrapper function around `compute_linearly_independent_point_with_pairing`
- which only returns a linearly independent point
- """
- Q, _ = compute_linearly_independent_point_with_pairing(E, P, D, x_start=x_start)
- return Q
-
-def torsion_basis_with_pairing(E, D):
- """
- Generate basis of E(Fp^2)[D] of supersingular curve
-
- While computing E[D] = <P, Q> we naturally compute the
- Weil pairing e(P,Q), which we also return as in some cases
- the Weil pairing is then used when solving the BiDLP
- """
- p = E.base().characteristic()
-
- # Ensure D divides the curve's order
- if (p + 1) % D != 0:
- print(f"{ZZ(D).factor() = }")
- print(f"{ZZ(p+1).factor() = }")
- raise ValueError(f"D must divide the point's order")
-
- P = compute_point_order_D(E, D)
- Q, ePQ = compute_linearly_independent_point_with_pairing(E, P, D, x_start=P[0])
-
- return P, Q, ePQ
-
-def torsion_basis(E, D):
- """
- Wrapper function around torsion_basis_with_pairing which only
- returns the torsion basis <P,Q> = E[D]
- """
- P, Q, _ = torsion_basis_with_pairing(E, D)
- return P, Q
-
-def torsion_basis_2f_frobenius(E,f):
- """
- Returns a basis (P, Q) of E[2^f] with pi_p(P)=P and pi_p(Q)=-Q,
- pi_p being the p-th Frobenius endomorphism of E.
-
- Copyright (c) Pierrick Dartois 2025.
- """
-
- P = compute_Fp_point_order_D(E, 2**f)
-
- i = E.base_field().gen()
- p = E.base_field().characteristic()
-
- R = compute_linearly_independent_point(E, P, 2**f)
- piR = E(R[0]**p,R[1]**p)
- piR_p_R = piR + R
-
- two_lamb = ell_discrete_log_pari(E,piR_p_R,P,2**f)
- lamb = two_lamb//2
- Q = R-lamb*P
-
- piQ = E(Q[0]**p,Q[1]**p)
-
- assert piQ == -Q
-
- return P, Q
-
-def torsion_basis_to_Fp_rational_point(E,P,Q,D):
- """
- Returns an Fp-rational point R=[lamb]P+[mu]Q
- of D-torsion given a D-torsion basis (P,Q) of E.
-
- Copyright (c) Pierrick Dartois 2025.
- """
-
- p=E.base_field().characteristic()
- piP = E(P[0]**p,P[1]**p)
- piQ = E(Q[0]**p,Q[1]**p)
-
- e_PQ = weil_pairing_pari(P,Q,D)
- e_PpiP = weil_pairing_pari(P, piP, D)
- e_QpiP = weil_pairing_pari(Q, piP, D)
- e_PpiQ = weil_pairing_pari(P, piQ, D)
- e_QpiQ = weil_pairing_pari(Q, piQ, D)
-
- # pi(P)=[a]P+[b]Q, pi(Q)=[c]P+[d]Q
- a = -discrete_log_pari(e_QpiP,e_PQ,D)
- b = discrete_log_pari(e_PpiP,e_PQ,D)
- c = -discrete_log_pari(e_QpiQ,e_PQ,D)
- d = discrete_log_pari(e_PpiQ,e_PQ,D)
-
- ZD = Integers(D)
-
- if a%D==1:
- mu = ZD(b/d)
- R=P+mu*Q
- elif c==0:
- # c=0 and a!=1 => d=1 so pi(Q)=Q
- R=Q
- else:
- # c!=0 and a!=1
- mu = ZD((1-a)/c)
- R=P+mu*Q
-
- piR = E(R[0]**p,R[1]**p)
-
- assert piR==R
-
- return R
-
-# =========================================== #
-# Entangled torsion basis for fast E[2^k] #
-# =========================================== #
-
-def precompute_elligator_tables(F):
- """
- Precomputes tables of quadratic non-residue or
- quadratic residue in Fp2. Used to compute entangled
- torsion bases following https://ia.cr/2017/1143
- """
- u = 2*F.gen()
-
- T1 = dict()
- T2 = dict()
- # TODO: estimate how large r should be
- for r in range(1, 30):
- v = 1 / (1 + u*r**2)
- if v.is_square():
- T2[r] = v
- else:
- T1[r] = v
- return T1, T2
-
-def entangled_torsion_basis(E, elligator_tables, cofactor):
- """
- Optimised algorithm following https://ia.cr/2017/1143
- which modifies the elligator method of hashing to points
- to find points P,Q of order k*2^b. Clearing the cofactor
- gives the torsion basis without checking the order or
- computing a Weil pairing.
-
- To do this, we need tables TQNR, TQR of pairs of values
- (r, v) where r is an integer and v = 1/(1 + ur^2) where
- v is either a quadratic non-residue or quadratic residue
- in Fp2 and u = 2i = 2*sqrt(-1).
- """
- F = E.base_ring()
- p = F.characteristic()
- p_sqrt = (p+1)//4
-
- i = F.gen()
- u = 2 * i
- u0 = 1 + i
-
- TQNR, TQR = elligator_tables
-
- # Pick the look up table depending on whether
- # A = a + ib is a QR or NQR
- A = E.a_invariants()[1]
- if (0,A,0,1,0) != E.a_invariants():
- raise ValueError("The elliptic curve E must be in Montgomery form")
- if A.is_square():
- T = TQNR
- else:
- T = TQR
-
- # Look through the table to find point with
- # rational (x,y)
- y = None
- for r, v in T.items():
- x = -A * v
-
- t = x * (x**2 + A*x + 1)
-
- # Break when we find rational y: t = y^2
- c, d = t.list()
- z = c**2 + d**2
- s = z**p_sqrt
- if s**2 == z:
- y = sqrt_Fp2(t)
- break
-
- if y is None:
- raise ValueError("Never found a y-coordinate, increase the lookup table size")
-
- z = (c + s) // 2
- alpha = z**p_sqrt
- beta = d / (2*alpha)
-
- if alpha**2 == z:
- y = F([alpha, beta])
- else:
- y = -F([beta, alpha])
-
- S1 = E([x, y])
- S2 = E([u*r**2*x, u0*r*y])
-
- return cofactor*S1, cofactor*S2
-
-# =============================================== #
-# Ensure Basis <P,Q> of E[2^k] has (0,0) under Q #
-# =============================================== #
-
-def fix_torsion_basis_renes(P, Q, k):
- """
- Set the torsion basis P,Q such that
- 2^(k-1)Q = (0,0) to ensure that (0,0)
- is never a kernel of a two isogeny
- """
- cofactor = 2**(k-1)
-
- R = cofactor*P
- if R[0] == 0:
- return Q, P
- R = cofactor*Q
- if R[0] == 0:
- return P, Q
- return P, P + Q