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)」をどこに引くかで決まる。署名という究極の承認行為を、ただのバイト列の処理で終わらせるな。構造化し、型を定義し、常に文脈を検証し続けろ。
それが、我々が守るべきシステムの「防波堤」になるのだから。
コメント