Diffie-Hellman鍵交換の理論と実装:離散対数問題・Safe Prime・MITM対策・X25519までPythonで解説

Diffie-Hellman 鍵交換 + 離散対数問題 + Python実装 (sympy.isprime / sympy.primitive_root / pow(g,a,p) / gmpy2.powmod / hashlib.sha256) で TLS/SSH の鍵共有を解説。Safe prime・MITM対策・ECDH・X25519・Signal Triple-DH まで体系的に整理。

なぜ Diffie-Hellman を学ぶか

Diffie-Hellman 鍵交換 (DH) は 1976 年に Whitfield Diffie と Martin Hellman が提案した、現代公開鍵暗号の原点となるプロトコルです。それ以前の暗号は、通信前にあらかじめ秘密鍵を共有しておく共通鍵暗号しか存在せず、「鍵をどう安全に渡すか」という根本問題が未解決のままでした。DH は公開された通信路だけで共通の秘密鍵を確立できることを示し、その翌年に登場する RSA 暗号 と並んで公開鍵暗号時代の幕を開けました。

DH と RSA は混同されがちですが、目的が異なります。RSA は「メッセージ自体を暗号化する」公開鍵暗号方式であり、DH は「メッセージは暗号化せず、共通鍵を作るためだけに使う」鍵共有プロトコルです。実運用では、DH で共有した鍵を AES などの共通鍵暗号に渡してメッセージを暗号化する、というハイブリッド方式が標準です。

現代の DH の応用は広く、TLS 1.3 の鍵交換は ECDHE (Ephemeral ECDH) が必須SSH では DH group exchange または curve25519-sha256Signal プロトコルでは Triple Diffie-Hellman (3DH/X3DH) が採用されています。いずれも DH の数学的核心は同じで、本記事ではそれを Python 実装と共に深掘りします。基盤となる高速指数計算については 繰り返し二乗法による高速べき乗剰余計算 を、暗号領域全体の俯瞰は 暗号技術ロードマップ を参照してください。

離散対数問題 (DLP)

DH の安全性は離散対数問題 (Discrete Logarithm Problem; DLP) の計算困難性に基づきます。素数 \(p\) と \(\mathbb{Z}_p^*\) の生成元 (原始根) \(g\) を固定したとき、ある \(h \in \mathbb{Z}_p^*\) に対して

\[ g^x \equiv h \pmod{p} \tag{1} \]

を満たす整数 \(x\) を離散対数と呼びます。順方向の \(g^x \bmod p\) は 繰り返し二乗法 によって \(O(\log x)\) で計算できますが、逆方向の \(x\) の復元は、現在知られている最速のアルゴリズム (Number Field Sieve, Pollard’s rho) でも準指数時間 (\(L_p[1/3, c]\) ) を要し、\(p\) を 2048 bit 以上にすれば事実上解けないとされています。この指数計算は易しく、対数計算は困難という非対称性こそが、 RSA 暗号 における素因数分解と同じく、公開鍵暗号の数学的基盤となります。

原始根 (primitive root)

\(\mathbb{Z}_p^*\) の位数は \(p-1\) で、\(g\) が原始根であるとは、\(g\) の冪 \(\{g^0, g^1, \dots, g^{p-2}\}\) が \(\mathbb{Z}_p^*\) 全体を尽くすことを言います。これは \(g\) の位数が \(p-1\) に等しいことと同値です。原始根が存在する \(\mathbb{Z}_p^*\) は巡回群となり、後述のように共通鍵 \(K = g^{ab} \bmod p\) が \(\mathbb{Z}_p^*\) 全体に均等に分布することが保証されます。

プロトコル本体

通信したい Alice と Bob は、まず公開パラメータ \((p, g)\) を合意します。\(p\) は十分大きな素数、\(g\) は \(\mathbb{Z}_p^*\) の原始根です。これらは秘密にする必要はなく、RFC 3526 / RFC 7919 で標準化された値を使うのが実務的です。

Step 1. Alice はランダムに秘密整数 \(a \in [2, p-2]\) を選び、公開鍵 \(A\) を計算します。

\[ A = g^a \bmod p \tag{2} \]

Step 2. Bob も同様に秘密整数 \(b\) を選び、公開鍵 \(B\) を計算します。

\[ B = g^b \bmod p \tag{3} \]

Step 3. Alice と Bob は公開鍵 \(A, B\) を公開通信路上で交換します。盗聴者 Eve は \((p, g, A, B)\) を全て観測できる前提です。

Step 4. Alice は Bob から受け取った \(B\) と自身の秘密 \(a\) から共通鍵 \(K\) を計算します。

\[ K_A = B^a \bmod p = (g^b)^a \bmod p = g^{ab} \bmod p \tag{4} \]

Step 5. Bob も同様に \(A\) と自身の秘密 \(b\) から共通鍵を計算します。

\[ K_B = A^b \bmod p = (g^a)^b \bmod p = g^{ab} \bmod p \tag{5} \]

冪指数の交換則により \(K_A = K_B = g^{ab} \bmod p\) となり、両者は同じ共通鍵 \(K\) を共有できます。実運用ではこの \(K\) をそのまま AES の鍵に使うのではなく、SHA-256 などの ハッシュ関数 / KDF に通して鍵導出するのが標準です (後述)。

Eve は \(A, B\) から \(g^{ab}\) を直接計算する手段を持ちません。これを計算的 Diffie-Hellman 仮定 (CDH 仮定) と呼び、より強い判定 Diffie-Hellman 仮定 (DDH 仮定) と合わせて DH の安全性の根拠になっています。CDH が解ければ DLP も解けますが、その逆は自明ではないことに注意してください。

安全性の根拠と Safe Prime

DH の安全性は単に「\(p\) が大きい」だけでは担保されません。攻撃者は部分群攻撃 (small subgroup attack) によって、\(g\) の位数が小さい部分群に共通鍵を閉じ込め、Pohlig–Hellman アルゴリズムで離散対数を高速に解けてしまいます。これを防ぐために、\(p-1\) の素因数分解に小さな素因数が混ざらないようにする必要があります。

最も標準的な対策は Safe Prime の使用です。素数 \(p\) が

\[ p = 2q + 1 \quad (q \text{ も素数}) \tag{6} \]

の形をとるとき、\(p\) を Safe Prime、\(q\) を Sophie Germain 素数と呼びます。このとき \(p-1 = 2q\) なので \(\mathbb{Z}_p^*\) の位数 \(p-1\) の素因数は \(2\) と \(q\) だけになり、攻撃可能な小部分群が存在しなくなります。生成元としては位数 \(q\) の元を選び、\(\mathbb{Z}_p^*\) の位数 \(q\) の部分群 (平方剰余の集合) 上で DH を行うのが現代的な作法です。

鍵長の目安は次の通りです。

用途\(p\) のビット長等価な対称鍵強度
短期テスト1024 bit80 bit (非推奨)
一般用途 (最低)2048 bit112 bit
長期保護3072 bit128 bit
高セキュリティ4096 bit152 bit
トップシークレット8192 bit200 bit

NIST SP 800-57 では、現代の用途には最低 2048 bit、できれば 3072 bit 以上を推奨しています。1024 bit DH は 2015 年の Logjam 攻撃で実用的に破られたため、現在は使用すべきではありません。

MITM 攻撃と認証付き鍵交換

DH 単体の最大の弱点は中間者攻撃 (Man-in-the-Middle; MITM) です。Eve が Alice/Bob の間に介在し、\(A\) を \(A' = g^{a'}\) に、\(B\) を \(B' = g^{b'}\) に置き換えれば、Alice は Eve と \(g^{a b'}\) を、Bob は Eve と \(g^{a' b}\) を共有してしまい、Eve は両者の通信を平文で読めてしまいます。

上段: 正規のDiffie-Hellman鍵交換ではAlice・Bobがそれぞれ計算する共通鍵K_A, K_Bが一致し、Eveは盗聴しかできない。下段: 中間者攻撃(MITM)ではEveが公開鍵A, Bをそれぞれ自分の値A’, B’に差し替え、AliceとはK_AE、BobとはK_EBという別々の鍵を共有してしまい、両者の通信を平文で中継・盗聴できる

図から分かるように、正規のプロトコルと MITM の違いは「Eve が通信路上のデータを読むだけか、書き換えるか」の一点に尽きます。DH 自体には \(A, B\) の発信元を確認する手段が組み込まれていないため、Eve が能動的に介入すれば検出できません。

この攻撃が成立する根本原因は、DH では公開鍵 \(A, B\) が「相手のもの」である保証がないことです。対策は公開鍵の真正性を別途認証することで、実務では以下のいずれかを組み合わせます。

  1. 署名付き DH (Signed DH): Alice は \(A\) を秘密鍵で署名 ( RSA や ECDSA) し、Bob は対応する公開鍵証明書 (PKI) で検証します。TLS 1.2 の ECDHE-RSA がこれに該当します。
  2. PAKE (Password-Authenticated Key Exchange): 共有パスワードを使って DH を補強します。SPAKE2, OPAQUE などが代表例で、低エントロピーの秘密でも MITM 耐性を持たせます。
  3. STS プロトコル (Station-to-Station): DH の公開鍵交換に署名と認証を組み込んだ古典的設計。
  4. TLS 1.3 / Noise Protocol: 証明書認証と ECDHE を統合し、ハンドシェイクの 1RT 化と前方秘匿性を両立。

OAuth 2.0 / OIDC の文脈での認証アーキテクチャは OAuth 2.0/OIDC 入門 に、組織レベルのゼロトラスト設計は ゼロトラストセキュリティ にまとめています。

Python による実装

教育向け最小実装

sympy の素数判定・原始根計算と、Python 組み込みの pow(g, a, p) (内部で 繰り返し二乗法 を使用) だけで、Safe Prime ベースの DH を素直に実装できます。

import secrets
import hashlib
from sympy import isprime, primitive_root


def gen_safe_prime(bits: int) -> tuple[int, int]:
    """p = 2q + 1 を満たす Safe Prime (p, q) を生成"""
    while True:
        q = secrets.randbits(bits - 1) | (1 << (bits - 2)) | 1
        if not isprime(q):
            continue
        p = 2 * q + 1
        if isprime(p):
            return p, q


def derive_key(shared_secret: int) -> bytes:
    """共通鍵 g^{ab} mod p から SHA-256 で 256bit セッション鍵を導出"""
    # 整数をバイト列にエンコードしてからハッシュ
    nbytes = (shared_secret.bit_length() + 7) // 8
    return hashlib.sha256(shared_secret.to_bytes(nbytes, "big")).digest()


# --- パラメータ生成 (Safe Prime) ---
p, q = gen_safe_prime(512)  # 教育用に 512 bit、実用では 2048 bit 以上
g = primitive_root(p)

# --- Alice ---
a = secrets.randbelow(p - 2) + 2
A = pow(g, a, p)

# --- Bob ---
b = secrets.randbelow(p - 2) + 2
B = pow(g, b, p)

# --- 共通鍵計算 ---
K_alice = pow(B, a, p)
K_bob = pow(A, b, p)
assert K_alice == K_bob

session_key = derive_key(K_alice)
print(f"共通鍵 (先頭 16 byte): {session_key[:16].hex()}")

実際に実行すると、512 bit の Safe Prime が生成され、以下のような出力が得られます (secrets による乱数なので実行のたびに値は変わります)。

p (512bit safe prime) = 13218332357488551467516559264825623790005044520636005396648441145800235071016302612638629864404576627468196025621538508813206460844413702142028153726001579
q = 6609166178744275733758279632412811895002522260318002698324220572900117535508151306319314932202288313734098012810769254406603230422206851071014076863000789
g (primitive root) = 2
A = 12322118143501225073671333285517151118595792560242845888501209290961584402661227352040238111324862055888433304050750471616026040977135181508218566306196028
B = 8448598785130363115807570455163263351759770980869225353744625472479937931109088888964753475781227661278406114486266237549732608889596380136248477220391184
K_alice == K_bob: True
共通鍵(先頭16byte): 5515b8545246fb2e8e2380837217d2d
共通鍵(全32byte): 5515b8545246fb2e8e2380837217d2d50eebdda728a2c0a944f012d00531e5d7

\( K_{\text{alice}} = K_{\text{bob}} \)

の assert が例外を出さずに通過し、両者が独立に計算した \(g^{ab} \bmod p\) が完全に一致していることが確認できます。\(p\) は 512 bit (155 桁強) の Safe Prime で、\(g=2\) が原始根として得られました。

ポイントは 4 点です。

  1. 乱数は secrets: random モジュールは暗号用途には不適切で、CSPRNG である secrets.randbits / secrets.randbelow を使います。
  2. pow(g, a, p) で繰り返し二乗法: Python の pow は C 実装の バイナリ法 なので追加実装は不要です。
  3. hashlib.sha256 で KDF: 生の \(g^{ab}\) をそのまま鍵に使わず、SHA-256 で正規化することで \(\mathbb{Z}_p^*\) 内の偏り (位数 \(q\) の部分群に閉じる) や整数長の不均一を吸収します。
  4. sympy.primitive_root は小さな \(p\) 用です。2048 bit の本格運用では、Safe Prime に対し \(g=2\) が原始根になることが多く、明示的に固定パラメータ (RFC 3526) を使うのが標準です。

固定シードによる再現可能な検証

上記は secrets (CSPRNG) を使うため実行毎に値が変わります。ここでは検証目的に限定して random.seed() で乱数を固定し、同じ入力から同じ出力が再現されることを確認します (本番コードで random を使ってはいけません。乱数を固定する理由は再現性の提示のみです)。

import random
import hashlib
from sympy import isprime, primitive_root

random.seed(20260614)  # 検証用の固定シード。本番は secrets を使う


def gen_safe_prime(bits: int) -> tuple[int, int]:
    while True:
        q = random.getrandbits(bits - 1) | (1 << (bits - 2)) | 1
        if not isprime(q):
            continue
        p = 2 * q + 1
        if isprime(p):
            return p, q


def derive_key(shared_secret: int) -> bytes:
    nbytes = (shared_secret.bit_length() + 7) // 8
    return hashlib.sha256(shared_secret.to_bytes(nbytes, "big")).digest()


p, q = gen_safe_prime(256)
g = primitive_root(p)

a = random.randrange(2, p - 2)
A = pow(g, a, p)

b = random.randrange(2, p - 2)
B = pow(g, b, p)

K_alice = pow(B, a, p)
K_bob = pow(A, b, p)
assert K_alice == K_bob

session_key = derive_key(K_alice)
print(f"p = {p}")
print(f"g = {g}")
print(f"a = {a}")
print(f"b = {b}")
print(f"A = {A}")
print(f"B = {B}")
print(f"K  = {K_alice}")
print(f"session_key.hex() = {session_key.hex()}")

実行結果 (このスクリプトは何度実行しても同じ値になります):

p = 66575616948038377445539429795132878256980195668554356069884263848216667252559
g = 13
a = 22071323109805543890258502561468447972734873442036236587291865671753037173016
b = 43406188405209672097347738883726485118762736511118495407770999805898118744546
A = 17626773632107550791481774575953842105294130954914522230733892518771412915265
B = 27116112464237150180615704853975155125775244034435951887061859079882194029377
K  = 4366696324097873572815777205789404913279110001326052910638077010571506035417
session_key.hex() = c4481ede32245c6bcade44d23066b179d92658f34695a7f757b958b8d70ac2df

読者はこのスクリプトをそのまま実行し、同一の \(p, g, a, b, A, B, K\) が得られることで、DH の核心である

\( K_{\text{alice}} = K_{\text{bob}} = g^{ab} \bmod p \)

を手元で再現・検証できます。

gmpy2 による高速化

\(p\) が 2048 bit を超えると CPython の pow でも体感できる程度に遅くなります。gmpy2.powmod は GMP ライブラリの mpz 演算を呼ぶため、大きな指数で数倍〜十数倍速くなります。

import gmpy2
import secrets

p = gmpy2.mpz("FFFFFFFF" "FFFFFFFF" "C90FDAA2" "2168C234" ...)  # RFC 3526 group 14
g = gmpy2.mpz(2)

a = gmpy2.mpz(secrets.randbits(256))
A = gmpy2.powmod(g, a, p)
# ... 以下同様

実際に pow(g, a, p)gmpy2.powmod(g, a, p) を鍵長 512/1024/2048/3072/4096 bit で 30 回ずつ実行し、平均実行時間を計測しました (macOS arm64, CPython 3.14)。

import time
import secrets
import gmpy2

def rand_odd(bits):
    return secrets.randbits(bits) | 1 | (1 << (bits - 1))

for bits in [512, 1024, 2048, 3072, 4096]:
    p = rand_odd(bits)
    g = secrets.randbelow(p - 2) + 2
    a = secrets.randbelow(p - 2) + 2

    t0 = time.perf_counter()
    for _ in range(30):
        pow(g, a, p)
    t_pow = (time.perf_counter() - t0) / 30

    p_m, g_m, a_m = gmpy2.mpz(p), gmpy2.mpz(g), gmpy2.mpz(a)
    t0 = time.perf_counter()
    for _ in range(30):
        gmpy2.powmod(g_m, a_m, p_m)
    t_gmpy = (time.perf_counter() - t0) / 30

    print(f"{bits:5d} bit: pow()={t_pow*1000:.4f} ms, gmpy2={t_gmpy*1000:.4f} ms, speedup={t_pow/t_gmpy:.2f}x")

実行結果:

  512 bit: pow()=0.9718 ms, gmpy2=0.0728 ms, speedup=13.36x
 1024 bit: pow()=5.2600 ms, gmpy2=0.3716 ms, speedup=14.16x
 2048 bit: pow()=34.0342 ms, gmpy2=1.8802 ms, speedup=18.10x
 3072 bit: pow()=106.8577 ms, gmpy2=6.1426 ms, speedup=17.40x
 4096 bit: pow()=250.5079 ms, gmpy2=13.3575 ms, speedup=18.75x

CPythonのpow()とgmpy2.powmod()の実行時間を鍵長512/1024/2048/3072/4096bitで比較した実測ベンチマーク。左図は対数スケールの実行時間で、鍵長が増えるほどpow()とgmpy2の差が開くことを示す。右図はgmpy2による高速化倍率で、2048bit以上では17〜19倍程度に達する

2048 bit 以上の実用的な鍵長では gmpy217〜19 倍高速という結果になりました。TLS ハンドシェイクのように 1 接続あたり複数回の modexp が発生する場面や、サーバ側で大量の同時接続をさばく場面では、この差がレイテンシとサーバ負荷に直結します。RFC 3526 の 2048 bit 固定パラメータ (group 14) を使う場合、\(g=2\) は既知の原始根なので sympy.primitive_root の呼び出しも省略できます。

実用ライブラリ (cryptography.hazmat)

実運用では自前実装ではなく、検証済みライブラリ (cryptography, PyNaCl, OpenSSL) を使うべきです。Python では cryptography.hazmat.primitives.asymmetric.dh が標準的です。

from cryptography.hazmat.primitives.asymmetric import dh
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes

# 2048 bit の Safe Prime を含む DH パラメータ生成 (RFC 7919 推奨)
parameters = dh.generate_parameters(generator=2, key_size=2048)

# Alice の鍵ペア
alice_private = parameters.generate_private_key()
alice_public = alice_private.public_key()

# Bob の鍵ペア
bob_private = parameters.generate_private_key()
bob_public = bob_private.public_key()

# 共通鍵 (生の DH 出力)
alice_shared = alice_private.exchange(bob_public)
bob_shared = bob_private.exchange(alice_public)
assert alice_shared == bob_shared

# HKDF-SHA256 で 32 byte セッション鍵を導出
session_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=b"handshake data",
).derive(alice_shared)

cryptographyexchange() 内部で公開鍵の妥当性検証 (部分群攻撃のチェック、\(1, p-1\) の排除など) を自動で行います。自前実装ではこの検証を忘れがちで、これだけで本番運用に踏み切る理由になります。

DH vs RSA vs ECDH の比較

3 方式の特徴を整理します。

項目DH (FFDH)RSAECDH
用途鍵交換のみ暗号化・署名・鍵交換鍵交換のみ
数学的基盤DLP (有限体)素因数分解ECDLP (楕円曲線)
1996 年当時の鍵長1024 bit1024 bit160 bit
2026 年推奨鍵長3072 bit3072 bit256 bit (Curve25519)
ハンドシェイクのコスト高 (大きな冪剰余)中 (公開指数 65537)低 (スカラ倍)
前方秘匿性 (PFS)DHE で実現RSA 鍵交換は PFS なしECDHE で実現
量子計算機耐性なし (Shor)なし (Shor)なし (Shor)
TLS 1.3 採用廃止署名のみ必須

ECDH は同じセキュリティ強度を桁違いに短い鍵長で達成できるため、現在の TLS / Signal / WireGuard では ECDH (特に X25519) が標準です。一方で、楕円曲線の実装は手元で書くと脆弱になりがちで、\(\mathbb{Z}_p^*\) 上の DH に比べて検証が難しいというトレードオフがあります。

なお、楕円曲線版の ElGamal 暗号については 楕円曲線 ElGamal 暗号 を参照してください。基礎版の DH 鍵交換は Diffie-Hellman 鍵交換プロトコル:理論と Python 実装 にもまとめてあります。

派生プロトコル

DH の枠組みは数十年にわたって拡張され続けています。

ECDH (楕円曲線 Diffie-Hellman)

有限体 \(\mathbb{Z}_p^*\) の代わりに楕円曲線 \(E(\mathbb{F}_p)\) の有理点群を使います。DH における冪乗 \(g^a\) がスカラ倍 \(aG\) に置き換わり、ECDLP の困難性に依拠します。同じ強度に対して鍵が短く、TLS 1.3 では secp256r1 / secp384r1 / x25519 が標準曲線です。楕円曲線の数学(点加算・スカラー倍算・ECDLP)と Python 実装の詳細は 楕円曲線暗号(ECC)の数学と Python 実装 を参照してください。

X25519 / X448 (Curve25519 系)

Daniel J. Bernstein が設計した Montgomery 曲線 Curve25519 上の ECDH を X25519 と呼びます。設計の特徴は次の通りです。

  • クランプ処理: 秘密鍵の特定ビットを固定して低位部分群攻撃を完全に排除。
  • 定数時間性: スカラ倍を定数時間で実装でき、サイドチャネル攻撃に強い。
  • 公開鍵が一律 32 byte: シリアライズが単純で実装ミスの余地が少ない。

X25519 は OpenSSH、TLS 1.3、Signal、WireGuard、Tor v3 など、現代プロトコルのほぼ全てで採用されています。X448 はより高い強度 (224 bit) が必要な場面用です。

Triple DH (3DH) / X3DH

Signal Protocol で使われる非同期な認証付き鍵交換です。受信者がオフラインでも初回メッセージを送れるよう、3 個 (X3DH では 4 個) の DH 出力を連結して鍵を導出します。

\[ SK = \mathrm{KDF}\bigl( \mathrm{DH}(IK_A, SPK_B) \,\|\, \mathrm{DH}(EK_A, IK_B) \,\|\, \mathrm{DH}(EK_A, SPK_B) \,\|\, \mathrm{DH}(EK_A, OPK_B) \bigr) \tag{7} \]

ここで \(IK\) は長期 ID 鍵、\(SPK\) は署名済み prekey、\(OPK\) は使い捨て prekey、\(EK\) は揮発的鍵です。X3DH の後段に Double Ratchet を接続することで、E2E 暗号メッセンジャの安全性が成立します。

ハイブリッド DH (PQC 移行)

量子計算機の登場に備えて、古典 ECDH と耐量子鍵カプセル化機構 (KEM) を併用するハイブリッド鍵交換が TLS 1.3 の実運用に入っています。設計原理は単純で、\(\mathrm{DH}(\text{ECDH 出力}) \,\|\, \mathrm{KEM}(\text{PQC 出力})\) を KDF に通して 1 本の共通鍵にし、ECDH と PQC の両方が同時に破られない限り安全という「弱いほうに引きずられない」構成にします。

タイムラインは次の通りです。Google Chrome は 2023 年 8 月 (Chrome 116) に X25519 と Kyber-768 を組み合わせた X25519Kyber768Draft00 をデフォルト有効化し、Cloudflare も同時期にオリジンサーバとの通信をこの方式に対応させました。その後 NIST が Kyber を標準化して ML-KEM と改称したのに伴い、2024 年 9 月に Chrome は X25519MLKEM768 へ切り替えています。IETF TLS WG の draft-ietf-tls-ecdhe-mlkem(X25519MLKEM768 / SecP256r1MLKEM768 / SecP384r1MLKEM1024 の 3 方式を規定)は Standards Track で IESG 承認済みの段階まで進んでおり、本記事執筆時点でも継続してメンテナンスされています。ECDH がそれ単体では量子計算機に敗れる(後述)としても、ハイブリッド方式ならその移行期間中の安全性を実務的に確保できます。

まとめ

DH 鍵交換は半世紀にわたり公開鍵暗号の中心であり続け、TLS / SSH / Signal といった現代インフラの安全性を支えています。本記事の要点は次の通りです。

  • 共通鍵 \(K = g^{ab} \bmod p\) が指数の可換性で成立する、エレガントな構造。
  • 安全性は 離散対数問題 と CDH/DDH 仮定に依拠する。
  • Safe Prime \(p = 2q+1\) と原始根の選択で部分群攻撃を防ぐ。
  • MITM 対策には RSA や ECDSA 署名による認証が必須で、DH 単体では使わない。
  • 実装は 繰り返し二乗法 を内部で使い、pow(g, a, p)gmpy2.powmod で高速化できる。
  • 本番では cryptography.hazmatdh モジュールを使い、HKDF-SHA256 で鍵を導出する。
  • 現代のデファクトは ECDH、特に X25519 で、量子時代に向けてハイブリッド方式へ移行中。

DH を完全に理解するには、隣接領域 ( RSA , ElGamal , 楕円曲線, KDF, PKI) を縦横に行き来する必要があります。次に学ぶ題材は 暗号技術ロードマップ から選んでください。

関連記事

関連ツール

参考文献

  • Diffie, W., & Hellman, M. (1976). “New Directions in Cryptography”. IEEE Transactions on Information Theory, 22(6), 644–654.
  • Kivinen, T., & Kojo, M. (2003). “More Modular Exponential (MODP) Diffie-Hellman groups for Internet Key Exchange (IKE)”. RFC 3526.
  • Gillmor, D. K. (2016). “Negotiated Finite Field Diffie-Hellman Ephemeral Parameters for TLS”. RFC 7919.
  • Adrian, D., et al. (2015). “Imperfect Forward Secrecy: How Diffie-Hellman Fails in Practice” (Logjam attack). CCS 2015.
  • Bernstein, D. J. (2006). “Curve25519: new Diffie-Hellman speed records”. PKC 2006.
  • Marlinspike, M., & Perrin, T. (2016). “The X3DH Key Agreement Protocol”. Signal Foundation.
  • Kwiatkowski, K., Kampanakis, P., Westerbaan, B., & Stebila, D. “Post-quantum hybrid ECDHE-MLKEM Key Agreement for TLSv1.3”. IETF TLS Working Group, draft-ietf-tls-ecdhe-mlkem (Standards Track). https://datatracker.ietf.org/doc/draft-ietf-tls-ecdhe-mlkem/
  • Google Security Blog (2023). “Google’s Post-Quantum Cryptography Timeline” — Chrome 116 における X25519Kyber768Draft00 のデフォルト有効化 (2023年8月)。 https://www.theregister.com/2023/08/12/google_chrome_kem/