aboutsummaryrefslogtreecommitdiff
path: root/cmd
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 /cmd
parent54d980afcbd6f0011d6a162e0003587d26a3e311 (diff)
Add drafty prototype
Diffstat (limited to 'cmd')
-rw-r--r--cmd/silent-ctmoon/main.go217
-rw-r--r--cmd/silent-ctnode/main.go127
2 files changed, 344 insertions, 0 deletions
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():
+ }
+}
diff --git a/cmd/silent-ctnode/main.go b/cmd/silent-ctnode/main.go
new file mode 100644
index 0000000..fec088c
--- /dev/null
+++ b/cmd/silent-ctnode/main.go
@@ -0,0 +1,127 @@
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ "rgdd.se/silent-ct/internal/flagopt"
+ "rgdd.se/silent-ct/internal/ioutil"
+ "rgdd.se/silent-ct/internal/logger"
+ "rgdd.se/silent-ct/pkg/crtutil"
+ "rgdd.se/silent-ct/pkg/policy"
+ "rgdd.se/silent-ct/pkg/submission"
+)
+
+const usage = `
+A utility that generates a submission of one or more certificate chains.
+The generated submission is protected by a message authentication code.
+
+Usage:
+
+ silent-ctnode --help
+ silent-ctnode [Options] -n NAME -s SECRET FILE [FILE ...]
+
+Options:
+
+ -h, --help: Output usage message and exit
+ -v, --verbosity Leveled logging output (default: NOTICE)
+
+ -n, --name: Name of the node generating the submission
+ -s, --secret: Shared secret between the node and its monitor
+ -o, --output: File to write submission to (default: stdout)
+
+Each trailing FILE argument must contain a single certificate chain.
+`
+
+type config struct {
+ // Options
+ verbosity string
+ name string
+ secret string
+ output string
+
+ // Extracted
+ log logger.Logger
+ files []string
+}
+
+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.StringOpt(fs, &cfg.name, "name", "n", "")
+ flagopt.StringOpt(fs, &cfg.secret, "secret", "s", "")
+ flagopt.StringOpt(fs, &cfg.output, "output", "o", "")
+ 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.name == "" {
+ return cfg, fmt.Errorf("node name is required")
+ }
+ if cfg.secret == "" {
+ return cfg, fmt.Errorf("node secret is required")
+ }
+ cfg.log = logger.New(logger.Config{Level: lv, File: os.Stderr})
+
+ // Arguments
+ cfg.files = fs.Args()
+ if len(cfg.files) == 0 {
+ return cfg, fmt.Errorf("at least one certificate chain file is required")
+ }
+
+ return cfg, err
+}
+
+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)
+ }
+
+ var chains [][]byte
+ for i, path := range cfg.files {
+ b, err := ioutil.ReadData(path)
+ if err != nil {
+ cfg.log.Dief("file %d: %v\n", i, err)
+ }
+ if _, err := crtutil.CertificateChainFromPEM(b); err != nil {
+ cfg.log.Dief("file %d: %v\n", i, err)
+ }
+
+ chains = append(chains, b)
+ }
+
+ node, err := policy.NewNode(cfg.name, cfg.secret, "http://www.example.org/unused", nil)
+ if err != nil {
+ cfg.log.Dief("api: %v\n", err)
+ }
+ s, err := submission.New(node, chains)
+ if err != nil {
+ cfg.log.Dief("api: %v\n", err)
+ }
+
+ fp := os.Stdout
+ if cfg.output != "" {
+ if fp, err = os.OpenFile(cfg.output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
+ cfg.log.Dief("output: %v\n", err)
+ }
+ }
+
+ fmt.Fprintf(fp, "%s", string(s))
+}