Repository: google/certificate-transparency-go
Pull request #925 (2022-05-06 22:16:01)

Update to use the loglist v3 schema everywhere
The `loglist3` package appears complete and is in use in a few places
throughout the repository. However, most of the repo still relies on the
`loglist2` package. Although Chrome is still publishing log lists in both
formats, it seems appropriate to update the preponderance of usage to
the new schema.

This also adds a v3-formatted testdata file, with the same contents as
the corresponding v2 file. (This file will need to be updated when the
//ctpolicy/chromepolicy.go logic is updated, as it does not contain enough
different operator groups to truly exercise the new Chrome policy, but
that is an issue for #906).

### Checklist

- [ ] I have updated the [CHANGELOG](CHANGELOG.md).
  - It doesn't seem to be necessary for a change this internal?
- [X] I have updated [documentation](docs/) accordingly.


pavelkalinnikov: /gcbrun
roger2hk: /gcbrun
roger2hk: /gcbrun

Patch:
diff --git a/README.md b/README.md
index c3147152..e440f3ec 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,8 @@ The main parts of the repository are:
    - `loglist/` has a library for reading v1 JSON lists of CT Logs.
    - `loglist2/` has a library for reading
      [v2 JSON lists of CT Logs](https://www.certificate-transparency.org/known-logs).
+   - `loglist3/` has a library for reading
+     [v3 JSON lists of CT Logs](https://groups.google.com/a/chromium.org/g/ct-policy/c/IdbrdAcDQto/m/i5KPyzYwBAAJ).
 
 
 ## Trillian CT Personality
diff --git a/ctpolicy/applepolicy.go b/ctpolicy/applepolicy.go
index f5abef6e..f2088c80 100644
--- a/ctpolicy/applepolicy.go
+++ b/ctpolicy/applepolicy.go
@@ -15,7 +15,7 @@
 package ctpolicy
 
 import (
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/x509"
 )
 
@@ -25,7 +25,7 @@ type AppleCTPolicy struct{}
 // LogsByGroup describes submission requirements for embedded SCTs according to
 // https://support.apple.com/en-us/HT205280. Returns an error if it's not
 // possible to satisfy the policy with the provided loglist.
-func (appleP AppleCTPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist2.LogList) (LogPolicyData, error) {
+func (appleP AppleCTPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist3.LogList) (LogPolicyData, error) {
 	var incCount int
 	switch m := lifetimeInMonths(cert); {
 	case m < 15:
diff --git a/ctpolicy/chromepolicy.go b/ctpolicy/chromepolicy.go
index c3aae941..fbfd5fed 100644
--- a/ctpolicy/chromepolicy.go
+++ b/ctpolicy/chromepolicy.go
@@ -15,7 +15,7 @@
 package ctpolicy
 
 import (
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/x509"
 )
 
@@ -26,15 +26,15 @@ type ChromeCTPolicy struct {
 // LogsByGroup describes submission requirements for embedded SCTs according to
 // https://github.com/chromium/ct-policy/blob/master/ct_policy.md#qualifying-certificate.
 // Returns an error if it's not possible to satisfy the policy with the provided loglist.
-func (chromeP ChromeCTPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist2.LogList) (LogPolicyData, error) {
+func (chromeP ChromeCTPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist3.LogList) (LogPolicyData, error) {
 	googGroup := LogGroupInfo{Name: "Google-operated", IsBase: false}
-	googGroup.populate(approved, func(op *loglist2.Operator) bool { return op.GoogleOperated() })
+	googGroup.populate(approved, func(op *loglist3.Operator) bool { return op.GoogleOperated() })
 	if err := googGroup.setMinInclusions(1); err != nil {
 		return nil, err
 	}
 
 	nonGoogGroup := LogGroupInfo{Name: "Non-Google-operated", IsBase: false}
-	nonGoogGroup.populate(approved, func(op *loglist2.Operator) bool { return !op.GoogleOperated() })
+	nonGoogGroup.populate(approved, func(op *loglist3.Operator) bool { return !op.GoogleOperated() })
 	if err := nonGoogGroup.setMinInclusions(1); err != nil {
 		return nil, err
 	}
diff --git a/ctpolicy/ctpolicy.go b/ctpolicy/ctpolicy.go
index 06d2e8d7..a250eadd 100644
--- a/ctpolicy/ctpolicy.go
+++ b/ctpolicy/ctpolicy.go
@@ -19,7 +19,7 @@ import (
 	"fmt"
 	"sync"
 
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/x509"
 )
 
@@ -50,7 +50,7 @@ func (group *LogGroupInfo) setMinInclusions(i int) error {
 	return nil
 }
 
-func (group *LogGroupInfo) populate(ll *loglist2.LogList, included func(op *loglist2.Operator) bool) {
+func (group *LogGroupInfo) populate(ll *loglist3.LogList, included func(op *loglist3.Operator) bool) {
 	group.LogURLs = make(map[string]bool)
 	group.LogWeights = make(map[string]float32)
 	for _, op := range ll.Operators {
@@ -180,14 +180,14 @@ func (groups LogPolicyData) TotalLogs() int {
 type CTPolicy interface {
 	// LogsByGroup provides info on Log-grouping. Returns an error if it's not
 	// possible to satisfy the policy with the provided loglist.
-	LogsByGroup(cert *x509.Certificate, approved *loglist2.LogList) (LogPolicyData, error)
+	LogsByGroup(cert *x509.Certificate, approved *loglist3.LogList) (LogPolicyData, error)
 	Name() string
 }
 
 // BaseGroupFor creates and propagates all-log group.
-func BaseGroupFor(approved *loglist2.LogList, incCount int) (*LogGroupInfo, error) {
+func BaseGroupFor(approved *loglist3.LogList, incCount int) (*LogGroupInfo, error) {
 	baseGroup := LogGroupInfo{Name: BaseName, IsBase: true}
-	baseGroup.populate(approved, func(op *loglist2.Operator) bool { return true })
+	baseGroup.populate(approved, func(op *loglist3.Operator) bool { return true })
 	err := baseGroup.setMinInclusions(incCount)
 	return &baseGroup, err
 }
diff --git a/internal/witness/README.md b/internal/witness/README.md
index 1fe99fb7..ebb12fda 100644
--- a/internal/witness/README.md
+++ b/internal/witness/README.md
@@ -32,6 +32,6 @@ this directory, with the following flags:
     - `logID`, which is the alphanumeric identifier for the log.
     - `pubKey`, which is the base64-encoded public key of the log.
   Both of these fields should be populated using an "official" 
-  [CT log list](https://www.gstatic.com/ct/log_list/v2/log_list.json).
+  [CT log list](https://www.gstatic.com/ct/log_list/v3/log_list.json).
 - `private_key`, which specifies the private signing key of the witness.  In its
   current state the witness does not sign STHs so this can exist in any form.
diff --git a/internal/witness/cmd/feeder/main.go b/internal/witness/cmd/feeder/main.go
index d289ff71..8b5a9843 100644
--- a/internal/witness/cmd/feeder/main.go
+++ b/internal/witness/cmd/feeder/main.go
@@ -35,7 +35,7 @@ import (
 	"github.com/google/certificate-transparency-go/client"
 	wh "github.com/google/certificate-transparency-go/internal/witness/client/http"
 	"github.com/google/certificate-transparency-go/jsonclient"
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 )
 
 var (
@@ -63,11 +63,11 @@ func populateLogs(logListURL string) ([]ctLog, error) {
 		return nil, fmt.Errorf("failed to get log list data: %v", err)
 	}
 	// Get data for all usable logs.
-	logList, err := loglist2.NewFromJSON(body)
+	logList, err := loglist3.NewFromJSON(body)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse JSON: %v", err)
 	}
-	usable := logList.SelectByStatus([]loglist2.LogStatus{loglist2.UsableLogStatus})
+	usable := logList.SelectByStatus([]loglist3.LogStatus{loglist3.UsableLogStatus})
 	var logs []ctLog
 	for _, operator := range usable.Operators {
 		for _, log := range operator.Logs {
diff --git a/internal/witness/cmd/witness/config/config.go b/internal/witness/cmd/witness/config/config.go
index 0a1a6843..f50cd6ac 100644
--- a/internal/witness/cmd/witness/config/config.go
+++ b/internal/witness/cmd/witness/config/config.go
@@ -25,7 +25,7 @@ import (
 
 	"github.com/golang/glog"
 	"github.com/google/certificate-transparency-go/internal/witness/cmd/witness/impl"
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"gopkg.in/yaml.v3"
 )
 
@@ -47,12 +47,12 @@ func main() {
 		glog.Exitf("Failed to get log list data: %v", err)
 	}
 	// Get data for all usable logs.
-	logList, err := loglist2.NewFromJSON(body)
+	logList, err := loglist3.NewFromJSON(body)
 	if err != nil {
 		glog.Exitf("failed to parse JSON: %v", err)
 	}
 	var config impl.LogConfig
-	usable := logList.SelectByStatus([]loglist2.LogStatus{loglist2.UsableLogStatus})
+	usable := logList.SelectByStatus([]loglist3.LogStatus{loglist3.UsableLogStatus})
 	for _, operator := range usable.Operators {
 		for _, log := range operator.Logs {
 			key := base64.StdEncoding.EncodeToString(log.Key)
diff --git a/loglist2/loglist2.go b/loglist2/loglist2.go
index 43863d64..861df4da 100644
--- a/loglist2/loglist2.go
+++ b/loglist2/loglist2.go
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 // Package loglist2 allows parsing and searching of the master CT Log list.
-// It expects the log list to conform to the v2beta schema.
+// It expects the log list to conform to the v2 schema.
 package loglist2
 
 import (
@@ -43,7 +43,7 @@ const (
 	AllLogListURL = "https://www.gstatic.com/ct/log_list/v2/all_logs_list.json"
 )
 
-// Manually mapped from https://www.gstatic.com/ct/log_list/v2beta/log_list_schema.json
+// Manually mapped from https://www.gstatic.com/ct/log_list/v2/log_list_schema.json
 
 // LogList holds a collection of CT logs, grouped by operator.
 type LogList struct {
diff --git a/submission/distributor.go b/submission/distributor.go
index 5b5c0d42..7ce60cf6 100644
--- a/submission/distributor.go
+++ b/submission/distributor.go
@@ -27,7 +27,7 @@ import (
 	"github.com/google/certificate-transparency-go/client"
 	"github.com/google/certificate-transparency-go/ctpolicy"
 	"github.com/google/certificate-transparency-go/jsonclient"
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/trillian/ctfe"
 	"github.com/google/certificate-transparency-go/x509"
 	"github.com/google/certificate-transparency-go/x509util"
@@ -66,7 +66,7 @@ const (
 type pendingLogsPolicy struct {
 }
 
-func (stubP pendingLogsPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist2.LogList) (ctpolicy.LogPolicyData, error) {
+func (stubP pendingLogsPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist3.LogList) (ctpolicy.LogPolicyData, error) {
 	baseGroup, err := ctpolicy.BaseGroupFor(approved, 1)
 	groups := ctpolicy.LogPolicyData{baseGroup.Name: baseGroup}
 	return groups, err
@@ -78,15 +78,15 @@ func (stubP pendingLogsPolicy) Name() string {
 
 // Distributor operates policy-based submission across Logs.
 type Distributor struct {
-	ll                 *loglist2.LogList
-	usableLl           *loglist2.LogList
-	pendingQualifiedLl *loglist2.LogList
+	ll                 *loglist3.LogList
+	usableLl           *loglist3.LogList
+	pendingQualifiedLl *loglist3.LogList
 
 	mu sync.RWMutex
 
 	// helper structs produced out of ll during init.
 	logClients map[string]client.AddLogClient
-	logRoots   loglist2.LogRoots
+	logRoots   loglist3.LogRoots
 	rootPool   *x509util.PEMCertPool
 
 	rootDataFull bool
@@ -140,7 +140,7 @@ func (d *Distributor) RefreshRoots(ctx context.Context) map[string]error {
 	}
 
 	// Collect get-roots results for every Log-client.
-	freshRoots := make(loglist2.LogRoots)
+	freshRoots := make(loglist3.LogRoots)
 	errors := make(map[string]error)
 	for range d.logClients {
 		r := <-ch
@@ -251,7 +251,7 @@ func (d *Distributor) addSomeChain(ctx context.Context, rawChain [][]byte, loadP
 	}
 
 	// Helper function establishing responsibility of locking while determining log list and root chain.
-	compatibleLogsAndChain := func() (loglist2.LogList, []*x509.Certificate, error) {
+	compatibleLogsAndChain := func() (loglist3.LogList, []*x509.Certificate, error) {
 		d.mu.RLock()
 		defer d.mu.RUnlock()
 		vOpts := ctfe.NewCertValidationOpts(d.rootPool, time.Time{}, false, false, nil, nil, false, nil)
@@ -261,13 +261,13 @@ func (d *Distributor) addSomeChain(ctx context.Context, rawChain [][]byte, loadP
 		}
 		if d.rootDataFull {
 			// Could not verify the chain while root info for logs is complete.
-			return loglist2.LogList{}, nil, fmt.Errorf("distributor unable to process cert-chain: %v", err)
+			return loglist3.LogList{}, nil, fmt.Errorf("distributor unable to process cert-chain: %v", err)
 		}
 
 		// Chain might be rooted to the Log which has no root-info yet.
 		parsedChain, err := parseRawChain(rawChain)
 		if err != nil {
-			return loglist2.LogList{}, nil, fmt.Errorf("distributor unable to parse cert-chain: %v", err)
+			return loglist3.LogList{}, nil, fmt.Errorf("distributor unable to parse cert-chain: %v", err)
 		}
 		return d.usableLl.Compatible(parsedChain[0], nil, d.logRoots), parsedChain, nil
 	}
@@ -328,10 +328,10 @@ func (d *Distributor) AddChain(ctx context.Context, rawChain [][]byte, loadPendi
 }
 
 // LogClientBuilder builds client-interface instance for a given Log.
-type LogClientBuilder func(*loglist2.Log) (client.AddLogClient, error)
+type LogClientBuilder func(*loglist3.Log) (client.AddLogClient, error)
 
 // BuildLogClient is default (non-mock) LogClientBuilder.
-func BuildLogClient(log *loglist2.Log) (client.AddLogClient, error) {
+func BuildLogClient(log *loglist3.Log) (client.AddLogClient, error) {
 	u, err := url.Parse(log.URL)
 	if err != nil {
 		return nil, err
@@ -347,22 +347,22 @@ func BuildLogClient(log *loglist2.Log) (client.AddLogClient, error) {
 // The Distributor will asynchronously fetch the latest roots from all of the
 // logs when active. Call Run() to fetch roots and init regular updates to keep
 // the local copy of the roots up-to-date.
-func NewDistributor(ll *loglist2.LogList, plc ctpolicy.CTPolicy, lcBuilder LogClientBuilder, mf monitoring.MetricFactory) (*Distributor, error) {
+func NewDistributor(ll *loglist3.LogList, plc ctpolicy.CTPolicy, lcBuilder LogClientBuilder, mf monitoring.MetricFactory) (*Distributor, error) {
 	var d Distributor
 	// Divide Logs by statuses.
 	d.ll = ll
-	usableStat := []loglist2.LogStatus{loglist2.UsableLogStatus}
+	usableStat := []loglist3.LogStatus{loglist3.UsableLogStatus}
 	active := ll.SelectByStatus(usableStat)
 	d.usableLl = &active
-	pendingQualifiedStat := []loglist2.LogStatus{
-		loglist2.PendingLogStatus, loglist2.QualifiedLogStatus}
+	pendingQualifiedStat := []loglist3.LogStatus{
+		loglist3.PendingLogStatus, loglist3.QualifiedLogStatus}
 	pending := ll.SelectByStatus(pendingQualifiedStat)
 	d.pendingQualifiedLl = &pending
 
 	d.policy = plc
 	d.pendingLogsPolicy = pendingLogsPolicy{}
 	d.logClients = make(map[string]client.AddLogClient)
-	d.logRoots = make(loglist2.LogRoots)
+	d.logRoots = make(loglist3.LogRoots)
 	d.rootPool = x509util.NewPEMCertPool()
 
 	// Build clients for each of the Logs. Also build log-to-id map.
@@ -380,7 +380,7 @@ func NewDistributor(ll *loglist2.LogList, plc ctpolicy.CTPolicy, lcBuilder LogCl
 
 // buildLogClients builds clients for every Log provided and adds them into
 // Distributor internals.
-func (d *Distributor) buildLogClients(lcBuilder LogClientBuilder, ll *loglist2.LogList) error {
+func (d *Distributor) buildLogClients(lcBuilder LogClientBuilder, ll *loglist3.LogList) error {
 	for _, op := range ll.Operators {
 		for _, log := range op.Logs {
 			lc, err := lcBuilder(log)
diff --git a/submission/loglist_refresher.go b/submission/loglist_refresher.go
index 0c315397..3b83b0f7 100644
--- a/submission/loglist_refresher.go
+++ b/submission/loglist_refresher.go
@@ -21,7 +21,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/x509util"
 )
 
@@ -34,7 +34,7 @@ const (
 // of download.
 type LogListData struct {
 	JSON         []byte
-	List         *loglist2.LogList
+	List         *loglist3.LogList
 	DownloadTime time.Time
 }
 
@@ -85,7 +85,7 @@ func (llr *logListRefresherImpl) Refresh() (*LogListData, error) {
 		return nil, nil
 	}
 
-	ll, err := loglist2.NewFromJSON(json)
+	ll, err := loglist3.NewFromJSON(json)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse %q: %v", llr.path, err)
 	}
diff --git a/submission/proxy.go b/submission/proxy.go
index b8466133..25284458 100644
--- a/submission/proxy.go
+++ b/submission/proxy.go
@@ -25,7 +25,7 @@ import (
 	ct "github.com/google/certificate-transparency-go"
 	"github.com/google/certificate-transparency-go/asn1"
 	"github.com/google/certificate-transparency-go/ctpolicy"
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/schedule"
 	"github.com/google/certificate-transparency-go/tls"
 	"github.com/google/certificate-transparency-go/x509util"
@@ -54,17 +54,17 @@ func proxyInitMetrics(mf monitoring.MetricFactory) {
 }
 
 // DistributorBuilder builds distributor instance for a given Log list.
-type DistributorBuilder func(*loglist2.LogList) (*Distributor, error)
+type DistributorBuilder func(*loglist3.LogList) (*Distributor, error)
 
 // GetDistributorBuilder given CT-policy type and Log-client builder produces
 // Distributor c-tor.
 func GetDistributorBuilder(plc CTPolicyType, lcBuilder LogClientBuilder, mf monitoring.MetricFactory) DistributorBuilder {
 	if plc == AppleCTPolicy {
-		return func(ll *loglist2.LogList) (*Distributor, error) {
+		return func(ll *loglist3.LogList) (*Distributor, error) {
 			return NewDistributor(ll, ctpolicy.AppleCTPolicy{}, lcBuilder, mf)
 		}
 	}
-	return func(ll *loglist2.LogList) (*Distributor, error) {
+	return func(ll *loglist3.LogList) (*Distributor, error) {
 		return NewDistributor(ll, ctpolicy.ChromeCTPolicy{}, lcBuilder, mf)
 	}
 }
@@ -159,7 +159,7 @@ func (p *Proxy) Run(ctx context.Context, llRefresh time.Duration, rootsRefresh t
 
 // restartDistributor activates new Distributor instance with Log List provided
 // and sets it as active.
-func (p *Proxy) restartDistributor(ctx context.Context, ll *loglist2.LogList) error {
+func (p *Proxy) restartDistributor(ctx context.Context, ll *loglist3.LogList) error {
 	d, err := p.distributorBuilder(ll)
 	if err != nil {
 		// losing ll info. No good.
diff --git a/submission/server/main.go b/submission/server/main.go
index 72f8d025..1282becc 100644
--- a/submission/server/main.go
+++ b/submission/server/main.go
@@ -32,7 +32,7 @@ import (
 // Flags.
 var (
 	httpEndpoint             = flag.String("http_endpoint", "localhost:5951", "Endpoint for HTTP (host:port)")
-	logListPath              = flag.String("loglist_path", "https://www.gstatic.com/ct/log_list/v2/log_list.json", "Path for list of CT Logs in JSON format")
+	logListPath              = flag.String("loglist_path", "https://www.gstatic.com/ct/log_list/v3/log_list.json", "Path for list of CT Logs in JSON format")
 	logListRefreshInterval   = flag.Duration("loglist_refresh_interval", 24*time.Hour, "Interval between consecutive reads of Log-list")
 	rootsRefreshInterval     = flag.Duration("roots_refresh_interval", 24*time.Hour, "Interval between consecutive get-roots calls")
 	policyType               = flag.String("policy_type", "chrome", "CT-policy <chrome|apple>")
diff --git a/submission/stub.go b/submission/stub.go
index 044384fc..5f43cbcf 100644
--- a/submission/stub.go
+++ b/submission/stub.go
@@ -21,7 +21,7 @@ import (
 
 	ct "github.com/google/certificate-transparency-go"
 	"github.com/google/certificate-transparency-go/client"
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/tls"
 	"github.com/google/certificate-transparency-go/x509util"
 )
@@ -94,16 +94,16 @@ func testSCT(logURL string) *ct.SignedCertificateTimestamp {
 	}
 }
 
-func newRootedStubLogClient(log *loglist2.Log, rCerts map[string][]rootInfo) (client.AddLogClient, error) {
+func newRootedStubLogClient(log *loglist3.Log, rCerts map[string][]rootInfo) (client.AddLogClient, error) {
 	return stubLogClient{logURL: log.URL, rootsCerts: rCerts}, nil
 }
 
-func newEmptyStubLogClient(log *loglist2.Log) (client.AddLogClient, error) {
+func newEmptyStubLogClient(log *loglist3.Log) (client.AddLogClient, error) {
 	return newRootedStubLogClient(log, map[string][]rootInfo{})
 }
 
 // NewStubLogClient is builder for log-client stubs. Used for dry-runs and
 // testing.
-func NewStubLogClient(log *loglist2.Log) (client.AddLogClient, error) {
+func NewStubLogClient(log *loglist3.Log) (client.AddLogClient, error) {
 	return stubLogClient{logURL: log.URL, rootsCerts: map[string][]rootInfo{log.URL: {}}}, nil
 }


Test patch:
diff --git a/ctpolicy/ctpolicy_test.go b/ctpolicy/ctpolicy_test.go
index 0f3ae352..0d4c1576 100644
--- a/ctpolicy/ctpolicy_test.go
+++ b/ctpolicy/ctpolicy_test.go
@@ -19,7 +19,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/testdata"
 	"github.com/google/certificate-transparency-go/x509"
 	"github.com/google/certificate-transparency-go/x509util"
@@ -48,12 +48,12 @@ func getTestCertPEMLongOriginal() *x509.Certificate {
 	return cert
 }
 
-func sampleLogList(t *testing.T) *loglist2.LogList {
+func sampleLogList(t *testing.T) *loglist3.LogList {
 	t.Helper()
-	var ll loglist2.LogList
-	err := json.Unmarshal([]byte(testdata.SampleLogList2), &ll)
+	var ll loglist3.LogList
+	err := json.Unmarshal([]byte(testdata.SampleLogList3), &ll)
 	if err != nil {
-		t.Fatalf("Unable to Unmarshal testdata.SampleLogList2 %v", err)
+		t.Fatalf("Unable to Unmarshal testdata.SampleLogList3 %v", err)
 	}
 	return &ll
 }
diff --git a/submission/distributor_test.go b/submission/distributor_test.go
index f55a2c0e..fbae107a 100644
--- a/submission/distributor_test.go
+++ b/submission/distributor_test.go
@@ -26,7 +26,7 @@ import (
 	"github.com/golang/glog"
 	"github.com/google/certificate-transparency-go/client"
 	"github.com/google/certificate-transparency-go/ctpolicy"
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/schedule"
 	"github.com/google/certificate-transparency-go/testdata"
 	"github.com/google/certificate-transparency-go/x509"
@@ -36,7 +36,7 @@ import (
 	"github.com/google/trillian/monitoring"
 )
 
-func newLocalStubLogClient(log *loglist2.Log) (client.AddLogClient, error) {
+func newLocalStubLogClient(log *loglist3.Log) (client.AddLogClient, error) {
 	return newRootedStubLogClient(log, RootsCerts)
 }
 
@@ -100,19 +100,19 @@ var (
 )
 
 // newNoLogClient is LogClientBuilder that always fails.
-func newNoLogClient(_ *loglist2.Log) (client.AddLogClient, error) {
+func newNoLogClient(_ *loglist3.Log) (client.AddLogClient, error) {
 	return nil, errors.New("bad log-client builder")
 }
 
-func sampleLogList() *loglist2.LogList {
-	var ll loglist2.LogList
+func sampleLogList() *loglist3.LogList {
+	var ll loglist3.LogList
 	if err := json.Unmarshal([]byte(testdata.SampleLogList2), &ll); err != nil {
 		panic(fmt.Errorf("unable to Unmarshal testdata.SampleLogList: %v", err))
 	}
 	return &ll
 }
 
-func sampleValidLogList() *loglist2.LogList {
+func sampleValidLogList() *loglist3.LogList {
 	ll := sampleLogList()
 	// Id of invalid Log description Racketeer
 	inval := 2
@@ -120,15 +120,15 @@ func sampleValidLogList() *loglist2.LogList {
 	return ll
 }
 
-func sampleUncollectableLogList() *loglist2.LogList {
+func sampleUncollectableLogList() *loglist3.LogList {
 	ll := sampleValidLogList()
 	// Append loglist that is unable to provide roots on request.
-	ll.Operators[0].Logs = append(ll.Operators[0].Logs, &loglist2.Log{
+	ll.Operators[0].Logs = append(ll.Operators[0].Logs, &loglist3.Log{
 		Description: "Does not return roots", Key: []byte("VW5jb2xsZWN0YWJsZUxvZ0xpc3Q="),
 		URL:   "uncollectable-roots/log/",
 		DNS:   "uncollectavle.ct.googleapis.com",
 		MMD:   123,
-		State: &loglist2.LogStates{Usable: &loglist2.LogState{}},
+		State: &loglist3.LogStates{Usable: &loglist3.LogState{}},
 	})
 	return ll
 }
@@ -136,7 +136,7 @@ func sampleUncollectableLogList() *loglist2.LogList {
 func TestNewDistributorLogClients(t *testing.T) {
 	testCases := []struct {
 		name      string
-		ll        *loglist2.LogList
+		ll        *loglist3.LogList
 		lcBuilder LogClientBuilder
 		errRegexp *regexp.Regexp
 	}{
@@ -153,7 +153,7 @@ func TestNewDistributorLogClients(t *testing.T) {
 		},
 		{
 			name:      "NoLogClientsEmptyLogList",
-			ll:        &loglist2.LogList{},
+			ll:        &loglist3.LogList{},
 			lcBuilder: newNoLogClient,
 		},
 	}
@@ -177,7 +177,7 @@ func TestNewDistributorLogClients(t *testing.T) {
 func TestNewDistributorRootPools(t *testing.T) {
 	testCases := []struct {
 		name     string
-		ll       *loglist2.LogList
+		ll       *loglist3.LogList
 		rootNum  map[string]int
 		wantErrs int
 	}{
@@ -240,7 +240,7 @@ func buildStubCTPolicy(n int) stubCTPolicy {
 	return stubCTPolicy{baseNum: n}
 }
 
-func (stubP stubCTPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist2.LogList) (ctpolicy.LogPolicyData, error) {
+func (stubP stubCTPolicy) LogsByGroup(cert *x509.Certificate, approved *loglist3.LogList) (ctpolicy.LogPolicyData, error) {
 	baseGroup, err := ctpolicy.BaseGroupFor(approved, stubP.baseNum)
 	groups := ctpolicy.LogPolicyData{baseGroup.Name: baseGroup}
 	return groups, err
@@ -253,7 +253,7 @@ func (stubP stubCTPolicy) Name() string {
 func TestDistributorAddChain(t *testing.T) {
 	testCases := []struct {
 		name         string
-		ll           *loglist2.LogList
+		ll           *loglist3.LogList
 		plc          ctpolicy.CTPolicy
 		pemChainFile string
 		getRoots     bool
@@ -349,7 +349,7 @@ func TestDistributorAddChain(t *testing.T) {
 func TestDistributorAddPreChain(t *testing.T) {
 	testCases := []struct {
 		name         string
-		ll           *loglist2.LogList
+		ll           *loglist3.LogList
 		plc          ctpolicy.CTPolicy
 		pemChainFile string
 		getRoots     bool
diff --git a/submission/loglist_refresher_test.go b/submission/loglist_refresher_test.go
index 686f7d03..f1298a36 100644
--- a/submission/loglist_refresher_test.go
+++ b/submission/loglist_refresher_test.go
@@ -25,7 +25,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/schedule"
 	"github.com/google/go-cmp/cmp"
 )
@@ -117,13 +117,13 @@ func TestNewLogListRefresher(t *testing.T) {
 	testCases := []struct {
 		name      string
 		ll        string
-		wantLl    *loglist2.LogList
+		wantLl    *loglist3.LogList
 		errRegexp *regexp.Regexp
 	}{
 		{
 			name:   "SuccessfulRead",
 			ll:     `{"operators": [{"id":0,"name":"Google"}]}`,
-			wantLl: &loglist2.LogList{Operators: []*loglist2.Operator{{Name: "Google"}}},
+			wantLl: &loglist3.LogList{Operators: []*loglist3.Operator{{Name: "Google"}}},
 		},
 		{
 			name:      "CannotParseInput",
@@ -173,7 +173,7 @@ func TestNewLogListRefresherUpdate(t *testing.T) {
 		name      string
 		ll        string
 		llNext    string
-		wantLl    *loglist2.LogList
+		wantLl    *loglist3.LogList
 		errRegexp *regexp.Regexp
 	}{
 		{
@@ -187,7 +187,7 @@ func TestNewLogListRefresherUpdate(t *testing.T) {
 			name:      "LogListUpdated",
 			ll:        `{"operators": [{"id":0,"name":"Google"}]}`,
 			llNext:    `{"operators": [{"id":0,"name":"GoogleOps"}]}`,
-			wantLl:    &loglist2.LogList{Operators: []*loglist2.Operator{{Name: "GoogleOps"}}},
+			wantLl:    &loglist3.LogList{Operators: []*loglist3.Operator{{Name: "GoogleOps"}}},
 			errRegexp: nil,
 		},
 		{
diff --git a/submission/proxy_test.go b/submission/proxy_test.go
index 4d11e566..736dd780 100644
--- a/submission/proxy_test.go
+++ b/submission/proxy_test.go
@@ -27,7 +27,7 @@ import (
 
 	ct "github.com/google/certificate-transparency-go"
 	"github.com/google/certificate-transparency-go/client"
-	"github.com/google/certificate-transparency-go/loglist2"
+	"github.com/google/certificate-transparency-go/loglist3"
 	"github.com/google/certificate-transparency-go/testdata"
 	"github.com/google/certificate-transparency-go/tls"
 	"github.com/google/trillian/monitoring"
@@ -88,7 +88,7 @@ func (m stubNoRootsLogClient) GetAcceptedRoots(ctx context.Context) ([]ct.ASN1Ce
 	return nil, fmt.Errorf("stubNoRootsLogClient cannot provide roots")
 }
 
-func buildStubNoRootsLogClient(log *loglist2.Log) (client.AddLogClient, error) {
+func buildStubNoRootsLogClient(log *loglist3.Log) (client.AddLogClient, error) {
 	return stubNoRootsLogClient{logURL: log.URL}, nil
 }
 
diff --git a/testdata/loglist3.go b/testdata/loglist3.go
new file mode 100644
index 00000000..6cfad930
--- /dev/null
+++ b/testdata/loglist3.go
@@ -0,0 +1,20 @@
+// Copyright 2018 Google LLC. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package testdata
+
+import _ "embed"
+
+//go:embed loglist3.json
+var SampleLogList3 string
diff --git a/testdata/loglist3.json b/testdata/loglist3.json
new file mode 100644
index 00000000..b2467cf8
--- /dev/null
+++ b/testdata/loglist3.json
@@ -0,0 +1,101 @@
+{
+  "version": "9.4",
+  "log_list_timestamp": "2022-05-06T12:55:11Z",
+  "operators": [
+    {
+      "name": "Google",
+      "email": [
+        "google-ct-logs@googlegroups.com"
+      ],
+      "logs": [
+        {
+          "description": "Google 'Aviator' log",
+          "log_id": "aPaY+B9kgr46jO65KB1M/HFRXWeT1ETRCmesu09P+8Q=",
+          "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1/TMabLkDpCjiupacAlP7xNi0I1JYP8bQFAHDG1xhtolSY1l4QgNRzRrvSe8liE+NPWHdjGxfx3JhTsN9x8/6Q==",
+          "url": "https://ct.googleapis.com/aviator/",
+          "mmd": 86400,
+          "state":{
+            "readonly": {
+              "timestamp": "2016-11-30T13:24:18.33Z",
+              "final_tree_head": {
+                "sha256_root_hash": "LcGcZRsm+LGYmrlyC5LXhV1T6OD8iH5dNlb0sEJl9bA=",
+                "tree_size": 46466472
+              }
+            }
+          }
+        },
+        {
+          "description": "Google 'Icarus' log",
+          "log_id": "KTxRllTIOWW6qlD8WAfUt2+/WHopctykwwz05UVH9Hg=",
+          "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETtK8v7MICve56qTHHDhhBOuV4IlUaESxZryCfk9QbG9co/CqPvTsgPDbCpp6oFtyAHwlDhnvr7JijXRD9Cb2FA==",
+          "url": "https://ct.googleapis.com/icarus/",
+          "mmd": 86400,
+          "state": {
+            "usable": {
+              "timestamp": "2018-02-27T00:00:00Z"
+            }
+          }
+        },
+        {
+          "description": "Google 'Racketeer' log",
+          "log_id": "7kEv4llINIlh4vPgjGgugT7A/3cLbXUXF2OvMBT/l2g=",
+          "key": "Hy2TPTZ2yq9ASMmMZiB9SZEUx5WNH5G0Ft5Tm9vKMcPXA+ic/Ap3gg6fXzBJR8zLkt5lQjvKMdbHYMGv7yrsZg==",
+          "url": "https://ct.googleapis.com/racketeer/",
+          "mmd": 86400
+        },
+        {
+          "description": "Google 'Rocketeer' log",
+          "log_id": "7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/cs=",
+          "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg==",
+          "url": "https://ct.googleapis.com/rocketeer/",
+          "mmd": 86400,
+          "state": {
+            "usable": {
+              "timestamp": "2018-02-27T00:00:00Z"
+            }
+          }
+        },
+        {
+          "description": "Google 'Argon2020' log",
+          "log_id": "sh4FzIuizYogTodm+Su5iiUgZ2va+nDnsklTLe+LkF4=",
+          "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6Tx2p1yKY4015NyIYvdrk36es0uAc1zA4PQ+TGRY+3ZjUTIYY9Wyu+3q/147JG4vNVKLtDWarZwVqGkg6lAYzA==",
+          "url": "https://ct.googleapis.com/logs/argon2020/",
+          "mmd": 86400,
+          "state": {
+            "qualified": {
+              "timestamp": "2018-02-27T00:00:00Z"
+            }
+          },
+          "temporal_interval": {
+            "start_inclusive": "2018-02-27T00:00:00Z",
+            "end_exclusive": "2020-01-01T00:00:00Z"
+          }
+        }
+      ]
+    },
+    {
+      "name": "Bob's CT Log Shop",
+      "email": [
+        "bob@example.com"
+      ],
+      "logs": [
+        {
+          "description": "Bob's Dubious Log",
+          "log_id": "zbUXm3/BwEb+6jETaj+PAC5hgvr4iW/syLL1tatgSQA=",
+          "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECyPLhWKYYUgEc+tUXfPQB4wtGS2MNvXrjwFCCnyYJifBtd2Sk7Cu+Js9DNhMTh35FftHaHu6ZrclnNBKwmbbSA==",
+          "url": "https://log.bob.io",
+          "mmd": 86400,
+          "state": {
+            "retired": {
+              "timestamp": "2016-04-15T00:00:00Z"
+            }
+          },
+          "temporal_interval": {
+            "start_inclusive": "2014-11-07T12:00:00Z",
+            "end_exclusive": "2015-03-07T12:00:00Z"
+          }
+        }
+      ]
+    }
+  ]
+}