1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package sumdb
6
7import (
8	"bytes"
9	"fmt"
10	"strings"
11	"sync"
12	"testing"
13
14	"golang.org/x/mod/sumdb/note"
15	"golang.org/x/mod/sumdb/tlog"
16)
17
18const (
19	testName        = "localhost.localdev/sumdb"
20	testVerifierKey = "localhost.localdev/sumdb+00000c67+AcTrnkbUA+TU4heY3hkjiSES/DSQniBqIeQ/YppAUtK6"
21	testSignerKey   = "PRIVATE+KEY+localhost.localdev/sumdb+00000c67+AXu6+oaVaOYuQOFrf1V59JK1owcFlJcHwwXHDfDGxSPk"
22)
23
24func TestClientLookup(t *testing.T) {
25	tc := newTestClient(t)
26	tc.mustHaveLatest(1)
27
28	// Basic lookup.
29	tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
30	tc.mustHaveLatest(3)
31
32	// Everything should now be cached, both for the original package and its /go.mod.
33	tc.getOK = false
34	tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
35	tc.mustLookup("rsc.io/sampler", "v1.3.0/go.mod", "rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=")
36	tc.mustHaveLatest(3)
37	tc.getOK = true
38	tc.getTileOK = false // the cache has what we need
39
40	// Lookup with multiple returned lines.
41	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")
42	tc.mustHaveLatest(3)
43
44	// Lookup with need for !-encoding.
45	// rsc.io/Quote is the only record written after rsc.io/samper,
46	// so it is the only one that should need more tiles.
47	tc.getTileOK = true
48	tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=")
49	tc.mustHaveLatest(4)
50}
51
52func TestClientBadTiles(t *testing.T) {
53	tc := newTestClient(t)
54
55	flipBits := func() {
56		for url, data := range tc.remote {
57			if strings.Contains(url, "/tile/") {
58				for i := range data {
59					data[i] ^= 0x80
60				}
61			}
62		}
63	}
64
65	// Bad tiles in initial download.
66	tc.mustHaveLatest(1)
67	flipBits()
68	_, err := tc.client.Lookup("rsc.io/sampler", "v1.3.0")
69	tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile")
70	flipBits()
71	tc.newClient()
72	tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
73
74	// Bad tiles after initial download.
75	flipBits()
76	_, err = tc.client.Lookup("rsc.io/Quote", "v1.5.2")
77	tc.mustError(err, "rsc.io/Quote@v1.5.2: checking tree#3 against tree#4: downloaded inconsistent tile")
78	flipBits()
79	tc.newClient()
80	tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=")
81
82	// Bad starting tree hash looks like bad tiles.
83	tc.newClient()
84	text := tlog.FormatTree(tlog.Tree{N: 1, Hash: tlog.Hash{}})
85	data, err := note.Sign(&note.Note{Text: string(text)}, tc.signer)
86	if err != nil {
87		tc.t.Fatal(err)
88	}
89	tc.config[testName+"/latest"] = data
90	_, err = tc.client.Lookup("rsc.io/sampler", "v1.3.0")
91	tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile")
92}
93
94func TestClientFork(t *testing.T) {
95	tc := newTestClient(t)
96	tc2 := tc.fork()
97
98	tc.addRecord("rsc.io/pkg1@v1.5.2", `rsc.io/pkg1 v1.5.2 h1:hash!=
99`)
100	tc.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!=
101`)
102	tc.mustLookup("rsc.io/pkg1", "v1.5.2", "rsc.io/pkg1 v1.5.2 h1:hash!=")
103
104	tc2.addRecord("rsc.io/pkg1@v1.5.3", `rsc.io/pkg1 v1.5.3 h1:hash!=
105`)
106	tc2.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!=
107`)
108	tc2.mustLookup("rsc.io/pkg1", "v1.5.4", "rsc.io/pkg1 v1.5.4 h1:hash!=")
109
110	key := "/lookup/rsc.io/pkg1@v1.5.2"
111	tc2.remote[key] = tc.remote[key]
112	_, err := tc2.client.Lookup("rsc.io/pkg1", "v1.5.2")
113	tc2.mustError(err, ErrSecurity.Error())
114
115	/*
116	   SECURITY ERROR
117	   go.sum database server misbehavior detected!
118
119	   old database:
120	   	go.sum database tree!
121	   	5
122	   	nWzN20+pwMt62p7jbv1/NlN95ePTlHijabv5zO/s36w=
123
124localhost.localdev/sumdb AAAMZ5/2FVAdMH58kmnz/0h299pwyskEbzDzoa2/YaPdhvLya4YWDFQQxu2TQb5GpwAH4NdWnTwuhILafisyf3CNbgg=
125
126	   new database:
127	   	go.sum database tree
128	   	6
129	   	wc4SkQt52o5W2nQ8To2ARs+mWuUJjss+sdleoiqxMmM=
130
131localhost.localdev/sumdb AAAMZ6oRNswlEZ6ZZhxrCvgl1MBy+nusq4JU+TG6Fe2NihWLqOzb+y2c2kzRLoCr4tvw9o36ucQEnhc20e4nA4Qc/wc=
132
133	   proof of misbehavior:
134	   	T7i+H/8ER4nXOiw4Bj0koZOkGjkxoNvlI34GpvhHhQg=
135	   	Nsuejv72de9hYNM5bqFv8rv3gm3zJQwv/DT/WNbLDLA=
136	   	mOmqqZ1aI/lzS94oq/JSbj7pD8Rv9S+xDyi12BtVSHo=
137	   	/7Aw5jVSMM9sFjQhaMg+iiDYPMk6decH7QLOGrL9Lx0=
138	*/
139
140	wants := []string{
141		"SECURITY ERROR",
142		"go.sum database server misbehavior detected!",
143		"old database:\n\tgo.sum database tree\n\t5\n",
144		"— localhost.localdev/sumdb AAAMZ5/2FVAd",
145		"new database:\n\tgo.sum database tree\n\t6\n",
146		"— localhost.localdev/sumdb AAAMZ6oRNswl",
147		"proof of misbehavior:\n\tT7i+H/8ER4nXOiw4Bj0k",
148	}
149	text := tc2.security.String()
150	for _, want := range wants {
151		if !strings.Contains(text, want) {
152			t.Fatalf("cannot find %q in security text:\n%s", want, text)
153		}
154	}
155}
156
157func TestClientGONOSUMDB(t *testing.T) {
158	tc := newTestClient(t)
159	tc.client.SetGONOSUMDB("p,*/q")
160	tc.client.Lookup("rsc.io/sampler", "v1.3.0") // initialize before we turn off network
161	tc.getOK = false
162
163	ok := []string{
164		"abc",
165		"a/p",
166		"pq",
167		"q",
168		"n/o/p/q",
169	}
170	skip := []string{
171		"p",
172		"p/x",
173		"x/q",
174		"x/q/z",
175	}
176
177	for _, path := range ok {
178		_, err := tc.client.Lookup(path, "v1.0.0")
179		if err == ErrGONOSUMDB {
180			t.Errorf("Lookup(%q): ErrGONOSUMDB, wanted failed actual lookup", path)
181		}
182	}
183	for _, path := range skip {
184		_, err := tc.client.Lookup(path, "v1.0.0")
185		if err != ErrGONOSUMDB {
186			t.Errorf("Lookup(%q): %v, wanted ErrGONOSUMDB", path, err)
187		}
188	}
189}
190
191// A testClient is a self-contained client-side testing environment.
192type testClient struct {
193	t          *testing.T // active test
194	client     *Client    // client being tested
195	tileHeight int        // tile height to use (default 2)
196	getOK      bool       // should tc.GetURL succeed?
197	getTileOK  bool       // should tc.GetURL of tiles succeed?
198	treeSize   int64
199	hashes     []tlog.Hash
200	remote     map[string][]byte
201	signer     note.Signer
202
203	// mu protects config, cache, log, security
204	// during concurrent use of the exported methods
205	// by the client itself (testClient is the Client's ClientOps,
206	// and the Client methods can both read and write these fields).
207	// Unexported methods invoked directly by the test
208	// (for example, addRecord) need not hold the mutex:
209	// for proper test execution those methods should only
210	// be called when the Client is idle and not using its ClientOps.
211	// Not holding the mutex in those methods ensures
212	// that if a mistake is made, go test -race will report it.
213	// (Holding the mutex would eliminate the race report but
214	// not the underlying problem.)
215	// Similarly, the get map is not protected by the mutex,
216	// because the Client methods only read it.
217	mu       sync.Mutex // prot
218	config   map[string][]byte
219	cache    map[string][]byte
220	security bytes.Buffer
221}
222
223// newTestClient returns a new testClient that will call t.Fatal on error
224// and has a few records already available on the remote server.
225func newTestClient(t *testing.T) *testClient {
226	tc := &testClient{
227		t:          t,
228		tileHeight: 2,
229		getOK:      true,
230		getTileOK:  true,
231		config:     make(map[string][]byte),
232		cache:      make(map[string][]byte),
233		remote:     make(map[string][]byte),
234	}
235
236	tc.config["key"] = []byte(testVerifierKey + "\n")
237	var err error
238	tc.signer, err = note.NewSigner(testSignerKey)
239	if err != nil {
240		t.Fatal(err)
241	}
242
243	tc.newClient()
244
245	tc.addRecord("rsc.io/quote@v1.5.2", `rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
246rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
247rsc.io/quote v1.5.2 h2:xyzzy
248`)
249
250	tc.addRecord("golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", `golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
251golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
252`)
253	tc.addRecord("rsc.io/sampler@v1.3.0", `rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
254rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
255`)
256	tc.config[testName+"/latest"] = tc.signTree(1)
257
258	tc.addRecord("rsc.io/!quote@v1.5.2", `rsc.io/Quote v1.5.2 h1:uppercase!=
259`)
260	return tc
261}
262
263// newClient resets the Client associated with tc.
264// This clears any in-memory cache from the Client
265// but not tc's on-disk cache.
266func (tc *testClient) newClient() {
267	tc.client = NewClient(tc)
268	tc.client.SetTileHeight(tc.tileHeight)
269}
270
271// mustLookup does a lookup for path@vers and checks that the lines that come back match want.
272func (tc *testClient) mustLookup(path, vers, want string) {
273	tc.t.Helper()
274	lines, err := tc.client.Lookup(path, vers)
275	if err != nil {
276		tc.t.Fatal(err)
277	}
278	if strings.Join(lines, "\n") != want {
279		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))
280	}
281}
282
283// mustHaveLatest checks that the on-disk configuration
284// for latest is a tree of size n.
285func (tc *testClient) mustHaveLatest(n int64) {
286	tc.t.Helper()
287
288	latest := tc.config[testName+"/latest"]
289	lines := strings.Split(string(latest), "\n")
290	if len(lines) < 2 || lines[1] != fmt.Sprint(n) {
291		tc.t.Fatalf("/latest should have tree %d, but has:\n%s", n, latest)
292	}
293}
294
295// mustError checks that err's error string contains the text.
296func (tc *testClient) mustError(err error, text string) {
297	tc.t.Helper()
298	if err == nil || !strings.Contains(err.Error(), text) {
299		tc.t.Fatalf("err = %v, want %q", err, text)
300	}
301}
302
303// fork returns a copy of tc.
304// Changes made to the new copy or to tc are not reflected in the other.
305func (tc *testClient) fork() *testClient {
306	tc2 := &testClient{
307		t:          tc.t,
308		getOK:      tc.getOK,
309		getTileOK:  tc.getTileOK,
310		tileHeight: tc.tileHeight,
311		treeSize:   tc.treeSize,
312		hashes:     append([]tlog.Hash{}, tc.hashes...),
313		signer:     tc.signer,
314		config:     copyMap(tc.config),
315		cache:      copyMap(tc.cache),
316		remote:     copyMap(tc.remote),
317	}
318	tc2.newClient()
319	return tc2
320}
321
322func copyMap(m map[string][]byte) map[string][]byte {
323	m2 := make(map[string][]byte)
324	for k, v := range m {
325		m2[k] = v
326	}
327	return m2
328}
329
330// ReadHashes is tc's implementation of tlog.HashReader, for use with
331// tlog.TreeHash and so on.
332func (tc *testClient) ReadHashes(indexes []int64) ([]tlog.Hash, error) {
333	var list []tlog.Hash
334	for _, id := range indexes {
335		list = append(list, tc.hashes[id])
336	}
337	return list, nil
338}
339
340// addRecord adds a log record using the given (!-encoded) key and data.
341func (tc *testClient) addRecord(key, data string) {
342	tc.t.Helper()
343
344	// Create record, add hashes to log tree.
345	id := tc.treeSize
346	tc.treeSize++
347	rec, err := tlog.FormatRecord(id, []byte(data))
348	if err != nil {
349		tc.t.Fatal(err)
350	}
351	hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash([]byte(data)), tc)
352	if err != nil {
353		tc.t.Fatal(err)
354	}
355	tc.hashes = append(tc.hashes, hashes...)
356
357	// Create lookup result.
358	tc.remote["/lookup/"+key] = append(rec, tc.signTree(tc.treeSize)...)
359
360	// Create new tiles.
361	tiles := tlog.NewTiles(tc.tileHeight, id, tc.treeSize)
362	for _, tile := range tiles {
363		data, err := tlog.ReadTileData(tile, tc)
364		if err != nil {
365			tc.t.Fatal(err)
366		}
367		tc.remote["/"+tile.Path()] = data
368		// TODO delete old partial tiles
369	}
370}
371
372// signTree returns the signed head for the tree of the given size.
373func (tc *testClient) signTree(size int64) []byte {
374	h, err := tlog.TreeHash(size, tc)
375	if err != nil {
376		tc.t.Fatal(err)
377	}
378	text := tlog.FormatTree(tlog.Tree{N: size, Hash: h})
379	data, err := note.Sign(&note.Note{Text: string(text)}, tc.signer)
380	if err != nil {
381		tc.t.Fatal(err)
382	}
383	return data
384}
385
386// ReadRemote is for tc's implementation of Client.
387func (tc *testClient) ReadRemote(path string) ([]byte, error) {
388	// No mutex here because only the Client should be running
389	// and the Client cannot change tc.get.
390	if !tc.getOK {
391		return nil, fmt.Errorf("disallowed remote read %s", path)
392	}
393	if strings.Contains(path, "/tile/") && !tc.getTileOK {
394		return nil, fmt.Errorf("disallowed remote tile read %s", path)
395	}
396
397	data, ok := tc.remote[path]
398	if !ok {
399		return nil, fmt.Errorf("no remote path %s", path)
400	}
401	return data, nil
402}
403
404// ReadConfig is for tc's implementation of Client.
405func (tc *testClient) ReadConfig(file string) ([]byte, error) {
406	tc.mu.Lock()
407	defer tc.mu.Unlock()
408
409	data, ok := tc.config[file]
410	if !ok {
411		return nil, fmt.Errorf("no config %s", file)
412	}
413	return data, nil
414}
415
416// WriteConfig is for tc's implementation of Client.
417func (tc *testClient) WriteConfig(file string, old, new []byte) error {
418	tc.mu.Lock()
419	defer tc.mu.Unlock()
420
421	data := tc.config[file]
422	if !bytes.Equal(old, data) {
423		return ErrWriteConflict
424	}
425	tc.config[file] = new
426	return nil
427}
428
429// ReadCache is for tc's implementation of Client.
430func (tc *testClient) ReadCache(file string) ([]byte, error) {
431	tc.mu.Lock()
432	defer tc.mu.Unlock()
433
434	data, ok := tc.cache[file]
435	if !ok {
436		return nil, fmt.Errorf("no cache %s", file)
437	}
438	return data, nil
439}
440
441// WriteCache is for tc's implementation of Client.
442func (tc *testClient) WriteCache(file string, data []byte) {
443	tc.mu.Lock()
444	defer tc.mu.Unlock()
445
446	tc.cache[file] = data
447}
448
449// Log is for tc's implementation of Client.
450func (tc *testClient) Log(msg string) {
451	tc.t.Log(msg)
452}
453
454// SecurityError is for tc's implementation of Client.
455func (tc *testClient) SecurityError(msg string) {
456	tc.mu.Lock()
457	defer tc.mu.Unlock()
458
459	fmt.Fprintf(&tc.security, "%s\n", strings.TrimRight(msg, "\n"))
460}
461