From 5216c1d5c607dcc2ef7a6257dc2a6e604a0174af Mon Sep 17 00:00:00 2001 From: Rob Stradling Date: Thu, 10 Sep 2026 16:50:09 +0100 Subject: [PATCH] Reject custom test log lists whose logs lack the "test" marker A custom test log list (strategy.testLogListFilename) replaces the test TLS logs but never touches the active/usable lists. If one of its logs is also present in the active log list, it would appear in active_tls_logs.json too. Require every log and tiled log in a custom test log list to set log_type: "test". validateLogList now rejects any that don't, so LoadCustomTestLogList fails and ctsubmit refuses to start rather than serving a non-test log as active/usable. --- docs/INSTALL.md | 4 ++-- loglists/customTestLogs.go | 8 ++++++++ loglists/customTestLogs_test.go | 21 +++++++++++++++------ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 4c91e3c..e6d17f3 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -129,7 +129,7 @@ strategy: tryNextResponseThreshold: 500ms # Start submitting to the next log after this long. slowResponseThreshold: 2s # Mark a response as "slow" after this long. httpTimeout: 15s # HTTP client timeout for log submissions. - testLogListFilename: "" # Path to a log list JSON file that replaces the built-in test logs. + testLogListFilename: "" # Path to a log list JSON file that replaces the built-in test TLS logs. Every log must set "log_type": "test". sthMonitor: refreshInterval: 30s # How often to fetch each log's latest STH/checkpoint. @@ -189,7 +189,7 @@ logging: | `strategy.submission.tryNextResponseThreshold` | `500ms` | Time to wait before speculatively starting a submission to the next log. | | `strategy.submission.slowResponseThreshold` | `2s` | Time after which a response is recorded as "slow" for future dispreferal. | | `strategy.submission.httpTimeout` | `15s` | HTTP client timeout for submissions to CT logs. | -| `strategy.testLogListFilename` | _(empty)_ | Path to a [v3 log list JSON](https://www.gstatic.com/ct/log_list/v3/log_list_schema.json) file. When set, its logs replace the test logs derived from `ctloglists`, and are used for `test_tls_logs.json` and for submissions that request test logs. Chain validation is skipped for logs with no known accepted roots, so the log itself decides whether to accept a submission. | +| `strategy.testLogListFilename` | _(empty)_ | Path to a [v3 log list JSON](https://www.gstatic.com/ct/log_list/v3/log_list_schema.json) file. When set, its logs replace the test TLS logs derived from `ctloglists`, and are used for `test_tls_logs.json` and for TLS certificate submissions that request test logs. Every log (and tiled log) in the file must set `"log_type": "test"`; ctsubmit refuses to start otherwise. Chain validation is skipped for logs with no known accepted roots, so the log itself decides whether to accept a submission. | #### STH Monitor diff --git a/loglists/customTestLogs.go b/loglists/customTestLogs.go index daba12d..a7e9fcd 100644 --- a/loglists/customTestLogs.go +++ b/loglists/customTestLogs.go @@ -70,6 +70,10 @@ func validateLogList(logList *loglist3.LogList) error { if log.URL == "" { return fmt.Errorf("log %q: url is required", log.Description) } + // Every custom log must be a test log, so it can never leak into the usable/active log lists. + if log.Type != "test" { + return fmt.Errorf("log %q: log_type must be \"test\", got %q", log.Description, log.Type) + } if err := validateLogID(log.LogID, log.Key); err != nil { return fmt.Errorf("log %q: %w", log.Description, err) } @@ -81,6 +85,10 @@ func validateLogList(logList *loglist3.LogList) error { if tiledLog.MonitoringURL == "" { return fmt.Errorf("tiled log %q: monitoring_url is required", tiledLog.Description) } + // Every custom log must be a test log, so it can never leak into the usable/active log lists. + if tiledLog.Type != "test" { + return fmt.Errorf("tiled log %q: log_type must be \"test\", got %q", tiledLog.Description, tiledLog.Type) + } if err := validateLogID(tiledLog.LogID, tiledLog.Key); err != nil { return fmt.Errorf("tiled log %q: %w", tiledLog.Description, err) } diff --git a/loglists/customTestLogs_test.go b/loglists/customTestLogs_test.go index 113b350..66d78cd 100644 --- a/loglists/customTestLogs_test.go +++ b/loglists/customTestLogs_test.go @@ -44,6 +44,7 @@ func writeCustomTestLogList(t *testing.T, timestamp string) (string, [sha256.Siz "key": base64.StdEncoding.EncodeToString(spki), "url": "https://custom.test.log.example.com/", "mmd": 86400, + "log_type": "test", }, }, }, @@ -187,25 +188,33 @@ func TestValidateLogList(t *testing.T) { wantError bool }{ "valid log": { - logList: newLogList(&loglist3.Log{LogID: logID[:], Key: spki, URL: "https://log.example.com/"}, nil), + logList: newLogList(&loglist3.Log{LogID: logID[:], Key: spki, URL: "https://log.example.com/", Type: "test"}, nil), }, "valid tiled log": { - logList: newLogList(nil, &loglist3.TiledLog{LogID: logID[:], Key: spki, SubmissionURL: "https://log.example.com/", MonitoringURL: "https://log.example.com/"}), + logList: newLogList(nil, &loglist3.TiledLog{LogID: logID[:], Key: spki, SubmissionURL: "https://log.example.com/", MonitoringURL: "https://log.example.com/", Type: "test"}), }, "log without url": { - logList: newLogList(&loglist3.Log{LogID: logID[:], Key: spki}, nil), + logList: newLogList(&loglist3.Log{LogID: logID[:], Key: spki, Type: "test"}, nil), + wantError: true, + }, + "log without test marker": { + logList: newLogList(&loglist3.Log{LogID: logID[:], Key: spki, URL: "https://log.example.com/"}, nil), wantError: true, }, "tiled log without submission_url": { - logList: newLogList(nil, &loglist3.TiledLog{LogID: logID[:], Key: spki, MonitoringURL: "https://log.example.com/"}), + logList: newLogList(nil, &loglist3.TiledLog{LogID: logID[:], Key: spki, MonitoringURL: "https://log.example.com/", Type: "test"}), wantError: true, }, "tiled log without monitoring_url": { - logList: newLogList(nil, &loglist3.TiledLog{LogID: logID[:], Key: spki, SubmissionURL: "https://log.example.com/"}), + logList: newLogList(nil, &loglist3.TiledLog{LogID: logID[:], Key: spki, SubmissionURL: "https://log.example.com/", Type: "test"}), + wantError: true, + }, + "tiled log without test marker": { + logList: newLogList(nil, &loglist3.TiledLog{LogID: logID[:], Key: spki, SubmissionURL: "https://log.example.com/", MonitoringURL: "https://log.example.com/"}), wantError: true, }, "log_id does not match key": { - logList: newLogList(&loglist3.Log{LogID: make([]byte, sha256.Size), Key: spki, URL: "https://log.example.com/"}, nil), + logList: newLogList(&loglist3.Log{LogID: make([]byte, sha256.Size), Key: spki, URL: "https://log.example.com/", Type: "test"}, nil), wantError: true, }, }