平方剰余を理解したい
はじめに
某CTFでCryptoの問題に挑戦したのですが、
よくお世話になるこちらの書籍などを参考にして、Pythonで実装までしようと思います。
※計算することにフォーカスするため、証明は省略します。
以下、
※注意
・🐺が勉強としてやっているだけなので、参考程度に見てください。
・この記事は予告なく加筆・修正される場合があります
平方剰余
まずはじめに、
平方剰余とは
たとえば、
となるので、
ルジャンドル記号
-
が法a における平方剰余のとき、p \left(\frac{a}{p}\right)=1 -
が法a における平方非剰余のとき、p \left(\frac{a}{p}\right)=-1 - (
のとき、a\equiv{0}{\pmod{p}} )\left(\frac{a}{p}\right)=0
ルジャンドル記号は
で求めることができ、これをオイラーの規準といいます。
オイラーの規準は、Pythonで次のように書けます。
def legendre_symbol(a: int, p: int) -> int:
legendre_symbol = pow(a, (p - 1) // 2, p)
if legendre_symbol == 1:
return 1
elif legendre_symbol == p - 1:
return -1
else:
return 0
平方根
p=4k+3 のとき
となります。よって、
となるので、
Pythonで次のように書けます。
def mod_sqrt(a:int, p:int) -> list:
a %= p
x = pow(a, (p + 1) // 4, p)
return [x, p - x]
具体的な値で試してみましょう。奇素数
>>> p = 1122278419650073878178964596327
>>> p % 4
3
>>> a = 299321295248724911916559376201
>>> legendre_symbol(a, p)
1
>>> print(mod_sqrt(a, p))
[239651537938722371683492056230, 882626881711351506495472540097]
>>> pow(239651537938722371683492056230, 2, p) == a
True
>>> pow(882626881711351506495472540097, 2, p) == a
True
p=4k+1 のとき
-
((z/p)=-1 は法z における平方非剰余)となるp を選ぶz\in{Z_{p}^{*}} -
(p-1=2^{s}Q は奇数)となるQ を求めるs, Q - 以下のように初期値を設定する
M=s c=z^{Q}\mod{p} t=a^{Q}\mod{p} R=a^{(Q+1)/2}\mod{p}
- 以下を繰り返す
-
のとき、t=0 r=0 -
のとき、t=1 r=R - それ以外のとき、
となる最小のt^{2i}=1 (0<i<M) を求めてi を計算し、以下のように値を更新するb=c^{2^{M-i-1}}\mod{p} M\leftarrow i c\leftarrow{b^{2}}\mod{p} t\leftarrow{tb^{2}}\mod{p} R\leftarrow{Rb}\mod{p}
-
この結果として得られる
Pythonで次のように書けます。
def mod_sqrt(a:int, p:int) -> list:
a %= p
# 1. 平方非剰余を選ぶ
z = 2
while legendre_symbol(z, p) != -1:
z += 1
# 2. s, Qを求める
s = 0
q = p - 1
while q % 2 == 0:
q //= 2
s += 1
# 3. M, c, t, Rの初期値を設定する
m = s
c = pow(z, q, p)
t = pow(a, q, p)
r = pow(a, (q + 1) // 2, p)
# 4. M, c, t, Rの値を更新する
while t != 1 :
if t == 0: # t = 0のときは0を返す
return [0, 0]
i = 1
while pow(t, pow(2, i), p) != 1:
i += 1
b = pow(c, pow(2, m - i - 1, p))
m = i
c = pow(b, 2, p)
t = (t * b**2) % p
r = (r * b) % p
return [r, p - r]
具体的な値で試してみましょう。こちらも、奇素数
>>> p = 822985351289460529207697791361
>>> p % 4
1
>>> a = 635837859252342443813240556561
>>> legendre_symbol(a, p)
1
>>> print(mod_sqrt(a, p))
[249110913840436119894350236958, 573874437449024409313347554403]
>>> pow(249110913840436119894350236958, 2, p) == a
True
>>> pow(573874437449024409313347554403, 2, p) == a
True
まとめ
完成したスクリプトはこんな感じです。
def legendre_symbol(a: int, p: int) -> int:
legendre_symbol = pow(a, (p - 1) // 2, p)
if legendre_symbol == 1:
return 1
elif legendre_symbol == p - 1:
return -1
else:
return 0
def mod_sqrt(a:int, p:int) -> list:
a %= p
if p % 4 == 3:
a %= p
x = pow(a, (p + 1) // 4, p)
return [x, p - x]
elif p % 4 == 1:
# 1. 平方非剰余を選ぶ
z = 2
while legendre_symbol(z, p) != -1:
z += 1
# 2. s, Qを求める
s = 0
q = p - 1
while q % 2 == 0:
q //= 2
s += 1
# 3. M, c, t, Rの初期値を設定する
m = s
c = pow(z, q, p)
t = pow(a, q, p)
r = pow(a, (q + 1) // 2, p)
# 4. M, c, t, Rの値を更新する
while t != 1 :
if t == 0: # t = 0のときは0を返す
return [0, 0]
i = 1
while pow(t, pow(2, i), p) != 1:
i += 1
b = pow(c, pow(2, m - i - 1, p))
m = i
c = pow(b, 2, p)
t = (t * b**2) % p
r = (r * b) % p
return [r, p - r]
def main():
print(mod_sqrt(3, 11))
# [5, 6]
if __name__ == "__main__":
main()
おまけ
>>> from sage.all import *
>>> p = 822985351289460529207697791361
>>> a = 635837859252342443813240556561
>>> mod(a, p).sqrt(all=True)
[249110913840436119894350236958, 573874437449024409313347554403]
Discussion