本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
GCM(Galois/Counter Mode)はブロック暗号における利用モードの1つです。ECBやCBCといったモードとは異なり、GCMは「認証付き」のモードです。イメージとしては、改竄防止のMAC(Message Authentication Code)が暗号にくっついているようなモードです。
今回は、Webアプリ等で(特にPHPで)GCMを利用するにあたっての注意点を書いてみます。PHPでは7.1以降のバージョンでGCMが利用できます。
GCMの使い方
注意点の前にGCMの非常に大雑把な説明です。
GCMモードで暗号化を行うと、下記の2つが出力されます。
- 暗号文
- タグ(認証タグ)
出力に「タグ」が含まれている点がCBC等のモードとの違いです。
タグは改竄を検出するMACの役割を果たします。タグは可変長で、暗号化する関数を呼ぶ際に、生成したいタグの長さを引数等として指定します。PHPのOpenSSLライブラリにおける既定のタグ長は16バイトです。
出力された暗号文とタグ、加えて入力のIVの3点セットを、どこかに保存します。Railsでは、この3点をBase64エンコードしてCookieにセットしてます(「--
」で区切られたCookieです)。
復号時には、この3点(下の網掛け)と鍵を、復号用の関数に渡します。下記はPHPのOpenSSLライブラリを使った復号コードの例です。
$plaintext = openssl_decrypt($encrypted, "aes-256-gcm", $key, 0, $iv, $tag);
タグがMACの役割を果たすため、誤ったタグが渡されると復号する関数は何らかのエラーを投げてくれます。PHPのopenssl_decrypt()
の場合はfalse
を返します。
注意点
タグの長さチェック
上記のように、復号時に間違ったタグを与えた場合はエラーにならなければなりません。
しかし、PHPのopenssl_decrypt()
は「不完全なタグ」を有効としてしまうことがあります。例えば、正しいタグが「ABCDEF0123456789
」であったとすると、末尾を切り落とした「ABCD
」や「A
」のようなタグも正しいとみなされてしまいます。
この奇妙な挙動には、前述した「タグの長さが可変である」というGCMの仕様が関係しています。長さが可変であるため、短いタグをエラーとするには、タグの「あるべき長さ」の情報が必要です。暗号文にはタグの長さの情報は含まれていないため、タグの長さが充分であるかをopenssl_decrypt()
側でチェックするには、呼び出し元から「あるべき長さ」を渡してもらうしかありませんが、現状はそれ用の引数が定義されていない状況です。
したがって、(今のところ)openssl_decrypt()
を呼びだす側のプログラムで、タグ長の検証をしなくてはなりません。
もしタグ長の検証を行っておらず、短いタグが許されるなら、改竄への耐性が低下します。GCMは、内部的に生成した鍵ストリームと平文のXORを暗号文にするので、暗号文のビットを反転すると該当する平文のビットが反転します。これを使って望みの平文になるよう暗号文を操作した上で、それに対応する短いタグ(1バイトのタグなら256通り)を総当たりすれば、攻撃は無事完了です。
このopenssl_decrypt()
の問題は、2018年1月にPHP Bugsに報告された既知の問題です。PHP Bugsでの議論では、ひとまずPHPマニュアルにユーザへの注意書きを加える方向となりましたが、本記事の執筆時点では注意書きはありません(追記: 8/20に注意書きが追加されました)。ネット上にはタグ長をチェックしないコードもあるようだったので、注意喚起のためにPHPマニュアルの「User Contributed Notes」に記載するとともに、このブログ記事で取り上げています。
他のプログラム言語を見てみると、RubyのOpenSSLライブラリにも同じ問題がありますが、こちらのマニュアルには「呼び出す側でタグの長さをチェックして」という趣旨の注意書きがあります。呼び出す側の一つである、Active Supportのmessage_encryptor
のソースを見てみたところ、ちゃんと注意書き通りにタグ長をチェックしていました(網掛け部分)。
# Currently the OpenSSL bindings do not raise an error if auth_tag is # truncated, which would allow an attacker to easily forge it. See # https://github.com/ruby/openssl/issues/63 raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)
さらに別の言語の例を言うと、Pythonではpython-cryptographyライブラリの脆弱性(CVE-2018-10903)として同種の問題が修正された記録があります。修正後のバージョンでは、呼び出し側からライブラリにmin_tag_length
を渡し、ライブラリ側がタグ長をチェックするようになっています。
PHPの話に戻ると、7.2以降のバージョンではOpenSSL以外の暗号ライブラリとしてSodiumが利用できます。PHPマニュアルにはSodiumの情報がほぼ無いため、余り積極的に使いたい気持ちにはならないのですが、今回は試しに動かして挙動を調べてみました。
SodiumのAES-GCM暗号用の関数であるsodium_crypto_aead_aes256gcm_encrypt()
の戻り値は暗号文とタグを結合したバイト列であり、戻り値の末尾16バイトがタグです。対応する復号関数は、引数の$ciphertext
の末尾をタグと仮定します。タグは固定長であり、自由度が少ない作りだけに、短いタグを受け付けることは無いようです。
IV(初期化ベクトル)の重複
もう一つ、GCMで忘れてはならないのはIVに関連する注意点です。
セキュリティ診断では、暗号化(CBCモード)で固定のIVを使用しているWebアプリを見ることがあります。CBCでIVを固定、または予測可能にすると重大な問題になりうることが知られていますが、実際のアプリにおいて意味がある攻撃ができるケースは少ないと思います。
しかしGCMでは事情が異なり、固定のIV(またはIVの重複)は即アウトです。同一のIVが使われると平文同士のXORが漏洩しますが、それに加えて「改竄チェックの回避」という致命的なオマケまで付いてくるからです(これについての技術的な解説は、専門家ではない筆者の能力に余るので、jovi0608氏の「本当は怖いAES-GCMの話」を参照してください)。
IVの重複に関連して、NIST SP 800-38D(8.3)は「同一鍵における暗号化回数の上限(2^32)」を規定しています。Webアプリでありそうなユースケース(Cookie等の暗号化。システム全体で長期間にわたり同じ鍵を使い、IVは96ビットの乱数)では、この上限が効いてきます。上限がそこまで大きい数でないため、アクセス数が多いサイトでは、鍵のローテーションが必要になるかもしれません。
参考までに書くと、(TLS等のレイヤではなく)アプリに近いところでGCMのIVを再利用してしまった例として、Rubyのattr-encryptedで顕在化したバグがあります。これは意図せずにIVが固定になってしまった事例ですが、そうなるとGCMはほぼ無力になります。
本ページに関するサービスの詳細
おすすめ記事