logo
0
0
Login
update doc

go get 发生了什么

当执行以下命令时:

go get github.com/kataras/iris/v12@latest

抓包可以看到:

go-get-request

可以看到依次请求了:

  1. proxy.golang.org/github.com/kataras/iris/@v/list
  2. proxy.golang.org/github.com/kataras/iris/@v/v12.2.11.info
  3. proxy.golang.org/github.com/kataras/iris/@v/v12.2.11.mod
  4. sum.golang.org/lookup/github.com/kataras/iris/v12@v12.2.11
  5. sum.golang.org/tile/...
  6. proxy.golang.org/github.com/kataras/iris/@v/v12.2.11.zip

根据 GOPROXY protocol 的描述, 这些接口的作用分别为:

  1. /@v/list 返回给定模块的所有版本信息

    v-list

  2. /@v/v12.2.11.info 返回指定版本的 metadata v-info

  3. /@v/v12.2.11.mod 返回指定版本的 go.mod 文件内容 v-mod

  4. /@v/v12.2.11.zip 返回指定版本的源代码 zip 文件

那请求 sum.golang.org 的接口用途是干啥的? 看起来直接使用 proxy.golang.org 已经能够下载到依赖代码了。

安全

想象一下,如何保证下载的文件没有被意外修改呢?

给 go.mod 和 zip 文件加上 hash 似乎是一个可行的方案。

就像 sum.golang.org/lookup/ 接口正在做的事情:

sum-lookup

这个接口返回了很多信息,让我们先关注模块的 hash. 这里返回了两种 hash:

  1. 完整模块哈希

    格式:模块路径 版本 h1:哈希

    说明:这是对整个模块所有文件(源代码、资源文件等,包括 go.mod)的哈希。(注意:不是对 zip 文件直接求 hash)

  2. 仅 go.mod 文件的哈希

    格式:模块路径 版本/go.mod h1:哈希

    说明:这是仅针对模块 go.mod 文件本身的哈希,不包含其他文件。

两种哈希并存,分别服务于不同场景(完整代码校验 vs 快速依赖解析),共同保障模块管理的安全性和效率。

似乎到这里,问题就解决啦?

  1. 通过 GOPROXY protocol 下载依赖

  2. 通过 sum.golang.org/lookup/ 接口获取 hash 校验下载文件的完整性

  3. 将 hash 记录到, go.sum 文件中, 保证之后下载的文件也不会有意外修改

这里主要问题是:

  1. 通过 go.sum 中存储的 hash 可以验证下载文件的完整性。但是首次添加模块,或者更新模块时,go.sum 文件中并没有 hash,如何保证首次获取的 hash 是正确的?

  2. 如何监督 sum.golang.org 服务本身的行为是正确的? 会不会因为 bug 或者恶意攻击,返回了错误的 hash 值?

  3. 当使用代理服务器时,如何保证代理服务器不会篡改 hash 值?

透明日志

Transparent Logs 中描述了一个去中心化的解决方案:

the log server is not trusted to store the log properly, nor is it trusted to put the right records into the log. Instead, clients and auditors interact skeptically with the server, able to verify for themselves in each interaction that the server really is behaving correctly

大概意思是: 使用透明日志存储 hash 可以让客户端在不信任服务端的情况下,可以自行验证服务端是否正确地存储了 hash。

Merkle Trees

透明日志使用了一种叫做 Merkle Trees 的数据结构来保障这一安全性。

下图展示了一棵大小为 16 的 Merkle Tree: merkle-trees

  1. 最下面一层的小方格,表示存储的数据,在这里就是模块的 hash 信息。

  2. level 0 ~ level 4 是树的高度

  3. 每一层节点上的数字,代表一个 hash

  4. level 0 层的每个节点,都是由数据直接计算出来的 hash

  5. level 1 ~ level 4 的节点,都是由下层节点,两两组合计算出来的 hash

基于此,只要知道根节点的 hash 值。通过从下往上计算 hash, 就可以验证任意节点是否被修改。因为任意一个叶子结点的修改,都会导致根节点的 hash 值变化。

此时再来看下 sum.golang.org/lookup/ 接口返回的其他部分内容 sum-lookup-merkle-tree

  1. 25349969 表示当前请求模块信息的日志记录编号(即叶子结点编号)
  2. go.sum database tree 描述
  3. 42190987 树的大小(准确来说是叶子数,不一定是最大树,只要是包含该模块哈希的树即可)
  4. A/lCddVYmNj417QKH59jHdQ9ClJq3/hC+LCdGUrYJDk= 叶子数为 42190987 这棵树的根哈希值

客户端拿到这些信息后,如何验证呢?

原始的树太大了,我们缩小一下: 25349969 => 9, 42190987 => 15merkle-tree-validate

所以我们现在拿到了 (level 0, 9),和 (level 4, 0) 节点的 hash

现在只要再获取到 (level 0, 8), (level 1, 5), (level 2, 3), (level 3, 0) 这 4 个节点, 就可以计算出跟节点的 hash 了, 再与返回的根节点 hash 进行对比,就可以验证该模块信息是否被篡改了。

还记得最开始的请求记录截图里的那些 sum.golang.org/tile/... 请求吗? 没错, 就是用来获取这些辅助节点的。

看着是不是有点脱裤放屁的感觉?所有的数据都是从 sum.golang.org 获取的,那还不是想要什么 hash 服务端都能给你构造出来吗?

其实不然, 参与计算的节点会被缓存到 $GOMODCACHE/cache/download/sumdb/sum.golang.org/tile 目录下. 也就是说本地缓存会参与计算, 如果服务端有恶意修改, 最终计算出来的根节点 hash 也会不一样的。因为没法修改你本地的缓存。

传输安全

中间人攻击(例如恶意的第三方代理), 中间人可能会篡改 hash 值, 从而导致客户端一开始就被欺骗。那么如何保障传输安全呢? sum-lookup-sign

这就是最后一部分信息的作用了, 通过签名来保障传输安全。私钥存储在 sum.golang.org 服务器上, 公钥则已经写在客户端的源代码里了。

https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/key.go

// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package modfetch var knownGOSUMDB = map[string]string{ "sum.golang.org": "sum.golang.org+033de0ae+Ac4zctda0e5eza+HJyk9SxEdh+s3Ux18htTTAD8OuAn8", }

总结

  1. 通过 GOPROXY protocol 下载依赖
  2. 通过 sum.golang.org 透明日志服务, 让客户端有能力自行验证下载文件的正确性
  3. 将 hash 记录到 go.sum 文件中, 保证之后下载的文件也不会有意外修改
  4. 通过传输签名, 防止中间人攻击

参考

  1. goproxy-protocol
  2. checksum-database
  3. Proposal: Secure the Public Go Module Ecosystem
  4. Transparent Logs for Skeptical Clients
  5. Golang Module 背后的安全与代理机制