cmd/go/internal/modfetch: check go.mod content using go.sum

go.sum (formerly go.modverify) already contains lines like

	github.com/pkg/foo v1.2.3 h1:hashhashhashhashhash

where the hash is a checksum over the whole file tree of the module.
After this CL, it will also contain lines like:

	github.com/pkg/foo v1.2.3/go.mod h1:hashhashhashhashhash

(note the /go.mod suffix on the version), where the hash is a checksum
over the one-file tree containing only the go.mod for that module.
This hash is recorded even for auto-generated go.mod files,
because no matter how it arises, the exact go.mod is important
for later reproducibility.

Fixes golang/go#25526.

Change-Id: Ib0ff32f45dd30421907f3ba181db726c7ac9379c
Reviewed-on: https://go-review.googlesource.com/121301
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/internal/modfetch/cache.go b/vendor/cmd/go/internal/modfetch/cache.go
index 14def8d..1f6551e 100644
--- a/vendor/cmd/go/internal/modfetch/cache.go
+++ b/vendor/cmd/go/internal/modfetch/cache.go
@@ -133,10 +133,20 @@
 	c := r.cache.Do("gomod:"+rev, func() interface{} {
 		file, text, err := readDiskGoMod(r.path, rev)
 		if err == nil {
+			// Note: readDiskGoMod already called checkGoMod.
 			return cached{text, nil}
 		}
 
+		// Convert rev to canonical version
+		// so that we use the right identifier in the go.sum check.
+		info, err := r.Stat(rev)
+		if err != nil {
+			return cached{nil, err}
+		}
+		rev = info.Version
+
 		text, err = r.r.GoMod(rev)
+		checkGoMod(r.path, rev, text)
 		if err == nil {
 			if err := writeDiskGoMod(file, text); err != nil {
 				fmt.Fprintf(os.Stderr, "go: writing go.mod cache: %v\n", err)
@@ -267,6 +277,10 @@
 		data = nil
 	}
 
+	if err == nil {
+		checkGoMod(path, rev, data)
+	}
+
 	return file, data, err
 }
 
diff --git a/vendor/cmd/go/internal/modfetch/fetch.go b/vendor/cmd/go/internal/modfetch/fetch.go
index 7f82e4a..984acae 100644
--- a/vendor/cmd/go/internal/modfetch/fetch.go
+++ b/vendor/cmd/go/internal/modfetch/fetch.go
@@ -81,6 +81,7 @@
 	if err != nil {
 		return err
 	}
+	checkOneSum(mod, hash) // check before installing the zip file
 	r, err := os.Open(tmpfile)
 	if err != nil {
 		return err
@@ -163,11 +164,35 @@
 
 	data, err := ioutil.ReadFile(filepath.Join(SrcMod, "cache/download", mod.Path, "@v", mod.Version+".ziphash"))
 	if err != nil {
-		base.Fatalf("vgo: verifying %s %s: %v", mod.Path, mod.Version, err)
+		base.Fatalf("vgo: verifying %s@%s: %v", mod.Path, mod.Version, err)
 	}
 	h := strings.TrimSpace(string(data))
 	if !strings.HasPrefix(h, "h1:") {
-		base.Fatalf("vgo: verifying %s %s: unexpected ziphash: %q", mod.Path, mod.Version, h)
+		base.Fatalf("vgo: verifying %s@%s: unexpected ziphash: %q", mod.Path, mod.Version, h)
+	}
+
+	checkOneSum(mod, h)
+}
+
+func checkGoMod(path, version string, data []byte) {
+	initGoSum()
+	if !useGoSum {
+		return
+	}
+
+	h, err := dirhash.Hash1([]string{"go.mod"}, func(string) (io.ReadCloser, error) {
+		return ioutil.NopCloser(bytes.NewReader(data)), nil
+	})
+	if err != nil {
+		base.Fatalf("vgo: verifying %s %s go.mod: %v", path, version, err)
+	}
+	checkOneSum(module.Version{Path: path, Version: version + "/go.mod"}, h)
+}
+
+func checkOneSum(mod module.Version, h string) {
+	initGoSum()
+	if !useGoSum {
+		return
 	}
 
 	for _, vh := range goSum[mod] {
@@ -175,11 +200,11 @@
 			return
 		}
 		if strings.HasPrefix(vh, "h1:") {
-			base.Fatalf("vgo: verifying %s %s: module hash mismatch\n\tdownloaded: %v\n\tgo.sum:     %v", mod.Path, mod.Version, h, vh)
+			base.Fatalf("vgo: verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\tgo.sum:     %v", mod.Path, mod.Version, h, vh)
 		}
 	}
 	if len(goSum[mod]) > 0 {
-		fmt.Fprintf(os.Stderr, "warning: verifying %s %s: unknown hashes in go.sum: %v; adding %v", mod.Path, mod.Version, strings.Join(goSum[mod], ", "), h)
+		fmt.Fprintf(os.Stderr, "warning: verifying %s@%s: unknown hashes in go.sum: %v; adding %v", mod.Path, mod.Version, strings.Join(goSum[mod], ", "), h)
 	}
 	goSum[mod] = append(goSum[mod], h)
 }
diff --git a/vendor/cmd/go/internal/module/module.go b/vendor/cmd/go/internal/module/module.go
index 93e8cb6..7fad513 100644
--- a/vendor/cmd/go/internal/module/module.go
+++ b/vendor/cmd/go/internal/module/module.go
@@ -210,6 +210,21 @@
 		if mi.Path != mj.Path {
 			return mi.Path < mj.Path
 		}
-		return semver.Compare(mi.Version, mj.Version) < 0
+		// To help go.sum formatting, allow version/file.
+		// Compare semver prefix by semver rules,
+		// file by string order.
+		vi := mi.Version
+		vj := mj.Version
+		var fi, fj string
+		if k := strings.Index(vi, "/"); k >= 0 {
+			vi, fi = vi[:k], vi[k:]
+		}
+		if k := strings.Index(vj, "/"); k >= 0 {
+			vj, fj = vj[:k], vj[k:]
+		}
+		if vi != vj {
+			return semver.Compare(vi, vj) < 0
+		}
+		return fi < fj
 	})
 }
diff --git a/vendor/cmd/go/internal/vgo/init.go b/vendor/cmd/go/internal/vgo/init.go
index a88633a..5352fd3 100644
--- a/vendor/cmd/go/internal/vgo/init.go
+++ b/vendor/cmd/go/internal/vgo/init.go
@@ -189,6 +189,7 @@
 	}
 
 	modfetch.SrcMod = SrcMod
+	modfetch.GoSumFile = filepath.Join(ModRoot, "go.sum")
 	codehost.WorkRoot = filepath.Join(SrcMod, "cache/vcs")
 
 	if CmdModInit {
diff --git a/vendor/cmd/go/vgo_test.go b/vendor/cmd/go/vgo_test.go
index db8feea..9b240fc 100644
--- a/vendor/cmd/go/vgo_test.go
+++ b/vendor/cmd/go/vgo_test.go
@@ -12,6 +12,7 @@
 	"path/filepath"
 	"runtime"
 	"sort"
+	"strings"
 	"testing"
 
 	"cmd/go/internal/modconv"
@@ -660,24 +661,60 @@
 	tg.grepStdout("v0.6.0", "expected github.com/pkg/errors at v0.6.0")
 }
 
-func TestVerifyNotDownloaded(t *testing.T) {
+func TestVerify(t *testing.T) {
 	testenv.MustHaveExternalNetwork(t)
 	tg := testgo(t)
 	defer tg.cleanup()
 	tg.makeTempdir()
-	tg.setenv("GOPATH", tg.path("gp"))
+	gopath := tg.path("gp")
+	tg.setenv("GOPATH", gopath)
 	tg.must(os.MkdirAll(tg.path("x"), 0777))
 	tg.must(ioutil.WriteFile(tg.path("x/go.mod"), []byte(`
 		module x
 		require github.com/pkg/errors v0.8.0
 	`), 0666))
+	tg.must(ioutil.WriteFile(tg.path("x/x.go"), []byte(`package x; import _ "github.com/pkg/errors"`), 0666))
+
+	// With correct go.sum,verify succeeds but avoids download.
 	tg.must(ioutil.WriteFile(tg.path("x/go.sum"), []byte(`github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
 `), 0666))
-	tg.must(ioutil.WriteFile(tg.path("x/x.go"), []byte(`package x`), 0666))
 	tg.cd(tg.path("x"))
 	tg.run("-vgo", "mod", "-verify")
-	tg.mustNotExist(filepath.Join(tg.path("gp"), "/src/mod/cache/github.com/pkg/errors/@v/v0.8.0.zip"))
-	tg.mustNotExist(filepath.Join(tg.path("gp"), "/src/mod/github.com/pkg"))
+	tg.mustNotExist(filepath.Join(gopath, "src/mod/cache/download/github.com/pkg/errors/@v/v0.8.0.zip"))
+	tg.mustNotExist(filepath.Join(gopath, "src/mod/github.com/pkg"))
+
+	// With incorrect sum, sync (which must download) fails.
+	// Even if the incorrect sum is in the old legacy go.modverify file.
+	tg.must(ioutil.WriteFile(tg.path("x/go.sum"), []byte(`
+`), 0666))
+	tg.must(ioutil.WriteFile(tg.path("x/go.modverify"), []byte(`github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf1+qw=
+`), 0666))
+	tg.runFail("-vgo", "mod", "-sync") // downloads pkg/errors
+	tg.grepStderr("checksum mismatch", "must detect mismatch")
+	tg.mustNotExist(filepath.Join(gopath, "src/mod/cache/download/github.com/pkg/errors/@v/v0.8.0.zip"))
+	tg.mustNotExist(filepath.Join(gopath, "src/mod/github.com/pkg"))
+
+	// With corrected sum, sync works.
+	tg.must(ioutil.WriteFile(tg.path("x/go.modverify"), []byte(`github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
+`), 0666))
+	tg.run("-vgo", "mod", "-sync")
+	tg.mustExist(filepath.Join(gopath, "src/mod/cache/download/github.com/pkg/errors/@v/v0.8.0.zip"))
+	tg.mustExist(filepath.Join(gopath, "src/mod/github.com/pkg"))
+	tg.mustNotExist(tg.path("x/go.modverify")) // moved into go.sum
+
+	// Sync should have added sum for go.mod.
+	data, err := ioutil.ReadFile(tg.path("x/go.sum"))
+	if !strings.Contains(string(data), "\ngithub.com/pkg/errors v0.8.0/go.mod ") {
+		t.Fatalf("cannot find go.mod hash in go.sum: %v\n%s", err, data)
+	}
+
+	// Even the most basic attempt to load the module graph should detect incorrect go.mod files.
+	tg.run("-vgo", "mod", "-graph") // loads module graph, is OK
+	tg.must(ioutil.WriteFile(tg.path("x/go.sum"), []byte(`github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl1=
+`), 0666))
+	tg.runFail("-vgo", "mod", "-graph") // loads module graph, fails (even though sum is in old go.modverify file)
+	tg.grepStderr("go.mod: checksum mismatch", "must detect mismatch")
 }
 
 func TestVendorWithoutDeps(t *testing.T) {