1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package loglist3
18
19 import (
20 "bytes"
21 "crypto"
22 "crypto/ecdsa"
23 "crypto/rsa"
24 "crypto/sha256"
25 "encoding/base64"
26 "encoding/hex"
27 "encoding/json"
28 "fmt"
29 "regexp"
30 "strings"
31 "time"
32 "unicode"
33
34 "github.com/google/certificate-transparency-go/tls"
35 )
36
37 const (
38
39 LogListURL = "https://www.gstatic.com/ct/log_list/v3/log_list.json"
40
41 LogListSignatureURL = "https://www.gstatic.com/ct/log_list/v3/log_list.sig"
42
43 AllLogListURL = "https://www.gstatic.com/ct/log_list/v3/all_logs_list.json"
44 )
45
46
47
48
49 type LogList struct {
50
51
52 IsAllLogs bool `json:"is_all_logs,omitempty"`
53
54 Version string `json:"version,omitempty"`
55
56 LogListTimestamp time.Time `json:"log_list_timestamp,omitempty"`
57
58 Operators []*Operator `json:"operators"`
59 }
60
61
62
63 type Operator struct {
64
65 Name string `json:"name"`
66
67
68 Email []string `json:"email"`
69
70 Logs []*Log `json:"logs"`
71 }
72
73
74 type Log struct {
75
76 Description string `json:"description,omitempty"`
77
78 LogID []byte `json:"log_id"`
79
80 Key []byte `json:"key"`
81
82 URL string `json:"url"`
83
84 DNS string `json:"dns,omitempty"`
85
86
87 MMD int32 `json:"mmd"`
88
89
90 PreviousOperators []*PreviousOperator `json:"previous_operators,omitempty"`
91
92
93 State *LogStates `json:"state,omitempty"`
94
95
96 TemporalInterval *TemporalInterval `json:"temporal_interval,omitempty"`
97
98 Type string `json:"log_type,omitempty"`
99 }
100
101
102
103 type PreviousOperator struct {
104
105 Name string `json:"name"`
106
107 EndTime time.Time `json:"end_time"`
108 }
109
110
111 type TemporalInterval struct {
112
113 StartInclusive time.Time `json:"start_inclusive"`
114
115 EndExclusive time.Time `json:"end_exclusive"`
116 }
117
118
119 type LogStatus int
120
121
122 const (
123 UndefinedLogStatus LogStatus = iota
124 PendingLogStatus
125 QualifiedLogStatus
126 UsableLogStatus
127 ReadOnlyLogStatus
128 RetiredLogStatus
129 RejectedLogStatus
130 )
131
132
133
134
135
136 type LogStates struct {
137
138 Pending *LogState `json:"pending,omitempty"`
139
140 Qualified *LogState `json:"qualified,omitempty"`
141
142 Usable *LogState `json:"usable,omitempty"`
143
144 ReadOnly *ReadOnlyLogState `json:"readonly,omitempty"`
145
146 Retired *LogState `json:"retired,omitempty"`
147
148 Rejected *LogState `json:"rejected,omitempty"`
149 }
150
151
152 type LogState struct {
153
154 Timestamp time.Time `json:"timestamp"`
155 }
156
157
158 type ReadOnlyLogState struct {
159 LogState
160
161
162 FinalTreeHead TreeHead `json:"final_tree_head"`
163 }
164
165
166 type TreeHead struct {
167
168 SHA256RootHash []byte `json:"sha256_root_hash"`
169
170 TreeSize int64 `json:"tree_size"`
171 }
172
173
174 func (ls *LogStates) LogStatus() LogStatus {
175 switch {
176 case ls == nil:
177 return UndefinedLogStatus
178 case ls.Pending != nil:
179 return PendingLogStatus
180 case ls.Qualified != nil:
181 return QualifiedLogStatus
182 case ls.Usable != nil:
183 return UsableLogStatus
184 case ls.ReadOnly != nil:
185 return ReadOnlyLogStatus
186 case ls.Retired != nil:
187 return RetiredLogStatus
188 case ls.Rejected != nil:
189 return RejectedLogStatus
190 default:
191 return UndefinedLogStatus
192 }
193 }
194
195
196 func (ls *LogStates) String() string {
197 return ls.LogStatus().String()
198 }
199
200
201 func (ls *LogStates) Active() (*LogState, *ReadOnlyLogState) {
202 if ls == nil {
203 return nil, nil
204 }
205 switch {
206 case ls.Pending != nil:
207 return ls.Pending, nil
208 case ls.Qualified != nil:
209 return ls.Qualified, nil
210 case ls.Usable != nil:
211 return ls.Usable, nil
212 case ls.ReadOnly != nil:
213 return nil, ls.ReadOnly
214 case ls.Retired != nil:
215 return ls.Retired, nil
216 case ls.Rejected != nil:
217 return ls.Rejected, nil
218 default:
219 return nil, nil
220 }
221 }
222
223
224 func (op *Operator) GoogleOperated() bool {
225 for _, email := range op.Email {
226 if strings.Contains(email, "google-ct-logs@googlegroups") {
227 return true
228 }
229 }
230 return false
231 }
232
233
234 func NewFromJSON(llData []byte) (*LogList, error) {
235 var ll LogList
236 if err := json.Unmarshal(llData, &ll); err != nil {
237 return nil, fmt.Errorf("failed to parse log list: %v", err)
238 }
239 return &ll, nil
240 }
241
242
243
244
245 func NewFromSignedJSON(llData, rawSig []byte, pubKey crypto.PublicKey) (*LogList, error) {
246 var sigAlgo tls.SignatureAlgorithm
247 switch pkType := pubKey.(type) {
248 case *rsa.PublicKey:
249 sigAlgo = tls.RSA
250 case *ecdsa.PublicKey:
251 sigAlgo = tls.ECDSA
252 default:
253 return nil, fmt.Errorf("unsupported public key type %v", pkType)
254 }
255 tlsSig := tls.DigitallySigned{
256 Algorithm: tls.SignatureAndHashAlgorithm{
257 Hash: tls.SHA256,
258 Signature: sigAlgo,
259 },
260 Signature: rawSig,
261 }
262 if err := tls.VerifySignature(pubKey, llData, tlsSig); err != nil {
263 return nil, fmt.Errorf("failed to verify signature: %v", err)
264 }
265 return NewFromJSON(llData)
266 }
267
268
269 func (ll *LogList) FindLogByName(name string) []*Log {
270 name = strings.ToLower(name)
271 var results []*Log
272 for _, op := range ll.Operators {
273 for _, log := range op.Logs {
274 if strings.Contains(strings.ToLower(log.Description), name) {
275 results = append(results, log)
276 }
277 }
278 }
279 return results
280 }
281
282
283 func (ll *LogList) FindLogByURL(url string) *Log {
284 for _, op := range ll.Operators {
285 for _, log := range op.Logs {
286
287 if strings.TrimRight(log.URL, "/") == strings.TrimRight(url, "/") {
288 return log
289 }
290 }
291 }
292 return nil
293 }
294
295
296 func (ll *LogList) FindLogByKeyHash(keyhash [sha256.Size]byte) *Log {
297 for _, op := range ll.Operators {
298 for _, log := range op.Logs {
299 if bytes.Equal(log.LogID, keyhash[:]) {
300 return log
301 }
302 }
303 }
304 return nil
305 }
306
307
308 func (ll *LogList) FindLogByKeyHashPrefix(prefix string) []*Log {
309 var results []*Log
310 for _, op := range ll.Operators {
311 for _, log := range op.Logs {
312 hh := hex.EncodeToString(log.LogID[:])
313 if strings.HasPrefix(hh, prefix) {
314 results = append(results, log)
315 }
316 }
317 }
318 return results
319 }
320
321
322 func (ll *LogList) FindLogByKey(key []byte) *Log {
323 for _, op := range ll.Operators {
324 for _, log := range op.Logs {
325 if bytes.Equal(log.Key[:], key) {
326 return log
327 }
328 }
329 }
330 return nil
331 }
332
333 var hexDigits = regexp.MustCompile("^[0-9a-fA-F]+$")
334
335
336
337
338
339 func (ll *LogList) FuzzyFindLog(input string) []*Log {
340 input = strings.Trim(input, " \t")
341 if logs := ll.FindLogByName(input); len(logs) > 0 {
342 return logs
343 }
344 if log := ll.FindLogByURL(input); log != nil {
345 return []*Log{log}
346 }
347
348 if data, err := base64.StdEncoding.DecodeString(input); err == nil {
349 if len(data) == sha256.Size {
350 var hash [sha256.Size]byte
351 copy(hash[:], data)
352 if log := ll.FindLogByKeyHash(hash); log != nil {
353 return []*Log{log}
354 }
355 }
356 if log := ll.FindLogByKey(data); log != nil {
357 return []*Log{log}
358 }
359 }
360
361 input = stripInternalSpace(input)
362 if data, err := hex.DecodeString(input); err == nil {
363 if len(data) == sha256.Size {
364 var hash [sha256.Size]byte
365 copy(hash[:], data)
366 if log := ll.FindLogByKeyHash(hash); log != nil {
367 return []*Log{log}
368 }
369 }
370 if log := ll.FindLogByKey(data); log != nil {
371 return []*Log{log}
372 }
373 }
374
375 if hexDigits.MatchString(input) {
376 if logs := ll.FindLogByKeyHashPrefix(input); len(logs) > 0 {
377 return logs
378 }
379 }
380
381 return nil
382 }
383
384 func stripInternalSpace(input string) string {
385 return strings.Map(func(r rune) rune {
386 if !unicode.IsSpace(r) {
387 return r
388 }
389 return -1
390 }, input)
391 }
392
View as plain text