自動更新功能的安全性基本 - 糟糕的模式與最佳實踐

· · Windows 開發, 安全性, updater, 自動更新, 簽章, MSIX, ClickOnce

1. 先講結論

相當粗略但在實務上好用地說,是這樣的。

  • 如果需求符合,首先優先考慮 MSIX App Installer 或 ClickOnce 等既有的更新基礎架構
  • 如果需要自製 updater,首先要放進去的不是 UI 而是簽章驗證與失敗時的復原
  • latest.json 之類的更新資訊,不要當成未簽章的設定檔處理,而要當成已簽章的 metadata 來處理
  • TLS 是必要的,但不是充分條件
  • 更新判定不是「因為伺服器這麼說」,而是「因為用戶端可以驗證並判斷是正確的」
  • 簽章金鑰要將開發用與正式用分離,並以 HSM 或簽章服務保護
  • 更新失敗時不要 fail-open 而要 fail-closed
  • 沒有回滾對策的 updater,最好以會被退回到脆弱版本為前提來考慮才安全
  • 如果還沒辦法放入簽章驗證的階段,比起自動更新,手動的已簽章 installer 發佈還比較安全

簡言之,自動更新的核心不是「如何下載」,而是「信任什麼、在何處驗證、壞掉時怎麼還原」。

2. 為什麼自動更新是危險領域

通常的功能是封閉在應用程式中的。 另一方面,updater 則一次具備以下 3 項。

  1. 從外部取得檔案
  2. 信任該檔案
  3. 置換既有的執行檔

也就是說,任意程式碼執行的路徑從一開始就被內建在產品裡

這裡常見的誤解是「因為是 HTTPS 所以安全」。 當然 TLS 是必要的。只是,它守護的主要是通訊路徑與連線對象的正當性。更新伺服器本身遭入侵、錯誤的成品被放到正規 CDN 上、未簽章的 manifest 被替換掉,這些光靠 TLS 是不夠的。

實際上,光是 TUF 整理的威脅,更新系統就有以下幾種。

  • 被植入任意的惡意軟體
  • 被降級回脆弱的舊版 rollback
  • 讓人看不到新版 freeze
  • 混入互相不一致的 metadata 與成品 mix-and-match

也就是說,自動更新不是「檔案傳輸」,而是「信任的發佈」。 設計好這裡之後,自動更新才能安全地運作。

3. 糟糕的模式

先整理實務上常見的危險形態。

糟糕的模式 哪裡危險 最低限度的修正方法
用 HTTPS 取得 version.json,直接執行 URL 的 zip / exe 不耐 origin 入侵、設定替換、錯誤發佈 改成用簽章 metadata 與成品的用戶端驗證
只簽章二進位檔案,manifest 未簽章 URL、version、channel、必須更新旗標可被竄改 做成包含 version / hash / size / channel / expiry 的 signed manifest
把簽章金鑰放在開發 PC 或 CI 的檔案中 遭入侵則會派發具正規簽章的惡意程式 HSM / 簽章服務 + 核准流程 + 稽核日誌
更新失敗時「忽略驗證錯誤繼續執行」 事故時最弱的路徑被打開 改成 fail-closed
不保留舊版直接覆寫更新 停電、磁碟不足、中途失敗導致無法啟動 staging + atomic activate + rollback
只比較版本就允許舊版 允許 rollback 到脆弱版本 單調遞增的 release version 與最高已知版本的保存
整個 updater 以管理者權限執行 入侵時的傷害範圍廣 下載/驗證為低權限,只有置換分離到最小 helper
從差分更新開始 實作複雜且驗證遺漏增加 先從完整套件更新開始

以下稍微詳細看看。

3.1 停在「因為是 HTTPS 所以沒問題」

這是最多的。

  • 啟動時讀取 latest.json
  • 取出 downloadUrl
  • 下載 zip / exe
  • 展開並替換
  • 結束

外觀看起來還像樣,但信任的根過度偏向伺服器回應。 如果更新伺服器或發佈設定遭入侵,就可以在正確的 HTTPS 上發佈不正當的更新。

TLS 是必要的。 但是,只靠 TLS 並不能完成 updater 的設計。

3.2 雖然有簽章,但用戶端沒有驗證

就算發佈時對檔案進行簽章,如果用戶端沒有檢查,也沒有意義。

常見的是:

  • CI 中有簽章
  • 但 updater 只看 hash
  • 而且該 hash 本身是來自未簽章的 manifest

這種形式。

如此一來,manifest 被替換的那一刻,hash 也一起被掉包。 「有看 hash 所以安全」,必須連 hash 的來源都守護才成立。

3.3 manifest 未簽章

在更新系統中真正該守護的,不僅是執行檔本身。 至少以下資訊,若被竄改就很危險。

  • version / release id
  • 下載目標的 URL 或檔名
  • hash / size
  • channel(stable / beta 等)
  • 是否為必須更新
  • 適用的 OS / 架構
  • metadata 的有效期限
  • 最低所需的 updater version

也就是說,用於更新判斷的資訊全部放入 signed metadata 這種感覺剛剛好。

3.4 簽章金鑰的處理太隨便

更新功能的安全性,有相當大的比例就是金鑰管理的安全性。

如果正式簽章金鑰被這樣放置,就相當危險。

  • 一直放在開發 PC 的憑證存放區
  • .pfx 上傳為 CI 的 secret
  • 多人將相同的私鑰分發到本機
  • 開發用簽章與正式用簽章是相同的信任鏈

這樣的話,就算 updater 本身是正確的,也無法阻止「具正規簽章的不正當更新」。

3.5 不保留舊版的覆寫更新

更新,失敗時的設計比成功時更重要。

  • 下載中斷
  • 展開失敗
  • 置換中途停電
  • 新版雖然啟動但初次 migration 失敗

此時,如果舊版已經消失,復原就會很沉重。 實務上,「更新失敗」這個事實比「現場 app 無法啟動」還不嚴重。

3.6 沒考慮 rollback

就算是有簽章的正規版,舊版有漏洞的情況下對攻擊者也可能很方便。

例如:

  • version 1.8 有已知漏洞
  • 現場已升到 2.3
  • 攻擊者重新派發 1.8

如果這個通過了,簽章本身是正確的卻很危險。

不是只看「是否有簽章」,還要看「現在可以安裝那個版本嗎」才夠。

3.7 fail-open

在正式環境最不該做的就是這個。

  • 簽章驗證失敗時只顯示警告就繼續
  • 有能忽略憑證期限錯誤的 hidden flag
  • 除錯用的 skipVerify=true 留在正式環境中

故障時或攻擊時,這種後門會成為主路徑。

4. 最佳實踐

4.1 首先乘坐既有的更新基礎架構

自製 updater 是否真的必要,首先懷疑一下比較安全。

在 Windows 上,只要需求符合,就容易優先檢討以下方案。

  • MSIX + App Installer
  • ClickOnce
  • Store / MDM / 公司內部發佈基礎架構
  • MSI + 企業端的發佈管理

理由很簡單,因為可以把更新本身的責任範圍一定程度交給平台。 當然自由度會降低,但更新 UI、發佈 manifest、套件簽章、與運營的整合會變得容易。

需要自製 updater 的情況,例如以下:

  • 想要嚴密控制 stable / beta / preview 等多個 channel
  • 想要階段性發佈或 rollout 比率
  • 想要因獨自的業務需要細緻控制更新時機
  • 有無法用 MSIX / ClickOnce 的構成

這種情況下,也比起「想要自由度」,理解為「自己承擔更新責任」比較不會搖擺。

4.2 將信任起點放在用戶端

安全的 updater 不會直接相信伺服器的回應。 用戶端至少需要以下 2 項。

  1. 信任的公鑰或憑證鏈
  2. 用該金鑰驗證已簽章 metadata 的機制

簡言之,需要製造這樣的狀態:不是「伺服器說是最新版」,而是「這個 metadata 是信任的簽章者發佈的最新版」,由用戶端確認。

4.3 以 signed metadata 為中心設計

最低限度,更新 metadata 中要放入以下項目並作為簽章對象。

項目 放入的理由
release version / release id rollback 防止、稽核
artifact 名稱、URL、package type 固定要取哪個檔案
hash、size 竄改檢測、損壞派送的檢測
channel stable 不混入 beta
對象 OS / architecture 防止錯誤發佈
minimum updater version protocol 變更時阻止舊 updater
expires_at freeze 對策
published_at 稽核、切分
mandatory / optional 更新 UX 的分歧也變得不可竄改

這裡重要的是,將更新的判斷材料全部集中到 signed metadata 裡。 將邏輯放在用戶端,資訊的真實性以簽章守護的形式,事故會減少。

4.4 也要驗證成品本身

驗證 metadata 之後,對下載的成品也要確認以下項目。

  • size
  • hash
  • 套件簽章 / 程式碼簽章
  • 發行者或期待的識別符

如果處理 Windows 的 PE / MSI / MSIX,最好以用戶端進行 AuthentiCode 或套件簽章驗證為前提才安全。 macOS 的話,最好以 Developer ID 與 notarization 為更新路徑的前提才不會搖擺。

4.5 金鑰以運營而非功能來守護

金鑰管理,差距是由運營而非實作產生的。

至少以下這些要分開比較安全。

  • 開發用簽章金鑰
  • staging 用簽章金鑰
  • 正式用簽章金鑰

此外正式用還要連同以下項目一起設計。

  • HSM
  • 雲端簽章服務
  • 附有核准流程的 signing system
  • 稽核日誌
  • key rotation 流程
  • 附有 timestamp 的簽章

「正式 build 通過 CI 就自動簽章」雖然方便,但入侵時的傷害半徑也會變大。 至少要能追蹤「誰在什麼時候簽章了什麼」。

運營穩定之後,將幾乎不改的 root trust 與頻繁重新簽章的更新 metadata 金鑰分開,會更安全。 將 root 保持在偏 offline 狀態,更新 metadata 使用別的金鑰的設計,能降低金鑰入侵時的傷害半徑。

4.6 fail-closed 與 staged update

更新流程的基本順序如下。

  1. 取得 metadata
  2. 驗證簽章、有效期限、version
  3. 下載成品到 staging 區域
  4. 驗證 hash / size / 簽章
  5. 保留舊版的狀態下準備 activation
  6. 重啟時或用專用 helper 切換
  7. 初次啟動的健全性確認
  8. 有問題則 rollback

這裡重要的是: 驗證結束之前不置換 失敗的話不前進 這兩點。

4.7 限制 updater 的權限

要避免整個 updater 以管理者權限執行。

理想是以下的分離。

  • 下載與驗證:低權限
  • 只有實際檔案置換:具最小權限的 helper
  • helper 不做「將已驗證 package 放到指定位置」以外的事

需要權限提升的設計,如果不在提升前明確分開什麼已驗證,就會變得危險。

4.8 從一開始就消滅 rollback / freeze / mix-and-match

這裡之後才加會很痛苦,所以最好一開始就放入。

  • rollback 對策
    用戶端保持「至今看到的最高 metadata version / release version」,拒絕比這個更舊的

  • freeze 對策
    給 metadata 附加 expiry,拒絕過舊的 metadata

  • mix-and-match 對策
    讓 metadata 之間有整合性。至少在 manifest 本身固定對象 artifact 的 hash / size / version

加上,如果能用 signed metadata 派發特定 build 的 blocklist 或 minimum allowed version,事故時的封鎖就會快。

就算不直接採用 TUF,這 3 個性質都相當重要。

4.9 一開始從全量更新開始

差分更新對頻寬有效,但作為最初的實作很複雜。

  • 從哪個舊版打到哪個新版的差分
  • 差分適用前的前提 hash
  • 差分適用後的最終 hash
  • 中途失敗時的復原
  • 部分適用或舊差分的清理

這些一口氣增加。 初期版本,安全地置換已簽章全量套件 就足夠了。

5. 最小安全構成

就算不像完整 TUF 那樣誇張,自製 updater 的最小安全構成大概如下。

5.1 用戶端持有的

  • 信任的 root 公鑰,或固定的憑證鏈
  • 目前運作中的 version
  • 過去看過的最高 metadata version / release version
  • 許可的 channel
  • rollback 用的前一版

5.2 伺服器回傳的

  • 已簽章的 update metadata
  • 已簽章或 platform 簽章的成品
  • 必要時 blocklist / minimum allowed version 資訊

5.3 典型流程

取得 metadata
  ↓
驗證簽章、expiry、version、channel
  ↓
將成品下載到 staging
  ↓
驗證 size / hash / package signature
  ↓
保留舊版的狀態下 activation
  ↓
初次啟動失敗則 rollback

這裡重要的是,光靠更新伺服器的回應什麼都不成立。 使其成立的,是用戶端持有的 trust anchor 與驗證邏輯。

6. Windows 案件如何思考

Windows 應用程式,先從發佈方式逆推會比較容易整理。

  • 如果需求符合就 MSIX App Installer
  • .NET 的公司內部 app,per-user 符合的話就 ClickOnce
  • 需要服務、driver、shell extension、獨自 channel 控制的話,MSI + 獨自 updater 也是比較對象

但是,就算選擇獨自 updater,該做的事也不會減少。 反而會增加。

  • Authenticode / 套件簽章的驗證
  • signed manifest
  • rollback 對策
  • 更新 helper 的權限分離
  • updater 本身的更新策略

Windows 上常見的危險形態是 DownloadFile -> unzip -> kill process -> overwrite -> restart 的一條直線。 這個雖然會動,但安全性與復原性兩方面都很弱。

讓 SmartScreen 或 UAC 的警告靠「詳細資訊 → 執行」闖過去的運營方式,不是更新設計而是警告馴化。 如果要做正確的更新路徑,應該不是讓人習慣警告,而是靠向不易出現警告的發佈與驗證的構成。

發佈方式的比較本身,在下一篇文章也有整理。
Windows 應用程式的發佈方式如何選擇 - MSI / MSIX / ClickOnce / xcopy / 獨自 updater 的判斷表

7. 最低限度的檢核表

在推出自製 updater 之前,至少要確認以下項目。

  • 更新 metadata 已簽章
  • metadata 中包含 version / hash / size / channel / expiry
  • 用戶端進行簽章與 version 的驗證
  • 驗證成品的 hash 與 platform 簽章
  • 正式簽章金鑰與開發環境分離
  • 保留金鑰的使用日誌與核准紀錄
  • 使用附有 timestamp 的簽章
  • 在 staging 更新中,保留舊版的狀態下切換
  • 有 rollback 的條件與步驟
  • 驗證失敗時以 fail-closed 停止
  • 有 updater 本身的更新方針
  • 可以派發 blocklist / minimum allowed version
  • 有停止階段性發佈的 kill switch
  • 可以觀測失敗率、rollback 率、簽章驗證失敗

如果這份檢核表空缺很多,比起先做 updater 的 UI,先紮實做好發佈信任模型會更有效果。

8. 總結

自動更新功能的安全性,可以相當濃縮地用以下一句話表達。

要設計的不是更新的便利性,
而是信任誰、用戶端如何驗證那份信任。

在這基礎上,實務方面的判斷大略如下。

  • 如果既有基礎架構夠用,就先乘坐它
  • 如果要做自製 updater,HTTPS 之前先放入簽章 metadata 與金鑰管理
  • 不設計失敗時的復原與 rollback 的 updater,在正式環境會很痛苦
  • updater 不是發佈功能,而是產品的安全邊界本身

如果現在的構成接近 latest.json + zip 替換,首先該修的不是下載處理,而是信任的放置方式。 光是修這裡,危險度就會相當改變。

9. 參考資料

相關主題

這是與此主題相近的主題頁面。可從文章為起點前往相關的服務或其他文章。

Windows 技術主題

匯整 Windows 開發、不具合調查、既有資產活用等技術主題的入口。

此主題連接的服務

Windows 應用程式開發

自動更新不只是 UI 的話題,而是包含發佈方式、權限、復原、運營的設計。在 Windows 應用程式的新規開發或既有軟體的檢視中,可從更新方式的整理開始對應。

技術諮詢・設計審查

可從「是否需要獨自 updater」「MSIX / ClickOnce 是否足夠」「現在的更新設計哪裡危險」這類整理階段開始諮詢。

作者簡介

小村 豪

合同會社小村軟體 代表

以 Windows 軟體開發、技術諮詢、不具合調查為中心,在既有資產存留的案件與原因難以看見的故障調查上具備優勢。

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽