无论采用何种流程或技术手段,每一个依赖关系都必须是相互信任的关系。然而,Go的工具和设计有助于降低所有阶段的风险。所有构建都“锁定”到外部世界的变化,例如发布新版本的依赖项,并且不影响Go构建。与大多数包管理器文件不同,Go模块没有单独的约束列表和锁定文件,而是锁定到特定版本。任何Go构建的每个依赖项的版本完全取决于主模块的go.mod文件。从Go1.16开始,这种确定性被强制执行,如果go.mod不完整,构建命令(gobuild、gotest、goinstall、gorun...)将失败。改变go.mod(以及构建)的唯一命令是goget和gomodtidy。这些命令不会自动运行或在CI中运行,因此对依赖树的更改必须是有意的,并且有机会通过代码审查。这对安全来说非常重要,因为当一个CI系统或一台新机器运行时,签入的源代码是最终的和完整的,代码会说要构建什么,第三方无法影响它.还有,在用goget添加依赖的时候,它的交叉依赖会根据依赖的go中指定的版本来添加。调用goinstallexample.com/cmd/devtoolx@latest时也会发生同样的情况,它的等效绕过某些生态系统中的固定。在Go中,将获取最新版本的example.com/cmd/devtoolx,但所有依赖项都将由其go.mod文件设置。如果一个模块被破坏并发布了一个新的恶意版本,在他们明确更新该依赖关系之前不会受到任何影响,这提供了审查更改并为生态系统提供足够时间检测事件的机会。版本内容永不改变确保第三方无法影响构建的另一个关键属性是模块版本的内容是不可变的。如果攻击者破坏了依赖关系,则可以重新上传现有版本,并且他们可以自动破坏依赖它的所有项目。这就是go.sum文件的作用。它包含构建所需的每个依赖项的加密哈希列表。此外,不完整的go.sum会导致错误,只有goget和gomodtidy会修改它,因此对它的任何修改都会伴随着故意的依赖关系更改。其他构建保证有一套完整的校验和。这是大多数锁定文件的共同特征。Go超越了Checksum数据库(简称sumdb),这是一个全局的、仅附加的加密验证go.sum条目列表。当goget需要向go.sum文件中添加一个条目时,它会从sumdb中获取它并以密码方式证明sumdb的完整性。这不仅确保模块的每个构建都使用相同的依赖项,而且每个模块都使用相同的依赖项。sumdb使得破坏的依赖关系甚至谷歌运营的Go基础设施都不可能使用修改后的(例如后门)源代码来针对特定的依赖关系。确保您使用的代码与其他人使用的代码完全相同,例如example.com/modulex的v1.9.2使用的代码,并且已经过审查。最后,我最喜欢sumdb的特性是它不需要模块作者的任何密钥管理,并且可以与Go模块的去中心化特性无缝配合。VCS是事实的来源大多数项目都是通过某种版本控制系统(VCS)开发的,然后在其他生态系统中上传到包存储库。这意味着有两个帐户可能会受到威胁,即VCS主机和软件包存储库,后者使用较少且更容易被忽视。这也意味着在上传到存储库的版本中更容易隐藏恶意代码,特别是如果源代码在上传过程中经常被修改,比如缩小。在Go中,没有包存储库帐户这样的东西。包的导入路径嵌入了gomoddownload直接从VCS获取其模块所需的信息,其中标签定义了版本。我们确实有GoModuleMirror,但这只是一个代理。模块作者不需要注册账号,也不需要上传版本到broker。代理使用与go工具相同的逻辑(实际上,代理运行gomoddownload)来获取和缓存版本。由于校验和数据库保证给定模块版本只能有一个源代码树,因此使用代理的每个人都会看到相同的结果,就好像它们绕过代理直接从VCS获取一样。(如果该版本在VCS中不再可用,或者如果其内容已更改,直接获取将导致错误,而从代理获取可能仍然有效,从而提高可用性并保护生态系统免受“左键”问题的影响)。在客户端运行VCS工具会暴露出相当大的攻击面。这是GoModuleMirror的另一个作用:代理上的Go工具运行在一个强大的沙箱中,并配置为支持所有VCS工具,而默认情况下仅支持两个主要的VCS系统(git和Mercurial)。使用非默认VCS系统发布的代码仍然可以被任何使用代理的人获取,但在大多数安装中攻击者无法访问该代码。只构建代码,而不是执行它Go工具链的一个明确的安全设计目标是无法获取或构建代码来执行该代码,即使它是不受信任的和恶意的。这与大多数生态系统不同,其中许多生态系统对在获取包时运行代码提供一流的支持。这些“安装后”挂钩在过去被用作最方便的攻击形式:通过受损的依赖项攻击开发人员的机器,并通过模块作者进行蠕虫攻击。公平地说,如果你要获取一些代码,它往往会在不久之后执行,或者作为开发人员机器上测试的一部分,或者作为生产中二进制文件的一部分,因此缺少安装后挂钩会只会减慢攻击者的速度。(在构建过程中没有安全边界:任何参与构建的包都可以定义一个初始函数)。但是,这也是一种有意义的风险缓解措施,因为您在执行二进制文件或测试包时可能只使用模块依赖项的子集。例如,如果您在macOS上构建并执行example.com/cmd/devtoolx,则不太可能只有Windows依赖项或example.com/cmd/othertool依赖项会危害您的计算机。在Go中,不为特定构建提供代码的模块对其没有安全影响。“一点点复制胜过一点点依赖”Go生态系统中最后也是最重要的软件供应链风险缓解是技术最少的:Go有拒绝大依赖树的文化,更喜欢复制愿意添加新的依赖项.这可以追溯到Go的一句格言:“一点点复制胜过一点点依赖”。高质量的可重用Go模块自豪地贴上了“零依赖”的标签。如果您发现自己需要一个库,您可能会发现它不会导致您依赖来自其他作者和所有者的许多模块。这也得到丰富的标准库和其他模块(位于golang.org/x/…的模块)的支持,这些模块提供常用的高级构建块,例如HTTP堆栈、TLS库、JSON编码等。所有这些都意味着只需很少的依赖项就可以构建丰富、复杂的应用程序。无论一个工具有多好,它都不能消除重用代码的风险,所以最强的缓解措施永远是一个小的依赖树。
