From aba0f17953c9947bb51e78ed581f4e66b7012518 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Thu, 16 May 2024 12:48:22 +0200 Subject: Add man pages and installer Makefile Includes renaming of the tools, part one of trying to simplify terminology and letting go of "node" and "moon". Improving the terminology was suggested by Martin H a while back, thank you. --- .gitignore | 1 + .gitlab-ci.yml | 11 ++ INSTALL | 21 ++++ Makefile | 45 +++++++ README.md | 14 +-- cmd/silent-ctmoon/main.go | 224 ---------------------------------- cmd/silent-ctnode/main.go | 127 ------------------- cmd/silentct-mac/examples.help2man | 6 + cmd/silentct-mac/main.go | 121 ++++++++++++++++++ cmd/silentct-mac/name.help2man | 2 + cmd/silentct-mac/see-also.help2man | 2 + cmd/silentct-mon/examples.help2man | 38 ++++++ cmd/silentct-mon/main.go | 222 +++++++++++++++++++++++++++++++++ cmd/silentct-mon/name.help2man | 2 + cmd/silentct-mon/see-also.help2man | 2 + docs/help2man/reporting-bugs.help2man | 12 ++ docs/help2man/return-codes.help2man | 2 + 17 files changed, 494 insertions(+), 358 deletions(-) create mode 100644 .gitignore create mode 100644 INSTALL create mode 100644 Makefile delete mode 100644 cmd/silent-ctmoon/main.go delete mode 100644 cmd/silent-ctnode/main.go create mode 100644 cmd/silentct-mac/examples.help2man create mode 100644 cmd/silentct-mac/main.go create mode 100644 cmd/silentct-mac/name.help2man create mode 100644 cmd/silentct-mac/see-also.help2man create mode 100644 cmd/silentct-mon/examples.help2man create mode 100644 cmd/silentct-mon/main.go create mode 100644 cmd/silentct-mon/name.help2man create mode 100644 cmd/silentct-mon/see-also.help2man create mode 100644 docs/help2man/reporting-bugs.help2man create mode 100644 docs/help2man/return-codes.help2man diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c64b172..96f63e8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ image: golang:1.19 stages: - test + - build go-fmt: stage: test @@ -14,3 +15,13 @@ go-vet: go-test: stage: test script: go test -race ./... + +install: + stage: build + before_script: + - apt update + - apt install make help2man + script: + - make DESTDIR=out PREFIX= install + - make DESTDIR=out PREFIX= uninstall + - make clean diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..c324a16 --- /dev/null +++ b/INSTALL @@ -0,0 +1,21 @@ +Type + + make install + +to install silentct in a standard location. + +You may override the following Makefile variables: + + DESTDIR default: + PREFIX default: $HOME/.local + BINDIR default: $(PREFIX)/bin + MANDIR default: $(PREFIX)/share/man + VERSION default: the latest git-commit + +Expect the programs in cmd/ to be installed with man pages. + +Type + + make uninstall + +to uninstall silentct. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7da46b1 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +DESTDIR ?= +PREFIX ?= $(HOME)/.local +BINDIR ?= $(PREFIX)/bin +MANDIR ?= $(PREFIX)/share/man + +VERSION ?= $(shell git rev-parse HEAD) + +PROGRAMS = silentct-mac silentct-mon +SRC_DIRS = $(patsubst %,cmd/%,$(PROGRAMS)) + +all: build man + +.PHONY: build +build: $(PROGRAMS) + +$(PROGRAMS): + @mkdir -p build + go build -o build/$@ $(patsubst %,cmd/%/main.go,$@) + +man: $(patsubst %,man-%,$(PROGRAMS)) + +man-%: build + help2man \ + --no-info --version-string=$(VERSION) \ + --include=cmd/$*/name.help2man \ + --include=cmd/$*/examples.help2man \ + --include=cmd/$*/see-also.help2man \ + --include=docs/help2man/return-codes.help2man \ + --include=docs/help2man/reporting-bugs.help2man \ + -o build/$*.1 build/$* + +install: all + @mkdir -p $(DESTDIR)$(BINDIR) + @mkdir -p $(DESTDIR)$(MANDIR)/man1 + install -m 755 $(patsubst %,build/%,$(PROGRAMS)) $(DESTDIR)$(BINDIR) + install -m 644 $(patsubst %,build/%.1,$(PROGRAMS)) $(DESTDIR)$(MANDIR)/man1 + +.PHONY: uninstall +uninstall: + rm -f $(patsubst %,$(DESTDIR)$(BINDIR)/%,$(PROGRAMS)) + rm -f $(patsubst %,$(DESTDIR)$(MANDIR)/man1/%.1,$(PROGRAMS)) + +.PHONY: clean +clean: + rm -rf build diff --git a/README.md b/README.md index d60d933..eb0e74d 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,15 @@ certificate is found that no node submitted, only then is an alert printed. ### Setup a node -You will need the `silent-ctnode` tool to create submissions that the monitor -can pull. Install: +You will need the `silentct-mac` tool to create submissions that the monitor can +pull. Install: - $ go install rgdd.se/silent-ct/cmd/silent-ctnode@latest + $ go install rgdd.se/silent-ct/cmd/silentct-mac@latest Locate the node's certificates that are still valid (i.e., not expired) and prepare a submission for them: - $ silent-ctnode -n NAME -s SECRET /path/to/chain-1.pem /path/to/chain-2.pem ... + $ silentct-mac -n NAME -s SECRET /path/to/chain-1.pem /path/to/chain-2.pem ... `NAME` is an arbitrary name of the node. @@ -56,7 +56,7 @@ Repeat this setup if there are multiple nodes. Install on the system that will run the monitor: - $ go install rgdd.se/silent-ct/cmd/silent-ctmoon@latest + $ go install rgdd.se/silent-ct/cmd/silentct-mon@latest Create a monitor policy file in JSON format. Below is an example that looks for all certificates related to `example.org`, expect for certificates that are @@ -95,12 +95,12 @@ your setup. Also add the domains each node is allowed to put into certificates. Bootstrap the monitor in a non-existent directory: - $ silent-ctmoon --bootstrap -f policy.json -d /path/to/directory -v INFO + $ silentct-mon --bootstrap -f policy.json -d /path/to/directory -v INFO ... Leave the monitor running: - $ silent-ctmoon -f policy.json -d /path/to/directory + $ silentct-mon -f policy.json -d /path/to/directory Any noteworthy events (like a potentially mis-issued certificate that no node submitted) will be printed on stdout. If you prefer to get the monitor's output diff --git a/cmd/silent-ctmoon/main.go b/cmd/silent-ctmoon/main.go deleted file mode 100644 index c651e68..0000000 --- a/cmd/silent-ctmoon/main.go +++ /dev/null @@ -1,224 +0,0 @@ -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: 1) -` - -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", 1) - 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") - } - output := os.Stdout - if cfg.outputFile != "" { - if output, err = os.OpenFile(cfg.outputFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644); err != nil { - return cfg, fmt.Errorf("failed to open output file: %v", err) - } - } - 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: output}) - 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 deleted file mode 100644 index 99f4437..0000000 --- a/cmd/silent-ctnode/main.go +++ /dev/null @@ -1,127 +0,0 @@ -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)) -} diff --git a/cmd/silentct-mac/examples.help2man b/cmd/silentct-mac/examples.help2man new file mode 100644 index 0000000..f7bbff5 --- /dev/null +++ b/cmd/silentct-mac/examples.help2man @@ -0,0 +1,6 @@ +[EXAMPLES] +Allowlist the current certificate in a Let's Encrypt deployment: + +.B $ silentct-mac -n example.org -s sikritpassword -o /var/www/example.org/silent-ct/allowlist /etc/letsencrypt/live/example.org/fullchain.pem + +You may run the above as part of your crontab or certbot renewal configuration. diff --git a/cmd/silentct-mac/main.go b/cmd/silentct-mac/main.go new file mode 100644 index 0000000..2add812 --- /dev/null +++ b/cmd/silentct-mac/main.go @@ -0,0 +1,121 @@ +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 = ` +silentct-mac is a utility that helps allowlist legitimately issued certificates +while monitoring Certificate Transparency logs. One or more certificate chains +are bundled with a message authentication code, such that the silentct-mon tool +can fetch them over an insecure channel or from untrusted intermediary storage. + +Usage: silentct-mac [Options] -n NAME -s SECRET CRT-FILE [CRT-FILE ...] + +Options: + -n, --name Name of the system that allowlists certificates + -o, --output Filename to write allowlisted certificates to (default: stdout) + -s, --secret Shared secret between the allowlisting system and its monitor + -v, --verbosity Leveled logging output (default: NOTICE) +` + +type config struct { + // Options + name string + output string + secret string + verbosity 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.name, "name", "n", "") + flagopt.StringOpt(fs, &cfg.output, "output", "o", "") + flagopt.StringOpt(fs, &cfg.secret, "secret", "s", "") + flagopt.StringOpt(fs, &cfg.verbosity, "verbosity", "v", logger.LevelNotice.String()) + if err = fs.Parse(args); err != nil { + return cfg, err + } + + // Options + if cfg.name == "" { + return cfg, fmt.Errorf("node name is required") + } + if cfg.secret == "" { + return cfg, fmt.Errorf("node secret is required") + } + lv, err := logger.NewLevel(cfg.verbosity) + if err != nil { + return cfg, fmt.Errorf("invalid verbosity: %v", err) + } + 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.Stdout, "%s", usage[1:]) + os.Exit(0) + } + if !strings.Contains(err.Error(), "flag provided but not defined") { + fmt.Fprintf(os.Stdout, "%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)) +} diff --git a/cmd/silentct-mac/name.help2man b/cmd/silentct-mac/name.help2man new file mode 100644 index 0000000..d7f6a28 --- /dev/null +++ b/cmd/silentct-mac/name.help2man @@ -0,0 +1,2 @@ +[NAME] +silentct-mac - allowlist certificates with message authentication codes diff --git a/cmd/silentct-mac/see-also.help2man b/cmd/silentct-mac/see-also.help2man new file mode 100644 index 0000000..02987dc --- /dev/null +++ b/cmd/silentct-mac/see-also.help2man @@ -0,0 +1,2 @@ +[SEE ALSO] +.BR silentct-mon (1) diff --git a/cmd/silentct-mon/examples.help2man b/cmd/silentct-mon/examples.help2man new file mode 100644 index 0000000..7a1e8fc --- /dev/null +++ b/cmd/silentct-mon/examples.help2man @@ -0,0 +1,38 @@ +[EXAMPLES] + +A basic configuration is shown below. + + { + "monitor": [ + { + "bootstrap_at": "2024-05-16T00:00:00Z", + "wildcard": "example.org", + "excludes": [ + "test" + ] + } + ], + "nodes": [ + { + "name": "example.org", + "secret": "sikritpassword", + "url": "https://www.example.org/silent-ct/allowlist", + "issues": [ + "example.org", + "www.example.org" + ] + } + ] + } + +Bootstrap a new monitor in a non-existent directory: + +.B $ silentct-mon -b -d ~/.local/lib/silent-ct -f ~/.config/silent-ct/config.json + +Run the monitor continuously: + +.B $ silentct-mon -d ~/.local/lib/silent-ct -f ~/.config/silent-ct/config.json + +Use +.B -v DEBUG +to see what's happening underneath the hood. diff --git a/cmd/silentct-mon/main.go b/cmd/silentct-mon/main.go new file mode 100644 index 0000000..2d070fb --- /dev/null +++ b/cmd/silentct-mon/main.go @@ -0,0 +1,222 @@ +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 = ` +silentct-mon is a tool that monitors Certificate Transparency logs. The tool +can operate silently, which means there need not be any output unless a +certificate is possibly mis-issued. This requires use of the silentct-mac +utility on the trusted systems that legitimately request certificates. + +The same list of Certificate Transparency logs as Google Chrome is used. This +list can be overridden in the silentct-mon configuration file. + +Usage: silentct-mon [Options] -d DIRECTORY -f POLICY-FILE + +Options: + + -b, --bootstrap Initializes 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 + -w, --num-workers Number of parallel workers to fetch each log with (Default: 1) + -o, --output-file File that all output will be written to (Default: stdout) + -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 + -p, --pull-interval How often nodes are pulled for certificates (Default: 15m) + -v, --verbosity Leveled logging output (default: NOTICE) +` + +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.BoolOpt(fs, &cfg.bootstrap, "bootstrap", "b", false) + flagopt.StringOpt(fs, &cfg.contact, "contact", "c", "") + flagopt.StringOpt(fs, &cfg.directory, "directory", "d", "") + flagopt.UintOpt(fs, &cfg.numWorkers, "num-workers", "w", 1) + flagopt.StringOpt(fs, &cfg.outputFile, "output-file", "o", "") + flagopt.BoolOpt(fs, &cfg.pleaseExit, "please-exit", "e", false) + flagopt.StringOpt(fs, &cfg.policyFile, "policy-file", "f", "") + flagopt.DurationOpt(fs, &cfg.pullInterval, "pull-interval", "p", 15*time.Minute) + flagopt.StringOpt(fs, &cfg.verbosity, "verbosity", "v", logger.LevelNotice.String()) + if err = fs.Parse(args); err != nil { + return cfg, err + } + + // Options + if cfg.directory == "" { + return cfg, fmt.Errorf("directory is a required option") + } + if cfg.numWorkers == 0 || cfg.numWorkers >= 4 { + return cfg, fmt.Errorf("number of workers must be in [1, 4)") + } + output := os.Stdout + if cfg.outputFile != "" { + if output, err = os.OpenFile(cfg.outputFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644); err != nil { + return cfg, fmt.Errorf("failed to open output file: %v", err) + } + } + if cfg.policyFile == "" { + return cfg, fmt.Errorf("policy file is a required option") + } + 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") + } + lv, err := logger.NewLevel(cfg.verbosity) + if err != nil { + return cfg, fmt.Errorf("invalid verbosity: %v", err) + } + cfg.log = logger.New(logger.Config{Level: lv, File: output}) + + // 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.Stdout, "%s", usage[1:]) + os.Exit(0) + } + if !strings.Contains(err.Error(), "flag provided but not defined") { + fmt.Fprintf(os.Stdout, "%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/silentct-mon/name.help2man b/cmd/silentct-mon/name.help2man new file mode 100644 index 0000000..a4edcc1 --- /dev/null +++ b/cmd/silentct-mon/name.help2man @@ -0,0 +1,2 @@ +[NAME] +silentct-mon - monitor Certificate Transparency logs diff --git a/cmd/silentct-mon/see-also.help2man b/cmd/silentct-mon/see-also.help2man new file mode 100644 index 0000000..d4b9782 --- /dev/null +++ b/cmd/silentct-mon/see-also.help2man @@ -0,0 +1,2 @@ +[SEE ALSO] +.BR silentct-mac (1) diff --git a/docs/help2man/reporting-bugs.help2man b/docs/help2man/reporting-bugs.help2man new file mode 100644 index 0000000..893bb0f --- /dev/null +++ b/docs/help2man/reporting-bugs.help2man @@ -0,0 +1,12 @@ +[REPORTING BUGS] +Use +.B https://git.glasklar.is/rgdd/silent-ct/-/issues +for filing issues. +.br +Reach out to +.B rgdd +in room +.B #certificate-transparency +at +.B OFTC.net +and Matrix. diff --git a/docs/help2man/return-codes.help2man b/docs/help2man/return-codes.help2man new file mode 100644 index 0000000..0a14310 --- /dev/null +++ b/docs/help2man/return-codes.help2man @@ -0,0 +1,2 @@ +[RETURN CODES] +A non-zero return code is used to indicate failure. -- cgit v1.2.3