本文へ移動
ブログに戻る

macOSサンドボックス署名の顛末(v1.4.0からv1.4.5までが存在する理由)

5分で読めます

今日変更履歴をご覧になって、1日に6回のパッチのリリースが並んでいるのに気づいたなら、何が起きたのか気になったかもしれません。手短に言えば、macOSのコード署名とApp Sandboxに、あやうくやられるところでした。以下がその一部始終です。

MailVaultの内側の仕組み

MailVaultはTauri v2のデスクトップアプリです。フロントエンドはネイティブのwebviewで動くReactで、バックエンドはBunでネイティブのバイナリにコンパイルしたNode.jsのExpressのサーバーです。このバイナリ(「サイドカー」と呼びます)は本体のアプリと並んで動き、IMAPとSMTPの通信をすべて担当します。

macOSでの配布にあたり、AppleはアプリにDeveloper IDの証明書での署名と公証を求めます。私たちはさらにApp Sandboxも有効にしています — アプリがシステム上で何にアクセスできるかを制限する、Appleのセキュリティの層です。Mac App Storeでは必須で、どのデスクトップアプリにとっても好ましい習慣です。

問題

v1.4.0を公開したあと、リリースビルドではサイドカーのサーバーが起動していないことが分かりました。アプリ自体は問題なく立ち上がるのに、バックエンドが終了コード133(SIGTRAP)で黙ってクラッシュしていたのです。開発環境では完璧に動いていました — 影響を受けたのは、署名され、サンドボックス化されたリリースビルドだけでした。

層を1枚ずつはがすのに、何度かのやり直しが必要でした。

  • v1.4.0 — サーバーは0.0.0.0にバインドしていたのに、フロントエンドはlocalhostに接続しており、IPv4とIPv6の食い違いが起きていました。127.0.0.1に明示的にバインドして解決しました。
  • v1.4.1 — 「Add Account」のモーダルが、APIの補助関数ではなく生のfetch('/api/test-connection')を使っていました。Tauriのリリースのビルドでは、webviewのオリジンはhttp://localhostではなくtauri://localhostです — そのため相対のURLは違う場所を指してしまいます。
  • v1.4.2 — com.apple.security.network.serverのエンタイトルメントが不足していました。サンドボックスが、サイドカーによるポートの待ち受けを妨げていたのです。
  • v1.4.3 — シェルを使ったプロセスの後始末(lsof、kill)を行っていましたが、これはApp Sandboxの中では禁じられています。TauriのCommandChildのハンドルに置き換えました。
  • v1.4.4 — 真犯人はこれでした。BunでコンパイルしたバイナリにはJITが必要です(実行時のコンパイル)。JITのコンパイラは、実行時に実行可能なメモリを確保します。macOSにはこれを許可するエンタイトルメント(com.apple.security.cs.allow-jit)がありますが、Tauriは--deepを使って、アプリのバンドル内のすべてのバイナリを同じエンタイトルメントで署名します。その結果サイドカーはApp Sandboxのエンタイトルメントを持つことになり、allow-jitの指定があってもJITが妨げられていました。
  • v1.4.5 — 署名のパイプラインを直しました。サイドカーは今、独自のエンタイトルメント(App Sandboxなし、JITは許可)で別に署名されます。本体のアプリはApp Sandboxを保ちます。署名の順序が重要です。まずサイドカー、次に本体のアプリのバンドルを、サイドカーのエンタイトルメントを保つために--deepを付けずに署名します。あわせて、CIのパイプラインが、正しく署名されたDMGではなくTauriが自動生成した未署名のDMGをアップロードしていたことも見つけました。

対処

解決策は2つの部分からなります。

  1. エンタイトルメントを分ける — サイドカーにはentitlements-sidecar.plist(JITとネットワーク、サンドボックスなし)を与えます。本体のアプリにはentitlements.plist(サンドボックス、JIT、ネットワーク)を与えます。
  2. 正しい署名の順序 — まずサイドカーを独自のエンタイトルメントで署名し、次に本体の.appのバンドルを--deepなしで署名します。--deepを使うと、バンドルの中身がすべて再帰的に署名し直され、サイドカーのエンタイトルメントが上書きされてしまいます。

署名を自分で確かめる方法

MailVaultがお使いのマシンで正しく署名され、公証されていることを確かめたい場合は、ターミナルを開いて次のコマンドを実行してください。

1. アプリの署名を確認する:

codesign --verify --verbose /Applications/MailVault.app

次のように表示されるはずです:

/Applications/MailVault.app: valid on disk
/Applications/MailVault.app: satisfies its Designated Requirement

2. メインのアプリのentitlementsを確認する:

codesign -d --entitlements - /Applications/MailVault.app

com.apple.security.app-sandboxがtrueになっているか確認してください — これで、アプリがサンドボックスの中で動いていることが分かります。

3. サイドカーのentitlementsを確認する:

codesign -d --entitlements - /Applications/MailVault.app/Contents/MacOS/mailvault-server

サイドカーはcom.apple.security.cs.allow-jitを持つべきですが、com.apple.security.app-sandboxは持っていないべきです。

4. 公証を確認する:

spctl --assess --type execute -v /Applications/MailVault.app

次のように表示されるはずです:

/Applications/MailVault.app: accepted
source=Notarized Developer ID

5. DMGの公証を確認する(インストール前):

spctl --assess --type open --context context:primary-signature -v ~/Downloads/MailVault-v1.4.5.dmg

学んだこと

  • リリースのビルドを早めに試すこと。 開発のビルドは、コード署名もサンドボックスもまるごと省きます。「開発では動く」と「リリースでも動く」の隔たりは、とても大きくなりえます。
  • サイドカーには特別な扱いが要ること。 BunやNodeやDenoでコンパイルしたバイナリをTauriのアプリの中に同梱するなら、そのバイナリには独自のエンタイトルメントが必要です。Tauriはバイナリごとのエンタイトルメントに標準では対応していません — ビルドのあとに署名し直すことになります。
  • --deepは危険だということ。 便利ではありますが、入れ子になった署名をすべて同じエンタイトルメントで上書きします。バイナリごとに必要なエンタイトルメントが違うなら、個別に署名してください。
  • ローカルとCIでビルドのスクリプトを1つにすること。 ローカルのビルドとCIで署名の処理を分けていたせいで、CIが誤った(未署名の)DMGをアップロードしていました。今はどちらも同じスクリプトを使います。

壊れたリリースの影響を受けた方には、ご不便をおかけしてお詫びします。v1.4.5が、安定していて正しく署名されたリリースです。メールが危険にさらされたことは一度もありません — 問題は、アプリのバイナリがバックエンドのサーバーを起動できないという点だけでした。