自動更新,以及為何還需要額外一層簽署

第 1 篇中,我們完成了 Developer ID 憑證和公證的設定。至此,應用程式已具備首次交付給使用者的條件。但應用程式並非發布一次就結束——你需要持續推出修復錯誤、增加功能的新版本。

對於 Mac App Store 應用程式,更新由 App Store 全權處理。直接分發的應用程式則不然,你必須在應用程式內自行整合自動更新功能。在 macOS 上,承擔這一角色的事實標準是開源框架 Sparkle。整合 Sparkle 後,應用程式會定期檢查「更新 Feed (appcast)」,如有新版本則通知使用者並自動下載安裝。

這裡產生了一個疑問:第 1 篇已經用 Developer ID 憑證對應用程式進行了簽署,為什麼還需要另一把金鑰?

原因在於兩種簽署驗證的對象不同:

  • 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"

若輸出一行路徑則表示成功。若無任何輸出,說明跳過了前面「建置一次專案」的步驟,請先在 Xcode 中執行一次建置後再試。

該資料夾內包含以下工具:

  • generate_keys — 產生、備份、還原和驗證簽署金鑰(本文使用)
  • sign_update — 對更新檔案進行簽署(實際發布時使用)
  • generate_appcast — 產生更新 Feed(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>

這一條指令會產生兩個金鑰:

  • 公開金鑰 (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 — 更新 Feed 的位址。該網域名稱目前尚不存在,將在第 3 篇中建立。現在只是先佔個位置。

由於公開金鑰嵌入在應用程式中,而私密金鑰只有開發者持有,因此應用程式只會接受用私密金鑰簽署的更新。這就是 Sparkle 更新驗證的核心結構。

第 3 步 — 備份私密金鑰(務必執行!)

跳過此步驟,日後可能追悔莫及。

私密金鑰存儲在鑰匙圈中,在當前使用的電腦上沒有問題。但如果電腦遺失、磁碟損壞或重新安裝 macOS,金鑰也會隨之消失。

私密金鑰遺失會怎樣?用新金鑰簽署的更新將被現有使用者的應用程式(嵌入了舊公開金鑰的應用程式)拒絕驗證。也就是說,將永遠無法向已在使用應用程式的使用者推送自動更新,只能逐一通知使用者「請手動下載新版本並重新安裝」。

因此,金鑰建立後請立即備份。

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

cat 輸出的單行 base64 字串即為私密金鑰,範例如下:

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=

將此字串以安全備忘錄 (secure note) 的形式儲存到 1Password 等密碼管理工具中。 備忘錄名稱建議使用便於日後查找的名稱,例如 FocusTimer Sparkle EdDSA Private Key

確認儲存完畢後,立即刪除磁碟上的明文檔案。

rm ~/focustimer-sparkle-private.key

原則是不將私密金鑰以明文檔案形式留在磁碟上。備份只保存在加密的密碼管理工具中。

關鍵陷阱 — % 符號不是金鑰的一部分

cat 輸出金鑰時,終端機(尤其是 zsh)可能在行末附加 % 符號

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=%

這個 % 只是 Shell 用於提示「輸出結束時無換行符號」的標記,不是金鑰的一部分。如果將 % 一同複製到備份中,日後還原時金鑰會損壞。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

輸出單行公開金鑰。此值必須與第 2 步中寫入 Info.plistSUPublicEDKey 完全一致。若不一致,說明應用程式中嵌入的公開金鑰與實際簽署金鑰不符,更新驗證將會失敗。

第 2 篇小結

跟到這裡,你現在已經準備好以下內容:

  • ✅ 產生了 Sparkle EdDSA 金鑰對(公開金鑰 + 私密金鑰)
  • ✅ 將公開金鑰嵌入應用程式的 Info.plistSUPublicEDKey
  • ✅ 將私密金鑰安全備份到密碼管理工具
  • ✅ 了解了在其他電腦上還原金鑰的方法

應用程式現在已具備驗證「下載的更新是否真實」的手段。但有一件事仍未完成:第 2 篇中為 SUFeedURL 填寫的 https://updates.example.com/appcast.xml該位址目前還什麼都沒有

下一篇將建立用於存放更新 Feed(appcast.xml)和 .dmg 檔案的公開儲存庫,將其連接到我們自己控制的網域名稱,並完成建置設定,從而結束全部一次性準備工作。