この記事は AIによって生成されたコンテンツを含みます。
(スクショ、コマンド出力、ログ、トレースなどには含みません)
はじめに
量子コンピューターの進化により、従来の公開鍵暗号(RSA や ECDSAなど)が将来的に破られる可能性があると言われています。量子コンピューターの脅威に備えるため、耐量子暗号(PQC: Post-Quantum Cryptography)の導入が現実的な課題になりつつあります。
Open Quantum Safe (OQS) は、耐量子暗号の実装と普及を目指すオープンソース・プロジェクトであり、TLSやSSHなどの主要プロトコルにPQCを組み込むためのツール群を提供しています。
本記事では、OQSが提供するDockerイメージ openquantumsafe/nginx をリバース・プロキシー方式で構成することで、既存のレガシー暗号を使用するWebサーバーを変更せずに、nginxで耐量子TLS通信を構築する方法をご紹介します。
OQS nginx Dockerイメージの概要
このDockerイメージは、OpenSSL と OQS Providerを組み込んだ nginx を含み、耐量子の鍵交換と認証を TLS 1.3で実現します。
-
非rootユーザー対応:Kubernetesなどの制限環境でも安全に実行可能
-
耐量子暗号対応:Kyber、Falcon、Dilithiumなど、OQS Providerがサポートするアルゴリズムに対応
-
基本的に通常のnginxと同じ構成ファイルを使用するため、リバース・プロキシーなど一般的なnginxで実現できる構成を実現可能
検証環境の概要
今回の検証は、サーバーとなる2台の仮想マシン(Ubuntu 24.04.2 LTS)と、クライアントとなるWindows 11 マシンで行いました。
構成要素の配置は前出の図と同じで、curl がPQC未対応クライアント、Chrome がハイブリッドPQC対応クライアント、nginx がPQC対応リバース・プロキシー、http-https-echo がPQC未対応アプリとなっています。
TLS通信のネゴシエーションは目に見えないため、以下の方法で可視化を試みています。
-
nginx稼働マシンからtcpdumpを使ってpcapファイルを取得し、Wiresharkで内容を確認
(暗号通信の内容を読み取るのが目的ではなく、ネゴシエーションの過程を観察) -
nginx(リバース・プロキシー構成)のバックエンドに、HTTPリクエストの内容を返すサービスを置き、nginxが挿入したHTTPヘッダー情報を確認
-
Chromeブラウザーの開発者モードを使用し、TLSに関連する情報を表示
検証環境の実装方法
OQS nginxを通常のdockerコマンドで起動する方法については、openquantumsafe/nginx サイトや oqs-demos/nginx/USAGE.md に詳しく説明されていますので、個人的な好みもあり、今回はdocker composeを使用する方法で行います。
常時稼働させるコンテナーの場合、docker composeは比較的管理しやすいと思います。
docker composeの使用
使用したdocker-compose.yml
ファイルは以下のとおりです。
ドキュメントの記述に合わせてポート番号を4433
にしていますが、一般ユーザーからもアクセスしてもらう場合は、443:4433
というports
定義を使用することになると思います。
services:
nginx:
# 使用するDockerイメージ(Open Quantum Safe対応のnginx)
image: openquantumsafe/nginx:latest
# コンテナー名
container_name: nginx.server.my.org
# ポートマッピング(ホストポート:コンテナポート)
ports:
- "4433:4433"
# ボリュームマウント(ホストパス:コンテナパス)
volumes:
# ログディレクトリ
- ./nginx-logs:/opt/nginx/logs
# PKIディレクトリ
- ./server-pki:/opt/nginx/pki
# Nginx 設定ファイルディレクトリ
- ./nginx-conf:/opt/nginx/nginx-conf
# コンテナーが停止された場合、手動停止しない限り自動再起動
restart: unless-stopped
ファイルを置く場所として、oqs-nginx
というフォルダーを作成しました。
このフォルダーの中に、コンテナー内部のフォルダーにマッピングする実体として、nginx-logs, nginx-conf, server-pki というフォルダーを作成(mkdir)しています。
サーバー証明書の作成(自己署名証明書)
nginx が使用するサーバー証明書を作成します。
今回は、ハイブリッドPQCに対応している Chrome を使用して検証を行うため、接続実績のあるサイト:https://test.openquantumsafe.org:6003/ と同じ仕様で、ECDSA(楕円曲線デジタル署名アルゴリズム)を使った証明書を作成したいと思います。
-
鍵と証明書を格納するためのフォルダーを作成します。(docker composeの定義とも合わせます)
mkdir -p server-pki && cd server-pki
※今回の証明書は従来のアルゴリズムを使用するため、PQC対応ではない openssl コマンドで作成しています。(OpenSSL 3.0.13 30 Jan 2024)
-
CA鍵と証明書を作成します。
# CA秘密鍵の作成 openssl ecparam -name prime256v1 -genkey -noout -out CA.key # CA証明書の作成(自己署名) openssl req -x509 -new -key CA.key -out CA.crt -subj "/CN=PQC Test CA" -days 365
-
サーバー証明書のためのOpenSSL設定ファイルを作成します。
openssl-san.cnf
という名前で、以下の内容を書き込みます。[ req ] default_bits = 2048 distinguished_name = req_distinguished_name req_extensions = v3_req prompt = no [ req_distinguished_name ] CN = ubuntu2.iss.local [ v3_req ] subjectAltName = @alt_names [ alt_names ] DNS.1 = ubuntu2.iss.local DNS.2 = localhost
※ブラウザーで
Certificate Alternative Name missing
の警告が出るのを防ぐために、設定ファイルを使用してsubjectAltName
を指定しています。PQC検証の動作に影響があるわけではないので、テスト用に警告を無視する想定であれば、次のopenssl req
コマンドで-config openssl-san.cnf
を省略し、代わりに-subj "/CN=ubuntu2.iss.local"
を付ける方法もあります。
-
サーバー鍵と CSR を作成します。
# サーバー秘密鍵の作成 openssl ecparam -name prime256v1 -genkey -noout -out server.key # CSR(証明書署名要求)の作成(SAN付き) openssl req -new -key server.key -out server.csr -config openssl-san.cnf
-
サーバー証明書に署名します。
openssl x509 -req -in server.csr -out server.crt -CA CA.crt -CAkey CA.key -CAcreateserial -days 365 -extfile openssl-san.cnf -extensions v3_req
途中の openssl コマンドには何も応答がないのですが、最後の
openssl x509
コマンドには以下の応答がありました。Certificate request self-signature ok subject=CN = ubuntu2.iss.local
nginx構成ファイルの作成
oqs-demos/nginx/nginx-conf/nginx.conf をベースに、リバース・プロキシーの定義を加えたものを用意します。
# 使用するワーカープロセス数を自動で決定(CPUコア数に基づく)
worker_processes auto;
events {
# 各ワーカープロセスが同時に処理できる接続数の上限
worker_connections 1024;
}
http {
# MIMEタイプの定義ファイルを読み込む(ファイルの種類に応じたContent-Typeを設定)
include ../conf/mime.types;
# デフォルトのMIMEタイプ(未定義のファイルタイプに適用)
default_type application/octet-stream;
# sendfileを有効化(ファイル送信のパフォーマンス向上)
sendfile on;
# Keep-Alive接続のタイムアウト時間(秒)
keepalive_timeout 65;
# バックエンドサーバーの定義(リバースプロキシ先)
upstream backend_servers {
server 192.168.254.30:8443;
}
server {
# サーバーがSSLで待ち受けるポートとアドレス(4433番ポート)
listen 0.0.0.0:4433 ssl;
# アクセスログとエラーログの保存先
access_log /opt/nginx/logs/access.log;
error_log /opt/nginx/logs/error.log;
# SSL証明書と秘密鍵の指定
ssl_certificate /opt/nginx/pki/server.crt;
ssl_certificate_key /opt/nginx/pki/server.key;
# SSLセッションキャッシュの設定(1MBの共有メモリ)
ssl_session_cache shared:SSL:1m;
# SSLセッションのタイムアウト時間
ssl_session_timeout 5m;
# 使用するSSLプロトコルの指定(TLS 1.3のみ)
ssl_protocols TLSv1.3;
# Open Quantum Safe対応のKEM(鍵共有アルゴリズム)を選択可能 (コメントアウトしていないのでデフォルトを使用) https://github.com/open-quantum-safe/liboqs#supported-algorithms
# Example (nginxがサポートする文字列の長さに制限があるため注意):
# ssl_ecdh_curve oqs_kem_default:frodo976shake:frodo1344shake:p256_kyber512:kyber768:kyber1024:ntru_hps2048509:ntru_hps2048677:ntru_hrss701:lightsaber:saber:kyber512:X25519;
# リバースプロキシの設定(すべてのリクエストをバックエンドに転送)
location / {
# バックエンドサーバーへの転送先
proxy_pass https://backend_servers/;
# クライアント情報をヘッダーに追加(バックエンドでの識別やログに使用)
proxy_set_header Host ubuntu2.iss.local;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# SSL関連の情報をヘッダーに追加(セキュリティ分析やログ用)
proxy_set_header X-SSL-Protocol $ssl_protocol;
proxy_set_header X-SSL-Cipher $ssl_cipher;
proxy_set_header X-SSL-Ciphers $ssl_ciphers;
proxy_set_header X-SSL-Curve $ssl_curve;
proxy_set_header X-SSL-Curves $ssl_curves;
proxy_set_header X-Client-Cert $ssl_client_cert;
proxy_set_header X-Client-Verify $ssl_client_verify;
# バックエンドのSSL証明書検証を無効化(テスト環境などで使用)
proxy_ssl_verify off;
# リダイレクトを無効化(バックエンドからのリダイレクトをそのまま返す)
proxy_redirect off;
}
}
}
ここまでの構成を完了したフォルダー構造は、以下のとおりです。
(テストの都合上、nginx.conf
は、複数の実ファイルをシンボリックリンクで切り替えています)
khrz@ubuntu2:~/oqs-nginx$ ls -lR
.:
total 16
-rw-rw-r-- 1 khrz khrz 761 Oct 17 08:29 docker-compose.yml
drwxrwxr-x 2 khrz khrz 4096 Oct 17 08:30 nginx-conf
drwxrwxr-x 2 khrz khrz 4096 Oct 20 23:04 nginx-logs
drwxrwxr-x 2 khrz khrz 4096 Oct 20 23:06 server-pki
./nginx-conf:
total 8
lrwxrwxrwx 1 khrz khrz 16 Oct 17 05:32 nginx.conf -> nginx_local.conf
-rw-rw-r-- 1 khrz khrz 3634 Oct 17 08:30 nginx_local.conf
-rw-rw-r-- 1 khrz khrz 1571 Oct 16 07:08 nginx_testfire.conf
./nginx-logs:
total 16
-rw-r--r-- 1 khrz khrz 8811 Oct 20 22:45 access.log
-rw-r--r-- 1 khrz khrz 1257 Oct 14 09:55 error.log
./server-pki:
total 28
-rw-rw-r-- 1 khrz khrz 583 Oct 20 23:06 CA.crt
-rw------- 1 khrz khrz 227 Oct 20 23:06 CA.key
-rw-rw-r-- 1 khrz khrz 41 Oct 20 23:06 CA.srl
-rw-rw-r-- 1 khrz khrz 280 Oct 20 23:06 openssl-san.cnf
-rw-rw-r-- 1 khrz khrz 623 Oct 20 23:06 server.crt
-rw-rw-r-- 1 khrz khrz 448 Oct 20 23:06 server.csr
-rw------- 1 khrz khrz 227 Oct 20 23:06 server.key
起動方法は以下のとおりです。
-
コマンドラインで、
docker-compose.yml
ファイルが置かれているフォルダー(今回はoqs-nginx
)に移動(cd)します。 -
以下のコマンドで起動します:
docker compose up -d
バックエンドWebサーバーの構成
本質的な話ではありませんが、nginx をリバース・プロキシーとして構成する以上、実体となるWebサーバーが必要です。
今回は、nginxがセットするヘッダーの情報を可視化するために、軽量な mendhak/http-https-echo というDockerイメージを利用します。
こちらも docker compose で起動したいと思いますので、以下の docker-compose.yml
ファイルを作成します。
services:
http-echo:
# 使用するDockerイメージ(HTTP/HTTPSリクエストの情報をJSONで返すエコーサーバー)
image: mendhak/http-https-echo:31
# ホストのポート8080と8443をコンテナーの同じポートにマッピング
# 8080: HTTP用、8443: HTTPS用
ports:
- "8080:8080"
- "8443:8443"
# コンテナーが停止された場合、手動停止しない限り自動再起動
restart: unless-stopped
使用方法は以下のとおりです。
-
上記
docker-compose.yml
ファイルを、任意のディレクトリーに保存します。 -
コマンドラインで、そのディレクトリーに移動(cd)します。
-
以下のコマンドで起動します:
docker compose up -d
正常に起動すると、通常のブラウザー (または curl) から、稼働マシン上のポート 8080 / 8443 にアクセスできます。
- http://<稼働マシンのIP>:8080
- https://<稼働マシンのIP>:8443
検証結果
以下に検証結果をまとめます。
フローの可視化には、Wiresharkのフローグラフ機能 からエクスポートしたものを利用しています。
ハイブリッドPQCアルゴリズムのTLSネゴシエーション・フロー
以下は、[Chrome] – [OQS nginx] – [バックエンド] 構成における TLSネゴシエーションの様子です。(取得されたpcapからメインのフローをフィルターしたもの)
ネゴシエーションは2箇所で発生しています。
1つ目は [Chrome] – [nginx] 間、2つ目は [nginx] – [バックエンド] 間 です。
このうち、ハイブリッドPQCアルゴリズムの通信が成立しているのは、1つ目だけです。
Chromeがnginxに送ったClient Helloの抜粋
- Internet Protocol Version 4, Src: 192.168.254.104, Dst: 192.168.254.31
- Transmission Control Protocol, Src Port: 56014, Dst Port: 4433, Seq: 1, Ack: 1, Len: 1765
- Transport Layer Security
- [Stream index: 0]
- TLSv1.3 Record Layer: Handshake Protocol: Client Hello
- Content Type: Handshake (22)
- Version: TLS 1.0 (0x0301)
- Length: 1760
- Handshake Protocol: Client Hello
- Handshake Type: Client Hello (1)
- Length: 1756
- Version: TLS 1.2 (0x0303)
- Random: 2994d197b09686e07f11415f72eb9993b53b2bb07cd998db89a2d85d7e7bdff1
- Session ID Length: 32
- Session ID: ef27de22c339ed94af0284fcc439467eb6a7b4a5454a191affd86af8e39022ad
- Cipher Suites Length: 32
- Cipher Suites (16 suites)
- Cipher Suite: Reserved (GREASE) (0x4a4a)
- Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
- Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
- Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9)
- Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
- Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
- Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)
- Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
- Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
- Compression Methods Length: 1
- Compression Methods (1 method)
- Extensions Length: 1651
- Extension: Reserved (GREASE) (len=0)
- Extension: signature_algorithms (len=18)
-
Extension: key_share (len=1263) X25519MLKEM768, x25519
- Type: key_share (51)
- Length: 1263
- Key Share extension
- Client Key Share Length: 1261
- Key Share Entry: Group: Reserved (GREASE), Key Exchange length: 1
- Key Share Entry: Group: X25519MLKEM768, Key Exchange length: 1216
- Key Share Entry: Group: x25519, Key Exchange length: 32
- Extension: extended_master_secret (len=0)
- Extension: encrypted_client_hello (len=250)
- Extension: supported_groups (len=12)
- Type: supported_groups (10)
- Length: 12
- Supported Groups List Length: 10
- Supported Groups (5 groups)
- Supported Group: Reserved (GREASE) (0xfafa)
- Supported Group: X25519MLKEM768 (0x11ec)
- Supported Group: x25519 (0x001d)
- Supported Group: secp256r1 (0x0017)
- Supported Group: secp384r1 (0x0018)
nginxがChromeに返したServer Helloの抜粋
- Internet Protocol Version 4, Src: 192.168.254.31, Dst: 192.168.254.104
- Transmission Control Protocol, Src Port: 4433, Dst Port: 56014, Seq: 1, Ack: 1766, Len: 1871
- Transport Layer Security
- [Stream index: 0]
- TLSv1.3 Record Layer: Handshake Protocol: Server Hello
- Content Type: Handshake (22)
- Version: TLS 1.2 (0x0303)
- Length: 1210
- Handshake Protocol: Server Hello
- Handshake Type: Server Hello (2)
- Length: 1206
- Version: TLS 1.2 (0x0303)
- Random: b21bae7da19c5789b1fa764184701f7562fee327bb979585d8a67ebdff2cbec1
- Session ID Length: 32
- Session ID: ef27de22c339ed94af0284fcc439467eb6a7b4a5454a191affd86af8e39022ad
- Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
- Compression Method: null (0)
- Extensions Length: 1134
- Extension: supported_versions (len=2) TLS 1.3
-
Extension: key_share (len=1124) X25519MLKEM768
- Type: key_share (51)
- Length: 1124
- Key Share extension
- Key Share Entry: Group: X25519MLKEM768, Key Exchange length: 1120
このように、Chrome 側が Client Hello で提示した選択肢の中から、nginx 側が X25519MLKEM768
を選択し、Server Helloで返しています。
X25519MLKEM768
は、TLS 1.3 におけるハイブリッド耐量子鍵交換方式の1つです。従来の楕円曲線暗号(ECDH)である X25519 と、PQCである ML-KEM(Module-Lattice-based Key Encapsulation Mechanism) を組み合わせています。
PQCはまだ新しく、理論的には安全でも、実装や運用面で未知の脆弱性がある可能性があります。そこで、従来の暗号(X25519)とPQC(ML-KEM768)を組み合わせることで、どちらか一方が破られても、直ちに危険にさらされることがないように配慮されています。
従来の暗号アルゴリズムのTLSネゴシエーション・フロー
2つ目のネゴシエーションを見てみましょう。
nginxがバックエンドに送ったClient Helloの抜粋
- Internet Protocol Version 4, Src: 192.168.254.31, Dst: 192.168.254.30
- Transmission Control Protocol, Src Port: 46370, Dst Port: 8443, Seq: 1, Ack: 1, Len: 410
- Transport Layer Security
- [Stream index: 1]
- TLSv1.3 Record Layer: Handshake Protocol: Client Hello
- Content Type: Handshake (22)
- Version: TLS 1.0 (0x0301)
- Length: 405
- Handshake Protocol: Client Hello
- Handshake Type: Client Hello (1)
- Length: 401
- Version: TLS 1.2 (0x0303)
- Random: 5325c660228fb708ef600b9e0967e74bf6dbe217e09fb65f82e1cdc283a9df38
- Session ID Length: 32
- Session ID: bad242c39c58538e95537e779abcd4b2d15124e6d5a21a7a64909a3fb5cea4be
- Cipher Suites Length: 60
- Cipher Suites (30 suites)
- Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
- Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
- Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
- Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9)
- Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0cca8)
- Cipher Suite: TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0ccaa)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
- Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028)
- Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 (0x006b)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027)
- Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (0x0067)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
- Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
- Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
- Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)
- Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
- Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA256 (0x003d)
- Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA256 (0x003c)
- Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
- Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
- Compression Methods Length: 1
- Compression Methods (1 method)
- Extensions Length: 268
- Extension: renegotiation_info (len=1)
- Extension: ec_point_formats (len=4)
- Extension: supported_groups (len=22)
- Type: supported_groups (10)
- Length: 22
- Supported Groups List Length: 20
- Supported Groups (10 groups)
- Supported Group: x25519 (0x001d)
- Supported Group: x448 (0x001e)
- Supported Group: secp256r1 (0x0017)
- Supported Group: secp384r1 (0x0018)
- Supported Group: secp521r1 (0x0019)
- Supported Group: MLKEM512 (0x0200)
- Supported Group: MLKEM768 (0x0201)
- Supported Group: MLKEM1024 (0x0202)
- Supported Group: X25519MLKEM768 (0x11ec)
- Supported Group: SecP256r1MLKEM768 (0x11eb)
- Extension: session_ticket (len=0)
- Extension: encrypt_then_mac (len=0)
- Extension: extended_master_secret (len=0)
- Extension: signature_algorithms (len=156)
- Extension: supported_versions (len=5) TLS 1.3, TLS 1.2
- Extension: psk_key_exchange_modes (len=2)
-
Extension: key_share (len=38) x25519
- Type: key_share (51)
- Length: 38
- Key Share extension
- Client Key Share Length: 36
- Key Share Entry: Group: x25519, Key Exchange length: 32
バックエンドがnginxに返したServer Helloの抜粋
- Internet Protocol Version 4, Src: 192.168.254.30, Dst: 192.168.254.31
- Transmission Control Protocol, Src Port: 8443, Dst Port: 46370, Seq: 1, Ack: 411, Len: 2302
- Transport Layer Security
- [Stream index: 1]
- TLSv1.3 Record Layer: Handshake Protocol: Server Hello
- Content Type: Handshake (22)
- Version: TLS 1.2 (0x0303)
- Length: 122
- Handshake Protocol: Server Hello
- Handshake Type: Server Hello (2)
- Length: 118
- Version: TLS 1.2 (0x0303)
- Random: d3efeef649beba572f2dd3a7597150f06c78712bcd456516cf5496985bcab286
- Session ID Length: 32
- Session ID: bad242c39c58538e95537e779abcd4b2d15124e6d5a21a7a64909a3fb5cea4be
- Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
- Compression Method: null (0)
- Extensions Length: 46
- Extension: supported_versions (len=2) TLS 1.3
-
Extension: key_share (len=36) x25519
- Type: key_share (51)
- Length: 36
- Key Share extension
- Key Share Entry: Group: x25519, Key Exchange length: 32
このように、nginx 側が Client Hello で提示した選択肢として、supported_groups の中には PQCアルゴリズムが含まれていますが、key_share の中には含まれていないため、バックエンド側は従来のアルゴリズムである x25519
を選択し、Server Hello で返しています。
比較のために、Chrome から直接バックエンドとTLSネゴシエーションを行うケースを確認してみます。
Chromeがバックエンドに送ったClient Helloの抜粋
- Internet Protocol Version 4, Src: 192.168.254.104, Dst: 192.168.254.30
- Transmission Control Protocol, Src Port: 61641, Dst Port: 8443, Seq: 1, Ack: 1, Len: 1765
- Transport Layer Security
- [Stream index: 0]
- TLSv1.3 Record Layer: Handshake Protocol: Client Hello
- Content Type: Handshake (22)
- Version: TLS 1.0 (0x0301)
- Length: 1760
- Handshake Protocol: Client Hello
- Handshake Type: Client Hello (1)
- Length: 1756
- Version: TLS 1.2 (0x0303)
- Random: 8c70d7c10f71d7207f69725d1c5d86b16d1b22897ea58ce2ec95db974a1baf6b
- Session ID Length: 32
- Session ID: f97eccd0152d5ba04a5c6ae76f600c0175d65c42b4d93f9fd62e6c271efd0812
- Cipher Suites Length: 32
- Cipher Suites (16 suites)
- Cipher Suite: Reserved (GREASE) (0xcaca)
- Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
- Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
- Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
- Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9)
- Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
- Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
- Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
- Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)
- Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
- Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
- Compression Methods Length: 1
- Compression Methods (1 method)
- Extensions Length: 1651
- Extension: Reserved (GREASE) (len=0)
- Extension: status_request (len=5)
- Extension: compress_certificate (len=3)
- Extension: extended_master_secret (len=0)
- Extension: renegotiation_info (len=1)
- Extension: supported_versions (len=7) TLS 1.3, TLS 1.2
- Extension: session_ticket (len=0)
- Extension: encrypted_client_hello (len=250)
- Extension: application_settings (len=5)
- Extension: psk_key_exchange_modes (len=2)
- Extension: application_layer_protocol_negotiation (len=14)
- Extension: signed_certificate_timestamp (len=0)
- Extension: ec_point_formats (len=2)
-
Extension: key_share (len=1263) X25519MLKEM768, x25519
- Type: key_share (51)
- Length: 1263
- Key Share extension
- Client Key Share Length: 1261
- Key Share Entry: Group: Reserved (GREASE), Key Exchange length: 1
- Key Share Entry: Group: X25519MLKEM768, Key Exchange length: 1216
- Key Share Entry: Group: x25519, Key Exchange length: 32
- Extension: supported_groups (len=12)
- Type: supported_groups (10)
- Length: 12
- Supported Groups List Length: 10
- Supported Groups (5 groups)
- Supported Group: Reserved (GREASE) (0x8a8a)
- Supported Group: X25519MLKEM768 (0x11ec)
- Supported Group: x25519 (0x001d)
- Supported Group: secp256r1 (0x0017)
- Supported Group: secp384r1 (0x0018)
バックエンドがChromeに返したServer Helloの抜粋
- Internet Protocol Version 4, Src: 192.168.254.30, Dst: 192.168.254.104
- Transmission Control Protocol, Src Port: 8443, Dst Port: 61641, Seq: 1, Ack: 1766, Len: 2317
- Transport Layer Security
- [Stream index: 0]
- TLSv1.3 Record Layer: Handshake Protocol: Server Hello
- Content Type: Handshake (22)
- Version: TLS 1.2 (0x0303)
- Length: 122
- Handshake Protocol: Server Hello
- Handshake Type: Server Hello (2)
- Length: 118
- Version: TLS 1.2 (0x0303)
- Random: 76a0ceca5f19880004af39a12c721d52c0b6a514c1dea1462d589c4ffa8dd4cf
- Session ID Length: 32
- Session ID: f97eccd0152d5ba04a5c6ae76f600c0175d65c42b4d93f9fd62e6c271efd0812
- Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
- Compression Method: null (0)
- Extensions Length: 46
- Extension: supported_versions (len=2) TLS 1.3
-
Extension: key_share (len=36) x25519
- Type: key_share (51)
- Length: 36
- Key Share extension
- Key Share Entry: Group: x25519, Key Exchange length: 32
このように、Chrome 側は ハイブリッドPQCアルゴリズムを含む選択肢を Client Hello の key_share で示しましたが、バックエンド側は従来のアルゴリズムである x25519
を選択し、Server Hello で返しています。
nginxが挿入したHTTPヘッダーを確認する
バックエンドで稼働する mendhak/http-https-echo は、リクエスト内容をJSON形式で返すことで、nginxがセットしたヘッダーの情報を可視化することができます。
以下は、[Chrome] – [OQS nginx] – [バックエンド] 構成において、Chromeブラウザーに返されたコンテンツです。"x-ssl-curve" というヘッダーに注目してください。
{
"path": "/",
"headers": {
"host": "ubuntu2.iss.local",
"x-real-ip": "192.168.254.104",
"x-forwarded-for": "192.168.254.104",
"x-forwarded-proto": "https",
"x-forwarded-port": "4433",
"x-ssl-protocol": "TLSv1.3",
"x-ssl-cipher": "TLS_AES_128_GCM_SHA256",
"x-ssl-ciphers": "0x4a4a:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA:AES256-SHA",
"x-ssl-curve": "X25519MLKEM768",
"x-ssl-curves": "0xfafa:X25519MLKEM768:X25519:prime256v1:secp384r1",
"x-client-verify": "NONE",
"connection": "close",
"cache-control": "max-age=0",
"sec-ch-ua": "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"sec-fetch-site": "none",
"sec-fetch-mode": "navigate",
"sec-fetch-user": "?1",
"sec-fetch-dest": "document",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "ja"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "ubuntu2.iss.local",
"ip": "192.168.254.104",
"ips": [
"192.168.254.104"
],
"protocol": "https",
"query": {},
"subdomains": [
"ubuntu2"
],
"xhr": false,
"os": {
"hostname": "5da998794c28"
},
"connection": {
"servername": false
},
"clientCertificate": {}
}
このように、Chromeの場合は、ハイブリッドPQCアルゴリズム X25519MLKEM768
が選択されていることがわかります。
この "x-ssl-curve" というヘッダーは、nginx.conf
内で proxy_set_header X-SSL-Curve $ssl_curve;
として定義されています。$ssl_curve
は、nginxのSSL/TLSモジュールで使用される組み込み変数で、SSL/TLS接続で楕円曲線ディフィー・ヘルマン鍵交換 (ECDHE) に使用された楕円曲線の名前を保持しています。
比較のために、[curl] – [OQS nginx] – [バックエンド] 構成の場合を見てみます。
{
"path": "/",
"headers": {
"host": "ubuntu2.iss.local",
"x-real-ip": "192.168.254.104",
"x-forwarded-for": "192.168.254.104",
"x-forwarded-proto": "https",
"x-forwarded-port": "4433",
"x-ssl-protocol": "TLSv1.3",
"x-ssl-cipher": "TLS_AES_256_GCM_SHA384",
"x-ssl-ciphers": "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA",
"x-ssl-curve": "X25519",
"x-ssl-curves": "X25519:prime256v1:secp384r1",
"x-client-verify": "NONE",
"connection": "close",
"user-agent": "curl/8.14.1",
"accept": "*/*"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "ubuntu2.iss.local",
"ip": "192.168.254.104",
"ips": [
"192.168.254.104"
],
"protocol": "https",
"query": {},
"subdomains": [
"ubuntu2"
],
"xhr": false,
"os": {
"hostname": "80c9e0d7211c"
},
"connection": {
"servername": false
},
"clientCertificate": {}
}
このように、curl の場合は、従来のアルゴリズム X25519
が選択されていることがわかります。
ChromeブラウザーのTLS暗号情報を確認する
Chromeブラウザーの開発者ツールで、TLS接続に使用された暗号アルゴリズムを確認できます。
開発者ツールの Privacy and security
タブを開くと、Security
> Overview
にTLSの情報が表示されます。
ハイブリッドPQCアルゴリズムが使用されている場合 (nginx経由でバックエンドに接続)
従来のアルゴリズムが使用されている場合 (バックエンドにダイレクトに接続)
まとめ
OQS版の nginx をリバース・プロキシー構成で使用することで、バックエンドのWebサービスを変更することなく、ハイブリッドPQCアルゴリズムに対応した通信を行えることが確認できました。将来的には入手可能なほとんどのインフラがPQC対応になると予想されますが、現在はまだ過渡期ですので、プロキシーのような移行ソリューションが必要になる場面も多いと思われます。
図や検証結果には含まれていませんが、Chrome だけでなく Firefox 144 や Edge 141.0.3537.85 でも、ハイブリッドPQC での接続に成功しています。
なお、基本的に既存Webサービスの改修は必要ありませんが、リバース・プロキシーとの相性が悪い場合はリンク切れなどが起こりますので、アプリの改修が必要になる可能性があります。また、PQC未対応のクライアントから接続した場合は警告を出すなど、アプリ側に追加ロジックが欲しくなるかもしれません。
さらに、OQS nginxのサイト上に、THIS IS NOT FIT FOR PRODUCTIVE USE の記載があることについても留意が必要です。重要なアプリケーションを本番環境で利用する場合は、IBM Quantum Safe Remediator のような商用製品の利用についてもご検討ください。