Skip to content

Commit 8a01611

Browse files
authored
Switch to loglist3 package for parsing CT log list (#7930)
The schema tool used to parse log_list_schema.json doesn't work well with the updated schema. This is going to be required to support static-ct-api logs from current Chrome log lists. Instead, use the loglist3 package inside the certificate-transparency-go project, which Boulder already uses for CT submission otherwise. As well, the Log IDs and keys returned from loglist3 have already been base64 decoded, so this re-encodes them to minimize the impact on the rest of the codebase and keep this change small. The test log_list.json file needed to be made a bit more realistic for loglist3 to parse without base64 or date parsing errors.
1 parent e4668b4 commit 8a01611

File tree

17 files changed

+2028
-677
lines changed

17 files changed

+2028
-677
lines changed

ctpolicy/loglist/loglist.go

Lines changed: 15 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ package loglist
22

33
import (
44
_ "embed"
5-
"encoding/json"
5+
"encoding/base64"
66
"errors"
77
"fmt"
88
"math/rand/v2"
99
"os"
1010
"strings"
1111
"time"
1212

13-
"github.com/letsencrypt/boulder/ctpolicy/loglist/schema"
13+
"github.com/google/certificate-transparency-go/loglist3"
1414
)
1515

1616
// purpose is the use to which a log list will be put. This type exists to allow
@@ -52,53 +52,19 @@ type Log struct {
5252
Key string
5353
StartInclusive time.Time
5454
EndExclusive time.Time
55-
State state
56-
}
57-
58-
// State is an enum representing the various states a CT log can be in. Only
59-
// pending, qualified, and usable logs can be submitted to. Only usable and
60-
// readonly logs are trusted by Chrome.
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
55+
State loglist3.LogStatus
9056
}
9157

9258
// usableForPurpose returns true if the log state is acceptable for the given
9359
// log list purpose, and false otherwise.
94-
func usableForPurpose(s state, p purpose) bool {
60+
func usableForPurpose(s loglist3.LogStatus, p purpose) bool {
9561
switch p {
9662
case Issuance:
97-
return s == usable
63+
return s == loglist3.UsableLogStatus
9864
case Informational:
99-
return s == usable || s == qualified || s == pending
65+
return s == loglist3.UsableLogStatus || s == loglist3.QualifiedLogStatus || s == loglist3.PendingLogStatus
10066
case Validation:
101-
return s == usable || s == readonly
67+
return s == loglist3.UsableLogStatus || s == loglist3.ReadOnlyLogStatus
10268
}
10369
return false
10470
}
@@ -118,8 +84,7 @@ func New(path string) (List, error) {
11884
// newHelper is a helper to allow the core logic of `New()` to be unit tested
11985
// without having to write files to disk.
12086
func newHelper(file []byte) (List, error) {
121-
var parsed schema.LogListSchemaJson
122-
err := json.Unmarshal(file, &parsed)
87+
parsed, err := loglist3.NewFromJSON(file)
12388
if err != nil {
12489
return nil, fmt.Errorf("failed to parse CT Log List: %w", err)
12590
}
@@ -128,34 +93,19 @@ func newHelper(file []byte) (List, error) {
12893
for _, op := range parsed.Operators {
12994
group := make(OperatorGroup)
13095
for _, log := range op.Logs {
131-
var name string
132-
if log.Description != nil {
133-
name = *log.Description
134-
}
135-
13696
info := Log{
137-
Name: name,
138-
Url: log.Url,
139-
Key: log.Key,
140-
State: stateFromState(log.State),
97+
Name: log.Description,
98+
Url: log.URL,
99+
Key: base64.StdEncoding.EncodeToString(log.Key),
100+
State: log.State.LogStatus(),
141101
}
142102

143103
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
104+
info.StartInclusive = log.TemporalInterval.StartInclusive
105+
info.EndExclusive = log.TemporalInterval.EndExclusive
156106
}
157107

158-
group[log.LogId] = info
108+
group[base64.StdEncoding.EncodeToString(log.LogID)] = info
159109
}
160110
result[op.Name] = group
161111
}

ctpolicy/loglist/loglist_test.go

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"testing"
55
"time"
66

7+
"github.com/google/certificate-transparency-go/loglist3"
8+
79
"github.com/letsencrypt/boulder/test"
810
)
911

@@ -56,24 +58,24 @@ func TestSubset(t *testing.T) {
5658
func TestForPurpose(t *testing.T) {
5759
input := List{
5860
"Operator A": {
59-
"ID A1": Log{Name: "Log A1", State: usable},
60-
"ID A2": Log{Name: "Log A2", State: rejected},
61+
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
62+
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
6163
},
6264
"Operator B": {
63-
"ID B1": Log{Name: "Log B1", State: usable},
64-
"ID B2": Log{Name: "Log B2", State: retired},
65+
"ID B1": Log{Name: "Log B1", State: loglist3.UsableLogStatus},
66+
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
6567
},
6668
"Operator C": {
67-
"ID C1": Log{Name: "Log C1", State: pending},
68-
"ID C2": Log{Name: "Log C2", State: readonly},
69+
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
70+
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
6971
},
7072
}
7173
expected := List{
7274
"Operator A": {
73-
"ID A1": Log{Name: "Log A1", State: usable},
75+
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
7476
},
7577
"Operator B": {
76-
"ID B1": Log{Name: "Log B1", State: usable},
78+
"ID B1": Log{Name: "Log B1", State: loglist3.UsableLogStatus},
7779
},
7880
}
7981
actual, err := input.forPurpose(Issuance)
@@ -82,27 +84,27 @@ func TestForPurpose(t *testing.T) {
8284

8385
input = List{
8486
"Operator A": {
85-
"ID A1": Log{Name: "Log A1", State: usable},
86-
"ID A2": Log{Name: "Log A2", State: rejected},
87+
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
88+
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
8789
},
8890
"Operator B": {
89-
"ID B1": Log{Name: "Log B1", State: qualified},
90-
"ID B2": Log{Name: "Log B2", State: retired},
91+
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
92+
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
9193
},
9294
"Operator C": {
93-
"ID C1": Log{Name: "Log C1", State: pending},
94-
"ID C2": Log{Name: "Log C2", State: readonly},
95+
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
96+
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
9597
},
9698
}
9799
_, err = input.forPurpose(Issuance)
98100
test.AssertError(t, err, "should only have one acceptable log")
99101

100102
expected = List{
101103
"Operator A": {
102-
"ID A1": Log{Name: "Log A1", State: usable},
104+
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
103105
},
104106
"Operator C": {
105-
"ID C2": Log{Name: "Log C2", State: readonly},
107+
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
106108
},
107109
}
108110
actual, err = input.forPurpose(Validation)
@@ -111,13 +113,13 @@ func TestForPurpose(t *testing.T) {
111113

112114
expected = List{
113115
"Operator A": {
114-
"ID A1": Log{Name: "Log A1", State: usable},
116+
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
115117
},
116118
"Operator B": {
117-
"ID B1": Log{Name: "Log B1", State: qualified},
119+
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
118120
},
119121
"Operator C": {
120-
"ID C1": Log{Name: "Log C1", State: pending},
122+
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
121123
},
122124
}
123125
actual, err = input.forPurpose(Informational)
@@ -128,10 +130,10 @@ func TestForPurpose(t *testing.T) {
128130
func TestOperatorForLogID(t *testing.T) {
129131
input := List{
130132
"Operator A": {
131-
"ID A1": Log{Name: "Log A1", State: usable},
133+
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
132134
},
133135
"Operator B": {
134-
"ID B1": Log{Name: "Log B1", State: qualified},
136+
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
135137
},
136138
}
137139

@@ -146,16 +148,16 @@ func TestOperatorForLogID(t *testing.T) {
146148
func TestPermute(t *testing.T) {
147149
input := List{
148150
"Operator A": {
149-
"ID A1": Log{Name: "Log A1", State: usable},
150-
"ID A2": Log{Name: "Log A2", State: rejected},
151+
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
152+
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
151153
},
152154
"Operator B": {
153-
"ID B1": Log{Name: "Log B1", State: qualified},
154-
"ID B2": Log{Name: "Log B2", State: retired},
155+
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
156+
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
155157
},
156158
"Operator C": {
157-
"ID C1": Log{Name: "Log C1", State: pending},
158-
"ID C2": Log{Name: "Log C2", State: readonly},
159+
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
160+
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
159161
},
160162
}
161163

0 commit comments

Comments
 (0)