ブロックチェーンの世界へようこそ!スマートコントラクトの開発に少しずつ慣れてくると、「ユーザーにガスの手数料を払わせずに、オフチェーン(チェーンの外)で署名だけしてもらい、後からコントラクトで実行したい」という要件に出会うはずです。いわゆる「メタトランザクション」や「ガスレス取引」と呼ばれる便利な仕組みですね。
でも、ちょっと待ってください。
その「オフチェーンで書いてもらった署名」、そのまま受け取っていませんか?もし対策を怠っていると、悪意ある攻撃者にその署名を盗み見られ、何度も何度も悪用されるという「署名リプレイ攻撃」の餌食になってしまいます。
今回は、身近な「防犯」のたとえを交えながら、この恐ろしい攻撃の仕組みと、それを完璧に防ぐための最強の盾「EIP-712」について、一歩ずつ優しく紐解いていきましょう!
—
1. 家の鍵の「コピー」と「使い回し」にたとえてみよう
セキュリティの仕組みを考えるときは、現実世界の防犯に置き換えるとすごく分かりやすくなります。
例えば、あなたが遠くに住む家族に、自宅の合鍵を渡す場面を想像してください。
普通なら、鍵を一本渡して「この鍵を使って、今度うちに来てね」と言いますよね。
ここで、もしその鍵が「コピーし放題の魔法の鍵」で、さらに玄関の鍵穴が「一度開けられた鍵でも、何度でも同じように開いてしまうガバガバな構造」だったらどうなるでしょうか?
- 悪意ある泥棒が、その鍵のやり取りを途中でこっそり盗み見ます。
- 泥棒は全く同じ鍵を勝手に複製します。
- 泥棒は、家族がまだ来ていないうちに、その鍵を使ってあなたの家に何度も侵入し、財産を根こそぎ盗んでいきます。
ブロックチェーンのオフチェーン署名もこれと全く同じです。
ユーザーがウォレット(MetaMaskなど)で「この内容に同意します」と署名したデータ(=デジタルな合鍵)は、ネットの海を流れていきます。もし対策をしないと、攻撃者はその署名をそっくりそのままコピーして、何回もコントラクトに送りつけて不正な処理を実行(リプレイ)してしまうのです。
これが、署名リプレイ攻撃(Signature Replay Attack)の正体です。
—
2. 泥棒を防ぐ二つの鉄則:「使い捨ての番号」と「宛先の縛り」
この泥棒に入られないようにするためには、現実の世界でもセキュリティ対策をしますよね。
- 対策1:使い捨ての整理券(Nonce)を使う
銀行のATMやライブのチケットのように、「1回使ったら無効になる番号(ノンンス)」をチケットに毎回印刷しておきます。もし泥棒が古いチケットをもう一度持ってきたとしても、「この番号はもう使用済みです」とATMに拒否されます。
- 対策2:鍵に「行き先(宛先)」を刻印する
「この鍵は、A町のAマンションの部屋専用です」とハッキリ書いておけば、たとえ万が一鍵を落としても、B町のマンションで使われる心配はありません。
スマートコントラクトの世界でも、これと全く同じ仕組みを実装する必要があります。それを体系化し、標準規格として定めたのが「EIP-712」という魔法の仕様書なんです!
—
3. EIP-712とは? なぜ「型付きデータ」が必要なのか?
昔のイーサリアムでは、署名するときに画面に 0x48656c6c6f20... のような、何が書いてあるかさっぱり分からない「ただの16進数の文字列」が表示されていました。これでは、ユーザーは「自分が何を許可したのか」が分からず、詐欺に気づけませんよね。
そこで登場したのが EIP-712 です。
EIP-712を使うと、ウォレットの画面に以下のように綺麗で分かりやすい「型付きのデータ(構造化データ)」が表示されるようになります。
- 誰から誰への転送なのか
- 金額はいくらか
- どのコントラクト(宛先)で使うものか
- 今何回目の取引か(Nonce)
これにより、ユーザーは安心して署名できますし、プログラム側も安全に検証できるようになります。
—
4. 実装コードで見てみよう!安全なEIP-712コントラクト
百聞は一見にしかず。実際にSolidityでどのように書くのか、初心者の方にも分かりやすいように日本語のコメントをたっぷり入れてコードを見ていきましょう。
以下のサンプルは、「特定のメッセージに署名してもらい、それをコントラクト側で安全に検証して実行する」ためのスマートコントラクトの例です。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
// EIP-712を簡単に実装するため、OpenZeppelinのライブラリを継承します
contract SecureSignExample is EIP712 {
using ECDSA for bytes32;
// ユーザーごとのNonce(使い捨ての番号)を管理するマッピング
// 例: ユーザーAが何回トランザクションを成功させたかを記録します
mapping(address => uint256) public nonces;
// EIP-712のデータ構造(型)の定義
// 「Messageというデータ構造には、sender, receiver, amount, nonceが入っていますよ」と定義します
bytes32 private constant MESSAGE_TYPEHASH = keccak256(
"Message(address sender,address receiver,uint256 amount,uint256 nonce)"
);
// コントラクトの初期化(ドメイン名とバージョンを指定します)
// これにより、「このアプリ専用の署名ですよ」という宛先の縛りが生まれます
constructor() EIP712("SecureSignExample", "1") {}
// オフチェーン署名を使って安全に処理を実行する関数
// @param receiver 受取人アドレス
// @param amount 金額
// @param signature ユーザーがオフチェーンで生成した署名データ
function executeWithSig(
address receiver,
uint256 amount,
bytes calldata signature
) external {
// 1. 現在のユーザーのNonceを取得する
uint256 currentNonce = nonces[msg.sender];
// 2. EIP-712の仕様に沿って、ハッシュ化されたデータ(ダイジェ스트)を組み立てる
// ここで sender, receiver, amount, そして使い捨ての nonce をガッチリ組み込みます
bytes32 structHash = keccak256(
abi.encode(
MESSAGE_TYPEHASH,
msg.sender,
receiver,
amount,
currentNonce // ←ここでリプレイ攻撃を完全にブロック!
)
);
bytes32 digest = _hashTypedDataV4(structHash);
// 3. 署名したのが本当に本人のウォレットか検証する
address signer = digest.recover(signature);
require(signer == msg.sender, "Invalid signature: 署名が一致しません");
// 4. 次回のリプレイ攻撃を防ぐために、Nonceを必ず「+1」インクリメントする
// これで、いま使ったチケットは二度と使えなくなります!
nonces[msg.sender] += 1;
// 5. 本来実行したかった安全な処理をここに書く
// 例: トークンの転送処理など
}
}
コードのポイント解説
nonces[msg.sender] += 1;の重要性:関数が成功した瞬間にNonceを必ず増やしています。これにより、万が一攻撃者が同じ署名をもう一度ブロックチェーンに投げ込んでも、コントラクト側で「あれ?Nonceの番号が古くない?」と気づき、自動的にエラー(リバート)にして弾き返してくれます。_hashTypedDataV4の働き:この関数が、コントラクトのアドレスやチェーンID(Ethereumメインネットなのか、テストネットなのか等)を自動的に計算に混ぜてくれます。そのため、テストネット用の署名をメインネットで悪用するような「クロスチェーン・リプレイ攻撃」も綺麗に防ぐことができます。
—
5. まとめ:一歩ずつ安全なWeb3開発へ
いかがでしたでしょうか?
「署名リプレイ攻撃」という言葉を聞くと難しく感じるかもしれませんが、要するに「使い捨ての整理券(Nonce)」と「行き先が書かれた宛名(EIP-712のドメイン分離)」をしっかりセットにしてあげるだけで、泥棒の侵入をピタリと防ぐことができるんです。
実務の開発現場では、「面倒だからNonceの管理は後回しにしよう…」という油断が、数億円規模ハッキングの引き金になることが少なくありません。
新しい技術に触れるときは、ぜひ今回の「家の鍵と防犯」のたとえを思い出して、堅牢で美しいコードを書く癖をつけていきましょう!
それでは、次のステップでも一緒に一歩ずつ安全なセキュリティ対策を学んでいきましょうね!
コメント