boulder/cmd/rocsp-tool/main.go

269 lines
8.0 KiB
Go

package notmain
import (
"context"
"encoding/base64"
"encoding/pem"
"flag"
"fmt"
"os"
"strings"
"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/ocsp"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/db"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/metrics"
rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test/ocsp/helper"
)
type Config struct {
ROCSPTool struct {
DebugAddr string `validate:"omitempty,hostname_port"`
Redis rocsp_config.RedisConfig
// If using load-from-db, this provides credentials to connect to the DB
// and the CA. Otherwise, it's optional.
LoadFromDB *LoadFromDBConfig
}
Syslog cmd.SyslogConfig
OpenTelemetry cmd.OpenTelemetryConfig
}
// LoadFromDBConfig provides the credentials and configuration needed to load
// data from the certificateStatuses table in the DB and get it signed.
type LoadFromDBConfig struct {
// Credentials to connect to the DB.
DB cmd.DBConfig
// Credentials to request OCSP signatures from the CA.
GRPCTLS cmd.TLSConfig
// Timeouts and hostnames for the CA.
OCSPGeneratorService cmd.GRPCClientConfig
// How fast to process rows.
Speed ProcessingSpeed
}
type ProcessingSpeed struct {
// If using load-from-db, this limits how many items per second we
// scan from the DB. We might go slower than this depending on how fast
// we read rows from the DB, but we won't go faster. Defaults to 2000.
RowsPerSecond int `validate:"min=0"`
// If using load-from-db, this controls how many parallel requests to
// boulder-ca for OCSP signing we can make. Defaults to 100.
ParallelSigns int `validate:"min=0"`
// If using load-from-db, the LIMIT on our scanning queries. We have to
// apply a limit because MariaDB will cut off our response at some
// threshold of total bytes transferred (1 GB by default). Defaults to 10000.
ScanBatchSize int `validate:"min=0"`
}
func init() {
cmd.RegisterCommand("rocsp-tool", main, &cmd.ConfigValidator{Config: &Config{}})
}
func main() {
err := main2()
if err != nil {
cmd.FailOnError(err, "")
}
}
var startFromID *int64
func main2() error {
debugAddr := flag.String("debug-addr", "", "Debug server address override")
configFile := flag.String("config", "", "File path to the configuration file for this service")
startFromID = flag.Int64("start-from-id", 0, "For load-from-db, the first ID in the certificateStatus table to scan")
flag.Usage = helpExit
flag.Parse()
if *configFile == "" || len(flag.Args()) < 1 {
helpExit()
}
var conf Config
err := cmd.ReadConfigFile(*configFile, &conf)
if err != nil {
return fmt.Errorf("reading JSON config file: %w", err)
}
if *debugAddr != "" {
conf.ROCSPTool.DebugAddr = *debugAddr
}
_, logger, oTelShutdown := cmd.StatsAndLogging(conf.Syslog, conf.OpenTelemetry, conf.ROCSPTool.DebugAddr)
defer oTelShutdown(context.Background())
logger.Info(cmd.VersionString())
clk := cmd.Clock()
redisClient, err := rocsp_config.MakeClient(&conf.ROCSPTool.Redis, clk, metrics.NoopRegisterer)
if err != nil {
return fmt.Errorf("making client: %w", err)
}
var db *db.WrappedMap
var ocspGenerator capb.OCSPGeneratorClient
var scanBatchSize int
if conf.ROCSPTool.LoadFromDB != nil {
lfd := conf.ROCSPTool.LoadFromDB
db, err = sa.InitWrappedDb(lfd.DB, nil, logger)
if err != nil {
return fmt.Errorf("connecting to DB: %w", err)
}
ocspGenerator, err = configureOCSPGenerator(lfd.GRPCTLS,
lfd.OCSPGeneratorService, clk, metrics.NoopRegisterer)
if err != nil {
return fmt.Errorf("configuring gRPC to CA: %w", err)
}
setDefault(&lfd.Speed.RowsPerSecond, 2000)
setDefault(&lfd.Speed.ParallelSigns, 100)
setDefault(&lfd.Speed.ScanBatchSize, 10000)
scanBatchSize = lfd.Speed.ScanBatchSize
}
ctx := context.Background()
cl := client{
redis: redisClient,
db: db,
ocspGenerator: ocspGenerator,
clk: clk,
scanBatchSize: scanBatchSize,
logger: logger,
}
for _, sc := range subCommands {
if flag.Arg(0) == sc.name {
return sc.cmd(ctx, cl, conf, flag.Args()[1:])
}
}
fmt.Fprintf(os.Stderr, "unrecognized subcommand %q\n", flag.Arg(0))
helpExit()
return nil
}
// subCommand represents a single subcommand. `name` is the name used to invoke it, and `help` is
// its help text.
type subCommand struct {
name string
help string
cmd func(context.Context, client, Config, []string) error
}
var (
Store = subCommand{"store", "for each filename on command line, read the file as an OCSP response and store it in Redis",
func(ctx context.Context, cl client, _ Config, args []string) error {
err := cl.storeResponsesFromFiles(ctx, flag.Args()[1:])
if err != nil {
return err
}
return nil
},
}
Get = subCommand{
"get",
"for each serial on command line, fetch that serial's response and pretty-print it",
func(ctx context.Context, cl client, _ Config, args []string) error {
for _, serial := range flag.Args()[1:] {
resp, err := cl.redis.GetResponse(ctx, serial)
if err != nil {
return err
}
parsed, err := ocsp.ParseResponse(resp, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing error on %x: %s", resp, err)
continue
} else {
fmt.Printf("%s\n", helper.PrettyResponse(parsed))
}
}
return nil
},
}
GetPEM = subCommand{"get-pem", "for each serial on command line, fetch that serial's response and print it PEM-encoded",
func(ctx context.Context, cl client, _ Config, args []string) error {
for _, serial := range flag.Args()[1:] {
resp, err := cl.redis.GetResponse(ctx, serial)
if err != nil {
return err
}
block := pem.Block{
Bytes: resp,
Type: "OCSP RESPONSE",
}
err = pem.Encode(os.Stdout, &block)
if err != nil {
return err
}
}
return nil
},
}
LoadFromDB = subCommand{"load-from-db", "scan the database for all OCSP entries for unexpired certificates, and store in Redis",
func(ctx context.Context, cl client, c Config, args []string) error {
if c.ROCSPTool.LoadFromDB == nil {
return fmt.Errorf("config field LoadFromDB was missing")
}
err := cl.loadFromDB(ctx, c.ROCSPTool.LoadFromDB.Speed, *startFromID)
if err != nil {
return fmt.Errorf("loading OCSP responses from DB: %w", err)
}
return nil
},
}
ScanResponses = subCommand{"scan-responses", "scan Redis for OCSP response entries. For each entry, print the serial and base64-encoded response",
func(ctx context.Context, cl client, _ Config, args []string) error {
results := cl.redis.ScanResponses(ctx, "*")
for r := range results {
if r.Err != nil {
return r.Err
}
fmt.Printf("%s: %s\n", r.Serial, base64.StdEncoding.EncodeToString(r.Body))
}
return nil
},
}
)
var subCommands = []subCommand{
Store, Get, GetPEM, LoadFromDB, ScanResponses,
}
func helpExit() {
var names []string
var helpStrings []string
for _, s := range subCommands {
names = append(names, s.name)
helpStrings = append(helpStrings, fmt.Sprintf(" %s -- %s", s.name, s.help))
}
fmt.Fprintf(os.Stderr, "Usage: %s [%s] --config path/to/config.json\n", os.Args[0], strings.Join(names, "|"))
os.Stderr.Write([]byte(strings.Join(helpStrings, "\n")))
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr)
flag.PrintDefaults()
os.Exit(1)
}
func configureOCSPGenerator(tlsConf cmd.TLSConfig, grpcConf cmd.GRPCClientConfig, clk clock.Clock, scope prometheus.Registerer) (capb.OCSPGeneratorClient, error) {
tlsConfig, err := tlsConf.Load(scope)
if err != nil {
return nil, fmt.Errorf("loading TLS config: %w", err)
}
caConn, err := bgrpc.ClientSetup(&grpcConf, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to CA")
return capb.NewOCSPGeneratorClient(caConn), nil
}
// setDefault sets the target to a default value, if it is zero.
func setDefault(target *int, def int) {
if *target == 0 {
*target = def
}
}