From 7a2f2df0f619dcdcb40bcf7898c8b61b2202a3f7 Mon Sep 17 00:00:00 2001 From: David Hontecillas Date: Wed, 3 Jul 2024 12:06:47 +0200 Subject: [PATCH 1/4] add config field to serve tls using multiple key pairs Signed-off-by: David Hontecillas --- config/config.go | 29 ++++++++++++++++++----------- config/parser.go | 34 +++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/config/config.go b/config/config.go index 18afd9ae..c15e795e 100644 --- a/config/config.go +++ b/config/config.go @@ -303,19 +303,26 @@ type Plugin struct { Pattern string `mapstructure:"pattern"` } +// TLSKeyPair contains a pair of public and private keys +type TLSKeyPair struct { + PublicKey string `mapstructure:"public_key"` + PrivateKey string `mapstructure:"private_key"` +} + // TLS defines the configuration params for enabling TLS (HTTPS & HTTP/2) at the router layer type TLS struct { - IsDisabled bool `mapstructure:"disabled"` - PublicKey string `mapstructure:"public_key"` - PrivateKey string `mapstructure:"private_key"` - CaCerts []string `mapstructure:"ca_certs"` - MinVersion string `mapstructure:"min_version"` - MaxVersion string `mapstructure:"max_version"` - CurvePreferences []uint16 `mapstructure:"curve_preferences"` - PreferServerCipherSuites bool `mapstructure:"prefer_server_cipher_suites"` - CipherSuites []uint16 `mapstructure:"cipher_suites"` - EnableMTLS bool `mapstructure:"enable_mtls"` - DisableSystemCaPool bool `mapstructure:"disable_system_ca_pool"` + IsDisabled bool `mapstructure:"disabled"` + PublicKey string `mapstructure:"public_key"` + PrivateKey string `mapstructure:"private_key"` + CaCerts []string `mapstructure:"ca_certs"` + MinVersion string `mapstructure:"min_version"` + MaxVersion string `mapstructure:"max_version"` + CurvePreferences []uint16 `mapstructure:"curve_preferences"` + PreferServerCipherSuites bool `mapstructure:"prefer_server_cipher_suites"` + CipherSuites []uint16 `mapstructure:"cipher_suites"` + EnableMTLS bool `mapstructure:"enable_mtls"` + DisableSystemCaPool bool `mapstructure:"disable_system_ca_pool"` + Keys []TLSKeyPair `mapstructure:"keys"` } // ClientTLS defines the configuration params for an HTTP Client diff --git a/config/parser.go b/config/parser.go index 5b35e854..3c595919 100644 --- a/config/parser.go +++ b/config/parser.go @@ -212,6 +212,12 @@ func (p *parseableServiceConfig) normalize() ServiceConfig { EnableMTLS: p.TLS.EnableMTLS, DisableSystemCaPool: p.TLS.DisableSystemCaPool, } + for _, k := range p.TLS.Keys { + cfg.TLS.Keys = append(cfg.TLS.Keys, TLSKeyPair{ + PublicKey: k.PublicKey, + PrivateKey: k.PrivateKey, + }) + } } if p.ClientTLS != nil { cfg.ClientTLS = &ClientTLS{ @@ -244,18 +250,24 @@ func (p *parseableServiceConfig) normalize() ServiceConfig { return cfg } +type parseableTLSKeyPair struct { + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` +} + type parseableTLS struct { - IsDisabled bool `json:"disabled"` - PublicKey string `json:"public_key"` - PrivateKey string `json:"private_key"` - CaCerts []string `json:"ca_certs"` - MinVersion string `json:"min_version"` - MaxVersion string `json:"max_version"` - CurvePreferences []uint16 `json:"curve_preferences"` - PreferServerCipherSuites bool `json:"prefer_server_cipher_suites"` - CipherSuites []uint16 `json:"cipher_suites"` - EnableMTLS bool `json:"enable_mtls"` - DisableSystemCaPool bool `json:"disable_system_ca_pool"` + IsDisabled bool `json:"disabled"` + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` + CaCerts []string `json:"ca_certs"` + MinVersion string `json:"min_version"` + MaxVersion string `json:"max_version"` + CurvePreferences []uint16 `json:"curve_preferences"` + PreferServerCipherSuites bool `json:"prefer_server_cipher_suites"` + CipherSuites []uint16 `json:"cipher_suites"` + EnableMTLS bool `json:"enable_mtls"` + DisableSystemCaPool bool `json:"disable_system_ca_pool"` + Keys []parseableTLSKeyPair `json:"keys"` } type parseableClientTLS struct { From 55406617e2a54eeb019862c2401ebe056bc955ac Mon Sep 17 00:00:00 2001 From: David Hontecillas Date: Wed, 3 Jul 2024 18:36:35 +0200 Subject: [PATCH 2/4] use list of certificates for serving TLS Signed-off-by: David Hontecillas --- transport/http/server/server.go | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/transport/http/server/server.go b/transport/http/server/server.go index 5d4389a4..9d628125 100644 --- a/transport/http/server/server.go +++ b/transport/http/server/server.go @@ -116,14 +116,33 @@ func RunServerWithLoggerFactory(l logging.Logger) func(context.Context, config.S done <- s.ListenAndServe() }() } else { - if cfg.TLS.PublicKey == "" { + if len(cfg.TLS.PublicKey) > 0 || len(cfg.TLS.PrivateKey) > 0 { + cfg.TLS.Keys = append(cfg.TLS.Keys, config.TLSKeyPair{ + PublicKey: cfg.TLS.PublicKey, + PrivateKey: cfg.TLS.PrivateKey, + }) + } + if len(cfg.TLS.Keys) == 0 { return ErrPublicKey } - if cfg.TLS.PrivateKey == "" { - return ErrPrivateKey + for _, k := range cfg.TLS.Keys { + if k.PublicKey == "" { + return ErrPublicKey + } + if k.PrivateKey == "" { + return ErrPrivateKey + } + cert, err := tls.LoadX509KeyPair(k.PublicKey, k.PrivateKey) + if err != nil { + return err + } + s.TLSConfig.Certificates = append(s.TLSConfig.Certificates, cert) } + go func() { - done <- s.ListenAndServeTLS(cfg.TLS.PublicKey, cfg.TLS.PrivateKey) + // since we already use the list of certificates in the config + // we do not need to specify the files for public and private key here + done <- s.ListenAndServeTLS("", "") }() } From 1bbf4c56c51ec2251d78fa55e730d5e251583c8e Mon Sep 17 00:00:00 2001 From: David Hontecillas Date: Wed, 3 Jul 2024 23:06:46 +0200 Subject: [PATCH 3/4] test serving multiple certificates Signed-off-by: David Hontecillas --- config/parser.go | 5 +- transport/http/server/server_test.go | 102 +++++++++++++++++++++++++++ transport/http/server/tls_test.go | 58 ++++++++++----- 3 files changed, 145 insertions(+), 20 deletions(-) diff --git a/config/parser.go b/config/parser.go index 3c595919..da78f243 100644 --- a/config/parser.go +++ b/config/parser.go @@ -213,10 +213,7 @@ func (p *parseableServiceConfig) normalize() ServiceConfig { DisableSystemCaPool: p.TLS.DisableSystemCaPool, } for _, k := range p.TLS.Keys { - cfg.TLS.Keys = append(cfg.TLS.Keys, TLSKeyPair{ - PublicKey: k.PublicKey, - PrivateKey: k.PrivateKey, - }) + cfg.TLS.Keys = append(cfg.TLS.Keys, TLSKeyPair(k)) } } if p.ClientTLS != nil { diff --git a/transport/http/server/server_test.go b/transport/http/server/server_test.go index 90401889..d86f8e92 100644 --- a/transport/http/server/server_test.go +++ b/transport/http/server/server_test.go @@ -14,6 +14,7 @@ import ( "net" "net/http" "os" + "strings" "testing" "time" @@ -470,3 +471,104 @@ func h2cClient() *http.Client { func newPort() int { return 16666 + rand.Intn(40000) } + +func TestRunServer_MultipleTLS(t *testing.T) { + testKeysAreAvailable(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + port := newPort() + + done := make(chan error) + go func() { + done <- RunServer( + ctx, + config.ServiceConfig{ + Port: port, + TLS: &config.TLS{ + CaCerts: []string{"ca.pem", "exampleca.pem"}, + Keys: []config.TLSKeyPair{ + config.TLSKeyPair{ + PublicKey: "cert.pem", + PrivateKey: "key.pem", + }, + config.TLSKeyPair{ + PublicKey: "examplecert.pem", + PrivateKey: "examplekey.pem", + }, + }, + }, + }, + http.HandlerFunc(dummyHandler), + ) + }() + + client, err := httpsClient("cert.pem") + if err != nil { + t.Error(err) + return + } + + <-time.After(100 * time.Millisecond) + + resp, err := client.Get(fmt.Sprintf("https://localhost:%d", port)) + if err != nil { + t.Error(err) + return + } + if resp.StatusCode != 200 { + t.Errorf("unexpected status code: %d", resp.StatusCode) + return + } + + client, err = httpsClient("examplecert.pem") + if err != nil { + t.Error(err) + return + } + resp, err = client.Get(fmt.Sprintf("https://127.0.0.1:%d", port)) + // should fail, because it will be served with cert.pem + if err == nil || strings.Contains(err.Error(), "bad certificate") { + t.Error("expected to have 'bad certificate' error") + return + } + + req, _ := http.NewRequest("GET", fmt.Sprintf("https://example.com:%d", port), nil) + OverrideHostTransport(client) + resp, err = client.Do(req) + if err != nil { + t.Error(err) + return + } + + cancel() + if err = <-done; err != nil { + t.Error(err) + } +} + +// OverrideHostTransport subtitutes the actual address that the request will +// connecto (overriding the dns resolution). +func OverrideHostTransport(client *http.Client) { + t := http.DefaultTransport.(*http.Transport).Clone() + if client.Transport != nil { + if tt, ok := client.Transport.(*http.Transport); ok { + t = tt + } + } + myDialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + } + t.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { + _, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + overrideAddress := net.JoinHostPort("127.0.0.1", port) + return myDialer.DialContext(ctx, network, overrideAddress) + } + client.Transport = t +} diff --git a/transport/http/server/tls_test.go b/transport/http/server/tls_test.go index ce4961de..d40f954c 100644 --- a/transport/http/server/tls_test.go +++ b/transport/http/server/tls_test.go @@ -16,15 +16,41 @@ import ( "time" ) -func init() { - if err := generateCerts(); err != nil { - log.Fatal(err.Error()) +type certDef struct { + Prefix string + IPAddresses []string + DNSNames []string +} + +func (c certDef) Org() string { + if len(c.Prefix) == 0 { + return "Acme Co" } + return c.Prefix + " " + "Acme Co" } -func generateCerts() error { - hosts := []string{"127.0.0.1", "::1", "localhost"} +func init() { + certs := []certDef{ + certDef{ + Prefix: "", + IPAddresses: []string{"127.0.0.1", "::1"}, + DNSNames: []string{"localhost"}, + }, + certDef{ + Prefix: "example", + IPAddresses: []string{"127.0.0.1"}, + DNSNames: []string{"example.com"}, + }, + } + for _, cd := range certs { + if err := generateNamedCert(cd); err != nil { + log.Fatal(err.Error()) + } + } +} + +func generateNamedCert(hostCert certDef) error { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return fmt.Errorf("Failed to generate private key: %v", err) @@ -44,23 +70,23 @@ func generateCerts() error { template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ - Organization: []string{"Acme Co"}, + Organization: []string{hostCert.Org()}, }, - NotBefore: notBefore, - NotAfter: notAfter, - + NotBefore: notBefore, + NotAfter: notAfter, KeyUsage: keyUsage, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } - for _, h := range hosts { - if ip := net.ParseIP(h); ip != nil { + for _, strIP := range hostCert.IPAddresses { + if ip := net.ParseIP(strIP); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) - } else { - template.DNSNames = append(template.DNSNames, h) } } + for _, dname := range hostCert.DNSNames { + template.DNSNames = append(template.DNSNames, dname) + } template.IsCA = true template.KeyUsage |= x509.KeyUsageCertSign @@ -75,9 +101,9 @@ func generateCerts() error { return fmt.Errorf("Failed to create ca: %v", err) } - serverCert := "cert.pem" - serverKey := "key.pem" - caCert := "ca.pem" + serverCert := hostCert.Prefix + "cert.pem" + serverKey := hostCert.Prefix + "key.pem" + caCert := hostCert.Prefix + "ca.pem" certOut, err := os.Create(serverCert) if err != nil { From fc8ed5ca76b0938fb8029f9de9b20903a6466fd6 Mon Sep 17 00:00:00 2001 From: David Hontecillas Date: Thu, 4 Jul 2024 11:33:13 +0200 Subject: [PATCH 4/4] fix style issues in certs tests Signed-off-by: David Hontecillas --- transport/http/server/server_test.go | 14 +++++++++----- transport/http/server/tls_test.go | 6 ++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/transport/http/server/server_test.go b/transport/http/server/server_test.go index d86f8e92..20c5f0f3 100644 --- a/transport/http/server/server_test.go +++ b/transport/http/server/server_test.go @@ -527,20 +527,24 @@ func TestRunServer_MultipleTLS(t *testing.T) { t.Error(err) return } - resp, err = client.Get(fmt.Sprintf("https://127.0.0.1:%d", port)) + _, err = client.Get(fmt.Sprintf("https://127.0.0.1:%d", port)) // should fail, because it will be served with cert.pem if err == nil || strings.Contains(err.Error(), "bad certificate") { t.Error("expected to have 'bad certificate' error") return } - req, _ := http.NewRequest("GET", fmt.Sprintf("https://example.com:%d", port), nil) - OverrideHostTransport(client) + req, _ := http.NewRequest("GET", fmt.Sprintf("https://example.com:%d", port), http.NoBody) + overrideHostTransport(client) resp, err = client.Do(req) if err != nil { t.Error(err) return } + if resp.StatusCode != 200 { + t.Errorf("unexpected status code: %d", resp.StatusCode) + return + } cancel() if err = <-done; err != nil { @@ -548,9 +552,9 @@ func TestRunServer_MultipleTLS(t *testing.T) { } } -// OverrideHostTransport subtitutes the actual address that the request will +// overrideHostTransport subtitutes the actual address that the request will // connecto (overriding the dns resolution). -func OverrideHostTransport(client *http.Client) { +func overrideHostTransport(client *http.Client) { t := http.DefaultTransport.(*http.Transport).Clone() if client.Transport != nil { if tt, ok := client.Transport.(*http.Transport); ok { diff --git a/transport/http/server/tls_test.go b/transport/http/server/tls_test.go index d40f954c..6fb7c393 100644 --- a/transport/http/server/tls_test.go +++ b/transport/http/server/tls_test.go @@ -23,7 +23,7 @@ type certDef struct { } func (c certDef) Org() string { - if len(c.Prefix) == 0 { + if c.Prefix == "" { return "Acme Co" } return c.Prefix + " " + "Acme Co" @@ -84,9 +84,7 @@ func generateNamedCert(hostCert certDef) error { template.IPAddresses = append(template.IPAddresses, ip) } } - for _, dname := range hostCert.DNSNames { - template.DNSNames = append(template.DNSNames, dname) - } + template.DNSNames = append(template.DNSNames, hostCert.DNSNames...) template.IsCA = true template.KeyUsage |= x509.KeyUsageCertSign