鍵の保管

Casa Tookan がリカバリーフレーズと秘密鍵をどこに保管し、誰がそれを解錠でき、署名にどう使われるか。バージョン 2.1.0 以降。

ひとことで言うとリカバリーフレーズと秘密鍵が、アプリの JavaScript に入ることはありません。それらはネイティブ署名機能の中にあり、所有者の Face ID、指紋、端末のパスコード、またはウォレットの PIN でしか取り出せない鍵でスマートフォン上に暗号化されています。そして、固定されレビュー済みの署名コアを実行する隔離されたエンジンの中でのみ使われます。

概要

秘密はすべて署名機能が保持しています。アプリが扱うのは、送金に使えない情報だけです。

何を保管場所読める者
リカバリーフレーズ、秘密鍵封印されたエステートのレコード(キーチェーン / アプリ専用ファイル)所有者の認証後のネイティブ署名機能
それらを封印する鍵(DEK)2重にラップ:OS のロックと、ウォレットの PINFace ID / 指紋 / パスコード後の OS、正しい PIN の後の署名機能
ウォレットの PINどこにもなし。PIN から導出しハードウェアの秘密と混ぜた鍵のみ誰も読めない
PIN 失敗回数カウンターキーチェーン / 専用ファイル、HMAC で保護ネイティブ署名機能
xpub、アドレス、エステート一覧署名機能のメタデータ + アプリのストレージロック解除されたスマートフォンを持つ人なら誰でも。ただし送金には使えない
Casa の名前、色、通貨アプリのストレージ(AsyncStorage)アプリ

3つの層

アプリは署名を依頼することしかできず、署名するのは署名機能です。それも、署名される正確なバイト列からコアがデコードした内容を、所有者が署名機能自身の画面で確認した後に限られます。

アプリ(React Native / Hermes)

鍵を預けるに足りない
  • 残高を表示し、未署名のトランザクションを組み立てます。
  • すべてのアドレスを、署名機能が公開する xpub から導出します。
sign(request)署名のみ

ネイティブ署名機能(Swift / Kotlin)

信頼済み
  • カストディ:DEK、封印されたレコード、PIN ラップ、試行回数カウンター。
  • 認証:Face ID / 指紋 / 端末のパスコード、または独自の PIN パッド。
  • 独自の確認画面。スクリーンショットと録画はブロックされます。
操作ごとに新しいコンテキスト

署名コア(signer-core.js)

隔離されたエンジン
  • iOS:専用の JavaScriptCore VM · Android:JavaScriptSandbox(QuickJS にフォールバック)。
  • noble / scure 2.2.0 のみ。ネットワークなし、タイマーなし、require なし。
  • その SHA-256 はネイティブバイナリに組み込まれており、それ以外のバイト列は拒否されます。

カストディモデル

ひとつのランダムなデータ鍵が、すべてのエステートを封印します。その鍵は2重にラップされているので、入口をひとつ失ってもウォレットを失うことはありません。

1 · ひとつのデータ鍵と、封印されたレコード

セットアップ時に、署名機能は32バイトのランダムなデータ暗号化鍵(DEK)を生成します。各エステート(リカバリーフレーズ、またはインポートした鍵)は、DEK のもとで AES-256-GCM により封印されたレコードになります。認証付きデータがレコードをそのエステートに結び付けるため、別のエステート id に移されたレコードは復号できなくなります。

record = AES-256-GCM( DEK, iv, recordJSON,
                      aad = "casa-signer:v1:estate:" + estateId )

レコードには、フレーズとその事前計算済みシード(JIT のないエンジンでは BIP-39 のストレッチングが遅すぎるため)、またはインポートした鍵が入っています。

2 · DEK は2重にラップされる

普段

dek.os:OS のロック

  • iOS:アクセス制御 .userPresence、アクセス可能性 WhenPasscodeSetThisDeviceOnly のキーチェーン項目。パスコードが削除されると iOS がこれを削除します。
  • Android:認証(強力な生体認証または端末の認証情報)を必要とする AndroidKeyStore の RSA-2048 鍵(OAEP / SHA-256)のもとの DEK。利用可能なら StrongBox。指紋の追加では無効化されず、画面ロックが削除されると無効化されます。API 30 以降は使用ごとにプロンプトと結び付き、API 26–29 では認証後10秒間使用できます。
フォールバック

dek.pin:PIN + ハードウェア

  • スマートフォンに画面ロックがない場合や、OS ラップを失った場合に使われます。
  • iOS:ハードウェア部分は、チップの外に出ることのない Secure Enclave 鍵による ECDH の秘密です(Secure Enclave がない場合はキーチェーンのペッパー)。
  • Android:PIN でラップされた DEK を、エクスポート不可能な Keystore の AES 鍵でさらに封印します。
  • コピーされたファイルは、スマートフォンの外では総当たりできません。ハードウェア部分がないからです。
KEK = HKDF-SHA256( PBKDF2-SHA256(PIN, salt, 600 000) ‖ hw,
                   info = "casa-signer:v1:pin-kek" )
なぜ2重のラップ?OS ラップだけだと、画面ロックを外した時点でウォレットが壊れてしまいます。PIN ラップをフォールバックにしておけば、ウォレットは PIN で開き続け、ロックを戻した後に 設定 › 画面ロックで再び保護 で DEK をラップし直せます。

3 · PIN の試行

PIN は保存されません。誤入力はネイティブで数えられます。最初の5回までは自由で、その後は失敗のたびに PIN が30秒ロックされ、最大1時間まで倍増します。カウンターは端末専用の鍵で HMAC 保護されているため、アプリの JavaScript からも、復元したバックアップからもリセットできません。

4 · コミットポイント

セットアップのたびに、封印されたレコード、続いて両方のラップを書き込み、すべてを読み戻してバイト単位で比較します。その後でようやく、ウォレットの公開情報である meta を書き込みます。meta がなければここには何もないという意味です。中断されたセットアップの残骸は、次回起動時に消去されます。

データの保存場所

iOSAndroid
レコード、ラップ、カウンター、metaキーチェーン、クラス Internet password、サーバー casa-signer.local、ThisDeviceOnlynoBackupFilesDir/casa-signer/ 内のファイル
ハードウェア鍵Secure Enclave(PIN ラップ)、キーチェーンのアクセス制御(OS ラップ)AndroidKeyStore、あれば StrongBox
バックアップ、ほかの端末一切なし(ThisDeviceOnly)一切なし(バックアップ対象外のディレクトリ、ハードウェアに結び付いた鍵)

アプリの JavaScript には、キーチェーンやキーストアのモジュールが一切ありません。iOS では、署名機能の項目は、汎用パスワードのストア(React Native のモジュールが使う種類)からはアクセスできないキーチェーンのクラスを使っています。

アプリが持つもの

エステートごとに、署名機能は公開データだけを公開します。アプリはそこからすべてのアドレスを導出します(src/signer/publicKeys.ts)。残高を表示しトランザクションを組み立てることはできても、送金はできません。

アカウントの xpub

  • Bitcoin レガシー m/0'/0
  • Bitcoin segwit m/84'/0'/0'/0
  • Bitcoin taproot m/86'/0'/0'/0
  • EVM m/44'/60'/0'/0
  • 各 Casa は非強化の子です:…/hd。

そのほか

  • インポートした鍵の公開鍵。
  • 同じフレーズや鍵の2重追加を拒否するためのフィンガープリント。
  • ガードテスト(forbidden.test.ts)は、アプリのコードが BIP-39 をインポートしたり、シードから導出したり、秘密鍵に触れたり、トランザクションに署名したり、秘密のストアを開いたりすると、ビルドを失敗させます。

署名の流れ

  1. アプリが未署名のリクエストを組み立てるPSBT、nonce・ガス・手数料が入力済みの EVM トランザクション、メッセージ、型付きデータ。
  2. コアが表示内容を準備する新しいコンテキストで、prepare が正確なバイト列をデコードします:金額、受取人、お釣り、手数料、Counterparty のメッセージ、トークンの承認、署名するアドレス。アプリが渡すコンテキスト(サイト名、トークンのシンボル、Casa の名前)には未検証のラベルが付きます。
  3. 所有者が確認して認証する署名機能自身の画面で長押しし、続いて OS のロックまたは PIN で。
  4. コアが署名する、または拒否するDEK のラップが解かれ、レコードが復号されます。sign はリクエストを再度デコードし、署名しようとする内容が表示した内容と異なれば拒否します(E_DIGEST_MISMATCH)。
  5. 結果だけが返される署名、または署名済みトランザクション。秘密と DEK はコンテキストとともに破棄されます。

ひとつの画面とひとつの認証で、まとめて処理できます(複数の PSBT、マルチチェーンのログイン)。リクエストは一度にひとつ、10秒間に最大3件までです。

秘密の表示

設定 › 表示 は、署名機能に所有者の認証を求め、フレーズや Casa の鍵を署名機能自身の画面に表示させます。スクリーンショットと画面録画はブロックされ、60秒後に自動で隠れます。秘密がアプリに返されることはありません。

移行

どちらもネイティブで実行されます。フレーズがアプリの JavaScript を通ることはありません。コミット前に中断された移行は、単に最初からやり直されます。古い PIN を忘れた場合は、そのウォレットを消去してリカバリーフレーズから復元します。

ビルド ≤ 44

JS ボールト(expo-secure-store)

署名機能は古いボールトを読み、独自のパッドで古い PIN を尋ね(以前の試行回数は引き継ぎ)、メインと追加のエステートを復号し、新しい DEK のもとで封印し、検証し、コミットします。その後、古い項目をすべて削除します。

Titanium ≤ 1.2.0

初代アプリ

署名機能は古いキーチェーン / キーストアのレコードを読み、古い4桁の PIN を確認し(回数制限あり)、新しい PIN を尋ね、封印し、検証し、コミットし、古いレコードを削除します。Casa の名前と通貨は秘密の情報ではないため、別に読み込まれます。

何から守るのか

脅威結果
アプリ内の npm 依存パッケージの侵害秘密は一切読めません。できるのは署名を依頼することだけで、その都度ネイティブで表示・確認されます。対策済み
誤解を招く内容への署名を求める dApp署名機能はバイト列が実際に何をするかを表示します。無制限の承認、permit、部分署名には警告が付きます。対策済み
ロックされたスマートフォンの盗難レコードには OS のロック、または PIN とスマートフォンのハードウェアが必要です。PIN の推測は回数制限されます。対策済み
バックアップやコピーされたファイルバックアップには何も含まれず、コピーされたファイルにはハードウェア鍵がありません。対策済み
画面ロックの解除ウォレットは PIN で開き続けます。対策済み
端末のパスコードを知っていて、ロック解除されたスマートフォンを持つ人署名できます。Apple Pay と同じく、端末のパスコードが認証だからです。設計どおり
侵害された OS(脱獄、root 化)対象外です。対策なし
リカバリーフレーズなしでのスマートフォンの紛失資産は失われます。鍵はそのスマートフォンにしか存在しません。対策なし

開発者向け

ソース

  • コア:signer-core/src/ → npm run signer:build。signer:check はテストの前に実行され、バンドルがずれていれば失敗します。
  • カストディ:modules/casa-signer/ios/Custody.swift、…/android/…/Custody.kt。

テスト

  • スマートフォンで:Dev Tools › Signer self-test。
  • npm run signer:native-test は macOS 上でカストディとコアを実行します。
  • Jest は modules/casa-signer/mock.ts を使います。Node VM 内の実際のバンドル済みコアと、メモリ上のカストディです。アプリには含まれません。