可复制, 可验证, 已验证的构建(vgo)

lingchao · · 2446 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

本文译自 Reproducible, Verifiable, Verified Builds, Go & Versioning 的第 5 部分, 版权@归原文所有.

一旦 Go 开发人员和工具都共享了包版本控制的词汇表, 那么在工具链中添加可复制, 可验证以及已验证的构建就相对简单了。事实上, 这基本上已经在 vgo 原型中了.

由于人们有时对这些术语的确切定义不一致,让我们来建立一些基本的术语. 这篇文章:

  • 一个可复制的构建, 当重复构建时, 会产生相同的结果.
  • 一个可验证的构建, 记录足够的信息以精确地描述如何重复它.
  • 一个已验证的构建, 可以检查是否使用了预期的源代码.

Vgo 在默认情况下提供可复制的构建. 生成的二进制文件是可验证的, 因为它们记录了进入构建的确切源代码的版本. 并且可以配置你的代码库, 以便用户重新构建你的软件, 验证他们的构建与您的构建匹配, 使用加密的散列, 无论它们如何获得依赖项.

可复制(重复)的构建

至少, 我们希望确保在你构建我的程序时, 构建系统决定使用相同的代码版本. 最小版本选择在默认情况下交付该属性. 仅使用 go.mod 文件就足以确定应该使用哪个模块版本(假设依赖关系可用), 而且即使将模块的新版本引入到生态系统中, 这个决策也是稳定的. 这与大多数其他系统不同, 后者自动地采用新版本, 并且需要限制来生成可复制的构建. 我在 最小版本选择 文章中提到过这个, 但它是一个重要的, 微妙的细节, 所以我将尝试在这里简短地重复一下.

为了使这个具体化, 让我们看看来自 Rust的包管理器 Cargo 的几个真正的包. 很明显,我不是在挑剔 Cargo, 我认为 Cargo 是包管理当前艺术水平的一个例子, 并且有很多可以从中学习. 如果我们能让 Go 的包管理像 Cargo 一样平稳, 那么我很开心. 但我也认为, 在选择版本时, 我们是否会从选择不同的默认值中受益, 这是值得探讨的.

Cargo 更喜欢以下意义上的最大版本. 当我写这篇文章时, crates.io 上最新的 toml 版本是 0.4.5. 它列出了对 serde 1.0 或更高版本的依赖; 最新的 serde 是 1.0.27. 如果你启动一个新项目并添加对 toml 0.4.1 或更高版本的依赖, Cargo 可以作出选择. 根据约束, 0.4.1, 0.4.2, 0.4.3, 0.4.4 或 0.4.5 中的任何一个都是可接受的. 在所有其他条件相同的情况下, Cargo 倾向于使用最新的可接受版本 0.4.5. 同样, 从 1.0.0 到 1.0.27 的任何一个 serde 都是可以接受的, Cargo 选择 1.0.27. 这些选择随着新版本的推出而改变. 如果今晚发布了 serde 1.0.28, 并且明天我将 toml 0.4.5 添加到了项目中, 那么我将得到 1.0.28 而不是 1.0.27. 正如迄今为止所描述的, Cargo 的构建是不可复制的. Cargo 对这个问题的(完全合理的)答案是不仅有一个约束文件 (manifest, Cargo.toml), 而且还有一个在构建中使用的确切组件(artifacts)的列表(lock 文件 Cargo.lock). lock 文件阻止将来的升级; 一旦写入, 即使 1.0.28 发布, 你的构建仍然保留在 serde 1.0.27 上.

相比之下, 最小版本选择偏好允许的最小版本, 这是项目中某些 go.mod 所要求的确切版本. 这个答案不会随着新版本的添加而改变. 对比 Cargo 示例中给出的选择, vgo 会选择 toml 0.4.1 (你要求的), 然后选择 serde 1.0 (toml 要求的). 这些选择是稳定的, 没有 lock 文件. 这就是我说 vgo 的构建默认可复制的意思.

可验证的构建

Go 的二进制文件一直包含一个字符串, 表示它们的 Go 版本. 去年, 我编写了一个工具 rsc.io/goversion, 它从给定的可执行文件或可执行文件树中获取这些信息. 例如, 在我的 Ubuntu Linux 笔记本电脑上, 我可以看看哪些系统实用程序是 Go 实现的:

$ go get -u rsc.io/goversion
$ goversion /usr/bin
/usr/bin/containerd go1.8.3
/usr/bin/containerd-shim go1.8.3
/usr/bin/ctr go1.8.3
/usr/bin/go go1.8.3
/usr/bin/gofmt go1.8.3
/usr/bin/kbfsfuse go1.8.3
/usr/bin/kbnm go1.8.3
/usr/bin/keybase go1.8.3
/usr/bin/snap go1.8.3
/usr/bin/snapctl go1.8.3
$

现在 vgo 原型可以理解模块版本, 它也将这些信息包含在最终的二进制文件中, 并且新的 goversion -m 标志将其打印出来. 使用我们来自 tour 的 "hello, world" 程序:

$ go get -u rsc.io/goversion
$ goversion ./hello
./hello go1.10
$ goversion -m hello
./hello go1.10
    path  github.com/you/hello
    mod   github.com/you/hello  (devel)
    dep   golang.org/x/text     v0.0.0-20170915032832-14c0d48ead0c
    dep   rsc.io/quote          v1.5.2
    dep   rsc.io/sampler        v1.3.0
$

主模块 github.com/you/hello, 没有版本信息, 因为它是本地开发副本, 而不是我们下载的特定版本. 但是如果我们直接从有版本的模块构建命令, 那么列表会报告所有模块的版本:

$ vgo build -o hello2 rsc.io/hello
vgo: resolving import "rsc.io/hello"
vgo: finding rsc.io/hello (latest)
vgo: adding rsc.io/hello v1.0.0
vgo: finding rsc.io/hello v1.0.0
vgo: finding rsc.io/quote v1.5.1
vgo: downloading rsc.io/hello v1.0.0
$ goversion -m ./hello2
./hello2 go1.10
    path  rsc.io/hello
    mod   rsc.io/hello       v1.0.0
    dep   golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c
    dep   rsc.io/quote       v1.5.2
    dep   rsc.io/sampler     v1.3.0
$

当我们集成版本进 Go 工具链时, 我们将添加 API 以从运行时库访问此信息, 就像 [runtime.Version](https://golang.org/pkg/runtime/#Version) 提供了受限的 Go 版本信息访问.

为了尝试重构二进制文件, 通过 goversion -m 列出的信息就足够了: 将版本放入 go.mod 文件并构建在路径行上命名的目标. 但如果结果不是相同的二进制文件, 你可能想知道如何缩小不同的方法. 什么改变了 ?

当 vgo 下载每个模块时, 它会计算与该模块相对应的文件树的哈希值. 该哈希也包含在二进制文件中, 并附有版本信息, 而且 goversion -mh 也可以打印出它:

$ goversion -mh ./hello
hello go1.10
    path  github.com/you/hello
    mod   github.com/you/hello  (devel)
    dep   golang.org/x/text     v0.0.0-20170915032832-14c0d48ead0c  h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
    dep   rsc.io/quote          v1.5.2                              h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
    dep   rsc.io/sampler        v1.3.1                              h1:F0c3J2nQCdk9ODsNhU3sElnvPIxM/xV1c/qZuAeZmac=
$ goversion -mh ./hello2
hello go1.10
    path  rsc.io/hello
    mod   rsc.io/hello       v1.0.0                              h1:CDmhdOARcor1WuRUvmE46PK91ahrSoEJqiCbf7FA56U=
    dep   golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c  h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
    dep   rsc.io/quote       v1.5.2                              h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
    dep   rsc.io/sampler     v1.3.0                              h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
$

h1: 前缀指示正在报告哪个哈希. 今天, 只有 "hash 1", 文件列表的 SHA-256 哈希及其内容的 SHA-256 哈希. 如果我们需要稍后更新一个新的哈希, 这个前缀将帮助我们从新的哈希中告诉旧的.

我必须强调这些哈希是由构建系统自我报告的. 如果某人在构建信息中为您提供了具有特定哈希值的二进制文件, 则无法保证其准确性. 它们是支持以后验证的非常有用的信息, 而不是自己可以信任的签名.

已验证的构建

以源代码形式发布程序的作者可能希望让用户验证他们是否正在使用预期的依赖构建它. 我们知道 vgo 会做出与使用哪个版本的依赖相同的决定, 但仍然存在将 v1.5.2 等版本映射到实际源码树的问题. 如果 v1.5.2 的作者将标签(tag)更改为指向不同的文件树, 该怎么办 ? 如果恶意中间件拦截下载请求并提供不同的 zip 文件会怎么样 ? 如果用户不小心编辑了 v1.5.2 的本地副本中的源文件, 该怎么办 ? vgo 原型也支持这种验证.

最终形式可能有所不同, 但如果你在 go.mod 旁边创建一个名为 go.modverify 的文件, 那么构建将使用特定版本的模块的已知哈希使该文件保持最新:

$ echo >go.modverify
$ vgo build
$ tcat go.modverify  # go get rsc.io/tcat, or use cat
golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c  h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
rsc.io/quote       v1.5.2                              h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/sampler     v1.3.0                              h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
$

go.modverify 文件是所有有史以来遇到的版本的哈希日志: 只添加行, 不删除. 如果我们将 rsc.io/sampler 更新为 v1.3.1, 则日志现在将包含两个版本的哈希值:

$ vgo get rsc.io/[email protected]
$ tcat go.modverify
golang.org/x/text  v0.0.0-20170915032832-14c0d48ead0c  h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
rsc.io/quote       v1.5.2                              h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/sampler     v1.3.0                              h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler     v1.3.1                              h1:F0c3J2nQCdk9ODsNhU3sElnvPIxM/xV1c/qZuAeZmac=
$

当 go.modverify 存在时, vgo 将检查给定内部版本中使用的所有下载模块是否与文件中已有的条目一致. 例如, 如果我们将 rsc.io/quote 散列的第一个数字从 w 更改为 v:

$ vgo build
vgo: verifying rsc.io/quote v1.5.2: module hash mismatch
    downloaded:   h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
    go.modverify: h1:v5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
$

或者假设我们修复了那个, 但是修改了 v1.3.0 哈希. 现在我们的构建成功了, 因为构建版本没有使用 v1.3.0, 所以它的行被(正确地)忽略了. 但是, 如果我们尝试降级到 v1.3.0, 那么构建验证将失败:

$ vgo build
$ vgo get rsc.io/[email protected]
vgo: verifying rsc.io/sampler v1.3.0: module hash mismatch
    downloaded:   h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
    go.modverify: h1:8uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
$

希望确保其他人使用与他们完全相同的源重建其程序的开发人员可以将 go.modverify 存储在其代码库中. 然后其他构建使用相同的代码库会自动获得验证构建. 目前, 只有构建的顶级模块中的 go.modverify 适用. 但请注意, go.modverify 会列出所有依赖关系, 包括间接依赖关系, 因此整个构建都会被验证.

go.modverify 特性可帮助检测不同机器下载的依赖之间的不匹配情况. 它比较 go.modverify 中的哈希值和模块下载时计算和保存的哈希值. 还可以检查下载的模块是否在本地机器上有没有发生更改. 这不是关于安全性的攻击, 更多的是关于避免错误. 例如, 因为源文件路径出现在堆栈跟踪中, 所以在调试时打开这些文件是很常见的. 如果你在调试过程中意外地(或者我认为是故意地)修改文件, 那么稍后能够检测到它将是很好的. vgo verify 命令执行此操作:

$ go get -u golang.org/x/vgo  # fixed a bug, sorry! :-)
$ vgo verify
all modules verified
$

如果源文件更改, vgo verify 会通知:

$ echo >>$GOPATH/src/v/rsc.io/[email protected]/quote.go
$ vgo verify
rsc.io/quote v1.5.2: dir has been modified (/Users/rsc/src/v/rsc.io/[email protected])
$

如果我们恢复文件, 一切都很好:

$ gofmt -w $GOPATH/src/v/rsc.io/[email protected]/quote.go
$ vgo verify
all modules verified
$

如果下载后修改了缓存的 zip 文件, vgo verify 也会通知, 尽管我无法合理解释可能发生的情况:

$ zip $GOPATH/src/v/cache/rsc.io/quote/@v/v1.5.2.zip /etc/resolv.conf
  adding: etc/resolv.conf (deflated 36%)
$ vgo verify
rsc.io/quote v1.5.2: zip has been modified (/Users/rsc/src/v/cache/rsc.io/quote/@v/v1.5.2.zip)
$

由于 vgo 在解压缩后会保留原始 zip 文件, 因此如果 vgo verify 确定只有 zip 文件和目录树中的一个已被修改, 则甚至可以打印这两者的差异.

接下来呢 ?

这已经在 vgo 中实现. 你可以尝试一下并使用它. 与 vgo 的其他部分一样, 对于哪些工作不正常(或工作出色)的反馈表示感激.

这里展示的功能更多的是一些东西的开始, 而不是一个完成的功能. 文件树的加密哈希是一个构建块. 建立在它之上的 go.modverify 检查开发人员是否都使用完全相同的依赖构建特定的模块, 但是在下载新版本的模块时没有验证(除非其他人已经将其添加到 go.modverify), 模块之间也没有共享预期的哈希值.

如何解决这两个缺点的确切细节并不明显. 允许某种类型的文件树的加密签名是有意义的, 并且要验证升级发现的版本与上一个版本的密钥相同. 或者, 在更新框架(TUF)中采用一种方法是有意义的, 尽管直接使用它们的网络协议是不实际的. 或者, 不要使用每个代码库 go.modverify 日志, 建立某种共享的全局日志可能有意义, 有点像证书透明度, 或者使用一个类似 Upspin 的公共身份服务器. 我们可能会探索很多途径, 但这些都有点超前了. 目前, 我们的重点是成功地将版本控制集成到 go 命令中.


有疑问加站长微信联系(非本文作者)

本文来自:lingchao.xin

感谢作者:lingchao

查看原文:可复制, 可验证, 已验证的构建(vgo)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2446 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传