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