MBSD® MBSD®

2020.09.01

寺田健

AES-GCMモード

GCMGalois/Counter Mode)はブロック暗号における利用モードの1つです。ECBCBCといったモードとは異なり、GCMは「認証付き」のモードです。イメージとしては、改竄防止のMACMessage Authentication Code)が暗号にくっついているようなモードです。

今回は、Webアプリ等で(特にPHPで)GCMを利用するにあたっての注意点を書いてみます。PHPでは7.1以降のバージョンでGCMが利用できます。

 

GCMの使い方

注意点の前にGCMの非常に大雑把な説明です。

GCMモードで暗号化を行うと、下記の2つが出力されます。

  • 暗号文
  • タグ(認証タグ)

出力に「タグ」が含まれている点がCBC等のモードとの違いです。

タグは改竄を検出するMACの役割を果たします。タグは可変長で、暗号化する関数を呼ぶ際に、生成したいタグの長さを引数等として指定します。PHPOpenSSLライブラリにおける既定のタグ長は16バイトです。

出力された暗号文とタグ、加えて入力のIV3点セットを、どこかに保存します。Railsでは、この3点をBase64エンコードしてCookieにセットしてます(「--」で区切られたCookieです)。

復号時には、この3点(下の網掛け)と鍵を、復号用の関数に渡します。下記はPHPOpenSSLライブラリを使った復号コードの例です。

$plaintext = openssl_decrypt($encrypted, "aes-256-gcm", $key, 0, $iv, $tag);

タグがMACの役割を果たすため、誤ったタグが渡されると復号する関数は何らかのエラーを投げてくれます。PHPopenssl_decrypt()の場合はfalseを返します。

 

注意点

タグの長さチェック

上記のように、復号時に間違ったタグを与えた場合はエラーにならなければなりません。

しかし、PHPopenssl_decrypt()は「不完全なタグ」を有効としてしまうことがあります。例えば、正しいタグが「ABCDEF0123456789」であったとすると、末尾を切り落とした「ABCD」や「A」のようなタグも正しいとみなされてしまいます。

この奇妙な挙動には、前述した「タグの長さが可変である」というGCMの仕様が関係しています。長さが可変であるため、短いタグをエラーとするには、タグの「あるべき長さ」の情報が必要です。暗号文にはタグの長さの情報は含まれていないため、タグの長さが充分であるかをopenssl_decrypt()側でチェックするには、呼び出し元から「あるべき長さ」を渡してもらうしかありませんが、現状はそれ用の引数が定義されていない状況です。

したがって、(今のところ)openssl_decrypt()を呼びだす側のプログラムで、タグ長の検証をしなくてはなりません。

もしタグ長の検証を行っておらず、短いタグが許されるなら、改竄への耐性が低下します。GCMは、内部的に生成した鍵ストリームと平文のXORを暗号文にするので、暗号文のビットを反転すると該当する平文のビットが反転します。これを使って望みの平文になるよう暗号文を操作した上で、それに対応する短いタグ(1バイトのタグなら256通り)を総当たりすれば、攻撃は無事完了です。

このopenssl_decrypt()の問題は、20181月にPHP Bugs報告された既知の問題です。PHP Bugsでの議論では、ひとまずPHPマニュアルにユーザへの注意書きを加える方向となりましたが、本記事の執筆時点では注意書きはありません(追記: 8/20に注意書きが追加されました)。ネット上にはタグ長をチェックしないコードもあるようだったので、注意喚起のためにPHPマニュアルの「User Contributed Notes」に記載するとともに、このブログ記事で取り上げています。

他のプログラム言語を見てみると、RubyOpenSSLライブラリにも同じ問題がありますが、こちらのマニュアルには「呼び出す側でタグの長さをチェックして」という趣旨の注意書きがあります。呼び出す側の一つである、Active Supportmessage_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の情報がほぼ無いため、余り積極的に使いたい気持ちにはならないのですが、今回は試しに動かして挙動を調べてみました。

SodiumAES-GCM暗号用の関数であるsodium_crypto_aead_aes256gcm_encrypt()の戻り値は暗号文とタグを結合したバイト列であり、戻り値の末尾16バイトがタグです。対応する復号関数は、引数の$ciphertextの末尾をタグと仮定します。タグは固定長であり、自由度が少ない作りだけに、短いタグを受け付けることは無いようです。

 

IV(初期化ベクトル)の重複

もう一つ、GCMで忘れてはならないのはIVに関連する注意点です。

セキュリティ診断では、暗号化(CBCモード)で固定のIVを使用しているWebアプリを見ることがあります。CBCIVを固定、または予測可能にすると重大な問題になりうることが知られていますが、実際のアプリにおいて意味がある攻撃ができるケースは少ないと思います。

しかしGCMでは事情が異なり、固定のIV(またはIVの重複)は即アウトです。同一のIVが使われると平文同士のXORが漏洩しますが、それに加えて「改竄チェックの回避」という致命的なオマケまで付いてくるからです(これについての技術的な解説は、専門家ではない筆者の能力に余るので、jovi0608氏の「本当は怖いAES-GCMの話」を参照してください)。

IVの重複に関連して、NIST SP 800-38D8.3)は「同一鍵における暗号化回数の上限(2^32)」を規定しています。Webアプリでありそうなユースケース(Cookie等の暗号化。システム全体で長期間にわたり同じ鍵を使い、IV96ビットの乱数)では、この上限が効いてきます。上限がそこまで大きい数でないため、アクセス数が多いサイトでは、鍵のローテーションが必要になるかもしれません。

参考までに書くと、(TLS等のレイヤではなく)アプリに近いところでGCMIVを再利用してしまった例として、Rubyattr-encryptedで顕在化したバグがあります。これは意図せずにIVが固定になってしまった事例ですが、そうなるとGCMはほぼ無力になります。

プロフェッショナルサービス事業部

寺田健

シェアする ツイートする