Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

embedetcd: Allow passing in a *tls.Config object for the embedded ETCD Server #17130

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
etcdembed: refactored updateMinMaxVersions and updateCipherSuites to not
only allow transport.TLSInfo but also tls.Config

Fields CustomClientTLSConfig and CustomPeerTLSConfig added to
embed.Config to allow for the injection of programmatically generated
TLS configurations. Therefore, the above mentioned functions were
refactored to allow the usage of both. Also, two unit tests were being
added to config_test.go.

Signed-off-by: Raphael Skuza <rapsku.dev@gmail.com>
  • Loading branch information
RaphSku committed Jul 27, 2024
commit fba1b2709abde63ba2c0ac1c67e1f0307f7a5ad5
57 changes: 45 additions & 12 deletions server/embed/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,15 @@ type Config struct {
AdvertisePeerUrls, AdvertiseClientUrls []url.URL
//revive:enable:var-naming

ClientTLSInfo transport.TLSInfo
ClientAutoTLS bool
PeerTLSInfo transport.TLSInfo
PeerAutoTLS bool
// User can either specify CustomTLSConfig or ClientTLSInfo. CustomTLSConfig is especially useful
// when you want to programmatically inject certificates instead of referencing file
// paths to certificates.
CustomClientTLSConfig tls.Config
ClientTLSInfo transport.TLSInfo
ClientAutoTLS bool
CustomPeerTLSConfig tls.Config
PeerTLSInfo transport.TLSInfo
PeerAutoTLS bool

// ExperimentalSetMemberLocalAddr enables using the first specified and
// non-loopback local address from initial-advertise-peer-urls as the local
Expand Down Expand Up @@ -464,6 +469,11 @@ type Config struct {
ServerFeatureGate featuregate.FeatureGate
}

// Interface for different TLS Configs
type tlsConfigConstraint interface {
*tls.Config | *transport.TLSInfo
}

// configYAML holds the config suitable for yaml parsing
type configYAML struct {
Config
Expand Down Expand Up @@ -897,27 +907,50 @@ func (cfg *configYAML) configFromFile(path string) error {
return cfg.Validate()
}

func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
if len(tls.CipherSuites) > 0 && len(ss) > 0 {
func updateCipherSuites[TLS tlsConfigConstraint](info TLS, ss []string) error {
transportInfo, ok := any(info).(*transport.TLSInfo)
if ok {
if len(transportInfo.CipherSuites) > 0 && len(ss) > 0 {
return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss)
}
if len(ss) > 0 {
cs, err := tlsutil.GetCipherSuites(ss)
if err != nil {
return err
}
transportInfo.CipherSuites = cs
}
return nil
}

tlsConfig, _ := any(info).(*tls.Config)
if len(tlsConfig.CipherSuites) > 0 && len(ss) > 0 {
return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss)
}
if len(ss) > 0 {
cs, err := tlsutil.GetCipherSuites(ss)
if err != nil {
return err
}
tls.CipherSuites = cs
tlsConfig.CipherSuites = cs
Fixed Show fixed Hide fixed
}
return nil
}

func updateMinMaxVersions(info *transport.TLSInfo, min, max string) {
func updateMinMaxVersions[TLS tlsConfigConstraint](info TLS, 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)
transportInfo, ok := any(info).(*transport.TLSInfo)
if ok {
var err error
if transportInfo.MinVersion, err = tlsutil.GetTLSVersion(min); err != nil {
panic(err)
}
return
}
if info.MaxVersion, err = tlsutil.GetTLSVersion(max); err != nil {

tlsConfig, _ := any(info).(*tls.Config)
var err error
if tlsConfig.MinVersion, err = tlsutil.GetTLSVersion(min); err != nil {
panic(err)
}
}
Expand Down
173 changes: 173 additions & 0 deletions server/embed/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@
package embed

import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"net/url"
"os"
Expand Down Expand Up @@ -743,3 +750,169 @@ func TestUndefinedAutoCompactionModeValidate(t *testing.T) {
err := cfg.Validate()
require.Error(t, err)
}

type Subject struct {
Organization []string
Country []string
Province []string
Locality []string
StreetAddress []string
PostalCode []string
}

func generateCACert(t *testing.T, subject *Subject) (*x509.Certificate, *rsa.PrivateKey) {
caCert := &x509.Certificate{
IsCA: true,
SerialNumber: big.NewInt(2023),
Subject: pkix.Name{
Organization: subject.Organization,
Country: subject.Country,
Province: subject.Province,
Locality: subject.Locality,
StreetAddress: subject.StreetAddress,
PostalCode: subject.PostalCode,
},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, 1),
BasicConstraintsValid: true,
}

caPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)
require.NoError(t, err)

caBytes, err := x509.CreateCertificate(rand.Reader, caCert, caCert, &caPrivateKey.PublicKey, caPrivateKey)
require.NoError(t, err)

caPEM := new(bytes.Buffer)
pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})

caPrivateKeyPEM := new(bytes.Buffer)
pem.Encode(caPrivateKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(caPrivateKey),
})

return caCert, caPrivateKey
}

func generateHostCertificateFromCA(t *testing.T, caCert *x509.Certificate, caPrivateKey *rsa.PrivateKey, subject *Subject) tls.Certificate {
tlsCertSpecification := &x509.Certificate{
IsCA: false,
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Company, Testing"},
Country: []string{"Test"},
Province: []string{"Test Province"},
Locality: []string{"Testing"},
StreetAddress: []string{"Test Street"},
PostalCode: []string{"01234"},
},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
SubjectKeyId: []byte{1, 2, 3, 4, 6},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
}

certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
require.NoError(t, err)

certBytes, err := x509.CreateCertificate(rand.Reader, tlsCertSpecification, caCert, &certPrivKey.PublicKey, caPrivateKey)
require.NoError(t, err)

certPEM := new(bytes.Buffer)
pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})

certPrivKeyPEM := new(bytes.Buffer)
pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})

serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
require.NoError(t, err)

return serverCert
}

func TestUpdateCipherSuite(t *testing.T) {
t.Parallel()

caSubject := &Subject{
Organization: []string{"Company, Testing"},
Country: []string{"Test"},
Province: []string{"Test Province"},
Locality: []string{"Testing"},
StreetAddress: []string{"Test Street"},
PostalCode: []string{"01234"},
}
caCert, caPrivateKey := generateCACert(t, caSubject)

serverSubject := &Subject{
Organization: []string{"Company, Testing"},
Country: []string{"Test"},
Province: []string{"Test Province"},
Locality: []string{"Testing"},
StreetAddress: []string{"Test Street"},
PostalCode: []string{"01234"},
}
serverCert := generateHostCertificateFromCA(t, caCert, caPrivateKey, serverSubject)

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{serverCert},
}

cfg := NewConfig()
cfg.CustomClientTLSConfig = *tlsConfig

expCipherSuites := []uint16([]uint16{0x2f})
err := updateCipherSuites(tlsConfig, []string{"TLS_RSA_WITH_AES_128_CBC_SHA"})
require.NoError(t, err)
assert.Equal(t, expCipherSuites, tlsConfig.CipherSuites)
}

func TestMinMaxVersion(t *testing.T) {
t.Parallel()

caSubject := &Subject{
Organization: []string{"Company, Testing"},
Country: []string{"Test"},
Province: []string{"Test Province"},
Locality: []string{"Testing"},
StreetAddress: []string{"Test Street"},
PostalCode: []string{"01234"},
}
caCert, caPrivateKey := generateCACert(t, caSubject)

serverSubject := &Subject{
Organization: []string{"Company, Testing"},
Country: []string{"Test"},
Province: []string{"Test Province"},
Locality: []string{"Testing"},
StreetAddress: []string{"Test Street"},
PostalCode: []string{"01234"},
}
serverCert := generateHostCertificateFromCA(t, caCert, caPrivateKey, serverSubject)

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{serverCert},
}

cfg := NewConfig()
cfg.CustomClientTLSConfig = *tlsConfig

expMinVersion := uint16(0x303)
expMaxVersion := uint16(0x0)
updateMinMaxVersions(tlsConfig, "TLS1.2", "TLS1.3")
assert.Equal(t, tlsConfig.MinVersion, expMinVersion)
assert.Equal(t, tlsConfig.MaxVersion, expMaxVersion)
}