xref: /aosp_15_r20/external/avb/tools/transparency/verify/internal/checkpoint/checkpoint.go (revision d289c2ba6de359471b23d594623b906876bc48a0)
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	// originIDPixel identifies a checkpoint for the Pixel Binary Transparency Log.
42	originIDPixel = "developers.google.com/android/binary_transparency/0\n"
43	// originIDG1P identifies a checkpoint for the Google System APK Transparency Log.
44	originIDG1P = "developers.google.com/android/binary_transparency/google1p/0\n"
45)
46
47type verifier interface {
48	Verify(msg []byte, sig []byte) bool
49	Name() string
50	KeyHash() uint32
51}
52
53// EcdsaVerifier verifies a message signature that was signed using ECDSA.
54type EcdsaVerifier struct {
55	PubKey *ecdsa.PublicKey
56	name   string
57	hash   uint32
58}
59
60// Verify returns whether the signature of the message is valid using its
61// pubKey.
62func (v EcdsaVerifier) Verify(msg, sig []byte) bool {
63	h := sha256.Sum256(msg)
64	if !ecdsa.VerifyASN1(v.PubKey, h[:], sig) {
65		return false
66	}
67	return true
68}
69
70// KeyHash returns a 4 byte hash of the public key to be used as a hint to the
71// verifier.
72func (v EcdsaVerifier) KeyHash() uint32 {
73	return v.hash
74}
75
76// Name returns the name of the key.
77func (v EcdsaVerifier) Name() string {
78	return v.name
79}
80
81// NewVerifier expects an ECDSA public key in PEM format in a file with the provided path and key name.
82func NewVerifier(pemKey []byte, name string) (EcdsaVerifier, error) {
83	b, _ := pem.Decode(pemKey)
84	if b == nil || b.Type != "PUBLIC KEY" {
85		return EcdsaVerifier{}, fmt.Errorf("Failed to decode public key, must contain an ECDSA public key in PEM format")
86	}
87
88	key := b.Bytes
89	sum := sha256.Sum256(key)
90	keyHash := binary.BigEndian.Uint32(sum[:])
91
92	pub, err := x509.ParsePKIXPublicKey(key)
93	if err != nil {
94		return EcdsaVerifier{}, fmt.Errorf("Can't parse key: %v", err)
95	}
96	return EcdsaVerifier{
97		PubKey: pub.(*ecdsa.PublicKey),
98		hash:   keyHash,
99		name:   name,
100	}, nil
101}
102
103// Root contains the checkpoint data.
104type Root struct {
105	// Size is the number of entries in the log at this point.
106	Size uint64
107	// Hash commits to the contents of the entire log.
108	Hash []byte
109}
110
111func parseCheckpoint(ckpt string) (Root, error) {
112	var body string
113	// Strip the origin ID and parse the rest of the checkpoint.
114	if strings.HasPrefix(ckpt, originIDPixel) {
115		body = ckpt[len(originIDPixel):]
116	} else if strings.HasPrefix(ckpt, originIDG1P) {
117		body = ckpt[len(originIDG1P):]
118	} else {
119		return Root{}, errors.New(fmt.Sprintf("invalid checkpoint - unknown origin, must be either %s or %s", originIDPixel, originIDG1P))
120	}
121
122	// body must contain exactly 2 lines, size and the root hash.
123	l := strings.SplitN(body, "\n", 3)
124	if len(l) != 3 || len(l[2]) != 0 {
125		return Root{}, errors.New("invalid checkpoint - bad format: must have origin id, size and root hash each followed by newline")
126	}
127	size, err := strconv.ParseUint(l[0], 10, 64)
128	if err != nil {
129		return Root{}, fmt.Errorf("invalid checkpoint - cannot read size: %w", err)
130	}
131	rh, err := base64.StdEncoding.DecodeString(l[1])
132	if err != nil {
133		return Root{}, fmt.Errorf("invalid checkpoint - invalid roothash: %w", err)
134	}
135	return Root{Size: size, Hash: rh}, nil
136}
137
138func getSignedCheckpoint(logURL string) ([]byte, error) {
139	// Sanity check the input url.
140	u, err := url.Parse(logURL)
141	if err != nil {
142		return []byte{}, fmt.Errorf("invalid URL %s: %v", u, err)
143	}
144
145	u.Path = path.Join(u.Path, "checkpoint.txt")
146
147	resp, err := http.Get(u.String())
148	if err != nil {
149		return []byte{}, fmt.Errorf("http.Get(%s): %v", u, err)
150	}
151	defer resp.Body.Close()
152	if code := resp.StatusCode; code != 200 {
153		return []byte{}, fmt.Errorf("http.Get(%s): %s", u, http.StatusText(code))
154	}
155
156	return io.ReadAll(resp.Body)
157}
158
159// FromURL verifies the signature and unpacks and returns a Root.
160//
161// Validates signature before reading data, using a provided verifier.
162// Data at `logURL` is the checkpoint and must be in the note format
163// (golang.org/x/mod/sumdb/note).
164//
165// The checkpoint must be for the Pixel Binary Transparency Log origin.
166//
167// Returns error if the signature fails to verify or if the checkpoint
168// does not conform to the following format:
169//
170//	[]byte("[origin]\n[size]\n[hash]").
171func FromURL(logURL string, v verifier) (Root, error) {
172	b, err := getSignedCheckpoint(logURL)
173	if err != nil {
174		return Root{}, fmt.Errorf("failed to get signed checkpoint: %v", err)
175	}
176
177	n, err := note.Open(b, note.VerifierList(v))
178	if err != nil {
179		return Root{}, fmt.Errorf("failed to verify note signatures: %v", err)
180	}
181	return parseCheckpoint(n.Text)
182}
183