| // Package tiles contains methods to work with tlog based verifiable logs. |
| package tiles |
| |
| import ( |
| "crypto/sha256" |
| "errors" |
| "fmt" |
| "io" |
| "net/http" |
| "net/url" |
| "path" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/mod/sumdb/tlog" |
| ) |
| |
| // HashReader implements tlog.HashReader, reading from tlog-based log located at |
| // URL. |
| type HashReader struct { |
| URL string |
| } |
| |
| |
| // Domain separation prefix for Merkle tree hashing with second preimage |
| // resistance similar to that used in RFC 6962. |
| const ( |
| leafHashPrefix = 0 |
| ) |
| |
| // ReadHashes implements tlog.HashReader's ReadHashes. |
| // See: https://pkg.go.dev/golang.org/x/mod/sumdb/tlog#HashReader. |
| func (h HashReader) ReadHashes(indices []int64) ([]tlog.Hash, error) { |
| tiles := make(map[string][]byte) |
| hashes := make([]tlog.Hash, 0, len(indices)) |
| for _, index := range indices { |
| // The PixelBT log is tiled at height = 1. |
| tile := tlog.TileForIndex(1, index) |
| |
| var content []byte |
| var exists bool |
| var err error |
| content, exists = tiles[tile.Path()] |
| if !exists { |
| content, err = readFromURL(h.URL, tile.Path()) |
| if err != nil { |
| return nil, fmt.Errorf("failed to read from %s: %v", tile.Path(), err) |
| } |
| tiles[tile.Path()] = content |
| } |
| |
| hash, err := tlog.HashFromTile(tile, content, index) |
| if err != nil { |
| return nil, fmt.Errorf("failed to read data from tile for index %d: %v", index, err) |
| } |
| hashes = append(hashes, hash) |
| } |
| return hashes, nil |
| } |
| |
| // ImageInfosIndex returns a map from payload to its index in the |
| // transparency log according to the image_info.txt. |
| func ImageInfosIndex(logBaseURL string) (map[string]int64, error) { |
| b, err := readFromURL(logBaseURL, "image_info.txt") |
| if err != nil { |
| return nil, err |
| } |
| |
| imageInfos := string(b) |
| return parseImageInfosIndex(imageInfos) |
| } |
| |
| func parseImageInfosIndex(imageInfos string) (map[string]int64, error) { |
| m := make(map[string]int64) |
| |
| infosStr := strings.Split(imageInfos, "\n\n") |
| for _, infoStr := range infosStr { |
| pieces := strings.SplitN(infoStr, "\n", 2) |
| if len(pieces) != 2 { |
| return nil, errors.New("missing newline, malformed image_info.txt") |
| } |
| |
| idx, err := strconv.ParseInt(pieces[0], 10, 64) |
| if err != nil { |
| return nil, fmt.Errorf("failed to convert %q to int64", pieces[0]) |
| } |
| |
| // Ensure that each log entry does not have extraneous whitespace, but |
| // also terminates with a newline. |
| logEntry := strings.TrimSpace(pieces[1]) + "\n" |
| m[logEntry] = idx |
| } |
| |
| return m, nil |
| } |
| |
| func readFromURL(base, suffix string) ([]byte, error) { |
| u, err := url.Parse(base) |
| if err != nil { |
| return nil, fmt.Errorf("invalid URL %s: %v", base, err) |
| } |
| u.Path = path.Join(u.Path, suffix) |
| |
| resp, err := http.Get(u.String()) |
| if err != nil { |
| return nil, fmt.Errorf("http.Get(%s): %v", u.String(), err) |
| } |
| defer resp.Body.Close() |
| if code := resp.StatusCode; code != 200 { |
| return nil, fmt.Errorf("http.Get(%s): %s", u.String(), http.StatusText(code)) |
| } |
| |
| return io.ReadAll(resp.Body) |
| } |
| |
| // PayloadHash returns the hash of the payload. |
| func PayloadHash(p []byte) (tlog.Hash, error) { |
| l := append([]byte{leafHashPrefix}, p...) |
| h := sha256.Sum256(l) |
| |
| var hash tlog.Hash |
| copy(hash[:], h[:]) |
| return hash, nil |
| } |