1 package notmain
2
3 import (
4 "context"
5 "encoding/base64"
6 "encoding/pem"
7 "flag"
8 "fmt"
9 "os"
10 "strings"
11
12 "github.com/jmhodges/clock"
13 "github.com/prometheus/client_golang/prometheus"
14 "golang.org/x/crypto/ocsp"
15
16 capb "github.com/letsencrypt/boulder/ca/proto"
17 "github.com/letsencrypt/boulder/cmd"
18 "github.com/letsencrypt/boulder/db"
19 bgrpc "github.com/letsencrypt/boulder/grpc"
20 "github.com/letsencrypt/boulder/metrics"
21 rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
22 "github.com/letsencrypt/boulder/sa"
23 "github.com/letsencrypt/boulder/test/ocsp/helper"
24 )
25
26 type Config struct {
27 ROCSPTool struct {
28 DebugAddr string `validate:"required,hostname_port"`
29 Redis rocsp_config.RedisConfig
30
31
32
33 LoadFromDB *LoadFromDBConfig
34 }
35 Syslog cmd.SyslogConfig
36 OpenTelemetry cmd.OpenTelemetryConfig
37 }
38
39
40
41 type LoadFromDBConfig struct {
42
43 DB cmd.DBConfig
44
45 GRPCTLS cmd.TLSConfig
46
47 OCSPGeneratorService cmd.GRPCClientConfig
48
49 Speed ProcessingSpeed
50 }
51
52 type ProcessingSpeed struct {
53
54
55
56 RowsPerSecond int `validate:"min=0"`
57
58
59 ParallelSigns int `validate:"min=0"`
60
61
62
63 ScanBatchSize int `validate:"min=0"`
64 }
65
66 func init() {
67 cmd.RegisterCommand("rocsp-tool", main, &cmd.ConfigValidator{Config: &Config{}})
68 }
69
70 func main() {
71 err := main2()
72 if err != nil {
73 cmd.FailOnError(err, "")
74 }
75 }
76
77 var startFromID *int64
78
79 func main2() error {
80 configFile := flag.String("config", "", "File path to the configuration file for this service")
81 startFromID = flag.Int64("start-from-id", 0, "For load-from-db, the first ID in the certificateStatus table to scan")
82 flag.Usage = helpExit
83 flag.Parse()
84 if *configFile == "" || len(flag.Args()) < 1 {
85 helpExit()
86 }
87
88 var conf Config
89 err := cmd.ReadConfigFile(*configFile, &conf)
90 if err != nil {
91 return fmt.Errorf("reading JSON config file: %w", err)
92 }
93
94 _, logger, oTelShutdown := cmd.StatsAndLogging(conf.Syslog, conf.OpenTelemetry, conf.ROCSPTool.DebugAddr)
95 defer oTelShutdown(context.Background())
96 logger.Info(cmd.VersionString())
97
98 clk := cmd.Clock()
99 redisClient, err := rocsp_config.MakeClient(&conf.ROCSPTool.Redis, clk, metrics.NoopRegisterer)
100 if err != nil {
101 return fmt.Errorf("making client: %w", err)
102 }
103
104 var db *db.WrappedMap
105 var ocspGenerator capb.OCSPGeneratorClient
106 var scanBatchSize int
107 if conf.ROCSPTool.LoadFromDB != nil {
108 lfd := conf.ROCSPTool.LoadFromDB
109 db, err = sa.InitWrappedDb(lfd.DB, nil, logger)
110 if err != nil {
111 return fmt.Errorf("connecting to DB: %w", err)
112 }
113
114 ocspGenerator, err = configureOCSPGenerator(lfd.GRPCTLS,
115 lfd.OCSPGeneratorService, clk, metrics.NoopRegisterer)
116 if err != nil {
117 return fmt.Errorf("configuring gRPC to CA: %w", err)
118 }
119 setDefault(&lfd.Speed.RowsPerSecond, 2000)
120 setDefault(&lfd.Speed.ParallelSigns, 100)
121 setDefault(&lfd.Speed.ScanBatchSize, 10000)
122 scanBatchSize = lfd.Speed.ScanBatchSize
123 }
124
125 ctx := context.Background()
126 cl := client{
127 redis: redisClient,
128 db: db,
129 ocspGenerator: ocspGenerator,
130 clk: clk,
131 scanBatchSize: scanBatchSize,
132 logger: logger,
133 }
134
135 for _, sc := range subCommands {
136 if flag.Arg(0) == sc.name {
137 return sc.cmd(ctx, cl, conf, flag.Args()[1:])
138 }
139 }
140 fmt.Fprintf(os.Stderr, "unrecognized subcommand %q\n", flag.Arg(0))
141 helpExit()
142 return nil
143 }
144
145
146
147 type subCommand struct {
148 name string
149 help string
150 cmd func(context.Context, client, Config, []string) error
151 }
152
153 var (
154 Store = subCommand{"store", "for each filename on command line, read the file as an OCSP response and store it in Redis",
155 func(ctx context.Context, cl client, _ Config, args []string) error {
156 err := cl.storeResponsesFromFiles(ctx, flag.Args()[1:])
157 if err != nil {
158 return err
159 }
160 return nil
161 },
162 }
163 Get = subCommand{
164 "get",
165 "for each serial on command line, fetch that serial's response and pretty-print it",
166 func(ctx context.Context, cl client, _ Config, args []string) error {
167 for _, serial := range flag.Args()[1:] {
168 resp, err := cl.redis.GetResponse(ctx, serial)
169 if err != nil {
170 return err
171 }
172 parsed, err := ocsp.ParseResponse(resp, nil)
173 if err != nil {
174 fmt.Fprintf(os.Stderr, "parsing error on %x: %s", resp, err)
175 continue
176 } else {
177 fmt.Printf("%s\n", helper.PrettyResponse(parsed))
178 }
179 }
180 return nil
181 },
182 }
183 GetPEM = subCommand{"get-pem", "for each serial on command line, fetch that serial's response and print it PEM-encoded",
184 func(ctx context.Context, cl client, _ Config, args []string) error {
185 for _, serial := range flag.Args()[1:] {
186 resp, err := cl.redis.GetResponse(ctx, serial)
187 if err != nil {
188 return err
189 }
190 block := pem.Block{
191 Bytes: resp,
192 Type: "OCSP RESPONSE",
193 }
194 err = pem.Encode(os.Stdout, &block)
195 if err != nil {
196 return err
197 }
198 }
199 return nil
200 },
201 }
202 LoadFromDB = subCommand{"load-from-db", "scan the database for all OCSP entries for unexpired certificates, and store in Redis",
203 func(ctx context.Context, cl client, c Config, args []string) error {
204 if c.ROCSPTool.LoadFromDB == nil {
205 return fmt.Errorf("config field LoadFromDB was missing")
206 }
207 err := cl.loadFromDB(ctx, c.ROCSPTool.LoadFromDB.Speed, *startFromID)
208 if err != nil {
209 return fmt.Errorf("loading OCSP responses from DB: %w", err)
210 }
211 return nil
212 },
213 }
214 ScanResponses = subCommand{"scan-responses", "scan Redis for OCSP response entries. For each entry, print the serial and base64-encoded response",
215 func(ctx context.Context, cl client, _ Config, args []string) error {
216 results := cl.redis.ScanResponses(ctx, "*")
217 for r := range results {
218 if r.Err != nil {
219 return r.Err
220 }
221 fmt.Printf("%s: %s\n", r.Serial, base64.StdEncoding.EncodeToString(r.Body))
222 }
223 return nil
224 },
225 }
226 )
227
228 var subCommands = []subCommand{
229 Store, Get, GetPEM, LoadFromDB, ScanResponses,
230 }
231
232 func helpExit() {
233 var names []string
234 var helpStrings []string
235 for _, s := range subCommands {
236 names = append(names, s.name)
237 helpStrings = append(helpStrings, fmt.Sprintf(" %s -- %s", s.name, s.help))
238 }
239 fmt.Fprintf(os.Stderr, "Usage: %s [%s] --config path/to/config.json\n", os.Args[0], strings.Join(names, "|"))
240 os.Stderr.Write([]byte(strings.Join(helpStrings, "\n")))
241 fmt.Fprintln(os.Stderr)
242 fmt.Fprintln(os.Stderr)
243 flag.PrintDefaults()
244 os.Exit(1)
245 }
246
247 func configureOCSPGenerator(tlsConf cmd.TLSConfig, grpcConf cmd.GRPCClientConfig, clk clock.Clock, scope prometheus.Registerer) (capb.OCSPGeneratorClient, error) {
248 tlsConfig, err := tlsConf.Load(scope)
249 if err != nil {
250 return nil, fmt.Errorf("loading TLS config: %w", err)
251 }
252
253 caConn, err := bgrpc.ClientSetup(&grpcConf, tlsConfig, scope, clk)
254 cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to CA")
255 return capb.NewOCSPGeneratorClient(caConn), nil
256 }
257
258
259 func setDefault(target *int, def int) {
260 if *target == 0 {
261 *target = def
262 }
263 }
264
View as plain text