Skip to content

Commit

Permalink
Switch to loglist3 package for parsing CT log list (#7930)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mcpherrinm authored Jan 10, 2025
1 parent e4668b4 commit 8a01611
Show file tree
Hide file tree
Showing 17 changed files with 2,028 additions and 677 deletions.
80 changes: 15 additions & 65 deletions ctpolicy/loglist/loglist.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package loglist

import (
_ "embed"
"encoding/json"
"encoding/base64"
"errors"
"fmt"
"math/rand/v2"
"os"
"strings"
"time"

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

// purpose is the use to which a log list will be put. This type exists to allow
Expand Down Expand Up @@ -52,53 +52,19 @@ type Log struct {
Key string
StartInclusive time.Time
EndExclusive time.Time
State state
}

// State is an enum representing the various states a CT log can be in. Only
// pending, qualified, and usable logs can be submitted to. Only usable and
// readonly logs are trusted by Chrome.
type state int

const (
unknown state = iota
pending
qualified
usable
readonly
retired
rejected
)

func stateFromState(s *schema.LogListSchemaJsonOperatorsElemLogsElemState) state {
if s == nil {
return unknown
} else if s.Rejected != nil {
return rejected
} else if s.Retired != nil {
return retired
} else if s.Readonly != nil {
return readonly
} else if s.Pending != nil {
return pending
} else if s.Qualified != nil {
return qualified
} else if s.Usable != nil {
return usable
}
return unknown
State loglist3.LogStatus
}

// usableForPurpose returns true if the log state is acceptable for the given
// log list purpose, and false otherwise.
func usableForPurpose(s state, p purpose) bool {
func usableForPurpose(s loglist3.LogStatus, p purpose) bool {
switch p {
case Issuance:
return s == usable
return s == loglist3.UsableLogStatus
case Informational:
return s == usable || s == qualified || s == pending
return s == loglist3.UsableLogStatus || s == loglist3.QualifiedLogStatus || s == loglist3.PendingLogStatus
case Validation:
return s == usable || s == readonly
return s == loglist3.UsableLogStatus || s == loglist3.ReadOnlyLogStatus
}
return false
}
Expand All @@ -118,8 +84,7 @@ func New(path string) (List, error) {
// newHelper is a helper to allow the core logic of `New()` to be unit tested
// without having to write files to disk.
func newHelper(file []byte) (List, error) {
var parsed schema.LogListSchemaJson
err := json.Unmarshal(file, &parsed)
parsed, err := loglist3.NewFromJSON(file)
if err != nil {
return nil, fmt.Errorf("failed to parse CT Log List: %w", err)
}
Expand All @@ -128,34 +93,19 @@ func newHelper(file []byte) (List, error) {
for _, op := range parsed.Operators {
group := make(OperatorGroup)
for _, log := range op.Logs {
var name string
if log.Description != nil {
name = *log.Description
}

info := Log{
Name: name,
Url: log.Url,
Key: log.Key,
State: stateFromState(log.State),
Name: log.Description,
Url: log.URL,
Key: base64.StdEncoding.EncodeToString(log.Key),
State: log.State.LogStatus(),
}

if log.TemporalInterval != nil {
startInclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.StartInclusive)
if err != nil {
return nil, fmt.Errorf("failed to parse log %q start timestamp: %w", log.Url, err)
}

endExclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.EndExclusive)
if err != nil {
return nil, fmt.Errorf("failed to parse log %q end timestamp: %w", log.Url, err)
}

info.StartInclusive = startInclusive
info.EndExclusive = endExclusive
info.StartInclusive = log.TemporalInterval.StartInclusive
info.EndExclusive = log.TemporalInterval.EndExclusive
}

group[log.LogId] = info
group[base64.StdEncoding.EncodeToString(log.LogID)] = info
}
result[op.Name] = group
}
Expand Down
56 changes: 29 additions & 27 deletions ctpolicy/loglist/loglist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"testing"
"time"

"github.com/google/certificate-transparency-go/loglist3"

"github.com/letsencrypt/boulder/test"
)

Expand Down Expand Up @@ -56,24 +58,24 @@ func TestSubset(t *testing.T) {
func TestForPurpose(t *testing.T) {
input := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A2": Log{Name: "Log A2", State: rejected},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: usable},
"ID B2": Log{Name: "Log B2", State: retired},
"ID B1": Log{Name: "Log B1", State: loglist3.UsableLogStatus},
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}
expected := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: usable},
"ID B1": Log{Name: "Log B1", State: loglist3.UsableLogStatus},
},
}
actual, err := input.forPurpose(Issuance)
Expand All @@ -82,27 +84,27 @@ func TestForPurpose(t *testing.T) {

input = List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A2": Log{Name: "Log A2", State: rejected},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B2": Log{Name: "Log B2", State: retired},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}
_, err = input.forPurpose(Issuance)
test.AssertError(t, err, "should only have one acceptable log")

expected = List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator C": {
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}
actual, err = input.forPurpose(Validation)
Expand All @@ -111,13 +113,13 @@ func TestForPurpose(t *testing.T) {

expected = List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
},
}
actual, err = input.forPurpose(Informational)
Expand All @@ -128,10 +130,10 @@ func TestForPurpose(t *testing.T) {
func TestOperatorForLogID(t *testing.T) {
input := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
},
}

Expand All @@ -146,16 +148,16 @@ func TestOperatorForLogID(t *testing.T) {
func TestPermute(t *testing.T) {
input := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A2": Log{Name: "Log A2", State: rejected},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B2": Log{Name: "Log B2", State: retired},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}

Expand Down
Loading

0 comments on commit 8a01611

Please sign in to comment.