Trusted Signer

Vela は承認の前にすべての取引をデコードしますし、そのデコードは誠実な仕事です。けれど、その仕事をしているのは、取引を組み立てたのと同じアプリです。アプリ自体、あるいはアプリがあなたに届く経路が改ざんされれば、あるものを見せて、別のものに署名させることができます。Bybit で起きたのは、まさにそれでした。

Trusted Signer は、それを 2 つに分けるために存在します。アプリはリクエストを渡すだけで、確認と署名は別のページで行われます。端から端まで読むことも、1 バイトも違わずに再ビルドすることも、自分で動かすこともできるページです。

どこで動くか

公式のページは sign.getvela.app から配信されています。デスクトップ(macOS、Windows、Linux)、iPhone、Android のアプリは、このページにリクエストを送れます。アプリはリクエストをリンクに載せてページをブラウザのタブで開き、あなたはそこで内容を確認して、パスキーで署名します。ページは velawallet:// のリンクで署名をアプリに返します。ウェブウォレットでは使えません。

使うかどうかは自分で選びます。ウォレットを作成するときやサインインするときに署名の方法として選ぶと、それ以降、その端末でのそのウォレットの署名はすべてこのページを通ります。ウォレットの鍵を作成することもできます。sign.getvela.app 上では、アプリと同じ getvela.app のパスキーを使います。

署名の前にすること

  • リクエストを自分でデコードします。その呼び出しが何をするのか、誰に、いくらなのかを、calldata から読み取ります。バッチの中に入れ子になった呼び出しも含みます。
  • 自分で計算したダイジェストにしか署名しません。EIP-191、EIP-712、SafeOp、SafeMessage のダイジェストはページ内で計算し、リクエスト元から受け取ることはありません。SafeOp と SafeMessage のダイジェストは、ウォレットが使うコードである vela-core とテストで照合しています。そしてアプリは、自分で計算したもの以外のダイジェストに対する署名を拒否します。
  • 求められた取引と同じものかを確認します。サイトが求めた呼び出しが、署名される操作の中に実際に含まれていなければ、ページは拒否します。
  • 承認が無制限であれば、そう伝えます。このページは額を変えられません。届いたバイト列に署名するか、何もしないかのどちらかです。そのため、無制限の承認や permit(2^200 以上、Permit2 では 2^152 以上。アプリと同じ基準です)は、その理由を添えて赤で表示され、そのままの内容で署名できます。オンチェーンの上限は、リクエストがここに届く前に、ウォレット自身の承認画面で選びます。NFT コレクション全体に対する承認は拒否します。
  • 責任を持てないものは拒否します。eth_sign、知らないメソッド、トークンをそのトークン自身のコントラクトに送ること、読めない操作、そしてリクエスト元がチャレンジを指定したサインインです。
  • アカウントを明け渡すことになるものは拒否します。アプリと同じ規則で、アカウントが自分自身のオーナー・モジュール・ガード・フォールバックの関数を呼び出すこと(バッチの中でも)、Safe の MultiSend コントラクト(1 つの操作の呼び出しをまとめるもの)以外への delegatecall、そして SafeTx の署名です。サイトが求めた呼び出しだけでなく、アプリが組み立てた操作のすべての呼び出しを確認します。
  • アカウントのアドレスと、ページ上で計算したアイデンティコンを表示します。受取人やコントラクトの名前を、リクエストから取って表示することはありません。コントラクトに名前を付けられるのは、ページ自身が持つレビュー済みの一覧だけです。アカウント自身の名前は、正しいパスキーを選べるようにアプリが送ってくるもので、アドレスの横に表示されます。
  • 署名のたびに本人確認(指紋、顔、または PIN)を求めます。

あえて持たないもの

  • 編集機能はありません。リクエストは届いた時点で確定しています。署名するか、しないかだけです。手数料の選択や上限額の編集は calldata を書き換えることになり、それこそこのページが防ごうとしている病です。
  • ネットワークにアクセスしません。このページは 1 つのファイルで、そのコンテンツセキュリティポリシー(default-src 'none')はファイル自身のバイト列の中にあります。そのため、何かを取得することも、接続を開くことも、画像を読み込むこともできません。ページから外に出ていくのは返答だけで、それもリクエストに含まれるコールバックのリンク(Vela のアプリからのリクエストなら velawallet://)をたどるときに限られます。トークンのロゴは文字で描きます。

アプリの側でも確認する

アプリも、このページを信用しません。署名を受け入れるのは、署名されたチャレンジがアプリが計算したダイジェストであり、本人確認が行われ、鍵があなたのウォレットの鍵のひとつで、P-256 の署名がその鍵で検証できたときだけです。

公開したバージョンはすべて検証できる

各バージョンは app-web/trusted-signer/src/ から単一のファイルとして、再現可能な形でビルドされ(Bun でも Node でも同じバイト列になります)、それ専用のアドレス sign.getvela.app/b/<sha256>/sign.html で、それまでのすべてのバージョンと並べて公開されます。一覧は sign.getvela.app/index.json にあります。

cd app-web/trusted-signer
node samples/build-single.mjs --check   # rebuilds a version listed in dist/
curl -sL https://sign.getvela.app/b/<sha256>/sign.html | shasum -a 256

デスクトップアプリは起動時に、これから開く公開バージョンを取得してハッシュを計算し、アプリに組み込まれたバージョンと比較します。その結果はログに記録されるだけで、一致しないページもそのまま開かれます。スマートフォンのアプリは、まだこの確認をしていません。

自分のコピーを動かす

「設定」には、アプリが開くページのアドレスが保存されているので、それを自分のデプロイ先に向けられます。HTTPS のアドレスならどれでもよく、テスト用には localhost も使えます。bun samples/build-single.mjs(または node)でビルドし、dist/ を自分のホストにコピーしてください。

自分のドメイン上のコピーは、getvela.app のパスキーではなく、そのドメインのために作られたパスキーで署名します。つまりこれは、鍵が自分のドメインの下にあるウォレットを作って使うための方法であって、既存の getvela.app のウォレットのために署名する方法ではありません。ひとつのウォレットの鍵は、すべて同じドメインに属します。

コードと、それをビルドして検証するスクリプトは、app-web/trusted-signer/ にあります。