1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package main
17
18 import (
19 "context"
20 "crypto/x509"
21 "encoding/base64"
22 "encoding/json"
23 "flag"
24 "fmt"
25 "io"
26 "net/http"
27 "net/url"
28 "os"
29 "sync"
30 "time"
31
32 ct "github.com/google/certificate-transparency-go"
33 wit_api "github.com/google/certificate-transparency-go/internal/witness/api"
34 wh "github.com/google/certificate-transparency-go/internal/witness/client/http"
35 "github.com/google/certificate-transparency-go/internal/witness/verifier"
36 "github.com/google/certificate-transparency-go/loglist3"
37 "k8s.io/klog/v2"
38 )
39
40 var (
41 logList = flag.String("log_list_url", "https://www.gstatic.com/ct/log_list/v3/log_list.json", "The location of the log list")
42 witness = flag.String("witness_url", "", "The endpoint of the witness HTTP API")
43 witnessPK = flag.String("witness_pk", "", "The base64-encoded witness public key")
44 interval = flag.Duration("poll", 10*time.Second, "How frequently to poll to get new witnessed STHs")
45 )
46
47
48 type WitnessSigVerifier interface {
49 VerifySignature(cosigned wit_api.CosignedSTH) error
50 }
51
52
53 type Witness struct {
54 Client *wh.Witness
55 Verifier WitnessSigVerifier
56 }
57
58 type ctLog struct {
59 id string
60 name string
61 wsth *wit_api.CosignedSTH
62 verifier *ct.SignatureVerifier
63 }
64
65 func main() {
66 klog.InitFlags(nil)
67 flag.Parse()
68 if *witness == "" {
69 klog.Exit("--witness_url must not be empty")
70 }
71 if *witnessPK == "" {
72 klog.Exit("--witness_pk must not be empty")
73 }
74 ctx := context.Background()
75
76 wURL, err := url.Parse(*witness)
77 if err != nil {
78 klog.Exitf("Failed to parse witness URL: %v", err)
79 }
80 pk, err := ct.PublicKeyFromB64(*witnessPK)
81 if err != nil {
82 klog.Exitf("Failed to create witness public key: %v", err)
83 }
84 wv, err := verifier.NewWitnessVerifier(pk)
85 if err != nil {
86 klog.Exitf("Failed to create witness signature verifier: %v", err)
87 }
88 w := Witness{
89 Client: &wh.Witness{
90 URL: wURL,
91 },
92 Verifier: wv,
93 }
94
95 ctLogs, err := populateLogs(*logList)
96 if err != nil {
97 klog.Exitf("Failed to set up log data: %v", err)
98 }
99
100 wg := &sync.WaitGroup{}
101 for _, log := range ctLogs {
102 wg.Add(1)
103 go func(witness *Witness, log ctLog) {
104 defer wg.Done()
105 if err := log.getSTH(ctx, witness, *interval); err != nil {
106 klog.Errorf("getSTH: %v", err)
107 }
108 }(&w, log)
109 }
110 wg.Wait()
111 }
112
113
114 func populateLogs(logListURL string) ([]ctLog, error) {
115 u, err := url.Parse(logListURL)
116 if err != nil {
117 return nil, fmt.Errorf("failed to parse URL: %v", err)
118 }
119 body, err := readURL(u)
120 if err != nil {
121 return nil, fmt.Errorf("failed to get log list data: %v", err)
122 }
123
124 logList, err := loglist3.NewFromJSON(body)
125 if err != nil {
126 return nil, fmt.Errorf("failed to parse JSON: %v", err)
127 }
128 usable := logList.SelectByStatus([]loglist3.LogStatus{loglist3.UsableLogStatus})
129 var logs []ctLog
130 for _, operator := range usable.Operators {
131 for _, log := range operator.Logs {
132 logID := base64.StdEncoding.EncodeToString(log.LogID)
133
134
135 pk, err := x509.ParsePKIXPublicKey(log.Key)
136 if err != nil {
137 return nil, fmt.Errorf("failed to create public key for %s: %v", log.Description, err)
138 }
139 v, err := ct.NewSignatureVerifier(pk)
140 if err != nil {
141 return nil, fmt.Errorf("failed to create signature verifier: %v", err)
142 }
143 l := ctLog{
144 id: logID,
145 name: log.Description,
146 verifier: v,
147 }
148 logs = append(logs, l)
149 }
150 }
151 return logs, nil
152 }
153
154
155
156 func (l *ctLog) getSTH(ctx context.Context, witness *Witness, interval time.Duration) error {
157 tik := time.NewTicker(interval)
158 defer tik.Stop()
159 for {
160 func() {
161 ctx, cancel := context.WithTimeout(ctx, interval)
162 defer cancel()
163
164 klog.V(2).Infof("Requesting STH for %s from witness", l.name)
165 if err := l.getOnce(ctx, witness); err != nil {
166 klog.Warningf("Failed to retrieve STH for %s: %v", l.name, err)
167 } else {
168 klog.Infof("Verified the STH for %s at size %d!", l.name, l.wsth.TreeSize)
169 }
170 }()
171
172 select {
173 case <-ctx.Done():
174 return ctx.Err()
175 case <-tik.C:
176 }
177 }
178 }
179
180
181
182 func (l *ctLog) getOnce(ctx context.Context, witness *Witness) error {
183
184 var cSTH wit_api.CosignedSTH
185 sthRaw, err := witness.Client.GetLatestSTH(ctx, l.id)
186 if err != nil {
187 return fmt.Errorf("failed to get STH: %v", err)
188 }
189 if err := json.Unmarshal(sthRaw, &cSTH); err != nil {
190 return fmt.Errorf("failed to unmarshal STH: %v", err)
191 }
192
193 if err := witness.Verifier.VerifySignature(cSTH); err != nil {
194 return fmt.Errorf("failed to verify witness signature: %v", err)
195 }
196
197 plainSTH := cSTH.SignedTreeHead
198 if err := l.verifier.VerifySTHSignature(plainSTH); err != nil {
199 return fmt.Errorf("failed to verify log signature: %v", err)
200 }
201 l.wsth = &cSTH
202 return nil
203 }
204
205 var getByScheme = map[string]func(*url.URL) ([]byte, error){
206 "http": readHTTP,
207 "https": readHTTP,
208 "file": func(u *url.URL) ([]byte, error) {
209 return os.ReadFile(u.Path)
210 },
211 }
212
213
214 func readHTTP(u *url.URL) ([]byte, error) {
215 resp, err := http.Get(u.String())
216 if err != nil {
217 return nil, err
218 }
219 defer resp.Body.Close()
220 return io.ReadAll(resp.Body)
221 }
222
223
224 func readURL(u *url.URL) ([]byte, error) {
225 s := u.Scheme
226 queryFn, ok := getByScheme[s]
227 if !ok {
228 return nil, fmt.Errorf("failed to identify suitable scheme for the URL %q", u.String())
229 }
230 return queryFn(u)
231 }
232
View as plain text