blob: 47bc2f5a25956505c533db1a47dc3bad7e8898d0 [file] [log] [blame]
// 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 sumweb
import (
"bytes"
"fmt"
"strings"
"sync"
"testing"
"golang.org/x/exp/notary/internal/note"
"golang.org/x/exp/notary/internal/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 TestConnLookup(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 TestConnBadTiles(t *testing.T) {
tc := newTestClient(t)
flipBits := func() {
for url, data := range tc.get {
if strings.Contains(url, "/tile/") {
for i := range data {
data[i] ^= 0x80
}
}
}
}
// Bad tiles in initial download.
tc.mustHaveLatest(1)
flipBits()
_, err := tc.conn.Lookup("rsc.io/sampler", "v1.3.0")
tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumweb.Conn: checking tree#1: downloaded inconsistent tile")
flipBits()
tc.newConn()
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.conn.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.newConn()
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.newConn()
text := tlog.FormatTree(tlog.Tree{N: 1, Hash: tlog.Hash{}})
data, err := note.Sign(&note.Note{Text: string(text)}, tc.signer)
if err != nil {
tc.t.Fatal(err)
}
tc.config[testName+"/latest"] = data
_, err = tc.conn.Lookup("rsc.io/sampler", "v1.3.0")
tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumweb.Conn: checking tree#1: downloaded inconsistent tile")
}
func TestConnFork(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 := "https://" + testName + "/lookup/rsc.io/pkg1@v1.5.2"
tc2.get[key] = tc.get[key]
_, err := tc2.conn.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)
}
}
}
// A testClient is a self-contained client-side testing environment.
type testClient struct {
t *testing.T // active test
conn *Conn // conn 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
get map[string][]byte
signer note.Signer
// mu protects config, cache, log, security
// during concurrent use of the exported methods
// by the conn itself (testClient is the Conn's Client,
// 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 Conn is idle and not using its Client.
// 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),
get: 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.newConn()
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
}
// newConn resets the Conn associated with tc.
// This clears any in-memory cache from the Conn
// but not tc's on-disk cache.
func (tc *testClient) newConn() {
tc.conn = NewConn(tc)
tc.conn.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.conn.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),
get: copyMap(tc.get),
}
tc2.newConn()
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.get["https://"+testName+"/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.get["https://"+testName+"/"+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(&note.Note{Text: string(text)}, tc.signer)
if err != nil {
tc.t.Fatal(err)
}
return data
}
// GetURL is for tc's implementation of Client.
func (tc *testClient) GetURL(url 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 URL %s", url)
}
if strings.Contains(url, "/tile/") && !tc.getTileOK {
return nil, fmt.Errorf("disallowed Tile URL %s", url)
}
data, ok := tc.get[url]
if !ok {
return nil, fmt.Errorf("no URL %s", url)
}
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"))
}