1 package loglist
2
3 import (
4 _ "embed"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "math/rand"
9 "os"
10 "strings"
11 "time"
12
13 "github.com/letsencrypt/boulder/ctpolicy/loglist/schema"
14 )
15
16
17
18 type purpose string
19
20
21
22 const Issuance purpose = "scts"
23
24
25
26
27 const Informational purpose = "info"
28
29
30
31
32 const Validation purpose = "lint"
33
34
35
36
37
38
39 type List map[string]OperatorGroup
40
41
42
43
44 type OperatorGroup map[string]Log
45
46
47
48
49 type Log struct {
50 Name string
51 Url string
52 Key string
53 StartInclusive time.Time
54 EndExclusive time.Time
55 State state
56 }
57
58
59
60
61 type state int
62
63 const (
64 unknown state = iota
65 pending
66 qualified
67 usable
68 readonly
69 retired
70 rejected
71 )
72
73 func stateFromState(s *schema.LogListSchemaJsonOperatorsElemLogsElemState) state {
74 if s == nil {
75 return unknown
76 } else if s.Rejected != nil {
77 return rejected
78 } else if s.Retired != nil {
79 return retired
80 } else if s.Readonly != nil {
81 return readonly
82 } else if s.Pending != nil {
83 return pending
84 } else if s.Qualified != nil {
85 return qualified
86 } else if s.Usable != nil {
87 return usable
88 }
89 return unknown
90 }
91
92
93
94 func usableForPurpose(s state, p purpose) bool {
95 switch p {
96 case Issuance:
97 return s == usable
98 case Informational:
99 return s == usable || s == qualified || s == pending
100 case Validation:
101 return s == usable || s == readonly
102 }
103 return false
104 }
105
106
107
108
109 func New(path string) (List, error) {
110 file, err := os.ReadFile(path)
111 if err != nil {
112 return nil, fmt.Errorf("failed to read CT Log List: %w", err)
113 }
114
115 return newHelper(file)
116 }
117
118
119
120 func newHelper(file []byte) (List, error) {
121 var parsed schema.LogListSchemaJson
122 err := json.Unmarshal(file, &parsed)
123 if err != nil {
124 return nil, fmt.Errorf("failed to parse CT Log List: %w", err)
125 }
126
127 result := make(List)
128 for _, op := range parsed.Operators {
129 group := make(OperatorGroup)
130 for _, log := range op.Logs {
131 var name string
132 if log.Description != nil {
133 name = *log.Description
134 }
135
136 info := Log{
137 Name: name,
138 Url: log.Url,
139 Key: log.Key,
140 State: stateFromState(log.State),
141 }
142
143 if log.TemporalInterval != nil {
144 startInclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.StartInclusive)
145 if err != nil {
146 return nil, fmt.Errorf("failed to parse log %q start timestamp: %w", log.Url, err)
147 }
148
149 endExclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.EndExclusive)
150 if err != nil {
151 return nil, fmt.Errorf("failed to parse log %q end timestamp: %w", log.Url, err)
152 }
153
154 info.StartInclusive = startInclusive
155 info.EndExclusive = endExclusive
156 }
157
158 group[log.LogId] = info
159 }
160 result[op.Name] = group
161 }
162
163 return result, nil
164 }
165
166
167
168
169
170
171 func (ll List) SubsetForPurpose(names []string, p purpose) (List, error) {
172 sub, err := ll.subset(names)
173 if err != nil {
174 return nil, err
175 }
176
177 res, err := sub.forPurpose(p)
178 if err != nil {
179 return nil, err
180 }
181
182 return res, nil
183 }
184
185
186
187
188 func (ll List) subset(names []string) (List, error) {
189 remaining := make(map[string]struct{}, len(names))
190 for _, name := range names {
191 remaining[name] = struct{}{}
192 }
193
194 newList := make(List)
195 for operator, group := range ll {
196 newGroup := make(OperatorGroup)
197 for id, log := range group {
198 if _, found := remaining[log.Name]; !found {
199 continue
200 }
201
202 newLog := Log{
203 Name: log.Name,
204 Url: log.Url,
205 Key: log.Key,
206 State: log.State,
207 StartInclusive: log.StartInclusive,
208 EndExclusive: log.EndExclusive,
209 }
210
211 newGroup[id] = newLog
212 delete(remaining, newLog.Name)
213 }
214 if len(newGroup) > 0 {
215 newList[operator] = newGroup
216 }
217 }
218
219 if len(remaining) > 0 {
220 missed := make([]string, len(remaining))
221 for name := range remaining {
222 missed = append(missed, fmt.Sprintf("%q", name))
223 }
224 return nil, fmt.Errorf("failed to find logs matching name(s): %s", strings.Join(missed, ", "))
225 }
226
227 return newList, nil
228 }
229
230
231
232
233
234 func (ll List) forPurpose(p purpose) (List, error) {
235 newList := make(List)
236 for operator, group := range ll {
237 newGroup := make(OperatorGroup)
238 for id, log := range group {
239 if !usableForPurpose(log.State, p) {
240 continue
241 }
242
243 newLog := Log{
244 Name: log.Name,
245 Url: log.Url,
246 Key: log.Key,
247 State: log.State,
248 StartInclusive: log.StartInclusive,
249 EndExclusive: log.EndExclusive,
250 }
251
252 newGroup[id] = newLog
253 }
254 if len(newGroup) > 0 {
255 newList[operator] = newGroup
256 }
257 }
258
259 if len(newList) < 2 && p != Informational {
260 return nil, errors.New("log list does not have enough groups to satisfy Chrome policy")
261 }
262
263 return newList, nil
264 }
265
266
267
268 func (ll List) OperatorForLogID(logID string) (string, error) {
269 for op, group := range ll {
270 if _, found := group[logID]; found {
271 return op, nil
272 }
273 }
274 return "", fmt.Errorf("no log with ID %q found", logID)
275 }
276
277
278 func (ll List) Permute() []string {
279 keys := make([]string, 0, len(ll))
280 for k := range ll {
281 keys = append(keys, k)
282 }
283
284 result := make([]string, len(ll))
285 for i, j := range rand.Perm(len(ll)) {
286 result[i] = keys[j]
287 }
288 return result
289 }
290
291
292
293
294 func (ll List) PickOne(operator string, expiry time.Time) (string, string, error) {
295 group, ok := ll[operator]
296 if !ok {
297 return "", "", fmt.Errorf("no log operator group named %q", operator)
298 }
299
300 candidates := make([]Log, 0)
301 for _, log := range group {
302 if log.StartInclusive.IsZero() || log.EndExclusive.IsZero() {
303 candidates = append(candidates, log)
304 continue
305 }
306
307 if (log.StartInclusive.Equal(expiry) || log.StartInclusive.Before(expiry)) && log.EndExclusive.After(expiry) {
308 candidates = append(candidates, log)
309 }
310 }
311
312
313 if len(candidates) < 1 {
314 return "", "", fmt.Errorf("no log found for group %q and expiry %s", operator, expiry)
315 }
316
317 log := candidates[rand.Intn(len(candidates))]
318 return log.Url, log.Key, nil
319 }
320
View as plain text