完全可複製、經過驗證的 Go 工具鏈

来源:https://www.cnblogs.com/lianshuiwuyi/archive/2023/09/05/17680270.html
-Advertisement-
Play Games

原文在[這裡](https://go.dev/blog/rebuild)。 > 由 Russ Cox 發佈於 2023年8月28日 開源軟體的一個關鍵優勢是任何人都可以閱讀源代碼並檢查其功能。然而,大多數軟體,甚至是開源軟體,都以編譯後的二進位形式下載,這種形式更難以檢查。如果攻擊者想對開源項目進行 ...


原文在這裡

由 Russ Cox 發佈於 2023年8月28日

開源軟體的一個關鍵優勢是任何人都可以閱讀源代碼並檢查其功能。然而,大多數軟體,甚至是開源軟體,都以編譯後的二進位形式下載,這種形式更難以檢查。如果攻擊者想對開源項目進行供應鏈攻擊,最不可見的方式是替換正在提供的二進位文件,同時保持源代碼不變。

解決這種類型的攻擊的最佳方法是使開源軟體的構建具有可重現性,這意味著以相同的源代碼開始的每個構建都會產生相同的輸出。這樣,任何人都可以通過從真實源代碼構建並檢查重建的二進位文件是否與已發佈的二進位文件完全相同來驗證發佈的二進位文件是否沒有隱藏的更改。這種方法證明瞭二進位文件沒有後門或源代碼中不存在的其他更改,而無需分解或查看其中的內容。由於任何人都可以驗證二進位文件,因此獨立的團體可以輕鬆檢測並報告供應鏈攻擊。

隨著供應鏈安全的重要性日益增加,可重現構建變得越來越重要,因為它們提供了一種驗證開源項目已發佈的二進位文件的簡單方式。

Go 1.21.0 是第一個具有完全可重現構建的 Go 工具鏈。以前的工具鏈也可以重現,但需要付出大量的努力,而且可能沒有人這樣做:他們只是相信在 go.dev/dl 上發佈的二進位文件是正確的。現在,“信任但要驗證”變得容易了。

本文解釋了使構建具有可重現性所需的內容,檢查了我們必須對 Go 進行的許多更改,以使 Go 工具鏈具有可重現性,並通過驗證 Go 1.21.0 的 Ubuntu 包的一個好處來演示可重現性之一。

使構建具有可重現性

電腦通常是確定性的,因此您可能認為所有構建都將同樣可重現。從某種意義上說,這是正確的。讓我們將某個信息稱為相關輸入,當構建的輸出取決於該輸入時。如果構建可以重覆使用所有相同的相關輸入,那麼構建是可重現的。不幸的是,許多構建工具事實上包含了我們通常不會意識到是相關的輸入,而且可能難以重新創建或提供作為輸入。當輸入事實上是相關的但我們沒有打算讓它成為相關輸入時,讓我們稱之為意外輸入。

構建系統中最常見的意外輸入是當前時間。如果構建將可執行文件寫入磁碟,文件系統會將當前時間記錄為可執行文件的修改時間。如果構建然後使用類似於 “tar” 或 “zip” 之類的工具打包該文件,那麼修改時間將寫入存檔中。我們當然不希望構建根據當前時間更改,但實際上它確實發生了。因此,當前時間事實上成為構建的意外輸入。更糟糕的是,大多數程式都不允許您將當前時間提供為輸入,因此沒有辦法重覆此構建。為瞭解決這個問題,我們可以將創建的文件的時間戳設置為 Unix 時間 0 或從構建的某個源文件中讀取的特定時間。這樣,當前時間不再是構建的相關輸入。

構建的常見相關輸入包括:

  • 要構建的源代碼的特定版本;
  • 將包括在構建中的依賴項的特定版本;
  • 運行構建的操作系統,這可能會影響生成的二進位文件中的路徑名;
  • 構建系統上運行的CPU架構,這可能會影響編譯器使用的優化或某些數據結構的佈局;
  • 正在使用的編譯器版本以及傳遞給它的編譯器選項,這會影響代碼的編譯方式;
  • 包含源代碼的目錄的名稱,這可能會出現在調試信息中;
  • 運行構建的帳戶的用戶名、組名、uid和gid,這可能會出現在存檔中的文件元數據中;
  • 還有許多其他因素。

要使構建具有可重現性,每個相關輸入都必須在構建中是可配置的,然後必須將二進位文件發佈在明確列出了每個相關輸入的配置旁邊。如果你已經做到了這一點,那麼你有一個可重現的構建。恭喜!

但我們還沒有完成。如果只有在首先找到具有正確體繫結構的電腦,安裝特定操作系統版本,編譯器版本,將源代碼放在正確目錄中,正確設置用戶身份等情況下才能重現這些二進位文件,那麼在實踐中這可能是太麻煩了。

我們希望構建不僅具有可重現性,而且易於重現。為此,我們需要識別相關輸入,然後不是僅僅將它們記錄下來,而是消除它們。構建顯然必須依賴於正在構建的源代碼,但其他一切都可以被消除。當構建的唯一相關輸入是其源代碼時,我們可以稱之為完全可重現的

完全可重現的 Go 構建

從 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 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 中的可重現性

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 工具鏈。

  • 在 macOS 上,我們重寫了 package net,使用了 cgo 使用的底層機制,而沒有實際的 C 代碼。這樣可以避免調用主機的 C 工具鏈,但仍然生成一個引用所需系統 DLLs 的二進位文件。這種方法之所以可行,是因為每台 Mac 都安裝了相同的動態庫。使非 cgo macOS 版本的 package net 使用系統 DLLs 也意味著交叉編譯的 macOS 可執行文件現在使用系統 DLLs 進行網路訪問,解決了一個長期存在的功能請求。
  • 在 Windows 上,package net 已經直接使用 DLLs 而沒有 C 代碼,因此不需要進行任何更改。
  • 在 Unix 系統上,我們不能假定網路代碼的特定 DLL 介面,但純 Go 版本對於使用典型 IP 和 DNS 設置的系統來說效果很好。此外,在 Unix 系統上安裝 C 工具鏈要容易得多,而在 macOS 和尤其是 Windows 上則要困難得多。我們更改了 go 命令,根據系統是否安裝了 C 工具鏈,自動啟用或禁用 cgo。沒有 C 工具鏈的 Unix 系統將退回到 package net 的純 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 中的復現性

在 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工具鏈

僅一次性使Go工具鏈可重覆是不夠的。我們希望確保它們保持可重覆性,也希望確保其他人能夠輕鬆地複製它們。

為了保持自己的誠實,我們現在在受信任的Linux/x86-64系統和Windows/x86-64系統上構建所有Go發行版。除了架構之外,這兩個系統幾乎沒有共同之處。這兩個系統必鬚生成位對位相同的存檔,否則我們不會繼續發佈。

為了讓其他人驗證我們的誠實,我們編寫併發布了一個驗證器,golang.org/x/build/cmd/gorebuild。該程式將從我們的Git存儲庫中的源代碼開始重新構建當前的Go版本,並檢查它們是否與在 go.dev/dl 上發佈的存檔匹配。大多數存檔必須位對位匹配。如上所述,有三個例外情況,其中使用更寬鬆的檢查:

  • macOS tar.gz文件預計會有所不同,但然後驗證器會比較內部內容。重新構建和發佈的副本必須包含相同的文件,並且所有文件必須完全匹配,除了可執行二進位文件。在剝離代碼簽名後,可執行二進位文件必須完全匹配。
  • macOS PKG安裝程式不會被重新構建。相反,驗證器會讀取PKG安裝程式內部的文件並檢查它們是否與macOS tar.gz完全匹配,同樣是在剝離代碼簽名後。從長遠來看,PKG創建足夠簡單,可以潛在地添加到cmd/distpack,但驗證器仍然必須解析PKG文件以運行忽略簽名的代碼可執行文件比較。
  • Windows MSI安裝程式不會被重新構建。相反,驗證器會調用Linux程式msiextract來提取內部文件,並檢查它們是否與重新構建的Windows zip文件完全匹配。從長遠來看,可能可以將MSI創建添加到cmd/distpack,然後驗證器可以使用位對位的MSI比較。

我們每晚運行gorebuild,併在 go.dev/rebuild 上發佈結果,當然其他任何人也可以運行它。

驗證Ubuntu的Go工具鏈

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軟體包的可執行文件,並確定了剩下的完整更改集:

  • 刪除了各種元數據和支持文件。
  • 修改了 gopher.png 文件。仔細檢查後,這兩個文件是相同的,唯一的區別是嵌入的時間戳,Ubuntu 已經更新了它。也許 Ubuntu 的打包腳本使用了重新壓縮 png 的工具,即使在不能改善現有壓縮的情況下,也會重新寫入時間戳。
  • 二進位文件 dist 和 distpack 是在引導過程中構建的,但未包含在標準存檔中,但包含在 Ubuntu 軟體包中。
  • Plan 9構建腳本(*.rc)已被刪除,儘管Windows構建腳本(*.bat)仍然存在。
  • 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: 戀水無意



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 一、瞭解天地圖 http://lbs.tianditu.gov.cn/api/js4.0/examples.html 在其中可以瞭解天地圖的基本使用教程 但其中的教程均為h5引入cdn的方式 以h5定位為例來改成vue項目 源碼: ```html5 天地圖-地圖API-範例-H5定位 本示例演示 ...
  • 小白對於將 unix 時間戳轉換為日期時間和使用日期時間轉換為 unix 時間戳,在項目中見到過很多,每次使用時不是用現有的方法轉換就是網上搜索方法。 小白見過各種轉換方式覺得moment庫很是方便,但是用法較多,所以小白決定整理一下。以後再遇到時間日期轉換可能手寫代碼而省去翻看資料的時間。 vue ...
  • 1.自動裝配,簡單來說就是自動把第三方組件的Bean裝載到Spring IOC容器裡面,不需要開發人員再去寫Bean的裝配配置, 2.在Spring Boot應用裡面,只需要在啟動類加上@SpringBootApplication註解就可以實現自動裝配。 ...
  • 提交代碼是程式員們每天的工作日常,今天敬姐給大家分享一個好的編程習慣,就是關於Git Commit規範。 ## 效果預覽 ``` (): ``` 提交之後的效果如下: ![img](https://img2023.cnblogs.com/blog/37001/202309/37001-2023090 ...
  • # What is Bridge Pattern 橋接模式(Bridge Pattern),旨在將抽象部分和實現部分解耦,使它們可以獨立地變化。該模式通過將抽象和實現分離,使它們可以獨立地進行擴展和修改,同時通過橋接(Bridge)將它們連接起來。 將一個事物原本耦合在一起的東西,通過定義成抽象和實 ...
  • ## 1、標準原子類型 標準原子類型的定義位於頭文件``內。原子操作的關鍵用途是取代需要互斥的同步方式,但假設原子操作本身也在內部使用了互斥,就很可能無法達到期望的性能提升。有三種方法來判斷一個原子類型是否屬於無鎖數據結構: - 所有標準原子類型(`std::atomic_flag`除外,因為它必須 ...
  • ![file](https://img2023.cnblogs.com/other/268922/202309/268922-20230905194637565-1884920101.png) 上圖的意思: 百戰百勝,屢試不爽。 # 故事 ![file](https://img2023.cnblog ...
  • # Hook Method 鉤子方法(Hook Method)之所以被稱為“鉤子”,是因為它在演算法或流程中提供了一個“鉤子”,允許子類在特定的點上“鉤入”自己的實現邏輯,從而影響演算法的行為或流程的執行。 它類似於一個掛鉤、錨點,所以叫Hook method, 它允許子類插入自定義的代碼來改變或擴展算 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...