Merge pull request #15156 from Nordix/add-minmax-allow-tls13

Add TLSv1.3 support.
This commit is contained in:
Marek Siarkowicz 2023-01-31 10:11:38 +01:00 committed by GitHub
commit f4cc163190
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 351 additions and 11 deletions

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -166,6 +166,14 @@ type TLSInfo struct {
// Note that cipher suites are prioritized in the given order.
CipherSuites []uint16
// MinVersion is the minimum TLS version that is acceptable.
// If not set, the minimum version is TLS 1.2.
MinVersion uint16
// MaxVersion is the maximum TLS version that is acceptable.
// If not set, the default used by Go is selected (see tls.Config.MaxVersion).
MaxVersion uint16
selfCert bool
// parseFunc exists to simplify testing. Typically, parseFunc
@ -380,8 +388,17 @@ func (info TLSInfo) baseConfig() (*tls.Config, error) {
}
}
var minVersion uint16
if info.MinVersion != 0 {
minVersion = info.MinVersion
} else {
// Default minimum version is TLS 1.2, previous versions are insecure and deprecated.
minVersion = tls.VersionTLS12
}
cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
MinVersion: minVersion,
MaxVersion: info.MaxVersion,
ServerName: info.ServerName,
}
@ -512,11 +529,6 @@ func (info TLSInfo) ServerConfig() (*tls.Config, error) {
// "h2" NextProtos is necessary for enabling HTTP2 for go's HTTP server
cfg.NextProtos = []string{"h2"}
// go1.13 enables TLS 1.3 by default
// and in TLS 1.3, cipher suites are not configurable
// setting Max TLS version to TLS 1.2 for go 1.13
cfg.MaxVersion = tls.VersionTLS12
return cfg, nil
}
@ -571,11 +583,6 @@ func (info TLSInfo) ClientConfig() (*tls.Config, error) {
}
}
// go1.13 enables TLS 1.3 by default
// and in TLS 1.3, cipher suites are not configurable
// setting Max TLS version to TLS 1.2 for go 1.13
cfg.MaxVersion = tls.VersionTLS12
return cfg, nil
}

View File

@ -15,6 +15,7 @@
package embed
import (
"crypto/tls"
"errors"
"fmt"
"math"
@ -226,6 +227,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"`
@ -660,6 +666,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 {
@ -776,6 +793,25 @@ func (cfg *Config) Validate() error {
zap.String("name", cfg.Name))
}
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
}

View File

@ -15,6 +15,7 @@
package embed
import (
"crypto/tls"
"errors"
"fmt"
"net"
@ -429,3 +430,80 @@ func TestLogRotation(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)
})
}
}

View File

@ -499,6 +499,7 @@ func configurePeerListeners(cfg *Config) (peers []*peerListener, err error) {
if err = cfg.PeerSelfCert(); err != nil {
cfg.logger.Fatal("failed to get peer self-signed certs", zap.Error(err))
}
updateMinMaxVersions(&cfg.PeerTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)
if !cfg.PeerTLSInfo.Empty() {
cfg.logger.Info(
"starting with peer TLS",
@ -611,6 +612,7 @@ func configureClientListeners(cfg *Config) (sctxs map[string]*serveCtx, err erro
if err = cfg.ClientSelfCert(); err != nil {
cfg.logger.Fatal("failed to get client self-signed certs", zap.Error(err))
}
updateMinMaxVersions(&cfg.ClientTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)
if cfg.EnablePprof {
cfg.logger.Info("pprof is enabled", zap.String("path", debugutil.HTTPPrefixPProf))
}

View File

@ -26,6 +26,7 @@ import (
"go.etcd.io/etcd/api/v3/version"
"go.etcd.io/etcd/client/pkg/v3/logutil"
"go.etcd.io/etcd/client/pkg/v3/tlsutil"
"go.etcd.io/etcd/pkg/v3/flags"
cconfig "go.etcd.io/etcd/server/v3/config"
"go.etcd.io/etcd/server/v3/embed"
@ -215,6 +216,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("*", "*"),

View File

@ -199,6 +199,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'

View File

@ -22,6 +22,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
@ -424,3 +425,32 @@ func TestEtcdHealthyWithTinySnapshotCatchupEntries(t *testing.T) {
}
require.NoError(t, g.Wait())
}
func TestEtcdTLSVersion(t *testing.T) {
e2e.SkipInShortMode(t)
d := t.TempDir()
proc, err := e2e.SpawnCmd(
[]string{
e2e.BinPath.Etcd,
"--data-dir", d,
"--name", "e1",
"--listen-client-urls", "https://0.0.0.0:0",
"--advertise-client-urls", "https://0.0.0.0:0",
"--listen-peer-urls", fmt.Sprintf("https://127.0.0.1:%d", e2e.EtcdProcessBasePort),
"--initial-advertise-peer-urls", fmt.Sprintf("https://127.0.0.1:%d", e2e.EtcdProcessBasePort),
"--initial-cluster", fmt.Sprintf("e1=https://127.0.0.1:%d", e2e.EtcdProcessBasePort),
"--peer-cert-file", e2e.CertPath,
"--peer-key-file", e2e.PrivateKeyPath,
"--cert-file", e2e.CertPath2,
"--key-file", e2e.PrivateKeyPath2,
"--tls-min-version", "TLS1.2",
"--tls-max-version", "TLS1.3",
}, nil,
)
assert.NoError(t, err)
assert.NoError(t, e2e.WaitReadyExpectProc(context.TODO(), proc, e2e.EtcdServerReadyLines), "did not receive expected output from etcd process")
assert.NoError(t, proc.Stop())
}

View File

@ -20,6 +20,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
clientv3 "go.etcd.io/etcd/client/v3"
@ -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 := integration.NewCluster(t, &integration.ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)
@ -72,3 +79,66 @@ func testTLSCipherSuites(t *testing.T, valid bool) {
t.Fatalf("expected TLS handshake success, got %v", cerr)
}
}
func TestTLSMinMaxVersion(t *testing.T) {
integration.BeforeTest(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 := integration.TestTLSInfo
srvTLS.MinVersion = tls.VersionTLS13
srvTLS.MaxVersion = tls.VersionTLS13
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cc, err := integration.TestTLSInfo.ClientConfig()
assert.NoError(t, err)
cc.MinVersion = tt.minVersion
cc.MaxVersion = tt.maxVersion
cli, cerr := integration.NewClient(t, clientv3.Config{
Endpoints: []string{clus.Members[0].GRPCURL()},
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()
})
}
}