聊聊 go.sum

目录

 一.为什么要引入go.sum 

1.GOPATH(go 1.5 版本之前)

2.vendor(go 1.5 版本)

3.go module

二.go.sum工作机制

1.go.sum文件记录

2.生成

3.校验

4.校验和数据库

三.go.sum的内容

1.go mod download -x -json github.com/google/[email protected]  

2.下载完成后,会去校验go.sum的内容和缓存目录的.ziphash对比,不一致的话拒绝构建。

3.更改go.sum

四.GOSUMDB原理

1.默克尔树的构建

2.证明记录R包含在散列树中

3.证明历史日志是当前日志的前缀


 一.为什么要引入go.sum 


先来看下go的包管理发展历史

GOPATH  =>  vender  =>  go module

1.GOPATH(go 1.5 版本之前)

  • 标准库:$GOROOT/src/目录下

  • 第三方库:$GOPATH/src/目录下

  • 项目私有库:$GOPATH/src/目录下

编译时会去 $GOPATH/src/ 目录去查找需要的代码,以 github.com/google/uuid 这个包为例,需要放到$GOPATH/src/  github.com/google/uuid 目录下,当其他项目在 import github.com/google/uuid 的时候也就能找到对应的代码了。

缺点:

  • 没有版本控制,如果有两个项目依赖同一个包的不同版本就无法共享一个GOPATH了。 

  • 没有版本控制,无法一致性构建。

2.vendor(go 1.5 版本)

go 1.5版本推出 vendor 机制。所谓 vendor 机制,就是每个项目的任意目录下可以有一个 vendor 目录,里面存放了该项目的依赖的 package。go build 的时候会先去 vendor 目录查找依赖,如果没有找到会再去 GOPATH 目录下查找。

优点:解决了不同项目依赖同一个包的不同版本问题。

缺点:

  • 编译后的二进制文件体积增大。  比如直接依赖了A、B两个包。但是A包有一个vendor目录包含了B包

  • 没有从根本上解决一致性构建的问题。

3.go module

go module的核心功能 准确的记录了项目的依赖记录,使得一致性构建成为了可能。

聊聊 go.sum_第1张图片

缺点:Go 并没有一个中央仓库来保证包不会被篡改(比如发布了一个1.1.1的包 然后又提交了不同的内容 把之前的1.1.1的tag删掉 重新打上1.1.1的tag)。

go.sum的出现正是为了解决这个问题

二.go.sum工作机制

为了确保一致性构建,Go引入了go.mod文件来标记每个依赖包的版本,在构建过程中go命令会下载go.mod中的依赖包,下载的依赖包会缓存在本地,以便下次构建。 考虑到下载的依赖包有可能是被黑客恶意篡改的,以及缓存在本地的依赖包也有被篡改的可能,单单一个go.mod文件并不能保证一致性构建。

为了解决Go module的这一安全隐患,Go开发团队在引入go.mod的同时也引入了go.sum文件,用于记录每个依赖包的哈希值,在构建时,如果本地的依赖包hash值与go.sum文件中记录得不一致,则会拒绝构建。

本节暂不对模块校验细节展开介绍,只从日常应用层面介绍:

  • go.sum 文件记录含义
  • go.sum文件内容是如何生成的
  • go.sum是如何保证一致性构建的

1.go.sum文件记录

go.sum文件中每行记录由module名、版本和哈希组成,并由空格分开:

 [/go.mod] 

比如,某个go.sum文件中记录了github.com/google/uuid 这个依赖包的v1.1.1版本的哈希值:

github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=  
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

在Go module机制下,我们需要同时使用依赖包的名称和版本才可以准确的描述一个依赖,为了方便叙述,下面我们使用依赖包版本来指代依赖包名称和版本。

正常情况下,每个依赖包版本会包含两条记录,第一条记录为该依赖包版本整体(所有文件)的哈希值,第二条记录仅表示该依赖包版本go.mod文件的哈希值,如果该依赖包版本没有go.mod文件,则只有第一条记录。如上面的例子中,v1.1.1表示该依赖包版本整体,而v1.1.1/go.mod表示该依赖包版本go.mod文件。

依赖包版本中任何一个文件(包括go.mod)改动,都会改变其整体哈希值,此处再额外记录依赖包版本go.mod文件主要用于计算依赖树时不必下载完整的依赖包版本,只根据go.mod即可计算依赖树。

每条记录中的哈希值前均有一个表示哈希算法的h1:,表示后面的哈希值是由算法SHA-256计算出来的,自Go module从v1.11版本初次实验性引入,直至v1.14 ,只有这一个算法。

此外,细心的读者或许会发现go.sum文件中记录的依赖包版本数量往往比go.mod文件中要多,这是因为二者记录的粒度不同导致的。go.mod只需要记录直接依赖的依赖包版本,只在依赖包版本不包含go.mod文件时候才会记录间接依赖包版本,而go.sum则是要记录构建用到的所有依赖包版本

2.生成

假设我们在开发某个项目,当我们在GOMODULE模式下引入一个新的依赖时,通常会使用go get命令获取该依赖,比如:

go get github.com/google/[email protected]

go get命令首先会将该依赖包下载到本地缓存目录$GOPATH/pkg/mod/cache/download,该依赖包为一个后缀为.zip的压缩包,如v1.0.0.zipgo get下载完成后会对该.zip包做哈希运算,并将结果存放在后缀为.ziphash的文件中,如v1.0.0.ziphash。如果在项目的根目录中执行go get命令的话,go get会同步更新go.modgo.sum文件,go.mod中记录r 的是依赖名及其版本,如:

require (
	github.com/google/uuid v1.0.0
)

go.sum文件中则会记录依赖包的哈希值(同时还有依赖包中go.mod的哈希值),如:

github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

值得一提的是,在更新go.sum之前,为了确保下载的依赖包是真实可靠的,go命令在下载完依赖包后还会查询GOSUMDB环境变量所指示的服务器,以得到一个权威的依赖包版本哈希值。如果go命令计算出的依赖包版本哈希值与GOSUMDB服务器给出的哈希值不一致,go命令将拒绝向下执行,也不会更新go.sum文件。

go.sum存在的意义在于,我们希望别人或者在别的环境中构建当前项目时所使用依赖包跟go.sum中记录的是完全一致的,从而达到一致构建的目的。

3.校验

假设我们拿到某项目的源代码并尝试在本地构建,go命令会从本地缓存中查找所有go.mod中记录的依赖包,并计算本地依赖包的哈希值,然后与go.sum中的记录进行对比,即检测本地缓存中使用的依赖包版本是否满足项目go.sum文件的期望。

如果校验失败,说明本地缓存目录中依赖包版本的哈希值和项目中go.sum中记录的哈希值不一致,go命令将拒绝构建。 这就是go.sum存在的意义,即如果不使用我期望的版本,就不能构建。

当校验失败时,有必要确认到底是本地缓存错了,还是go.sum记录错了。 需要说明的是,二者都可能出错,本地缓存目录中的依赖包版本有可能被有意或无意地修改过,项目中go.sum中记录的哈希值也可能被篡改过。

当校验失败时,go命令倾向于相信go.sum,因为一个新的依赖包版本在被添加到go.sum前是经过GOSUMDB(校验和数据库)验证过的。此时即便系统中配置了GOSUMDB(校验和数据库),go命令也不会查询该数据库。

4.校验和数据库

环境变量GOSUMDB标识一个checksum database,即校验和数据库,实际上是一个web服务器,该服务器提供查询依赖包版本哈希值的服务。

该数据库中记录了很多依赖包版本的哈希值,比如Google官方的sum.golang.org则记录了所有的可公开获得的依赖包版本。除了使用官方的数据库,还可以指定自行搭建的数据库,甚至干脆禁用它(export GOSUMDB=off)。

如果系统配置了GOSUMDB,在依赖包版本被写入go.sum之前会向该数据库查询该依赖包版本的哈希值进行二次校验,校验无误后再写入go.sum

如果系统禁用了GOSUMDB,在依赖包版本被写入go.sum之前则不会进行二次校验,go命令会相信所有下载到的依赖包,并把其哈希值记录到go.sum中。

三.go.sum的内容

格式:

  
 /go.mod 

比如:

github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0=
github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

h1 表示的hash算法 目前仅有一种SHA-256。(具体计算过程有兴趣的可以看下$GOROOT/src/cmd/vendor/golang.org/x/mod/sumdb/dirhash)

// Hash1 is the "h1:" directory hash function, using SHA-256.
//
// Hash1 is "h1:" followed by the base64-encoded SHA-256 hash of a summary
// prepared as if by the Unix command:
//
// find . -type f | sort | sha256sum
//
// More precisely, the hashed summary contains a single line for each file in the list,
// ordered by sort.Strings applied to the file names, where each line consists of
// the hexadecimal SHA-256 hash of the file content,
// two spaces (U+0020), the file name, and a newline (U+000A).
//
// File names with newlines (U+000A) are disallowed.
func Hash1(files []string, open func(string) (io.ReadCloser, error)) (string, error) {
   h := sha256.New()
   files = append([]string(nil), files...)
   sort.Strings(files)
   for _, file := range files {
      if strings.Contains(file, "\n") {
         return "", errors.New("dirhash: filenames with newlines are not supported")
      }
      r, err := open(file)
      if err != nil {
         return "", err
      }
      hf := sha256.New()
      _, err = io.Copy(hf, r)
      r.Close()
      if err != nil {
         return "", err
      }
      fmt.Fprintf(h, "%x  %s\n", hf.Sum(nil), file)
   }
   return "h1:" + base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
}

go.sum文件是怎么生成的呢?看一个包的下载信息($GOPTAH=/home/work/go/)

1.go mod download -x -json github.com/google/[email protected]  

{
    "Path": "github.com/google/uuid",
    "Version": "v1.1.4",
    "Info": "/home/work/go/pkg/mod/cache/download/github.com/google/uuid/@v/v1.1.4.info",
    "GoMod": "/home/work/go/pkg/mod/cache/download/github.com/google/uuid/@v/v1.1.4.mod",
    "Zip": "/home/work/go/pkg/mod/cache/download/github.com/google/uuid/@v/v1.1.4.zip",
    "Dir": "/home/work/go/pkg/mod/github.com/google/[email protected]",
    "Sum": "h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0=",
    "GoModSum": "h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo="
}

Zip目录是下载的原始包的信息,同时会解压到Dir供编译使用。Sum值是根据zip包hash的结果 会写入到v1.1.4.ziphash文件中。(下载过程$GOPATH/src/cmd/go/internal/modcmd/download.go)
注:Dir目录的文件属性为只读,防止更改。

2.下载完成后,会去校验go.sum的内容和缓存目录的.ziphash对比,不一致的话拒绝构建。

/usr/local/go/src/cmd/go/internal/modfetch/fetch.go

// haveModSumLocked reports whether the pair mod,h is already listed in go.sum.
// If it finds a conflicting pair instead, it calls base.Fatalf.
// goSum.mu must be locked.
func haveModSumLocked(mod module.Version, h string) bool {
   goSum.checked[modSum{mod, h}] = true
   for _, vh := range goSum.m[mod] {
      if h == vh {
         return true
      }
      if strings.HasPrefix(vh, "h1:") {
         base.Fatalf("verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\tgo.sum:     %v"+goSumMismatch, mod.Path, mod.Version, h, vh)
      }
   }
   return false
}

3.更改go.sum

更新go.sum之前会进行二次校验。会去校验和数据库查找该版本的校验和(以GOSUMDB=sum.golang.org (官方的检验和数据库代理)为例)

https://sum.golang.org/lookup/github.com/google/[email protected]

2385493
github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0=
github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

go.sum database tree
8441690
5ZL8Doro3D8bAOXMQiiXX8g+6LI6p0ZvHGJKg0Ly8Sw=

— sum.golang.org Az3gri4+C8z4aOk82R/xIZXI/rgDBQTqE2zTtUZIsAtQMfTS0qx9t9YpohPyNzgbFUPmOFgRu2fowsmKBGAhlZgV0gA=

对比hash值 保证下载的包是正确的。(还会在本地的/home/work/go/pkg/sumdb/缓存一份)

/usr/local/go/src/cmd/go/internal/modfetch/fetch.go

// checkSumDB checks the mod, h pair against the Go checksum database.
// It calls base.Fatalf if the hash is to be rejected.
func checkSumDB(mod module.Version, h string) error {
   db, lines, err := lookupSumDB(mod)
   if err != nil {
      return module.VersionError(mod, fmt.Errorf("verifying module: %v", err))
   }

   have := mod.Path + " " + mod.Version + " " + h
   prefix := mod.Path + " " + mod.Version + " h1:"
   for _, line := range lines {
      if line == have {
         return nil
      }
      if strings.HasPrefix(line, prefix) {
         return module.VersionError(mod, fmt.Errorf("verifying module: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+sumdbMismatch, h, db, line[len(prefix)-len("h1:"):]))
      }
   }
   return nil
}

当然二次校验的前提是打开了校验和,然而我们的配置是

$(GO) env -w GONOPROXY=\*\*.baidu.com\*\*
$(GO) env -w GOPROXY=http://goproxy.baidu-int.com
$(GO) env -w GONOSUMDB=\*
$(GO) env -w GO111MODULE=on

即所有模块都没有走校验和,全部以下载的包的sha256为准。建议GONOSUMDB和GONOPROXY设为一致。

四.GOSUMDB原理

采用Transparent Logs 技术,核心是构建一棵Merkle Trees(默克尔树)。

Transparent Logs :维护和发布公共的、仅可追加的数据日志。能够说服客户仅仅是追加操作。

请记住,每个使用日志的客户端都对日志的正确操作持怀疑态度。日志服务器必须让客户端很容易验证两件事:第一,任何特定记录都在日志中,第二,当前日志是先前观察到的早期日志的仅附加扩展(历史日志是当前日期的前缀)。

1.默克尔树的构建

A Merkle tree is constructed from N records, where N is a power of two. First,
each record is hashed independently, producing N hashes. Then pairs of hashes
are themselves hashed, producing N/2 new hashes. Then pairs of those hashes
are hashed, to produce N/4 hashes, and so on, until a single hash remains. This
diagram shows the Merkle tree of size N = 16:

聊聊 go.sum_第2张图片

我们可以通过它的坐标来引用任何散列:L级散列编号K,我们将其缩写为h(L,K)。在级别0,每个散列的输入是单个记录;在更高级别,每个散列的输入是来自以下级别的一对散列。

h(0, K)= H(record K)
h(L+1, K) = H(h(L, 2 K), h(L, 2 K+1))。

To generalize the Merkle tree to non-power-of-two sizes, we can write N as a
sum of decreasing powers of two, then build complete Merkle trees of those
sizes for successive sections of the input, and finally hash the at-most-lg N complete
trees together to produce a single top-level hash. For example, 13 = 8 + 4
+ 1:

聊聊 go.sum_第3张图片

h(3, x)为虚拟节点,h(3, x)= H(h(2, 2), h(0, 12))

2.证明记录R包含在散列树中

To prove that a particular record is contained in the tree represented by a given
top-level hash (that is, to allow the client to authenticate a record, or verify a
prior commitment, or both), it suffices to provide the hashes needed to recompute
the overall top-level hash from the record’s hash. For example, suppose we
want to prove that a certain bit string B is in fact record 9 in a tree of 16 records
with top-level hash T. We can provide those bits along with the other hash inputs
needed to reconstruct the overall tree hash using those bits. Specifically, the
client can derive as well as we can that:
T = h(4, 0)
= H(h(3, 0), h(3, 1))
= H(h(3, 0), H(h(2, 2), h(2, 3)))
= H(h(3, 0), H(H(h(1, 4), h(1, 5)), h(2, 3)))
= H(h(3, 0), H(H(H(h(0, 8), h(0, 9)), h(1, 5)), h(2, 3)))
= H(h(3, 0), H(H(H(h(0, 8), H(record 9)), h(1, 5)), h(2, 3)))
= H(h(3, 0), H(H(H(h(0, 8), H(B)), h(1, 5)), h(2, 3)))
If we give the client the values [h(3, 0), h(0, 8), h(1, 5), h(2, 3)], the client can
calculate H(B) and then combine all those hashes using the formula and check
whether the result matches T.

聊聊 go.sum_第4张图片

3.证明历史日志是当前日志的前缀

To prove that the log with tree
hash T is included in the log with tree hash T', we can follow the same idea:
give verifiable computations of T and T', in which all the inputs to the computation
of T are also inputs to the computation of T'. For example, consider the
trees of size 7 and 13:

聊聊 go.sum_第5张图片

In the diagram, the “x” nodes complete the tree of size 13 with hash T₁₃, while
the “y” nodes complete the tree of size 7 with hash T₇. To prove that T₇’s leaves
are included in T₁₃, we first give the computation of T₇ in terms of complete
subtrees (circled in blue):
T₇ = H(h(2, 0), H(h(1, 2), h(0, 6)))
Then we give the computation of T₁₃, expanding hashes as needed to expose the
same subtrees. Doing so exposes sibling subtrees (circled in red):
T₁₃ = H(h(3, 0), H(h(2, 2), h(0, 12)))
= H(H(h(2, 0), h(2, 1)), H(h(2, 2), h(0, 12)))
= H(H(h(2, 0), H(h(1, 2), h(1, 3))), H(h(2, 2), h(0, 12)))
= H(H(h(2, 0), H(h(1, 2), H(h(0, 6), h(0, 7)))), H(h(2, 2), h(0, 12)))

只介绍了原理部分,工程上的实现博客中也有说明。论文链接:https://research.swtch.com/tlog.pdf

 作者:Russ Cox

Russ Cox为目前Go团队的leader。2008年MIT博士毕业后就加入了Go核心设计开发团队,非常年轻。代码提交量排第一。

聊聊 go.sum_第6张图片

Go语言设计和工具链核心团队成员介绍 - 知乎

你可能感兴趣的:(Golang,golang)