diff --git a/cmd/skylight/skylight.go b/cmd/skylight/skylight.go index e6d8880..7f0dda5 100644 --- a/cmd/skylight/skylight.go +++ b/cmd/skylight/skylight.go @@ -84,6 +84,10 @@ type Config struct { // HomeRedirect is the 302 destination of the root. Optional. HomeRedirect string + // OperatorName is the human-readable name of the CT log operator, + // served at /logs.json per the operator-list-v1 schema. Optional. + OperatorName string + Logs []LogConfig } @@ -321,13 +325,19 @@ func main() { handler = promhttp.InstrumentHandlerResponseSize(resSize.MustCurryWith(labels), handler, promhttp.WithLabelFromCtx("kind", kindFromContext)) - // Then, apply the rate limit handler. - handler = newRateLimitHandler(handler) + // Then, apply the rate limit handler. Keep an unrestricted handler for + // small browser-friendly endpoints like checkpoint and JSON metadata. + rateLimitedHandler := newRateLimitHandler(handler) + unlimitedHandler := handler // Next, the request counter. It needs to go before the mux as it uses // the context keys we set in the per-path handlers, but after the rate // limit handler, so it will capture the 429 errors. - handler = promhttp.InstrumentHandlerCounter(reqCount.MustCurryWith(labels), handler, + unlimitedHandler = promhttp.InstrumentHandlerCounter(reqCount.MustCurryWith(labels), unlimitedHandler, + promhttp.WithLabelFromCtx("kind", kindFromContext), + promhttp.WithLabelFromCtx("reused", reused.LabelFromContext), + promhttp.WithLabelFromCtx("client", clientFromContext)) + rateLimitedHandler = promhttp.InstrumentHandlerCounter(reqCount.MustCurryWith(labels), rateLimitedHandler, promhttp.WithLabelFromCtx("kind", kindFromContext), promhttp.WithLabelFromCtx("reused", reused.LabelFromContext), promhttp.WithLabelFromCtx("client", clientFromContext)) @@ -351,13 +361,13 @@ func main() { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Cache-Control", "no-store") r = r.WithContext(context.WithValue(r.Context(), kindContextKey{}, "checkpoint")) - handler.ServeHTTP(w, r) + unlimitedHandler.ServeHTTP(w, r) }) mux.HandleFunc(patternPrefix+"/log.v3.json", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Type", "application/json") r = r.WithContext(context.WithValue(r.Context(), kindContextKey{}, "log.v3.json")) - handler.ServeHTTP(w, r) + unlimitedHandler.ServeHTTP(w, r) }) mux.HandleFunc(patternPrefix+"/issuer/{issuer}", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") @@ -368,7 +378,7 @@ func main() { return } r = r.WithContext(context.WithValue(r.Context(), kindContextKey{}, "issuer")) - handler.ServeHTTP(w, r) + rateLimitedHandler.ServeHTTP(w, r) }) mux.HandleFunc(patternPrefix+"/tile/{tile...}", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") @@ -395,7 +405,7 @@ func main() { default: r = r.WithContext(context.WithValue(r.Context(), kindContextKey{}, "tile")) } - handler.ServeHTTP(w, r) + rateLimitedHandler.ServeHTTP(w, r) }) } @@ -409,10 +419,16 @@ func main() { var logs []string for log := range roots { - logs = append(logs, log.MonitoringPrefix) + logs = append(logs, log.MonitoringPrefix+"/log.v3.json") } slices.Sort(logs) - json.NewEncoder(w).Encode(logs) + json.NewEncoder(w).Encode(struct { + OperatorName string `json:"operator_name,omitempty"` + Logs []string `json:"logs"` + }{ + OperatorName: c.OperatorName, + Logs: logs, + }) }) if c.HomeRedirect != "" { diff --git a/cmd/sunlight/sunlight.go b/cmd/sunlight/sunlight.go index 4c903c9..e87f82a 100644 --- a/cmd/sunlight/sunlight.go +++ b/cmd/sunlight/sunlight.go @@ -331,19 +331,36 @@ type logInfo struct { MMD int `json:"mmd"` // Fields from the "Operator-published CT Log Metadata" proposal. - TLSOnly bool `json:"tls_only"` // always true - IntendedUse string `json:"intended_use,omitzero"` // "production" or "test" - FinalTree struct { // only included for sunsetted logs + LogSpec string `json:"log_spec"` // always "static-ct-api" + MMDSeconds int `json:"mmd_seconds"` // always 60, same as MMD + TLSOnly bool `json:"tls_only"` // always true + IntendedUse string `json:"intended_use,omitzero"` // "production" or "test" + Status string `json:"status"` // "active" or "readonly" or "inactive" + StatusTimestamp string `json:"status_timestamp"` + PlannedChanges []PlannedChange `json:"planned_changes,omitempty"` + FinalTree struct { // only included for non-active logs RootHash []byte `json:"sha256_root_hash"` Size int64 `json:"tree_size"` Timestamp int64 `json:"timestamp"` } `json:"final_tree_head,omitzero"` + SubmissionEndpoint struct { + URL string `json:"url"` + } `json:"submission_endpoint"` + MonitoringEndpoint struct { + URL string `json:"url"` + } `json:"monitoring_endpoint"` Software struct { Name string `json:"name"` Version string `json:"version"` } `json:"log_software"` } +type PlannedChange struct { + NewStatus string `json:"new_status"` + EffectiveDate string `json:"effective_date"` + Comment string `json:"comment,omitempty"` +} + //go:embed home.html var homeHTML string var homeTmpl = template.Must(template.New("home").Parse(homeHTML)) @@ -898,10 +915,14 @@ func updateMetadata(ctx context.Context, setLogInfo func(string, logInfo), lc Lo PublicKeyDER: pkix, PublicKeyBase64: base64.StdEncoding.EncodeToString(pkix), MMD: 60, + MMDSeconds: 60, TLSOnly: true, + LogSpec: "static-ct-api", } log.Interval.NotAfterStart = lc.NotAfterStart log.Interval.NotAfterLimit = lc.NotAfterLimit + log.SubmissionEndpoint.URL = log.SubmissionPrefix + log.MonitoringEndpoint.URL = log.MonitoringPrefix switch { case lc.Roots != "": // No IntendedUse for custom roots. @@ -910,10 +931,20 @@ func updateMetadata(ctx context.Context, setLogInfo func(string, logInfo), lc Lo case lc.CCADBRoots == "testing": log.IntendedUse = "test" } + readOnlyAt := cc.NotAfterLimit.Add(ctlog.ReadOnlyAfter) if e != nil { + log.Status = "readonly" + log.StatusTimestamp = readOnlyAt.Format(time.RFC3339) log.FinalTree.RootHash = e.FinalTree.Hash[:] log.FinalTree.Size = e.FinalTree.N log.FinalTree.Timestamp = e.FinalTimestamp + } else { + log.Status = "active" + log.StatusTimestamp = lc.Inception + "T00:00:00Z" + log.PlannedChanges = []PlannedChange{{ + NewStatus: "readonly", + EffectiveDate: readOnlyAt.Format(time.RFC3339), + }} } info, _ := debug.ReadBuildInfo() log.Software.Name = info.Main.Path diff --git a/internal/ctlog/ctlog.go b/internal/ctlog/ctlog.go index 0a87f48..97f69d9 100644 --- a/internal/ctlog/ctlog.go +++ b/internal/ctlog/ctlog.go @@ -422,11 +422,14 @@ func (l *Log) SetRootsFromPEM(ctx context.Context, pemBytes []byte) error { return nil } +// ReadOnlyAfter is how long after the end of the shard window +// the log becomes read-only: one week. +const ReadOnlyAfter = 7 * 24 * time.Hour + // AcceptingSubmissions returns whether the log is accepting submissions. It can // only go from true to false, when the log becomes read-only, not back. func (l *Log) AcceptingSubmissions() bool { - // Turn read-only one week after the end of the shard window. - return time.Since(l.c.NotAfterLimit) < 7*24*time.Hour + return time.Since(l.c.NotAfterLimit) < ReadOnlyAfter } // Backend is an object storage. It is dedicated to a single log instance.