• 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 "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