
最近、OpenSSL 3.5が正式リリースされ、PQC(Post-Quantum Cryptography:耐量子暗号)がデフォルトで有効になったことが話題になっていますね。どうやらOpenSSL 3.5を使うと、量子コンピュータによる将来の解読攻撃にも耐えられるTLS通信が実装できるようです。
そこで今回は、Docker + Python環境でOpenSSL 3.5のPQC対応TLS通信の実装と動作確認を行ってみました。コードを1行ずつ追えば再現できる内容ですので、ぜひ記事を読んで自分の環境で試してみてください。
この記事で分かること
- PQCとは何か、2026年時点でなぜ今すぐ対応が必要なのか
- OpenSSL 3.5でサポートされたML-KEM・ML-DSA・SLH-DSAアルゴリズムの違いと用途
- DockerでOpenSSL 3.5環境を構築し、PythonからPQC対応TLS通信を動かす手順
PQCとは・2026年時点でなぜ対応が必要か
PQC(Post-Quantum Cryptography:耐量子暗号)とは、量子コンピュータによる攻撃にも耐えられる暗号アルゴリズムの総称だ。現在広く使われているRSAやECDH(楕円曲線ディフィー・ヘルマン鍵交換)は、Shorのアルゴリズムを持つ十分な規模の量子コンピュータが実現した時点で即座に破られる。
2026年時点での量子コンピュータの現状を数字で見ると、IBMはKookaburraプロセッサで4,158物理qubitを達成し、MicrosoftとQuantinuumの共同チームは2026年3月に12論理qubitで「信頼できる量子計算」と表現できる水準に到達している。RSA-2048を破るには理論上で数百万の論理qubitが必要なため、現時点でRSAを直接破れる量子コンピュータは存在しない。しかし「今暗号化した通信を記録しておき、将来復号する」というHarvest Now, Decrypt Later(HNDL)攻撃はすでに現実的な脅威だ。政府機関・金融・医療など長期的にデータを保護する必要があるシステムは、今すぐ移行を開始しなければならない。
NISTは2024年8月13日、以下のアルゴリズムを正式にFIPSとして標準化した。
| FIPS番号 | アルゴリズム名(正式・略称) | ベース技術 | 用途 |
|---|---|---|---|
| FIPS 203 | Module-Lattice-Based KEM(ML-KEM)/ CRYSTALS-Kyber | 格子暗号 | 鍵交換(KEM) |
| FIPS 204 | Module-Lattice-Based DSA(ML-DSA)/ CRYSTALS-Dilithium | 格子暗号 | デジタル署名 |
| FIPS 205 | Stateless Hash-Based DSA(SLH-DSA)/ SPHINCS+ | ハッシュベース | デジタル署名 |
| FIPS 206(策定中) | Fast Fourier Lattice-Based Compact Signature(FALCON) | 格子暗号 | デジタル署名 |
「PQC対応って難しそう…」と感じるかもしれないが、OpenSSL 3.5とDockerを組み合わせれば、既存のPython開発環境を壊さずにPQC対応TLS通信の実装から確認まで完結できる。順を追って環境構築から動作確認まで解説する。
OpenSSL 3.5でサポートされたPQCアルゴリズム一覧
OpenSSL 3.5.0は2025年4月8日に正式リリースされ、LTS(Long-Term Support)版として2030年4月8日まで公式サポートが続く。この版からoqs-provider等の外部プラグイン不要で、ML-KEM・ML-DSA・SLH-DSAがネイティブに使えるようになった。
各アルゴリズムの仕様をまとめると以下の通りだ。
| アルゴリズム名 | FIPS | 種別 | 代表的な鍵長 | TLSでの用途 |
|---|---|---|---|---|
| ML-KEM-512 | 203 | KEM(鍵カプセル化) | 公開鍵800B / 秘密鍵1632B | 鍵交換 |
| ML-KEM-768 | 203 | KEM(鍵カプセル化) | 公開鍵1184B / 秘密鍵2400B | 鍵交換(推奨) |
| ML-KEM-1024 | 203 | KEM(鍵カプセル化) | 公開鍵1568B / 秘密鍵3168B | 鍵交換(最高強度) |
| ML-DSA-44 | 204 | デジタル署名 | 公開鍵1312B / 署名2420B | 証明書署名 |
| ML-DSA-65 | 204 | デジタル署名 | 公開鍵1952B / 署名3293B | 証明書署名(推奨) |
| ML-DSA-87 | 204 | デジタル署名 | 公開鍵2592B / 署名4595B | 証明書署名(最高強度) |
| SLH-DSA-SHA2-128s | 205 | デジタル署名(ハッシュベース) | 公開鍵32B / 署名7856B | 証明書署名 |
| X25519MLKEM768 | — | ハイブリッドKEM | — | TLSデフォルト鍵交換 |
ML-KEM(鍵交換)とML-DSA(署名)の役割の違いを明確にしておく。ML-KEMはTLSハンドシェイク中にセッション鍵を安全に共有するために使う。対してML-DSAはサーバ証明書の署名検証に使い、「このサーバが本物かどうか」を証明する役割を担う。TLSを完全にPQC化するには、鍵交換と署名の両方を置き換える必要がある。OpenSSL 3.5のデフォルトでは鍵交換にX25519MLKEM768(古典暗号X25519とML-KEM-768のハイブリッド)が使われ、署名は従来のECDSAのまま運用できるため、段階的な移行が可能だ。
注意:Pythonのssl標準モジュールはPQC未対応
現時点でPythonの標準sslモジュールはPQC未対応だ。 理由はシンプルで、PythonのsslモジュールはOSにインストールされたOpenSSLライブラリをラップしているだけであり、CPythonのビルドシステムがOpenSSL 3.5の新しいPQC APIにまだ対応するバインディングを公開していない。cryptographyパッケージ(PyCA)もOpenSSL 3.5の新機能をexpose中(Issue #12610)であり、2026年6月現在ではpip install cryptographyだけでML-KEMを直接呼び出すAPIは使えない。
現状の制限をまとめると、
import ssl; ssl.PROTOCOL_TLS_CLIENT→ ML-KEM鍵交換のグループ指定APIなしfrom cryptography.hazmat.primitives.asymmetric import mlkem→ モジュール未存在pyOpenSSL→ OpenSSL 3.5ネイティブバインディング未対応
回避策として、DockerコンテナにOpenSSL 3.5をソースビルドし、PythonからはsubprocessでOpenSSL CLIを呼び出す方針を採る。この方法なら既存のPythonプロジェクトに手を加えず、今すぐPQC対応TLS通信を試験・検証できる。
環境構築:OpenSSL 3.5対応Dockerfileを作る
以下のDockerfileはUbuntu 24.04 LTS上でOpenSSL 3.5.xをソースからビルドし、Python 3.12と組み合わせた動作環境を作る。/usr/local/ssl配下にインストールすることでシステムのOpenSSLを上書きせず、安全に並存させる。
まずプロジェクトディレクトリを作成し、以下のファイルを配置する。
# ファイル名: setup.sh
mkdir pqc-lab && cd pqc-lab
touch Dockerfile pqc_tls_client.py
次にDockerfileを作成する。このDockerfileはOpenSSL 3.5をビルドし、opensslコマンドでML-KEMが使えることを確認する。
# ファイル名: Dockerfile
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
ENV OPENSSL_VERSION=3.5.0
ENV OPENSSL_PREFIX=/usr/local/ssl35
ENV PATH="${OPENSSL_PREFIX}/bin:${PATH}"
ENV LD_LIBRARY_PATH="${OPENSSL_PREFIX}/lib64:${OPENSSL_PREFIX}/lib:${LD_LIBRARY_PATH}"
# ビルド依存パッケージ
RUN apt-get update && apt-get install -y \
build-essential \
curl \
wget \
ca-certificates \
perl \
python3 \
python3-pip \
python3-venv \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# OpenSSL 3.5.0 ソースダウンロード & ビルド
WORKDIR /tmp
RUN wget -q "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" \
&& tar xzf "openssl-${OPENSSL_VERSION}.tar.gz" \
&& cd "openssl-${OPENSSL_VERSION}" \
&& ./Configure \
--prefix="${OPENSSL_PREFIX}" \
--openssldir="${OPENSSL_PREFIX}/etc/ssl" \
enable-fips \
shared \
linux-x86_64 \
&& make -j"$(nproc)" \
&& make install_sw install_ssldirs \
&& cd /tmp \
&& rm -rf "openssl-${OPENSSL_VERSION}" "openssl-${OPENSSL_VERSION}.tar.gz"
# ldconfig でライブラリパスを更新
RUN echo "${OPENSSL_PREFIX}/lib64" > /etc/ld.so.conf.d/openssl35.conf \
&& echo "${OPENSSL_PREFIX}/lib" >> /etc/ld.so.conf.d/openssl35.conf \
&& ldconfig
# バージョン確認(ビルド成功チェック)
RUN ${OPENSSL_PREFIX}/bin/openssl version
# 作業ディレクトリ
WORKDIR /app
COPY pqc_tls_client.py /app/pqc_tls_client.py
CMD ["/bin/bash"]
ビルドと起動コマンドは以下の通りだ。ビルドには環境によって5〜10分かかる。
# ファイル名: build_and_run.sh
# イメージビルド(初回のみ)
docker build -t pqc-lab:latest .
# コンテナ起動(インタラクティブシェル)
docker run --rm -it pqc-lab:latest
# --- コンテナ内で確認 ---
openssl version
# 出力例: OpenSSL 3.5.0 8 Apr 2025 (Library: OpenSSL 3.5.0 8 Apr 2025)
openssl list -kem-algorithms | grep -i mlkem
# 出力例: ML-KEM-512, ML-KEM-768, ML-KEM-1024
ML-KEM-512などの行が表示されれば、OpenSSL 3.5のPQCビルドは成功だ。
PythonからOpenSSL 3.5のCLIをsubprocessで呼び出す
PythonのsubprocessモジュールでOpenSSL CLIを呼び出し、ML-KEMを使ったTLSハンドシェイクの実行と結果パースを行う。以下のスクリプトはサーバへのTLS接続を試み、使用されたkeyshare(鍵交換グループ)がX25519MLKEM768であるかを検証する。
まず自己署名証明書とPQC対応のミニHTTPSサーバを立ち上げる準備をする。
# ファイル名: gen_cert.sh(コンテナ内で実行)
# ML-DSA-65 で自己署名証明書を生成する
# 1. 秘密鍵生成
openssl genpkey \
-algorithm ML-DSA-65 \
-out /app/server_mldsa.key
# 2. 自己署名証明書生成(有効期間365日)
openssl req -x509 \
-new \
-key /app/server_mldsa.key \
-out /app/server_mldsa.crt \
-days 365 \
-subj "/CN=pqc-test-server/O=PQC Lab/C=JP"
# 生成確認(署名アルゴリズムがML-DSAであることを確認)
openssl x509 -in /app/server_mldsa.crt -text -noout | grep "Signature Algorithm"
# 出力例: Signature Algorithm: id-ml-dsa-65
次にPythonのsubprocessからOpenSSL s_serverとs_clientを制御して接続確認を行うスクリプトを書く。
# ファイル名: pqc_tls_client.py
import subprocess
import time
import signal
import os
import re
import sys
OPENSSL_BIN = "/usr/local/ssl35/bin/openssl"
CERT = "/app/server_mldsa.crt"
KEY = "/app/server_mldsa.key"
PORT = 14433
def start_server():
"""ML-DSA証明書を使ったTLSサーバをバックグラウンドで起動する"""
cmd = [
OPENSSL_BIN, "s_server",
"-cert", CERT,
"-key", KEY,
"-accept", str(PORT),
"-tls1_3",
"-www",
]
proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
time.sleep(1) # サーバ起動待ち
return proc
def run_client():
"""
X25519MLKEM768 グループを明示指定してサーバに接続する。
ハンドシェイク情報を標準エラーから取得し、使用KEMを確認する。
"""
cmd = [
OPENSSL_BIN, "s_client",
"-connect", f"127.0.0.1:{PORT}",
"-tls1_3",
"-groups", "X25519MLKEM768:X25519", # PQCハイブリッドを優先
"-CAfile", CERT, # 自己署名なのでCA指定
"-brief",
]
result = subprocess.run(
cmd,
input=b"HEAD / HTTP/1.0\r\n\r\n",
capture_output=True,
timeout=10,
)
return result.stdout.decode(errors="replace"), result.stderr.decode(errors="replace")
def parse_handshake(output: str) -> dict:
"""
s_client の出力からTLSバージョン・暗号スイート・鍵交換グループを抽出する。
戻り値: {"tls_version": ..., "cipher": ..., "group": ...}
"""
info = {}
for line in output.splitlines():
if "Protocol :" in line:
info["tls_version"] = line.split(":")[1].strip()
if "Cipher :" in line:
info["cipher"] = line.split(":")[1].strip()
if "Server Temp Key:" in line or "group:" in line.lower():
info["group"] = line.strip()
# X25519MLKEM768 が含まれているかチェック
if "X25519MLKEM768" in line:
info["pqc_kem_confirmed"] = True
return info
def main():
server_proc = start_server()
try:
stdout, stderr = run_client()
combined = stdout + stderr
info = parse_handshake(combined)
print("=== PQC TLS Handshake Result ===")
print(f"TLS Version : {info.get('tls_version', 'N/A')}")
print(f"Cipher Suite: {info.get('cipher', 'N/A')}")
print(f"Group/KEM : {info.get('group', 'N/A')}")
pqc_ok = info.get("pqc_kem_confirmed", False)
print(f"PQC KEM確認 : {'✓ X25519MLKEM768 使用中' if pqc_ok else '✗ PQCハイブリッドKEM未確認'}")
if not pqc_ok:
print("\n--- raw stderr (デバッグ用) ---")
print(stderr[:800])
finally:
server_proc.terminate()
server_proc.wait()
if __name__ == "__main__":
main()
コンテナ内でスクリプトを実行する。
# ファイル名: run_pqc.sh(コンテナ内で実行)
# 証明書がなければ先に生成する
[ -f /app/server_mldsa.key ] || bash /app/gen_cert.sh
# PQC TLS 接続テスト実行
python3 /app/pqc_tls_client.py
正常に接続できた場合の出力例は以下の通りだ。
=== PQC TLS Handshake Result ===
TLS Version : TLSv1.3
Cipher Suite: TLS_AES_256_GCM_SHA384
Group/KEM : Server Temp Key: X25519MLKEM768
PQC KEM確認 : ✓ X25519MLKEM768 使用中
Group/KEMにX25519MLKEM768が表示されれば、ML-KEM-768と古典暗号X25519のハイブリッド鍵交換が成立している証拠だ。
WiresharkでPQCハンドシェイクを確認する
Wiresharkを使うとPQCハンドシェイクのパケットを目視で検証できる。ホストマシン側でWiresharkを起動し、Dockerのループバックインターフェース(loまたはdocker0)をキャプチャ対象にする。
最初にキャプチャを絞り込むフィルタを設定する。
tcp.port == 14433
上記フィルタを入力してEnterを押してからコンテナ内でPythonスクリプトを実行する。キャプチャ停止後、以下の手順でPQCハンドシェイクの証拠を確認する。
- ClientHello を探す:
tls.handshake.type == 1でフィルタ。supported_groups拡張の中に0x11ec(X25519MLKEM768)のグループIDが含まれていればPQC対応クライアントとして動いている - ServerHello を確認する:
tls.handshake.type == 2でフィルタ。key_share拡張で選択されたグループが0x11ecであれば、サーバ側がML-KEM鍵交換に合意したことを意味する - 鍵交換データサイズを見る:ML-KEM-768のカプセル化データは約1,088バイトあり、X25519の32バイトと比べて大きい。ServerHelloパケットのサイズが通常より大きければ(数KB以上)PQC鍵交換が機能している指標になる
- 証明書署名アルゴリズムを確認する:Certificateパケット(
tls.handshake.type == 11)を展開し、signatureAlgorithmがid-ml-dsa-65であれば証明書署名もPQC化されている
TLS 1.3ではデータの大半が暗号化されているため、Wiresharkで読める情報はClientHello/ServerHelloまでに限られる。それだけでもグループID0x11ecの存在がPQC移行の実証として十分機能する。
本番移行ロードマップ
PQCへの移行は一度に完全切替するのではなく、4ステップで段階的に進めるのが現実的だ。各ステップの目的と作業内容を以下に示す。
| ステップ | フェーズ名 | 主な作業 | 目安期間 |
|---|---|---|---|
| 1 | 現状調査 | 使用中の暗号アルゴリズム・証明書・TLS設定を全棚卸し。openssl s_clientで各エンドポイントのkeyshareを確認する | 1〜2週間 |
| 2 | テスト環境検証 | 本記事のDocker環境でML-KEM/ML-DSAの動作確認。既存のPythonサービスとのsubprocess統合テストを実施する | 2〜4週間 |
| 3 | ハイブリッド移行 | 本番のTLS設定にX25519MLKEM768をX25519より上位に配置する。古典暗号との並存で既存クライアントへの影響をゼロにしながらPQC対応クライアントのみが恩恵を受ける状態を作る | 1〜3ヶ月 |
| 4 | 完全移行 | 証明書をML-DSA-65以上に切り替え、古典暗号のみのクライアントサポートを終了する。CAがML-DSA証明書の発行を開始した時点で実施が現実的になる | 2026年末〜2027年 |
ステップ3のハイブリッド移行は、既存システムを壊さずに始められる最も安全な起点だ。NginxやApache httpd、HAProxyはOpenSSL 3.5にリンクし直すだけでX25519MLKEM768グループのネゴシエーションが可能になる。Cloudflare、AWS、Googleはすでにハイブリッドモードをデフォルト有効にしており、2026年時点ではパブリックなTLS通信の一定割合がすでにPQCハイブリッド鍵交換で保護されている。
ステップ4の完全移行の最大の障壁はCA(証明機関)側のML-DSA対応だ。Let's Encryptを含む主要CAがML-DSA証明書の自動発行を開始するまでの間は、自己署名証明書またはプライベートCAで先行検証を続けるのが合理的な判断だ。