原文在這裡。
由 Russ Cox 釋出於 2023年8月28日
開源軟體的一個關鍵優勢是任何人都可以閱讀原始碼並檢查其功能。然而,大多數軟體,甚至是開源軟體,都以編譯後的二進位制形式下載,這種形式更難以檢查。如果攻擊者想對開源專案進行供應鏈攻擊,最不可見的方式是替換正在提供的二進位制檔案,同時保持原始碼不變。
解決這種型別的攻擊的最佳方法是使開源軟體的構建具有可重現性,這意味著以相同的原始碼開始的每個構建都會產生相同的輸出。這樣,任何人都可以通過從真實原始碼構建並檢查重建的二進位制檔案是否與已釋出的二進位制檔案完全相同來驗證釋出的二進位制檔案是否沒有隱藏的更改。這種方法證明了二進位制檔案沒有後門或原始碼中不存在的其他更改,而無需分解或檢視其中的內容。由於任何人都可以驗證二進位制檔案,因此獨立的團體可以輕鬆檢測並報告供應鏈攻擊。
隨著供應鏈安全的重要性日益增加,可重現構建變得越來越重要,因為它們提供了一種驗證開源專案已釋出的二進位制檔案的簡單方式。
Go 1.21.0 是第一個具有完全可重現構建的 Go 工具鏈。以前的工具鏈也可以重現,但需要付出大量的努力,而且可能沒有人這樣做:他們只是相信在 go.dev/dl 上釋出的二進位制檔案是正確的。現在,「信任但要驗證」變得容易了。
本文解釋了使構建具有可重現性所需的內容,檢查了我們必須對 Go 進行的許多更改,以使 Go 工具鏈具有可重現性,並通過驗證 Go 1.21.0 的 Ubuntu 包的一個好處來演示可重現性之一。
計算機通常是確定性的,因此您可能認為所有構建都將同樣可重現。從某種意義上說,這是正確的。讓我們將某個資訊稱為相關輸入,當構建的輸出取決於該輸入時。如果構建可以重複使用所有相同的相關輸入,那麼構建是可重現的。不幸的是,許多構建工具事實上包含了我們通常不會意識到是相關的輸入,而且可能難以重新建立或提供作為輸入。當輸入事實上是相關的但我們沒有打算讓它成為相關輸入時,讓我們稱之為意外輸入。
構建系統中最常見的意外輸入是當前時間。如果構建將可執行檔案寫入磁碟,檔案系統會將當前時間記錄為可執行檔案的修改時間。如果構建然後使用類似於 「tar」 或 「zip」 之類的工具打包該檔案,那麼修改時間將寫入存檔中。我們當然不希望構建根據當前時間更改,但實際上它確實發生了。因此,當前時間事實上成為構建的意外輸入。更糟糕的是,大多數程式都不允許您將當前時間提供為輸入,因此沒有辦法重複此構建。為了解決這個問題,我們可以將建立的檔案的時間戳設定為 Unix 時間 0 或從構建的某個原始檔中讀取的特定時間。這樣,當前時間不再是構建的相關輸入。
構建的常見相關輸入包括:
要使構建具有可重現性,每個相關輸入都必須在構建中是可設定的,然後必須將二進位制檔案發布在明確列出了每個相關輸入的設定旁邊。如果你已經做到了這一點,那麼你有一個可重現的構建。恭喜!
但我們還沒有完成。如果只有在首先找到具有正確體系結構的計算機,安裝特定作業系統版本,編譯器版本,將原始碼放在正確目錄中,正確設定使用者身份等情況下才能重現這些二進位制檔案,那麼在實踐中這可能是太麻煩了。
我們希望構建不僅具有可重現性,而且易於重現。為此,我們需要識別相關輸入,然後不是僅僅將它們記錄下來,而是消除它們。構建顯然必須依賴於正在構建的原始碼,但其他一切都可以被消除。當構建的唯一相關輸入是其原始碼時,我們可以稱之為完全可重現的。
從 Go 1.21 版本開始,Go 工具鏈具有完全可重現的特性:它的唯一相關輸入是該構建的原始碼。我們可以在支援 Go 的任何主機上構建特定的工具鏈(例如,針對 Linux/x86-64 的 Go),包括在 Linux/x86-64 主機、Windows/ARM64 主機、FreeBSD/386 主機或其他支援 Go 的主機上構建,並且可以使用任何 Go 引導編譯器,包括一直追溯到 Go 1.4 的 C 實現的引導編譯器,還可以改變其他任何細節。但這些都不會改變構建出來的工具鏈。如果我們從相同的工具鏈原始碼開始,我們將得到完全相同的工具鏈二進位制檔案。
這種完全可重現性是自從 Go 1.10 以來努力的巔峰,儘管大部分工作集中在 Go 1.20 和 Go 1.21 中進行。以下是一些最有趣的相關輸入,它們被消除了,從而實現了這種完美的可重現性。
Go 1.10 引入了一個內容感知的構建快取,它根據構建輸入的指紋而不是檔案修改時間來決定目標是否為最新。因為工具鏈本身是這些構建輸入之一,而且 Go 是用 Go 編寫的,所以引導過程只有在單臺機器上的工具鏈構建是可重複的情況下才能收斂。整個工具鏈構建過程如下:
我們首先使用早期版本的 Go 構建當前 Go 工具鏈的原始碼,這個早期版本是引導工具鏈(Go 1.10 使用 Go 1.4,用 C 編寫;Go 1.21 使用 Go 1.17)。這會生成 "toolchain1",然後我們再次使用 "toolchain1" 來構建一切,生成 "toolchain2",接著使用 "toolchain2" 再次構建一切,生成 "toolchain3"。
"toolchain1" 和 "toolchain2" 是從相同的原始碼構建的,但使用了不同的 Go 實現(編譯器和庫),所以它們的二進位制檔案肯定是不同的。然而,如果這兩個 Go 實現都是非有錯誤的、正確的實現,那麼 "toolchain1" 和 "toolchain2" 應該表現完全相同。特別是,當給出 Go 1.X 原始碼時,"toolchain1" 的輸出("toolchain2")和 "toolchain2" 的輸出("toolchain3")應該是相同的,這意味著 "toolchain2" 和 "toolchain3" 應該是相同的。
至少,這是理論上的想法。在實際操作中,要使其成為真實情況,需要消除一些無意的輸入:
在構建系統中,有一些常見的無意的輸入(unintentional inputs)可能導致構建的結果不可重複,這裡介紹了其中兩個主要問題:
隨機性(Randomness):在使用多個 Goroutines 和鎖進行序列化的情況下,例如地圖迭代和並行工作,可能會引入結果生成的順序上的隨機性。這種隨機性會導致工具鏈每次執行時產生幾種不同的可能輸出之一。為了使構建可重複,必須找到這些隨機性,並在用於生成輸出之前對相關專案的列表進行排序。
引導庫(Bootstrap Libraries):編譯器使用的任何庫,如果它可以從多個不同的正確輸出中選擇,可能會在不同的 Go 版本之間更改其輸出。如果該庫的輸出更改導致編譯器輸出更改,那麼 "toolchain1" 和 "toolchain2" 將不會在語意上相同,"toolchain2" 和 "toolchain3" 也不會在位元位上相同。
一個經典的例子是 sort
包,它可以以任何順序放置比較相等的元素。暫存器分配器可能會根據常用變數對其進行排序,連結器會根據大小對資料段中的符號進行排序。為了完全消除排序演演算法的任何影響,使用的比較函數不能將兩個不同的元素報告為相等。在實踐中,要在工具鏈的每次使用 sort
的地方強制執行這種不變性太困難,因此我們安排將 Go 1.X 中的 sort
包複製到呈現給引導編譯器的原始碼樹中。這樣,編譯器在使用引導工具鏈時將使用相同的排序演演算法,就像在使用自身構建時一樣。
另一個我們不得不復制的包是 compress/zlib
,因為連結器會寫入壓縮的偵錯資訊,而對壓縮庫的優化可能會更改精確的輸出。隨著時間的推移,我們還將其他包新增到了這個列表中。這種方法的額外好處是允許 Go 1.X 編譯器立即使用這些包中新增的新 API,但代價是這些包必須編寫以與較舊版本的 Go 相容。
Go 1.20 為易於重現的構建和工具鏈管理做了準備,通過從工具鏈構建中移除兩個相關輸入來解決了更多的問題。
主機 C 工具鏈:一些 Go 包,尤其是 net
包,預設在大多數作業系統上使用 cgo。在某些情況下,比如 macOS 和 Windows,使用 cgo 呼叫系統 DLL 是解析主機名的唯一可靠方法。然而,當我們使用 cgo 時,會呼叫主機的 C 工具鏈(即特定的 C 編譯器和 C 庫),不同的工具鏈具有不同的編譯演演算法和庫程式碼,從而產生不同的輸出。一個使用 cgo 的包的構建圖如下所示:
因此,主機的 C 工具鏈是預編譯的 net.a(與工具鏈一起提供的庫檔案)的相關輸入。在 Go 1.20 中,我們決定通過從工具鏈中刪除 net.a 來解決這個問題。換句話說,Go 1.20 停止提供預編譯的包來填充構建快取。現在,當程式第一次使用 net 包時,Go 工具鏈會使用本地系統的 C 工具鏈進行編譯並快取結果。除了從工具鏈構建中刪除相關輸入和減小工具鏈下載的大小外,不提供預編譯包還使工具鏈下載更加便攜。如果我們在一個系統上使用一個 C 工具鏈構建 net 包,然後在不同的系統上使用不同的 C 工具鏈編譯程式的其他部分,通常不能保證這兩部分可以連結在一起。
最初我們提供預編譯的 net 包的一個原因是允許在沒有安裝 C 工具鏈的系統上構建使用 net 包的程式。如果沒有預編譯的包,那麼在這些系統上會發生什麼呢?答案因作業系統而異,但在所有情況下,我們都安排好了 Go 工具鏈,以便繼續很好地構建純 Go 程式,而無需主機的 C 工具鏈。
在刪除了預編譯包之後,Go 工具鏈中仍然依賴於主機 C 工具鏈的部分是使用 package net 構建的二進位制檔案,特別是 go 命令。有了 macOS 的改進,現在可以使用 cgo 禁用構建這些命令,完全消除了主機 C 工具鏈作為輸入的問題,但我們將這最後一步留給了 Go 1.21。
主機動態連結器:當程式在使用動態連結的 C 庫的系統上使用 cgo 時,生成的二進位制檔案會包含系統的動態連結器路徑,類似於 /lib64/ld-linux-x86-64.so.2。如果路徑錯誤,二進位制檔案將無法執行。通常,每種作業系統/架構組合都有一個正確的路徑。不幸的是,像 Alpine Linux 這樣的基於 musl 的 Linux 和像 Ubuntu 這樣的基於 glibc 的 Linux 使用不同的動態連結器。為了使 Go 在 Alpine Linux 上執行,Go 引導過程如下:
載入程式 cmd/dist 檢查了本地系統的動態連結器,並將該值寫入一個新的原始檔,與其餘連結器原始碼一起編譯,實際上將預設值寫死到連結器本身。然後,當連結器從一組已編譯的包構建程式時,它使用該預設值。結果是,在 Alpine 上構建的 Go 工具鏈與在 Ubuntu 上構建的工具鏈不同:主機設定是工具鏈構建的一個相關輸入。這是一個可重複性問題,但也是一個可移植性問題:在 Alpine 上構建的 Go 工具鏈不會在 Ubuntu 上構建可工作的二進位制檔案,反之亦然。
對於 Go 1.20,我們採取了一步措施來解決可重複性問題,即在執行時更改連結器,以便在執行時諮詢主機設定,而不是在工具鏈構建時寫死預設值:
這解決了在 Alpine Linux 上連結器二進位制檔案的可移植性問題,儘管工具鏈整體上沒有解決,因為 go
命令仍然使用了 package net
,因此也使用了 cgo
,因此在其自身的二進位制檔案中有一個動態連結器參照。就像前一節一樣,編譯 go
命令時禁用 cgo
將解決這個問題,但我們將這個更改留到了 Go 1.21 版本中(我們覺得在 Go 1.20 版本週期內沒有足夠的時間來充分測試這個更改)。
在 Go 1.21 中,完美可復現性的目標在望,我們處理了其餘的,主要是一些小的相關輸入。
Host C toolchain and dynamic linker(主機C工具鏈和動態連結器):在 Go 1.20 中,已經採取了一些重要措施來消除主機C工具鏈和動態連結器作為相關輸入的問題。Go 1.21 則通過禁用cgo來完成了消除這些相關輸入的工作。這提高了工具鏈的可移植性。Go 1.21 是第一個可以在Alpine Linux系統上無需修改就能執行的標準Go工具鏈版本。
去除這些相關的輸入使得可以在不損失功能的情況下從不同系統進行交叉編譯 Go 工具鏈成為可能。這反過來提高了 Go 工具鏈的供應鏈安全性:現在我們可以使用受信任的 Linux/x86-64 系統為所有目標系統構建 Go 工具鏈,而不需要為每個目標系統安排一個單獨的受信任系統。因此,Go 1.21 是首個在 go.dev/dl/ 中釋出適用於所有系統的二進位制檔案的版本。
Source directory(原始碼目錄):Go程式包含了執行時和偵錯後設資料中的完整路徑,以便在程式崩潰或在偵錯程式中執行時,堆疊跟蹤包含原始檔的完整路徑,而不僅僅是檔名。不幸的是,包含完整路徑使原始碼儲存目錄成為構建的相關輸入。為了解決這個問題,Go 1.21 將釋出工具鏈構建更改為使用go install -trimpath
來安裝命令,將源目錄替換為程式碼的模組路徑。這樣,如果釋出的編譯器崩潰,堆疊跟蹤將列印類似cmd/compile/main.go
的路徑,而不是/home/user/go/src/cmd/compile/main.go
。由於完整路徑將參照不同機器上的目錄,這個重寫不會有損失。另外,在非釋出構建中,保留完整路徑,以便在開發人員自身導致編譯器崩潰時,IDE和其他工具可以輕鬆找到正確的原始檔。
Host operating system(主機作業系統):Windows系統上的路徑是用反斜槓分隔的,如 cmd\compile\main.go
。而其他系統使用正斜槓,如 cmd/compile/main.go
。儘管早期版本的Go已經規範化了大多數這些路徑以使用正斜槓,但某種不一致性又重新出現了,導致Windows上的工具鏈構建略有不同。我們找到並修復了這個錯誤。
Host architecture(主機架構):Go可以執行在各種ARM系統上,並且可以使用軟體浮點數庫(SWFP)或使用硬體浮點指令(HWFP)來生成程式碼。預設使用其中一種模式的工具鏈將會有所不同。就像我們之前在動態連結器中看到的那樣,Go引導過程會檢查構建系統,以確保生成的工具鏈在該系統上可以正常工作。出於歷史原因,規則是「假設SWFP,除非構建執行在帶有浮點硬體的ARM系統上」,跨編譯工具鏈會假定為SWFP。如今,絕大多數ARM系統都配備了浮點硬體,因此這引入了本地編譯和跨編譯工具鏈之間不必要的差異,而且進一步複雜的是,Windows ARM構建始終假定為HWFP,使這個決策依賴於作業系統。我們將規則更改為「假設HWFP,除非構建執行在不帶浮點硬體的ARM系統上」。這樣,跨編譯和在現代ARM系統上構建將產生相同的工具鏈。
Packaging logic(打包邏輯):用於建立我們釋出供下載的工具鏈檔案的所有程式碼都儲存在單獨的Git儲存庫中(golang.org/x/build),檔案的確切細節隨時間而變。如果要重現這些檔案,您需要具有該儲存庫的正確版本。我們通過將程式碼移動到Go主原始碼樹中(作為cmd/distpack)來消除了這個相關輸入。截至Go 1.21,如果您擁有特定版本的Go原始碼,那麼您也擁有打包檔案的原始碼。golang.org/x/build儲存庫不再是相關輸入。
User IDs(使用者ID):我們釋出供下載的tar檔案是從寫入檔案系統的分發構建的,並且使用tar.FileInfoHeader將使用者和組ID從檔案系統複製到tar檔案中,使執行構建的使用者成為相關輸入。我們通過修改打包程式碼來清除這些相關輸入。
Current time(當前時間):與使用者ID一樣,我們釋出供下載的tar和zip檔案也是通過將檔案系統修改時間複製到檔案中來構建的,使當前時間成為相關輸入。我們可以清除時間,但我們認為這可能看起來會出人意料,甚至可能會破壞一些工具,因為它使用Unix或MS-DOS的零時間。相反,我們更改了儲存庫中的go/VERSION檔案,以新增與該版本關聯的時間:
$ cat go1.21.0/VERSION
go1.21.0
time 2023-08-04T20:14:06Z
$
現在,打包工具在將檔案寫入存檔時會複製VERSION檔案中的時間,而不是複製本地檔案的修改時間。
Cryptographic signing keys(加密簽名金鑰):macOS上的Go工具鏈除非我們使用獲得蘋果批准的簽名金鑰對二進位制檔案進行簽名,否則不會在終端使用者系統上執行。我們使用一個內部系統來使用Google的簽名金鑰對它們進行簽名,顯然,我們不能分享該祕密金鑰以允許其他人複製已簽名的二進位制檔案。相反,我們編寫了一個驗證器,可以檢查兩個二進位制檔案是否相同,除了它們的簽名。
OS-specific packagers(作業系統特定的打包工具):我們使用Xcode工具的pkgbuild和productbuild來建立可下載的macOS PKG安裝程式,使用WiX來建立可下載的Windows MSI安裝程式。我們不希望驗證器需要完全相同版本的這些工具,所以我們採用了與加密簽名金鑰相同的方法,編寫了一個驗證器,可以檢視軟體包內部並檢查工具鏈檔案是否與預期完全相同。
僅一次性使Go工具鏈可重複是不夠的。我們希望確保它們保持可重複性,也希望確保其他人能夠輕鬆地複製它們。
為了保持自己的誠實,我們現在在受信任的Linux/x86-64系統和Windows/x86-64系統上構建所有Go發行版。除了架構之外,這兩個系統幾乎沒有共同之處。這兩個系統必須生成位對位相同的存檔,否則我們不會繼續釋出。
為了讓其他人驗證我們的誠實,我們編寫並行布了一個驗證器,golang.org/x/build/cmd/gorebuild。該程式將從我們的Git儲存庫中的原始碼開始重新構建當前的Go版本,並檢查它們是否與在 go.dev/dl 上釋出的存檔匹配。大多數存檔必須位對位匹配。如上所述,有三個例外情況,其中使用更寬鬆的檢查:
我們每晚執行gorebuild,並在 go.dev/rebuild 上釋出結果,當然其他任何人也可以執行它。
Go工具鏈的易重現構建應該意味著在go.dev上釋出的工具鏈中的二進位制檔案與其他打包系統中包含的二進位制檔案相匹配,即使這些打包程式是從原始碼構建的。即使打包程式使用了不同的設定或其他更改進行編譯,易於重現的構建仍然應該使複製它們的二進位制檔案變得容易。為了證明這一點,讓我們複製Ubuntu的golang-1.21軟體包版本1.21.0-1,適用於Linux/x86-64。
首先,我們需要下載並提取Ubuntu軟體包,這些軟體包是 ar(1)存檔,包含zstd壓縮的tar存檔:
$ mkdir deb
$ cd deb
$ curl -LO http://mirrors.kernel.org/ubuntu/pool/main/g/golang-1.21/golang-1.21-src_1.21.0-1_all.deb
$ ar xv golang-1.21-src_1.21.0-1_all.deb
x - debian-binary
x - control.tar.zst
x - data.tar.zst
$ unzstd < data.tar.zst | tar xv
...
x ./usr/share/go-1.21/src/archive/tar/common.go
x ./usr/share/go-1.21/src/archive/tar/example_test.go
x ./usr/share/go-1.21/src/archive/tar/format.go
x ./usr/share/go-1.21/src/archive/tar/fuzz_test.go
...
$
那是原始碼存檔。現在是amd64二進位制存檔:
$ rm -f debian-binary *.zst
$ curl -LO http://mirrors.kernel.org/ubuntu/pool/main/g/golang-1.21/golang-1.21-go_1.21.0-1_amd64.deb
$ ar xv golang-1.21-src_1.21.0-1_all.deb
x - debian-binary
x - control.tar.zst
x - data.tar.zst
$ unzstd < data.tar.zst | tar xv | grep -v '/$'
...
x ./usr/lib/go-1.21/bin/go
x ./usr/lib/go-1.21/bin/gofmt
x ./usr/lib/go-1.21/go.env
x ./usr/lib/go-1.21/pkg/tool/linux_amd64/addr2line
x ./usr/lib/go-1.21/pkg/tool/linux_amd64/asm
x ./usr/lib/go-1.21/pkg/tool/linux_amd64/buildid
...
$
Ubuntu將普通的Go樹拆分成兩半,分別位於/usr/share/go-1.21和/usr/lib/go-1.21。讓我們將它們重新組合在一起:
$ mkdir go-ubuntu
$ cp -R usr/share/go-1.21/* usr/lib/go-1.21/* go-ubuntu
cp: cannot overwrite directory go-ubuntu/api with non-directory usr/lib/go-1.21/api
cp: cannot overwrite directory go-ubuntu/misc with non-directory usr/lib/go-1.21/misc
cp: cannot overwrite directory go-ubuntu/pkg/include with non-directory usr/lib/go-1.21/pkg/include
cp: cannot overwrite directory go-ubuntu/src with non-directory usr/lib/go-1.21/src
cp: cannot overwrite directory go-ubuntu/test with non-directory usr/lib/go-1.21/test
$
這些錯誤只是複製符號連結時出現的,我們可以忽略它們。
現在我們需要下載並提取上游的Go原始碼:
$ curl -LO https://go.googlesource.com/go/+archive/refs/tags/go1.21.0.tar.gz
$ mkdir go-clean
$ cd go-clean
$ curl -L https://go.googlesource.com/go/+archive/refs/tags/go1.21.0.tar.gz | tar xzv
...
x src/archive/tar/common.go
x src/archive/tar/example_test.go
x src/archive/tar/format.go
x src/archive/tar/fuzz_test.go
...
$
為了避免一些嘗試和錯誤,結果表明Ubuntu使用 GO386=softfloat
構建Go,這會在為32位元x86編譯時強制使用軟浮點,並剝離(從生成的ELF二進位制檔案中刪除符號表)。現在我們從 GO386=softfloat
構建開始:
$ cd src
$ GOOS=linux GO386=softfloat ./make.bash -distpack
Building Go cmd/dist using /Users/rsc/sdk/go1.17.13. (go1.17.13 darwin/amd64)
Building Go toolchain1 using /Users/rsc/sdk/go1.17.13.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building commands for host, darwin/amd64.
Building packages and commands for target, linux/amd64.
Packaging archives for linux/amd64.
distpack: 818d46ede85682dd go1.21.0.src.tar.gz
distpack: 4fcd8651d084a03d go1.21.0.linux-amd64.tar.gz
distpack: eab8ed80024f444f v0.0.1-go1.21.0.linux-amd64.zip
distpack: 58528cce1848ddf4 v0.0.1-go1.21.0.linux-amd64.mod
distpack: d8da1f27296edea4 v0.0.1-go1.21.0.linux-amd64.info
---
Installed Go for linux/amd64 in /Users/rsc/deb/go-clean
Installed commands in /Users/rsc/deb/go-clean/bin
*** You need to add /Users/rsc/deb/go-clean/bin to your PATH.
$
這將標準包留在了 pkg/distpack/go1.21.0.linux-amd64.tar.gz
中。讓我們解壓它並剝離二進位制檔案以匹配 Ubuntu :
$ cd ../..
$ tar xzvf go-clean/pkg/distpack/go1.21.0.linux-amd64.tar.gz
x go/CONTRIBUTING.md
x go/LICENSE
x go/PATENTS
x go/README.md
x go/SECURITY.md
x go/VERSION
...
$ elfstrip go/bin/* go/pkg/tool/linux_amd64/*
$
現在我們可以比較我們在 Mac 上建立的 Go 工具鏈與 Ubuntu 提供的 Go 工具鏈之間的差異:
$ diff -r go go-ubuntu
Only in go: CONTRIBUTING.md
Only in go: LICENSE
Only in go: PATENTS
Only in go: README.md
Only in go: SECURITY.md
Only in go: codereview.cfg
Only in go: doc
Only in go: lib
Binary files go/misc/chrome/gophertool/gopher.png and go-ubuntu/misc/chrome/gophertool/gopher.png differ
Only in go-ubuntu/pkg/tool/linux_amd64: dist
Only in go-ubuntu/pkg/tool/linux_amd64: distpack
Only in go/src: all.rc
Only in go/src: clean.rc
Only in go/src: make.rc
Only in go/src: run.rc
diff -r go/src/syscall/mksyscall.pl go-ubuntu/src/syscall/mksyscall.pl
1c1
< #!/usr/bin/env perl
---
> #! /usr/bin/perl
...
$
我們成功地複製了Ubuntu軟體包的可執行檔案,並確定了剩下的完整更改集:
mksyscall.pl
和其他七個未顯示的Perl指令碼的頭部已更改。特別注意的是,我們完全按位元重建了工具鏈二進位制檔案:它們根本不顯示在差異中。也就是說,我們證明了Ubuntu的Go二進位制檔案與上游Go原始碼完全對應。
更好的是,我們證明了這一點,完全不使用任何Ubuntu軟體:這些命令在Mac上執行,而unzstd和elfstrip是短小的Go程式。一個複雜的攻擊者可能會通過更改軟體包建立工具來將惡意程式碼插入到Ubuntu軟體包中。如果他們這樣做了,使用這些惡意工具從乾淨的原始碼重新生成Ubuntu軟體包仍將生成與惡意軟體包完全相同的位對位的副本。這種重新構建方式對於這種型別的重新構建來說是不可見的,就像Ken Thompson的編譯器攻擊一樣。不依賴於像主機作業系統、主機體系結構和主機C工具鏈這樣的細節的完美可重複構建是使這種更強的檢查成為可能的原因。
(順便提一下,為了歷史記錄,Ken Thompson曾告訴我,他的攻擊事實上已被檢測到,因為編譯器構建停止變得可重複。它有一個漏洞:在新增到編譯器的後門中的字串常數被不完全處理,並且每次編譯器編譯自身時都會增加一個NUL位元組。最終,有人注意到了不可重複構建,並嘗試通過編譯為組合來找到原因。編譯器的後門在組合輸出中根本沒有複製自己,因此組合該輸出會刪除後門。)
可重複構建是增強開源供應鏈的重要工具。像SLSA這樣的框架關注來源和軟體責任鏈,可以用來指導關於信任的決策。可重複構建通過提供一種驗證信任是否恰當的方法來補充這種方法。
完美可重複性(當原始檔是構建的唯一相關輸入時)僅對能夠自行構建的程式來說是可能的,例如編譯器工具鏈。這是一個崇高但值得追求的目標,因為自我託管的編譯器工具鏈在其他情況下很難驗證。Go的完美可重複性意味著,假設打包工具沒有修改原始碼,那麼任何形式的Go 1.21.0的重新打包(替換為您喜歡的系統)都應該分發完全相同的二進位制檔案,即使它們都是從原始碼構建的。正如我們在這篇文章中所看到的,對於Ubuntu Linux來說並不完全如此,但完美的可重複性仍然讓我們能夠使用非常不同的非Ubuntu系統來複制Ubuntu打包。
理想情況下,以二進位制形式分發的所有開源軟體都應具有易於複製的構建。實際上,正如我們在本文中所看到的,不經意的輸入很容易滲入構建過程。對於不需要cgo的Go程式,可重複構建就像使用CGO_ENABLED=0 go build -trimpath
這樣簡單。禁用cgo會刪除主機C工具鏈作為相關輸入,而-trimpath
會刪除當前目錄。如果您的程式需要cgo,您需要在執行go build
之前為特定的主機C工具鏈版本做安排,比如在特定的虛擬機器器或容器映象中執行構建。
超越Go,可重複構建專案旨在提高所有開源軟體的可重複性,是獲取有關使您自己的軟體構建可重複的更多資訊的良好起點。
宣告:本作品採用署名-非商業性使用-相同方式共用 4.0 國際 (CC BY-NC-SA 4.0)進行許可,使用時請註明出處。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 戀水無意