boulder/cmd/boulder-wfe2/main.go

385 lines
14 KiB
Go

package main
import (
"bytes"
"context"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log"
noncepb "github.com/letsencrypt/boulder/nonce/proto"
rapb "github.com/letsencrypt/boulder/ra/proto"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/wfe2"
"github.com/prometheus/client_golang/prometheus"
)
type config struct {
WFE struct {
cmd.ServiceConfig
ListenAddress string
TLSListenAddress string
ServerCertificatePath string
ServerKeyPath string
AllowOrigins []string
ShutdownStopTimeout cmd.ConfigDuration
SubscriberAgreementURL string
TLS cmd.TLSConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
// GetNonceService contains a gRPC config for any nonce-service instances
// which we want to retrieve nonces from. In a multi-DC deployment this
// should refer to any local nonce-service instances.
GetNonceService *cmd.GRPCClientConfig
// RedeemNonceServices contains a map of nonce-service prefixes to
// gRPC configs we want to use to redeem nonces. In a multi-DC deployment
// this should contain all nonce-services from all DCs as we want to be
// able to redeem nonces generated at any DC.
RedeemNonceServices map[string]cmd.GRPCClientConfig
// CertificateChains maps AIA issuer URLs to certificate filenames.
// Certificates are read into the chain in the order they are defined in the
// slice of filenames.
CertificateChains map[string][]string
// AlternateCertificateChains maps AIA issuer URLs to an optional alternate
// certificate chain, represented by an ordered slice of certificate filenames.
AlternateCertificateChains map[string][]string
Features map[string]bool
// DirectoryCAAIdentity is used for the /directory response's "meta"
// element's "caaIdentities" field. It should match the VA's "issuerDomain"
// configuration value (this value is the one used to enforce CAA)
DirectoryCAAIdentity string
// DirectoryWebsite is used for the /directory response's "meta" element's
// "website" field.
DirectoryWebsite string
// ACMEv2 requests (outside some registration/revocation messages) use a JWS with
// a KeyID header containing the full account URL. For new accounts this
// will be a KeyID based on the HTTP request's Host header and the ACMEv2
// account path. For legacy ACMEv1 accounts we need to whitelist the account
// ID prefix that legacy accounts would have been using based on the Host
// header of the WFE1 instance and the legacy 'reg' path component. This
// will differ in configuration for production and staging.
LegacyKeyIDPrefix string
// BlockedKeyFile is the path to a YAML file containing Base64 encoded
// SHA256 hashes of SubjectPublicKeyInfo's that should be considered
// administratively blocked.
BlockedKeyFile string
// StaleTimeout determines how old should data be to be accessed via Boulder-specific GET-able APIs
StaleTimeout cmd.ConfigDuration
// AuthorizationLifetimeDays defines how long authorizations will be
// considered valid for. The WFE uses this to find the creation date of
// authorizations by subtracing this value from the expiry. It should match
// the value configured in the RA.
AuthorizationLifetimeDays int
// PendingAuthorizationLifetimeDays defines how long authorizations may be in
// the pending state before expiry. The WFE uses this to find the creation
// date of pending authorizations by subtracting this value from the expiry.
// It should match the value configured in the RA.
PendingAuthorizationLifetimeDays int
}
Syslog cmd.SyslogConfig
Common struct {
IssuerCert string
}
}
// loadCertificateFile loads a PEM certificate from the certFile provided. It
// validates that the PEM is well-formed with no leftover bytes, and contains
// only a well-formed X509 certificate. If the cert file meets these
// requirements the PEM bytes from the file are returned along with the parsed
// certificate, otherwise an error is returned. If the PEM contents of
// a certFile do not have a trailing newline one is added.
func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, *x509.Certificate, error) {
pemBytes, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - error reading contents: %s",
aiaIssuerURL, certFile, err)
}
if bytes.Contains(pemBytes, []byte("\r\n")) {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - contents had CRLF line endings",
aiaIssuerURL, certFile)
}
// Try to decode the contents as PEM
certBlock, rest := pem.Decode(pemBytes)
if certBlock == nil {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - contents did not decode as PEM",
aiaIssuerURL, certFile)
}
// The PEM contents must be a CERTIFICATE
if certBlock.Type != "CERTIFICATE" {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - PEM block type incorrect, found "+
"%q, expected \"CERTIFICATE\"",
aiaIssuerURL, certFile, certBlock.Type)
}
// The PEM Certificate must successfully parse
var cert *x509.Certificate
if cert, err = x509.ParseCertificate(certBlock.Bytes); err != nil {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - certificate bytes failed to parse: %s",
aiaIssuerURL, certFile, err)
}
// If there are bytes leftover we must reject the file otherwise these
// leftover bytes will end up in a served certificate chain.
if len(rest) != 0 {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - PEM contents had unused remainder "+
"input (%d bytes)",
aiaIssuerURL, certFile, len(rest))
}
// If the PEM contents don't end in a \n, add it.
if pemBytes[len(pemBytes)-1] != '\n' {
pemBytes = append(pemBytes, '\n')
}
return pemBytes, cert, nil
}
// loadCertificateChains processes the provided chainConfig of AIA Issuer URLs
// and cert filenames. For each AIA issuer URL all of its cert filenames are
// read, validated as PEM certificates, and concatenated together separated by
// newlines. The combined PEM certificate chain contents for each are returned
// in the results map, keyed by the AIA Issuer URL. Additionally the first
// certificate in each chain is parsed and returned in a slice of issuer
// certificates.
func loadCertificateChains(chainConfig map[string][]string, requireAtLeastOneChain bool) (map[string][]byte, []*x509.Certificate, error) {
results := make(map[string][]byte, len(chainConfig))
var issuerCerts []*x509.Certificate
// For each AIA Issuer URL we need to read the chain cert files
for aiaIssuerURL, certFiles := range chainConfig {
var buffer bytes.Buffer
// There must be at least one chain file specified
if requireAtLeastOneChain && len(certFiles) == 0 {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has no chain "+
"file names configured",
aiaIssuerURL)
}
// certFiles are read and appended in the order they appear in the
// configuration
for i, c := range certFiles {
// Prepend a newline before each chain entry
buffer.Write([]byte("\n"))
// Read and validate the chain file contents
pemBytes, cert, err := loadCertificateFile(aiaIssuerURL, c)
if err != nil {
return nil, nil, err
}
// Save the first certificate as a direct issuer certificate
if i == 0 {
issuerCerts = append(issuerCerts, cert)
}
// Write the PEM bytes to the result buffer for this AIAIssuer
buffer.Write(pemBytes)
}
// Save the full PEM chain contents, if any
if buffer.Len() > 0 {
results[aiaIssuerURL] = buffer.Bytes()
}
}
return results, issuerCerts, nil
}
func setupWFE(c config, logger blog.Logger, stats prometheus.Registerer, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient, map[string]noncepb.NonceServiceClient) {
tlsConfig, err := c.WFE.TLS.Load()
cmd.FailOnError(err, "TLS config")
clientMetrics := bgrpc.NewClientMetrics(stats)
raConn, err := bgrpc.ClientSetup(c.WFE.RAService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
rac := bgrpc.NewRegistrationAuthorityClient(rapb.NewRegistrationAuthorityClient(raConn))
saConn, err := bgrpc.ClientSetup(c.WFE.SAService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := bgrpc.NewStorageAuthorityClient(sapb.NewStorageAuthorityClient(saConn))
var rns noncepb.NonceServiceClient
npm := map[string]noncepb.NonceServiceClient{}
if c.WFE.GetNonceService != nil {
rnsConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
rns = noncepb.NewNonceServiceClient(rnsConn)
for prefix, serviceConfig := range c.WFE.RedeemNonceServices {
conn, err := bgrpc.ClientSetup(&serviceConfig, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
npm[prefix] = noncepb.NewNonceServiceClient(conn)
}
}
return rac, sac, rns, npm
}
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")
// Map of AIA Issuer URLs to a slice of PEM-encoded certificate chains.
// The first chain in the slice is the default chain, and subsequent
// chains are alternates.
allCertChains := make(map[string][][]byte, len(c.WFE.CertificateChains))
certChains, issuerCerts, err := loadCertificateChains(c.WFE.CertificateChains, true)
cmd.FailOnError(err, "Couldn't read configured CertificateChains")
for aiaURL, chainPEM := range certChains {
allCertChains[aiaURL] = [][]byte{chainPEM}
}
err = features.Set(c.WFE.Features)
cmd.FailOnError(err, "Failed to set feature flags")
if c.WFE.AlternateCertificateChains != nil {
altCertChains, _, err := loadCertificateChains(c.WFE.AlternateCertificateChains, false)
cmd.FailOnError(err, "Couldn't read configured AlternateCertificateChains")
for aiaURL, chainPEM := range altCertChains {
if _, ok := allCertChains[aiaURL]; !ok {
cmd.Fail(fmt.Sprintf("AIA Issuer URL %s appeared in AlternateCertificateChains, "+
"but does not exist in CertificateChains", aiaURL))
}
allCertChains[aiaURL] = append(allCertChains[aiaURL], chainPEM)
}
}
stats, logger := cmd.StatsAndLogging(c.Syslog, c.WFE.DebugAddr)
defer logger.AuditPanic()
logger.Info(cmd.VersionString())
clk := cmd.Clock()
rac, sac, rns, npm := setupWFE(c, logger, stats, clk)
var blockedKeyFunc goodkey.BlockedKeyCheckFunc
if features.Enabled(features.BlockedKeyTable) {
blockedKeyFunc = sac.KeyBlocked
}
// don't load any weak keys, but do load blocked keys
kp, err := goodkey.NewKeyPolicy("", c.WFE.BlockedKeyFile, blockedKeyFunc)
cmd.FailOnError(err, "Unable to create key policy")
if c.WFE.StaleTimeout.Duration == 0 {
c.WFE.StaleTimeout.Duration = time.Minute * 10
}
authorizationLifetime := 30 * (24 * time.Hour)
if c.WFE.AuthorizationLifetimeDays != 0 {
authorizationLifetime = time.Duration(c.WFE.AuthorizationLifetimeDays) * (24 * time.Hour)
}
pendingAuthorizationLifetime := 7 * (24 * time.Hour)
if c.WFE.PendingAuthorizationLifetimeDays != 0 {
pendingAuthorizationLifetime = time.Duration(c.WFE.PendingAuthorizationLifetimeDays) * (24 * time.Hour)
}
wfe, err := wfe2.NewWebFrontEndImpl(stats, clk, kp, allCertChains, issuerCerts, rns, npm, logger, c.WFE.StaleTimeout.Duration, authorizationLifetime, pendingAuthorizationLifetime)
cmd.FailOnError(err, "Unable to create WFE")
wfe.RA = rac
wfe.SA = sac
wfe.SubscriberAgreementURL = c.WFE.SubscriberAgreementURL
wfe.AllowOrigins = c.WFE.AllowOrigins
wfe.DirectoryCAAIdentity = c.WFE.DirectoryCAAIdentity
wfe.DirectoryWebsite = c.WFE.DirectoryWebsite
wfe.LegacyKeyIDPrefix = c.WFE.LegacyKeyIDPrefix
wfe.IssuerCert, err = cmd.LoadCert(c.Common.IssuerCert)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read issuer cert [%s]", c.Common.IssuerCert))
logger.Infof("WFE using key policy: %#v", kp)
logger.Infof("Server running, listening on %s...\n", c.WFE.ListenAddress)
handler := wfe.Handler(stats)
srv := &http.Server{
Addr: c.WFE.ListenAddress,
Handler: handler,
}
go func() {
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
cmd.FailOnError(err, "Running HTTP server")
}
}()
var tlsSrv *http.Server
if c.WFE.TLSListenAddress != "" {
tlsSrv = &http.Server{
Addr: c.WFE.TLSListenAddress,
Handler: handler,
}
go func() {
err := tlsSrv.ListenAndServeTLS(c.WFE.ServerCertificatePath, c.WFE.ServerKeyPath)
if err != nil && err != http.ErrServerClosed {
cmd.FailOnError(err, "Running TLS server")
}
}()
}
done := make(chan bool)
go cmd.CatchSignals(logger, func() {
ctx, cancel := context.WithTimeout(context.Background(), c.WFE.ShutdownStopTimeout.Duration)
defer cancel()
_ = srv.Shutdown(ctx)
if tlsSrv != nil {
_ = tlsSrv.Shutdown(ctx)
}
done <- true
})
// https://godoc.org/net/http#Server.Shutdown:
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
// immediately return ErrServerClosed. Make sure the program doesn't exit and
// waits instead for Shutdown to return.
<-done
}