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