ctcloud: CertSpotter-backed CT cloud-resource monitor

Single stateless Go process that consumes CT logs via embedded (patched)
CertSpotter internals, classifies cloud/database/storage DNS identifiers
in-process with a zero-allocation classifier, enriches via round-robin DNS
resolvers with CNAME following, and batch-writes useful findings to PostgreSQL.

- third_party/certspotter: 2-hunk IdentifierSink patch, no stdout/JSON/files
- internal/classify: AWS/Azure/GCP/Firebase/Cloudflare R2/DigitalOcean/Supabase/OCI
- internal/dns: sharded bounded TTL cache, multi-resolver load balancing + failover
- internal/pipeline: bounded queues, dedup, direct-priority load shedding
- internal/store: pgx batch writer, Postgres CT checkpoints (no cert contents)
- internal/probe: optional non-destructive reachability checks (off by default)
- Docker: distroless non-root, read-only rootfs, no app volume, resource limits
- tests enforce 0 allocs/op on the classifier and sink hot path
This commit is contained in:
StasonJatham committed 2026-09-22 13:13:10 +02:00
1 parent 9e78a114ac
commit e8f7d6bfdf
118 files changed
+16473

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
// Copyright (C) 2020 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package loglist
import (
"time"
)
// Return all tiled and non-tiled logs from all operators
func (list *List) AllLogs() []*Log {
logs := []*Log{}
for operator := range list.Operators {
for log := range list.Operators[operator].Logs {
logs = append(logs, &list.Operators[operator].Logs[log])
}
for log := range list.Operators[operator].TiledLogs {
logs = append(logs, &list.Operators[operator].TiledLogs[log])
}
}
return logs
}
func (log *Log) LogIDString() string {
return log.LogID.Base64String()
}
func (log *Log) AcceptsExpiration(expiration time.Time) bool {
return log.TemporalInterval == nil || withinInterval(expiration, log.TemporalInterval.StartInclusive, log.TemporalInterval.EndExclusive)
}
func withinInterval(expiration, startInclusive, endExclusive time.Time) bool {
return !expiration.Before(startInclusive) && expiration.Before(endExclusive)
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright (C) 2020, 2023 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package loglist
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
var UserAgent = ""
// maxResponseBytes bounds the size of a log list we are willing to read into memory.
const maxResponseBytes = 16 << 20 // 16 MiB
// httpClient is used for fetching log lists.
var httpClient = &http.Client{
Timeout: 60 * time.Second,
}
type ModificationToken struct {
etag string
modified time.Time
}
var ErrNotModified = errors.New("loglist has not been modified")
func newModificationToken(response *http.Response) *ModificationToken {
token := &ModificationToken{
etag: response.Header.Get("ETag"),
}
if t, err := time.Parse(http.TimeFormat, response.Header.Get("Last-Modified")); err == nil {
token.modified = t
}
return token
}
func (token *ModificationToken) setRequestHeaders(request *http.Request) {
if token.etag != "" {
request.Header.Set("If-None-Match", token.etag)
} else if !token.modified.IsZero() {
request.Header.Set("If-Modified-Since", token.modified.Format(http.TimeFormat))
}
}
func Load(ctx context.Context, urlOrFile string) (*List, error) {
list, _, err := LoadIfModified(ctx, urlOrFile, nil)
return list, err
}
func LoadIfModified(ctx context.Context, urlOrFile string, token *ModificationToken) (*List, *ModificationToken, error) {
if strings.HasPrefix(urlOrFile, "https://") {
return FetchIfModified(ctx, urlOrFile, token)
} else {
list, err := ReadFile(urlOrFile)
return list, nil, err
}
}
func Fetch(ctx context.Context, url string) (*List, error) {
list, _, err := FetchIfModified(ctx, url, nil)
return list, err
}
func FetchIfModified(ctx context.Context, url string, token *ModificationToken) (*List, *ModificationToken, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, nil, err
}
request.Header.Set("User-Agent", UserAgent)
if token != nil {
token.setRequestHeaders(request)
}
response, err := httpClient.Do(request)
if err != nil {
return nil, nil, err
}
content, err := io.ReadAll(io.LimitReader(response.Body, maxResponseBytes+1))
response.Body.Close()
if err != nil {
return nil, nil, err
}
if len(content) > maxResponseBytes {
return nil, nil, fmt.Errorf("%s: response body exceeds maximum allowed size of %d bytes", url, maxResponseBytes)
}
if token != nil && response.StatusCode == http.StatusNotModified {
return nil, nil, ErrNotModified
}
if response.StatusCode != 200 {
return nil, nil, fmt.Errorf("%s: %s", url, response.Status)
}
list, err := Unmarshal(content)
if err != nil {
return nil, nil, fmt.Errorf("error parsing %s: %w", url, err)
}
return list, newModificationToken(response), err
}
func ReadFile(filename string) (*List, error) {
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return Unmarshal(content)
}
func Unmarshal(jsonBytes []byte) (*List, error) {
list := new(List)
if err := json.Unmarshal(jsonBytes, list); err != nil {
return nil, err
}
if err := list.Validate(); err != nil {
return nil, fmt.Errorf("invalid log list: %s", err)
}
return list, nil
}
+275
View File
@@ -0,0 +1,275 @@
// Copyright (C) 2025 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
// Package mozilla contains a parser for Mozilla's CTKnownLogs.h file
package mozilla
import (
"bufio"
"errors"
"io"
"strconv"
"strings"
"time"
)
// CTLogInfo describes a certificate transparency log from Mozilla's CTKnownLogs.h
// file.
type CTLogInfo struct {
Name string
State string // Admissible or Retired
Protocol string // RFC6962 or Tiled
Timestamp time.Time
OperatorIndex int
Key []byte
}
// CTLogOperatorInfo describes a CT log operator from Mozilla's CTKnownLogs.h
// file.
type CTLogOperatorInfo struct {
Name string
ID int
}
// Parse reads the CTKnownLogs.h content from r and returns the parsed logs and
// operators. Blocks enclosed by `#ifdef DEBUG` and `#endif` are ignored.
func Parse(r io.Reader) ([]CTLogInfo, []CTLogOperatorInfo, error) {
scanner := bufio.NewScanner(r)
skip := 0
inLogs := false
inOps := false
var logs []CTLogInfo
var ops []CTLogOperatorInfo
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
switch {
case strings.HasPrefix(trimmed, "#ifdef DEBUG"):
skip++
continue
case strings.HasPrefix(trimmed, "#endif"):
if skip > 0 {
skip--
continue
}
}
if skip > 0 {
continue
}
if strings.HasPrefix(trimmed, "const CTLogInfo kCTLogList[]") {
inLogs = true
continue
}
if strings.HasPrefix(trimmed, "const CTLogOperatorInfo kCTLogOperatorList[]") {
inOps = true
continue
}
if inLogs {
if trimmed == "};" {
inLogs = false
continue
}
if strings.HasPrefix(trimmed, "{") {
log, err := readLogEntry(trimmed, scanner)
if err != nil {
return nil, nil, err
}
logs = append(logs, log)
}
continue
}
if inOps {
if trimmed == "};" {
inOps = false
continue
}
if strings.HasPrefix(trimmed, "{") {
op, err := readOperatorEntry(trimmed)
if err != nil {
return nil, nil, err
}
ops = append(ops, op)
}
}
}
if err := scanner.Err(); err != nil {
return nil, nil, err
}
return logs, ops, nil
}
func readLogEntry(firstLine string, s *bufio.Scanner) (CTLogInfo, error) {
var log CTLogInfo
// Example first line:
// {"Name", CTLogState::Admissible, CTLogFormat::RFC6962,
firstLine = strings.TrimSpace(firstLine)
if !strings.HasPrefix(firstLine, "{") {
return log, errors.New("invalid log entry start")
}
firstLine = strings.TrimPrefix(firstLine, "{")
firstLine = strings.TrimSuffix(firstLine, ",")
parts := splitCSV(firstLine)
for len(parts) < 3 {
if !s.Scan() {
return log, io.ErrUnexpectedEOF
}
next := strings.TrimSpace(strings.TrimSuffix(s.Text(), ","))
parts = append(parts, splitCSV(next)...)
}
log.Name = trimQuotes(parts[0])
log.State = strings.TrimPrefix(strings.TrimSpace(parts[1]), "CTLogState::")
log.Protocol = strings.TrimPrefix(strings.TrimSpace(parts[2]), "CTLogFormat::")
// Next line: timestamp
if !s.Scan() {
return log, io.ErrUnexpectedEOF
}
tsLine := strings.TrimSpace(s.Text())
tsValue := strings.Split(tsLine, ",")[0]
ts, err := strconv.ParseInt(strings.TrimSpace(tsValue), 10, 64)
if err != nil {
return log, err
}
log.Timestamp = time.Unix(0, ts*int64(time.Millisecond))
// Next line: operator index
if !s.Scan() {
return log, io.ErrUnexpectedEOF
}
opLine := strings.TrimSpace(s.Text())
opValue := strings.Split(opLine, ",")[0]
opIndex, err := strconv.Atoi(strings.TrimSpace(opValue))
if err != nil {
return log, err
}
log.OperatorIndex = opIndex
// Key lines
var keyHex strings.Builder
for {
if !s.Scan() {
return log, io.ErrUnexpectedEOF
}
l := strings.TrimSpace(s.Text())
if strings.HasPrefix(l, "\"") {
// remove trailing comma if any
trimmed := strings.TrimSuffix(l, ",")
keyHex.WriteString(trimQuotes(trimmed))
if strings.HasSuffix(l, ",") {
// last key line
break
}
continue
}
return log, errors.New("unexpected line while reading key")
}
key, err := decodeHexEscapes(keyHex.String())
if err != nil {
return log, err
}
// key length line
if !s.Scan() {
return log, io.ErrUnexpectedEOF
}
lenLine := strings.TrimSpace(s.Text())
lenValue := strings.TrimSuffix(lenLine, "},")
keyLen, err := strconv.Atoi(strings.TrimSpace(lenValue))
if err != nil {
return log, err
}
if len(key) != keyLen {
// ignore mismatch but continue
}
log.Key = key
return log, nil
}
func readOperatorEntry(line string) (CTLogOperatorInfo, error) {
var op CTLogOperatorInfo
line = strings.TrimSuffix(strings.TrimSpace(line), ",")
if !strings.HasPrefix(line, "{") || !strings.HasSuffix(line, "}") {
return op, errors.New("invalid operator entry")
}
line = strings.TrimPrefix(line, "{")
line = strings.TrimSuffix(line, "}")
parts := splitCSV(line)
if len(parts) != 2 {
return op, errors.New("invalid operator fields")
}
op.Name = trimQuotes(parts[0])
id, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return op, err
}
op.ID = id
return op, nil
}
func splitCSV(s string) []string {
var parts []string
var cur strings.Builder
inQuote := false
for i := 0; i < len(s); i++ {
c := s[i]
if c == '"' {
inQuote = !inQuote
cur.WriteByte(c)
continue
}
if c == ',' && !inQuote {
parts = append(parts, strings.TrimSpace(cur.String()))
cur.Reset()
continue
}
cur.WriteByte(c)
}
if cur.Len() > 0 {
parts = append(parts, strings.TrimSpace(cur.String()))
}
return parts
}
func trimQuotes(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
func decodeHexEscapes(s string) ([]byte, error) {
var out []byte
for i := 0; i < len(s); {
if i+3 >= len(s) || s[i] != '\\' || s[i+1] != 'x' {
return nil, errors.New("invalid escape")
}
b, err := strconv.ParseUint(s[i+2:i+4], 16, 8)
if err != nil {
return nil, err
}
out = append(out, byte(b))
i += 4
}
return out, nil
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright (C) 2025 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package mozilla
import (
"crypto/sha256"
"encoding/base64"
"errors"
"net/http"
"testing"
)
// parseFromURL downloads the CTKnownLogs.h file from the given URL and parses it.
func parseFromURL(url string) ([]CTLogInfo, []CTLogOperatorInfo, error) {
resp, err := http.Get(url)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, nil, errors.New(resp.Status)
}
return Parse(resp.Body)
}
func TestParseFromURL(t *testing.T) {
logs, ops, err := parseFromURL("https://hg-edge.mozilla.org/mozilla-central/raw-file/tip/security/ct/CTKnownLogs.h")
if err != nil {
t.Fatal(err)
}
if len(ops) == 0 {
t.Fatal("no operators parsed")
}
foundGoogle := false
foundSectigo := false
foundLets := false
for _, op := range ops {
if op.Name == "" {
t.Error("operator with empty name")
}
switch op.Name {
case "Google":
foundGoogle = true
case "Sectigo":
foundSectigo = true
case "Let's Encrypt":
foundLets = true
}
}
if !foundGoogle || !foundSectigo || !foundLets {
t.Errorf("missing expected operators: Google=%v Sectigo=%v Let's=%v", foundGoogle, foundSectigo, foundLets)
}
if len(logs) == 0 {
t.Fatal("no logs parsed")
}
foundHash := false
targetHash := "1219ENGn9XfCx+lf1wC/+YLJM1pl4dCzAXMXwMjFaXc="
for _, l := range logs {
if l.Name == "" {
t.Error("log with empty name")
}
if len(l.Key) == 0 {
t.Error("log with empty key")
}
if l.State != "Admissible" && l.State != "Retired" {
t.Errorf("unexpected state %q", l.State)
}
if l.Protocol != "RFC6962" && l.Protocol != "Tiled" {
t.Errorf("unexpected protocol %q", l.Protocol)
}
hash := sha256.Sum256(l.Key)
if base64.StdEncoding.EncodeToString(hash[:]) == targetHash {
foundHash = true
}
}
if !foundHash {
t.Errorf("log with key hash %s not found", targetHash)
}
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright (C) 2025 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package mozilla_test
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"log"
"net/http"
"os"
"text/tabwriter"
"time"
"software.sslmate.com/src/certspotter/loglist/mozilla"
)
func ExampleParse() {
resp, err := http.Get("https://hg-edge.mozilla.org/mozilla-central/raw-file/tip/security/ct/CTKnownLogs.h")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Fatal(resp.Status)
}
logs, operators, err := mozilla.Parse(resp.Body)
if err != nil {
log.Fatal(err)
}
tw := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0)
fmt.Fprintln(tw, "Operator\tName")
for _, o := range operators {
fmt.Fprintf(tw, "%d\t%s\n", o.ID, o.Name)
}
tw.Flush()
tw = tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0)
fmt.Fprintln(tw, "LogID\tProtocol\tState\tTimestamp\tOperator\tName")
for _, l := range logs {
hash := sha256.Sum256(l.Key)
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%d\t%s\n",
base64.StdEncoding.EncodeToString(hash[:]),
l.Protocol, l.State, l.Timestamp.UTC().Format(time.RFC3339), l.OperatorIndex, l.Name)
}
tw.Flush()
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright (C) 2020 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package loglist
import (
"time"
"software.sslmate.com/src/certspotter/cttypes"
)
type List struct {
Version string `json:"version,omitzero"`
LogListTimestamp time.Time `json:"log_list_timestamp,omitzero"` // Only present in v3 of schema
Operators []Operator `json:"operators,omitzero"`
}
type Operator struct {
Name string `json:"name"`
Email []string `json:"email,omitzero"`
Logs []Log `json:"logs,omitzero"`
TiledLogs []Log `json:"tiled_logs,omitzero"`
}
// Log represents the log object in the "v3" JSON log list schema, as published by Chrome, Apple, and SSLMate.
type Log struct {
Key []byte `json:"key"`
LogID cttypes.LogID `json:"log_id"`
MMD int `json:"mmd"`
URL string `json:"url,omitzero"` // only for rfc6962 logs
SubmissionURL string `json:"submission_url,omitzero"` // only for static-ct-api logs
MonitoringURL string `json:"monitoring_url,omitzero"` // only for static-ct-api logs
Description string `json:"description"`
State State `json:"state,omitzero"`
LogType LogType `json:"log_type,omitzero"`
TemporalInterval *struct {
StartInclusive time.Time `json:"start_inclusive"`
EndExclusive time.Time `json:"end_exclusive"`
} `json:"temporal_interval,omitzero"`
LogSoftware *struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"log_software,omitzero"`
// certspotter-specific extensions
CertspotterDownloadSize int `json:"certspotter_download_size,omitzero"`
CertspotterDownloadJobs int `json:"certspotter_download_jobs,omitzero"`
CertspotterDownloadQPS float64 `json:"certspotter_download_qps,omitzero"`
// TODO: add previous_operators
}
func (log *Log) IsRFC6962() bool { return log.URL != "" }
func (log *Log) IsStaticCTAPI() bool { return log.SubmissionURL != "" && log.MonitoringURL != "" }
// Return URL prefix for submission using the RFC6962 protocol
func (log *Log) GetSubmissionURL() string {
if log.SubmissionURL != "" {
return log.SubmissionURL
} else {
return log.URL
}
}
// Return URL prefix for monitoring.
// Since the protocol understood by the URL might be either RFC6962 or static-ct-api, this URL is
// only useful for informational purposes.
func (log *Log) GetMonitoringURL() string {
if log.MonitoringURL != "" {
return log.MonitoringURL
} else {
return log.URL
}
}
type State struct {
Pending *struct {
Timestamp time.Time `json:"timestamp"`
} `json:"pending,omitzero"`
Qualified *struct {
Timestamp time.Time `json:"timestamp"`
} `json:"qualified,omitzero"`
Usable *struct {
Timestamp time.Time `json:"timestamp"`
} `json:"usable,omitzero"`
Readonly *struct {
Timestamp time.Time `json:"timestamp"`
FinalTreeHead struct {
TreeSize int64 `json:"tree_size"`
SHA256RootHash []byte `json:"sha256_root_hash"`
} `json:"final_tree_head"`
} `json:"readonly,omitzero"`
Retired *struct {
Timestamp time.Time `json:"timestamp"`
} `json:"retired,omitzero"`
Rejected *struct {
Timestamp time.Time `json:"timestamp"`
} `json:"rejected,omitzero"`
}
func (state *State) IsApproved() bool {
return state.Qualified != nil || state.Usable != nil || state.Readonly != nil
}
func (state *State) WasApprovedAt(t time.Time) bool {
return state.Retired != nil && t.Before(state.Retired.Timestamp)
}
type LogType string
const (
LogTypeProd = "prod"
LogTypeTest = "test"
)
type LogSpec string
const (
StaticCTAPI = "static-ct-api"
RFC6962 = "rfc6962"
)
// InclusionRequest represents the "v2" JSON log object, as published by log operators to request inclusion in browsers.
type InclusionRequest struct {
Key []byte `json:"key"`
LogID cttypes.LogID `json:"log_id"`
MMDSeconds int `json:"mmd_seconds"`
FriendlyName string `json:"friendly_name"`
LogSpec LogSpec `json:"log_spec"`
Endpoint struct {
URL string `json:"url"`
} `json:"endpoint,omitzero"`
SubmissionEndpoint struct {
URL string `json:"url"`
} `json:"submission_endpoint,omitzero"`
MonitoringEndpoint struct {
URL string `json:"url"`
} `json:"monitoring_endpoint,omitzero"`
TemporalInterval struct {
StartInclusive time.Time `json:"start_inclusive"`
EndExclusive time.Time `json:"end_exclusive"`
} `json:"temporal_interval"`
LogSoftware struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"log_software"`
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright (C) 2020 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package loglist
import (
"crypto/sha256"
"fmt"
)
func (list *List) Validate() error {
for i := range list.Operators {
if err := list.Operators[i].Validate(); err != nil {
return fmt.Errorf("problem with %dth operator (%s): %w", i, list.Operators[i].Name, err)
}
}
return nil
}
func (operator *Operator) Validate() error {
for i := range operator.Logs {
if err := operator.Logs[i].Validate(); err != nil {
return fmt.Errorf("problem with %dth non-tiled log (%s): %w", i, operator.Logs[i].LogIDString(), err)
}
}
for i := range operator.TiledLogs {
if err := operator.TiledLogs[i].Validate(); err != nil {
return fmt.Errorf("problem with %dth tiled log (%s): %w", i, operator.TiledLogs[i].LogIDString(), err)
}
}
return nil
}
func (log *Log) Validate() error {
realLogID := sha256.Sum256(log.Key)
if log.LogID != realLogID {
return fmt.Errorf("log ID does not match log key")
}
if !log.IsRFC6962() && !log.IsStaticCTAPI() {
return fmt.Errorf("URL(s) not provided")
} else if log.IsRFC6962() && log.IsStaticCTAPI() {
return fmt.Errorf("inconsistent URLs provided")
}
if log.MMD < 0 {
return fmt.Errorf("log has a negative MMD")
}
return nil
}