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 --- cmd/silent-ctmoon/main.go | 217 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 cmd/silent-ctmoon/main.go (limited to 'cmd/silent-ctmoon') diff --git a/cmd/silent-ctmoon/main.go b/cmd/silent-ctmoon/main.go new file mode 100644 index 0000000..436ea64 --- /dev/null +++ b/cmd/silent-ctmoon/main.go @@ -0,0 +1,217 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "rgdd.se/silent-ct/internal/feedback" + "rgdd.se/silent-ct/internal/flagopt" + "rgdd.se/silent-ct/internal/ioutil" + "rgdd.se/silent-ct/internal/logger" + "rgdd.se/silent-ct/internal/manager" + "rgdd.se/silent-ct/internal/monitor" + "rgdd.se/silent-ct/pkg/policy" +) + +const usage = ` +A utility that follows relevant Certificate Transparency logs to +discover certificates that may be mis-issued. To be silent, any +legitimately issued certificates are pulled from trusted nodes. + +Usage: + + silent-ctmoon --help + silent-ctmoon [Opts] -d DIRECTORY -f POLICY_FILE + +Options: + + -h, --help: Output usage message and exit + -v, --verbosity: Leveled logging output (default: NOTICE) + + -b, --bootstrap: Initializate a new state directory (Default: false) + -c, --contact: A string that helps log operators know who you are (Default: "") + -d, --directory: Path to a directory where all state will be stored + -e, --please-exit Toggle to only run until up-to-date (Default: false) + -f, --policy-file: Path to the monitor's policy file in JSON format + -o, --output-file File that all output will be written to (Default: stdout) + -p, --pull-interval: How often nodes are pulled for certificates (Default: 15m) + -w, --num-workers: Number of parallel workers to fetch each log with (Default: 2) +` + +type config struct { + // Options + verbosity string + bootstrap bool + contact string + directory string + pleaseExit bool + policyFile string + outputFile string + pullInterval time.Duration + numWorkers uint + + // Extracted + log logger.Logger + policy policy.Policy +} + +func configure(cmd string, args []string) (cfg config, err error) { + fs := flag.NewFlagSet(cmd, flag.ContinueOnError) + fs.Usage = func() {} + flagopt.StringOpt(fs, &cfg.verbosity, "verbosity", "v", logger.LevelNotice.String()) + flagopt.BoolOpt(fs, &cfg.bootstrap, "bootstrap", "b", false) + flagopt.StringOpt(fs, &cfg.contact, "contact", "c", "") + flagopt.StringOpt(fs, &cfg.directory, "directory", "d", "") + flagopt.BoolOpt(fs, &cfg.pleaseExit, "please-exit", "e", false) + flagopt.StringOpt(fs, &cfg.policyFile, "policy-file", "f", "") + flagopt.StringOpt(fs, &cfg.outputFile, "output-file", "o", "") + flagopt.DurationOpt(fs, &cfg.pullInterval, "pull-interval", "p", 15*time.Minute) + flagopt.UintOpt(fs, &cfg.numWorkers, "num-workers", "w", 2) + if err = fs.Parse(args); err != nil { + return cfg, err + } + + // Options + lv, err := logger.NewLevel(cfg.verbosity) + if err != nil { + return cfg, fmt.Errorf("invalid verbosity: %v", err) + } + if cfg.directory == "" { + return cfg, fmt.Errorf("directory is a required option") + } + if cfg.policyFile == "" { + return cfg, fmt.Errorf("policy file is a required option") + } + if cfg.numWorkers == 0 || cfg.numWorkers > 4 { + return cfg, fmt.Errorf("number of workers must be in [1, 4]") + } + cfg.log = logger.New(logger.Config{Level: lv, File: os.Stdout}) + if err := ioutil.ReadJSON(cfg.policyFile, &cfg.policy); err != nil { + return cfg, err + } + if len(cfg.policy.Monitor) == 0 { + return cfg, fmt.Errorf("policy: need at least one wildcard to monitor") + } + + // Arguments + if len(fs.Args()) != 0 { + return cfg, fmt.Errorf("trailing arguments are not permitted") + } + + return cfg, nil +} + +func main() { + cfg, err := configure(os.Args[0], os.Args[1:]) + if err != nil { + if errors.Is(err, flag.ErrHelp) { + fmt.Fprintf(os.Stderr, "%s", usage[1:]) + os.Exit(0) + } + if !strings.Contains(err.Error(), "flag provided but not defined") { + fmt.Fprintf(os.Stderr, "%v\n", err) + } + os.Exit(1) + } + + feventCh := make(chan []feedback.Event) + defer close(feventCh) + + mconfigCh := make(chan monitor.MonitoredLog) + defer close(mconfigCh) + + meventCh := make(chan monitor.Event) + defer close(meventCh) + + errorCh := make(chan error) + defer close(errorCh) + + mgr, err := manager.New(manager.Config{ + Policy: cfg.policy, + Bootstrap: cfg.bootstrap, + Directory: cfg.directory, + Logger: cfg.log, + AlertDelay: cfg.pullInterval * 3 / 2, + }, feventCh, meventCh, mconfigCh, errorCh) + if err != nil { + cfg.log.Dief("manager: %v\n", err) + } + mon, err := monitor.New(monitor.Config{ + Matcher: &cfg.policy.Monitor, + Logger: cfg.log, + Contact: cfg.contact, + NumWorkers: cfg.numWorkers, + }, meventCh, mconfigCh, errorCh) + if err != nil { + cfg.log.Dief("monitor: %v\n", err) + } + fb, err := feedback.New(feedback.Config{ + Policy: cfg.policy, + Logger: cfg.log, + PullInterval: cfg.pullInterval, + }, feventCh) + if err != nil { + cfg.log.Dief("feedback: %v\n", err) + } + + if cfg.bootstrap { + os.Exit(0) + } + if cfg.pleaseExit { + cfg.log.Dief("the --please-exit option is not supported yet\n") + } + + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(context.Background()) + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + await(ctx) + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + mon.RunForever(ctx) + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + fb.RunForever(ctx) + }() + + os.Exit(func() int { + defer wg.Wait() + defer cancel() + if err := mgr.Run(ctx); err != nil { + log.Fatalf("manager: %v\n", err) + return 1 + } + return 0 + }()) +} + +func await(ctx context.Context) { + sigs := make(chan os.Signal, 1) + defer close(sigs) + + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + select { + case <-sigs: + case <-ctx.Done(): + } +} -- cgit v1.2.3