程式碼推送到雲端 Mac 後,CI 通常會立即簽出、建置並測試。問題在於,Git 提交中的作者姓名與電子郵件只是可編輯的文字:它們只能表明該提交聲稱由誰建立,卻無法證明物件確實由該開發者簽署。更可靠的做法是為提交加上 SSH 簽章,再由 CI 使用受控的公開金鑰清單,驗證從合併基準到目前提交的完整範圍。
這道門檻無法取代程式碼審查,也不會判斷程式碼是否安全。它處理的是範圍較窄但同樣重要的問題:進入建置鏈的 Git 物件是否完整、簽章是否有效,以及簽署金鑰是否屬於目前獲准提交程式碼的人員。
先定義驗證真實性的信任邊界
提交驗證至少包含三個層次的判斷:
- Git 物件內容是否與簽章相符。
- 簽章公開金鑰是否列在允許簽署者清單中。
- 該身分在提交發生時是否仍具備提交權限。
第一層由密碼學簽章完成,第二層由儲存庫維護的清單完成,第三層仍仰賴團隊的權限撤銷流程。僅執行 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 版本支援 SSH 簽章。
- 一般提交、合併提交與自動產生的提交都能找到明確的簽署者。
- CI 已取得完整基準,而非依賴固定深度。
- 修改允許簽署者檔案時需要獨立審查。
- 發布標籤及其指向的提交分別通過驗證。
- 機器人身分使用獨立金鑰,不得與個人共用。
- 金鑰輪替、外洩及成員離組都有可執行的處理步驟。
排查失敗原因時,先使用 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,並依團隊所在地從五個實體節點中設定租用方案。