设为首页 加入收藏

TOP

完全可复制、经过验证的 Go 工具链(一)
2023-09-09 10:25:26 】 浏览:340
Tags:全可复 经过验

原文在这里

由 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):编译器使用的任何库,如果它可以从

首页 上一页 1 2 3 4 5 下一页 尾页 1/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇每日一库:pprof简介 下一篇微服务架构|go-zero 的自适应熔..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目