notary/internal/sumweb: add GONOVERIFY support

This CL implements respect for the $GONOVERIFY environment
variable as described in golang.org/design/25530-notary.
It also updates gosumcheck to use it.

Change-Id: I937058222eca374a5616feaa83f388f062bb64a2
Reviewed-on: https://go-review.googlesource.com/c/exp/+/172966
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Katie Hockman <katie@golang.org>
diff --git a/notary/gosumcheck/main.go b/notary/gosumcheck/main.go
index dbb0b9e..f6accb1 100644
--- a/notary/gosumcheck/main.go
+++ b/notary/gosumcheck/main.go
@@ -4,12 +4,6 @@
 
 // Gosumcheck checks a go.sum file against a go.sum database server.
 //
-// WARNING! This program is meant as a proof of concept demo and
-// should not be used in production scripts.
-// It does not set an exit status to report whether the
-// checksums matched, and it does not filter the go.sum
-// according to the $GONOVERIFY environment variable.
-//
 // Usage:
 //
 //	gosumcheck [-h H] [-k key] [-u url] [-v] go.sum
@@ -18,12 +12,23 @@
 //
 // The -k flag changes the go.sum database server key.
 //
-// The -u flag overrides the URL of the server.
+// The -u flag overrides the URL of the server (usually set from the key name).
 //
 // The -v flag enables verbose output.
 // In particular, it causes gosumcheck to report
 // the URL and elapsed time for each server request.
 //
+// WARNING! WARNING! WARNING!
+//
+// Gosumcheck is meant as a proof of concept demo and should not be
+// used in production scripts or continuous integration testing.
+// It does not cache any downloaded information from run to run,
+// making it expensive and also keeping it from detecting server
+// misbehavior or successful HTTPS man-in-the-middle timeline forks.
+//
+// To discourage misuse in automated settings, gosumcheck does not
+// set any exit status to report whether any problems were found.
+//
 package main
 
 import (
@@ -34,6 +39,7 @@
 	"log"
 	"net/http"
 	"os"
+	"os/exec"
 	"strings"
 	"sync"
 	"time"
@@ -65,7 +71,20 @@
 
 	conn := sumweb.NewConn(new(client))
 
-	for _, arg := range flag.Args()[1:] {
+	// Look in environment explicitly, so that if 'go env' is old and
+	// doesn't know about GONOVERIFY, we at least get anything
+	// set in the environment.
+	env := os.Getenv("GONOVERIFY")
+	if env == "" {
+		out, err := exec.Command("go", "env", "GONOVERIFY").CombinedOutput()
+		if err != nil {
+			log.Fatalf("go env GONOVERIFY: %v\n%s", err, out)
+		}
+		env = strings.TrimSpace(string(out))
+	}
+	conn.SetGONOVERIFY(env)
+
+	for _, arg := range flag.Args() {
 		data, err := ioutil.ReadFile(arg)
 		if err != nil {
 			log.Fatal(err)
@@ -96,7 +115,12 @@
 
 			dbLines, err := conn.Lookup(f[0], f[1])
 			if err != nil {
-				errs[i] = err.Error()
+				if err == sumweb.ErrGONOVERIFY {
+					errs[i] = fmt.Sprintf("%s@%s: %v", f[0], f[1], err)
+				} else {
+					// Otherwise Lookup properly adds the prefix itself.
+					errs[i] = err.Error()
+				}
 				return
 			}
 			hashAlgPrefix := f[0] + " " + f[1] + " " + f[2][:strings.Index(f[2], ":")+1]
diff --git a/notary/gosumcheck/test.bash b/notary/gosumcheck/test.bash
index bff14b0..4557b03 100755
--- a/notary/gosumcheck/test.bash
+++ b/notary/gosumcheck/test.bash
@@ -2,5 +2,6 @@
 
 set -e
 go build -o gosumcheck.exe
+export GONOVERIFY=*/text # rsc.io/text but not golang.org/x/text
 ./gosumcheck.exe "$@" -v test.sum
 rm -f ./gosumcheck.exe
diff --git a/notary/gosumcheck/test.sum b/notary/gosumcheck/test.sum
index c7105a0..1b252ff 100644
--- a/notary/gosumcheck/test.sum
+++ b/notary/gosumcheck/test.sum
@@ -2,4 +2,5 @@
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
 rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/text v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/notary/internal/sumweb/client.go b/notary/internal/sumweb/client.go
index 8c535b2..cf6891f 100644
--- a/notary/internal/sumweb/client.go
+++ b/notary/internal/sumweb/client.go
@@ -8,6 +8,7 @@
 	"bytes"
 	"errors"
 	"fmt"
+	"path"
 	"strings"
 	"sync"
 
@@ -82,6 +83,7 @@
 	verifiers  note.Verifiers // accepted verifiers (just one, but Verifiers for note.Open)
 	tileReader tileReader
 	tileHeight int
+	noverify   []string
 
 	record    parCache // cache of record lookup, keyed by path@vers
 	tileCache parCache // cache of tile from client.ReadCache, keyed by tile
@@ -159,8 +161,59 @@
 	c.tileHeight = height
 }
 
+// SetGONOVERIFY sets the list of comma-separated GONOVERIFY patterns for the Conn.
+// For any module path matching one of the patterns,
+// Lookup will return ErrGONOVERIFY.
+// Any call to SetGONOVERIFY must happen before the first call to Lookup.
+func (c *Conn) SetGONOVERIFY(list string) {
+	c.noverify = nil
+	for _, glob := range strings.Split(list, ",") {
+		if glob != "" {
+			c.noverify = append(c.noverify, glob)
+		}
+	}
+}
+
+// ErrGONOVERIFY is returned by Lookup for paths that match
+// a pattern listed in the GONOVERIFY list (set by SetGONOVERIFY,
+// usually from the environment variable).
+var ErrGONOVERIFY = errors.New("skipped (listed in GONOVERIFY)")
+
+func (c *Conn) skip(target string) bool {
+	for _, glob := range c.noverify {
+		// A glob with N+1 path elements (N slashes) needs to be matched
+		// against the first N+1 path elements of target,
+		// which end just before the N+1'th slash.
+		n := strings.Count(glob, "/")
+		prefix := target
+		// Walk target, counting slashes, truncating at the N+1'th slash.
+		for i := 0; i < len(target); i++ {
+			if target[i] == '/' {
+				if n == 0 {
+					prefix = target[:i]
+					break
+				}
+				n--
+			}
+		}
+		if n > 0 {
+			// Not enough prefix elements.
+			continue
+		}
+		matched, _ := path.Match(glob, prefix)
+		if matched {
+			return true
+		}
+	}
+	return false
+}
+
 // Lookup returns the go.sum lines for the given module path and version.
 func (c *Conn) Lookup(path, vers string) (lines []string, err error) {
+	if c.skip(path) {
+		return nil, ErrGONOVERIFY
+	}
+
 	defer func() {
 		if err != nil {
 			err = fmt.Errorf("%s@%s: %v", path, vers, err)
diff --git a/notary/internal/sumweb/client_test.go b/notary/internal/sumweb/client_test.go
index 47bc2f5..e500ef4 100644
--- a/notary/internal/sumweb/client_test.go
+++ b/notary/internal/sumweb/client_test.go
@@ -154,6 +154,40 @@
 	}
 }
 
+func TestConnGONOVERIFY(t *testing.T) {
+	tc := newTestClient(t)
+	tc.conn.Lookup("rsc.io/sampler", "v1.3.0") // initialize before we turn off network
+	tc.getOK = false
+	tc.conn.SetGONOVERIFY("p,*/q")
+
+	ok := []string{
+		"abc",
+		"a/p",
+		"pq",
+		"q",
+		"n/o/p/q",
+	}
+	skip := []string{
+		"p",
+		"p/x",
+		"x/q",
+		"x/q/z",
+	}
+
+	for _, path := range ok {
+		_, err := tc.conn.Lookup(path, "v1.0.0")
+		if err == ErrGONOVERIFY {
+			t.Errorf("Lookup(%q): ErrGONOVERIFY, wanted failed actual lookup", path)
+		}
+	}
+	for _, path := range skip {
+		_, err := tc.conn.Lookup(path, "v1.0.0")
+		if err != ErrGONOVERIFY {
+			t.Errorf("Lookup(%q): %v, wanted ErrGONOVERIFY", path, err)
+		}
+	}
+}
+
 // A testClient is a self-contained client-side testing environment.
 type testClient struct {
 	t          *testing.T // active test