開發任務筆記

雲端 Mac CI 的 Git 提交簽章驗證與合併門檻

雲端 Mac CI 的 Git 提交簽章驗證與合併門檻

程式碼推送到雲端 Mac 後,CI 通常會立即簽出、建置並測試。問題在於,Git 提交中的作者姓名與電子郵件只是可編輯的文字:它們只能表明該提交聲稱由誰建立,卻無法證明物件確實由該開發者簽署。更可靠的做法是為提交加上 SSH 簽章,再由 CI 使用受控的公開金鑰清單,驗證從合併基準到目前提交的完整範圍。

這道門檻無法取代程式碼審查,也不會判斷程式碼是否安全。它處理的是範圍較窄但同樣重要的問題:進入建置鏈的 Git 物件是否完整、簽章是否有效,以及簽署金鑰是否屬於目前獲准提交程式碼的人員。

先定義驗證真實性的信任邊界

提交驗證至少包含三個層次的判斷:

  1. Git 物件內容是否與簽章相符。
  2. 簽章公開金鑰是否列在允許簽署者清單中。
  3. 該身分在提交發生時是否仍具備提交權限。

第一層由密碼學簽章完成,第二層由儲存庫維護的清單完成,第三層仍仰賴團隊的權限撤銷流程。僅執行 git log --show-signature 並看到「Good signature」還不夠,因為有效但未經授權的金鑰同樣能產生有效簽章。

電子郵件用於顯示與通知,公開金鑰用於驗證真實性,允許簽署者清單則用於授權。不要將三者視為同一件事。

建議將清單放在僅允許受控變更的獨立目錄中,例如 .ci/trusted_signers。清單本身的修改應要求額外審查,避免提交者在同一項變更中加入自己的公開金鑰,同時放行程式碼。

設定 Git SSH 提交簽章

Git 2.34 及以上版本可直接使用 SSH 金鑰簽署。開發者應先在本機指定簽章格式與公開金鑰路徑:

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgSign true

此處設定的是公開金鑰檔案,實際簽署則由對應的私密金鑰完成。建立提交後,可先確認物件確實包含簽章:

git cat-file commit HEAD | sed -n '/^gpgsig /,/^[^ ]/p'

允許簽署者檔案的每一行都包含身分、選用限制條件與公開金鑰。身分應使用穩定的團隊識別碼,而不是經常變更的顯示名稱:

ci-release namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
ios-team namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5BBBB...

CI 簽出儲存庫後,將 Git 指向這份檔案:

git config --local gpg.format ssh
git config --local gpg.ssh.allowedSignersFile .ci/trusted_signers
git verify-commit HEAD

不要將私密金鑰放入儲存庫。CI 的驗證步驟只需要公開金鑰;只有建立已簽署提交或標籤的環境才需要私密金鑰。

為完整提交範圍建立門檻

只驗證 HEAD 是常見錯誤。惡意或未經授權的變更可能藏在前一個提交中,再由最後一個已簽署提交掩蓋表面狀態。門檻必須逐一檢查可信基準之後的所有提交。

#!/bin/bash
set -euo pipefail

base="${MERGE_BASE_SHA:?missing MERGE_BASE_SHA}"
head="${HEAD_SHA:?missing HEAD_SHA}"

git cat-file -e "${base}^{commit}"
git cat-file -e "${head}^{commit}"

count=0
while IFS= read -r commit; do
  git verify-commit "$commit"
  count=$((count + 1))
done < <(git rev-list --reverse "${base}..${head}")

printf 'verified_commits=%s\n' "$count"

MERGE_BASE_SHA 應由 CI 根據目標分支與待合併分支計算,或透過可靠方式傳入,不能直接寫成 HEAD~1。對於合併提交,還必須確認基準選擇符合團隊政策,否則可能遺漏由第二個父分支帶入的物件。

門檻輸出應記錄提交雜湊值與失敗階段,但不要將完整提交訊息或環境變數複製到公開日誌。失敗時停止建置,比起繼續產生來源未經確認的成品,更容易進行稽核。

處理淺層複製、標籤與金鑰輪替

淺層複製經常導致指令碼誤報:當基準物件不在本機時,rev-list 無法取得完整範圍。應在驗證前補齊目標分支的歷史記錄,並使用 git cat-file -e 明確檢查基準是否存在。找不到基準時,不要降級成只驗證 HEAD。

情境 正確處理方式 不應採用的捷徑
基準不在淺層複製中 補取所需歷史後重新計算範圍 只驗證最後一個提交
發布標籤 使用已簽署的註解標籤並執行 git verify-tag 只檢查標籤名稱
新舊金鑰交接 短期內同時保留兩把公開金鑰 直接取代,導致歷史工作失敗
金鑰外洩 立即移除並重新檢查其簽署範圍 僅修改顯示身分
離開團隊的成員 刪除其允許簽署者記錄 只停用日常登入權限

歷史提交是否應在金鑰移除後繼續通過驗證,需要事先決定。最簡單的嚴格模式是「依目前清單驗證」,適合合併門檻;若需要長期驗證歷史發布版本,則應保存包含有效期間的信任記錄,並將發布標籤、提交雜湊值及當時使用的清單版本一併封存。

上線前檢查與疑難排解

正式阻擋合併前,可以先執行一段觀察期,只記錄失敗情況,但不允許未簽署的發布通過。觀察期間應重點檢查以下項目:

排查失敗原因時,先使用 git verify-commit --raw <hash> 區分「物件沒有簽章」、「簽章損毀」與「公開金鑰不在允許清單中」。接著檢查 gpg.format、gpg.ssh.allowedSignersFile 的儲存庫層級設定,以及清單中的命名空間、金鑰類型與換行。如此便能將身分授權問題與 Git 歷史不完整問題分開,而不是反覆重新簽署同一個提交。

當逐筆提交驗證、標籤驗證及清單變更審查都納入管線後,建置系統便具備一條可供查核的來源鏈:它無法證明程式碼沒有缺陷,但能明確回答哪些物件由哪一把獲准金鑰簽署,以及未經確認的物件為何沒有進入後續建置。

常見問題

提交顯示正確的作者信箱,為什麼仍要驗證簽章?

作者姓名與信箱只是提交物件中的一般文字,建立提交的人可以自行填寫。有效簽章只能證明簽署者持有私鑰,還要透過允許簽署者清單確認該金鑰屬於獲准身分。

CI 只驗證分支最後一個提交就足夠嗎?

不足。可信基準之後的每個提交都會影響最終原始碼,應對完整範圍逐一執行 git verify-commit;發布標籤還要另外執行 git verify-tag。

如何在不中斷建置的情況下輪替簽章金鑰?

先將新公鑰加入受版本控制的允許清單,短期同時接受新舊金鑰,確認新提交可通過驗證後,再移除舊公鑰。

獨享開發環境

為下一項開發任務設定雲端 Mac

選擇 Oak M4、Oak M4 Plus 或 Oak M4 Pro,並依團隊所在地從五個實體節點中設定租用方案。

設定雲端 Mac