// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package tlsutil import ( "crypto/tls" "crypto/x509" "fmt" "io" "net" "os" "strings" "testing" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/yamux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( // See README.md for documentation cacert = "./testdata/nomad-agent-ca.pem" fooclientcert = "./testdata/regionFoo-client-nomad.pem" fooclientkey = "./testdata/regionFoo-client-nomad-key.pem" fooservercert = "./testdata/regionFoo-server-nomad.pem" fooserverkey = "./testdata/regionFoo-server-nomad-key.pem" badcert = "./testdata/badRegion-client-bad.pem" badkey = "./testdata/badRegion-client-bad-key.pem" ) func TestConfig_AppendCA_None(t *testing.T) { ci.Parallel(t) require := require.New(t) conf := &Config{} pool := x509.NewCertPool() err := conf.AppendCA(pool) require.Nil(err) } func TestConfig_AppendCA_Valid(t *testing.T) { ci.Parallel(t) require := require.New(t) conf := &Config{ CAFile: cacert, } pool := x509.NewCertPool() err := conf.AppendCA(pool) require.Nil(err) } func TestConfig_AppendCA_Valid_MultipleCerts(t *testing.T) { ci.Parallel(t) require := require.New(t) certs := ` -----BEGIN CERTIFICATE----- MIICMzCCAdqgAwIBAgIUNZ9L86Xp9EuDH0/qyAesh599LXQwCgYIKoZIzj0EAwIw eDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh biBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMFTm9tYWQx GDAWBgNVBAMTD25vbWFkLmhhc2hpY29ycDAeFw0xNjExMTAxOTQ4MDBaFw0yMTEx MDkxOTQ4MDBaMHgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw FAYDVQQHEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxDjAMBgNV BAsTBU5vbWFkMRgwFgYDVQQDEw9ub21hZC5oYXNoaWNvcnAwWTATBgcqhkjOPQIB BggqhkjOPQMBBwNCAARfJmTdHzYIMPD8SK+kj5Gc79fmpOcg6wnb4JNVwCqWw9O+ uNdZJZWSi4Q/4HojM5FTSBqYxNgSrmY/o3oQrCPlo0IwQDAOBgNVHQ8BAf8EBAMC AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOjVq/BectnhcKn6EHUD4NJFm /UAwCgYIKoZIzj0EAwIDRwAwRAIgTemDJGSGtcQPXLWKiQNw4SKO9wAPhn/WoKW4 Ln2ZUe8CIDsQswBQS7URbqnKYDye2Y4befJkr4fmhhmMQb2ex9A4 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICNTCCAZagAwIBAgIRANjgoh5iVZI26+Hz/K65G0UwCgYIKoZIzj0EAwQwNjEb MBkGA1UEChMSSGFzaGlDb3JwIFRyYWluaW5nMRcwFQYDVQQDEw5zZXJ2aWNlLmNv bnN1bDAeFw0xODA4MjMxNzM0NTBaFw0xODA5MjIxNzM0NTBaMDYxGzAZBgNVBAoT Ekhhc2hpQ29ycCBUcmFpbmluZzEXMBUGA1UEAxMOc2VydmljZS5jb25zdWwwgZsw EAYHKoZIzj0CAQYFK4EEACMDgYYABAGjC4sWsOfirS/DQ9/e7PdQeJwlOjziiOx/ CALjS6ryEDkZPqRqMuoFXfudAmfdk6tl8AT1IKMVcgiQU5jkm7fliwFIk48uh+n2 obqZjwDyM76VYBVSYi6i3BPXown1ivIMJNQS1txnWZLZHsv+WxbHydS+GNOAwKDK KsXj9dEhd36pvaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w HQYDVR0OBBYEFIk3oG2hu0FxueW4e7fL+FdMOquBMAoGCCqGSM49BAMEA4GMADCB iAJCAPIPwPyk+8Ymj7Zlvb5qIUQg+UxoacAeJtFZrJ8xQjro0YjsM33O86rAfw+x sWWGul4Ews93KFBXvhbKCwb0F0PhAkIAh2z7COsKcQzvBoIy+Kx92+9j/sUjlzzl TttDu+g2VdbcBwVDZ49X2Md6OY2N3G8Irdlj+n+mCQJaHwVt52DRzz0= -----END CERTIFICATE----- ` tmpCAFile, err := os.CreateTemp("/tmp", "test_ca_file") require.NoError(err) defer os.Remove(tmpCAFile.Name()) _, err = tmpCAFile.Write([]byte(certs)) require.NoError(err) tmpCAFile.Close() conf := &Config{ CAFile: tmpCAFile.Name(), } pool := x509.NewCertPool() require.NoError(conf.AppendCA(pool)) require.Len(pool.Subjects(), 2) } // TestConfig_AppendCA_Valid_Whitespace asserts that a PEM file containing // trailing whitespace is valid. func TestConfig_AppendCA_Valid_Whitespace(t *testing.T) { ci.Parallel(t) require := require.New(t) const cacertWhitespace = "./testdata/whitespace-agent-ca.pem" conf := &Config{ CAFile: cacertWhitespace, } pool := x509.NewCertPool() require.NoError(conf.AppendCA(pool)) require.Len(pool.Subjects(), 1) } // TestConfig_AppendCA_Invalid_MultipleCerts_Whitespace asserts that a PEM file // containing non-PEM data between certificate blocks is still valid. func TestConfig_AppendCA_Valid_MultipleCerts_ExtraData(t *testing.T) { ci.Parallel(t) require := require.New(t) certs := ` Did you know... -----BEGIN CERTIFICATE----- MIICMzCCAdqgAwIBAgIUNZ9L86Xp9EuDH0/qyAesh599LXQwCgYIKoZIzj0EAwIw eDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh biBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMFTm9tYWQx GDAWBgNVBAMTD25vbWFkLmhhc2hpY29ycDAeFw0xNjExMTAxOTQ4MDBaFw0yMTEx MDkxOTQ4MDBaMHgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw FAYDVQQHEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxDjAMBgNV BAsTBU5vbWFkMRgwFgYDVQQDEw9ub21hZC5oYXNoaWNvcnAwWTATBgcqhkjOPQIB BggqhkjOPQMBBwNCAARfJmTdHzYIMPD8SK+kj5Gc79fmpOcg6wnb4JNVwCqWw9O+ uNdZJZWSi4Q/4HojM5FTSBqYxNgSrmY/o3oQrCPlo0IwQDAOBgNVHQ8BAf8EBAMC AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOjVq/BectnhcKn6EHUD4NJFm /UAwCgYIKoZIzj0EAwIDRwAwRAIgTemDJGSGtcQPXLWKiQNw4SKO9wAPhn/WoKW4 Ln2ZUe8CIDsQswBQS7URbqnKYDye2Y4befJkr4fmhhmMQb2ex9A4 -----END CERTIFICATE----- ...PEM parsers don't care about data... -----BEGIN CERTIFICATE----- MIICNTCCAZagAwIBAgIRANjgoh5iVZI26+Hz/K65G0UwCgYIKoZIzj0EAwQwNjEb MBkGA1UEChMSSGFzaGlDb3JwIFRyYWluaW5nMRcwFQYDVQQDEw5zZXJ2aWNlLmNv bnN1bDAeFw0xODA4MjMxNzM0NTBaFw0xODA5MjIxNzM0NTBaMDYxGzAZBgNVBAoT Ekhhc2hpQ29ycCBUcmFpbmluZzEXMBUGA1UEAxMOc2VydmljZS5jb25zdWwwgZsw EAYHKoZIzj0CAQYFK4EEACMDgYYABAGjC4sWsOfirS/DQ9/e7PdQeJwlOjziiOx/ CALjS6ryEDkZPqRqMuoFXfudAmfdk6tl8AT1IKMVcgiQU5jkm7fliwFIk48uh+n2 obqZjwDyM76VYBVSYi6i3BPXown1ivIMJNQS1txnWZLZHsv+WxbHydS+GNOAwKDK KsXj9dEhd36pvaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w HQYDVR0OBBYEFIk3oG2hu0FxueW4e7fL+FdMOquBMAoGCCqGSM49BAMEA4GMADCB iAJCAPIPwPyk+8Ymj7Zlvb5qIUQg+UxoacAeJtFZrJ8xQjro0YjsM33O86rAfw+x sWWGul4Ews93KFBXvhbKCwb0F0PhAkIAh2z7COsKcQzvBoIy+Kx92+9j/sUjlzzl TttDu+g2VdbcBwVDZ49X2Md6OY2N3G8Irdlj+n+mCQJaHwVt52DRzz0= -----END CERTIFICATE----- ...outside of -----XXX----- blocks? ` tmpCAFile, err := os.CreateTemp("/tmp", "test_ca_file_extra") require.NoError(err) defer os.Remove(tmpCAFile.Name()) _, err = tmpCAFile.Write([]byte(certs)) require.NoError(err) tmpCAFile.Close() conf := &Config{ CAFile: tmpCAFile.Name(), } pool := x509.NewCertPool() err = conf.AppendCA(pool) require.NoError(err) require.Len(pool.Subjects(), 2) } // TestConfig_AppendCA_Invalid_MultipleCerts asserts only the valid certificate // is returned. func TestConfig_AppendCA_Invalid_MultipleCerts(t *testing.T) { ci.Parallel(t) require := require.New(t) certs := ` -----BEGIN CERTIFICATE----- MIICMzCCAdqgAwIBAgIUNZ9L86Xp9EuDH0/qyAesh599LXQwCgYIKoZIzj0EAwIw eDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh biBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMFTm9tYWQx GDAWBgNVBAMTD25vbWFkLmhhc2hpY29ycDAeFw0xNjExMTAxOTQ4MDBaFw0yMTEx MDkxOTQ4MDBaMHgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw FAYDVQQHEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxDjAMBgNV BAsTBU5vbWFkMRgwFgYDVQQDEw9ub21hZC5oYXNoaWNvcnAwWTATBgcqhkjOPQIB BggqhkjOPQMBBwNCAARfJmTdHzYIMPD8SK+kj5Gc79fmpOcg6wnb4JNVwCqWw9O+ uNdZJZWSi4Q/4HojM5FTSBqYxNgSrmY/o3oQrCPlo0IwQDAOBgNVHQ8BAf8EBAMC AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOjVq/BectnhcKn6EHUD4NJFm /UAwCgYIKoZIzj0EAwIDRwAwRAIgTemDJGSGtcQPXLWKiQNw4SKO9wAPhn/WoKW4 Ln2ZUe8CIDsQswBQS7URbqnKYDye2Y4befJkr4fmhhmMQb2ex9A4 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- Invalid -----END CERTIFICATE-----` tmpCAFile, err := os.CreateTemp("/tmp", "test_ca_file") require.NoError(err) defer os.Remove(tmpCAFile.Name()) _, err = tmpCAFile.Write([]byte(certs)) require.NoError(err) tmpCAFile.Close() conf := &Config{ CAFile: tmpCAFile.Name(), } pool := x509.NewCertPool() require.NoError(conf.AppendCA(pool)) require.Len(pool.Subjects(), 1) } func TestConfig_AppendCA_Invalid(t *testing.T) { ci.Parallel(t) require := require.New(t) { conf := &Config{ CAFile: "invalidFile", } pool := x509.NewCertPool() err := conf.AppendCA(pool) require.NotNil(err) require.Contains(err.Error(), "Failed to read CA file") require.Equal(len(pool.Subjects()), 0) } { tmpFile, err := os.CreateTemp("/tmp", "test_ca_file") require.Nil(err) defer os.Remove(tmpFile.Name()) _, err = tmpFile.Write([]byte("Invalid CA Content!")) require.Nil(err) conf := &Config{ CAFile: tmpFile.Name(), } pool := x509.NewCertPool() err = conf.AppendCA(pool) require.Error(err) require.Contains(err.Error(), "Failed to parse any valid certificates in CA file:") require.Equal(len(pool.Subjects()), 0) } } func TestConfig_CACertificate_Valid(t *testing.T) { ci.Parallel(t) conf := &Config{ CAFile: cacert, } pool := x509.NewCertPool() err := conf.AppendCA(pool) if err != nil { t.Fatalf("err: %v", err) } if len(pool.Subjects()) == 0 { t.Fatalf("expected cert") } } func TestConfig_LoadKeyPair_None(t *testing.T) { ci.Parallel(t) conf := &Config{ KeyLoader: &config.KeyLoader{}, } cert, err := conf.LoadKeyPair() if err != nil { t.Fatalf("err: %v", err) } if cert != nil { t.Fatalf("bad: %v", cert) } } func TestConfig_LoadKeyPair_Valid(t *testing.T) { ci.Parallel(t) conf := &Config{ CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } cert, err := conf.LoadKeyPair() if err != nil { t.Fatalf("err: %v", err) } if cert == nil { t.Fatalf("expected cert") } } func TestConfig_OutgoingTLS_MissingCA(t *testing.T) { ci.Parallel(t) conf := &Config{ VerifyOutgoing: true, } tls, err := conf.OutgoingTLSConfig() if err == nil { t.Fatalf("expected err") } if tls != nil { t.Fatalf("bad: %v", tls) } } func TestConfig_OutgoingTLS_OnlyCA(t *testing.T) { ci.Parallel(t) conf := &Config{ CAFile: cacert, } tls, err := conf.OutgoingTLSConfig() if err != nil { t.Fatalf("err: %v", err) } if tls != nil { t.Fatalf("expected no config") } } func TestConfig_OutgoingTLS_VerifyOutgoing(t *testing.T) { ci.Parallel(t) conf := &Config{ VerifyOutgoing: true, CAFile: cacert, } tls, err := conf.OutgoingTLSConfig() if err != nil { t.Fatalf("err: %v", err) } if tls == nil { t.Fatalf("expected config") } if len(tls.RootCAs.Subjects()) != 1 { t.Fatalf("expect root cert") } if !tls.InsecureSkipVerify { t.Fatalf("should skip built-in verification") } } func TestConfig_OutgoingTLS_VerifyHostname(t *testing.T) { ci.Parallel(t) conf := &Config{ VerifyServerHostname: true, CAFile: cacert, } tls, err := conf.OutgoingTLSConfig() if err != nil { t.Fatalf("err: %v", err) } if tls == nil { t.Fatalf("expected config") } if len(tls.RootCAs.Subjects()) != 1 { t.Fatalf("expect root cert") } if tls.InsecureSkipVerify { t.Fatalf("should not skip built-in verification") } } func TestConfig_OutgoingTLS_WithKeyPair(t *testing.T) { ci.Parallel(t) assert := assert.New(t) conf := &Config{ VerifyOutgoing: true, CAFile: cacert, CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } tlsConf, err := conf.OutgoingTLSConfig() assert.Nil(err) assert.NotNil(tlsConf) assert.Equal(len(tlsConf.RootCAs.Subjects()), 1) assert.True(tlsConf.InsecureSkipVerify) clientHelloInfo := &tls.ClientHelloInfo{} cert, err := tlsConf.GetCertificate(clientHelloInfo) assert.Nil(err) assert.NotNil(cert) } func TestConfig_OutgoingTLS_PreferServerCipherSuites(t *testing.T) { ci.Parallel(t) require := require.New(t) { conf := &Config{ VerifyOutgoing: true, CAFile: cacert, } tlsConfig, err := conf.OutgoingTLSConfig() require.Nil(err) require.Equal(tlsConfig.PreferServerCipherSuites, false) } { conf := &Config{ VerifyOutgoing: true, CAFile: cacert, PreferServerCipherSuites: true, } tlsConfig, err := conf.OutgoingTLSConfig() require.Nil(err) require.Equal(tlsConfig.PreferServerCipherSuites, true) } } func TestConfig_OutgoingTLS_TLSCipherSuites(t *testing.T) { ci.Parallel(t) require := require.New(t) { defaultCiphers := []uint16{ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, } conf := &Config{ VerifyOutgoing: true, CAFile: cacert, CipherSuites: defaultCiphers, } tlsConfig, err := conf.OutgoingTLSConfig() require.Nil(err) require.Equal(tlsConfig.CipherSuites, defaultCiphers) } { conf := &Config{ VerifyOutgoing: true, CAFile: cacert, CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}, } tlsConfig, err := conf.OutgoingTLSConfig() require.Nil(err) require.Equal(tlsConfig.CipherSuites, []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}) } } func startTLSServer(config *Config) (net.Conn, chan error) { errc := make(chan error, 1) tlsConfigServer, err := config.IncomingTLSConfig() if err != nil { errc <- err return nil, errc } client, server := net.Pipe() // Use yamux to buffer the reads, otherwise it's easy to deadlock muxConf := yamux.DefaultConfig() serverSession, _ := yamux.Server(server, muxConf) clientSession, _ := yamux.Client(client, muxConf) clientConn, _ := clientSession.Open() serverConn, _ := serverSession.Accept() go func() { tlsServer := tls.Server(serverConn, tlsConfigServer) if err := tlsServer.Handshake(); err != nil { errc <- err } close(errc) // Because net.Pipe() is unbuffered, if both sides // Close() simultaneously, we will deadlock as they // both send an alert and then block. So we make the // server read any data from the client until error or // EOF, which will allow the client to Close(), and // *then* we Close() the server. io.Copy(io.Discard, tlsServer) tlsServer.Close() }() return clientConn, errc } // TODO sign the certificates for "server.regionFoo.nomad func TestConfig_outgoingWrapper_OK(t *testing.T) { ci.Parallel(t) config := &Config{ CAFile: cacert, CertFile: fooservercert, KeyFile: fooserverkey, VerifyServerHostname: true, VerifyOutgoing: true, KeyLoader: &config.KeyLoader{}, } client, errc := startTLSServer(config) if client == nil { t.Fatalf("startTLSServer err: %v", <-errc) } wrap, err := config.OutgoingTLSWrapper() if err != nil { t.Fatalf("OutgoingTLSWrapper err: %v", err) } tlsClient, err := wrap("regionFoo", client) if err != nil { t.Fatalf("wrapTLS err: %v", err) } defer tlsClient.Close() if err := tlsClient.(*tls.Conn).Handshake(); err != nil { t.Fatalf("write err: %v", err) } err = <-errc if err != nil { t.Fatalf("server: %v", err) } } func TestConfig_outgoingWrapper_BadCert(t *testing.T) { ci.Parallel(t) // TODO this test is currently hanging, need to investigate more. t.SkipNow() config := &Config{ CAFile: cacert, CertFile: fooclientcert, KeyFile: fooclientkey, VerifyServerHostname: true, VerifyOutgoing: true, } client, errc := startTLSServer(config) if client == nil { t.Fatalf("startTLSServer err: %v", <-errc) } wrap, err := config.OutgoingTLSWrapper() if err != nil { t.Fatalf("OutgoingTLSWrapper err: %v", err) } tlsClient, err := wrap("regionFoo", client) if err != nil { t.Fatalf("wrapTLS err: %v", err) } defer tlsClient.Close() err = tlsClient.(*tls.Conn).Handshake() if _, ok := err.(x509.HostnameError); !ok { t.Fatalf("should get hostname err: %v", err) } <-errc } func TestConfig_wrapTLS_OK(t *testing.T) { ci.Parallel(t) config := &Config{ CAFile: cacert, CertFile: fooclientcert, KeyFile: fooclientkey, VerifyOutgoing: true, KeyLoader: &config.KeyLoader{}, } client, errc := startTLSServer(config) if client == nil { t.Fatalf("startTLSServer err: %v", <-errc) } clientConfig, err := config.OutgoingTLSConfig() if err != nil { t.Fatalf("OutgoingTLSConfig err: %v", err) } tlsClient, err := WrapTLSClient(client, clientConfig) if err != nil { t.Fatalf("wrapTLS err: %v", err) } else { tlsClient.Close() } err = <-errc if err != nil { t.Fatalf("server: %v", err) } } func TestConfig_wrapTLS_BadCert(t *testing.T) { ci.Parallel(t) serverConfig := &Config{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, KeyLoader: &config.KeyLoader{}, } client, errc := startTLSServer(serverConfig) if client == nil { t.Fatalf("startTLSServer err: %v", <-errc) } clientConfig := &Config{ CAFile: cacert, VerifyOutgoing: true, } clientTLSConfig, err := clientConfig.OutgoingTLSConfig() if err != nil { t.Fatalf("OutgoingTLSConfig err: %v", err) } tlsClient, err := WrapTLSClient(client, clientTLSConfig) if err == nil { t.Fatalf("wrapTLS no err") } if tlsClient != nil { t.Fatalf("returned a client") } err = <-errc if err != nil { t.Fatalf("server: %v", err) } } func TestConfig_IncomingTLS(t *testing.T) { ci.Parallel(t) assert := assert.New(t) conf := &Config{ VerifyIncoming: true, CAFile: cacert, CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } tlsC, err := conf.IncomingTLSConfig() if err != nil { t.Fatalf("err: %v", err) } if tlsC == nil { t.Fatalf("expected config") } if len(tlsC.ClientCAs.Subjects()) != 1 { t.Fatalf("expect client cert") } if tlsC.ClientAuth != tls.RequireAndVerifyClientCert { t.Fatalf("should not skip verification") } clientHelloInfo := &tls.ClientHelloInfo{} cert, err := tlsC.GetCertificate(clientHelloInfo) assert.Nil(err) assert.NotNil(cert) } func TestConfig_IncomingTLS_MissingCA(t *testing.T) { ci.Parallel(t) conf := &Config{ VerifyIncoming: true, CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } _, err := conf.IncomingTLSConfig() if err == nil { t.Fatalf("expected err") } } func TestConfig_IncomingTLS_MissingKey(t *testing.T) { ci.Parallel(t) conf := &Config{ VerifyIncoming: true, CAFile: cacert, } _, err := conf.IncomingTLSConfig() if err == nil { t.Fatalf("expected err") } } func TestConfig_IncomingTLS_NoVerify(t *testing.T) { ci.Parallel(t) conf := &Config{} tlsC, err := conf.IncomingTLSConfig() if err != nil { t.Fatalf("err: %v", err) } if tlsC == nil { t.Fatalf("expected config") } if len(tlsC.ClientCAs.Subjects()) != 0 { t.Fatalf("do not expect client cert") } if tlsC.ClientAuth != tls.NoClientCert { t.Fatalf("should skip verification") } if len(tlsC.Certificates) != 0 { t.Fatalf("unexpected client cert") } } func TestConfig_IncomingTLS_PreferServerCipherSuites(t *testing.T) { ci.Parallel(t) require := require.New(t) { conf := &Config{} tlsConfig, err := conf.IncomingTLSConfig() require.Nil(err) require.Equal(tlsConfig.PreferServerCipherSuites, false) } { conf := &Config{ PreferServerCipherSuites: true, } tlsConfig, err := conf.IncomingTLSConfig() require.Nil(err) require.Equal(tlsConfig.PreferServerCipherSuites, true) } } func TestConfig_IncomingTLS_TLSCipherSuites(t *testing.T) { ci.Parallel(t) require := require.New(t) { defaultCiphers := []uint16{ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, } conf := &Config{ CipherSuites: defaultCiphers, } tlsConfig, err := conf.IncomingTLSConfig() require.Nil(err) require.Equal(tlsConfig.CipherSuites, defaultCiphers) } { conf := &Config{ CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}, } tlsConfig, err := conf.IncomingTLSConfig() require.Nil(err) require.Equal(tlsConfig.CipherSuites, []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}) } } // This test relies on the fact that the specified certificate has an ECDSA // signature algorithm func TestConfig_ParseCiphers_Valid(t *testing.T) { ci.Parallel(t) require := require.New(t) tlsConfig := &config.TLSConfig{ CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, TLSCipherSuites: strings.Join([]string{ "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", }, ","), } expectedCiphers := []uint16{ tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_128_CBC_SHA256, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA, } parsedCiphers, err := ParseCiphers(tlsConfig) require.Nil(err) require.Equal(parsedCiphers, expectedCiphers) } // This test relies on the fact that the specified certificate has an ECDSA // signature algorithm func TestConfig_ParseCiphers_Default(t *testing.T) { ci.Parallel(t) require := require.New(t) expectedCiphers := []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, } empty := &config.TLSConfig{ CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } parsedCiphers, err := ParseCiphers(empty) require.Nil(err) require.Equal(parsedCiphers, expectedCiphers) } // This test relies on the fact that the specified certificate has an ECDSA // signature algorithm func TestConfig_ParseCiphers_Invalid(t *testing.T) { ci.Parallel(t) require := require.New(t) invalidCiphers := []string{ "TLS_RSA_RSA_WITH_RC4_128_SHA", "INVALID_CIPHER", } for _, cipher := range invalidCiphers { tlsConfig := &config.TLSConfig{ TLSCipherSuites: cipher, CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } parsedCiphers, err := ParseCiphers(tlsConfig) require.NotNil(err) require.Equal(fmt.Sprintf("unsupported TLS cipher %q", cipher), err.Error()) require.Equal(0, len(parsedCiphers)) } } // This test relies on the fact that the specified certificate has an ECDSA // signature algorithm func TestConfig_ParseCiphers_SupportedSignature(t *testing.T) { ci.Parallel(t) require := require.New(t) // Supported signature { tlsConfig := &config.TLSConfig{ TLSCipherSuites: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } parsedCiphers, err := ParseCiphers(tlsConfig) require.Nil(err) require.Equal(1, len(parsedCiphers)) } // Unsupported signature { tlsConfig := &config.TLSConfig{ TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } parsedCiphers, err := ParseCiphers(tlsConfig) require.NotNil(err) require.Equal(0, len(parsedCiphers)) } } func TestConfig_ParseMinVersion_Valid(t *testing.T) { ci.Parallel(t) require := require.New(t) validVersions := []string{"tls10", "tls11", "tls12", } expected := map[string]uint16{ "tls10": tls.VersionTLS10, "tls11": tls.VersionTLS11, "tls12": tls.VersionTLS12, } for _, version := range validVersions { parsedVersion, err := ParseMinVersion(version) require.Nil(err) require.Equal(expected[version], parsedVersion) } } func TestConfig_ParseMinVersion_Invalid(t *testing.T) { ci.Parallel(t) require := require.New(t) invalidVersions := []string{"tls13", "tls15", } for _, version := range invalidVersions { parsedVersion, err := ParseMinVersion(version) require.NotNil(err) require.Equal(fmt.Sprintf("unsupported TLS version %q", version), err.Error()) require.Equal(uint16(0), parsedVersion) } } func TestConfig_NewTLSConfiguration(t *testing.T) { ci.Parallel(t) require := require.New(t) conf := &config.TLSConfig{ TLSCipherSuites: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", CertFile: fooclientcert, KeyFile: fooclientkey, KeyLoader: &config.KeyLoader{}, } tlsConf, err := NewTLSConfiguration(conf, true, true) require.Nil(err) require.True(tlsConf.VerifyIncoming) require.True(tlsConf.VerifyOutgoing) expectedCiphers := []uint16{ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, } require.Equal(tlsConf.CipherSuites, expectedCiphers) } func TestConfig_ShouldReloadRPCConnections(t *testing.T) { ci.Parallel(t) require := require.New(t) type shouldReloadTestInput struct { old *config.TLSConfig new *config.TLSConfig shouldReload bool errorStr string } testInput := []*shouldReloadTestInput{ { old: &config.TLSConfig{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, }, new: &config.TLSConfig{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, }, shouldReload: false, errorStr: "Same TLS Configuration should not reload", }, { old: &config.TLSConfig{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, }, new: &config.TLSConfig{ CAFile: cacert, CertFile: fooclientcert, KeyFile: fooclientkey, }, shouldReload: true, errorStr: "Different TLS Configuration should reload", }, { old: &config.TLSConfig{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, EnableRPC: true, }, new: &config.TLSConfig{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, EnableRPC: false, }, shouldReload: true, errorStr: "Downgrading RPC connections should force reload", }, { old: nil, new: &config.TLSConfig{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, EnableRPC: true, }, shouldReload: true, errorStr: "Upgrading RPC connections should force reload", }, { old: &config.TLSConfig{ CAFile: cacert, CertFile: badcert, KeyFile: badkey, EnableRPC: true, }, new: nil, shouldReload: true, errorStr: "Downgrading RPC connections should force reload", }, } for _, testCase := range testInput { shouldReload, err := ShouldReloadRPCConnections(testCase.old, testCase.new) require.NoError(err) require.Equal(shouldReload, testCase.shouldReload, testCase.errorStr) } }