// 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 sumdb import ( "bytes" "fmt" "strings" "sync" "testing" "golang.org/x/mod/sumdb/note" "golang.org/x/mod/sumdb/tlog" ) const ( testName = "localhost.localdev/sumdb" testVerifierKey = "localhost.localdev/sumdb+00000c67+AcTrnkbUA+TU4heY3hkjiSES/DSQniBqIeQ/YppAUtK6" testSignerKey = "PRIVATE+KEY+localhost.localdev/sumdb+00000c67+AXu6+oaVaOYuQOFrf1V59JK1owcFlJcHwwXHDfDGxSPk" ) func TestClientLookup(t *testing.T) { tc := newTestClient(t) tc.mustHaveLatest(1) // Basic lookup. tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=") tc.mustHaveLatest(3) // Everything should now be cached, both for the original package and its /go.mod. tc.getOK = false tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=") tc.mustLookup("rsc.io/sampler", "v1.3.0/go.mod", "rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=") tc.mustHaveLatest(3) tc.getOK = true tc.getTileOK = false // the cache has what we need // Lookup with multiple returned lines. tc.mustLookup("rsc.io/quote", "v1.5.2", "rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=\nrsc.io/quote v1.5.2 h2:xyzzy") tc.mustHaveLatest(3) // Lookup with need for !-encoding. // rsc.io/Quote is the only record written after rsc.io/samper, // so it is the only one that should need more tiles. tc.getTileOK = true tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=") tc.mustHaveLatest(4) } func TestClientBadTiles(t *testing.T) { tc := newTestClient(t) flipBits := func() { for url, data := range tc.remote { if strings.Contains(url, "/tile/") { for i := range data { data[i] ^= 0x80 } } } } // Bad tiles in initial download. tc.mustHaveLatest(1) flipBits() _, err := tc.client.Lookup("rsc.io/sampler", "v1.3.0") tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile") flipBits() tc.newClient() tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=") // Bad tiles after initial download. flipBits() _, err = tc.client.Lookup("rsc.io/Quote", "v1.5.2") tc.mustError(err, "rsc.io/Quote@v1.5.2: checking tree#3 against tree#4: downloaded inconsistent tile") flipBits() tc.newClient() tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=") // Bad starting tree hash looks like bad tiles. tc.newClient() text := tlog.FormatTree(tlog.Tree{N: 1, Hash: tlog.Hash{}}) data, err := note.Sign(¬e.Note{Text: string(text)}, tc.signer) if err != nil { tc.t.Fatal(err) } tc.config[testName+"/latest"] = data _, err = tc.client.Lookup("rsc.io/sampler", "v1.3.0") tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile") } func TestClientFork(t *testing.T) { tc := newTestClient(t) tc2 := tc.fork() tc.addRecord("rsc.io/pkg1@v1.5.2", `rsc.io/pkg1 v1.5.2 h1:hash!= `) tc.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!= `) tc.mustLookup("rsc.io/pkg1", "v1.5.2", "rsc.io/pkg1 v1.5.2 h1:hash!=") tc2.addRecord("rsc.io/pkg1@v1.5.3", `rsc.io/pkg1 v1.5.3 h1:hash!= `) tc2.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!= `) tc2.mustLookup("rsc.io/pkg1", "v1.5.4", "rsc.io/pkg1 v1.5.4 h1:hash!=") key := "/lookup/rsc.io/pkg1@v1.5.2" tc2.remote[key] = tc.remote[key] _, err := tc2.client.Lookup("rsc.io/pkg1", "v1.5.2") tc2.mustError(err, ErrSecurity.Error()) /* SECURITY ERROR go.sum database server misbehavior detected! old database: go.sum database tree! 5 nWzN20+pwMt62p7jbv1/NlN95ePTlHijabv5zO/s36w= — localhost.localdev/sumdb AAAMZ5/2FVAdMH58kmnz/0h299pwyskEbzDzoa2/YaPdhvLya4YWDFQQxu2TQb5GpwAH4NdWnTwuhILafisyf3CNbgg= new database: go.sum database tree 6 wc4SkQt52o5W2nQ8To2ARs+mWuUJjss+sdleoiqxMmM= — localhost.localdev/sumdb AAAMZ6oRNswlEZ6ZZhxrCvgl1MBy+nusq4JU+TG6Fe2NihWLqOzb+y2c2kzRLoCr4tvw9o36ucQEnhc20e4nA4Qc/wc= proof of misbehavior: T7i+H/8ER4nXOiw4Bj0koZOkGjkxoNvlI34GpvhHhQg= Nsuejv72de9hYNM5bqFv8rv3gm3zJQwv/DT/WNbLDLA= mOmqqZ1aI/lzS94oq/JSbj7pD8Rv9S+xDyi12BtVSHo= /7Aw5jVSMM9sFjQhaMg+iiDYPMk6decH7QLOGrL9Lx0= */ wants := []string{ "SECURITY ERROR", "go.sum database server misbehavior detected!", "old database:\n\tgo.sum database tree\n\t5\n", "— localhost.localdev/sumdb AAAMZ5/2FVAd", "new database:\n\tgo.sum database tree\n\t6\n", "— localhost.localdev/sumdb AAAMZ6oRNswl", "proof of misbehavior:\n\tT7i+H/8ER4nXOiw4Bj0k", } text := tc2.security.String() for _, want := range wants { if !strings.Contains(text, want) { t.Fatalf("cannot find %q in security text:\n%s", want, text) } } } func TestClientGONOSUMDB(t *testing.T) { tc := newTestClient(t) tc.client.SetGONOSUMDB("p,*/q") tc.client.Lookup("rsc.io/sampler", "v1.3.0") // initialize before we turn off network tc.getOK = false 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.client.Lookup(path, "v1.0.0") if err == ErrGONOSUMDB { t.Errorf("Lookup(%q): ErrGONOSUMDB, wanted failed actual lookup", path) } } for _, path := range skip { _, err := tc.client.Lookup(path, "v1.0.0") if err != ErrGONOSUMDB { t.Errorf("Lookup(%q): %v, wanted ErrGONOSUMDB", path, err) } } } // A testClient is a self-contained client-side testing environment. type testClient struct { t *testing.T // active test client *Client // client being tested tileHeight int // tile height to use (default 2) getOK bool // should tc.GetURL succeed? getTileOK bool // should tc.GetURL of tiles succeed? treeSize int64 hashes []tlog.Hash remote map[string][]byte signer note.Signer // mu protects config, cache, log, security // during concurrent use of the exported methods // by the client itself (testClient is the Client's ClientOps, // and the Client methods can both read and write these fields). // Unexported methods invoked directly by the test // (for example, addRecord) need not hold the mutex: // for proper test execution those methods should only // be called when the Client is idle and not using its ClientOps. // Not holding the mutex in those methods ensures // that if a mistake is made, go test -race will report it. // (Holding the mutex would eliminate the race report but // not the underlying problem.) // Similarly, the get map is not protected by the mutex, // because the Client methods only read it. mu sync.Mutex // prot config map[string][]byte cache map[string][]byte security bytes.Buffer } // newTestClient returns a new testClient that will call t.Fatal on error // and has a few records already available on the remote server. func newTestClient(t *testing.T) *testClient { tc := &testClient{ t: t, tileHeight: 2, getOK: true, getTileOK: true, config: make(map[string][]byte), cache: make(map[string][]byte), remote: make(map[string][]byte), } tc.config["key"] = []byte(testVerifierKey + "\n") var err error tc.signer, err = note.NewSigner(testSignerKey) if err != nil { t.Fatal(err) } tc.newClient() tc.addRecord("rsc.io/quote@v1.5.2", `rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= rsc.io/quote v1.5.2 h2:xyzzy `) tc.addRecord("golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", `golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= `) tc.addRecord("rsc.io/sampler@v1.3.0", `rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= `) tc.config[testName+"/latest"] = tc.signTree(1) tc.addRecord("rsc.io/!quote@v1.5.2", `rsc.io/Quote v1.5.2 h1:uppercase!= `) return tc } // newClient resets the Client associated with tc. // This clears any in-memory cache from the Client // but not tc's on-disk cache. func (tc *testClient) newClient() { tc.client = NewClient(tc) tc.client.SetTileHeight(tc.tileHeight) } // mustLookup does a lookup for path@vers and checks that the lines that come back match want. func (tc *testClient) mustLookup(path, vers, want string) { tc.t.Helper() lines, err := tc.client.Lookup(path, vers) if err != nil { tc.t.Fatal(err) } if strings.Join(lines, "\n") != want { tc.t.Fatalf("Lookup(%q, %q):\n\t%s\nwant:\n\t%s", path, vers, strings.Join(lines, "\n\t"), strings.Replace(want, "\n", "\n\t", -1)) } } // mustHaveLatest checks that the on-disk configuration // for latest is a tree of size n. func (tc *testClient) mustHaveLatest(n int64) { tc.t.Helper() latest := tc.config[testName+"/latest"] lines := strings.Split(string(latest), "\n") if len(lines) < 2 || lines[1] != fmt.Sprint(n) { tc.t.Fatalf("/latest should have tree %d, but has:\n%s", n, latest) } } // mustError checks that err's error string contains the text. func (tc *testClient) mustError(err error, text string) { tc.t.Helper() if err == nil || !strings.Contains(err.Error(), text) { tc.t.Fatalf("err = %v, want %q", err, text) } } // fork returns a copy of tc. // Changes made to the new copy or to tc are not reflected in the other. func (tc *testClient) fork() *testClient { tc2 := &testClient{ t: tc.t, getOK: tc.getOK, getTileOK: tc.getTileOK, tileHeight: tc.tileHeight, treeSize: tc.treeSize, hashes: append([]tlog.Hash{}, tc.hashes...), signer: tc.signer, config: copyMap(tc.config), cache: copyMap(tc.cache), remote: copyMap(tc.remote), } tc2.newClient() return tc2 } func copyMap(m map[string][]byte) map[string][]byte { m2 := make(map[string][]byte) for k, v := range m { m2[k] = v } return m2 } // ReadHashes is tc's implementation of tlog.HashReader, for use with // tlog.TreeHash and so on. func (tc *testClient) ReadHashes(indexes []int64) ([]tlog.Hash, error) { var list []tlog.Hash for _, id := range indexes { list = append(list, tc.hashes[id]) } return list, nil } // addRecord adds a log record using the given (!-encoded) key and data. func (tc *testClient) addRecord(key, data string) { tc.t.Helper() // Create record, add hashes to log tree. id := tc.treeSize tc.treeSize++ rec, err := tlog.FormatRecord(id, []byte(data)) if err != nil { tc.t.Fatal(err) } hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash([]byte(data)), tc) if err != nil { tc.t.Fatal(err) } tc.hashes = append(tc.hashes, hashes...) // Create lookup result. tc.remote["/lookup/"+key] = append(rec, tc.signTree(tc.treeSize)...) // Create new tiles. tiles := tlog.NewTiles(tc.tileHeight, id, tc.treeSize) for _, tile := range tiles { data, err := tlog.ReadTileData(tile, tc) if err != nil { tc.t.Fatal(err) } tc.remote["/"+tile.Path()] = data // TODO delete old partial tiles } } // signTree returns the signed head for the tree of the given size. func (tc *testClient) signTree(size int64) []byte { h, err := tlog.TreeHash(size, tc) if err != nil { tc.t.Fatal(err) } text := tlog.FormatTree(tlog.Tree{N: size, Hash: h}) data, err := note.Sign(¬e.Note{Text: string(text)}, tc.signer) if err != nil { tc.t.Fatal(err) } return data } // ReadRemote is for tc's implementation of Client. func (tc *testClient) ReadRemote(path string) ([]byte, error) { // No mutex here because only the Client should be running // and the Client cannot change tc.get. if !tc.getOK { return nil, fmt.Errorf("disallowed remote read %s", path) } if strings.Contains(path, "/tile/") && !tc.getTileOK { return nil, fmt.Errorf("disallowed remote tile read %s", path) } data, ok := tc.remote[path] if !ok { return nil, fmt.Errorf("no remote path %s", path) } return data, nil } // ReadConfig is for tc's implementation of Client. func (tc *testClient) ReadConfig(file string) ([]byte, error) { tc.mu.Lock() defer tc.mu.Unlock() data, ok := tc.config[file] if !ok { return nil, fmt.Errorf("no config %s", file) } return data, nil } // WriteConfig is for tc's implementation of Client. func (tc *testClient) WriteConfig(file string, old, new []byte) error { tc.mu.Lock() defer tc.mu.Unlock() data := tc.config[file] if !bytes.Equal(old, data) { return ErrWriteConflict } tc.config[file] = new return nil } // ReadCache is for tc's implementation of Client. func (tc *testClient) ReadCache(file string) ([]byte, error) { tc.mu.Lock() defer tc.mu.Unlock() data, ok := tc.cache[file] if !ok { return nil, fmt.Errorf("no cache %s", file) } return data, nil } // WriteCache is for tc's implementation of Client. func (tc *testClient) WriteCache(file string, data []byte) { tc.mu.Lock() defer tc.mu.Unlock() tc.cache[file] = data } // Log is for tc's implementation of Client. func (tc *testClient) Log(msg string) { tc.t.Log(msg) } // SecurityError is for tc's implementation of Client. func (tc *testClient) SecurityError(msg string) { tc.mu.Lock() defer tc.mu.Unlock() fmt.Fprintf(&tc.security, "%s\n", strings.TrimRight(msg, "\n")) }