aboutsummaryrefslogtreecommitdiff
path: root/cmd/silentct-mac/main.go
blob: 2add812e5b3ef8296626e3bc5ed487abaab3b581 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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))
}