Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ Usage of ./observatorium-api:
Comma-separated list of cipher suites for the server. Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). If omitted, the default Go cipher suites will be used. Note that TLS 1.3 ciphersuites are not configurable.
-tls.client-auth-type string
Policy for TLS client-side authentication. Values are from ClientAuthType constants in https://pkg.go.dev/crypto/tls#ClientAuthType (default "RequestClientCert")
-tls.curve-preferences string
Comma-separated list of key exchange groups for the server. Values are IANA "TLS Supported Groups" names (e.g. X25519, secp256r1, X25519MLKEM768); Go crypto/tls constant names (e.g. CurveP256) are also accepted for the classic curves. If omitted, the default Go groups will be used. The list is a filter of allowed groups; crypto/tls chooses the preference order.
-tls.healthchecks.server-ca-file string
File containing the TLS CA against which to verify servers. If no server CA is specified, the client will use the system certificates.
-tls.healthchecks.server-name string
Expand Down
24 changes: 19 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,12 @@ type serverConfig struct {
}

type tlsConfig struct {
minVersion string
maxVersion string
cipherSuites []string
clientAuthType string
reloadInterval time.Duration
minVersion string
maxVersion string
cipherSuites []string
curvePreferences []string
clientAuthType string
reloadInterval time.Duration

serverCertFile string
serverKeyFile string
Expand Down Expand Up @@ -894,6 +895,7 @@ func main() {
cfg.tls.maxVersion,
cfg.tls.clientAuthType,
cfg.tls.cipherSuites,
cfg.tls.curvePreferences,
)
if err != nil {
stdlog.Fatalf("failed to initialize tls config: %v", err)
Expand Down Expand Up @@ -1004,6 +1006,7 @@ func main() {
cfg.tls.maxVersion,
cfg.tls.clientAuthType,
cfg.tls.cipherSuites,
cfg.tls.curvePreferences,
)
if err != nil {
stdlog.Fatalf("failed to initialize tls config: %v", err)
Expand Down Expand Up @@ -1108,6 +1111,7 @@ func (m *multiStringFlag) String() string {
func parseFlags() (config, error) {
var (
rawTLSCipherSuites string
rawTLSCurvePreferences string
rawMetricsReadEndpoint string
rawMetricsWriteEndpoint string
rawMetricsRulesEndpoint string
Expand Down Expand Up @@ -1277,6 +1281,12 @@ func parseFlags() (config, error) {
" Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants)."+
" If omitted, the default Go cipher suites will be used."+
" Note that TLS 1.3 ciphersuites are not configurable.")
flag.StringVar(&rawTLSCurvePreferences, "tls.curve-preferences", "",
"Comma-separated list of key exchange groups for the server."+
" Values are IANA \"TLS Supported Groups\" names (e.g. X25519, secp256r1, X25519MLKEM768);"+
" Go crypto/tls constant names (e.g. CurveP256) are also accepted for the classic curves."+
" If omitted, the default Go groups will be used."+
" The list is a filter of allowed groups; crypto/tls chooses the preference order.")
flag.StringVar(&cfg.tls.clientAuthType, "tls.client-auth-type", "RequestClientCert",
"Policy for TLS client-side authentication. Values are from ClientAuthType constants in https://pkg.go.dev/crypto/tls#ClientAuthType")
flag.DurationVar(&cfg.tls.reloadInterval, "tls.reload-interval", time.Minute,
Expand Down Expand Up @@ -1485,6 +1495,9 @@ func parseFlags() (config, error) {
if rawTLSCipherSuites != "" {
cfg.tls.cipherSuites = strings.Split(rawTLSCipherSuites, ",")
}
if rawTLSCurvePreferences != "" {
cfg.tls.curvePreferences = strings.Split(rawTLSCurvePreferences, ",")
}

return cfg, nil
}
Expand Down Expand Up @@ -1618,6 +1631,7 @@ func newGRPCServer(cfg *config, tenantHeader string, tenantIDs map[string]string
cfg.tls.maxVersion,
cfg.tls.clientAuthType,
cfg.tls.cipherSuites,
cfg.tls.curvePreferences,
)
if err != nil {
return nil, fmt.Errorf("failed to create gRPC TLS config: %w", err)
Expand Down
52 changes: 48 additions & 4 deletions tls/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,26 @@ import (
"github.com/go-kit/log/level"
)

// curveIDs maps supported key-exchange group names to crypto/tls CurveIDs.
// Both IANA names and Go crypto/tls constant names are accepted.
var curveIDs = map[string]tls.CurveID{
// X25519 and the ML-KEM hybrids: IANA name == Go constant name.
"X25519": tls.X25519,
"X25519MLKEM768": tls.X25519MLKEM768,
"SecP256r1MLKEM768": tls.SecP256r1MLKEM768,
"SecP384r1MLKEM1024": tls.SecP384r1MLKEM1024,

// Classic EC curves: IANA name (preferred) and Go constant name (alias).
"secp256r1": tls.CurveP256,
"secp384r1": tls.CurveP384,
"secp521r1": tls.CurveP521,
"CurveP521": tls.CurveP521,
"CurveP256": tls.CurveP256,
"CurveP384": tls.CurveP384,
}

// NewServerConfig provides new server TLS configuration.
func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersion, clientAuthType string, cipherSuites []string) (*tls.Config, error) {
func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersion, clientAuthType string, cipherSuites, curvePreferences []string) (*tls.Config, error) {
if certFile == "" && keyFile == "" {
level.Info(logger).Log("msg", "TLS disabled; key and cert must be set to enable")

Expand Down Expand Up @@ -42,6 +60,11 @@ func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersio
return nil, fmt.Errorf("TLS cipher suite name to ID conversion: %v", err)
}

curvePreferenceIDs, err := mapCurveNamesToIDs(curvePreferences)
if err != nil {
return nil, fmt.Errorf("TLS curve preference name to ID conversion: %v", err)
}

tlsClientAuthType, err := parseClientAuthType(clientAuthType)
if err != nil {
return nil, fmt.Errorf("can not parse TLS Client authentication policy: %w", err)
Expand All @@ -53,9 +76,11 @@ func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersio
// If CipherSuites is nil, a default list of secure cipher suites is used.
// Note that TLS 1.3 ciphersuites are not configurable.
CipherSuites: cipherSuiteIDs,
ClientAuth: tlsClientAuthType,
MinVersion: tlsMinVersion,
MaxVersion: tlsMaxVersion,
// If CurvePreferences is nil, a default list of secure curves is used.
CurvePreferences: curvePreferenceIDs,
ClientAuth: tlsClientAuthType,
MinVersion: tlsMinVersion,
MaxVersion: tlsMaxVersion,
}

return tlsCfg, nil
Expand Down Expand Up @@ -108,6 +133,25 @@ func mapCipherNamesToIDs(rawTLSCipherSuites []string) ([]uint16, error) {
return cipherSuites, nil
}

func mapCurveNamesToIDs(rawTLSCurvePreferences []string) ([]tls.CurveID, error) {
if rawTLSCurvePreferences == nil {
return nil, nil
}

curvePreferences := []tls.CurveID{}

for _, name := range rawTLSCurvePreferences {
id, ok := curveIDs[name]
if !ok {
return nil, fmt.Errorf("unknown TLSCurve: %s", name)
}

curvePreferences = append(curvePreferences, id)
}

return curvePreferences, nil
}

func parseClientAuthType(rawAuthType string) (tls.ClientAuthType, error) {
switch rawAuthType {
case "NoClientCert":
Expand Down
147 changes: 147 additions & 0 deletions tls/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package tls

import (
"crypto/tls"
"testing"

"github.com/stretchr/testify/require"

"github.com/observatorium/api/logger"
)

func TestMapCurveNamesToIDs(t *testing.T) {
tests := []struct {
name string
input []string
want []tls.CurveID
errContain string
}{
{
name: "nil input",
input: nil,
want: nil,
},
{
name: "all supported names (IANA and Go constant)",
input: []string{
// X25519 and the ML-KEM hybrids: IANA name == Go constant name.
"X25519",
"X25519MLKEM768",
"SecP256r1MLKEM768",
"SecP384r1MLKEM1024",
// Classic EC curves: IANA name and Go constant name.
"secp256r1", "CurveP256",
"secp384r1", "CurveP384",
"secp521r1", "CurveP521",
},
want: []tls.CurveID{
tls.X25519,
tls.X25519MLKEM768,
tls.SecP256r1MLKEM768,
tls.SecP384r1MLKEM1024,
tls.CurveP256, tls.CurveP256,
tls.CurveP384, tls.CurveP384,
tls.CurveP521, tls.CurveP521,
},
},
{
name: "mixed IANA and Go constant names",
input: []string{"secp256r1", "CurveP384"},
want: []tls.CurveID{tls.CurveP256, tls.CurveP384},
},
{
name: "unknown curve name",
input: []string{"CurveUnknown"},
errContain: "unknown TLSCurve: CurveUnknown",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := mapCurveNamesToIDs(tc.input)
if tc.errContain != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.errContain)
return
}

require.NoError(t, err)
require.Equal(t, tc.want, got)
})
}
}

func TestNewServerConfigCurvePreferences(t *testing.T) {
certPath, keyPath, cleanCerts, err := newSelfSignedCert("localhost")
require.NoError(t, err)
defer cleanCerts()

l := logger.NewLogger("info", logger.LogFormatLogfmt, "")

tests := []struct {
name string
curves []string
want []tls.CurveID
errContains []string
}{
{
name: "omitted uses default curves",
curves: nil,
want: nil,
},
{
name: "configured curve preferences",
curves: []string{
"X25519MLKEM768",
"X25519",
"secp256r1",
"CurveP384",
"secp521r1",
"SecP256r1MLKEM768",
"SecP384r1MLKEM1024",
},
want: []tls.CurveID{
tls.X25519MLKEM768,
tls.X25519,
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
tls.SecP256r1MLKEM768,
tls.SecP384r1MLKEM1024,
},
},
{
name: "invalid curve preferences",
curves: []string{"CurveUnknown"},
errContains: []string{
"TLS curve preference name to ID conversion",
"unknown TLSCurve: CurveUnknown",
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cfg, err := NewServerConfig(
l,
certPath,
keyPath,
"VersionTLS13",
"VersionTLS13",
"RequestClientCert",
nil,
tc.curves,
)
if len(tc.errContains) > 0 {
require.Error(t, err)
for _, msg := range tc.errContains {
require.ErrorContains(t, err, msg)
}
return
}

require.NoError(t, err)
require.Equal(t, tc.want, cfg.CurvePreferences)
})
}
}