セキュリティ仕様
このプロジェクトを信頼しやすくするために、どこで何を守っているかを説明します。ブラウザで動かすときの守り方と、署名エンジンの切り分けの2つです。
ブラウザエンジンを信頼するときのリスクの抑え方
Webアプリでは、鍵も署名エンジンもブラウザ(iPhoneではWebKit)の中にあります。ブラウザ自体は信頼するしかないので、その上で漏れる経路と、鍵がメモリに残る時間を減らしています。
| 対策 | 中身 | 防ぐもの | 残るリスク |
|---|---|---|---|
| 通信先を自分のサイトに限る | Content-Security-Policy でdefault-src 'self'、connect-src 'self'、form-action 'none'、object-src 'none' | ほかのサイトへの送信、外部スクリプトの読み込み | 同じサイトへの送信はできる。配る側が乗っ取られたバージョンには効かない |
| ネットにつながると鍵を消す | オンラインの間は鍵を読み込めず、読み込んだ鍵はつながった時点で消す | 鍵がある間の送信 | ブラウザが知らせる範囲でしか分からない |
| アプリを離れると鍵を消す | 画面が隠れたら鍵を消し、画面も隠す。戻るとページを読み込み直す | ほかのアプリを使っている間に鍵が残ること | アプリ切り替えの画面に残るかはブラウザ次第 |
| 使い終わったバイト列を0で埋める | エントロピー、サイコロの目、署名、カメラの画像 | メモリに残った鍵の材料 | JavaScriptの文字列(入力した単語、パスフレーズ)は消せない。そのため読み込み直しで捨てる |
| シードは画面内のキーで入力 | ネイティブ版と同じキーパッド | キーボードアプリによる記録 | パスフレーズはキーボードを通る。標準キーボードを使う |
| ネイティブと同じ署名エンジン | 同じWASMモジュールを、同じRustのホストごとWASMにして動かす(署名エンジンの切り分け) | Web版だけの実装の違い | ネイティブ版のSHA-256照合は、照合するコードもサイトから届くので効かない |
| 入れたバージョンだけを使う | Service Workerが全ファイルを取り込み、オフラインでもそのバージョンで動く。新しいバージョンは開き直したときだけ | 知らないうちにバージョンが変わること | ネットにつなぐと新しいバージョンが届く |
| バージョン名を見せる | 設定 › このアプリのバージョン。GitHub Actionsでビルドすれば、同じコミットは同じ名前 | 差し替えられたバージョンに気づかないこと | 比べる相手(リリースノート)を信頼する |
| パスキーで暗号化して保存 | WebAuthn PRF。開くたびにFace ID、指紋、またはパスコード。iPhoneはiOS 18以降 | ブラウザのデータを抜かれても開けない | パスコードを知る人は開ける。Appleアカウントへのサインインが要る |
防げないことは次の3つです。
- ブラウザ自体の脆弱性や改ざん: OSの更新で直すしかありません。ネットにつながないことで、突かれる機会を減らします。
- 配る側が乗っ取られた状態で入れること: 入れる時点のサイトを信頼します。避けたいなら自分のPagesから入れます。
- スクリーンショットと画面収録: ネイティブ版と違い、止められません。
署名エンジンの切り分け
署名エンジンは3つのWASMモジュールに分かれ、それぞれが受け取れるもの、返せるものを最小限にしています。3つとも同じバイト列のまま、ネイティブ版(Android・iOS)とWebアプリの wasmi ホストで動かします。
それぞれのモジュール
| モジュール | 役割 | 作り | していること |
|---|---|---|---|
| parser.wasm | 信頼しない入力を読む。鍵を持たない | C(同じ出力を返すRust版もある)、import 0 | アニメーションQR(UR)をつなぎ合わせてPSBT v0を解析し、6712バイトの決まった形の「計画」にまとめる。署名をPSBTに戻し、URにして返す |
| signer.wasm | 鍵を持つ | C(libsecp256k1)、import 0 | 計画をもう一度確かめて、画面に出す内容を作り、署名する。ニーモニック、SeedQR、BIP32、BIP85、BIP137、xpub、マルチシグも扱う。PSBTそのものは読まない |
| checker.wasm | 追加の照合で、署名を止めることしかできない。鍵を持たない | Lean 4 のモデルをCにしてWASMへ、import 0 | signer.wasmとは別に書いた、署名前の判断のモデル。合計、手数料、署名する入力、出力の持ち主、画面の表示を計算し直し、signer.wasmと1バイトでも違えば署名しない |
parser.wasm と signer.wasm は bitwasm-signer、checker.wasm は btcsigner-spec のモデルから bitwasm-signer-lean でビルドしています。この2つのリポジトリはまだ公開していないため、今は checker.wasm を公開されたソースから作り直したり確かめたりはできません。
読めるものと出せるもの
モジュールのあいだの受け渡しはすべてホスト(Rust)が行い、モジュールには import を1つも渡しません。そのため、どのモジュールも下の表にない情報には触れられず、ネット、ファイル、時計にも触れられません。出力は決まった大きさの領域からホストが読むだけで、読んだあとは0で埋めます。
| parser.wasm | signer.wasm | checker.wasm | 単語入力用 signer.wasm | 画面 | |
|---|---|---|---|---|---|
| シード・秘密鍵 | — | 持つ※1 | — | — | — |
| QRの生データ | 読む | — | — | — | 出す※2 |
| 計画 | 出す | 読む | 読む | — | — |
| 前の取引 | 出す | 読む | 読む | — | — |
| ナンス | 出す | 読む | — | — | — |
| 確認内容・画面の表示 | — | 出す | 読む・出す※3 | — | 読む |
| 署名 | — | 出す | — | — | 読む |
| xpub・ディスクリプタ | — | 出す | — | — | 読む |
| 単語・SeedQR | — | 出す※4 | — | 読む※5 | 読む※4 |
| 乱数 | — | 読む※6 | — | — | — |
※1 作るときと読み込むときに受け取ります。外へは出しません。
※2 カメラで読んで、ホストへ渡します。
※3 signer.wasmの事実を読み、計算し直した結果を出します。
※4 作ったときと、表示を選んだときだけです。
※5 入力中の単語です。返すのは候補とチェックサムの結果だけです。
※6 新しい鍵を作るとき、ホストから受け取ります。
単語入力用の signer.wasm は、同じ signer.wasm を鍵を入れずにもう一つ動かしたものです。単語を打っている間、鍵を持つインスタンスに触れません。
署名にはコーディネーターから届いたナンスを使う(anti-exfil)ので、署名の乱数に鍵を紛れ込ませて持ち出すことはできません。
鍵を消すと、ホストは signer.wasm のメモリを丸ごと0で埋めます。
キーパッドの入力状態はまだ画面側のJavaScriptにあります。エンジンに移すのは今後の作業です。