【入門編】 EIP-712における型付きデータ署名の検証不備とリプレイ攻撃 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

「同じ鍵で隣の家も開いちゃう?」EIP-712の落とし穴と、クロスチェーン時代の防犯術

こんにちは!今日は、Web3開発の現場で避けては通れない「EIP-712」という規格と、そこに潜むちょっと怖い「リプレイ攻撃」についてお話しします。

「ブロックチェーンって暗号化されてるから安全なんじゃないの?」と思われがちですが、実はアプリ側(スマートコントラクト)の「署名の取り扱い」を間違えると、泥棒に合鍵を渡してしまうのと同じくらい危険な事態になるんです。

一歩ずつ、身近な例えで紐解いていきましょう!

—

1. 署名って、結局なに?(デジタルな合鍵の話)

ブロックチェーンの世界では、パスワードを入力する代わりに「秘密鍵」を使って「署名(デジタル署名)」を作成し、「私が操作しました!」という証明をします。

EIP-712は、この署名を人間にも読みやすい形式にして、かつ「どこの、何のための署名なのか」を明確にするためのルールです。

例えば、「Aさんの家の鍵(署名)」を作ったとします。この鍵が「Aさんの家のドア」専用であれば問題ありませんよね。でも、もしこの鍵が「隣のBさんの家」や「全く別の町にある家」まで開けられてしまったら……それが「リプレイ攻撃」の入り口です。

—

2. なぜ「クロスチェーン・リプレイ攻撃」が起きるのか?

最近はイーサリアムだけでなく、PolygonやArbitrumなど、色々なネットワーク(チェーン)がありますよね。

悪意のある攻撃者は、あなたが「Aチェーン」で一度使った署名をコピーして、「Bチェーン」や「Cチェーン」に持っていきます。もし、あなたのアプリが「どのチェーンの署名か」を確認していなければ、Bチェーン上の契約は「あ、有効な署名だ!」と勘違いして、勝手に資産を移動させてしまうのです。

これが「クロスチェーン・リプレイ攻撃」の正体です。

—

3. 「ドメインセパレータ」という名の住所確認

この事故を防ぐために、EIP-712では「ドメインセパレータ」という仕組みを使います。これは、署名の中に「住所」を書き込むようなものです。

具体的には、以下の情報を署名データに含めます。

  • name: アプリの名前(例: “MyCoolApp”)
  • version: バージョン(例: “1”)
  • chainId: ネットワークのID(例: イーサリアムなら1、Polygonなら137)
  • verifyingContract: その署名を受け付ける契約のアドレス

これらをしっかり定義しておけば、「この署名はイーサリアム用だから、Polygonでは使えないよ!」と、契約側で弾くことができるようになります。

—

4. 安全な実装コード例:ここだけは守ろう!

では、実際にスマートコントラクト(Solidity)で、どのように署名を検証すべきか見てみましょう。

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

contract SecureVault {
    // ドメインセパレータの定義(ここが防波堤になります)
    bytes32 public DOMAIN_SEPARATOR;

    constructor() {
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256("MyCoolApp"),
                keccak256("1"),
                block.chainid, // 現在のネットワークのIDを自動取得
                address(this)   // この契約専用であることを明示
            )
        );
    }

    // 署名の検証ロジック
    function verify(address signer, bytes32 structHash, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {
        // ドメインとデータを結合してハッシュ化
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
        
        // 署名者と復元したアドレスが一致するか確認
        return ecrecover(digest, v, r, s) == signer;
    }
}

ポイント解説

  • block.chainid: これをドメインセパレータに含めるのが最大の防御です。これにより、チェーンが変わると署名が強制的に無効になります。
  • address(this): 「この契約専用の鍵」であることを担保します。他の契約に同じ署名を横流しされても無効になります。

—

まとめ:セキュリティは「疑うこと」から始まる

「便利に使えること」と「安全であること」は、往々にしてトレードオフの関係にあります。しかし、今回紹介したchainIdやverifyingContractの検証は、ほんの数行のコードで実装できる「必須の防犯対策」です。

新人の開発者さんが最初の一歩として意識すべきは、「自分のコードが、どこで、誰によって、どの環境で使われることを想定しているか」を常にコードに書き留める(=ドメインセパレータを正確に定義する)という習慣です。

最初は難しく感じるかもしれませんが、この「住所確認」の仕組みを理解するだけで、Web3開発者としてのレベルはグッと上がります。一緒に、安全なWeb3の未来を作っていきましょう!

もし実装で詰まったら、いつでも見返してみてくださいね。応援しています!

コメント

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