AES-GCM と ChaCha20-Poly1305 のどちらを使うかは、暗号化された通信のほぼすべてを担う 2 つの認証付き暗号アルゴリズムのどちらを選ぶかという問題です。TLS 1.3、QUIC、SSH、WireGuard、age、そしてアプリケーションレベルの暗号化の大半が、このどちらかを使っています。どちらも正しく使えば安全ですが、nonce が重複すると深刻な形で破綻し、速度はハードウェアに大きく左右されます。
比較表
| AES-GCM | ChaCha20-Poly1305 | |
|---|---|---|
| 仕様 | NIST SP 800-38D(2007年) | RFC 8439(2018年、RFC 7539 を置き換え) |
| 鍵長 | 128、192、256 ビット | 256 ビット |
| nonce | 96 ビットを推奨 | 96 ビット(XChaCha20 では 192 ビット) |
| タグ | 128 ビット(短いタグも可、ただし非推奨) | 128 ビット |
| 1 メッセージあたりの最大データ量 | 約 64 GiB(2³² − 2 ブロック) | 256 GiB(64 バイトのブロック 2³² 個) |
| 鍵 1 つあたりのランダム nonce の上限 | 2³² メッセージ(SP 800-38D) | 96 ビットの nonce では同程度、XChaCha20 では事実上無制限 |
| nonce の再利用 | 平文の XOR と GHASH 鍵が漏洩し、偽造が可能 | 平文の XOR が漏洩し、偽造が可能 |
| スループット(16 KiB、OpenSSL 3.6.5) | 10.4 GB/s(AES-128)、8.8 GB/s(AES-256) | 2.1 GB/s |
| スループット(16 バイトのメッセージ) | 0.14 GB/s(AES-256) | 0.52 GB/s |
| ハードウェアなしでの定数時間実装 | 困難(テーブル方式の AES はキャッシュ経由で漏洩) | 自然に実現(加算、ローテーション、XOR) |
| FIPS 140-3 承認 | あり | なし |
| TLS 1.3 | TLS_AES_128_GCM_SHA256(MUST)、TLS_AES_256_GCM_SHA384(SHOULD) | TLS_CHACHA20_POLY1305_SHA256(SHOULD) |
| その他の用途 | JWE の A128GCM と A256GCM、IPsec、SSH の aes256-gcm@openssh.com、ディスクとクラウドの暗号化 | WireGuard、SSH の chacha20-poly1305@openssh.com、age、libsodium |
スループットは、AES 命令と多項式乗算命令を備えた Apple M4 Max の 1 コアで、openssl speed -evp を使って測定しました。
性能は CPU によって変わる
AES-GCM はハードウェア向けに設計されています。AES のラウンドは単一の命令(x86 の AES-NI、ARMv8 の暗号拡張)で実行され、GHASH にはキャリーレス乗算を使います。両方を使える環境では、この測定で 1 コアあたり約 10 GB/s に達し、ChaCha20-Poly1305 の約 5 倍です。これらの命令がない場合、AES はルックアップテーブルを使うか(低速で、キャッシュタイミングを通じて鍵のビットが漏洩します)、ビットスライス実装を使うか(定数時間ですが低速です)のどちらかになります。ChaCha20 に必要なのは 32 ビットの加算、ローテーション、XOR だけなので、ローエンドのスマートフォンやマイコンを含むあらゆる CPU で、高速かつ定数時間で動作します。
メッセージが小さいと、この差は縮まるか逆転します。16 バイトの入力では、GCM のセットアップコストが支配的になるため、ChaCha20-Poly1305 が 0.52 GB/s に達したのに対し、AES-256-GCM は 0.14 GB/s でした。サーバーで扱う一般的な TLS レコード(最大 16 KiB)では AES-GCM のほうが高速です。ごく小さなパケットが大量にある場合は、どちらが速いにせよ差はわずかです。
nonce と上限
どちらもワンタイム認証子を組み合わせたカウンターモードの暗号なので、鍵と nonce の組み合わせを決して繰り返してはいけません。繰り返すと、2 つの平文の XOR が明らかになります。さらに GCM では、攻撃者が認証用のサブ鍵を復元して、その鍵のもとでメッセージを偽造できるようになります。Poly1305 ではワンタイム鍵が露呈し、その nonce でのメッセージの偽造が可能になります。NIST SP 800-38D は、衝突確率を無視できる程度に抑えるため、ランダムに生成した 96 ビットの IV の使用を鍵 1 つあたり 2³² 回までに制限しています。TLS は、レコードのシーケンス番号から nonce を導出し、鍵を更新することでこの問題を回避しています。
1 つの鍵で多数のメッセージにランダムな nonce を生成する設計なら、nonce 空間が大きい構成か、誤用耐性のある構成を使用してください。
- XChaCha20-Poly1305(nonce は 192 ビット。IRTF のインターネットドラフトに記述があり、libsodium などが実装)では、ランダムな nonce を事実上無制限のメッセージで安全に使えます。
- AES-GCM-SIV(RFC 8452)では、nonce を再利用しても、2 つのメッセージが同一だったかどうかしか明らかになりません。
- あるいは、TLS、age、HPKE のように、メッセージごとまたはセッションごとに鍵を導出します。
どちらのアルゴリズムにも鍵コミットメントの性質はありません。2 つの異なる鍵のどちらでも正しく復号される暗号文を意図的に作ることができ、これは複数の受信者を持つ設計やパスワードベースの設計で問題になります(「invisible salamanders」問題)。攻撃者が鍵を選べる場合は、鍵コミットメントを追加してください。
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
const plaintext = Buffer.from("card ending 4242, amount 19.99");
const aad = Buffer.from("order=7781");
for (const algorithm of ["aes-256-gcm", "chacha20-poly1305"]) {
const key = randomBytes(32);
const nonce = randomBytes(12); // 96-bit nonce for both; never reuse with the same key
const cipher = createCipheriv(algorithm, key, nonce, { authTagLength: 16 });
cipher.setAAD(aad);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const tag = cipher.getAuthTag();
const decipher = createDecipheriv(algorithm, key, nonce, { authTagLength: 16 });
decipher.setAAD(aad);
decipher.setAuthTag(tag);
const roundTrip = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
console.log(algorithm.padEnd(18), "ciphertext", ciphertext.length, "bytes + tag", tag.length, "bytes, decrypts:", roundTrip.equals(plaintext));
}
aes-256-gcm ciphertext 30 bytes + tag 16 bytes, decrypts: true
chacha20-poly1305 ciphertext 30 bytes + tag 16 bytes, decrypts: true
TLS での選択の仕組み
TLS 1.3 は、すべての実装に TLS_AES_128_GCM_SHA256 のサポートを義務付け、AES-256-GCM と ChaCha20-Poly1305 を推奨しています。AES ハードウェアを持たないクライアントは ChaCha20 を先頭に挙げ、その優先順位を尊重するサーバーは、各クライアントにとって速いほうの暗号を使います。OpenSSL はこれを PrioritizeChaCha オプション(SSL_OP_PRIORITIZE_CHACHA)で実装しており、クライアントが ChaCha20 を先頭に挙げた場合にだけ ChaCha20 を優先します。BoringSSL は同じ目的で、優先度が等しい暗号のグループを使います。TLS 設定生成ツールでは、両方のスイートを残したサーバー設定を生成できます。
どちらを選ぶべきか
- TLS と QUIC のサーバー:両方を有効にします。AES-GCM を優先しつつ、ChaCha20 を先頭に挙げたクライアントには ChaCha20 を使わせます。
- FIPS 140-3 環境:AES-GCM を使います。ChaCha20-Poly1305 は承認されたアルゴリズムではありません。
- AES アクセラレーションのないモバイル、IoT、マイコン:ChaCha20-Poly1305 を使います。
- ランダムな nonce と長期間使う鍵によるアプリケーションレベルの暗号化:XChaCha20-Poly1305 または AES-GCM-SIV を使います。
- JWE:
A256GCM(またはA128GCM)を使います。ChaCha20 は JWA のコアレジストリに含まれていません。 - 非常に大きな単一メッセージ(64 GiB 超):チャンクごとに nonce を割り当てて分割します(age が ChaCha20-Poly1305 の 64 KiB チャンクで行っているように)。1 メッセージあたりの上限に頼らないでください。
移行時の注意点
適切に設計されたシステムでは、AEAD を切り替えても変わるのは暗号文の形式だけです。
- 形式にバージョンを付ける:古いデータを読み続けられるよう、暗号文ごとにアルゴリズム識別子と鍵識別子を保存します。
- アルゴリズムごとに鍵を分ける:1 つの鍵を両方で再利用するのではなく、別々の鍵を導出します(たとえば HKDF で異なる info 文字列を使います)。
- nonce の生成方法を再確認する:カウンターは再起動をまたいで永続化しなければなりません。ランダムな nonce は前述の上限内に収めるか、XChaCha20 に移行しなければなりません。
- 再暗号化は必要になった時点で行う:コンプライアンス上の期限によって一括移行が求められる場合を除き、レコードにアクセスした時点で古いアルゴリズムで復号し、新しいアルゴリズムで書き込みます。
- 実際のハードウェアでベンチマークする:速度の順位は、サーバーの CPU と小型デバイスとで逆転します。