• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Package checkpoint implements methods to interact with checkpoints
2// as described below.
3//
4// Root is the internal representation of the information needed to
5// commit to the contents of the tree, and contains the root hash and size.
6//
7// When a commitment needs to be sent to other processes (such as a witness or
8// other log clients), it is put in the form of a checkpoint, which also
9// includes an "origin" string. The origin should is a unique identifier for
10// the log identity which issues the checkpoint. This package deals only with
11// the origin for the Pixel Binary Transparency Log.
12//
13// This checkpoint is signed in a note format (golang.org/x/mod/sumdb/note)
14// before sending out. An unsigned checkpoint is not a valid commitment and
15// must not be used.
16//
17// There is only a single signature.
18// Support for multiple signing identities will be added as needed.
19package checkpoint
20
21import (
22	"crypto/ecdsa"
23	"crypto/sha256"
24	"crypto/x509"
25	"encoding/base64"
26	"encoding/binary"
27	"encoding/pem"
28	"errors"
29	"fmt"
30	"io"
31	"net/http"
32	"net/url"
33	"path"
34	"strconv"
35	"strings"
36
37	"golang.org/x/mod/sumdb/note"
38)
39
40const (
41	// originID identifies a checkpoint for the Pixel Binary Transparency Log.
42	originID = "developers.google.com/android/binary_transparency/0\n"
43)
44
45type verifier interface {
46	Verify(msg []byte, sig []byte) bool
47	Name() string
48	KeyHash() uint32
49}
50
51// EcdsaVerifier verifies a message signature that was signed using ECDSA.
52type EcdsaVerifier struct {
53	PubKey *ecdsa.PublicKey
54	name   string
55	hash   uint32
56}
57
58// Verify returns whether the signature of the message is valid using its
59// pubKey.
60func (v EcdsaVerifier) Verify(msg, sig []byte) bool {
61	h := sha256.Sum256(msg)
62	if !ecdsa.VerifyASN1(v.PubKey, h[:], sig) {
63		return false
64	}
65	return true
66}
67
68// KeyHash returns a 4 byte hash of the public key to be used as a hint to the
69// verifier.
70func (v EcdsaVerifier) KeyHash() uint32 {
71	return v.hash
72}
73
74// Name returns the name of the key.
75func (v EcdsaVerifier) Name() string {
76	return v.name
77}
78
79// NewVerifier expects an ECDSA public key in PEM format in a file with the provided path and key name.
80func NewVerifier(pemKey []byte, name string) (EcdsaVerifier, error) {
81	b, _ := pem.Decode(pemKey)
82	if b == nil || b.Type != "PUBLIC KEY" {
83		return EcdsaVerifier{}, fmt.Errorf("Failed to decode public key, must contain an ECDSA public key in PEM format")
84	}
85
86	key := b.Bytes
87	sum := sha256.Sum256(key)
88	keyHash := binary.BigEndian.Uint32(sum[:])
89
90	pub, err := x509.ParsePKIXPublicKey(key)
91	if err != nil {
92		return EcdsaVerifier{}, fmt.Errorf("Can't parse key: %v", err)
93	}
94	return EcdsaVerifier{
95		PubKey: pub.(*ecdsa.PublicKey),
96		hash:   keyHash,
97		name:   name,
98	}, nil
99}
100
101// Root contains the checkpoint data.
102type Root struct {
103	// Size is the number of entries in the log at this point.
104	Size uint64
105	// Hash commits to the contents of the entire log.
106	Hash []byte
107}
108
109func parseCheckpoint(ckpt string) (Root, error) {
110	if !strings.HasPrefix(ckpt, originID) {
111		return Root{}, errors.New(fmt.Sprintf("invalid checkpoint - unknown origin, must be %s", originID))
112	}
113	// Strip the origin ID and parse the rest of the checkpoint.
114	body := ckpt[len(originID):]
115	// body must contain exactly 2 lines, size and the root hash.
116	l := strings.SplitN(body, "\n", 3)
117	if len(l) != 3 || len(l[2]) != 0 {
118		return Root{}, errors.New("invalid checkpoint - bad format: must have origin id, size and root hash each followed by newline")
119	}
120	size, err := strconv.ParseUint(l[0], 10, 64)
121	if err != nil {
122		return Root{}, fmt.Errorf("invalid checkpoint - cannot read size: %w", err)
123	}
124	rh, err := base64.StdEncoding.DecodeString(l[1])
125	if err != nil {
126		return Root{}, fmt.Errorf("invalid checkpoint - invalid roothash: %w", err)
127	}
128	return Root{Size: size, Hash: rh}, nil
129}
130
131func getSignedCheckpoint(logURL string) ([]byte, error) {
132	// Sanity check the input url.
133	u, err := url.Parse(logURL)
134	if err != nil {
135		return []byte{}, fmt.Errorf("invalid URL %s: %v", u, err)
136	}
137
138	u.Path = path.Join(u.Path, "checkpoint.txt")
139
140	resp, err := http.Get(u.String())
141	if err != nil {
142		return []byte{}, fmt.Errorf("http.Get(%s): %v", u, err)
143	}
144	defer resp.Body.Close()
145	if code := resp.StatusCode; code != 200 {
146		return []byte{}, fmt.Errorf("http.Get(%s): %s", u, http.StatusText(code))
147	}
148
149	return io.ReadAll(resp.Body)
150}
151
152// FromURL verifies the signature and unpacks and returns a Root.
153//
154// Validates signature before reading data, using a provided verifier.
155// Data at `logURL` is the checkpoint and must be in the note format
156// (golang.org/x/mod/sumdb/note).
157//
158// The checkpoint must be for the Pixel Binary Transparency Log origin.
159//
160// Returns error if the signature fails to verify or if the checkpoint
161// does not conform to the following format:
162//
163//	[]byte("[origin]\n[size]\n[hash]").
164func FromURL(logURL string, v verifier) (Root, error) {
165	b, err := getSignedCheckpoint(logURL)
166	if err != nil {
167		return Root{}, fmt.Errorf("failed to get signed checkpoint: %v", err)
168	}
169
170	n, err := note.Open(b, note.VerifierList(v))
171	if err != nil {
172		return Root{}, fmt.Errorf("failed to verify note signatures: %v", err)
173	}
174	return parseCheckpoint(n.Text)
175}
176