From e18d36ebae30536c77c61cd5da123991e0ca1629 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Sun, 31 Dec 2023 09:39:25 +0100 Subject: Add drafty prototype --- internal/monitor/monitor.go | 173 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 internal/monitor/monitor.go (limited to 'internal/monitor/monitor.go') 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{}) {} -- cgit v1.2.3