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

鍵のコピーは一度だけ?ブロックチェーンの「署名リプレイ攻撃」を泥棒対策から学ぼう

こんにちは!セキュリティの世界へようこそ。今日は、スマートコントラクト開発で避けては通れない「署名リプレイ攻撃」という、ちょっと怖いけど仕組みを知れば怖くない脆弱性についてお話しします。

「署名リプレイ攻撃」……名前からして物騒ですよね。でも、実はこれ、私たちの身近な生活でも起こりうる「空き巣の手口」と全く同じなんです。

—

「合鍵のコピー」が無限に使われたらどうなる?

想像してみてください。あなたは、信頼できる友人にだけ、家の玄関の「合鍵」を貸しました。この合鍵にはあなたのサインが刻印されていて、これがあればドアが開くようになっています。

さて、もしその友人が悪意を持って、その合鍵を「何百回もコピー」して、街中の見知らぬ人に配り歩いたらどうなるでしょう? あなたの家は、知らない人たちに何度も勝手に開けられてしまいますよね。

これが、ブロックチェーンの世界で起こる「署名リプレイ攻撃」です。

本来、署名とは「一度だけ使うためのチケット」であるべきです。しかし、コントラクト側が「このチケットはさっき使ったよ!」という記録(チェック機能)を忘れていると、攻撃者は同じ署名を何度も使い回して、あなたの資産を盗んだり、勝手に操作したりできてしまうのです。

—

守りの要:Nonce(ナンス)という「整理番号」

では、どうすれば防げるのでしょうか? 鍵の例で言えば、「鍵に整理番号(通し番号)を振る」のが一番の対策です。

「この鍵は1番です。次からは2番の鍵しか受け付けません」と決めておけば、たとえ悪意ある人が1番の鍵をコピーして何度も持ってきても、ドア側は「もう1番は使ったから、次は2番じゃないと開けないよ」と門前払いできます。

この「整理番号」のことを、コンピュータの世界では Nonce(ナンス) と呼びます。

—

実装してみよう:EIP-712とNonce管理

最近のスマートコントラクトでは、EIP-712 という標準規格を使って、「何を」「誰が」「どのコントラクトに対して」署名したかを明確にします。ここに nonce を組み込むのが鉄則です。

以下は、Solidityでのシンプルな実装例です。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureVault {
    // ユーザーごとのNonceを管理するマップ
    mapping(address => uint256) public nonces;

    // 署名を使って何かを実行する関数
    function executeAction(address user, bytes memory signature, uint256 nonce) public {
        // 1. ノンスが現在保存されているものと一致するか確認
        require(nonce == nonces[user], "無効なノンスです。既に使われた署名です。");

        // 2. ここで署名の検証処理を行う(省略)
        
        // 3. 処理が終わったら、ノンスをインクリメント(+1)する
        // これにより、古い署名は二度と使えなくなります!
        nonces[user]++;
    }
}

コードのポイント

1. nonces[user]:ユーザーごとに現在の「整理番号」を記録します。
2. require(nonce == nonces[user]):持ってきた番号が、今受け付けるべき番号と一致するか確認します。
3. nonces[user]++:処理が終わった瞬間に番号を更新します。これで、今の番号は「過去のもの」となり、二度と使えなくなります。

—

フロントエンドでの署名作成(JavaScript例)

フロントエンドから署名を送る際も、現在保持している nonce を取得して署名に含める必要があります。

// ユーザーが現在持つnonceをコントラクトから取得
const currentNonce = await contract.nonces(userAddress);

// 署名するデータにnonceを含める
const message = {
    action: "withdraw",
    amount: 100,
    nonce: currentNonce.toString() // ここが重要!
};

// 署名生成(ethers.js等のライブラリを使用)
const signature = await signer._signTypedData(domain, types, message);

—

まとめ:セキュリティは「一歩ずつ」が鉄則

今回の学びをまとめると、たったこれだけです。

  • 署名は「一度きり」のチケットであると心得る。
  • Nonce(整理番号) を使って、一度使った署名を無効化する。
  • コントラクト側で「この番号はもう終わったよ」と状態を更新(インクリメント)する。

最初からすべてを完璧にするのは難しいかもしれません。でも、このように「仕組み」を泥棒対策に置き換えて考えてみると、どんなに複雑なセキュリティ技術も、実はとてもシンプルで論理的な防犯対策の積み重ねであることがわかります。

まずは自分のコードに「整理番号」があるか、一度確認してみてくださいね。一歩ずつ、セキュアな開発者への道を歩んでいきましょう!

コメント

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