diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .gitlab-ci.yml | 11 | ||||
-rw-r--r-- | INSTALL | 21 | ||||
-rw-r--r-- | Makefile | 45 | ||||
-rw-r--r-- | README.md | 14 | ||||
-rw-r--r-- | cmd/silentct-mac/examples.help2man | 6 | ||||
-rw-r--r-- | cmd/silentct-mac/main.go (renamed from cmd/silent-ctnode/main.go) | 42 | ||||
-rw-r--r-- | cmd/silentct-mac/name.help2man | 2 | ||||
-rw-r--r-- | cmd/silentct-mac/see-also.help2man | 2 | ||||
-rw-r--r-- | cmd/silentct-mon/examples.help2man | 38 | ||||
-rw-r--r-- | cmd/silentct-mon/main.go (renamed from cmd/silent-ctmoon/main.go) | 60 | ||||
-rw-r--r-- | cmd/silentct-mon/name.help2man | 2 | ||||
-rw-r--r-- | cmd/silentct-mon/see-also.help2man | 2 | ||||
-rw-r--r-- | docs/help2man/reporting-bugs.help2man | 12 | ||||
-rw-r--r-- | docs/help2man/return-codes.help2man | 2 |
15 files changed, 198 insertions, 62 deletions
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 @@ -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 @@ -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/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/silent-ctnode/main.go b/cmd/silentct-mac/main.go index 99f4437..2add812 100644 --- a/cmd/silent-ctnode/main.go +++ b/cmd/silentct-mac/main.go @@ -16,32 +16,26 @@ import ( ) const usage = ` -A utility that generates a submission of one or more certificate chains. -The generated submission is protected by a message authentication code. +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: - - silent-ctnode --help - silent-ctnode [Options] -n NAME -s SECRET FILE [FILE ...] +Usage: silentct-mac [Options] -n NAME -s SECRET CRT-FILE [CRT-FILE ...] Options: - - -h, --help: Output usage message and exit + -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) - - -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 + secret string + verbosity string // Extracted log *logger.Logger @@ -51,25 +45,25 @@ type config struct { 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", "") + 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 - 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") } + 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 @@ -85,11 +79,11 @@ 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:]) + fmt.Fprintf(os.Stdout, "%s", usage[1:]) os.Exit(0) } if !strings.Contains(err.Error(), "flag provided but not defined") { - fmt.Fprintf(os.Stderr, "%v\n", err) + fmt.Fprintf(os.Stdout, "%v\n", err) } os.Exit(1) } 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/silent-ctmoon/main.go b/cmd/silentct-mon/main.go index c651e68..2d070fb 100644 --- a/cmd/silent-ctmoon/main.go +++ b/cmd/silentct-mon/main.go @@ -23,28 +23,27 @@ import ( ) 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. +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. -Usage: +The same list of Certificate Transparency logs as Google Chrome is used. This +list can be overridden in the silentct-mon configuration file. - silent-ctmoon --help - silent-ctmoon [Opts] -d DIRECTORY -f POLICY_FILE +Usage: silentct-mon [Options] -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 + -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) - -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) + -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 { @@ -67,29 +66,25 @@ type config struct { 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.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.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) + flagopt.StringOpt(fs, &cfg.verbosity, "verbosity", "v", logger.LevelNotice.String()) 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)") } output := os.Stdout if cfg.outputFile != "" { @@ -97,17 +92,20 @@ func configure(cmd string, args []string) (cfg config, err error) { 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]") + if cfg.policyFile == "" { + return cfg, fmt.Errorf("policy file is a required option") } - - 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") } + 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 { @@ -121,11 +119,11 @@ 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:]) + fmt.Fprintf(os.Stdout, "%s", usage[1:]) os.Exit(0) } if !strings.Contains(err.Error(), "flag provided but not defined") { - fmt.Fprintf(os.Stderr, "%v\n", err) + fmt.Fprintf(os.Stdout, "%v\n", err) } os.Exit(1) } 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. |