Solution to S0DH in D3CTF 2024

Here’s the official writeup for S0DH, including the challenge and solutions (intended and unintended).

The challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from sage.all import *

from Crypto.Util.number import bytes_to_long
from secret import flag
import hashlib

flag_start = b'd3ctf{'
assert flag.startswith(flag_start)
flag = flag[len(flag_start):-1]
assert len(flag) == 32

a = 38
b = 25
p = 2**a * 3**b - 1

assert is_prime(p)
Fp = GF(p)

Fpx = PolynomialRing(Fp, "x")
x = Fpx.gen()
Fp2 = Fp.extension(x**2 + 1, "ii")
ii = Fp2.gen()

A0 = Fp2(6)
E0 = EllipticCurve(Fp2, [0, A0, 0, 1, 0])
E0.set_order((p+1)**2)

"""
NOTE: Follow SIKE's spec in the generation of E[2^a] torsion basis.
It will allow you to compute the isogeny faster.
"""

sqrtof2 = Fp2(2).sqrt()
f = x**3 + A0 * x**2 + x

Pa = E0(0)
Qa = E0(0)
Pa_done = False
Qa_done = False
d = 0
for c in range(0, p):
Rx = ii + c
Ry_square = f(ii + c)
if not Ry_square.is_square():
continue
Ra = E0.lift_x(Rx)
Pa = 3**b * Ra

Ta = 2 ** (a - 1) * Pa
if Ta.is_zero():
continue
Tax_plus_3 = Ta.xy()[0] + 3
if Tax_plus_3 == 2 * sqrtof2 or Tax_plus_3 == -2 * sqrtof2:
Pa_done = True
elif Tax_plus_3 == 3 and not Qa_done:
Qa = Pa
Qa_done = True
else:
raise ValueError('Unexcepted order 2 point.')

if Pa_done and Qa_done:
break

assert Pa.order() == 2**a and Qa.order() == 2**a
assert Pa.weil_pairing(Qa, 2**a) ** (2 ** (a - 1)) != 1



Pb = E0(0)
while (3**(b-1))*Pb == 0:
Pb = 2**a * E0.random_point()
Qb = Pb
while Pb.weil_pairing(Qb, 3**b)**(3**(b-1)) == 1:
Qb = 2**a * E0.random_point()

# print(Pa,Qa,Pb,Qb)
print(f'Pa = {Pa.xy()}')
print(f'Qa = {Qa.xy()}')
print(f'Pb = {Pb.xy()}')
print(f'Qb = {Qb.xy()}')



sa = randint(0, 2**a-1)
Ra = Pa + sa * Qa
phia = E0.isogeny(kernel=Ra, algorithm='factored', model='montgomery', check=False)
Ea = phia.codomain()

sb = randint(0, 3**b-1)
Rb = Pb + sb * Qb
phib = E0.isogeny(kernel=Rb, algorithm='factored', model='montgomery', check=False)

Ea, phia_Pb, phia_Qb = phia.codomain(), phia(Pb), phia(Qb)
Eb, phib_Pa, phib_Qa = phib.codomain(), phib(Pa), phib(Qa)

# This time, no Alice torsion point image.
# print(phia_Pb,phia_Qb)

print(f'phib_Pa = {phib_Pa.xy()}')
print(f'phib_Qa = {phib_Qa.xy()}')

print(f'Ea: {Ea}')
print(f'Eb: {Eb}')

phib_Ra = phib_Pa + sa * phib_Qa
Eab = Eb.isogeny(kernel=phib_Ra, algorithm='factored', model='montgomery', check=False).codomain()
jab = Eab.j_invariant()

phia_Rb = phia_Pb + sb * phia_Qb
Eba = Ea.isogeny(kernel=phia_Rb, algorithm='factored', model='montgomery', check=False).codomain()
jba = Eba.j_invariant()

assert jab == jba
h = bytes_to_long(hashlib.sha256(str(jab).encode()).digest())
enc = h ^ bytes_to_long(flag)
print(f'enc = {enc}')


"""
Pa = (199176096138773310217268*ii + 230014803812894614137371, 21529721453350773259901*ii + 106703903226801547853572)
Qa = (8838268627404727894538*ii + 42671830598079803454272, 232086518469911650058383*ii + 166016721414371687782077)
Pb = (200990566762780078867585*ii + 156748548599313956052974, 124844788269234253758677*ii + 161705339892396558058330)
Qb = (39182754631675399496884*ii + 97444897625640048145787, 80099047967631528928295*ii + 178693902138964187125027)
phib_Pa = (149703758091223422379828*ii + 52711226604051274601866, 112079580687990456923625*ii + 147229726400811363889895)
phib_Qa = (181275595028116997198711*ii + 186563896197914896999639, 181395845909382894304538*ii + 69293294106635311075792)
Ea: Elliptic Curve defined by y^2 = x^3 + (11731710804095179287932*ii+170364860453198752624563)*x^2 + x over Finite Field in ii of size 232900919541184113672191^2
Eb: Elliptic Curve defined by y^2 = x^3 + (191884939246592021710422*ii+96782382528277357218650)*x^2 + x over Finite Field in ii of size 232900919541184113672191^2
enc = 48739425383997297710665612312049549178322149326453305960348697253918290539788
"""

The Flag

d3ctf{is0geny_gr4ph_m33t_1n_7he_m1ddl3}

Intended Solution

The main idea

Meet in the middle attack has time and space complexity $O(2^{a/2}) = O(p^{1/4})$. Using the given isogeny computing function, it takes about 1h to solve Alice’s secret isogeny $\phi_a$.

MITM

Let $K_a = 2^{a/2}P_a + s_a \cdot 2^{a/2}Q_a$, where $\left \langle P_a + s_a \cdot Q_a\right\rangle$ is the kernel of $\phi_a$. As a path in the 2-isogeny graph, $\phi_a$ passes through the node $E_{mid1} := E_0/ \left\langle{K_a}\right\rangle$.

Therefore we can compute all possible $2^{a/2}$-isogenies and construct a hash table. The keys are the j-invariant of codomains and the values are ${s_a}\pmod{2^{a/2}}$.

Remark: In my script, I compute $2^{a/2+1}$-isogenies and construct the hash table. Because I planned to set a=43 at first.

Use the j-invariant as the key of hash table

Note that each node of isogeny graph is an isomorphism class (over $\mathbb{F_{p^2}}$) of elliptic curve, so we need to compute the j-invariant (an isomorphic invariant) and use it as the key of our hash table.

The j-invariant of a untwisted Montgomery curve $E_A$ is

To avoid unnecessary computation of inversion, write $A = A_x/A_z$ and we get

See [1] for the j-invariant.

So we can use 1 inversion to compute the $j_A$ from the fractional representation of $A$.

Working with the given isogeny formula

Note that computing $2^a$-isogeny requires us to compute a chain a 2-isogeny and 4-isogeny. As SIKE’s spec pointed out in page 6~7, the input kernel point of isog2 cannot be $(0,0)$, and the input kernel point of isog4 cannot be $(1, \cdot)$ or $(-1, \cdot)$.

So how can we make use of this formula to compute isogenies starting from $E_a$?
The generation process of $P_a$ and $Q_a$ in challenge.py suggests that we can choose $Q_a’$ such that $2^{a-1} \cdot Q_a’ = (0,0)$, and choose a $P_a’$ such that $e_{2^{a}}(P_a’, Q_a’)$ is a primitive $2^a$-th root.

Searching from $E_a$

According to the structure of 2-isogeny graph, there are $3 \cdot 2^{a/2-1} $ isogenies to try. But taking $2^{a/2}P_a’ + s_a’ \cdot 2^{a/2}Q_a’$ as the kernel only makes $2^{a/2}$ isogenies.

In fact, the other isogenies correspond to $\left\langle{t_a’P_a’ + Q_a’}\right\rangle$, where $t_a’=0,2, …, 2^{a/2}$.

After MITM

The collision gives us ${s_a}\pmod{2^a}$ and $t_a’, s_a’$ , where $E_{mid2} := E_a/\left\langle K_a’\right\rangle \cong E_{mid1}$ and $K_a’ = 2^{a/2}(t_a’P_a’ + s_a’Q_a’)$

Denote $\phi_1: E_0 \to E_{mid1}$, $\phi_2: E_a \to E_{mid2}$, $\sigma: E_{mid1} \xrightarrow{\sim} E_{mid2}$ .
We can reconstruct the isogeny $\phi_a = \hat{\phi_2} \circ \sigma \circ \phi_1$ with the help of sagemath.

Since $\phi_a(P_a+s_aQ_a) = O$, we have $\phi_a(P_a) = -s_a\phi_a(Q_a)$. So we can solve the discrete logarithm to recover $s_a$

Now it is sufficient to follow SIDH protocol to find $j_{AB}$.

Some Further Optimization

In fact, there are some further optimization for the attack [3] and the computation of isogeny chain (optimal strategy) [2, 4].

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
from sage.all import *

from Crypto.Util.number import *
import hashlib

import json
# import multiprocessing as mp

import time

## Parameters

a = 38
b = 25
p = 2**a * 3**b - 1
assert is_prime(p)

Fp = GF(p)

Fpx = PolynomialRing(Fp, "x")
x = Fpx.gen()
Fp2 = Fp.extension(x**2 + 1, "ii")
ii = Fp2.gen()

A0 = Fp2(6)
E0 = EllipticCurve(Fp2, [0, A0, 0, 1, 0])
assert E0.is_supersingular(proof=False)


Pa = (199176096138773310217268*ii + 230014803812894614137371, 21529721453350773259901*ii + 106703903226801547853572)
Qa = (8838268627404727894538*ii + 42671830598079803454272, 232086518469911650058383*ii + 166016721414371687782077)
Pb = (200990566762780078867585*ii + 156748548599313956052974, 124844788269234253758677*ii + 161705339892396558058330)
Qb = (39182754631675399496884*ii + 97444897625640048145787, 80099047967631528928295*ii + 178693902138964187125027)


Pa = E0(Pa)
Qa = E0(Qa)
Pb = E0(Pb)
Qb = E0(Qb)

ka = 11731710804095179287932*ii+170364860453198752624563
kb = 191884939246592021710422*ii+96782382528277357218650
Ea = EllipticCurve(Fp2, [0, ka, 0, 1, 0])
Eb = EllipticCurve(Fp2, [0, kb, 0, 1, 0])
phib_Pa = (149703758091223422379828*ii + 52711226604051274601866, 112079580687990456923625*ii + 147229726400811363889895)
phib_Qa = (181275595028116997198711*ii + 186563896197914896999639, 181395845909382894304538*ii + 69293294106635311075792)
phib_Pa = Eb(phib_Pa)
phib_Qa = Eb(phib_Qa)

enc = 48739425383997297710665612312049549178322149326453305960348697253918290539788

## attack


### isogenys
def xA24(Aaffine):
return Aaffine + 2, 4


def get_A_from_A24(A24: tuple):
A2C, FOUR_C = A24
FOUR_A = 2 * A2C - FOUR_C # A+2C + A+2C - 4C = 2A
FOUR_A = 2 * FOUR_A # 2A + 2A = 4A
return FOUR_A, FOUR_C


# Get j-invariant from coeff (coeff in the form of fraction)
def get_j_invariant(A: tuple):
Ax, Az = A
Ax_square = Ax**2
Az_square = Az**2
Jx = 256 * (Ax_square - 3 * Az_square) ** 3 # Jx = 256(Ax^2 - 3Az^2)^3
Jz = Ax_square * Az_square**2 - 4 * Az_square**3 # Jz = Ax^2*Az^4 - 4Az^6
if Jz == 0:
print(f"A = {A}")
return Jx * Jz ** (-1)


# phi: E -> E/<P> =: E2
# All points of input and output are projective x-coordinate,
# i.e. if P = (XP: YP: ZP) ∈ E, then P is (XP, ZP) in the code
# Input: E's A24, kernel point K of order 2, T the point to push
# Output: E2's A24, phi(T)
def isog2(K, T):
if K[0] == 0:
print("Meet the case we cannot use isog2.")
return None

# return new curve's A24 = (A+2C: 4C).q
def xisog2(K):
XP, ZP = K
A24_plus = XP**2
C24 = ZP**2
A2C = C24 - A24_plus
return A2C, C24

# P: kernel point, Q: point to push
def xeval2(P, Q):
XP, ZP = P
XQ, ZQ = Q
T0 = XP + ZP
T1 = XP - ZP
T2 = XQ + ZQ
T3 = XQ - ZQ
T0 = T0 * T3
T1 = T1 * T2
T2 = T0 + T1
T3 = T0 - T1
XR = XQ * T2
ZR = ZQ * T3
return XR, ZR

A24new = xisog2(K)
# Anew = get_A_from_A24(A24new)
phiT = xeval2(K, T)
# return Anew, phiT
return A24new, phiT


def isog4(K, T):
# get new curve's A24 and three coeff for xeval4
if K[0] == 1 or K[0] == -1:
return None

def xisog4(P):
XP, ZP = P

K2 = XP - ZP
K3 = XP + ZP
K1 = ZP**2
K1 = K1 + K1
C24 = K1**2
K1 = K1 + K1
A24_plus = XP**2
A24_plus = A24_plus + A24_plus
A24_plus = A24_plus**2
return (A24_plus, C24), (K1, K2, K3)

def xeval4(P, coeff):
XP, ZP = P
T0 = XP + ZP
T1 = XP - ZP
XP = T0 * coeff[1]
ZP = T1 * coeff[2]
T0 = T0 * T1
T0 = coeff[0] * T0
T1 = XP + ZP
ZP = XP - ZP
T1 = T1**2
ZP = ZP**2
XP = T1 + T0
T0 = ZP - T0
XR = XP * T1
ZR = ZP * T0
return XR, ZR

A24, coeff = xisog4(K)
phi_T = xeval4(T, coeff)
return A24, phi_T


def xmul_2k(A24: tuple, k: int, T: tuple):
assert k >= 0

def xdbl(P: tuple, A24: tuple) -> tuple:
XP, ZP = P
# assert XP != 0 and ZP != 0
if XP == 0 or ZP == 0:
print(f"k={k}")
# print(f'XP={XP}')
# print(f'ZP={ZP}')
V1 = XP + ZP # line 1 of my pseudo code
V1 **= 2
V2 = XP - ZP
V2 **= 2
Z2P = A24[1] * V2
X2P = Z2P * V1 # line 6 of my pseudo code
V1 -= V2
Z2P += A24[0] * V1
Z2P *= V1
return X2P, Z2P

for _ in range(k):
T = xdbl(T, A24)
return T


# A: Estart affine coeff, T: kernel point (sagemath EC point object), k: T has order 2**k (same as the degree of this isogeny)
def isog_2k(A, T, k):
T = T[0], T[2]
A24 = xA24(A)
if k % 2 == 1:
# T has order 2^a
P = T
P = xmul_2k(A24, k - 1, P) # now P has order 2
result_isog2 = isog2(P, T)
if result_isog2 is None:
return None
A24, T = result_isog2
k -= 1
assert k % 2 == 0
for i in range(2, k + 2, 2):
# T has order 2^(a-i+1)
P = T
P = xmul_2k(A24, k - i, P) # now P has order 4
result_isog4 = isog4(P, T)
if result_isog4 is None:
return None
A24, T = result_isog4

Anew = get_A_from_A24(A24)
J = get_j_invariant(Anew)
return J, Anew


### MITM

#### Building a hash table, start from E0


def compute_hash_table(sa_start=0, sa_stop=2 ** (a // 2 - 1), steps=a // 2 + 1):
table = {}

Pa_new = 2 ** (a - steps) * Pa
Qa_new = 2 ** (a - steps) * Qa
sa = sa_start

Ra = Pa_new + sa * Qa_new
while sa < sa_stop:
# Using the basis in SIKE spec, our formula never fail.
Jnew, Anew = isog_2k(A0, Ra, steps)
# table[Jnew] = (Anew, sa)
table[Jnew] = sa
# table[str(Jnew)]
Ra += Qa_new
sa += 1
return table


time1 = time.time()

steps = a // 2 + 1

print("Start computing table.")

# NOTE: The following code for parallel will raise segmentation fault on my PC...
# Maybe the problem has something to do with thread lock of finite field element?

## multiproc
# num_proc = 12
# # assert num_proc % 2 == 0
# size_per_proc = 2**steps // num_proc
# parameters = [[i*size_per_proc, (i+1)*size_per_proc, steps] for i in range(0, num_proc)]
# parameters[-1][1] = 2**steps
# print(f'parameters = {parameters}')
# with mp.Pool(num_proc) as p:
# tables = p.starmap(compute_hash_table, parameters)

table2 = compute_hash_table(0, 2**steps, steps)

time2 = time.time()

print(f"Hash table built, takes {time2-time1} seconds")


# NOTE: You need to convert finite field type to str, Integer type to int before json.dump
# with open('hash_table.json', 'w') as f:
# json.dump(table2)


### from another side (start from Ea)

Pa_E0 = deepcopy(Pa)
Qa_E0 = deepcopy(Qa)

# First choose a nice basis for Ea[2^b]
Pa = Ea(0)
Qa = Ea(0)
while True:
Qa = Ea.random_point()
Qa = 3**b * Qa
Ra = 2 ** (a - 1) * Qa
if Ra.is_zero():
continue
assert Ra.order() == 2
if Ra.xy()[0] == 0:
break
else:
print(f"Ra = {Ra}")
print("Qa found.")
while True:
Pa = Ea.random_point()
Pa = 3**b * Pa
if Pa.weil_pairing(Qa, 2**a) ** (a - 1) != 1:
break
print("Pa found.")

assert Pa.order() == 2**a and Qa.order() == 2**a
assert Pa.weil_pairing(Qa, 2**a) ** (2 ** (a - 1)) != 1


steps = a - steps


def search_from_Ea(steps=steps):
# Save the collision with hashtable where the kernel is taPa + saQa
collision_isogenies = []
# Save failed isogeny(ta, sa) where the kernel is taPa + saQa
Pa_new = 2 ** (a - steps) * Pa
Qa_new = 2 ** (a - steps) * Qa
failed_isogenies = []

def search_one_isogeny(ta, sa, Ra):
result_isog2k = isog_2k(ka, Ra, steps)
if result_isog2k is None:
print(f"Isogeny failed for ta={ta}, sa={sa}")
failed_isogenies.append((ta, sa))
return
Jnew, Anew = result_isog2k
if Jnew in table2:
collision_isogenies.append((ta, sa, table2[Jnew]))
print("Found one collision.")
print(f"Start from Ea, sa={sa}, ta={ta}")
print(f"Start from E0, sa={table2[Jnew]}")
print(f"Meet at A={Anew}")
return

# First try these 2**steps SIDH-like isogeny
sa = 0
Ra = Pa_new
while sa < 2**steps:
search_one_isogeny(ta=1, sa=sa, Ra=Ra)
Ra += Qa_new
sa += 1
# Note that we also need to try these isogenies correspond to <(0, 1)>, <(2, 1)>, ..., <(2**steps, 1)>
ta = 0
Ra = Qa_new
two_Pa_new = 2 * Pa_new
if len(collision_isogenies) == 0:
print("Collision not found in SIDH like isogenies")
print("Start searching the second part!")
while ta < 2**steps:
search_one_isogeny(ta=ta, sa=1, Ra=Ra)
Ra += two_Pa_new
ta += 2
return collision_isogenies, failed_isogenies


time1 = time.time()
print("Start searching from Ea!!")
collision_isogenies, failed_isogenies = search_from_Ea(steps)

time2 = time.time()

print(f"Collision search from Ea over, takes {time2-time1} seconds")

### reconstruct phi using sagemath

print(f"collision_isogenies = {collision_isogenies}")
print(f"failed_isogenies = {failed_isogenies}")

ta, sa, sa_E0 = collision_isogenies[0]

Pa_new_E0 = (2**steps) * Pa_E0
Qa_new_E0 = (2**steps) * Qa_E0
Ka_E0 = Pa_new_E0 + sa_E0 * Qa_new_E0
phi1 = E0.isogeny(kernel=Ka_E0, algorithm="factored", model="montgomery")
E_middle_1 = phi1.codomain()

Pa_new_Ea = 2 ** (a - steps) * Pa
Qa_new_Ea = 2 ** (a - steps) * Qa
Ka_Ea = ta * Pa_new_Ea + sa * Qa_new_Ea
phi2 = Ea.isogeny(kernel=Ka_Ea, algorithm="factored", model="montgomery")
E_middle_2 = phi2.codomain()

assert E_middle_1.j_invariant() == E_middle_2.j_invariant()

sigma = E_middle_1.isomorphism_to(E_middle_2)
phi2_hat = phi2.dual()
phi = phi2_hat*sigma*phi1

phi_Pa_E0 = phi(Pa_E0)
phi_Qa_E0 = phi(Qa_E0)
sa = -phi_Qa_E0.discrete_log(phi_Pa_E0)
if sa < 0:
sa += 2**a

Ea_solved = E0.isogeny(kernel=Pa_E0+sa*Qa_E0, algorithm='factored',model='montgomery').codomain()
assert Ea_solved == Ea

Eab = Eb.isogeny(kernel=phib_Pa + sa*phib_Qa, algorithm='factored',model='montgomery').codomain()
jab = Eab.j_invariant()
print(f'jab = {jab}')

## decryption
h = bytes_to_long(hashlib.sha256(str(jab).encode()).digest())
flag = long_to_bytes(h ^ enc)
print(f'flag = {flag}')

Unintended Solution

Actually, there are some alternative ways to find a collision.

In [5] there’s an attack leverages modular polynomial, and it compute the j-invariant only when computing 2-isogeny, instead of computing coefficient of the codomain. That’s really cool!

Another way is to get order-two points with sagemath’s division_points when going one step in DFS. (I thank hash_hash for telling me that) It seems that division_points make use of division polynomial to get these order-two points with low cost. Usually, when the degree of isogeny $l$ is a larger prime, we need to sample a random point $R$ and compute $(p+1)/l * R$. This is one of the main reasons why isogeny is slow! Another way is to push some points through the isogeny, but I think this is complicated in the scenario of MITM attack, perhaps not doable.

Here’s a MITM snippet using division_points and DFS from hash_hash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# NOTE: from Hash Team hash_hash

from Crypto.Util.number import *

def dp(way, E):
if len(way) > deepth:
return 0
ker = E(0).division_points(2)[1:]
j_next = [E.isogeny_codomain(k).j_invariant() for k in ker]
for j in j_next:
if j not in forward.keys():
forward[j] = way+[j]
dp(way+[j], EllipticCurve(j=Fp2(j)))

def dp_find(way, E):
if len(way) > deepth:
return 0
ker = E(0).division_points(2)[1:]
j_next = [E.isogeny_codomain(k).j_invariant() for k in ker]
for j in j_next:
if j in forward.keys():
col = forward[j]+way[::-1]
print("FIND!", len(col))
elif j not in back.keys():
back[j] = way+[j]
dp_find(way+[j], EllipticCurve(j=Fp2(j)))

a = 38
b = 25
p = 2**a * 3**b - 1

assert is_prime(p)
Fp = GF(p)

Fpx = PolynomialRing(Fp, "x")
x = Fpx.gen()
Fp2 = Fp.extension(x**2 + 1, "ii")
ii = Fp2.gen()

A0 = Fp2(6)
E0 = EllipticCurve(Fp2, [0, A0, 0, 1, 0])

A1 = Fp2(11731710804095179287932*ii+170364860453198752624563)
A2 = Fp2(191884939246592021710422*ii+96782382528277357218650)

Ea = EllipticCurve(Fp2, [0, A1, 0, 1, 0])
Eb = EllipticCurve(Fp2, [0, A2, 0, 1, 0])

E_now = EllipticCurve(j=Fp2(0))
E_start = E0
E_end = Ea

forward = {}
back = {}
deepth = 19

forward[E_start.j_invariant()] = [E_start.j_invariant()]
back[E_end.j_invariant()] = E_end.j_invariant()
print("dp start")
dp([E_start.j_invariant()], E_start)
print("Search")
dp_find([E_end.j_invariant()], E_end)

By the way, a similar challenge had already appeared in the race SEETF 2023. Only after the race did I recognize that… See maple’s writeup for IsogenyMaze [6].

Reference

  1. Montgomery curves and their arithmetic: The case of large characteristic fields
  2. (SIKE’s spec) Supersingular Isogeny Key Encapsulation
  3. Improved Classical Cryptanalysis of SIKE in Practice
  4. Efficient algorithms for supersingular isogeny Diffie-Hellman
  5. Learning to SQI - Meet in the Middle Isogenies
  6. https://blog.maple3142.net/2023/06/12/seetf-2023-writeups/#isogeny-maze

And you don’t want to miss this interesting song! Kani came in like a wrecking ball

文章作者: 随缘(su1yu4n)
文章链接: https://su1yu4n.github.io/2024/04/30/Solution-to-S0DH/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 随缘的博客