Skip to content

Commit 8603887

Browse files
committed
notary/internal/notecheck: check go.sum against notary
This is part of a design sketch for a Go module notary. Eventually the code will live outside golang.org/x/exp. Everything here is subject to change! Don't depend on it! This is a demo of using the notary URL endpoints to validate go.sum entries. The real implementation in cmd/go would cache both the tree head and downloaded tiles. Change-Id: I6715cbe40e07fbab0c5cb9ab20e366195b66e6da Reviewed-on: https://go-review.googlesource.com/c/exp/+/162899 Run-TryBot: Russ Cox <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]>
1 parent 6983f9a commit 8603887

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-0
lines changed

notary/internal/notecheck/main.go

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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+
5+
// Notecheck checks a go.sum file against a notary.
6+
//
7+
// WARNING! This program is meant as a proof of concept demo and
8+
// should not be used in production scripts.
9+
// It does not set an exit status to report whether the
10+
// checksums matched, and it does not filter the go.sum
11+
// according to the $GONOVERIFY environment variable.
12+
//
13+
// Usage:
14+
//
15+
// notecheck [-v] notary-key go.sum
16+
//
17+
// The -v flag enables verbose output.
18+
//
19+
package main
20+
21+
import (
22+
"bytes"
23+
"encoding/hex"
24+
"flag"
25+
"fmt"
26+
"io/ioutil"
27+
"log"
28+
"net/http"
29+
"os"
30+
"strconv"
31+
"strings"
32+
"sync"
33+
"time"
34+
35+
"golang.org/x/exp/notary/internal/note"
36+
"golang.org/x/exp/notary/internal/tlog"
37+
)
38+
39+
func usage() {
40+
fmt.Fprintf(os.Stderr, "usage: notecheck [-u url] [-h H] [-v] notary-key go.sum...\n")
41+
os.Exit(2)
42+
}
43+
44+
var height = flag.Int("h", 2, "tile height")
45+
var vflag = flag.Bool("v", false, "enable verbose output")
46+
var url = flag.String("u", "", "url to notary (overriding name)")
47+
48+
func main() {
49+
log.SetPrefix("notecheck: ")
50+
log.SetFlags(0)
51+
52+
flag.Usage = usage
53+
flag.Parse()
54+
if flag.NArg() < 2 {
55+
usage()
56+
}
57+
58+
vkey := flag.Arg(0)
59+
verifier, err := note.NewVerifier(vkey)
60+
if err != nil {
61+
log.Fatal(err)
62+
}
63+
if *url == "" {
64+
*url = "https://" + verifier.Name()
65+
}
66+
msg, err := httpGet(*url + "/latest")
67+
if err != nil {
68+
log.Fatal(err)
69+
}
70+
treeNote, err := note.Open(msg, note.NotaryList(verifier))
71+
if err != nil {
72+
log.Fatalf("reading note: %v\nnote:\n%s", err, msg)
73+
}
74+
tree, err := tlog.ParseTree([]byte(treeNote.Text))
75+
if err != nil {
76+
log.Fatal(err)
77+
}
78+
79+
if *vflag {
80+
log.Printf("validating against %s @%d", verifier.Name(), tree.N)
81+
}
82+
83+
verifierURL := *url
84+
tr := &tileReader{url: verifierURL + "/"}
85+
thr := tlog.TileHashReader(tree, tr)
86+
if _, err := tlog.TreeHash(tree.N, thr); err != nil {
87+
log.Fatal(err)
88+
}
89+
90+
for _, arg := range flag.Args()[1:] {
91+
data, err := ioutil.ReadFile(arg)
92+
if err != nil {
93+
log.Fatal(err)
94+
}
95+
log.SetPrefix("notecheck: " + arg + ": ")
96+
checkGoSum(data, verifierURL, thr)
97+
log.SetPrefix("notecheck: ")
98+
}
99+
}
100+
101+
func checkGoSum(data []byte, verifierURL string, thr tlog.HashReader) {
102+
lines := strings.SplitAfter(string(data), "\n")
103+
if lines[len(lines)-1] != "" {
104+
log.Printf("error: final line missing newline")
105+
return
106+
}
107+
lines = lines[:len(lines)-1]
108+
if len(lines)%2 != 0 {
109+
log.Printf("error: odd number of lines")
110+
}
111+
for i := 0; i+2 <= len(lines); i += 2 {
112+
f1 := strings.Fields(lines[i])
113+
f2 := strings.Fields(lines[i+1])
114+
if len(f1) != 3 || len(f2) != 3 || f1[0] != f2[0] || f1[1]+"/go.mod" != f2[1] {
115+
log.Printf("error: bad line pair:\n\t%s\t%s", lines[i], lines[i+1])
116+
continue
117+
}
118+
119+
data, err := httpGet(verifierURL + "/lookup/" + f1[0] + "@" + f1[1])
120+
if err != nil {
121+
log.Printf("%s@%s: %v", f1[0], f1[1], err)
122+
continue
123+
}
124+
j := bytes.IndexByte(data, '\n')
125+
if j < 0 {
126+
log.Printf("%s@%s: short response from lookup", f1[0], f1[1])
127+
continue
128+
}
129+
id, err := strconv.ParseInt(strings.TrimSpace(string(data[:j])), 10, 64)
130+
if err != nil {
131+
log.Printf("%s@%s: unexpected response:\n%s", f1[0], f1[1], data)
132+
continue
133+
}
134+
ldata := data[j+1:]
135+
136+
c := make(chan *tlog.Hash, 1)
137+
go func() {
138+
hashes, err := thr.ReadHashes([]int64{tlog.StoredHashIndex(0, id)})
139+
if err != nil {
140+
log.Printf("%s@%s: %v", f1[0], f1[1], err)
141+
c <- nil
142+
return
143+
}
144+
c <- &hashes[0]
145+
}()
146+
147+
// The record lookup can be skipped in favor of using the /lookup response
148+
// but we fetch record and test that they match, to check the server.
149+
data, err = httpGet(verifierURL + "/record/" + fmt.Sprint(id))
150+
if err != nil {
151+
log.Printf("%s@%s: %v", f1[0], f1[1], err)
152+
continue
153+
}
154+
if !bytes.Equal(data, ldata) {
155+
log.Printf("%s@%s: different data from lookup and record:\n%s\n%s", hex.Dump(ldata), hex.Dump(data))
156+
continue
157+
}
158+
159+
hash := tlog.RecordHash(data)
160+
hash1 := <-c
161+
if hash1 == nil {
162+
continue
163+
}
164+
if *hash1 != hash {
165+
log.Printf("%s@%s: inconsistent records on notary!", f1[0], f1[1])
166+
continue
167+
}
168+
if string(data) != lines[i]+lines[i+1] {
169+
log.Printf("%s@%s: invalid go.sum entries:\nhave:\n\t%s\t%swant:\n\t%s", f1[0], f1[1], lines[i], lines[i+1], strings.Replace(string(data), "\n", "\n\t", -1))
170+
}
171+
}
172+
}
173+
174+
func init() {
175+
http.DefaultClient.Timeout = 10 * time.Second
176+
}
177+
178+
func httpGet(url string) ([]byte, error) {
179+
start := time.Now()
180+
resp, err := http.Get(url)
181+
if err != nil {
182+
return nil, err
183+
}
184+
defer resp.Body.Close()
185+
if resp.StatusCode != 200 {
186+
return nil, fmt.Errorf("GET %v: %v", url, resp.Status)
187+
}
188+
data, err := ioutil.ReadAll(resp.Body)
189+
if err != nil {
190+
return nil, err
191+
}
192+
if *vflag {
193+
fmt.Fprintf(os.Stderr, "%.3fs %s\n", time.Since(start).Seconds(), url)
194+
}
195+
return data, nil
196+
}
197+
198+
type tileReader struct {
199+
url string
200+
cache map[tlog.Tile][]byte
201+
cacheMu sync.Mutex
202+
}
203+
204+
func (r *tileReader) Height() int {
205+
return *height
206+
}
207+
208+
func (r *tileReader) Reject(tile tlog.Tile) {
209+
log.Printf("tile rejected: %v", tile.Path())
210+
}
211+
212+
// TODO(rsc): Move some variant of this to package tlog
213+
// once we are more sure of the API.
214+
215+
func (r *tileReader) ReadTiles(tiles []tlog.Tile) ([][]byte, error) {
216+
var wg sync.WaitGroup
217+
out := make([][]byte, len(tiles))
218+
errs := make([]error, len(tiles))
219+
r.cacheMu.Lock()
220+
if r.cache == nil {
221+
r.cache = make(map[tlog.Tile][]byte)
222+
}
223+
for i, tile := range tiles {
224+
if data := r.cache[tile]; data != nil {
225+
out[i] = data
226+
continue
227+
}
228+
wg.Add(1)
229+
go func(i int, tile tlog.Tile) {
230+
defer wg.Done()
231+
data, err := httpGet(r.url + tile.Path())
232+
if err != nil && tile.W != 1<<uint(tile.H) {
233+
fullTile := tile
234+
fullTile.W = 1 << uint(tile.H)
235+
if fullData, err1 := httpGet(r.url + fullTile.Path()); err1 == nil {
236+
data = fullData[:tile.W*tlog.HashSize]
237+
err = nil
238+
}
239+
}
240+
if err != nil {
241+
errs[i] = err
242+
return
243+
}
244+
r.cacheMu.Lock()
245+
r.cache[tile] = data
246+
r.cacheMu.Unlock()
247+
out[i] = data
248+
}(i, tile)
249+
}
250+
r.cacheMu.Unlock()
251+
wg.Wait()
252+
253+
for _, err := range errs {
254+
if err != nil {
255+
return nil, err
256+
}
257+
}
258+
259+
return out, nil
260+
}

notary/internal/notecheck/test.bash

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
set -e
4+
go build -o notecheck.exe
5+
./notecheck.exe "$@" -v rsc-goog.appspot.com+eecb1dec+AbTy1QXWdqYd1TTpuaUqsk6u7p+n4AqLiLB8SBwoB831 test.sum
6+
rm -f ./notecheck.exe

notary/internal/notecheck/test.sum

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
2+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
3+
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
4+
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
5+
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
6+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

0 commit comments

Comments
 (0)