Python OpenSSL 3.5 PQC耐量子暗号TLS実装サムネイル

最近、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 203Module-Lattice-Based KEM(ML-KEM)/ CRYSTALS-Kyber格子暗号鍵交換(KEM)
FIPS 204Module-Lattice-Based DSA(ML-DSA)/ CRYSTALS-Dilithium格子暗号デジタル署名
FIPS 205Stateless 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-512203KEM(鍵カプセル化)公開鍵800B / 秘密鍵1632B鍵交換
ML-KEM-768203KEM(鍵カプセル化)公開鍵1184B / 秘密鍵2400B鍵交換(推奨)
ML-KEM-1024203KEM(鍵カプセル化)公開鍵1568B / 秘密鍵3168B鍵交換(最高強度)
ML-DSA-44204デジタル署名公開鍵1312B / 署名2420B証明書署名
ML-DSA-65204デジタル署名公開鍵1952B / 署名3293B証明書署名(推奨)
ML-DSA-87204デジタル署名公開鍵2592B / 署名4595B証明書署名(最高強度)
SLH-DSA-SHA2-128s205デジタル署名(ハッシュベース)公開鍵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で先行検証を続けるのが合理的な判断だ。