Skip to content

Commit

Permalink
Allow operators to configure TLS cipher suites and client auth
Browse files Browse the repository at this point in the history
  • Loading branch information
pgporada committed Apr 25, 2024
1 parent 78285f7 commit 687c432
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 20 deletions.
89 changes: 87 additions & 2 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,83 @@ type TLSConfig struct {
// The CACertFile file may contain any number of root certificates and will
// be deduplicated internally.
CACertFile string `validate:"required"`
// Valid ClientAuth values can be found at
// https://pkg.go.dev/crypto/tls#ClientAuthType. Boulder will select its own
// default value, rather than the default value from //crypto/tls, if no
// client auth is configured
ClientAuth string `validate:"omitempty"`
// Each CipherSuites value must be supported by //crypto/tls. Boulder will
// select its own default value, rather than the default value from
// //crypto/tls, if no cipher suite(s) is configured. Cipher suites will be
// ignored by tls.Config for TLS v1.3. Valid cipher suites can be found at
// https://cs.opensource.google/go/go/+/refs/tags/go1.22.2:src/crypto/tls/cipher_suites.go;l=59-68
CipherSuites []string `validate:"omitempty"`
}

// makeCipherSuitesFromConfig takes a slice of human-readable TLS cipher suite
// names declared in a config file and constructs a new slice of cipher suites
// usable by a tls.Config or returns an error.
func (t *TLSConfig) makeCipherSuitesFromConfig() ([]uint16, error) {
// We'll set a sane default, rather than use the default from //crypto/tls.
if len(t.CipherSuites) == 0 {
return []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}, nil
}

// Construct a reverse lookup to validate the operator provided name(s) is
// usable. The "legacy names" are unfortunately not included.
cipherSuiteNameToId := make(map[string]uint16, len(tls.CipherSuites()))
for _, id := range tls.CipherSuites() {
cipherSuiteNameToId[id.Name] = id.ID
}

var configuredCipherSuiteIds []uint16
for _, cs := range t.CipherSuites {
id, ok := cipherSuiteNameToId[cs]
if !ok {
return nil, fmt.Errorf("unsupported TLS cipher suite: %s", cs)
}
configuredCipherSuiteIds = append(configuredCipherSuiteIds, id)
}

return configuredCipherSuiteIds, nil
}

// makeClientAuthFromConfig converts the value of clientAuth from config file
// and returns the equivalent tls.ClientAuthType value or an error.
func (t *TLSConfig) makeClientAuthFromConfig() (tls.ClientAuthType, error) {
// Construct some lookup tables to map the configured ClientAuthType to the
// appropriate //crypto/tls const integer.
clientAuthTypeNameToId := make(map[string]int)
clientAuthTypeIdToName := make(map[int]string)

for i := 0; ; i++ {
name := tls.ClientAuthType(i).String()
if strings.Contains(name, "ClientAuthType") {
// A const block using iota doesn't have length so to speak.
// Non-existent values will return the name of the underlying type
// (not the concrete type!). We can use that to bail out early as
// soon as our maps contain only the values used inside
// //crypto/tls.
break
}
clientAuthTypeNameToId[name] = i
clientAuthTypeIdToName[i] = name
}

// We'll set a sane default, rather than use the default from //crypto/tls
// which is "NoClientCert".
var defaultId int
if t.ClientAuth == "" {
defaultId = clientAuthTypeNameToId["RequireAndVerifyClientCert"]
t.ClientAuth = clientAuthTypeIdToName[defaultId]
}

id, ok := clientAuthTypeNameToId[t.ClientAuth]
if !ok {
return -1, fmt.Errorf("unsupported TLS client auth value: %s", t.ClientAuth)
}

return tls.ClientAuthType(id), nil
}

// Load reads and parses the certificates and key listed in the TLSConfig, and
Expand Down Expand Up @@ -162,6 +239,14 @@ func (t *TLSConfig) Load(scope prometheus.Registerer) (*tls.Config, error) {
return nil, fmt.Errorf("loading key pair from %q and %q: %s",
t.CertFile, t.KeyFile, err)
}
cipherSuites, err := t.makeCipherSuitesFromConfig()
if err != nil {
return nil, err
}
clientAuth, err := t.makeClientAuthFromConfig()
if err != nil {
return nil, err
}

tlsNotBefore := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Expand Down Expand Up @@ -207,13 +292,13 @@ func (t *TLSConfig) Load(scope prometheus.Registerer) (*tls.Config, error) {
return &tls.Config{
RootCAs: rootCAs,
ClientCAs: rootCAs,
ClientAuth: tls.RequireAndVerifyClientCert,
ClientAuth: clientAuth,
Certificates: []tls.Certificate{cert},
// Set the only acceptable TLS to v1.2 and v1.3.
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
// CipherSuites will be ignored for TLS v1.3.
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
CipherSuites: cipherSuites,
}, nil
}

Expand Down
54 changes: 42 additions & 12 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"crypto/tls"
"testing"

"github.com/letsencrypt/boulder/metrics"
Expand Down Expand Up @@ -63,80 +64,109 @@ func TestTLSConfigLoad(t *testing.T) {
name string
expectedErrSubstr string
expectedRootStoreSize int
expectedCipherSuites []string
expectedClientAuth string
testConf TLSConfig
}{
{
name: "Empty cert",
expectedErrSubstr: "nil CertFile in TLSConfig",
testConf: TLSConfig{"", null, null},
testConf: TLSConfig{"", null, null, "", nil},
},
{
name: "Empty key",
expectedErrSubstr: "nil KeyFile in TLSConfig",
testConf: TLSConfig{null, "", null},
testConf: TLSConfig{null, "", null, "", nil},
},
{
name: "Empty root",
expectedErrSubstr: "nil CACertFile",
testConf: TLSConfig{null, null, ""},
testConf: TLSConfig{null, null, "", "", nil},
},
{
name: "Could not parse cert",
expectedErrSubstr: "failed to find any PEM data",
testConf: TLSConfig{null, key, caCertOne},
testConf: TLSConfig{null, key, caCertOne, "", nil},
},
{
name: "Could not parse key",
expectedErrSubstr: "failed to find any PEM data",
testConf: TLSConfig{cert, null, caCertOne},
testConf: TLSConfig{cert, null, caCertOne, "", nil},
},
{
name: "Could not parse root",
expectedErrSubstr: "parsing CA certs",
testConf: TLSConfig{cert, key, null},
testConf: TLSConfig{cert, key, null, "", nil},
},
{
name: "Invalid cert location",
expectedErrSubstr: "no such file or directory",
testConf: TLSConfig{nonExistent, key, caCertOne},
testConf: TLSConfig{nonExistent, key, caCertOne, "", nil},
},
{
name: "Invalid key location",
expectedErrSubstr: "no such file or directory",
testConf: TLSConfig{cert, nonExistent, caCertOne},
testConf: TLSConfig{cert, nonExistent, caCertOne, "", nil},
},
{
name: "Invalid root location",
expectedErrSubstr: "no such file or directory",
testConf: TLSConfig{cert, key, nonExistent},
testConf: TLSConfig{cert, key, nonExistent, "", nil},
},
{
name: "Valid config with one root",
testConf: TLSConfig{cert, key, caCertOne},
testConf: TLSConfig{cert, key, caCertOne, "", nil},
expectedRootStoreSize: 1,
},
{
name: "Valid config with two roots",
testConf: TLSConfig{cert, key, caCertMultiple},
testConf: TLSConfig{cert, key, caCertMultiple, "", nil},
expectedRootStoreSize: 2,
},
{
name: "Valid config with duplicate roots",
testConf: TLSConfig{cert, key, caCertDuplicate},
testConf: TLSConfig{cert, key, caCertDuplicate, "", nil},
expectedRootStoreSize: 1,
},
{
name: "Valid config with alternate ClientAuth",
testConf: TLSConfig{cert, key, caCertDuplicate, "NoClientCert", nil},
expectedRootStoreSize: 1,
expectedClientAuth: "NoClientCert",
},
{
name: "Valid config with alternate CipherSuite",
testConf: TLSConfig{cert, key, caCertDuplicate, "", []string{"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}},
expectedRootStoreSize: 1,
expectedCipherSuites: []string{"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
conf, err := tc.testConf.Load(metrics.NoopRegisterer)
if tc.expectedErrSubstr == "" {
if tc.expectedCipherSuites == nil {
// This default is set by makeCipherSuitesFromConfig()
tc.expectedCipherSuites = []string{"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"}
}
if tc.expectedClientAuth == "" {
// This default is set by makeClientAuthFromConfig()
tc.expectedClientAuth = "RequireAndVerifyClientCert"
}

test.AssertNotError(t, err, "Should not have errored, but did")

// We are not using SystemCertPool, we are manually defining our
// own.
test.AssertEquals(t, len(conf.RootCAs.Subjects()), tc.expectedRootStoreSize)
test.AssertEquals(t, len(conf.ClientCAs.Subjects()), tc.expectedRootStoreSize)
test.AssertEquals(t, conf.ClientAuth.String(), tc.expectedClientAuth)
test.AssertEquals(t, len(conf.CipherSuites), len(tc.expectedCipherSuites))
for idx, cs := range conf.CipherSuites {
test.AssertEquals(t, tls.CipherSuiteName(cs), tc.expectedCipherSuites[idx])
}
} else {
test.AssertError(t, err, "Expected an error but received none")
test.AssertContains(t, err.Error(), tc.expectedErrSubstr)
Expand Down
12 changes: 10 additions & 2 deletions test/config-next/remoteva-a.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@
"tls": {
"caCertfile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/rva.boulder/cert.pem",
"keyFile": "test/grpc-creds/rva.boulder/key.pem"
"keyFile": "test/grpc-creds/rva.boulder/key.pem",
"clientAuth": "RequireAndVerifyClientCert",
"cipherSuites": [
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
]
},
"tlsclient": {
"caCertfile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/rva.boulder/cert.pem",
"keyFile": "test/grpc-creds/rva.boulder/key.pem"
"keyFile": "test/grpc-creds/rva.boulder/key.pem",
"clientAuth": "RequireAndVerifyClientCert",
"cipherSuites": [
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
]
},
"grpc": {
"maxConnectionAge": "30s",
Expand Down
12 changes: 10 additions & 2 deletions test/config-next/remoteva-b.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@
"tls": {
"caCertfile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/rva.boulder/cert.pem",
"keyFile": "test/grpc-creds/rva.boulder/key.pem"
"keyFile": "test/grpc-creds/rva.boulder/key.pem",
"clientAuth": "RequireAndVerifyClientCert",
"cipherSuites": [
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
]
},
"tlsclient": {
"caCertfile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/rva.boulder/cert.pem",
"keyFile": "test/grpc-creds/rva.boulder/key.pem"
"keyFile": "test/grpc-creds/rva.boulder/key.pem",
"clientAuth": "RequireAndVerifyClientCert",
"cipherSuites": [
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
]
},
"grpc": {
"maxConnectionAge": "30s",
Expand Down
6 changes: 5 additions & 1 deletion test/config-next/va-remote-a.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"tls": {
"caCertfile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/rva.boulder/cert.pem",
"keyFile": "test/grpc-creds/rva.boulder/key.pem"
"keyFile": "test/grpc-creds/rva.boulder/key.pem",
"clientAuth": "RequireAndVerifyClientCert",
"cipherSuites": [
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
]
},
"grpc": {
"maxConnectionAge": "30s",
Expand Down
6 changes: 5 additions & 1 deletion test/config-next/va-remote-b.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"tls": {
"caCertfile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/rva.boulder/cert.pem",
"keyFile": "test/grpc-creds/rva.boulder/key.pem"
"keyFile": "test/grpc-creds/rva.boulder/key.pem",
"clientAuth": "RequireAndVerifyClientCert",
"cipherSuites": [
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
]
},
"grpc": {
"maxConnectionAge": "30s",
Expand Down

0 comments on commit 687c432

Please sign in to comment.