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 sumweb 6 7import ( 8 "bytes" 9 "fmt" 10 "strings" 11 "sync" 12 "testing" 13 14 "golang.org/x/exp/sumdb/internal/note" 15 "golang.org/x/exp/sumdb/internal/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 TestConnLookup(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 TestConnBadTiles(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.conn.Lookup("rsc.io/sampler", "v1.3.0") 69 tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumweb.Conn: checking tree#1: downloaded inconsistent tile") 70 flipBits() 71 tc.newConn() 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.conn.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.newConn() 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.newConn() 84 text := tlog.FormatTree(tlog.Tree{N: 1, Hash: tlog.Hash{}}) 85 data, err := note.Sign(¬e.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.conn.Lookup("rsc.io/sampler", "v1.3.0") 91 tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumweb.Conn: checking tree#1: downloaded inconsistent tile") 92} 93 94func TestConnFork(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.conn.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 124 — localhost.localdev/sumdb AAAMZ5/2FVAdMH58kmnz/0h299pwyskEbzDzoa2/YaPdhvLya4YWDFQQxu2TQb5GpwAH4NdWnTwuhILafisyf3CNbgg= 125 126 new database: 127 go.sum database tree 128 6 129 wc4SkQt52o5W2nQ8To2ARs+mWuUJjss+sdleoiqxMmM= 130 131 — localhost.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 TestConnGONOSUMDB(t *testing.T) { 158 tc := newTestClient(t) 159 tc.conn.SetGONOSUMDB("p,*/q") 160 tc.conn.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.conn.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.conn.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 conn *Conn // conn 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 conn itself (testClient is the Conn's Client, 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 Conn is idle and not using its Client. 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.newConn() 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// newConn resets the Conn associated with tc. 264// This clears any in-memory cache from the Conn 265// but not tc's on-disk cache. 266func (tc *testClient) newConn() { 267 tc.conn = NewConn(tc) 268 tc.conn.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.conn.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.newConn() 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(¬e.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