XAES-256-GCM: 拡張ノンス AEAD
(words.filippo.io)- XAES-256-GCM は、高レベル暗号化 API におけるノンス管理のリスクを下げるため、256 ビット鍵と 192 ビットノンスを使う新しい AEAD 仕様
- 大きなノンスにより、メッセージごとに OS の CSPRNG から新しい値を自動生成する方式が可能になり、2⁸⁰ 個のメッセージで衝突リスクを 2⁻³² 程度に抑えることを目指す
- 内部的には、入力鍵と大きなノンスから派生鍵と 96 ビットノンスを作り、そのうえで標準の AES-256-GCM をそのまま使う拡張ノンス構成
- Go の参照実装は
crypto/cipherとcrypto/aesだけで 100 行未満に収まり、メッセージあたり 3 回の AES-256 呼び出しの一部は事前計算できる - FIPS 140 準拠とライブラリ互換性を重視しており、XChaCha20Poly1305・AES-GCM-SIV と並ぶ ノンスなし AEAD API の候補として利用できる
高レベル API 向けの大きなノンスを持つ AEAD
- XAES-256-GCM は、追加認証データ付き認証暗号(AEAD)アルゴリズムで、256 ビット鍵と 192 ビットノンスを使用する
- 設計目標は 3 つに要約される
- 事実上ほぼ無制限に近いメッセージ数でもランダム生成が安全な 大きなノンス のサポート
- 完全かつ直接的な FIPS 140 準拠
- 一般的な暗号ライブラリ上で容易に実装できること
- 大きなノンスのおかげで、ユーザーが birthday bound の計算を自分で行わなくても、メッセージごとに OS の CSPRNG から新しいノンスを読み出す API を作れる
- 準拠性と互換性を優先しているため、ほかの大きなノンスを持つ AEAD を使いにくい環境でも、AEAD が必要な場面に適用できる
AES-256-GCM をそのまま活用する拡張ノンス構成
- XAES-256-GCM は、XChaCha20Poly1305 のように既存の AEAD の上に載せる 拡張ノンス構成
- 入力鍵と 192 ビットノンスから、内部 AES-256-GCM 用の派生鍵と派生ノンスを計算する
- 入力鍵とノンスはそれぞれ
K,N - 派生 AES-256-GCM 鍵とノンスは
Kₓ,Nₓ
- 入力鍵とノンスはそれぞれ
AES-256ₖ呼び出しとノンスの前半部分でKₓを作り、入力ノンスの後ろ 96 ビットをNₓとして使う- メッセージあたり
AES-256ₖ呼び出しが 3 回必要- そのうち 1 回は、与えられた鍵について 事前計算 できる
- 残り 2 回の呼び出しは同じ鍵スケジュールを再利用できる
実装は短く、標準コンポーネントで記述可能
- Go 参照実装 は、事前計算の最適化と大半のボイラープレートを含めても 100 行未満
- Go 実装は標準ライブラリの
crypto/cipherとcrypto/aesだけを使う - XAES-256-GCM は、標準の NIST SP 800-108r1 KDF と標準の NIST AES-256-GCM AEAD としても記述できる
- KDF は counter-based KDF
- PRF は CMAC-AES256
- 入力鍵は
Kin - ラベルは ASCII 文字
X、つまり0x58 - コンテキストは入力ノンスの最初の 96 ビット
- カウンターサイズは 16 ビット
- 任意の
Lフィールドは省略 - 出力は 256 ビット派生鍵
- 派生鍵と入力ノンスの最後の 96 ビットが AES-256-GCM に渡される
- パラメータの選択により、KDF と CMAC の抽象化を取り除くと、単にカウンターに対して AES-256 を呼び出す方式よりわずかに遅く複雑なだけで済む
- 同じパラメータは 高レベル OpenSSL API でもサポートされる
サードパーティ実装とテストベクター
- 2024-06-29 時点の編集で、サードパーティ実装が追加された
- Web Cryptography API 実装は 256 ビット AES-CBC
CryptoKeyを使用する - 仕様には 2 つの主要なコードパスに対する テストベクター が含まれる
MSB₁(L) = 0MSB₁(L) = 1
- 10,000 回または 1,000,000 回のランダム反復を圧縮した 累積テストベクター も提供されている
/11 の断念と代替案
- 以前の構想には
XAES-256-GCM/11という名前があったが、最終仕様では/11を断念した /11は性能最適化だったが、AES-GCM を使う理由の 1 つが FIPS 140 準拠 であるため、ラウンド数を変えると準拠性が失われる- FIPS 140 準拠が目標でないなら、代替案はいくつもある
- AES-GCM-SIV
- AES コアベースの現代的な AEAD 構成
- 仕様の Alternatives セクション は、それぞれの代替案を XAES-256-GCM と比較している
Go とノンスなし AEAD API における位置づけ
- XAES-256-GCM は、安全で、退屈で、準拠可能で、相互運用可能な AEAD を目指している
- 主な用途は、Go に追加したい種類の 高レベル API
- XAES-256-GCM は XChaCha20Poly1305 と AES-GCM-SIV を補完し、仮想的な ノンスなし AEAD API 実装候補として設計された
- Go 標準ライブラリに Go 専用の構成を追加することは好まれないため、ほかの暗号ライブラリのメンテナーからの意見が必要
1件のコメント
Hacker News のコメント
設計が非常に巧妙です。CMAC ベースなので、低レベルのプリミティブがなくても AES-CBC で鍵を導出できます。
AES-CBC の観点では、
L = AES-CBC-256ₖ(iv = 0¹²⁸, plaintext = 0¹²⁸)[:16]から始めてK1を作り、M1、M2を暗号化してKₓを得たうえでNₓ = N[12:]を使う、という形に見えます。AES-CBC-256 は暗号文の最初の 128 ビットブロックだけを返し、パディングブロックは捨てると考えればよく、パディングを無効にできなくても、低レベル実装より同じ鍵での AES 呼び出しが 3 回余分に必要になる程度なので悪くありません。
この特性を利用した WebCrypto API ベースの JS 実装は https://github.com/dchest/xaes にあり、AES-CBC 用の
CryptoKeyをそのまま受け取り、extractable=falseで IndexedDB に保存する機能など、CryptoKeyの特性にも対応しています。あの擬似コードでは、数字の半分はバイト数を、残り半分はビット数を数えているように見え、アルゴリズムをすでに知らないとどちらなのかほとんど分かりません。
たとえば
N[:12]は 12 バイトのように見えますが、0¹²⁸は 16 バイトですし、Xは実際の文字'X'、つまりビット列01011000ですが、Lはビット列01001100ではなく変数です。数学者はコンピュータサイエンス系の人ほど、曖昧でない表記を好まないのは明らかに思えます。
0¹²⁰10000111のような不気味に見える定数は、CMAC が使う下位のブロック暗号のブロックサイズbについて、次数bの既約多項式のうち、0 でない項の数が最小のものの中で辞書順で最初の多項式の係数を表したビット列です。隠れた意図のようなものはありません。
これは AES-GCM の呼び出しごとにノンスだけでなく鍵も変える方式により、その問題を簡単に避けられるようにします。
また、AES-GCM があればたいてい利用できる「普通の」AES だけを使い、まだ弱点があるかもしれない新しい複雑な構成を避けています。
メッセージあたりのオーバーヘッドは、「普通の」AES で暗号化・復号する必要がある小さなバッファ 2 個と、より長い 192 ビットノンス程度です。
[1]: https://frereit.de/aes_gcm/
ランダムノンスを使う場合に約 2^32 個のメッセージごとに鍵を交換しなければならない素の AES-GCM の落とし穴をなくすものに見えます。
AES-GCM でのノンス衝突は致命的で、少なくとも攻撃者が任意のメッセージに署名できるようになります。
ランダムノンスを必ず使わなければならないわけではありませんが、通常は推奨されており、カウンタベースの鍵導出関数と素の GCM という 2 つのプリミティブを使って FIPS 準拠にしている点はかなり巧妙です。
標準の AES-GCM では、96 ビットはランダム衝突を避けるのに十分ではないため、決定的なノンス生成を使う必要があります。
またノンスをどう作ったかにかかわらず、カウンタがロールオーバーして次のブロックが最初のブロックと同じノンス+カウンタを使うことになるため、2^32 ブロック後にはノンスか鍵を変えなければなりません。
本当に素晴らしいです。数年前に最後に暗号化ファイルシステムを作ったとき、これがあればよかったのにと思います。
大規模なファイルシステム配備では、ノンス衝突が大きな懸念です。
2^32 は大きく見えますが、PB 級のアレイで秒間 100k IOPS の書き込みを行い、疑似乱数生成器のランダム性に頼るなら、衝突の可能性はほぼ保証されるレベルです。
[1] https://en.m.wikipedia.org/wiki/CAESAR_Competition
2 つのブロックが同じ暗号鍵を共有するという意味にすぎないのではありませんか?
どちらのブロックの平文も知らないなら、それがシステムの安全性をどう弱めるのか分かりません。
保存用ファイル暗号化の用途で、FIPS 準拠の age 変種にこれが使われるとよさそうです。
銀行業界の監査では、age が ChaCha を使っているという理由でこの用途では却下され、age の X25519 公開鍵部分は問題ないと見なされました。X25519 は比較的最近 NIST に承認されたものだと思っていました。
Go の経験はありませんが、age の仕様を見るとすぐ差し込めそうで、時間があれば試してみるかもしれません。
名前は “compliant actually good encryption” の意味で “cage” と呼べばよさそうです。
1: https://github.com/FiloSottile/age
暗号学者ではない者として気になるのですが、なぜ 192 ビットノンスを使い、256 ビットを使わないのでしょうか。
実用的なアプリケーションで、その追加ビットがコストと見なされるとは思えません。
CMAC 入力をもっと長くすることもできますが、そうすると AES-256 ブロック関数をさらに複数回実行する必要があり、CMAC 鍵導出関数で厄介な鍵制御の問題にも直面します。
XChaCha20Poly1305 が 192 ビットノンスを使う理由とも似ており、他の主要な拡張ノンス AEAD と一貫している点も弱い利点です。
「2⁸⁰ 個のメッセージで衝突リスク 2⁻³²」とありますが、AES のブロックサイズが 128 ビットしかないという事実のために、その前に問題は起きないのでしょうか?
XAES はメッセージごとに大きな鍵を導出するため、一般に誕生日限界より良い保証と呼ばれるものを達成します。
なぜ問題になると思うのか、もっと詳しい文脈がなければ、これ以上詳しく答えるのは難しいです。
「安全で、退屈で、準拠可能で、相互運用可能」
私が一番好きな種類の技術です。
良いですね。実際に NIST ベースの構成があるのはうれしいことです。
ただし、NIST の鍵導出関数の利点であるラベルやコンテキストのような機能をいくつも諦めることになる点は残念です。
AES 呼び出し回数を最小化するために犠牲にしたのは理解できますが、特に数百バイトより長いメッセージなら、AES 呼び出しを数回節約するより強い暗号学的分離を優先しただろうと思います。
最後に、96 ビットより長いランダム GCM ノンスは確かに大きく誤解されており、96 ビットノンスより良い保証を提供します[1]。
もちろん、メッセージごとに新しい鍵を導出できるなら、そのほうが間違いなく優れています。
[1] https://neilmadden.blog/2024/05/23/galois-counter-mode-and-r...