【実務・中級編】 署名リプレイ攻撃(Signature Replay)の防止策 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

署名リプレイ攻撃:なぜ君の「署名検証」はザルなのか

現場でインシデント対応をしていると、「電子署名を入れたから安全だ」と信じ込んでいる設計に何度も遭遇する。だが、残念ながら署名検証は「署名が正しいこと」を証明するだけで、「それが今まさに送られたリクエストであること」を保証してはいない。

攻撃者は、盗聴した署名パケットをそのまま再送する。これが「署名リプレイ攻撃」だ。今日は、IoTデバイスの認証やスマートコントラクトのオフチェーン署名において、この「使い回し」をどう物理的に遮断するか、泥臭い実装レベルで解説する。

—

1. なぜ「署名」だけでは足りないのか?(PoCのリスク)

想像してほしい。君が管理する工場内のIoTゲートウェイに対し、管理者が「機器の再起動」を指示する署名済みコマンドを発行したとする。

攻撃者は、ネットワークのどこかでこのパケットをキャプチャし、まったく同じ内容を数秒後に再送する。ゲートウェイ側で署名を検証すると、「あ、確かに管理者本人の公開鍵で検証成功だ。実行しよう」となる。これが100回繰り返されたらどうなるか?君の工場は停止と再起動を無限ループし、生産ラインは崩壊する。

この攻撃の肝は、「署名の中身(メッセージ)が変わっていない」ことだ。これを防ぐには、メッセージに「一度しか使えない値」を混ぜ込むしかない。

—

2. 決定的な防衛策:Nonce(ナンス)とChainIDの注入

防衛の基本原則は以下の2点に集約される。

1. Nonce(Number used once): リクエストごとにインクリメントする値を署名対象に含める。サーバー側は「最後に処理したNonce」をDBで保持し、それより小さいか等しい値が来たら即座に破棄する。
2. ChainID / DomainID: 開発環境(Staging)の署名を本番環境(Production)で使えないようにする。これを忘れると、テスト用の署名が本番環境で悪用されるという「クロスチェーン・リプレイ」の餌食になる。

—

3. 実装サンプル:Node.js + ethers.js での堅牢な設計

スマートコントラクトやWeb3バックエンドでよく使われる、EIP-712を模したセキュアな署名検証の実装例だ。

const { ethers } = require("ethers");

/**
 * 署名検証ロジック
 * @param {string} userAddress - 署名したユーザーの公開鍵アドレス
 * @param {string} signature - 受信した署名データ
 * @param {number} nonce - クライアントから送られてきたNonce
 * @param {string} message - 実行内容
 */
async function verifyRequest(userAddress, signature, nonce, message) {
    // 1. DBからユーザーの「現在の期待Nonce」を取得
    const expectedNonce = await db.getNonce(userAddress);

    // 2. Nonceの検証(リプレイ防止の核心)
    if (nonce !== expectedNonce) {
        throw new Error("Invalid Nonce: リプレイ攻撃の疑いがあります");
    }

    // 3. ChainIDを混ぜて、環境外での署名利用を禁止
    const chainId = 1; // 本番環境のID
    const domain = "Secure-IoT-Gateway-v1";
    
    // 署名対象のデータ構造(これ全体をハッシュ化して署名させる)
    const messageHash = ethers.utils.solidityKeccak256(
        ["string", "uint256", "uint256", "string"],
        [domain, chainId, nonce, message]
    );

    // 署名者アドレスの復元
    const signer = ethers.utils.verifyMessage(ethers.utils.arrayify(messageHash), signature);

    // 4. アドレスの一致確認
    if (signer.toLowerCase() !== userAddress.toLowerCase()) {
        throw new Error("署名が不正です");
    }

    // 5. 処理成功後に必ずNonceを更新(これを忘れるとリプレイし放題になる)
    await db.incrementNonce(userAddress);
    
    return true;
}

—

4. インフラ側での防衛:WAFでできること

アプリケーション層でのロジックが完璧でも、脆弱なライブラリや設定ミスが足元をすくう。Nginxなどのゲートウェイ層でも、「明らかに異常なリクエスト」は落とすべきだ。

Nginxでのレート制限設定(/etc/nginx/conf.d/security.conf):
特定のIPから短時間に同じ署名リクエストが大量に飛んでくる場合、論理検証を待たずにIP単位で遮断する。

# IPアドレスごとに1分間あたり10リクエストまで許可
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/m;

server {
    location /api/v1/execute {
        limit_req zone=api_limit burst=5 nodelay;
        # ここでバックエンドの検証ロジックへプロキシ
    }
}

—

現場のエンジニアへ:最後に伝えたいこと

署名リプレイ攻撃の恐ろしさは、「システムとしては正しく動いているように見える」ことにある。ログを見てもエラーは出ないし、正しい権限で処理が行われているように記録される。

セキュリティの極意は、「自分たちのコードは必ず攻撃される」と性悪説で考えることだ。もし君が現在、Nonce管理を実装していないのなら、今すぐバックエンドのデータベースに nonce カラムを追加し、署名検証フローを書き直すことを強く推奨する。

後回しにしていい技術的負債はない。特に、IoTのような「一度設置したら修正が困難なデバイス」においては、この実装漏れが将来的な致命傷になる。準備ができたら、まずはPoCコードで今の環境がリプレイに耐えられるか試してみてくれ。健闘を祈る。

コメント

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