離散対数問題 (DLP) への攻撃手法と Python & SageMath による実装のまとめです。 暗号技術として、Diffie-Hellman鍵共有などの安全性は「離散対数問題」に依存しています。 今年のセキュリティキャンプ2020の暗号解読ゼミでは、離散対数問題をテーマにしている方がいたので、話についていくために独学で勉強したときのまとめです。 なお、楕円曲線上の離散対数問題について勉強したときのまとめは次の記事で説明しています。
離散対数問題 (DLP)
有限体 について、素数 と生成元 、 が与えられたとき、 を満たす を探すことを離散対数問題といいます。 英語では DLP (Discrete Logarithm Problem) と言ったり、有限体上を強調するために FFDLP (Finite Field DLP) と書いたりします。
ここでは次の4つの離散対数問題への攻撃手法について説明します。
- Baby-step Giant-step法
- Pollard's rho法 (ポラード・ロー)
- Pohlig–Hellman法 (ポーリッヒ・ヘルマン)
- 指数計算法
Baby-step Giant-step法
離散対数問題 の を求める方法の1つである Baby-step Giant-step法 (ベイビーステップ・ジャイアントステップ; BSGS) は数え上げ法よりも少ない回数の演算でできますが、多くの格納領域を必要とします。
まず、 とおき、 を で割ったときの商 と余り の式を作ります。
この をBSGSで求めることで、離散対数問題の を満たす最小の を求まります。 まず、次のように式変形をします。
次に、Baby-stepの処理として、左辺の計算を事前にし、集合 を求めます。なお は余りなので 未満の整数となります。
もし、集合 の中に があれば、 より で なので離散対数問題が解かれます。 それ以外のときは、Giant-stepの処理として、右辺の計算をします。 に対して が集合の中に含まれるかを調べ、含まれるときは と に保存した から で離散対数問題が解かれます。
実装は以下の通りです(Python 3.8 以降)
def babystep_giantstep(g, y, p):
m = int((p-1)**0.5 + 0.5)
# Baby step
table = {}
gr = 1 # g^r
for r in range(m):
table[gr] = r
gr = (gr * g) % p
# Giant step
gm = pow(g, -m, p) # gm = g^{-m}
ygqm = y # ygqm = y * g^{-qm}
for q in range(m):
if ygqm in table: # 右辺と左辺が一致するとき
return q * m + table[ygqm]
ygqm = (ygqm * gm) % p
return None
g = 7
y = 765686981
p = 35808104999
x = babystep_giantstep(g, y, p)
print(x)
print(pow(g, x, p) == y)
Pollard's rho法
離散対数問題 の を求める方法の1つである Pollard's rho法(ポラード・ローのアルゴリズム)はBSGSと同じ計算量ですが、使用するメモリ空間は定数量のみです。 なので、BSGSよりも少ないメモリ空間で離散対数を計算することができます。
まず、関数 を次のように定義します(これはランダムに移動するための関数です)。
次に、数列 について考えます。乱数 を選び、 とします。 このときの数列 を漸化式 で計算すると、 この数列の項は となります。 ここで、 で、 は関数 の定義から次のようになります。
ポラード・ローのアルゴリズムでは、 を求めるために、まず を満たす を求めます。 は が生成する巡回群なので、 となる が存在します。つまり、
が成立します。よって、次の式から離散対数 を求めることができます。
を満たす の探し方は、巡回群の数列 は周期的であるので、フロイドの循環検出法(ウサギとカメのアルゴリズム)を使って求めます。
実装は以下の通りです(Python 3.8 以降)
def pollard_rho(g, y, p):
q = (p-1) // 2
# ランダムに移動するための関数
def new_xab(x, a, b, g, y, p, q):
subset = x % 3
if subset == 0:
return ((x*x) % p, (a*2) % q, (b*2) % q)
if subset == 1:
return ((x*g) % p, (a+1) % q, b )
if subset == 2:
return ((x*y) % p, a , (b+1) % q)
# フロイドの循環検出法
x, a, b = 1, 0, 0
X, A, B = x, a, b
for i in range(1, p):
x, a, b = new_xab(x, a, b, g, y, p, q)
X, A, B = new_xab(X, A, B, g, y, p, q)
X, A, B = new_xab(X, A, B, g, y, p, q)
if x == X:
break
res = ((a - A) * pow(B - b, -1, q)) % q
if pow(g, res, p) == y:
return res
if pow(g, res + q, p) == y:
return res + q
return None
g = 7
y = 765686981
p = 35808104999
x = pollard_rho(g, y, p)
print(x)
print(pow(g, x, p) == y)
Pohlig–Hellman法
Pohlig–Hellman法(ポーリッヒ・ヘルマンのアルゴリズム)は巡回群 の位数 が に因数分解できるとき、 での離散対数 の計算問題を、素数位数 での離散対数問題に帰着させることで問題を小さくして解き、得られた複数の結果から、最後に中国人剰余定理で答えを求める方法です。
まず、 が素数 に素因数分解できるとします。
離散対数 は任意の自然数なので、商 と余り を使って と書くことができます。 次に の両辺を 乗すると、以下のように式変形できます。 なお、途中でオイラーの定理 を使っています。
見やすくするために、 とおくと、
となり、最初に示した離散対数問題の形になりますが、余り は割る数 よりも小さいという事実から、より高速に離散対数 を求めることができます。
そして より の位数は です。ここから、全ての素因数での離散対数 をまとめると次のようになります。
は互いに素なので、中国人剰余定理(CRT)を使って、これらを満たす離散対数 を求めることができます。
実装は以下の通りです(SageMath 9.0, Python 3.8 以降)
# Baby-step Giant-step法
def babystep_giantstep(g, y, p, q=None):
if q is None:
q = p - 1
m = int(q**0.5 + 0.5)
# Baby step
table = {}
gr = 1 # g^r
for r in range(m):
table[gr] = r
gr = (gr * g) % p
# Giant step
try:
gm = pow(g, -m, p) # gm = g^{-m}
except:
return None
ygqm = y # ygqm = y * g^{-qm}
for q in range(m):
if ygqm in table: # 左辺と右辺が一致するとき
return q * m + table[ygqm]
ygqm = (ygqm * gm) % p
return None
# Pohlig–Hellman法
def pohlig_hellman_DLP(g, y, p):
crt_moduli = []
crt_remain = []
for q, _ in factor(p-1):
x = babystep_giantstep(pow(g,(p-1)//q,p), pow(y,(p-1)//q,p), p, q)
if (x is None) or (x <= 1):
continue
crt_moduli.append(q)
crt_remain.append(x)
x = crt(crt_remain, crt_moduli)
return x
g = 2
y = 1094511311619717224471473901707
p = 2 * 32803 * 196159 * 1981991353 * 47814426923 + 1 # 素数p
x = pohlig_hellman_DLP(g, y, p)
print(x)
print(pow(g, x, p) == y)
指数計算法
離散対数問題 の を求める方法の1つである指数計算法(Index Calculus Algorithm)は、BSGSやロー法 (法) よりも効率の良いアルゴリズムです。
合同式 を解くために、一つの上界 を決めます。任意の整数 の全ての素因数 が 以下のとき、この集合(因子基底)は と書き、このとき整数 は スムーズ(-smooth)といいます。
まず、因子基底の全ての元に対して離散対数を計算します。乱数 を選び、 が スムーズな数であるかを調べます。 スムーズな数であれば、その素因数分解を計算します ()。これが十分な数 個の合同式が集まるまで繰り返すと、次の式が得られます。
これらの合同式は、以下のように式変形することができます ( と、巡回群の位数は なので)。
次の目標は離散対数 を求めることです。この値はすべての合同式の各項で共通なので、行列の掛け算の形にすることができます。
行列の形にしたことで、ガウスの消去法により、 を求めることができます。 ここまでの事前計算が終わったら、最後に、求める離散対数 を計算します。 まず、乱数 を選び、以下の式を計算します。
この値が スムーズになるまで乱数を選びなおします。 もし スムーズであれば因数分解します。
この合同式は以下のように式変形できます。
ここで 以外は全てわかっているので、離散対数 は以下で求めることができます。
実装は以下の通りです(SageMath 9.0, Python 3.8)
# nがBスムーズな数であるかを調べる
def is_Bsmooth(b, n):
factors = list(factor(int(n)))
if len(factors) != 0 and factors[-1][0] <= b:
return True, dict(factors)
else:
return False, dict(factors)
# 連立合同式を求める
def find_congruences(B, g, p, congruences=[]):
unique = lambda l: list(set(l))
bases = []
max_equations = prime_pi(B)
while True:
k = randint(2, p-1)
ok, factors = is_Bsmooth(B, pow(g,k,p))
if ok:
# TODO: 線形独立のときだけ追加する
congruences.append((factors, k))
if len(congruences) >= max_equations:
break
bases = unique([base for c in [c[0].keys() for c in congruences] for base in c])
return bases, congruences
# 連立合同式を行列に変換する
def to_matrices(R, bases, congruences):
M = [[c[0][base] if base in c[0] else 0 \
for base in bases] for c in congruences]
b = [c[1] for c in congruences]
return Matrix(R, M), vector(R, b)
# 指数計算法
def index_calculus(g, y, p, B=None):
R = IntegerModRing(p-1)
if B is None:
B = ceil(exp(0.5*sqrt(2*log(p)*log(log(p)))))
bases = []
congruences = []
# 合同式を満たす行列ができるまで繰り返す。
for i in range(100):
bases, congruences = find_congruences(B, g, p, congruences)
M, b = to_matrices(R, bases, congruences)
# Mx = b を満たす行列xを求める
try:
exponents = M.solve_right(b)
break
except ValueError:
# matrix equation has no solutions
continue
else:
return None
# ag^y mod p がBスムーズである指数kを決定する
while True:
k = randint(2, p-1)
ok, factors = is_Bsmooth(B, (y * pow(g,k,p)) % p)
if ok and set(factors.keys()).issubset(bases):
print('found k = {}'.format(k))
break
print('bases:', bases)
print('q:', factors.keys())
dlogs = {b: exp for (b,exp) in zip(bases, exponents)}
x = (sum(dlogs[q] * e for q, e in factors.items()) - k) % (p-1)
if pow(g, x, p) == y:
return x
return None
g = 2
y = 330456869054588
p = 924614919573299
x = index_calculus(g, y, p)
print(x)
print(pow(g, x, p) == y)
数体ふるい法
指数計算法を改良したものに「数体ふるい法」があります。 いろいろ論文を見ながら特殊数体ふるい法の実装を試みたのですが、特定の値のときしか離散対数問題を解くことができないプログラムができてしまいました。 一応、数体ふるい法のPython実装は検索してもあまりないので、誰かの役に立てばいいなと思い、残骸をGistに残しておきます。
[WIP] Special Number Field Sieve (SageMath) – Gist
数体ふるい法で参考にした論文:
- Worked Example for the Special Number Field Sieve … 計算途中の値があるので実装時のテストに使えます
- An Introduction to the General Number Field Sieve … 5.5 Sieving や 5.6 Forming the Matrix の行列の作り方などが参考になります
- 離散対数問題の困難性に関する計算量についての調査・研究報告書 (CRYPTOREC) … 2.2 有限体上の離散対数問題へのNFS とかが参考になる (数少ない日本語での解説)
- smallnfs/smallnfs.sage at master · lgremy/smallnfs … 数体ふるい法のSageMathによる実装らしきもの。正しく動作するか未確認
以上です。
🎄この記事は セキュリティキャンプ Advent Calendar 2020 - Adventar の9日目です。明日は TumoiYorozu さんで「キャンプの講義の記事」です。お楽しみに🎄
参考文献
- J.A.ブーフマン 著、林 芳樹 訳『暗号理論入門 原書第3版』丸善出版 2012
- Douglas R. Stinson 著、櫻井幸一 監訳『暗号理論の基礎』共立出版 1994
- 代数曲線暗号とその安全性 - 第 15 回 整数論サマースクール 報告集, pp.223-238