Skip to content

Commit

Permalink
Backport tls 1.3 support.
Browse files Browse the repository at this point in the history
Signed-off-by: James Blair <mail@jamesblair.net>
  • Loading branch information
jmhbnz committed Mar 16, 2023
1 parent 2eabc0b commit 164a987
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 1 deletion.
35 changes: 35 additions & 0 deletions embed/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ type Config struct {
// Note that cipher suites are prioritized in the given order.
CipherSuites []string `json:"cipher-suites"`

// TlsMinVersion is the minimum accepted TLS version between client/server and peers.
TlsMinVersion string `json:"tls-min-version"`
// TlsMaxVersion is the maximum accepted TLS version between client/server and peers.
TlsMaxVersion string `json:"tls-max-version"`

ClusterState string `json:"initial-cluster-state"`
DNSCluster string `json:"discovery-srv"`
DNSClusterServiceName string `json:"discovery-srv-name"`
Expand Down Expand Up @@ -575,6 +580,17 @@ func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
return nil
}

func updateMinMaxVersions(info *transport.TLSInfo, min, max string) {
// Validate() has been called to check the user input, so it should never fail.
var err error
if info.MinVersion, err = tlsutil.GetTLSVersion(min); err != nil {
panic(err)
}
if info.MaxVersion, err = tlsutil.GetTLSVersion(max); err != nil {
panic(err)
}
}

// Validate ensures that '*embed.Config' fields are properly configured.
func (cfg *Config) Validate() error {
if err := cfg.setupLogging(); err != nil {
Expand Down Expand Up @@ -646,6 +662,25 @@ func (cfg *Config) Validate() error {
return fmt.Errorf("setting experimental-enable-lease-checkpoint-persist requires experimental-enable-lease-checkpoint")
}

minVersion, err := tlsutil.GetTLSVersion(cfg.TlsMinVersion)
if err != nil {
return err
}
maxVersion, err := tlsutil.GetTLSVersion(cfg.TlsMaxVersion)
if err != nil {
return err
}

// maxVersion == 0 means that Go selects the highest available version.
if maxVersion != 0 && minVersion > maxVersion {
return fmt.Errorf("min version (%s) is greater than max version (%s)", cfg.TlsMinVersion, cfg.TlsMaxVersion)
}

// Check if user attempted to configure ciphers for TLS1.3 only: Go does not support that currently.
if minVersion == tls.VersionTLS13 && len(cfg.CipherSuites) > 0 {
return fmt.Errorf("cipher suites cannot be configured when only TLS1.3 is enabled")
}

return nil
}

Expand Down
79 changes: 79 additions & 0 deletions embed/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
package embed

import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/url"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"go.etcd.io/etcd/pkg/transport"

"sigs.k8s.io/yaml"
Expand Down Expand Up @@ -202,3 +204,80 @@ func TestAutoCompactionModeParse(t *testing.T) {
}
}
}

func TestTLSVersionMinMax(t *testing.T) {
tests := []struct {
name string
givenTLSMinVersion string
givenTLSMaxVersion string
givenCipherSuites []string
expectError bool
expectedMinTLSVersion uint16
expectedMaxTLSVersion uint16
}{
{
name: "Minimum TLS version is set",
givenTLSMinVersion: "TLS1.3",
expectedMinTLSVersion: tls.VersionTLS13,
expectedMaxTLSVersion: 0,
},
{
name: "Maximum TLS version is set",
givenTLSMaxVersion: "TLS1.2",
expectedMinTLSVersion: 0,
expectedMaxTLSVersion: tls.VersionTLS12,
},
{
name: "Minimum and Maximum TLS versions are set",
givenTLSMinVersion: "TLS1.3",
givenTLSMaxVersion: "TLS1.3",
expectedMinTLSVersion: tls.VersionTLS13,
expectedMaxTLSVersion: tls.VersionTLS13,
},
{
name: "Minimum and Maximum TLS versions are set in reverse order",
givenTLSMinVersion: "TLS1.3",
givenTLSMaxVersion: "TLS1.2",
expectError: true,
},
{
name: "Invalid minimum TLS version",
givenTLSMinVersion: "invalid version",
expectError: true,
},
{
name: "Invalid maximum TLS version",
givenTLSMaxVersion: "invalid version",
expectError: true,
},
{
name: "Cipher suites configured for TLS 1.3",
givenTLSMinVersion: "TLS1.3",
givenCipherSuites: []string{"TLS_AES_128_GCM_SHA256"},
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
cfg.TlsMinVersion = tt.givenTLSMinVersion
cfg.TlsMaxVersion = tt.givenTLSMaxVersion
cfg.CipherSuites = tt.givenCipherSuites

err := cfg.Validate()
if err != nil {
assert.True(t, tt.expectError, "Validate() returned error while expecting success: %v", err)
return
}

updateMinMaxVersions(&cfg.PeerTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)
updateMinMaxVersions(&cfg.ClientTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)

assert.Equal(t, tt.expectedMinTLSVersion, cfg.PeerTLSInfo.MinVersion)
assert.Equal(t, tt.expectedMaxTLSVersion, cfg.PeerTLSInfo.MaxVersion)
assert.Equal(t, tt.expectedMinTLSVersion, cfg.ClientTLSInfo.MinVersion)
assert.Equal(t, tt.expectedMaxTLSVersion, cfg.ClientTLSInfo.MaxVersion)
})
}
}
6 changes: 6 additions & 0 deletions embed/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,9 @@ func configurePeerListeners(cfg *Config) (peers []*peerListener, err error) {
plog.Fatalf("could not get certs (%v)", err)
}
}

updateMinMaxVersions(&cfg.PeerTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)

if !cfg.PeerTLSInfo.Empty() {
if cfg.logger != nil {
cfg.logger.Info(
Expand Down Expand Up @@ -608,6 +611,9 @@ func configureClientListeners(cfg *Config) (sctxs map[string]*serveCtx, err erro
plog.Fatalf("could not get certs (%v)", err)
}
}

updateMinMaxVersions(&cfg.ClientTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)

if cfg.EnablePprof {
if cfg.logger != nil {
cfg.logger.Info("pprof is enabled", zap.String("path", debugutil.HTTPPrefixPProf))
Expand Down
3 changes: 3 additions & 0 deletions etcdmain/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"go.etcd.io/etcd/embed"
"go.etcd.io/etcd/pkg/flags"
"go.etcd.io/etcd/pkg/logutil"
"go.etcd.io/etcd/pkg/tlsutil"
"go.etcd.io/etcd/pkg/types"
"go.etcd.io/etcd/version"

Expand Down Expand Up @@ -216,6 +217,8 @@ func newConfig() *config {
fs.StringVar(&cfg.ec.PeerTLSInfo.AllowedHostname, "peer-cert-allowed-hostname", "", "Allowed TLS hostname for inter peer authentication.")
fs.Var(flags.NewStringsValue(""), "cipher-suites", "Comma-separated list of supported TLS cipher suites between client/server and peers (empty will be auto-populated by Go).")
fs.BoolVar(&cfg.ec.PeerTLSInfo.SkipClientSANVerify, "experimental-peer-skip-client-san-verification", false, "Skip verification of SAN field in client certificate for peer connections.")
fs.StringVar(&cfg.ec.TlsMinVersion, "tls-min-version", string(tlsutil.TLSVersion12), "Minimum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3.")
fs.StringVar(&cfg.ec.TlsMaxVersion, "tls-max-version", string(tlsutil.TLSVersionDefault), "Maximum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3 (empty defers to Go).")

fs.Var(
flags.NewUniqueURLsWithExceptions("*", "*"),
Expand Down
4 changes: 4 additions & 0 deletions etcdmain/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ Security:
Comma-separated whitelist of origins for CORS, or cross-origin resource sharing, (empty or * means allow all).
--host-whitelist '*'
Acceptable hostnames from HTTP client requests, if server is not secure (empty or * means allow all).
--tls-min-version 'TLS1.2'
Minimum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3.
--tls-max-version ''
Maximum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3 (empty will be auto-populated by Go).
Auth:
--auth-token 'simple'
Expand Down
69 changes: 69 additions & 0 deletions integration/v3_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"go.etcd.io/etcd/clientv3"
"go.etcd.io/etcd/pkg/testutil"

Expand Down Expand Up @@ -49,6 +50,12 @@ func testTLSCipherSuites(t *testing.T, valid bool) {
srvTLS.CipherSuites, cliTLS.CipherSuites = cipherSuites[:2], cipherSuites[2:]
}

// go1.13 enables TLS 1.3 by default
// and in TLS 1.3, cipher suites are not configurable,
// so setting Max TLS version to TLS 1.2 to test cipher config.
srvTLS.MaxVersion = tls.VersionTLS12
cliTLS.MaxVersion = tls.VersionTLS12

clus := NewClusterV3(t, &ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)

Expand All @@ -75,3 +82,65 @@ func testTLSCipherSuites(t *testing.T, valid bool) {
t.Fatalf("expected TLS handshake success, got %v", cerr)
}
}

func TestTLSMinMaxVersion(t *testing.T) {

tests := []struct {
name string
minVersion uint16
maxVersion uint16
expectError bool
}{
{
name: "Connect with default TLS version should succeed",
minVersion: 0,
maxVersion: 0,
},
{
name: "Connect with TLS 1.2 only should fail",
minVersion: tls.VersionTLS12,
maxVersion: tls.VersionTLS12,
expectError: true,
},
{
name: "Connect with TLS 1.2 and 1.3 should succeed",
minVersion: tls.VersionTLS12,
maxVersion: tls.VersionTLS13,
},
{
name: "Connect with TLS 1.3 only should succeed",
minVersion: tls.VersionTLS13,
maxVersion: tls.VersionTLS13,
},
}

// Configure server to support TLS 1.3 only.
srvTLS := testTLSInfo
srvTLS.MinVersion = tls.VersionTLS13
srvTLS.MaxVersion = tls.VersionTLS13
clus := NewClusterV3(t, &ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cc, err := testTLSInfo.ClientConfig()
assert.NoError(t, err)

cc.MinVersion = tt.minVersion
cc.MaxVersion = tt.maxVersion
cli, cerr := clientv3.New(clientv3.Config{
Endpoints: []string{clus.Members[0].GRPCAddr()},
DialTimeout: time.Second,
DialOptions: []grpc.DialOption{grpc.WithBlock()},
TLS: cc,
})
if cerr != nil {
assert.True(t, tt.expectError, "got TLS handshake error while expecting success: %v", cerr)
assert.Equal(t, context.DeadlineExceeded, cerr, "expected %v with TLS handshake failure, got %v", context.DeadlineExceeded, cerr)
return
}

cli.Close()
})
}
}
47 changes: 47 additions & 0 deletions pkg/tlsutil/versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2023 The etcd 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 tlsutil

import (
"crypto/tls"
"fmt"
)

type TLSVersion string

// Constants for TLS versions.
const (
TLSVersionDefault TLSVersion = ""
TLSVersion12 TLSVersion = "TLS1.2"
TLSVersion13 TLSVersion = "TLS1.3"
)

// GetTLSVersion returns the corresponding tls.Version or error.
func GetTLSVersion(version string) (uint16, error) {
var v uint16

switch version {
case string(TLSVersionDefault):
v = 0 // 0 means let Go decide.
case string(TLSVersion12):
v = tls.VersionTLS12
case string(TLSVersion13):
v = tls.VersionTLS13
default:
return 0, fmt.Errorf("unexpected TLS version %q (must be one of: TLS1.2, TLS1.3)", version)
}

return v, nil
}
63 changes: 63 additions & 0 deletions pkg/tlsutil/versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2023 The etcd 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 tlsutil

import (
"crypto/tls"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetVersion(t *testing.T) {
tests := []struct {
name string
version string
want uint16
expectError bool
}{
{
name: "TLS1.2",
version: "TLS1.2",
want: tls.VersionTLS12,
},
{
name: "TLS1.3",
version: "TLS1.3",
want: tls.VersionTLS13,
},
{
name: "Empty version",
version: "",
want: 0,
},
{
name: "Converting invalid version string to TLS version",
version: "not_existing",
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetTLSVersion(tt.version)
if err != nil {
assert.True(t, tt.expectError, "GetTLSVersion() returned error while expecting success: %v", err)
return
}
assert.Equal(t, tt.want, got)
})
}
}
Loading

0 comments on commit 164a987

Please sign in to comment.