diff --git a/pkg/config/tlscfg/ciphersuites.go b/pkg/config/tlscfg/ciphersuites.go new file mode 100644 index 00000000000..8580312de73 --- /dev/null +++ b/pkg/config/tlscfg/ciphersuites.go @@ -0,0 +1,42 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tlscfg + +import ( + "crypto/tls" + "fmt" +) + +func allCiphers() map[string]uint16 { + acceptedCiphers := make(map[string]uint16) + for _, suite := range tls.CipherSuites() { + acceptedCiphers[suite.Name] = suite.ID + } + return acceptedCiphers +} + +// CipherSuiteNamesToIDs returns a list of cipher suite IDs from the cipher suite names passed. +func CipherSuiteNamesToIDs(cipherNames []string) ([]uint16, error) { + var ciphersIDs []uint16 + possibleCiphers := allCiphers() + for _, cipher := range cipherNames { + intValue, ok := possibleCiphers[cipher] + if !ok { + return nil, fmt.Errorf("cipher suite %s not supported or doesn't exist", cipher) + } + ciphersIDs = append(ciphersIDs, intValue) + } + return ciphersIDs, nil +} diff --git a/pkg/config/tlscfg/ciphersuites_test.go b/pkg/config/tlscfg/ciphersuites_test.go new file mode 100644 index 00000000000..cae0f845db2 --- /dev/null +++ b/pkg/config/tlscfg/ciphersuites_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tlscfg + +import ( + "crypto/tls" + "reflect" + "testing" +) + +func TestTLSCipherSuites(t *testing.T) { + tests := []struct { + flag []string + expected []uint16 + expectedError bool + }{ + { + // Happy case + flag: []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}, + expected: []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384}, + expectedError: false, + }, + { + // One flag only + flag: []string{"TLS_AES_128_GCM_SHA256"}, + expected: []uint16{tls.TLS_AES_128_GCM_SHA256}, + expectedError: false, + }, + { + // Empty flag + flag: []string{}, + expected: nil, + expectedError: false, + }, + { + // Duplicated flag + flag: []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"}, + expected: []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_AES_128_GCM_SHA256}, + expectedError: false, + }, + { + // Invalid flag + flag: []string{"TLS_INVALID_CIPHER_SUITE"}, + expected: nil, + expectedError: true, + }, + } + + for i, test := range tests { + uIntFlags, err := CipherSuiteNamesToIDs(test.flag) + if !reflect.DeepEqual(uIntFlags, test.expected) { + t.Errorf("%d: expected %+v, got %+v", i, test.expected, uIntFlags) + } + if test.expectedError && err == nil { + t.Errorf("%d: expecting error, got %+v", i, err) + } + } +} diff --git a/pkg/config/tlscfg/flags.go b/pkg/config/tlscfg/flags.go index a59cca7d4f7..f74f73fb895 100644 --- a/pkg/config/tlscfg/flags.go +++ b/pkg/config/tlscfg/flags.go @@ -16,6 +16,7 @@ package tlscfg import ( "flag" + "strings" "github.com/spf13/viper" ) @@ -29,6 +30,7 @@ const ( tlsServerName = tlsPrefix + ".server-name" tlsClientCA = tlsPrefix + ".client-ca" tlsSkipHostVerify = tlsPrefix + ".skip-host-verify" + tlsCipherSuites = tlsPrefix + ".cipher-suites" ) // ClientFlagsConfig describes which CLI flags for TLS client should be generated. @@ -57,6 +59,7 @@ func (c ServerFlagsConfig) AddFlags(flags *flag.FlagSet) { flags.String(c.Prefix+tlsCert, "", "Path to a TLS Certificate file, used to identify this server to clients") flags.String(c.Prefix+tlsKey, "", "Path to a TLS Private Key file, used to identify this server to clients") flags.String(c.Prefix+tlsClientCA, "", "Path to a TLS CA (Certification Authority) file used to verify certificates presented by clients (if unset, all clients are permitted)") + flags.String(c.Prefix+tlsCipherSuites, "", "Comma-separated list of cipher suites for the server, values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants).") } // InitFromViper creates tls.Config populated with values retrieved from Viper. @@ -78,5 +81,11 @@ func (c ServerFlagsConfig) InitFromViper(v *viper.Viper) Options { p.CertPath = v.GetString(c.Prefix + tlsCert) p.KeyPath = v.GetString(c.Prefix + tlsKey) p.ClientCAPath = v.GetString(c.Prefix + tlsClientCA) + p.CipherSuites = strings.Split(stripWhiteSpace(v.GetString(c.Prefix+tlsCipherSuites)), ",") return p } + +// stripWhiteSpace removes all whitespace characters from a string +func stripWhiteSpace(str string) string { + return strings.Replace(str, " ", "", -1) +} diff --git a/pkg/config/tlscfg/flags_test.go b/pkg/config/tlscfg/flags_test.go index 908c848e954..159f4753127 100644 --- a/pkg/config/tlscfg/flags_test.go +++ b/pkg/config/tlscfg/flags_test.go @@ -74,6 +74,7 @@ func TestServerFlags(t *testing.T) { "--prefix.tls.enabled=true", "--prefix.tls.cert=cert-file", "--prefix.tls.key=key-file", + "--prefix.tls.cipher-suites=TLS_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", } tests := []struct { @@ -107,6 +108,7 @@ func TestServerFlags(t *testing.T) { CertPath: "cert-file", KeyPath: "key-file", ClientCAPath: test.file, + CipherSuites: []string{"TLS_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"}, }, tlsOpts) }) } diff --git a/pkg/config/tlscfg/options.go b/pkg/config/tlscfg/options.go index dc2e316cb5b..52ab518aec6 100644 --- a/pkg/config/tlscfg/options.go +++ b/pkg/config/tlscfg/options.go @@ -33,6 +33,7 @@ type Options struct { KeyPath string `mapstructure:"key"` ServerName string `mapstructure:"server_name"` // only for client-side TLS config ClientCAPath string `mapstructure:"client_ca"` // only for server-side TLS config for client auth + CipherSuites []string `mapstructure:"cipher_suites"` SkipHostVerify bool `mapstructure:"skip_host_verify"` certWatcher *certWatcher `mapstructure:"-"` } @@ -41,17 +42,24 @@ var systemCertPool = x509.SystemCertPool // to allow overriding in unit test // Config loads TLS certificates and returns a TLS Config. func (p *Options) Config(logger *zap.Logger) (*tls.Config, error) { - certPool, err := p.loadCertPool() if err != nil { return nil, fmt.Errorf("failed to load CA CertPool: %w", err) } + + cipherSuiteIds, err := CipherSuiteNamesToIDs(p.CipherSuites) + if err != nil { + return nil, fmt.Errorf("failed to get cipher suite ids from cipher suite names: %w", err) + } + // #nosec G402 tlsCfg := &tls.Config{ RootCAs: certPool, ServerName: p.ServerName, InsecureSkipVerify: p.SkipHostVerify, + CipherSuites: cipherSuiteIds, } + if p.ClientCAPath != "" { certPool := x509.NewCertPool() if err := addCertToPool(p.ClientCAPath, certPool); err != nil { diff --git a/pkg/config/tlscfg/options_test.go b/pkg/config/tlscfg/options_test.go index 4f8df04129d..6f8bbdbd766 100644 --- a/pkg/config/tlscfg/options_test.go +++ b/pkg/config/tlscfg/options_test.go @@ -121,6 +121,13 @@ func TestOptionsToConfig(t *testing.T) { ClientCAPath: testCertKeyLocation + "/example-CA-cert.pem", }, }, + { + name: "should fail with invalid Cipher Suite", + options: Options{ + CipherSuites: []string{"TLS_INVALID_CIPHER_SUITE"}, + }, + expectError: "failed to get cipher suite ids from cipher suite names: cipher suite TLS_INVALID_CIPHER_SUITE not supported or doesn't exist", + }, } for _, test := range tests {