耐量子暗号ML-KEMとは?Cloudflare WorkersのWeb Cryptoで共有秘密を生成・復元してみた

耐量子暗号ML-KEMとは?Cloudflare WorkersのWeb Cryptoで共有秘密を生成・復元してみた

NISTが標準化した耐量子暗号ML-KEMの仕組みと用途を整理し、Cloudflare WorkersのWeb Crypto APIでML-KEM-768をローカル実行しました。flagありでは共有秘密が一致し、flagなしではNotSupportedErrorでした。
2026.10.12

どうも!オペ部の西村祐二です!

Cloudflare WorkersのWeb Crypto APIに、ML-KEMを使う鍵カプセル化・デカプセル化のAPIが追加されました。公式Changelogの表示上の公開日は2026年10月1日です。

今回はローカルで webcrypto_modern_algorithms flagを有効にします。ML-KEM-768のカプセル化で得た共有秘密と、デカプセル化で復元した共有秘密のバイト列が一致するところまで試します。暗号化通信の実装やTLSの設定は扱いません。

ML-KEMとは

ML-KEMは、米国立標準技術研究所(NIST)が2024年8月13日にFIPS 203として標準化した、耐量子暗号の鍵カプセル化メカニズム(KEM)です。NISTの耐量子暗号標準化プロジェクトで選ばれたCRYSTALS-Kyberをもとにしています。安全性は、Module Learning With Errors(MLWE)という格子の問題の解きにくさに基づきます。

FIPS 203は、背景を次のように説明しています。

  • 大規模な量子コンピューターが実現すると、素因数分解や離散対数問題(楕円曲線を含む)の難しさに依存する公開鍵暗号が危険にさらされる
  • ML-KEMは、現時点では量子コンピューターを持つ攻撃者に対しても安全と考えられている

楕円曲線の離散対数問題に依存する方式の身近な例が、TLS 1.3の鍵交換で最もよく使われているX25519(楕円曲線Diffie-Hellman)です。Cloudflareのドキュメントでも、X25519はShorのアルゴリズム(量子コンピューター上で素因数分解や離散対数を高速に解くアルゴリズム)で破られうるとされています。

KEMは、送りたいデータそのものを暗号化する仕組みではありません。公開された通信路越しに、2者が同じ共有秘密を持つための仕組みです。FIPS 203では、こうして確立した共有秘密を共通鍵暗号のアルゴリズムと組み合わせ、暗号化や認証に使うとしています。

パラメータセット

FIPS 203には3つのパラメータセットがあり、後ろの数字が大きいほど安全性が高く、処理は重くなります。セキュリティカテゴリは、破るのに必要な計算量が決められた鍵長のブロック暗号などと比べてどの程度かを表すNISTの区分で、数字が大きいほど強い区分です。

パラメータセット セキュリティカテゴリ 公開鍵 暗号文 共有秘密
ML-KEM-512 1 800バイト 768バイト 32バイト
ML-KEM-768 3 1,184バイト 1,088バイト 32バイト
ML-KEM-1024 5 1,568バイト 1,568バイト 32バイト

FIPS 203は、十分な安全性の余裕と妥当な性能を両立するとして、ML-KEM-768を既定のパラメータセットに推奨しています。Workersで使えるのはML-KEM-768とML-KEM-1024で、今回はML-KEM-768を使います。

どういう用途で使われるか

主な用途は、通信の鍵交換を量子コンピューターに備えたものへ移行することです。「harvest now, decrypt later」(今のうちに暗号化通信を収集・保存しておき、将来量子コンピューターで解読する)攻撃への対策になります。

  • TLSのハイブリッド鍵交換: CloudflareやChrome・Firefox・Safariなどで、X25519とML-KEM-768を組み合わせた X25519MLKEM768 が使われています。Cloudflareのドキュメントでは、ML-KEMに予想外の弱点が見つかっても、現在のX25519と同等の安全性は保たれる構成と説明されています。
  • アプリケーション層での暗号化: 受け手の公開鍵で共有秘密をカプセル化し、その共有秘密をそのまま、または鍵導出して共通鍵として使い、データを暗号化する使い方です。TLSの外で、特定の相手だけが復号できるデータを作りたい場合に向きます。

Cloudflareの対応状況のページによると、ハイブリッド鍵交換はChrome 131以降、Firefox 132以降(デスクトップ)、Safari 26以降などで既定で有効です。Cloudflare経由のサイトにTLS 1.3で接続し、クライアントが対応していれば、この鍵交換が使われます。つまり、対応ブラウザでCloudflare経由のサイトを見ている時点で、TLSのML-KEMは意識しなくても使われています。

今回WorkersのWeb Crypto APIに入ったのは、2つ目の用途のように、アプリケーションのコードからML-KEMを直接呼ぶための機能です。同じ変更で、耐量子暗号の署名方式であるML-DSAも追加されました。

ML-KEMの3つの操作

今回のコードは、Modern Algorithms in the Web Cryptography APIのdraftに沿った次の3操作を使います。

  1. 受け手が generateKey() で公開鍵と秘密鍵を生成し、公開鍵を送り手に渡します。
  2. 送り手が encapsulateBits() に公開鍵を渡し、共有秘密 sharedKey とカプセル化暗号文 ciphertext を得ます。ciphertext を受け手に送ります。
  3. 受け手が decapsulateBits() に秘密鍵と ciphertext を渡し、同じ共有秘密を復元します。

通信路に流れるのは公開鍵と ciphertext だけで、共有秘密そのものは流れません。このサンプルでは、同じWorkerの1リクエスト内で送り手と受け手の両方の処理を行います。

一時プロジェクトを用意する

検証環境は以下のとおりです(2026年10月11日時点)。使い捨てのDockerコンテナ内で動かしました。

項目 バージョン
OS Ubuntu 24.04.5 LTS(コンテナ)、aarch64
Node.js 24.21.0
npm 11.19.0
Wrangler 4.146.0
workerd(Wranglerがローカル実行に使うWorkersランタイム) 1.20261001.1
compatibility date 2026-10-01

Wrangler 4.146.0は、Changelogと同じ2026年10月1日(UTC)にリリースされたバージョンです。記事執筆時点(2026年10月11日)の最新は4.149.0(workerd 1.20261006.1)で、flagありでHTTP 200と同じJSON、flagなしでHTTP 500と同じNotSupportedErrorになることを確認しました。

新しい作業ディレクトリを作り、Wranglerはバージョンを固定してプロジェクト内に導入します。

mkdir -p mlkem-local-tip/src
cd mlkem-local-tip
npm init -y
npm i -D wrangler@4.146.0
npx wrangler --version

npm 11.19.0では、インストール時に npm warn install-scripts workerd@1.20261001.1 (postinstall: node install.js) という警告が出ました。今回は承認(npm install-scripts approve)しないまま進め、以降の手順はそのまま動きました。

flagを有効にしてサンプルを動かす

プロジェクト直下に次の設定を保存します。

wrangler.jsonc
{
  "name": "mlkem-local-tip",
  "main": "src/index.js",
  "compatibility_date": "2026-10-01",
  "compatibility_flags": ["webcrypto_modern_algorithms"]
}

この設定では、compatibility_date に加えて Web Crypto modern algorithmsのcompatibility flagを指定しています。flagを外した場合の挙動は後半で比較します。

次のWorkerは、公式ChangelogのサンプルをHTTPハンドラー内に置き、共有秘密と暗号文のバイト長、一致判定の結果を返す処理を追加したものです。秘密鍵や共有秘密の値はレスポンスに含めません。

src/index.js
export default {
  async fetch() {
    const algorithm = "ML-KEM-768";
    const keyPair = await crypto.subtle.generateKey(algorithm, false, [
      "encapsulateBits",
      "decapsulateBits",
    ]);
    const { sharedKey, ciphertext } = await crypto.subtle.encapsulateBits(
      algorithm,
      keyPair.publicKey,
    );
    const recoveredSharedKey = await crypto.subtle.decapsulateBits(
      algorithm,
      keyPair.privateKey,
      ciphertext,
    );
    const original = new Uint8Array(sharedKey);
    const recovered = new Uint8Array(recoveredSharedKey);
    const equal = original.length === recovered.length &&
      original.every((byte, index) => byte === recovered[index]);

    return Response.json({
      algorithm,
      sharedKeyBytes: sharedKey.byteLength,
      recoveredSharedKeyBytes: recoveredSharedKey.byteLength,
      ciphertextBytes: ciphertext.byteLength,
      equal,
    });
  },
};

wrangler devをローカルモードで起動します。

WRANGLER_SEND_METRICS=false npx wrangler dev --local --ip 127.0.0.1 --port 8791 --show-interactive-dev-session=false

テレメトリ送信と対話UIは無効にしています(動作に必須ではありません)。Ready on http://127.0.0.1:8791 と表示されたら、別のターミナルからアクセスします。

curl -sS -w '\nstatus=%{http_code}\n' http://127.0.0.1:8791/

実行結果はHTTP 200で、次のJSONが返りました。

response.json
{"algorithm":"ML-KEM-768","sharedKeyBytes":32,"recoveredSharedKeyBytes":32,"ciphertextBytes":1088,"equal":true}

flagありの設定でcurlするとHTTP 200とequal: trueが返る

ciphertextBytes はカプセル化暗号文の長さです。共有秘密の32バイトと暗号文の1,088バイトは、FIPS 203に載っているML-KEM-768のサイズと一致しています。equal: true は、生成側と復元側の共有秘密が全バイト一致したことを示します。

flagなしだとどうなるか

比較用に、同じプロジェクトへflagだけを省いた設定を保存しました。ポートを分け、同じ src/index.js を実行します。

wrangler-no-flag.jsonc
{
  "name": "mlkem-local-tip-control",
  "main": "src/index.js",
  "compatibility_date": "2026-10-01"
}
WRANGLER_SEND_METRICS=false npx wrangler dev --config wrangler-no-flag.jsonc --local --ip 127.0.0.1 --port 8792 --show-interactive-dev-session=false

起動後、別のターミナルで次を実行しました。

curl -sS -w '\nstatus=%{http_code}\n' http://127.0.0.1:8792/

結果はHTTP 500で、generateKey() の呼び出し(src/index.js の4行目)で次のエラーが発生しました。スタックトレースのパスは検証コンテナ内の作業ディレクトリです(画像はスタックトレースの行を省いて撮影しています)。

error.txt
NotSupportedError: Unrecognized key generation algorithm "ML-KEM-768" requested.
    at Object.fetch (file:///work/proj/src/index.js:4:41)

flagなしの設定でcurlするとNotSupportedErrorとHTTP 500が返る

条件(コードとcompatibility dateは共通) 観測結果
flagあり HTTP 200、共有秘密が一致
flagなし HTTP 500、鍵生成時にNotSupportedError

検証が終わったら、各 wrangler dev を起動したターミナルで Ctrl+C を押して停止します。

ローカル検証で確認できた範囲

今回確認したのは、ML-KEM-768による鍵生成、カプセル化、デカプセル化です。実行環境は、Wrangler 4.146.0と4.149.0が使うローカルのworkerdです。Cloudflareへデプロイした環境、別クライアントとの通信、macOS・Windows上での実行、ML-KEM-1024は試していません。

公式の対応アルゴリズム表ではML-KEM-768とML-KEM-1024が使え、ML-KEM-512は使えません。また、compatibility flagsのドキュメントには、このflagが既定で有効になる日付が書かれていません。対応アルゴリズム表のページでは、APIは策定途中のdraftに基づいており、今後変更される可能性があるとされています。

まとめ

ML-KEMは、量子コンピューターでも破られにくいとされる、共有秘密を確立するための標準方式です。主要ブラウザのTLSではすでに既定で使われており、Workersでもflagを有効にすれば、アプリケーションのコードから直接呼べるようになりました。

Cloudflare WorkersのWeb CryptoでML-KEM-768を試すには、compatibility_date に加えて webcrypto_modern_algorithms flagの指定が必要でした。flagを付けると、ローカルのwrangler devで鍵生成からデカプセル化までが動き、共有秘密の一致まで確かめられます。既存のWeb CryptoコードにML-KEMを組み込む前に、この最小コードで、使うランタイムとflagの組み合わせで動くかを確かめておくとよさそうです。

この記事をシェアする

関連記事