Skip to content

Commit

Permalink
Create new crl-storer service (#6264)
Browse files Browse the repository at this point in the history
Create a new crl-storer service, which receives CRL shards via gRPC and
uploads them to an S3 bucket. It ignores AWS SDK configuration in the
usual places, in favor of configuration from our standard JSON service
config files. It ensures that the CRLs it receives parse and are signed
by the appropriate issuer before uploading them.

Integrate crl-updater with the new service. It streams bytes to the
crl-storer as it receives them from the CA, without performing any
checking at the same time. This new functionality is disabled if the
crl-updater does not have a config stanza instructing it how to connect
to the crl-storer.

Finally, add a new test component, the s3-test-srv. This acts similarly
to the existing mail-test-srv: it receives requests, stores information
about them, and exposes that information for later querying by the
integration test. The integration test uses this to ensure that a
newly-revoked certificate does show up in the next generation of CRLs
produced.

Fixes #6162
  • Loading branch information
aarongable authored Aug 8, 2022
1 parent 576b677 commit 6a9bb39
Show file tree
Hide file tree
Showing 507 changed files with 129,529 additions and 76 deletions.
34 changes: 18 additions & 16 deletions ca/crl.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,28 @@ func (ci *crlImpl) GenerateCRL(stream capb.CRLGenerator_GenerateCRLServer) error
logID, issuer.Cert.Subject.CommonName, template.Number.String(), shard, template.ThisUpdate, template.NextUpdate, len(rcs),
)

builder := strings.Builder{}
for i := 0; i < len(rcs); i += 1 {
if builder.Len() == 0 {
fmt.Fprintf(&builder, "Signing CRL: logID=[%s] entries=[", logID)
}
if len(rcs) > 0 {
builder := strings.Builder{}
for i := 0; i < len(rcs); i += 1 {
if builder.Len() == 0 {
fmt.Fprintf(&builder, "Signing CRL: logID=[%s] entries=[", logID)
}

reason := 0
if rcs[i].ReasonCode != nil {
reason = *rcs[i].ReasonCode
}
fmt.Fprintf(&builder, "%x:%d,", rcs[i].SerialNumber.Bytes(), reason)
reason := 0
if rcs[i].ReasonCode != nil {
reason = *rcs[i].ReasonCode
}
fmt.Fprintf(&builder, "%x:%d,", rcs[i].SerialNumber.Bytes(), reason)

if builder.Len() >= ci.maxLogLen {
fmt.Fprint(&builder, "]")
ci.log.AuditInfo(builder.String())
builder = strings.Builder{}
if builder.Len() >= ci.maxLogLen {
fmt.Fprint(&builder, "]")
ci.log.AuditInfo(builder.String())
builder = strings.Builder{}
}
}
fmt.Fprint(&builder, "]")
ci.log.AuditInfo(builder.String())
}
fmt.Fprint(&builder, "]")
ci.log.AuditInfo(builder.String())

template.RevokedCertificates = rcs

Expand Down
1 change: 1 addition & 0 deletions cmd/boulder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
_ "github.com/letsencrypt/boulder/cmd/ceremony"
_ "github.com/letsencrypt/boulder/cmd/cert-checker"
_ "github.com/letsencrypt/boulder/cmd/contact-auditor"
_ "github.com/letsencrypt/boulder/cmd/crl-storer"
_ "github.com/letsencrypt/boulder/cmd/crl-updater"
_ "github.com/letsencrypt/boulder/cmd/expiration-mailer"
_ "github.com/letsencrypt/boulder/cmd/id-exporter"
Expand Down
153 changes: 153 additions & 0 deletions cmd/crl-storer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package notmain

import (
"context"
"flag"
"net/http"
"os"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
awsl "github.com/aws/smithy-go/logging"
"github.com/honeycombio/beeline-go"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"

"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/crl/storer"
cspb "github.com/letsencrypt/boulder/crl/storer/proto"
"github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/issuance"
blog "github.com/letsencrypt/boulder/log"
)

type Config struct {
CRLStorer struct {
cmd.ServiceConfig

// IssuerCerts is a list of paths to issuer certificates on disk. These will
// be used to validate the CRLs received by this service before uploading
// them.
IssuerCerts []string

// S3Endpoint is the URL at which the S3-API-compatible object storage
// service can be reached. This can be used to point to a non-Amazon storage
// service, or to point to a fake service for testing. It should be left
// blank by default.
S3Endpoint string
// S3Region is the AWS Region (e.g. us-west-1) that uploads should go to.
S3Region string
// S3Bucket is the AWS Bucket that uploads should go to. Must be created
// (and have appropriate permissions set) beforehand.
S3Bucket string
// S3CredsFile is the path to a file on disk containing AWS credentials.
// The format of the credentials file is specified at
// https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html.
S3CredsFile string

Features map[string]bool
}

Syslog cmd.SyslogConfig
Beeline cmd.BeelineConfig
}

// awsLogger implements the github.com/aws/smithy-go/logging.Logger interface.
type awsLogger struct {
blog.Logger
}

func (log awsLogger) Logf(c awsl.Classification, format string, v ...interface{}) {
switch c {
case awsl.Debug:
log.Debugf(format, v...)
case awsl.Warn:
log.Warningf(format, v...)
}
}

func main() {
configFile := flag.String("config", "", "File path to the configuration file for this service")
flag.Parse()
if *configFile == "" {
flag.Usage()
os.Exit(1)
}

var c Config
err := cmd.ReadConfigFile(*configFile, &c)
cmd.FailOnError(err, "Reading JSON config file into config structure")

err = features.Set(c.CRLStorer.Features)
cmd.FailOnError(err, "Failed to set feature flags")

tlsConfig, err := c.CRLStorer.TLS.Load()
cmd.FailOnError(err, "TLS config")

scope, logger := cmd.StatsAndLogging(c.Syslog, c.CRLStorer.DebugAddr)
defer logger.AuditPanic()
logger.Info(cmd.VersionString())
clk := cmd.Clock()

bc, err := c.Beeline.Load()
cmd.FailOnError(err, "Failed to load Beeline config")
beeline.Init(bc)
defer beeline.Close()

issuers := make([]*issuance.Certificate, 0, len(c.CRLStorer.IssuerCerts))
for _, filepath := range c.CRLStorer.IssuerCerts {
cert, err := issuance.LoadCertificate(filepath)
cmd.FailOnError(err, "Failed to load issuer cert")
issuers = append(issuers, cert)
}

// Load the "default" AWS configuration, but override the set of config files
// it reads from to be the empty set, and override the set of credentials
// files it reads from to be just the one file specified in the Config. This
// helps stop us from accidentally loading unexpected or undesired config.
// Note that it *will* still load configuration from environment variables.
awsConfig, err := config.LoadDefaultConfig(
context.Background(),
config.WithSharedConfigFiles([]string{}),
config.WithSharedCredentialsFiles([]string{c.CRLStorer.S3CredsFile}),
config.WithRegion(c.CRLStorer.S3Region),
config.WithHTTPClient(new(http.Client)),
config.WithLogger(awsLogger{logger}),
config.WithClientLogMode(aws.LogRequestEventMessage|aws.LogResponseEventMessage),
)
cmd.FailOnError(err, "Failed to load AWS config")

s3opts := make([]func(*s3.Options), 0)
if c.CRLStorer.S3Endpoint != "" {
s3opts = append(
s3opts,
s3.WithEndpointResolver(s3.EndpointResolverFromURL(c.CRLStorer.S3Endpoint)),
func(o *s3.Options) { o.UsePathStyle = true },
)
}
s3client := s3.NewFromConfig(awsConfig, s3opts...)

csi, err := storer.New(issuers, s3client, c.CRLStorer.S3Bucket, scope, logger, clk)
cmd.FailOnError(err, "Failed to create CRLStorer impl")

serverMetrics := bgrpc.NewServerMetrics(scope)
grpcSrv, listener, err := bgrpc.NewServer(c.CRLStorer.GRPC, tlsConfig, serverMetrics, clk)
cmd.FailOnError(err, "Unable to setup CRLStorer gRPC server")
cspb.RegisterCRLStorerServer(grpcSrv, csi)
hs := health.NewServer()
healthpb.RegisterHealthServer(grpcSrv, hs)

go cmd.CatchSignals(logger, func() {
hs.Shutdown()
grpcSrv.GracefulStop()
})

err = cmd.FilterShutdownErrors(grpcSrv.Serve(listener))
cmd.FailOnError(err, "CRLStorer gRPC service failed")
}

func init() {
cmd.RegisterCommand("crl-storer", main)
}
54 changes: 47 additions & 7 deletions cmd/crl-updater/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import (
"os"

"github.com/honeycombio/beeline-go"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"

capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/cmd"
cspb "github.com/letsencrypt/boulder/crl/storer/proto"
"github.com/letsencrypt/boulder/crl/updater"
"github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc"
Expand All @@ -20,9 +23,9 @@ type Config struct {
CRLUpdater struct {
cmd.ServiceConfig

CRLGeneratorService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
// TODO(#6162): Add CRLStorerService stanza
CRLGeneratorService *cmd.GRPCClientConfig
CRLStorerService *cmd.GRPCClientConfig

// IssuerCerts is a list of paths to issuer certificates on disk. This
// controls the set of CRLs which will be published by this updater: it will
Expand Down Expand Up @@ -73,6 +76,7 @@ type Config struct {

func main() {
configFile := flag.String("config", "", "File path to the configuration file for this service")
debugAddr := flag.String("debug-addr", "", "Debug server address override")
runOnce := flag.Bool("runOnce", false, "If true, run once immediately and then exit")
flag.Parse()
if *configFile == "" {
Expand All @@ -84,6 +88,10 @@ func main() {
err := cmd.ReadConfigFile(*configFile, &c)
cmd.FailOnError(err, "Reading JSON config file into config structure")

if *debugAddr != "" {
c.CRLUpdater.DebugAddr = *debugAddr
}

err = features.Set(c.CRLUpdater.Features)
cmd.FailOnError(err, "Failed to set feature flags")

Expand All @@ -109,15 +117,22 @@ func main() {

clientMetrics := bgrpc.NewClientMetrics(scope)

caConn, err := bgrpc.ClientSetup(c.CRLUpdater.CRLGeneratorService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to CRLGenerator")
cac := capb.NewCRLGeneratorClient(caConn)

saConn, err := bgrpc.ClientSetup(c.CRLUpdater.SAService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := sapb.NewStorageAuthorityClient(saConn)

// TODO(#6162): Set up crl-storer client connection.
caConn, err := bgrpc.ClientSetup(c.CRLUpdater.CRLGeneratorService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to CRLGenerator")
cac := capb.NewCRLGeneratorClient(caConn)

var csc cspb.CRLStorerClient
if c.CRLUpdater.CRLStorerService != nil {
csConn, err := bgrpc.ClientSetup(c.CRLUpdater.CRLStorerService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to CRLStorer")
csc = cspb.NewCRLStorerClient(csConn)
} else {
csc = &fakeStorerClient{}
}

u, err := updater.NewUpdater(
issuers,
Expand All @@ -128,6 +143,7 @@ func main() {
c.CRLUpdater.MaxParallelism,
sac,
cac,
csc,
scope,
logger,
clk,
Expand All @@ -144,6 +160,30 @@ func main() {
}
}

// fakeStorerClient implements the cspb.CRLStorerClient interface. It is used
// to replace a real client if the CRLStorerService config stanza is not
// populated.
type fakeStorerClient struct{}

func (*fakeStorerClient) UploadCRL(ctx context.Context, opts ...grpc.CallOption) (cspb.CRLStorer_UploadCRLClient, error) {
return &fakeStorerStream{}, nil
}

// fakeStorerStream implements the cspb.CRLStorer_UploadCRLClient interface.
// It is used to replace a real client stream when the CRLStorerService config
// stanza is not populated.
type fakeStorerStream struct {
grpc.ClientStream
}

func (*fakeStorerStream) Send(*cspb.UploadCRLRequest) error {
return nil
}

func (*fakeStorerStream) CloseAndRecv() (*emptypb.Empty, error) {
return &emptypb.Empty{}, nil
}

func init() {
cmd.RegisterCommand("crl-updater", main)
}
Loading

0 comments on commit 6a9bb39

Please sign in to comment.