ブラインド署名:画面が何も役に立つことを言わないとき、あなたは実際に何を承認しているのか

·6 分で読める·SSP Editorial Team 著
SSP Academy カバー:ブラインド署名と読めない取引承認画面

ブラインド署名:画面が何も役に立つことを言わないとき、あなたは実際に何を承認しているのか

暗号資産の大きな損失のほとんどは、ある特定の瞬間に起きます。鍵が盗まれるときではありません。理解できない署名要求を眺めた人が、たぶん大丈夫だろうと判断して、承認を押す瞬間です。

業界にはこれを指す言葉があります。ブラインド署名。読めないデータに自分の署名を載せることです。そして長年、ウォレットはそれを当たり前として扱ってきました。

署名画面が果たすべき役割

自己保管の安全性の約束は、一つの前提に乗っています。あなたの承認なしには何も動かない、という前提です。2-of-2 マルチシグ、ハードウェアウォレット、エアギャップされた署名機――そのどれもが、あなたの意図を署名へ変換する機械です。

その機械の値打ちは、あなたが承認した内容をどれだけ理解しているかと、ぴったり同じです。

画面が 「0x8f3C… へ 0.5 ETH を送る」 と言っているなら、あなたの承認には意味があります。自分がしようとしていたことと突き合わせたからです。画面が 「コントラクト操作――データ:0xa22cb465000000000000000000000000d8dA6…0001」 と言っているなら、あなたの承認には何の意味もありません。あなたが同意しているのは取引ではなく、十六進数でできた長方形です。

攻撃者は、この二つの画面のどちらをあなたに見せる必要があるかを知っています。

バイトの中に隠れているもの

EVM 取引のペイロードは calldata です。四バイトの関数セレクターに続いて、ABI エンコードされた引数が並びます。機械にとっては完璧に読め、人間にとっては完璧に不透明です。お金が出ていく道筋は、そのセレクターのうちのほんの一握りです。

approve(address,uint256) ――セレクター 095ea7b3。あるコントラクトにあなたのトークンを使う許可を与えます。送金ではなく、恒常的な権限付与です。署名した時点では何も動きません。だからこそすり抜けるのです。吸い出しは後から、攻撃者の都合のよい時に起きます。

setApprovalForAll(address,bool) ――セレクター a22cb465。NFT 版であり、より悪質です。一度の署名で、あなたが保有するそのコレクションのすべてのトークンに対する権限をオペレーターに与えます。今持っているものも、これから持つものもです。安心の拠り所になる金額欄すら存在しません。

increaseAllowance(address,uint256) ――セレクター 39509351。既存の許容額を積み増します。誰もが見張れと言われたセレクターではないため、しばしば見落とされます。

transferFrom(address,address,uint256) ――セレクター 23b872dd。すでに許容額を与えたアドレスからトークンを動かします。以前の approve が認めたものを、ついに使う署名です。

腹に落としておくべき型はこれです。あなたのお金を盗む取引は、あなたが署名した取引であることはめったにない。 あなたが署名したのは許可です。窃取は後から送られる別の取引であり、あなたはそれを目にすることがありません。

なぜ「無制限」が探すべき言葉なのか

許容額を悪用する手口は、ほぼ例外なく一つの特徴を共有します。金額が事実上無限であることです。

Dapp が無制限の承認を求めるのは、そのほうが便利だからです。一度承認すれば二度と尋ねられません。その結果、利用者は無制限の支出権限を習慣的に与えるよう躾けられ、115792089237316195423570985008687907853269984665640564039457584007913129639935 を表示するインターフェースは誰にも何も伝えていません。

素朴な実装が引っかかる細部があります。「無限承認」の正準値はちょうど 2²⁵⁶−1 なので、その定数と比べるのが一番わかりやすい検査です。ところが Dapp は他の天文学的な数値――最大値の半分、0xff…f0、10¹⁸ × 10³⁸ ――も日常的に使います。実用上はどれも無制限でありながら、完全一致の判定はすり抜けてしまう。正確な番兵値でしか発火しない警告は、攻撃者が一を引くだけで回避できる警告です。

正しい閾値は、番兵値よりずっと下、現実にありうるどんな値よりずっと上にあります。SSP は 2²⁵⁵ 以上の許容額をすべて印付けします。およそ 5.8 × 10⁷⁶ で、ありうるどんな ERC-20 の発行総量も数十桁上回ります。正当なものが誤って印付けされることは決してなく、最大値のすぐ手前に値を削り込んでも警告は避けられません。

この閾値は許容額を与える呼び出しにのみ適用されます。単純な送金において「無制限」は意味のある概念ではない――特定の額を動かしているのですから――ので、そこでは正確な最大値の番兵はそのままにしてあります。

SSP がしていること

SSP は承認画面で calldata を平易な言葉に読み解きます。認識できたセレクターは、実際の中身として描かれます。相手は誰か、金額はいくらか、与えられる権限は無制限か。生の十六進数はもはや主役の内容ではなく、必要な人のために「詳細」の奥に置かれています。

三つの設計判断が、読み解きそのものより重要です。

デコーダーは表示層に徹します。 署名される内容を決して変えません。承認は常に元のペイロードそのものに署名し、この補助機能は以前は生の十六進数として示されていたバイトを提示し直すだけです。ペイロードを書き換えうるデコーダーは、防御ではなく新しい攻撃面になります。あなたが読むものと署名するものは、常に同じバイトでなければなりません。

閉じる方向に失敗します。 未知のセレクター、長さの不一致、壊れた十六進数、非標準のアドレス詰め――想定外は何も返さず、UI は汎用の操作へ退き、生の十六進数は「詳細」に収まります。決して推測しません。推測するデコーダーはデコーダーが無いことより悪い。自信ありげな誤った要約は、見えている十六進数より危険だからです。十六進数は少なくとも、あなたがそれを理解していないと正直に告げてくれます。

この「閉じる方向に失敗する」という本能は、未知の関数よりさらに深くまで及びます。ABI エンコードされたアドレスは十二個のゼロバイトに続く二十バイトのアドレスです。先頭のバイトに他の何かが入っている語は正準な符号化ではなく、SSP はそれを解釈しようとするのではなく疑わしいものとして扱います。真偽値も同じで、受け付けるのは正準な符号化――全ゼロ(偽)と、三十一個のゼロに 0x01(真)――だけです。支出権限を与える画面での非正準な符号化は、構文解析の腕試しではなく赤信号です。

トークンの記号と小数桁は決して推測しません。 人間向けの金額は、そのトークンが端末上のレジストリから確実に判明しているときにだけ表示されます。そうでなければ基本単位の生の数値が出ます。これは意図的に見栄えを落としています。ただ自称しているだけのコントラクトに対して「5.0 USDC」と表示すれば、デコーダーは嘘をつく機械に変わってしまう。それこそが攻撃者の望む結末です。

そして、装飾ではなく構造に属する部分があります。SSP では取引は一方で組み立てられ、もう一方で承認されます。ブラウザ拡張で構成され、あなたの電話で独立に読み解かれて表示され、そこで SSP Key が取引ハッシュを端末上で再計算し、示された内容と一致しなければ署名を拒みます。一台の端末でのブラインド署名は、画面が一つ乗っ取られれば足りるということです。ここでは、読み解かれた操作を見せる画面と、二本目の鍵を持つ端末が同一の端末であり、その端末は信じるのではなく検証します。

自分の承認をどう扱うか

approvesetApprovalForAll を危険なものとして扱ってください。 人にお金を失わせているのはこの二つの署名であり、何も動かないからこそ無害に見えるのです。

無制限の承認は、断れるなら断ってください。 多くの Dapp は、あなたが上限を設定すればその額を受け入れます。手間は増えますが、損失の上限を実際に使うつもりだった額に抑えられます。

すでに与えたものを点検してください。 古い承認は期限切れになりません。二年前にあるプロトコルへ与えた許可はいまも生きており、そのコントラクトが後に侵害されれば、あなたのトークンへ通じる生きた経路になります。承認の取り消しはすぐ終わり、自己保管において最も費用対効果の高い片づけの一時間です。

画面が何も告げないとき、それこそが合図です。 ウォレットが取引の中身を言えないなら、それは情報であって、クリックで押し通す不便ではありません。読めない要求への正しい応答は、目を凝らすことではなく、立ち止まることです。

目的はあなたに十六進数を読ませることでは決してありませんでした。あなたが何かを承認するとき、それが何であるかについて、あなたとウォレットの見解が一致していると確かめられるようにすることです。

この記事をシェアする

関連記事