【入門編】 AES-GCMにおけるNonce(IV)再利用による認証タグの脆弱性 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

こんにちは!日々システムの開発や運用の裏側で、セキュリティの確保に頭を悩ませている新人エンジニアの皆さん、お疲れ様です。

「暗号化」と聞くと、なんだか難解な数式が並ぶ世界のように感じて身構えてしまいますよね。でも大丈夫です。今回は、現代のWebやクラウドインフラでなくてはならない共通鍵暗号のスター選手「AES-GCM」に潜む、ちょっとした、しかし致命的な落とし穴「Nonce(ナンス)の再利用」について、身近な例えを交えながら一歩ずつ紐解いていきましょう。

現場の泥臭い話も含めて、分かりやすく解説していきますね!

—

1. 家の鍵で例える「AES-GCM」と「Nonce(IV)」の仕組み

まずは、私たちが普段使っている「鍵」に例えて、AES-GCMがやっていることを覗いてみましょう。

AES-GCMは、データを他所に見られても中身が読めないようにする「暗号化(秘密を守る)」と、データが途中で悪意ある人に改ざんされていないかチェックする「認証(正しさを証明する)」を同時にこなしてくれる、とても優秀な仕組みです。

ここで登場するのが、AES-GCMにおける「Nonce(初期化ベクトル / IVとも呼ばれます)」という値です。
これは、暗号化を行うたびに「毎回必ず違う使い捨ての数字(ランダムな値、あるいはカウンター)」をセットする場所だと考えてください。

もし、この使い捨ての数字(Nonce)を使い回したらどうなる?

想像してみてください。あなたが毎日使う金庫のダイヤル錠があったとします。
防犯の基本は「開けるたびにランダムな組み合わせに変えること」ですよね。しかし、もしあなたが「毎回まったく同じダイヤルの位置(同じNonce)」で鍵をかけ続けたらどうなるでしょうか?

泥棒(攻撃者)は、その金庫が開閉される様子をじっと観察し、「あれ? 今回も前回と同じダイヤルの手順でロックしているぞ」と気づきます。実はAES-GCMの数学的な構造上、「同じ鍵(Secret Key)」と「同じNonce」の組み合わせで2回以上暗号化を行ってしまうと、データのセキュリティを守るための「認証タグ(GCMの要であるハッシュ値)」の裏側にある秘密の数学的関係性が丸裸になってしまうのです。

結果として、攻撃者はあなたの通信の中身を簡単に改ざんできるようになり、最悪の場合は暗号化の鍵そのものを推測されてしまいます。「一度きりの使い捨て」をサボっただけで、頑丈なはずの金庫の扉がパカッと開いてしまう、これがNonce再利用の恐ろしい正体です。

—

2. 攻撃者はどうやってこの隙を突くのか?

現場のセキュリティ診断やインシデント対応をしていると、開発初期によく見かけるのが「テストだから」「面倒くさいから」という理由で、毎回固定の文字列(例えば 000000000000 のようなNonce)をハードコーディングしてしまっているケースです。

攻撃者は、ネットワークを流れる暗号化されたデータ(パケット)をこっそり盗み見(盗聴)しています。
もし同じNonceを使ったデータが2つ流れてくると、攻撃者は次のような悪巧みをします。

1. 共通の弱点を見つける: 同じNonceが使われた2つの暗号文の差分をとることで、暗号の裏側にある数学的なマスクを剥ぎ取る。
2. データの偽装(改ざん): 「このデータは正規のサーバーから送られたものですよ」と証明するための「認証タグ」を、攻撃者が自由自在に偽造できるようになる。
3. なりすまし・インジェクション: ユーザーになりすまして不正なコマンドやリクエストをサーバーに送り込み、システムを乗っ取る。

「まさかそんな偶然同じNonceが使われるわけ……」と思うかもしれませんが、プログラムのバグやカウンタの実装ミス(サーバーが再起動するたびにカウンタが 0 にリセットされるなど)は、現実のシステムで驚くほど頻繁に発生します。

—

3. 実装で防ぐ!正しいNonce生成のベストプラクティス

では、私たちは実務の中でどうやってこのリスクを防げば良いのでしょうか?
答えはシンプルです。「絶対に同じNonceを2度使わない仕組みをコードで担保する」これにつきます。

一般的に、AES-GCMのNonceには 96ビット(12バイト) の長さが推奨されています。完全にランダムな値を毎回の暗号化のたびにOSの暗号学的擬似乱数生成器(CSPRNG)から生成するのが最も安全です。

それでは、PHPを例に、安全なAES-GCMの暗号化と、Nonceの扱い方をコードで見てみましょう。

サンプルコード(PHPでの安全なAES-GCM暗号化)

<?php

/**
 * 安全なAES-GCM暗号化のサンプル関数
 * 
 * @param string $plainText 暗号化したい平文
 * @param string $secretKey 32バイトの秘密鍵(環境変数等から安全に読み込むこと)
 * @return string|false 暗号化データ(Nonce + 暗号文 + 認証タグを結合したもの)
 */
function secureEncrypt($plainText, $secretKey) {
    // 1. 使用する暗号アルゴリズムの指定
    $cipher = 'aes-256-gcm';

    // 2. AES-GCMの仕様に合わせたNonceの長さを取得(通常は12バイト = 96ビット)
    $ivLength = openssl_cipher_iv_length($cipher);

    // 3. 【超重要】毎回必ず「暗号学的に安全なランダムな値」をNonce(IV)として生成する
    // ここでハードコーディングや使い回しをしてはいけません!
    $nonce = random_bytes($ivLength);

    // 4. 認証タグを受け取るための変数を初期化(参照渡しでGCMがタグを書き込みます)
    $tag = "";

    // 5. 暗号化の実行
    // 引数: 平文, 暗号方式, 秘密鍵, オプション(0), Nonce, 認証タグ(参照), 追加認証データ(AAD)
    $ciphertext = openssl_encrypt(
        $plainText,
        $cipher,
        $secretKey,
        OPENSSL_RAW_DATA,
        $nonce,
        $tag,
        "", // AAD(必要に応じて通信の文脈データを入れますが、今回は空)
        16  % 認証タグの長さ(通常は16バイトが推奨)
    );

    if ($ciphertext === false) {
        return false;
    }

    // 6. 復号側では「Nonce」と「認証タグ」の両方が絶対に必要になるため、
    // 「Nonce + 暗号文 + 認証タグ」をセットにして保存・送信します。
    // ※暗号文の中にNonceが含まれていない場合、後から復号できなくなるので注意!
    return base64_encode($nonce . $ciphertext . $tag);
}

// --- 実際の利用イメージ ---
// 実際には32バイトのセキュアなランダムキーを安全なストレージや環境変数(Dotenv等)で管理します。
$mySecretKey = random_bytes(32); 
$secretMessage = "機密情報:本日のパスワードは秘密です";

$encryptedData = secureEncrypt($secretMessage, $mySecretKey);
echo "暗号化結果: " . $encryptedData . "\n";

現場で気をつけるべきポイント

  • Nonceは秘密にする必要はない: Nonce自体は隠す必要がありません(盗聴されても問題ありません)。そのため、上記のコードのように 暗号文の先頭にくっつけて一緒に相手に送る のが一般的な実装スタイルです。
  • カウンター方式を使う場合の注意: もしパフォーマンス上の理由等でランダム生成ではなくカウンター(連番)を使う場合は、サーバーがクラッシュして再起動した際にカウンターが初期化(0に戻る)されないよう、永続化ストレージやアトミックなカウンタ管理(Redis等)を徹底する必要があります。基本的には「毎回ランダム生成(random_bytes 等)」を選ぶのが、最もヒューマンエラーを防げる安全な選択肢です。

—

まとめ

いかがでしたでしょうか?
AES-GCMは非常に強力でモダンな暗号方式ですが、「Nonceの再利用」というたった一つの油断が、セキュリティの堅牢な城壁に大きな穴を開けてしまいます。

  • Nonceは絶対に使い回さない!
  • 暗号化のたびに、必ず新しいランダムなNonce(12バイト)を生成する!
  • 復号に必要なため、Nonceは暗号文と一緒に(ただしプレーンな状態で結合して)扱ってOK!

この基本原則さえ押さえておけば、攻撃者に隙を与えることはありません。一歩ずつ、確実で安全なコードを積み重ねていきましょう。皆さんの開発ライフがよりセキュアで素晴らしいものになるよう、これからも応援しています!

コメント

タイトルとURLをコピーしました