5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RSA暗号に対する攻撃 - 共通の公開モジュラスに対する攻撃

Posted at

1. はじめに

この記事では、RSA暗号に対する攻撃が成立する要件と、その対策について述べる。

RSA暗号では2つの大きな素数($=p, q$)を使って、次の数字を生成する。

  • 公開モジュラス $N = p \times q$
  • オイラー関数 $\phi(N) = (p - 1) \times (q - 1)$
  • 公開指数 $\gcd(e, \phi(N)) = 1$ となるような $e$
  • 秘密指数 $ed \equiv 1 \pmod{N}$ を満たす $d$

これらの値は通信ごとに異なるものを使用するのが好ましい。

今回はこれらの数字のうち、公開モジュラス $N$ を共通にした時成立する攻撃について考える。

2. 共通モジュラスに対する攻撃

Aさんが、BさんとCさんそれぞれに共通のメッセージを送ることを考える。このとき、メッセージの暗号化に利用する公開モジュラスを共通にする。それ以外の数値は、それぞれ異なるものを利用する。

2つの公開鍵 $(e_1, N), (e_2, N) $を利用して共通のメッセージ $M$ を暗号化すると、それぞれの暗号文は次のようになる。

  • $C_1 = M ^ {e_1} \pmod{N}$
  • $C_2 = M ^ {e_2} \pmod{N}$

攻撃者はこの2つの暗号文を得られたとする。このとき、拡張ユークリッドの互除法を利用して

$$
e_1 x_1 + e_2 x_2 = \gcd(e_1, e_2)
$$

この式が成り立つような $x_1, x_2$ を得られた場合、メッセージを復元することができる。

\begin{aligned}
C_1^{x_1} C_2^{x_2}
  &\equiv (M^{e_1})^{x_1}(M^{e_2})^{x_2} \pmod{N} \\
  &\equiv M^{e_1 x_1 + e_2 x_2} \pmod{N}
\end{aligned}

このとき、$\gcd(e_1, e_2) = 1$ すなわち $e_1, e_2$ が互いに素であるとき、平文 $M$ を取り出せる。

\begin{aligned}
C_1^{x_1} C_2^{x_2} &\equiv M^{e_1 x_1 + e_2 x_2} \pmod{N} \\
                    &\equiv M
\end{aligned}

3. Pythonによる実装

これらの一連の攻撃を Python で実装したものを次に示す。

import random

def miller_rabin(n, L = 50):
    """与えられた整数 n が素数かどうかを確率的に判定する(Miller–Rabin 素数判定法)

    Args:
        n (int): 判定対象の整数
        L (int): テストの繰り返し回数(多いほど精度が上がる)。デフォルトは50。

    Returns:
        bool: n が素数と判定された場合は True、合成数と判定された場合は False。
    """
    if n == 2:
        return True
    
    if (n < 2) or (n % 2 == 0):
        return False
    
    t = (n - 1) >> 1
    
    while (t % 2) == 0:
        t >>= 1
        
    for _ in range(L):
        a = random.randint(2, n - 1)
        tmp = t
        b = pow(a, tmp, n)
        
        while (tmp != n - 1) and (b != 1) and (b != n - 1):
            b = pow(b, 2, n)
            tmp <<= 1
            
        if (b != n - 1) and (tmp % 2 == 0):
            return False
        
    return True


def generate_prime(bits=16):
    """指定されたビット長のランダムな素数を生成する

    Args:
        bits (int): 素数のビット長(例:16, 512, 1024など)

    Returns:
        int: 生成された素数
    """
    while True:
        candidate = random.getrandbits(bits) | 1
        if miller_rabin(candidate):
            return candidate

def gcd(a, b):
    """2つの整数の最大公約数をユークリッドの互除法で求める

    Args:
        a (int): 整数 a
        b (int): 整数 b

    Returns:
        int: a と b の最大公約数
    """

    while b:
        a, b = b, a % b
    return a    
    

def generate_e(phi_n):
    """φ(N) と互いに素な公開指数 e をランダムに生成する

    Args:
        phi_n (int): φ(N) = (p - 1) * (q - 1)

    Returns:
        tuple[int, int] (e1, e2)
            - e1, e2: φ(N) と互いに素な e1(RSA の公開指数)
    """
    while True:
        e1 = random.randrange(3, phi_n, 2)
        e2 = random.randrange(3, phi_n, 2)
        
        if gcd(e1, phi_n) == 1 and gcd(e2, phi_n) == 1 and gcd(e1, e2) == 1:
            return (e1, e2)
    
def extended_euclid(a, b):
    """拡張ユークリッドの互除法を用いて、ax + by = gcd(a, b) を満たす (g, x, y) を求める

    Args:
        a (int): 整数 a
        b (int): 整数 b

    Returns:
        tuple[int, int, int]: (g, x, y)
            - g: a と b の最大公約数
            - x, y: ax + by = g を満たす整数
    """
    if b == 0:
        return (a, 1, 0)
    
    g, x1, y1 = extended_euclid(b, a % b)
    x = y1
    y = x1 - (a // b) * y1
    return (g, x, y)

def generate_m(N):
    """メッセージを作成する(1 <= M < N かつ gcd(M, N) = 1 を満たす)

    Args:
        N (int): モジュラス N(N > 1 を想定)

    Returns:
        int: 生成されたメッセージ M(1 <= M < N)
    """
    if N <= 1:
        raise ValueError("N must be > 1")

    while True:
        m = random.randrange(1, N)
        
        if gcd(m, N) == 1:
            return m


# ----- 2つの素数を作る ----- 
p = generate_prime(16)
q = generate_prime(16)

print("p =", p)
print("q =", q)

# ----- 公開モジュラスとそのオイラー関数を作る ----
N = p * q
phi_n = (p - 1) * (q - 1)

print(f"N = {N}")
print(f"phi_n = {phi_n}")

# ----- 公開指数 e1, e2 を作る -----
e1, e2 = generate_e(phi_n)
print(f"e1 = {e1}, e2 = {e2}\n")

# ----- メッセージ M を作る -----
M = generate_m(N)

# ---- M の暗号文を2つ、それぞれ異なる公開指数で作成する -----
C1 = pow(M, e1, N)
C2 = pow(M, e2, N)

g, x, y = extended_euclid(e1, e2)
recovered_message = pow(C1, x, N) * pow(C2, y, N) % N

print(f"Recovered M = {recovered_message}")
print(f"Original M = {M}")

if recovered_message == M:
    print("✅ success")
else:
    print("❌ fail")

コードを実行した結果を次に示す。

p = 2671
q = 11353
N = 30323863
phi_n = 30309840
e1 = 1973899, e2 = 18506453

Recovered M = 16776568
Original M = 16776568
✅ success

結果から元の$M$と、復元された$M$は一致していることがわかる。

4. まとめと対策

この攻撃はRSA暗号自体の脆弱性ではなく、ここまでで述べたように、非常に限定された状況でのみ成立するものである。

  • 公開モジュラス$N$が同じ
  • メッセージ$M$が同じ
  • 異なる公開指数$e_1, e_2$の最大公約数が$1$

これらの条件を満たして、初めて成立する攻撃である。しかし、$N$を共通にすることで攻撃のリスクは確実に上昇するため、共通の公開モジュラス$N$を使用することを避けるのがベストである。

5. 参考資料

[1] Pythonで学ぶ暗号理論

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?