T13 · ウォレット接続
DApp へのログインでは、実際に何が起きるのか?
- 練習する能力
- Builder
- 実践
- 署名によるログインと承認取り消しの入口を実装し、テストウォレットで両方の手順を試す。
- AI Lab
- AI に署名フィッシングの典型例を列挙させ、署名内容を UI がユーザーに正確に示しているか確認する。
現実の問題
企画担当が言います。「ウォレットログインを追加しよう。WeChat ログインと似たものでしょう。」
DApp を開き、右上の「Connect Wallet」を押し、ポップアップを確認するとアバターが現れます。確かに似て見えます。
しかし分解すると、少なくとも 3 つのまったく異なる動作からできています。
- ブラウザー拡張がアドレスをページへ渡す。何の検証も行われていません。ページがアドレスを「教えられた」だけです。
- ページが署名を要求し、ユーザーが確認する。何を証明し、誰へ証明したのでしょうか?
- ログイン直後に「承認」という別のポップアップが出るサイトもあります。ユーザーはログインの続きだと思って確認します。これでウォレット内のあるトークンをすべて渡してしまうかもしれません。
どれもポップアップで、確認を押すため似ていますが、結果はまったく違います。一つ目は何も起こらず、二つ目の最悪の結果はアカウントのなりすまし、三つ目は資金の喪失です。
ユーザーが区別できないのは、製品側も区別できていないことが多いからです。 本章では、3 つを明確に分けます。
思考実験
一つの鍵を、3 つの道具として考えます。
入社時に受け取った社員証が、次の役割をすべて持っているとします。
- 入館カード。かざすとゲートが開き、「社員である」とだけ証明します。
- 署名印。書類へ押すと、「読んで承認した」と証明します。
- 白紙小切手に署名できるペン。署名すると経理が支払い、しかも紛失届を出すまで、同じ署名で繰り返し換金できます。
現実には、この 3 つを一体化しません。カードをなくせば再発行し、印をなくせば公証の手続きをし、白紙小切手はそもそも渡されません。
Crypto ウォレットの秘密鍵は、3 つを一つにまとめます。同じ鍵で入館し、印を押し、繰り返し換金できる小切手へ署名できます。違いは鍵ではなく、署名する紙に何が書かれているかです。
そして「繰り返し換金できる小切手」は、チェーン上の状態を持ちます。一度渡すと、取引を送って取り消すまで有効なままです。ページを閉じても、キャッシュを消しても、パソコンを変えても残ります。Web2 のログインにはない種類のものです。
問いは、ユーザーが署名中の紙を理解できるか、になります。
あなたが決める
DApp のログインを作ります。4 つの方法からどれを選びますか?
結果を観察する
4 つの選択肢が示すのは、接続、認証、承認は異なる動作なのに、画面ではどれもポップアップに見えるということです。
| 接続 | 認証 | 承認 | |
|---|---|---|---|
| ユーザーの操作 | アドレスをページへ知らせることに同意 | メッセージへ署名 | 取引へ署名 |
| チェーンへ送るか | 送らない | 送らない | 送る。Gas が必要 |
| 検証者 | 誰も検証しない | 自分のバックエンド | チェーン上のコントラクト |
| 証明すること | 何も証明しない | 秘密鍵の保有 | 資金を使う権限を与える |
| 有効期限 | ページを閉じると終了 | 自分で設定 | 自分で取り消すまで期限なし |
| 問題の代償 | 公開アドレスの開示 | アカウントのなりすまし | 資金が移される |
特に 2 つの欄を見てください。「誰も検証しない」は最も見落としやすい欄です。取得したアドレスは偽装でき、表示には使えても認可には使えません。「期限なし」は、ページを閉じても残る唯一の欄です。承認には取り消し入口が必要で、それは製品の一部であるべきです。別の場所をユーザーに探させてはいけません。
表にない注意点もあります。「チェーンへ送らない」と「危険がない」は別です。 オフチェーン署名自体が承認の証票になる場合があります。実例では、ユーザーの取引送信を必要としない深刻なフィッシングを見ます。
モデルを作る
ログインを、役割が単一の 5 段階へ分けます。
- Connect:アドレス取得
- Authenticate:署名による証明
- Session:セッション発行
- Authorize:必要時に承認
- Revoke:いつでも取り消す
署名する内容がすべてを決める
署名要求は 3 種類あり、リスクには桁違いの差があります。
| 種類 | ウォレットの表示 | リスク |
|---|---|---|
| 人が読めるメッセージ | 理解できる全文 | 低い。ただし中に何かを隠していないこと |
| 構造化データ | フィールド名と値 | 中程度。名前で誤解させることができる |
| 生のハッシュ列 | 16 進数列 | 非常に高い。何に署名するか分からない |
3 番目はブラインド署名です。意味不明な文字列を確認すると、何が起きるか分かりません。製品がそれを要求するなら、設計に問題があります。
2 番目は安全そうに見えるため、さらに注意が必要です。spender がアドレス、value が 78 桁の数値なら、すべてのフィールドを「見た」ユーザーでも、「見知らぬアドレスに全残高を動かす権利を 1 年間与える」とは読み取れません。
正直な表示の基準は、生データを出すことではなく、読んだユーザーが結果を説明できることです。 両者は大きく違います。
ログインメッセージの形
純粋なテキストで示します。形式よりフィールドの意味が重要です。
example.com が、このアドレスでのログインを求めています:
0x<自分のアドレス>
署名すると example.com へのログインに同意します。
この署名は取引を送らず、資金操作の権限も与えません。
Chain ID: <チェーン ID>
Nonce: <バックエンドが生成した一度限りの乱数>
Issued At: <発行時刻>
Expiration Time: <有効期限>各フィールドが別の攻撃を防ぎます。ドメインはサイト間の再利用、アドレスは他人の署名によるなりすまし、Chain ID はチェーン間の再利用、Nonce は同じサイトでの再利用を防ぎます。Nonce は使用直後にバックエンドで無効にします。有効期限は盗まれた署名を使える期間を制限します。
「取引を送らず、資金操作の権限も与えません」は、ユーザーへの約束です。表示を見て実際にその通りだと確認できるたび、信頼が増します。ログインへ承認を一度でも忍び込ませれば、業界全体の信頼を損ないます。
セッションの寿命
検証後は、Web2 と同じ普通のセッション資格情報を発行します。ただし Crypto 特有の失効条件が 3 つあります。
type WalletSession = {
address: string; // 認証済みアドレス
chainId: number; // 認証時のチェーン
issuedAt: number;
expiresAt: number;
};
// 直ちにセッションを破棄する 3 条件:
// 1. ウォレットのアカウントが変わり、現在のアドレスが session.address と異なる
// 2. ウォレットのチェーンが変わり、単一チェーンの業務で chainId が一致しない
// 3. ウォレットが切断された最初は特に見落としやすいものです。ウォレットで別アカウントへ変えても、ページのセッションは古いままです。B の画面で A のデータを操作することになります。変更イベントを監視し、アドレスが変わったら、静かに切り替えるのではなく、直ちにセッションを破棄して再認証を求めます。
承認は必要な時に、必要な額だけ、取り消し可能に
承認はチェーン上の取引です。設計で問うのは 3 点です。
- いつ求めるか? ログイン時ではなく、初めてその機能を実際に使う時です。
- いくら承認するか? 無制限ではなく、その操作に必要な額です。一回のクリックを省く代わりに、永続的なリスクを残さないでください。
- どう取り消すか? 自製品で与えた承認を一覧にし、ワンクリックで取り消せるページを用意します。
3 番目が Lab の半分です。「別ツールで取り消せる」と考えるチームも多いですが、自分の製品が作ったリスクの出口は、自分の製品が提供すべきです。
何と呼ぶか
ブラウザー拡張、モバイルのディープリンク、QR ペアリング、コントラクトアカウントなど、異なる接続方法を統一する層です。上位へ同じアカウント要求、署名要求、取引送信のインターフェースを提供します。
価値はコード量よりも、業務ロジックを特定ウォレットの実装詳細から独立させることです。ウォレットを変えるために状態機械を変えるべきではありません。
ログインメッセージを標準化し、ドメイン、アドレス、チェーン ID、Nonce、発行時刻、有効期限の形式と順序を定める取り決めです。
中心的な貢献は形式ではなく、ウォレットがログインメッセージだと認識し、正しく表示できることです。 独自形式では通常のテキストとしてしか表示できず、ユーザー向けの識別支援が失われます。
認証成功後に発行され、「この期間、このアドレスが身元を証明済み」と表す資格情報です。
通常の Web セッションです。特有なのは破棄条件です。アカウント変更、チェーン変更、ウォレット切断のいずれかで、直ちに失効させます。
特定アドレスに、特定額まである資産を使う権限を与えるチェーン上の取引です。
覚える性質は 3 つです。持続すること(ページを閉じても消えない)、繰り返し使えること(額を使い切るまで有効)、ログインと無関係であること(ログインには不要)です。
与えた承認額をゼロへ戻します。これもチェーン上の取引で、Gas が必要です。
重要なのは、将来の使用は止められても、既に移された資産は取り戻せないことです。フィッシングに気付いたら残りの承認をまず取り消しますが、流出した資金は戻りません。
ウォレットに解読不能なデータだけが表示され、ユーザーが署名の結果を判断できない状態です。
ユーザーではなく、上流側がデータを理解可能な説明へ変えていないことが問題です。それを求める製品は自分を信用するよう要求していますが、Crypto が目指すのはその種の信頼を減らすことです。
実践
すべてテストネットで行い、テスト用コインだけを持つウォレットを使います。実資産のあるアドレスは使いません。 承認の取り消しは実際に取引を送るため、テストネットで行います。
接続し、何も証明していないと認識します。
アカウントを要求し、アドレスとチェーン ID を取得します。connection という状態へ保存し、user とは呼ばないでください。
名前にも意味があります。user と呼べば、いつか誰かが身元としてバックエンドへ渡してしまいます。
バックエンドへ Nonce を要求します。
バックエンドが一度限りの乱数を生成し、アドレスに結び付けて保存し、数分ほどの短い期限を設けます。
Nonce は必ずバックエンドで生成します。フロントエンドでの乱数生成は、攻撃者にもできるためリプレイを防げません。
ログインメッセージを作り、署名を求めます。
前のテンプレートに沿い、ドメイン、アドレス、チェーン ID、Nonce、発行時刻、有効期限を入れたテキストを作ります。
作ったら、まず自分でウォレットの表示を確認します。全文が同じで、切れたり文字化けしたり、16 進数になったりしていないことを確認します。自分に読めないものは、ユーザーにも読めません。
バックエンドが署名を検証し、Nonce を無効にします。
5 つの確認はすべて必須です。
- 署名からアドレスを復元し、メッセージのアドレスと照合
- ドメインが自サイトと等しいことを確認
- Nonce が存在し未使用であることを確認し、直ちに削除
- 期限内であることを確認
- チェーン ID が想定通りであることを確認
通過したら HttpOnly、Secure、SameSite を設定したセッション Cookie を発行します。
第 3 工程の削除はアトミックに行います。そうしないと同時要求が両方通過します。
失効処理を接続します。
アカウントとチェーンの変更を監視します。アドレスが変わったら、直ちにフロントエンド状態を消し、バックエンドのログアウトを呼びます。
ログイン後に別アカウントへ変更し、即座にログアウトするか試してください。古いアドレスのデータが残れば、そのバグは既にコードにあります。
承認パネルを作ります。
allowance を読み、現在のユーザーが自コントラクトへ与えた承認を一覧にします。数字を人が理解できる表現へ変えます。天文学的な額なら、長い数字の代わりに「無制限」と赤字で表示します。
取り消しボタンを追加し、額を 0 にする取引を送り、確定を待って一覧を更新します。
2 回試し、攻撃も実施します。
初回:正常なログイン → 有限額の承認 → 取り消し → ゼロになったことを確認。
2 回目:初回の署名を別ドメインの要求でバックエンドへ送り、必ず拒否されることを確認します。同じ署名を同じバックエンドへ 2 回送った場合も、2 回目は拒否されるべきです。前者はドメイン、後者は Nonce の無効化を検証します。
以上の 6 工程を終えれば、本番で使えるログイン手順ができ、2 つの防御を自分で検証したことになります。
AI Lab
2 段階で進めます。中心は 2 番目です。
第 1 段階:
Crypto ウォレットの署名フィッシングの典型を列挙し、各形態について説明してください。
- 攻撃者はどの種類のデータへ署名させるか
- ユーザーはウォレットのポップアップで実際に何を見るか
- 署名後に攻撃者は何ができ、ユーザーの追加操作は必要か
- ユーザー側から識別できる兆候はあるか
第 2 段階:
これは自分のログインと承認の画面文言、ポップアップ内容です(実際の内容を貼る)。
各形態と照合し、正直に表示できているか、ユーザーが誤解する可能性があるか調べてください。
一般論ではなく、具体的な修正を示してください。第 1 段階は列挙なので、モデルはよく答えるでしょう。本当に必要なのは第 2 段階です。一覧をチェックリストにして、「自分の画面は何を表示するか」をすべて具体的に答えます。
答えられない項目が、製品の作っているリスクです。
よくある偏りも知っておいてください。モデルは偽サイトや偽ドメインを詳しく説明し、本物のドメインでの誤解を誘う署名を軽く扱いがちです。後者こそ防ぎにくい問題です。URL もサイトも本物なのに、画面の説明と署名内容が違います。この形態を追加で尋ねてください。
AI の回答後、自分で必ず検証すること
- 各形態について、概要だけでなく、ユーザーがウォレットで具体的に何を見るかを説明できる
- 取引を送らず、オフチェーン署名だけで資産を移せる形態が含まれている。最も危険で、漏れやすい種類です
- 示された署名形式とフィールド名を実際のウォレットで表示し、一致を確認した
- ログイン署名と承認署名を混同していない
- 情報の表示を変えず、注意文を一つ足すだけの見せかけの対策が含まれていないか調べた
- 一覧と自分の画面を照合し、各項目で実際に何を表示するか具体的に答えた
実例
一回のクリックを省くため、初回訪問時に無制限の承認を求める製品があります。
何も操作せず資金も動いていないので、ユーザーは異常を感じません。しかし、その時からコントラクトに問題があれば、そのトークンは危険にさらされます。その機能をその後一度も使わなくてもです。長年の事故では、侵害されたコントラクトの被害者に、「既に使っていないが取り消していない」アドレスが多数含まれました。
教訓:機能を使う時に、必要額だけ承認を求めます。
ある種の承認は、オフチェーン署名一つで可能です。ユーザーが取引を送る必要がなく、Gas も消費しません。
怖いのは、「署名は安全で、取引が危険」と教えられたユーザーが、「無料の署名だけ」と聞いて警戒を解くことです。攻撃者は署名をチェーン上で実行して資産を移します。被害者は「取引を確認していない」とよく話します。
教訓:「チェーンへ送らない」と「危険がない」は別です。 画面が署名の結果を説明しなければなりません。
NFT 取引プロトコルは、特定価格で資産を売る意思を表す構造化データによる、オフライン注文を広く使います。
攻撃者はそれをログイン風の手順へ隠します。ユーザーは意味の分からないフィールドを確認し、実際には「価格ゼロで売る」注文へ署名します。
教訓:構造化署名のフィールド名は、誤解を誘うものにできます。読めることと理解できることは違い、基準はユーザーが結果を説明できることです。
小さな画面に 16 進数しか出ない操作もあります。何に署名するか判断できず、パソコン画面を信じるしかありません。しかし侵害されたのは、そのパソコンです。ハードウェアウォレットは秘密鍵の流出を防いでも、理解しないものへの署名は防げません。
教訓:プロトコル設計では、明確に表示できること自体が要件です。付加的な装飾ではありません。
変数を一つ変える
変更を監視していなければ、画面には A のデータ、署名には B のウォレットという最悪の状態になります。ユーザーは A を操作しているつもりで B を操作し、データ混乱や誤送金が起きます。
正しい対応は、変更直後にセッションを破棄して未ログインへ戻すことです。自動切り替えを試みないでください。再び身元を証明しないまま、別の身元の権限を与えることになるからです。
従来の秘密鍵署名ではなく、署名の定義と検証をコントラクト自身が行います。署名からアドレスを復元して照合する方式は、そのままでは失敗します。
メッセージハッシュと署名を、コントラクトの検証インターフェースへ渡し、「自分に有効な署名か」を答えてもらいます。
さらに、コントラクトウォレットの管理者は変更可能です。今日検証した人が明日も所有者とは限りません。セッションの期限を短くし、重要操作では再検証します。T27 でアカウント抽象化を扱います。
キャッシュ、ログ、中間者によって署名が漏れると、永久に有効な通行証になります。ユーザーが変更できず、別地域からのログイン通知もないため、パスワード漏洩より気付きにくい問題です。
期限は漏洩を防ぐためではなく、漏洩後の損失期間を制限するためです。安全設計における予防と、被害範囲の制限の違いです。
認証は存在しないも同然です。誰でも任意アドレスを API へ入力し、その非公開データを読んだり、名義を使って操作したりできます。よくある理由は、フロントエンドで実際に取得したアドレスをバックエンドへ渡すのが自然に見えることです。
レビュー用のルールを一つ設けます。保護された API のアドレスは、要求パラメータではなくセッションから取得します。 パラメータにアドレスがあれば、「照会対象の指定か、身元の主張か」を確認します。
持ち帰る問い
ユーザーは誰ですか?具体的には、バックエンドは何を根拠に、この要求がアドレスの持ち主から来たと考えますか。「フロントエンドが教えたから」なら、実際には誰か分かっていません。
誰がリスクを負いますか?承認後は、気付けない持続的なリスクをユーザーが負います。ポップアップ一回を省く体験は、長期的なリスクとの交換です。
どの判断に人の介入が必要ですか?署名確認は、この講座の最も基本的な Human-in-the-loop です。失敗しがちなのは読まないからではなく、ポップアップの内容を理解できないからです。理解可能にするのは製品の責任です。T32 では Agent の承認設計へ広げます。
章末チェック
アドレスは公開情報で、誰でも述べられるからです。接続は検証可能な証拠を何も生成していません。公開データの表示には使えますが、持ち主だけがアクセスできる API があれば署名検証が必要です。
基準は、このデータを他人に見られて問題があるかです。あれば検証が必要です。
署名は証明です。秘密鍵を持つことを示し、チェーンへ送らず、費用もかからず、それ自体では状態を変えません。
承認は許可です。特定アドレスへ資産の使用を許し、チェーンへ送り、Gas を使い、持続し、自分で取り消します。
重要な例外は、署名自体が承認になる場合があることです。オフチェーンでも他人がチェーンで実行できます。より正確な基準は、送信するかではなく、「署名したデータで他人に何ができるか」です。
ドメインはサイト間のリプレイを防ぎ、他サイトのログイン署名を無効にします。なければ、最も弱いサイトと同じ安全性になります。
Nonce は同じサイトでのリプレイを防ぎます。同じメッセージの署名は同じなので、一度限りの乱数がなければ無限に使えます。
両方をバックエンドで検証します。フロントエンドの確認は誤操作には効いても、自分のフロントエンドを使わない攻撃者には効きません。
承認が永続し、全残高を危険にさらします。コントラクトに将来問題があれば、使わなくなった人を含め、承認済みの全員が被害を受けます。
採用理由は現実的です。都度承認では、毎回一取引、一回の待機、一回の Gas が増え、転換率が下がります。
折衷案は、今回に十分だが無制限ではない額と、分かりやすい取り消し入口です。承認を大きくするより、取り消しを容易にする方が重要です。
唯一の正解はありません。次を調べます。
- 読んだユーザーが、署名後に起きることを一文で言えるか?言えなければ表示が不十分です。
- 巨大な数値を分かりやすく変えたか?78 桁なら「無制限」と赤字にします。
- 相手のアドレスの意味を説明したか?
0xから始まる列だけでは判断できません。 - 身元の証明と資金承認を、異なる視覚表現で区別したか?同じ見た目が事故の出発点になります。
- 署名拒否をエラー表示にするか、静かに戻すか?拒否はエラーではありません。
第 5 項は細部に見えますが、何でも確認する習慣が付くかを左右します。
要点
署名は身元の証明であり、権限の承認ではない。承認は別に設計し、取り消せるようにする。