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