本文へスキップ
ecdsa.com
← すべての記事

X25519MLKEM768 解説:TLS 1.3 のハイブリッド鍵交換

1 つのキーシェアに ML-KEM-768 の鍵と X25519 の鍵が入り、両方の共有秘密が鍵スケジュールに入力されます。バイト配置、すでにこれを送信しているクライアント、自分のサーバーで確認する方法、そしてこれを処理できない古いネットワーク機器を解説します。

要点:X25519MLKEM768 は、コードポイント 0x11EC(4588)の TLS 1.3 鍵交換グループです。ML-KEM-768 と X25519 を並行して実行し、両方の共有秘密を鍵スケジュールに入力します。RFC 10024(2026年8月)で標準化されています。Chrome、Firefox、バージョン 26 の各 OS に含まれる Apple のネットワーク API、OpenSSL 3.5、Go 1.24 以降、JDK 27 は、デフォルトでこのグループを提示します。OpenSSL 3.5 で openssl s_client -connect example.com:443 -brief を実行し、Negotiated TLS1.3 group: X25519MLKEM768 という行を探してください。このグループは、記録された通信を将来の量子コンピューターから守ります。一方、認証には何の効果もありません。認証は今も RSA または ECDSA の証明書に依存しています。

名前の意味

TLS 1.3 は、名前付きグループを通じて鍵交換をネゴシエートします。クライアントは、対応しているグループを supported_groups 拡張に列挙し、そのうち 1 つ以上のグループの公開鍵を key_share で送信します。サーバーはグループを 1 つ選び、自身のキーシェアで応答します。長年、現実的な選択肢は X25519 と P-256 の ECDH でした。どちらも十分に大きな量子コンピューター上ではショアのアルゴリズムによって破られ、今日記録された通信は、そうした量子コンピューターが実現した時点で解読できるようになります。これが harvest now, decrypt later(今収集し、後で解読)の問題です。

ハイブリッドグループは、新しい数学にすべてを賭けることなく、この問題に対処します。X25519MLKEM768 は、1 回のネゴシエーションで次の 2 つの鍵共有を実行します。

  • ML-KEM-768:FIPS 203(2024年8月)で定められたモジュール格子ベースの鍵カプセル化メカニズムで、カテゴリ 3 のパラメーターを使います。量子コンピューターによる攻撃に耐えるのは、こちらの部分です。
  • X25519:RFC 7748 で定められた楕円曲線ディフィー・ヘルマン関数です。長年にわたる暗号解析の実績があり、新しいほうの構成要素のバックアップになります。

汎用的な構成は RFC 9954(TLS 1.3 のハイブリッド鍵交換、2026年7月)で説明されています。この RFC が掲げる目標は、「構成要素のアルゴリズムのうち 1 つを除くすべてについて暗号化を破る方法が見つかったとしても」安全性を保つことです。具体的なグループは RFC 10024(2026年8月)で定義されています。

グループコードポイント推奨構成要素
X25519MLKEM7680x11EC(4588)YML-KEM-768 + X25519
SecP256r1MLKEM7680x11EB(4587)NP-256 ECDH + ML-KEM-768
SecP384r1MLKEM10240x11ED(4589)NP-384 ECDH + ML-KEM-1024

古いキャプチャには、コードポイント 0x6399(25497)の X25519Kyber768Draft00 が現れることがあります。これは Chrome が最初に提供した、標準化前の Kyber ハイブリッドです。FIPS 203 ではアルゴリズムが小さいながらも互換性のない形で変更されたため、Chrome 131 はこれを 0x11EC に置き換え、ドラフトのグループを廃止しました。Google の発表は、両方をサポートしなかった理由を次のように説明しています。「ポスト量子暗号はサイズが大きすぎるため、2 つのポスト量子キーシェアの予測を同時に提示することはできません」。RFC 10024 は、ドラフトのコードポイントを廃止済みとしています。

キーシェアの中身

ハイブリッドのキーシェアは、2 つの構成要素の値を単純に連結したもので、内部に長さのプレフィックスはありません。サイズは FIPS 203 と RFC 7748 で固定されているため、各フィールドは長さだけで確認できます。

フィールド構成バイト数
クライアントの key_exchangeML-KEM-768 のカプセル化鍵(1184)‖ X25519 の公開鍵(32)1216
サーバーの key_exchangeML-KEM-768 の暗号文(1088)‖ X25519 の公開鍵(32)1120
共有秘密ML-KEM の共有秘密(32)‖ X25519 の共有秘密(32)64

順序に注意してください。名前は X25519 から始まりますが、バイト列は ML-KEM から始まります。RFC 10024 は、この順序が「歴史的な理由により」命名規則とは「逆になっている」と述べています。同 RFC の第 5 節は、コンプライアンス上の理由も挙げています。NIST SP 800-56C では、FIPS で承認された鍵導出に結合した秘密値を使えるのは、最初の入力が承認済みの方式から得られた場合に限られます。この組み合わせでは、それが ML-KEM です。NIST 曲線を使う 2 つのハイブリッドが ECDH のキーシェアを先頭に置くのも、同じ理由によるものです。フィールドを 1184 バイト目ではなく 32 バイト目で分割するパーサーは、毎回失敗します。

次に示すのは、OpenSSL 3.5.7 から cloudflare.com への実際の ClientHello で、2026年10月20日に -trace を付けてキャプチャしたものです(鍵のバイト列は短縮しています)。

openssl s_client -trace、ClientHello(抜粋)
Sent TLS Record
  Content Type = Handshake (22)
  Length = 1551
    ClientHello, Length=1547
        extension_type=supported_groups(10), length=18
          X25519MLKEM768 (4588)
          ecdh_x25519 (29)
          secp256r1 (P-256) (23)
          ecdh_x448 (30)
          secp384r1 (P-384) (24)
          secp521r1 (P-521) (25)
          ffdhe2048 (256)
          ffdhe3072 (257)
        extension_type=key_share(51), length=1258
            NamedGroup: X25519MLKEM768 (4588)
            key_exchange:  (len=1216): A4696B297C695DEA602FB55157BC359F...
            NamedGroup: ecdh_x25519 (29)
            key_exchange:  (len=32): 919D957E3D7E402ECF27C3DD0B38DDA0...

クライアントは 2 つのキーシェアを送信しています。ハイブリッドのものと、単独の X25519 のものです。これが OpenSSL 3.5 のデフォルトです。Go 1.27.3 も同じ組み合わせを送信し、Chromium 156 も同様です。Chromium は GREASE のエントリを追加しており、その ClientHello は 1704 バイトのレコードとして届きます。JEP 527 も、JDK 27 について同じ組み合わせを定めています。単独のキーシェアにかかるコストは 32 バイトで、これがあれば 0x11EC を知らないサーバーでも、別のキーシェアを要求せずに 1 往復でハンドシェイクを完了できます。key_share 拡張のバイト数は計算どおりです。リストの長さが 2 バイト、ハイブリッドのエントリが 2 + 2 + 1216、X25519 のエントリが 2 + 2 + 32 で、合計 1258 バイトになります。サーバーはハイブリッドを選択しました。

openssl s_client -trace、ServerHello(抜粋)
    ServerHello, Length=1206
        extension_type=key_share(51), length=1124
            NamedGroup: X25519MLKEM768 (4588)
            key_exchange:  (len=1120): C01BC77BB1F9985B0DE82CDD1D0DD14B...

比較のために挙げると、同じクライアントに -groups X25519 を指定した場合の ClientHello は 313 バイトです。ハイブリッドによって各方向に約 1.2 KB が加わり、このサイズが互換性の問題の大部分の原因になっています(後述を参照)。

2 つの共有秘密から 1 つの鍵スケジュールへ

両側は、それぞれ互いに独立した 2 つの処理を行います。

  1. クライアントは ML-KEM-768 の鍵ペアと X25519 の鍵ペアを生成し、両方の公開鍵を送信します。
  2. サーバーは ML-KEM の鍵に対してカプセル化を行い、32 バイトの共有秘密と 1088 バイトの暗号文を得ます。さらに、自身の一時鍵で X25519 を実行し、暗号文と自身の X25519 公開鍵を送信します。
  3. クライアントは暗号文をデカプセル化して同じ ML-KEM の共有秘密を復元し、自分の側で X25519 の計算を完了します。
  4. 両側は結果を連結して 64 バイトにし、その値を、TLS 1.3 で通常 (EC)DHE の共有秘密が置かれる位置で使います。つまり、ハンドシェイクシークレットを導出する HKDF-Extract の入力です(RFC 8446 を廃止した 2026年7月の TLS 1.3 改訂版、RFC 9846 §7.1)。

それ以降は通常の TLS 1.3 です。暗号スイート、証明書、CertificateVerify、トラフィック鍵は変わりません。RFC 10024 は、実装が行わなければならない検証手順を追加しています。カプセル化鍵を FIPS 203 の要件どおりにチェックし、長さが正しくない暗号文を拒否し、X25519 の結果がすべてゼロの場合は処理を中止します。いずれかに失敗すると、illegal_parameter または internal_error アラートでハンドシェイクが終了します。

安全性の論拠は単純です。ハンドシェイクを記録した攻撃者がハンドシェイクシークレットを計算するには、両方の共有秘密を復元しなければなりません。ML-KEM が破られていない限り、将来の量子コンピューターで X25519 を破っても十分ではありません。X25519 が破られていない限り、歴史の浅い格子暗号方式に対して予想外の暗号解析が見つかっても十分ではありません。実際の鍵でカプセル化とデカプセル化を確認したい場合は、ML-KEM ラボがブラウザー内でこれらを実行します。

デフォルトで有効にしている実装

2026年10月の時点で、主要なクライアントはすべて、デフォルトでこのグループを提示しています。

ソフトウェアデフォルトになった時期出典
Chrome(デスクトップ)Chrome 131。Kyber のドラフトグループを置き換えGoogle セキュリティブログ
FirefoxFirefox 132(TLS)、Firefox 135(HTTP/3)以降でサポート。設定項目 security.tls.enable_kyber のデフォルトは trueリリースノート
Apple:URLSession と Network フレームワークバージョン 26 の各 OS:iOS、iPadOS、macOS Tahoe、visionOS(ネットワークガイド)、および tvOS と watchOS(セキュリティガイド)Apple サポート、プラットフォームセキュリティ
OpenSSL3.5.0:ハイブリッドグループがデフォルトのリストに入り、デフォルトのキーシェアは X25519MLKEM768 + X25519NEWS.md
Go の crypto/tlsGo 1.24(Config.CurvePreferences が nil の場合)。Go 1.26 で NIST 曲線の 2 つのハイブリッドを追加Go 1.24、Go 1.26
Java(JSSE)JDK 27、JEP 527:デフォルトのリストの先頭が X25519MLKEM768JEP 527

OpenSSL 3.5 にリンクされたソフトウェアも、設定でグループリストを上書きしていない限り、このデフォルトを引き継ぎます。Debian 13 のパッケージや、独自の OpenSSL 3.5 を同梱する Node.js 24 がこれに該当します。Debian 13 では、Python の ssl モジュールが、ほかのグループを一切受け付けない openssl s_server -groups X25519MLKEM768 とのハンドシェイクを完了し、curl と nginx も追加の設定なしでハイブリッドをネゴシエートしました。Apple の TLS セキュリティのページには、1 つの例外が記されています。「SecureTransport API を使用する TLS クライアントは TLS 1.3 を使用できません」。そのため、この API を使い続けているアプリでは、ハイブリッド鍵交換が使われません。

サーバー側でも、HTTPS 通信のかなりの部分が、クライアントが提示すればハイブリッドを選択するスタックですでに終端されています。2026年10月20日に OpenSSL 3.5.7 から調べたところ、cloudflare.com、google.com、www.amazon.com、www.microsoft.com、www.apple.com、example.com とは X25519MLKEM768 がネゴシエートされ、github.com と letsencrypt.org とは単独の X25519 がネゴシエートされました。

サーバー、クライアント、ブラウザーを確認する

コマンドラインから:OpenSSL 3.5 の -brief の出力には、グループ名がそのまま表示されます。

openssl s_client -brief(OpenSSL 3.5.7、2026年10月20日)
$ openssl s_client -connect cloudflare.com:443 -servername cloudflare.com -brief </dev/null
Connecting to 104.16.132.229
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=cloudflare.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE

従来型のグループを選ぶサーバーに接続した場合、出力には代わりに Peer Temp Key: X25519, 253 bits と表示されます。サーバーがそもそもハイブリッドに対応しているかを調べるには、ハイブリッド以外を何も提示しないようにします。従来型にしか対応していないサーバーでは、ハンドシェイクが失敗します。

ハイブリッドのみを提示、非対応のサーバーに対して(抜粋)
$ openssl s_client -groups X25519MLKEM768 -connect github.com:443 -servername github.com -brief </dev/null
...:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:...:SSL alert number 40

このプローブはテストにのみ使用してください。ハイブリッドしか提示しない実際のクライアントは、何も得ることなく互換性を犠牲にします。OpenSSL 3.5 でビルドされた curl は、詳細表示の接続行にグループを表示します。

curl -v(curl 8.14.1、OpenSSL 3.5.7)
$ curl -sv -o /dev/null https://www.google.com/ 2>&1 | grep 'SSL connection'
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / id-ecPublicKey

コードから:Go 1.25 以降ではネゴシエートされたグループを ConnectionState().CurveID で取得でき、Node.js 24 では getEphemeralKeyInfo() で確認できます。

Go:ネゴシエートされたグループを表示
conn, err := tls.Dial("tcp", "cloudflare.com:443", &tls.Config{ServerName: "cloudflare.com"})
if err != nil {
	log.Fatal(err)
}
fmt.Println(conn.ConnectionState().CurveID) // X25519MLKEM768 (Go 1.27.3)
Node.js 24:ネゴシエートされたグループを表示
import tls from "node:tls";
const socket = tls.connect({ host: "cloudflare.com", port: 443, servername: "cloudflare.com" }, () => {
  console.log(socket.getEphemeralKeyInfo()); // { type: 'TLSGroup', name: 'X25519MLKEM768', size: undefined } (Node 24.22.0)
  socket.end();
});

ブラウザーで:Chrome では、DevTools のセキュリティパネルにある接続の詳細にグループが表示されます。同じ値は、DevTools プロトコルの Network.responseReceived イベントの securityDetails.keyExchangeGroup として、プログラムから取得することもできます。2026年10月20日には、ヘッドレスの Chromium 156 が www.cloudflare.com に対して X25519MLKEM768、github.com に対して X25519 を報告しました。macOS 26 では、Apple が nscurl --tls-diagnostics https://your.host を推奨しています。自分のブラウザーが何を送信しているかを確認するには、ブラウザーのポスト量子チェックを開いてください。

自分のサーバーで有効にする

OpenSSL 3.5 を基盤にしていれば、デフォルトのままですでに正しい設定になっています。これが失われるよくある原因は、古い堅牢化ガイドからコピーした明示的なグループリストです。Debian 13(OpenSSL 3.5.7)上の nginx 1.30.5 で行った次のテストでは、ssl_ecdh_curve の行だけを変更し、openssl s_client -brief で確認しています。

nginx 1.30.5:グループは ssl_ecdh_curve に従う(プローブの出力のみ、nginx のログ行は省略)
--- no ssl_ecdh_curve (OpenSSL default list)
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768
--- ssl_ecdh_curve X25519:prime256v1:secp384r1;
Protocol version: TLSv1.3
Peer Temp Key: X25519, 253 bits
--- ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

したがって、修正方法は次のいずれかです。この行を削除するか、行の先頭に X25519MLKEM768 を置きます。古い OpenSSL のリリースはこの名前を知らないため、まず TLS ライブラリをアップグレードしてください。OpenSSL 1.1.1w 上の nginx 1.24.0 は、このような行(ここでは ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;)を nginx: [emerg] SSL_CTX_set1_curves_list("X25519MLKEM768:X25519:prime256v1") failed というエラーで拒否します。ほかのサーバーにも、名前は違っても同じパターンがあります。Apache httpd の SSLOpenSSLConfCmd Groups、HAProxy の curves、Go の CurvePreferences、Java の jdk.tls.namedGroups です。特定のサーバー向けの出発点が必要なら、TLS 設定生成ツールが、ハイブリッドグループを組み込んだ設定を生成します。

見落としやすい場所が、さらに 2 つあります。

  • CDN やロードバランサーの背後にあるオリジン:ブラウザーからの接続はエッジで終わり、エッジからオリジンへの接続は、独自のネゴシエーションを行う別個の TLS セッションです。この 2 つ目の区間で記録された通信も、同じように収集され、後で解読される対象になります。
  • 内部のサービス間 TLS:Go のサービスは、ツールチェーンのバージョンによってハイブリッドを利用できるようになります。Java のサービスは、JEP 527 でハイブリッドグループが追加された JDK 27 から利用できます。CurvePreferences、jdk.tls.namedGroups、その他の明示的なグループリストを固定しているものは、誰かがそのリストを編集するまで、ハイブリッドを利用できません。

キーシェアの予測と HelloRetryRequest

TLS 1.3 のクライアントは、サーバーがどのグループを選ぶかを推測し、そのグループのキーシェアを最初から送信しなければなりません。推測が外れると、サーバーは希望するグループを指定した HelloRetryRequest で応答し、クライアントはやり直します。これには丸 1 往復分の時間がかかります。ポスト量子のキーシェアでは、この推測がより重要になります。1216 バイトのキーシェアを「念のため」送るのにもコストがかかるからです。OpenSSL に、ハイブリッドをリストに含めつつ X25519 のキーシェアだけを送信させると、再試行が発生する様子を確認できます。

openssl s_client -groups X25519:X25519MLKEM768 -trace(抜粋)
    ClientHello, Length=315
        extension_type=key_share(51), length=38
            NamedGroup: ecdh_x25519 (29)
    ServerHello, Length=84                      <- HelloRetryRequest
        extension_type=key_share(51), length=2
            NamedGroup: X25519MLKEM768 (4588)
    ClientHello, Length=1499
        extension_type=key_share(51), length=1222
            NamedGroup: X25519MLKEM768 (4588)
    ServerHello, Length=1206
Negotiated TLS1.3 group: X25519MLKEM768

クライアントが X25519 を先に列挙していたにもかかわらず、サーバーはハイブリッドを優先したため、再試行を要求しました。OpenSSL 3.5 のグループ文字列では、* プレフィックスで、最初のフライトでキーシェアを送るグループを指定します。-groups '*X25519:*X25519MLKEM768' は両方のキーシェアを送信し(ClientHello は 1535 バイト)、再試行を回避します。主要なクライアントのデフォルトが両方を送信するのも、同じ理由です。TLS ワーキンググループのドラフト draft-ietf-tls-key-share-prediction では、サーバーが優先するグループを DNS の HTTPS レコードで公開し、クライアントが最初から正しく推測できるようにします。2026年10月の時点では、まだインターネットドラフトの段階でした。

壊れるもの:大きな ClientHello と古いミドルボックス

前述の計測した ClientHello は、1551 バイトのペイロードで TLS レコードを埋めています。レコード、TCP、IP のヘッダーを加えると、一般的な MTU 1500 バイトの経路では 1 パケットに収まらなくなります。仕様上はこれまでも常に許されていたことですが、ソケットからの最初の read() で ClientHello 全体が返ると想定していた実装もあります。そうした実装は、ハイブリッドによって ClientHello が 2 つのセグメントに分割されると動作しなくなります。

tldr.fail プロジェクトは、この種のバグを追跡しています。同プロジェクトは、問題のサーバーを「ClientHello 全体を読み取るために read() を複数回呼び出さなければならない事態を想定していない」ものと説明しています。そのリストには、複数のベンダーの TLS 検査プロキシ、ファイアウォール、ロードバランサーが含まれています。その後修正されたものもあれば、状況不明とされているものもあります。症状は暗号の問題ではなく、ネットワークの問題のように見えます。Chrome での ERR_CONNECTION_RESET や ERR_SSL_PROTOCOL_ERROR、ハンドシェイクのタイムアウト、ClientHello 直後のリセットなどです。このバグは、多くの場合ハイブリッドを送信するクライアントからしか見えません。Apple もバージョン 26 の各 OS に関する注意事項で同じ障害について警告しており、TLS コードが「大きな ClientHello メッセージの読み取りに失敗する」一部の古いサーバーにデバイスが接続できない可能性があるとしています。

この問題に遭遇したら、次のように対処します。

  1. リセットする機器を特定する:同じ経路で、openssl s_client -groups X25519 とデフォルトのグループリストを比較します。大きな ClientHello だけが失敗するなら、問題は証明書ではなくサイズです。
  2. その機器にパッチを当てるか、交換する:クライアントでハイブリッドを無効にすると、正常に動作していた接続も含め、すべての接続で保護が失われます。
  3. クライアント側のスイッチは一時しのぎとしてのみ使う:Go には GODEBUG=tlsmlkem=0 があり、Go 1.24 のリリースノートでは「大きなレコードを正しく処理できない、バグのある TLS サーバー」向けとして言及されています。Chrome には PostQuantumKeyAgreementEnabled というエンタープライズポリシーがありました。Google はこれを「一時的な措置」であり、「Google Chrome バージョン 145 より後のいずれかの時点で削除される」と説明していました。これを前提に計画を立てる前に、使用している Chrome のバージョンがまだこのポリシーに対応しているかを確認してください。

TLS を終端するプロキシについては、もう 1 つ注意点があります。通信を復号するプロキシは、2 つのハンドシェイクを行います。ノート PC とプロキシの間で何がネゴシエートされても、プロキシから外部への接続については何もわかりません。その区間がポスト量子になるのは、プロキシ自身の TLS スタックがハイブリッドに対応し、かつ優先する場合だけです。

保護できないもの

X25519MLKEM768 は、通信を今記録して後で解読する攻撃者から機密性を守ります。ハンドシェイクの認証をポスト量子にするものではありません。サーバーは今も、RSA または ECDSA の証明書と CertificateVerify の署名で身元を証明します。量子コンピューターでこれらの署名を偽造できる者がいれば、稼働中の接続でサーバーになりすますことができます。この脅威はそうした量子コンピューターが実現して初めて生じるものであり、だからこそ鍵交換が先に移行し、署名は別個の、より時間のかかる移行になっています。その作業は、このシリーズの他の記事で扱っています。ML-DSA の鍵と署名については開発者のための ML-DSA、インベントリや期限を含む全体的な計画については 2026年のポスト量子暗号への移行を参照してください。

また、TLS 1.3 以外の通信にも効果はありません。TLS 1.2 の接続、SSH のセッション、VPN のトンネルには、それぞれ独自の鍵交換と独自のアップグレード経路があります。保護されているとみなす前に、どの接続がまだ TLS 1.2 をネゴシエートしているかを確認してください。

出典

このページのコマンド出力はすべて、2026年10月20日にコンテナで取得したものです。使用したのは、OpenSSL 3.5.7、curl 8.14.1、Node.js 24.22.0、Debian の Python 3(いずれも node:24.22.0-trixie)、Go 1.27.3(golang:1.27.3-trixie)、nginx 1.30.5(nginx:1.30.5)、OpenSSL 1.1.1w 付きの nginx 1.24.0(nginx:1.24.0)、Chromium 156(mcr.microsoft.com/playwright:v1.64.1-noble)です。