• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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