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