自動アップデート、そしてもう一層の署名が必要な理由

1 編では、Developer ID 証明書と公証の設定を完了しました。これでアプリをユーザーに初めて届ける準備は整いました。しかし、アプリは一度リリースして終わりではなく、バグを修正したり機能を追加した新しいバージョンを継続してリリースし続ける必要があります。

Mac App Store のアプリであれば、アップデートは App Store が処理してくれます。直接配布のアプリにはその仕組みがないため、アプリ自体に自動アップデート機能を組み込む必要があります。macOS でこの役割の事実上の標準は、オープンソースフレームワーク Sparkle です。Sparkle を組み込むと、アプリが定期的に「アップデートフィード (appcast)」を確認し、新しいバージョンがあればユーザーに通知してダウンロード・インストールしてくれます。

ここで一つの疑問が生じます。1 編で作った Developer ID 証明書でアプリに既に署名しているのに、なぜもう一つのキーが必要なのでしょうか?

理由は、2 つの署名が検証する対象が異なるからです。

  • Developer ID 証明書 — macOS Gatekeeper が「このアプリをインストールしても良いか」を判断する際に使用
  • Sparkle EdDSA キー — アプリ内の Sparkle が「今ダウンロードしたアップデートファイルが本当にこのアプリの開発者が作ったものか」を判断する際に使用

自動アップデートは、インターネットからファイルをダウンロードして自分自身を上書きする、セキュリティ上とても重要な操作です。もし誰かがアップデートサーバーや通信経路を傍受して偽のファイルを混入させたら、重大な問題になります。Sparkle はこれを防ぐため、開発者だけが持つ秘密鍵で署名されたアップデートのみを受け入れ、署名が一致しなければインストールを拒否します。証明書とは別の検証レイヤーといえます。

この記事では、その検証に使われる EdDSA (Ed25519) キーペアを作成します。

1 編と同様、すべての名前・パス (FocusTimerexample.com など) はサンプル値です。実際には、ご自身のアプリ情報に置き換えて使用してください。

前提 — アプリに Sparkle が追加済みであること

キーを作成する前に、アプリプロジェクトに Sparkle フレームワークが依存関係として追加されている必要があります。まだであれば、Xcode で Swift Package Manager (SPM) を使って追加します。

  1. Xcode でプロジェクトを開く → File → Add Package Dependencies…
  2. 検索ボックスにリポジトリのアドレスを入力: https://github.com/sparkle-project/Sparkle
  3. バージョンルールを 2.x (最新のメジャー) に設定して追加

これを行った後、プロジェクトを一度ビルドすると SPM が Sparkle パッケージをダウンロードします。このとき一緒に入ってくるコマンドラインツールが次のステップの鍵となります。

ステップ 1 — Sparkle コマンドラインツールを見つける

Sparkle パッケージには、キーの生成・署名に使うコマンドラインツールが含まれています。これらのツールは SPM がパッケージをダウンロードしたフォルダ内にありますが、その場所は Xcode のバージョンや DerivedData の設定によって異なります。そのため、直接探すのが確実です。

SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData \
  -path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1)
echo "$SPARKLE_BIN"

パスが 1 行出力されれば成功です。何も表示されない場合は、前のステップの「プロジェクトを一度ビルド」が抜けているので、Xcode でビルドを一度実行してから再試行してください。

このフォルダには次のツールがあります。

  • generate_keys — 署名キーの生成・バックアップ・復元・確認 (この記事で使用)
  • sign_update — アップデートファイルへの署名 (実際のリリース時に使用)
  • generate_appcast — アップデートフィード (appcast.xml) の生成 (3 編で登場)

ステップ 2 — 署名キーの生成

キーペアを作成します。

"$SPARKLE_BIN/generate_keys"

次のような出力が表示されます。

Generating a new signing key...
A key has been generated and saved in your keychain. Add the SUPublicEDKey
key to the Info.plist of each app for which you intend to use Sparkle...

    <key>SUPublicEDKey</key>
    <string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>

このコマンド一つで、キーが 2 つ作成されます。

  • 公開鍵 (public key) — 上記の出力に表示されている SUPublicEDKey の値。秘密ではなく、アプリに埋め込むキーです。
  • 秘密鍵 (private key) — 画面には表示されません。「Private key for signing Sparkle updates」という項目名で macOS キーチェーン自動的に保存されます。絶対にプレーンテキストファイルとしてディスクに残してはいけない、本当の秘密です。

公開鍵をアプリに埋め込む

出力された公開鍵の文字列をアプリの Info.plist に追加します。サンプルアプリを基準に、FocusTimer-Info.plist に次のキーを追加します。

<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
  • SUPublicEDKey — 先ほど生成した公開鍵。アプリはこのキーを使ってダウンロードしたアップデートの署名を検証します。
  • SUFeedURL — アップデートフィードのアドレス。このドメインはまだ存在しません。3 編で作成します。今は場所だけ確保しておく形です。

公開鍵がアプリに埋め込まれており、秘密鍵は開発者だけが持っているため、秘密鍵で署名されたアップデートのみがそのアプリに受け入れられます。これが Sparkle のアップデート検証の核心的な仕組みです。

ステップ 3 — 秘密鍵のバックアップ (必須!)

このステップをスキップすると、後で大きく後悔することになりかねません。

秘密鍵はキーチェーンに保存されているため、今使っているコンピューターでは問題ありません。しかしコンピューターを紛失したり、ディスクが壊れたり、macOS を再インストールしたりすると、このキーも一緒に失われます。

秘密鍵が失われるとどうなるでしょうか?新しいキーで署名したアップデートは、既存ユーザーのアプリ (古い公開鍵が埋め込まれたアプリ) に検証を拒否されます。つまり、すでにアプリを使っているユーザーに自動アップデートを永遠に届けられなくなります。 ユーザー一人ひとりに「新しいバージョンを直接ダウンロードして再インストールしてください」と案内するしかなくなります。

ですから、キーを作成した直後にバックアップを取ります。

"$SPARKLE_BIN/generate_keys" -x ~/focustimer-sparkle-private.key
cat ~/focustimer-sparkle-private.key

cat が出力した 1 行の base64 文字列が秘密鍵です。例:

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=

この文字列を 1Password などのパスワードマネージャーにセキュアノート (secure note) として保存します。メモの名前は後で見つけやすいように FocusTimer Sparkle EdDSA Private Key のようにしておきます。

保存を確認した直後、ディスクに残ったプレーンテキストファイルを削除します。

rm ~/focustimer-sparkle-private.key

秘密鍵をプレーンテキストファイルとしてディスクに残さないことが原則です。バックアップは暗号化されたパスワードマネージャーの中だけに保管してください。

重要な落とし穴 — % 記号はキーの一部ではない

cat でキーを出力する際、ターミナル (特に zsh) が行末に % 記号を付けて表示することがあります。

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=%

この % は「改行なしで出力が終わった」ことを示すシェルの表示に過ぎず、キーの一部ではありません。 バックアップにこの % まで含めてコピーしてしまうと、後で復元するときにキーが壊れます。base64 文字列は通常 = で終わるため、= の後の % は除いて保存してください。

ステップ 4 — 別のコンピューターで復元する

新しいコンピューターでアプリをビルドする必要があるとき、バックアップしておいた秘密鍵をキーチェーンに再登録します。

echo "バックアップしたbase64文字列" > ~/focustimer-sparkle-private.key
"$SPARKLE_BIN/generate_keys" -f ~/focustimer-sparkle-private.key
rm ~/focustimer-sparkle-private.key

-f オプションは「ファイル内のキーをキーチェーンにインポートする」という意味です。復元が終わったら、こちらでもプレーンテキストファイルをすぐに削除します。

ステップ 5 — 確認

キーが正しく保存されているか確認します。

"$SPARKLE_BIN/generate_keys" -p

公開鍵の 1 行が出力されます。この値が、ステップ 2 で Info.plistSUPublicEDKey に設定した値と完全に一致している必要があります。異なる場合は、アプリに埋め込まれた公開鍵と実際の署名キーがずれており、アップデートの検証に失敗することになります。

2 編まとめ

ここまで進んだ方は、以下が揃っているはずです。

  • ✅ Sparkle EdDSA キーペアの生成 (公開鍵 + 秘密鍵)
  • ✅ 公開鍵をアプリの Info.plist (SUPublicEDKey) に埋め込み
  • ✅ 秘密鍵をパスワードマネージャーに安全にバックアップ
  • ✅ 別のコンピューターでの復元方法を把握

これでアプリは「ダウンロードしたアップデートが本物かどうか」を検証する手段を持ちました。しかし、一つ欠けているものがあります。2 編で SUFeedURLhttps://updates.example.com/appcast.xml を設定しましたが、そのアドレスにはまだ何もありません。

次の編では、アップデートフィード (appcast.xml) と .dmg ファイルを置く公開リポジトリを作成し、コントロールできるドメインに接続し、ビルド設定を完了させて事前設定を終わらせます。