【実務・中級編】 EIP-712による型付きデータ署名の検証とフィッシング対策 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

EIP-712の正体:なぜ「何に署名しているか分からない」状態が最大のセキュリティホールなのか

現場のエンジニア諸君、お疲れ様。SCADA/OTの現場で「謎のバイナリデータ」を解読するのと、Web3のフロントエンドで「ウォレットが吐き出す署名要求」を解析するのは、実は根本で繋がっている。

それは「人間が読めない情報を、人間が認可せざるを得ない状況」という名の脆弱性だ。

かつて、イーサリアムの署名といえばeth_signが主流だった。これは任意のバイト列に署名させるものだが、ユーザーには「何を承認しているか」が全く見えない。攻撃者はここを突く。「あ、このサイトのログイン認証ですね」と言いながら、裏でトークン送金トランザクションのバイト列を署名させ、財布を空にする。これが、いわゆるブラインド署名フィッシングの泥沼だ。

これを根本から断つのが EIP-712 だ。構造化されたデータに型を定義し、ユーザーに「誰が、何を、どうするのか」を可読な形で提示する。この実装をサボることは、家の玄関に「鍵はかかっていませんが、中身は見ないでください」と貼り紙をするのと同じだ。

—

攻撃者が狙う「署名のコンテキスト」

攻撃者は、署名リクエストの「ドメイン分離(Domain Separator)」の甘さを突く。
もし、あなたのコントラクトが chainId を確認せずに署名を検証していたらどうなるか? 攻撃者は、メインネットの正当な署名をコピーし、テストネットやフォーク環境で再生(リプレイ)攻撃を仕掛ける。

現場でよく見るミスは、バックエンドのバリデーションで「とりあえず署名が一致すればOK」という安直な実装だ。これでは domain separator に含まれる verifyingContract(コントラクトアドレス)が検証されていないため、全く別のコントラクト宛の署名を使い回される恐れがある。

—

実装サンプル:完璧な署名検証(JavaScript & Solidity)

フロントエンドからバックエンドまで、一貫して「型」を強制する実装を見せよう。

1. フロントエンド(署名生成)

ethers.js を使った、ユーザーに安心感を与える署名生成のフローだ。

// 署名対象の型定義(構造化データ)
const types = {
  Permit: [
    { name: "owner", type: "address" },
    { name: "spender", type: "address" },
    { name: "value", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" }
  ]
};

// ドメイン分離情報(ここが重要!)
const domain = {
  name: "MySecureDApp",
  version: "1",
  chainId: 1, // メインネット固定
  verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
};

const message = {
  owner: userAddress,
  spender: contractAddress,
  value: 1000,
  nonce: currentNonce,
  deadline: Math.floor(Date.now() / 1000) + 3600
};

// これにより、MetaMask等のウォレットに詳細な情報が表示される
const signature = await signer.signTypedData(domain, types, message);

2. Solidity(コントラクトでの検証)

コントラクト側では、ecrecover を使って署名者を抽出する。ここで domainSeparator を正確に再構成することが防御の要だ。

// OpenZeppelinのEIP712ライブラリを活用するのがベスト
function verifySignature(
    address owner, 
    uint256 value, 
    bytes memory signature
) public view returns (bool) {
    bytes32 structHash = keccak256(abi.encode(
        PERMIT_TYPEHASH,
        owner,
        spender,
        value,
        nonce,
        deadline
    ));
    
    // ドメイン分離とハッシュを結合して署名者を復元
    bytes32 hash = _hashTypedDataV4(structHash);
    address signer = ECDSA.recover(hash, signature);
    
    return signer == owner;
}

—

運用時のセキュリティTips:WAFとヘッダー設定

Webアプリ側(APIサーバー)でこの署名を検証する場合、単なる POST リクエストの検証に留まらない。署名データ自体を改ざんさせないために、以下のセキュリティレイヤーを意識してほしい。

1. リプレイアタックの防止:
バックエンドのDBで nonce を管理し、一度使われた署名は即座に無効化すること。署名の再利用は、IoTデバイスの認証でも鉄則だ。
2. Nginx/WAFでの制限:
署名リクエストが集中するエンドポイントには limit_req を設定し、ブルートフォースによる署名推測を遮断する。

# Nginx設定例:署名検証APIのレート制限
location /api/v1/verify-signature {
    limit_req zone=auth_limit burst=5 nodelay;
    # 署名データは大きいため、ボディサイズに余裕を持たせつつも制限する
    client_max_body_size 1k; 
}

最後に:エンジニアとしてのマインドセット

「動けばいい」というコードは、セキュリティリサーチャーにとっては「穴が空いている」と同義だ。今回紹介したEIP-712は、ただの技術仕様ではなく、ユーザーに対して「何を認可しているか」という誠実さを提示するためのコミュニケーションツールでもある。

SCADAのネットワークを堅牢にするのと同じで、Web3のセキュリティも「信頼できる境界線(Trust Boundary)」をどこに引くかで決まる。署名という究極の承認行為を、ただのバイト列の処理で終わらせるな。構造化し、型を定義し、常に文脈を検証し続けろ。

それが、我々が守るべきシステムの「防波堤」になるのだから。

コメント

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