aboutsummaryrefslogtreecommitdiff
path: root/internal/monitor/monitor.go
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@rgdd.se>2023-12-31 09:39:25 +0100
committerRasmus Dahlberg <rasmus@rgdd.se>2024-01-07 20:22:23 +0100
commite18d36ebae30536c77c61cd5da123991e0ca1629 (patch)
treebf4880c0019a6009ab1b671e23ef4a1a4a5e8e08 /internal/monitor/monitor.go
parent54d980afcbd6f0011d6a162e0003587d26a3e311 (diff)
Add drafty prototype
Diffstat (limited to 'internal/monitor/monitor.go')
-rw-r--r--internal/monitor/monitor.go173
1 files changed, 173 insertions, 0 deletions
diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go
new file mode 100644
index 0000000..6accd97
--- /dev/null
+++ b/internal/monitor/monitor.go
@@ -0,0 +1,173 @@
+// Package monitor provides monitoring of Certificate Transparency logs. If
+// running in continuous mode, the list of logs can be updated dynamically.
+//
+// Implement the Matcher interface to customize which certificates should be
+// included in the monitor's emitted events. See MatchAll for an example.
+//
+// Note that this package verifies that the monitored logs are locally
+// consistent with regard to the initial start-up state. It is up to the user
+// to process the monitor's emitted events and errors, and to persist state.
+package monitor
+
+import (
+ "context"
+ "crypto/x509"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "os"
+ "sync"
+
+ ct "github.com/google/certificate-transparency-go"
+ "github.com/google/certificate-transparency-go/client"
+ "github.com/google/certificate-transparency-go/jsonclient"
+ "gitlab.torproject.org/rgdd/ct/pkg/metadata"
+ "rgdd.se/silent-ct/internal/logger"
+)
+
+// MonitoredLog provides information about a log the monitor is following
+type MonitoredLog struct {
+ Config metadata.Log
+ State State
+}
+
+// State is the latest append-only state the monitor observed from its local
+// vantage point. The next entry to download is specified by NextIndex.
+type State struct {
+ ct.SignedTreeHead `json:"latest_sth"`
+ NextIndex uint64 `json:"next_index"`
+}
+
+// Event carries the latest consistent monitor state, found matches, as well as
+// errors that occurred while trying to match on the downloaded log entries.
+type Event struct {
+ State State
+ Matches []LogEntry
+ Errors []error
+}
+
+func (ev *Event) Summary() string {
+ return fmt.Sprintf("log %s: tree size %d at next index %d (%d matches, %d errors)",
+ base64.StdEncoding.EncodeToString(ev.State.LogID[:]),
+ ev.State.TreeSize, ev.State.NextIndex, len(ev.Matches), len(ev.Errors))
+}
+
+// LogEntry is a Merkle tree leaf in a log
+type LogEntry struct {
+ LeafIndex uint64 `json:"leaf_index"`
+ LeafData []byte `json:"leaf_data"`
+ ExtraData []byte `json:"extra_data"`
+}
+
+type Config struct {
+ // Optional
+ Matcher Matcher // Which log entries to match (default is to match all)
+ Logger logger.Logger // Debug prints only (no output by default)
+ Contact string // Something that help log operators get in touch
+ ChunkSize uint // Min number of leaves to propagate a chunk without matches
+ BatchSize uint // Max number of certificates to accept per worker
+ NumWorkers uint // Number of parallel workers to use for each log
+}
+
+type Monitor struct {
+ cfg Config
+ matcher Matcher
+
+ eventCh chan Event
+ configCh chan MonitoredLog
+ errorCh chan error
+}
+
+func New(cfg Config, evCh chan Event, cfgCh chan MonitoredLog, errCh chan error) (Monitor, error) {
+ if cfg.Matcher == nil {
+ cfg.Matcher = &MatchAll{}
+ }
+ if !cfg.Logger.IsConfigured() {
+ cfg.Logger = logger.New(logger.Config{Level: logger.LevelNotice, File: os.Stderr})
+ }
+ if cfg.Contact == "" {
+ cfg.Contact = "unknown-user"
+ }
+ if cfg.ChunkSize == 0 {
+ cfg.ChunkSize = 256 // FIXME: 16364
+ }
+ if cfg.BatchSize == 0 {
+ cfg.BatchSize = 1024
+ }
+ if cfg.NumWorkers == 0 {
+ cfg.NumWorkers = 2
+ }
+ return Monitor{cfg: cfg, matcher: cfg.Matcher, eventCh: evCh, configCh: cfgCh, errorCh: errCh}, nil
+}
+
+func (mon *Monitor) RunOnce(ctx context.Context, cfg []MonitoredLog, evCh chan Event, errCh chan error) error {
+ return fmt.Errorf("TODO")
+}
+
+func (mon *Monitor) RunForever(ctx context.Context) error {
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ mctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ monitoring := make(map[metadata.LogURL]context.CancelFunc)
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+ case log := <-mon.configCh:
+ if tcancel, ok := monitoring[log.Config.URL]; ok {
+ delete(monitoring, log.Config.URL)
+ tcancel()
+ continue
+ }
+
+ newTail := mon.newTailRFC6962
+ if log.Config.DNS != nil { // FIXME: get a real nob for tile-based logs
+ newTail = mon.newTailTile
+ }
+ t, err := newTail(log)
+ if err != nil {
+ return err
+ }
+
+ tctx, tcancel := context.WithCancel(mctx)
+ monitoring[log.Config.URL] = tcancel
+
+ wg.Add(1)
+ go func(log MonitoredLog, t tail) {
+ defer wg.Done()
+ defer tcancel()
+ t.run(tctx, log, mon.eventCh, mon.errorCh)
+ }(log, t)
+ }
+ }
+}
+
+const userAgentPrefix = "rgdd.se/silent-ct"
+
+func (mon *Monitor) newTailRFC6962(log MonitoredLog) (tail, error) {
+ key, err := x509.MarshalPKIXPublicKey(log.Config.Key.Public)
+ if err != nil {
+ return tail{}, err
+ }
+ cli, err := client.New(string(log.Config.URL), &http.Client{}, jsonclient.Options{
+ Logger: &discard{},
+ UserAgent: userAgentPrefix + "/" + mon.cfg.Contact,
+ PublicKeyDER: key,
+ })
+ if err != nil {
+ return tail{}, err
+ }
+
+ return tail{cfg: mon.cfg, scanner: cli, checker: cli, matcher: mon.matcher}, nil
+}
+
+func (mon *Monitor) newTailTile(cfg MonitoredLog) (tail, error) {
+ return tail{}, fmt.Errorf("TODO")
+}
+
+type discard struct{}
+
+func (n *discard) Printf(string, ...interface{}) {}