序文 §

毎年zer0ptsの皆さんが行っている高レベルな集団講義であるyoshi-campですが、例年なら指を咥えながら外側から眺めて終了後の残滓を必死で解読1していたのが、今年はTwitterで外部参加者を募集していたので、./Vespiaryから私とネコチャンが参加したいと伝えたところ、誠にありがたいことに快諾していただいたので参加してきました。というわけで恒例の参加記を書きます。

……と、言いたかったんですが、講義は(私の発表分も含めて)5つもあり、どれも高レベルかつ演習量が多かった事から、どの講義も用意された分を全部こなすことは無く終わってしまったので復習が完了した講義から参加記を書きます。そういうわけでこの記事は最初に行われたふるつきさんによる「猫と学ぶ同種写像暗号」の記録と復習になります。

他の人の参加記 §

(※ ここまで敬体で書いていますが、自分がWriteupや技術や数学に関する記事を書く時の癖で、次の節から常体で書きます)

同種写像 §

楕円曲線EEから、別の楕円曲線EE'への写像ϕ:EE\phi: E \to E'で、次を満たすものを同種写像と呼ぶ。

  1. 有理関数で表す事が出来る
  2. 全射
  3. ϕ(OE)=OE\phi(O_E) = O_{E'}

(理由はよくわかっていないし、今後もそんな感じで根拠不明の説明になってしまうが、)これは準同型写像になり、この性質は後にSIDHで活躍する。

同種写像に関する重要な命題として次のようなものがある。

楕円曲線E(Fp)E(\mathbb F_p)HE(Fp)H \in E(\overline {\mathbb F_p})が与えられた時に、kerϕ=H\ker \phi = \langle H \rangleとなる同種写像ϕ:EE\phi: E \to E'と、終域となる楕円曲線EE'が(同型を除いて)一意的に存在する。この時E=E/HE' = E/\langle H \rangleのように書く。また、以降では楕円曲線が「同じ」と言った時に大抵は「同型」を意味するものとする。

特にHHE(Fp)E(\overline {\mathbb F_p})ll-ねじれ点(ll倍すると無限遠点になる点)とすれば、ϕ\phill次の同種写像(ll-同種写像と呼ばれる)となり、ψϕ=[l]\psi \circ \phi = [l]となるような同種写像ψ:EE\psi: E' \to Eも存在して「双対同種写像」と呼ばれる。

そういうものの存在がわかったところで、実用するためには計算が出来ないとあまり意味がないが、これはちゃんと計算できてSageMathには実装が存在する。楕円曲線Eと、生成元Hに対してE.isogeny(H)を叩けば対応する同種写像が求められる。

というわけで、このメソッドを用いて以上の事を確かめてみる。講義では、与えられた楕円曲線に対して2-isogenyと3-isogenyを計算し、双対な同種写像が存在する事を確かめるという問題が出た。

p = 2^143 * 3 - 1
K = GF(p^2)
E = EllipticCurve(K, [1, 0])  # y^2 = x^3 + x
j_E = E.j_invariant()

# exercise1
l = 3
for x, _ in E.division_polynomial(l).roots():
    P = E.lift_x(x)
    phi = E.isogeny(P)
    _E = phi.codomain()

    found = False
    for _x, _ in _E.division_polynomial(l).roots():
        Q = _E.lift_x(_x)
        psi = _E.isogeny(Q)
        __E = psi.codomain()

        # __E != E but they have same j-invariant
        if j_E == __E.j_invariant():
            assert __E.is_isomorphic(E)
            found = True

            # psi \circ phi = [l] (duality)
            # ref: https://doc.sagemath.org/html/en/reference/arithmetic_curves/sage/schemes/elliptic_curves/ell_generic.html?highlight=isomorphism_to#sage.schemes.elliptic_curves.ell_generic.EllipticCurve_generic.isomorphism_to
            f = __E.isomorphism_to(E)
            R = E.random_point()
            print(-1 * f(psi(phi(R))) == l*R)  # `-1` is needed ([-1] \circ f is also an isomorphism from `__E` to `E`)
            break

    assert found

ll-ねじれ点のx座標はEEll等分多項式の根となっているので、(無限遠点を除いて)E.division_polynomial(l)で求める事が出来る。なお、E(0).divison_points(l)を用いるとll-ねじれ点を全て求める事が出来るが、(l2l \neq 2である)ll-ねじれ点PPに対して、P-Pはx座標が同じでy座標が異なるll-ねじれ点となり、これもこのメソッドで手に入る。そして不思議な事情で、E.isogeny(P)E.isogeny(-P)は同じになるようなので、過不足なく求めたいのなら等分多項式を用いる方が良い。

講義中では、2-isogenyと3-isogenyを求めるだけで満足してしまったので、同種写像ϕ\phiに対応する双対同種写像も求めてみる。これも終域EE'に対して、等分多項式からll-ねじれ点を求めてそれを生成元とする部分群がkernelとなる同種写像ψ\psi.isogeny()で求める事が出来るので、その終域がEEと同型、つまりj不変量が同じものが見つかれば良い。

あとはEEと同型なE0E_0からEEへの同型写像ff.isomorphism_to(E)で手に入るので、fψϕ=[l]f\circ \psi \circ \phi = [l]を確かめれば良い。但し、これまた不思議な力が働いてll倍写像[l][l]ではなく、l-l倍写像になってしまうが、細かいことはとりあえず気にしないでおく(ffが同型写像なら、[1]f[-1] \circ fも同型写像になるのであまり問題は無い)

ちなみに、これは演習後に教えてもらったのだが、単にll-同種写像が欲しくてllが素数なら、こんな難しい事をしなくてもE.isogenies_prime_degree(l)で手に入るらしい。

lel^e-同種写像の計算 §

一般的にll-同種写像を求める計算はllがデカければデカいほど重くなるらしく、lel^eのようなものはeeが少し増えただけでもかなり計算時間がかかってしまう。そこで、ll-同種写像でee回移して合成する事を考えると、またしても不思議な力が働いて2lel^e-同種写像を計算出来るらしい。

2つ目の課題としてこれの実装が課された。lel^e-同種写像「自体」を求めようとすると関数合成によって次数が爆発してしまうので引数に計算した点(後でSIDHを実装する都合上、複数とれるようにしている)を渡すとll-同種写像を計算する度に移すという形にしている。

p = 2^143 * 3 - 1
K = GF(p^2)
E = EllipticCurve(K, [1, 0])  # y^2 = x^3 + x
j_E = E.j_invariant()


def prime_power_isogeny(H, l, e, check=False, Ps=[]):
    # check H is a l^e-torsion point of E
    if check:
        assert l^e * H == E(0)

    curve = H.curve()
    for i in range(e):
        # get l-torsion point from H (l^(e-i)-torsion point)
        _H = l^(e - i - 1) * H
        phi = curve.isogeny(_H)
        # update
        curve = phi.codomain()
        H = phi(H)

        Ps = [phi(P) for P in Ps]

    return curve, Ps

# exercise2
l = 2
e = 10

# get l^e-torsion point
while True:
    P = E.random_point()
    o = P.order()
    if o % l^e == 0:
        P *= (o // l^e)
        break

_E, _ = prime_power_isogeny(P, l, e, True)
print(_E.j_invariant())

ちなみに、これまた演習後に教えてもらったのだが、SageMathのバージョンが9.5以上だとE.isogeny(H, algorithm="factored")とすることで同様の事が出来るらしい。残念ながら私が使っているSageMathは演習中は9.0で、自室で使っているデスクトップも9.3だったので未対応である。

CGLハッシュ §

ここまで説明していなかったが、ll-同種写像はどうやらl+1l+1個あるらしい。ll-同種写像ϕ:EE\phi: E \to E'に関して、EE'を定義域とするll-同種写像ψ:EEi\psi: E' \to E_iもまたl+1l+1個存在するが、その内1つは双対性よりEE(と同型な楕円曲線)が終域となる。よって、元の定義域EEに戻らない同種写像はll個存在することになる。

ここで、もしl=2l=2とすれば、定義域に戻らない同種写像が2つ存在することになるので、ある数値を入力にとってそのビットを下から見ていき、0,1でどちらの同種写像を使うかを決定するという事を考える。これを利用してハッシュ関数が構成出来る、というトチ狂った考えで提唱されたのがCGLハッシュである。

これの実装が3番目の課題として課されたが、講義中は実装が重すぎるという事で見送られた。復習で扱うにも面倒でCTF的にもあまり面白いものではない(出題例が特に無いし、実装例も見当たらないらしい)ので私は説明だけに留める(冒頭でリンクを貼っているptr-yudaiさんの参加記には載っている)。

SIDH §

「ある同種写像ϕ\phiが存在して、2つの楕円曲線E,E:=ϕ(E)E, E' := \phi(E)が与えられた時、ϕ\phiを求める」という問題が困難だという仮定3を置いて鍵共有が構成出来る。

大まかな流れとしては、ϕA:EEA,ϕB:EEB\phi_A: E \to E_A, \phi_B: E\to E_Bとなる2つの同種写像を秘密鍵として、ϕA:EBEBA\phi_A': E_B \to E_{BA}ϕB:EAEAB\phi_B': E_A \to E_{AB}という同種写像を計算し、同型の楕円曲線EAB,EBAE_{AB}, E_{BA}を共有するという様になっている(同型な楕円曲線を共有するので、同一の「値」としては楕円曲線のj不変量を共有することになる)。

使うパラメータは素数p=lAeAlBeB1p = l_A^{e_A}l_B^{e_B} - 1とSupersingularな楕円曲線E(Fp2)E(\mathbb F_p^2)PA,QAE[lAeA],PB,QBE[lBeB]P_A, Q_A \in E[l_A^{e_A}], P_B,Q_B \in E[l_B^{e_B}]であり、E[x]E[x]xx-ねじれ点全体からなる集合4を意味する。また、PA,QAP_A, Q_Aは線形独立(つまりsPA+tQA=OsP_A + tQ_A = Oとなるs,ts,tは0以外に存在しない)で、PB,QBP_B, Q_Bも線形独立とする。

Aliceは秘密鍵sAs_AlAeA1l_A^{e_A} - 1以下の非負整数から選択する。同様にBobは秘密鍵sBs_BlBeB1l_B^{e_B} - 1以下の非負整数から選択する。

2つ目の秘密鍵としてAliceはϕA=E/PA+sAQA\phi_A = E/\langle P_A + s_A Q_A \rangleを、BobはϕB=E/PB+sBQB\phi_B = E/\langle P_B + s_B Q_B \rangleを計算する。

公開鍵としてAliceはϕA(PB),ϕA(QB)\phi_A(P_B), \phi_A(Q_B)を計算し、ϕA\phi_Aの終域EA:=ϕA(E)E_A := \phi_A(E)と併せて公開する。同様にBobはϕB(PA),ϕB(QA),EB:=ϕB(E)\phi_B(P_A), \phi_B(Q_A), E_B := \phi_B(E)を公開する。

この後、AliceとBobはそれぞれ、ϕA,ϕB\phi_A', \phi_B'を相手の公開鍵と自分の秘密鍵を用いて計算する。これは次のようにして計算出来る。

ϕA=EB/ϕB(PA)+sAϕB(QA)ϕB=EA/ϕA(PB)+sBϕA(QB) \begin{aligned} \phi_A' &= E_B / \langle \phi_B(P_A) + s_A \phi_B(Q_A) \rangle \cr \phi_B' &= E_A / \langle \phi_A(P_B) + s_B \phi_A(Q_B) \rangle \end{aligned}

ここで、同種写像の準同型性からkerϕA=ϕB(PA+sAQA)=ϕB(kerϕA)\ker \phi_A' = \langle \phi_B(P_A + s_AQ_A) \rangle = \phi_B(\ker \phi_A)となり、同様にkerϕB=ϕA(PB+sBQB)=ϕA(kerϕB)\ker \phi_B' = \langle \phi_A(P_B + s_B Q_B) \rangle = \phi_A(\ker \phi_B)が成り立つ。最終的にϕAϕB\phi_A' \circ \phi_BϕBϕA\phi_B' \circ \phi_Aがどちらも同じkernelを持つことを示すために、それぞれのkernelに関して考察する。

まず前者に関して、Xker(ϕAϕB)X \in \ker (\phi_A' \circ \phi_B)となる任意のXXに対して、kerϕA=ϕB(ϕA)\ker \phi_A' = \phi_B(\phi_A)であるから、あるYkerϕAY \in \ker \phi_Aが存在して、ϕB(X)=ϕB(Y)\phi_B(X) = \phi_B(Y)を満たさなくてはならない。よって、ϕB(XY)=O\phi_B(X - Y) = Oとなるから、XYkerϕB=PB+sBQBX - Y \in \ker \phi_B = \langle P_B + s_BQ_B \rangleが成り立つ。更にYkerϕA=PA+sAQAY \in \ker \phi_A = \langle P_A + s_AQ_A \rangleであるから、係数v,wv, wを用いてXXは次のように表す事が出来る

X=v(PA+sAQA)+w(PB+sBQB) X = v(P_A + s_AQ_A) + w(P_B + s_BQ_B)

よって、XPA+sAQA,PB+sBQBX \in \langle P_A + s_A Q_A, P_B + s_B Q_B \rangleが成り立つことから、ker(ϕAϕB)PA+sAQA,PB+sBQB\ker (\phi_A' \circ \phi_B) \subset \langle P_A + s_A Q_A, P_B + s_B Q_B \rangleが成り立つ。

逆向きも示す事が出来て、PA+sAQA,PB+sBQB\langle P_A + s_A Q_A, P_B + s_B Q_B \ranglePB+sBQBP_B + s_BQ_B成分はkerϕB=PB+sBQB\ker \phi_B = \langle P_B + s_BQ_B \rangleより無限遠点となり、PA+sAQAP_A + s_AQ_A成分はϕB\phi_BによってϕB(kerϕA)\phi_B(\ker \phi_A)の成分となるから、kerϕA\ker \phi_A'となって無限遠点となる。

以上より、ker(ϕAϕB)=PA+sAQA,PB+sBQB\ker (\phi_A' \circ \phi_B) = \langle P_A + s_A Q_A, P_B + s_B Q_B \rangleが成り立ち、これを添字を変える事でker(ϕBϕA)=Pa+sAQA,PB+sBQB\ker (\phi_B' \circ \phi_A) = \langle P_a + s_AQ_A, P_B + s_BQ_B \rangleを示す事が出来る。

これで、同一の核を持つことから、同種写像としても同じ(特に終域の楕円曲線のj不変量)ものとなるので、AliceとBobで同じ値を共有出来ることとなる。

4番目の課題としてSIDHの実装をテンプレートが配られた上で課されたので次のように実装した。

import sys
import json

# public parameters
(lA, eA), (lB, eB) = (2, 91), (3, 57)
(lA, eA), (lB, eB) = (2, 250), (3, 159)
p = lA**eA * lB**eB - 1
assert is_prime(p)

F = GF(p**2, "z")
E = EllipticCurve(F, [1, 0])
PA, QA = lB**eB * E.gen(0), lB**eB * E.gen(1)  # PA, QA \in E[lA**eA]
PB, QB = lA**eA * E.gen(0), lA**eA * E.gen(1)  # PB, QB \in E[lB**eB]

assert (lA**eA * PA).is_zero()
assert (lA**eA * QA).is_zero()
assert (lB**eB * PB).is_zero()
assert (lB**eB * QB).is_zero()


def encode_p2(v):
    """encode v in F_{p^2} into json serializable"""
    v = v.polynomial()
    return [int(x) for x in list(v)]


def decode_p2(xs):
    return F(xs)


def prime_power_isogeny(H, l, e, check=False, Ps=[]):
    # check H is a l^e-torsion point of E
    if check:
        assert l^e * H == E(0)

    curve = H.curve()
    for i in range(e):
        # get l-torsion point from H (l^(e-i)-torsion point)
        _H = l^(e - i - 1) * H
        phi = curve.isogeny(_H)
        # update
        curve = phi.codomain()
        H = phi(H)

        Ps = [phi(P) for P in Ps]

    return curve, Ps


if __name__ == '__main__':
    # generate pub/priv parameters
    if sys.argv[1] == "alice":
        # ...
        sA = randint(0, lA**eA - 1)
        EA, UV = prime_power_isogeny(PA + sA*QA, lA, eA, Ps=[PB, QB])
        U = UV[0]
        V = UV[1]
        UAx, UAy = U.xy()
        VAx, VAy = V.xy()
        aA,bA = EA.a4(), EA.a6()

        # exchange parameters
        # {
        #   E: {a: ..., b: ...}  # parameter of curve. E: y^2 = x^3 + ax + b
        #   U: {x: ..., y: ...}  # coordinates of phi(P)
        #   V: {x: ..., y: ...}  # coordinates of phi(Q)
        # }
        print(json.dumps({
            "U": {"x": encode_p2(UAx), "y": encode_p2(UAy)},
            "V": {"x": encode_p2(VAx), "y": encode_p2(VAy)},
            "E": {"a": encode_p2(aA), "b": encode_p2(bA)},
        }))
        params = json.loads(input())

        # ...
        EBa = decode_p2(params["E"]["a"])
        EBb = decode_p2(params["E"]["b"])
        EB = EllipticCurve(F, [EBa, EBb])
        UB = params["U"]
        UBx = decode_p2(UB["x"])
        UBy = decode_p2(UB["y"])
        UB = EB((UBx, UBy))
        VB = params["V"]
        VBx = decode_p2(VB["x"])
        VBy = decode_p2(VB["y"])
        VB = EB((VBx, VBy))

        EBA, _ = prime_power_isogeny(UB + sA*VB, lA, eA)

        shared = EBA.j_invariant()

        print("shared key is:", shared, file=sys.stderr)

    elif sys.argv[1] == "bob":
        # ...
        sB = randint(0, lB**eB - 1)
        EB, UV = prime_power_isogeny(PB + sB*QB, lB, eB, Ps=[PA, QA])
        U = UV[0]
        V = UV[1]
        UBx, UBy = U.xy()
        VBx, VBy = V.xy()
        aB,bB = EB.a4(), EB.a6()

        # exchange parameters
        # {
        #   E: {a: ..., b: ...}  # parameter of curve. E: y^2 = x^3 + ax + b
        #   U: {x: ..., y: ...}  # coordinates of phi(P)
        #   V: {x: ..., y: ...}  # coordinates of phi(Q)
        # }
        print(json.dumps({
            "U": {"x": encode_p2(UBx), "y": encode_p2(UBy)},
            "V": {"x": encode_p2(VBx), "y": encode_p2(VBy)},
            "E": {"a": encode_p2(aB), "b": encode_p2(bB)},
        }))

        params = json.loads(input())
        # ...
        EAa = decode_p2(params["E"]["a"])
        EAb = decode_p2(params["E"]["b"])
        EA = EllipticCurve(F, [EAa, EAb])
        UA = params["U"]
        UAx = decode_p2(UA["x"])
        UAy = decode_p2(UA["y"])
        UA = EA((UAx, UAy))
        VA = params["V"]
        VAx = decode_p2(VA["x"])
        VAy = decode_p2(VA["y"])
        VA = EA((VAx, VAy))

        EAB, _ = prime_power_isogeny(UA + sB*VA, lB, eB)

        shared = EAB.j_invariant()

        print("shared key is:", shared, file=sys.stderr)

ちなみに、演習中は何故か動かなかったのだが、prime_power_isogeny()l, e引数をAliceとBobで逆にしていたのが原因だったので帰ってきてここを直したら動いた。

GPST Attack §

SIDHに対する攻撃として有名なものに、この夏に発表されたそもそもSIDH自体が脆弱だというものがあるが、それ以外にも攻撃が存在し、特にAliceが同一の秘密鍵sAs_Aを使いまわしている時に、Bobが自由な値でSIDHを実行出来てしかも鍵共有の可否(同一の値が両者で共有されたか?)が判明する時にAliceの秘密鍵sAs_Aをリーク出来る攻撃がGPST Attackである。

末尾1bitをリークする例が簡単なのでまずそれから紹介する。Bobは公開鍵としてϕB(PA),ϕB(QA),EB\phi_B(P_A), \phi_B(Q_A), E_BをAliceに渡すが、ϕB(QA)\phi_B(Q_A)の代わりにlAeA1ϕB(PA)+ϕB(QA)l_A^{e_A - 1} \phi_B(P_A) + \phi_B(Q_A)を送る事を考える。

この時Aliceがプロトコル通りにSIDHを実行すると次を計算して同種写像ϕA\phi_A'を計算する。

ϕA=EB/ϕB(PA)+sA(lAeA1ϕB(PA)+ϕB(QA))=EB/(ϕB(PA)+sAϕB(QA))+sAlAeA1ϕB(PA) \begin{aligned} \phi_A' &= E_B/ \langle \phi_B(P_A) + s_A(l_A^{e_A - 1} \phi_B(P_A) + \phi_B(Q_A)) \cr &= E_B/ \langle (\phi_B(P_A) + s_A\phi_B(Q_A)) + s_Al_A^{e_A - 1} \phi_B(P_A) \rangle \end{aligned}

ここで、PAP_AlAeA1l_A^{e_A - 1}-ねじれ点なのでもしsAs_AlAl_Aを約数として含んでいれば、sAlAeA1ϕB(PA)=Os_Al_A^{e_A - 1}\phi_B(P_A) = Oになる。特にSIDHではlA=2l_A = 2が用いられる事が多いのでsAs_Aが偶数なら、OOになるし、奇数なら非零な点となる。

これによってϕA\phi_A'の形も変わり、sAs_Aが偶数の時は余分な項が無限遠点となるので、BobがϕB(QA)\phi_B(Q_A)を通常通り公開した時と何ら変わらない同種写像となり、何の問題もなく鍵共有が行われる。一方、sAs_Aが奇数の時は、sAlAeA1ϕB(PA)s_A l_A^{e_A - 1} \phi_B(P_A)が無限遠点とならないため、核が異なることから通常とは別の同種写像となり、Bobとは別の楕円曲線が終域となる。よって、AliceとBobとで異なるj不変量が計算されることから、鍵共有は失敗する。

したがって、この成否をオラクルとして得られるのであれば、sAs_Aの末尾ビットをリーク出来る。

1bitだけリーク出来てもあまり嬉しさは無いが、これを応用することでsAs_Aの全てをリーク出来る。このためにはϕB(PA),ϕB(QA)\phi_B(P_A), \phi_B(Q_A)を次のように変える。

ϕB(PA)ϕB(PA)2eAi1KiϕB(QA)ϕB(QA)(1+2eAi1)ϕB(QA) \begin{aligned} \phi_B(P_A) &\to \phi_B(P_A) - 2^{e_A-i-1}K_i\phi_B(Q_A) \cr \phi_B(Q_A) &\to (1+2^{e_A-i-1})\phi_B(Q_A) \end{aligned}

ここでiiは特定したいビットのインデックスで、KiK_iii未満の既知ビット(但しK0=0K_0 = 0)とする。この状況でAliceは次のような同種写像を計算する。

ϕA=EB/ϕB(PA)2eAi1KiϕB(QA)+sA(1+2eAi1)ϕB(QA)=EB/(ϕB(PA)+sAϕB(QA))+2eAi1(sAKi)ϕB(QA) \begin{aligned} \phi_A' &= E_B/\langle \phi_B(P_A) - 2^{e_A-i-1}K_i\phi_B(Q_A) + s_A(1+2^{e_A-i-1})\phi_B(Q_A) \rangle \cr &= E_B/ \langle (\phi_B(P_A) + s_A\phi_B(Q_A)) + 2^{e_A - i - 1}(s_A - K_i)\phi_B(Q_A) \rangle \end{aligned}

ここで、sAKis_A - K_iは末尾iiビットが0となるので2i2^iの倍数となる。よって、もしsAs_Aiiビット目が0であれば2i+12^{i+1}の倍数となり、1であれば2i+12^{i+1}の倍数とはならない。その前の係数2eAi12^{e_A - i - 1}と併せて考えると、sAs_Aiiビット目が0なら2eAϕB(QA)=O2^{eA}\phi_B(Q_A) = Oとなり、一方で1なら無限遠点には収まらない。というわけで先ほどと同様のオラクルがあればsAs_Aが使い回されている限りは全てのビットをリーク出来る。

講義では、前節のSIDHのAlice5に対して攻撃を行う課題が宿題として課されたので次のようなスクリプトで解いた。

import sys
import json
from pwn import remote

# public parameters
(lA, eA), (lB, eB) = (2, 250), (3, 159)
p = lA**eA * lB**eB - 1
assert is_prime(p)

F = GF(p**2, "z")
E = EllipticCurve(F, [1, 0])
PA, QA = lB**eB * E.gen(0), lB**eB * E.gen(1)  # PA, QA \in E[lA**eA]
PB, QB = lA**eA * E.gen(0), lA**eA * E.gen(1)  # PB, QB \in E[lB**eB]

assert (lA**eA * PA).is_zero()
assert (lA**eA * QA).is_zero()
assert (lB**eB * PB).is_zero()
assert (lB**eB * QB).is_zero()


def encode_p2(v):
    """encode v in F_{p^2} into json serializable"""
    v = v.polynomial()
    return [int(x) for x in list(v)]


def decode_p2(xs):
    return F(xs)


def prime_power_isogeny(H, l, e, check=False, Ps=[]):
    # check H is a l^e-torsion point of E
    if check:
        assert l^e * H == E(0)

    curve = H.curve()
    for i in range(e):
        # get l-torsion point from H (l^(e-i)-torsion point)
        _H = l^(e - i - 1) * H
        phi = curve.isogeny(_H)
        # update
        curve = phi.codomain()
        H = phi(H)

        Ps = [phi(P) for P in Ps]

    return curve, Ps


known = 0
i = 0
while i < 200:
    sc = remote("localhost", 13337)
    # ...
    sB = randint(0, lB**eB - 1)
    EB, UV = prime_power_isogeny(PB + sB*QB, lB, eB, Ps=[PA, QA])
    U = UV[0]
    V = UV[1]
    _U = U - lA^(eA - i - 1)*known*V
    _V = (1 + lA^(eA - i - 1)) * V
    UBx, UBy = _U.xy()
    VBx, VBy = _V.xy()
    aB,bB = EB.a4(), EB.a6()

    # exchange parameters
    # {
    #   E: {a: ..., b: ...}  # parameter of curve. E: y^2 = x^3 + ax + b
    #   U: {x: ..., y: ...}  # coordinates of phi(P)
    #   V: {x: ..., y: ...}  # coordinates of phi(Q)
    # }
    payload = json.dumps({
        "U": {"x": encode_p2(UBx), "y": encode_p2(UBy)},
        "V": {"x": encode_p2(VBx), "y": encode_p2(VBy)},
        "E": {"a": encode_p2(aB), "b": encode_p2(bB)},
    })

    params = json.loads(sc.recvline())
    sc.sendline(payload.encode())
    # ...
    EAa = decode_p2(params["E"]["a"])
    EAb = decode_p2(params["E"]["b"])
    EA = EllipticCurve(F, [EAa, EAb])
    UA = params["U"]
    UAx = decode_p2(UA["x"])
    UAy = decode_p2(UA["y"])
    UA = EA((UAx, UAy))
    VA = params["V"]
    VAx = decode_p2(VA["x"])
    VAy = decode_p2(VA["y"])
    VA = EA((VAx, VAy))

    EAB, _ = prime_power_isogeny(UA + sB*VA, lB, eB)

    shared = EAB.j_invariant()

    sc.recvuntil(b"shared key is: ")
    alice_shared = F(sc.recvline().decode().strip())
    res = alice_shared != shared

    known += (res << i)

    print(i, bin(known))

    sc.close()
    if i % 8 == 7:
        print(int.to_bytes(int(known), (i+1) // 8, "big"))

    i += 1

prime_power_isogenyが遅く、1時間とちょっとぐらいかかるが無事に求められる。

次回予告 §

次回はpwnyaaさんによる「猫たちと学ぶ量子コンピュータ」の記録と復習を書く予定ですが、160ページに渡る膨大な資料が降ってきたのでいつ終わるかはわかりません。

当日の成果としては、テンソル積やブラケット記法から始まり、基本的なゲートの定義を学んでからBsides Ahmedabad CTF 2021のqunknownという問題を解きました。量子テレポーテーション(正確にはその応用であるゲートテレポーテーションらしい)を実装すると解けるという非常に教育的な問題だったので、しっかりWriteupを書こうと思います。

そんな調子で残りの講義の復習と記録を書いていく予定ですが、自分の講義(ペアリングについてCTFの出題例を元に講義をした)については参加記として書いても特に意味が無いのでCTF Advent Calendar 2022 - Adventarの2022/12/22に枠を取って概要と扱った問題の解説を書こうと思います。ちなみに、この参加記を書いている現在はまだ最終日の分が空いているので誰か埋めてください。


1

LLLのCTFでの使い方を初めて学んだのも、楕円曲線に対する攻撃をまとめて学んだのも、Coppersmith's Attackの実装をする気になったのも全部yoshicampの参加記が発端だった

2

直観的にはll-同種写像同士の合成は次数がl2l^2になるように、最終的にee回合成するとlel^e-同種写像になる、みたいな理解をしている。

3

では、SIDHが破られたのはこの問題が難しいとする仮定が破られたのかというとそうではなく、SIDHでは他にも多くの情報ϕA(PB)\phi_A(P_B)等を開示していたせいで破られたらしい

4

実はこれは群になる

5

前節のSIDHスクリプトはsAs_Aを毎度変えているので、これを固定の値にしたもの