1// Package tiles contains methods to work with tlog based verifiable logs. 2package tiles 3 4import ( 5 "crypto/sha256" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "path" 11 "strconv" 12 "strings" 13 14 "golang.org/x/mod/sumdb/tlog" 15) 16 17// HashReader implements tlog.HashReader, reading from tlog-based log located at 18// URL. 19type HashReader struct { 20 URL string 21} 22 23// Domain separation prefix for Merkle tree hashing with second preimage 24// resistance similar to that used in RFC 6962. 25const ( 26 leafHashPrefix = 0 27) 28 29// ReadHashes implements tlog.HashReader's ReadHashes. 30// See: https://pkg.go.dev/golang.org/x/mod/sumdb/tlog#HashReader. 31func (h HashReader) ReadHashes(indices []int64) ([]tlog.Hash, error) { 32 tiles := make(map[string][]byte) 33 hashes := make([]tlog.Hash, 0, len(indices)) 34 for _, index := range indices { 35 // The PixelBT log is tiled at height = 1. 36 tile := tlog.TileForIndex(1, index) 37 38 var content []byte 39 var exists bool 40 var err error 41 content, exists = tiles[tile.Path()] 42 if !exists { 43 content, err = readFromURL(h.URL, tile.Path()) 44 if err != nil { 45 return nil, fmt.Errorf("failed to read from %s: %v", tile.Path(), err) 46 } 47 tiles[tile.Path()] = content 48 } 49 50 hash, err := tlog.HashFromTile(tile, content, index) 51 if err != nil { 52 return nil, fmt.Errorf("failed to read data from tile for index %d: %v", index, err) 53 } 54 hashes = append(hashes, hash) 55 } 56 return hashes, nil 57} 58 59// BinaryInfosIndex returns a map from payload to its index in the 60// transparency log according to the `binaryInfoFilename` value. 61func BinaryInfosIndex(logBaseURL string, binaryInfoFilename string) (map[string]int64, error) { 62 b, err := readFromURL(logBaseURL, binaryInfoFilename) 63 if err != nil { 64 return nil, err 65 } 66 67 binaryInfos := string(b) 68 return parseBinaryInfosIndex(binaryInfos, binaryInfoFilename) 69} 70 71func parseBinaryInfosIndex(binaryInfos string, binaryInfoFilename string) (map[string]int64, error) { 72 m := make(map[string]int64) 73 74 infosStr := strings.Split(binaryInfos, "\n\n") 75 for _, infoStr := range infosStr { 76 pieces := strings.SplitN(infoStr, "\n", 2) 77 if len(pieces) != 2 { 78 return nil, fmt.Errorf("missing newline, malformed %s", binaryInfoFilename) 79 } 80 81 idx, err := strconv.ParseInt(pieces[0], 10, 64) 82 if err != nil { 83 return nil, fmt.Errorf("failed to convert %q to int64", pieces[0]) 84 } 85 86 // Ensure that each log entry does not have extraneous whitespace, but 87 // also terminates with a newline. 88 logEntry := strings.TrimSpace(pieces[1]) + "\n" 89 m[logEntry] = idx 90 } 91 92 return m, nil 93} 94 95func readFromURL(base, suffix string) ([]byte, error) { 96 u, err := url.Parse(base) 97 if err != nil { 98 return nil, fmt.Errorf("invalid URL %s: %v", base, err) 99 } 100 u.Path = path.Join(u.Path, suffix) 101 102 resp, err := http.Get(u.String()) 103 if err != nil { 104 return nil, fmt.Errorf("http.Get(%s): %v", u.String(), err) 105 } 106 defer resp.Body.Close() 107 if code := resp.StatusCode; code != 200 { 108 return nil, fmt.Errorf("http.Get(%s): %s", u.String(), http.StatusText(code)) 109 } 110 111 return io.ReadAll(resp.Body) 112} 113 114// PayloadHash returns the hash of the payload. 115func PayloadHash(p []byte) (tlog.Hash, error) { 116 l := append([]byte{leafHashPrefix}, p...) 117 h := sha256.Sum256(l) 118 119 var hash tlog.Hash 120 copy(hash[:], h[:]) 121 return hash, nil 122} 123