diff --git a/configuration/configuration.go b/configuration/configuration.go index b347d63b9..dea13cd0d 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -108,6 +108,12 @@ type Configuration struct { // A file may contain multiple CA certificates encoded as PEM ClientCAs []string `yaml:"clientcas,omitempty"` + // Specifies the lowest TLS version allowed + MinimumTLS string `yaml:"minimumtls,omitempty"` + + // Specifies a list of cipher suites allowed + CipherSuites []string `yaml:"ciphersuites,omitempty"` + // LetsEncrypt is used to configuration setting up TLS through // Let's Encrypt instead of manually specifying certificate and // key. If a TLS certificate is specified, the Let's Encrypt diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index e5f714867..74a06c497 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -80,10 +80,12 @@ var configStruct = Configuration{ RelativeURLs bool `yaml:"relativeurls,omitempty"` DrainTimeout time.Duration `yaml:"draintimeout,omitempty"` TLS struct { - Certificate string `yaml:"certificate,omitempty"` - Key string `yaml:"key,omitempty"` - ClientCAs []string `yaml:"clientcas,omitempty"` - LetsEncrypt struct { + Certificate string `yaml:"certificate,omitempty"` + Key string `yaml:"key,omitempty"` + ClientCAs []string `yaml:"clientcas,omitempty"` + MinimumTLS string `yaml:"minimumtls,omitempty"` + CipherSuites []string `yaml:"ciphersuites,omitempty"` + LetsEncrypt struct { CacheFile string `yaml:"cachefile,omitempty"` Email string `yaml:"email,omitempty"` Hosts []string `yaml:"hosts,omitempty"` @@ -102,10 +104,12 @@ var configStruct = Configuration{ } `yaml:"http2,omitempty"` }{ TLS: struct { - Certificate string `yaml:"certificate,omitempty"` - Key string `yaml:"key,omitempty"` - ClientCAs []string `yaml:"clientcas,omitempty"` - LetsEncrypt struct { + Certificate string `yaml:"certificate,omitempty"` + Key string `yaml:"key,omitempty"` + ClientCAs []string `yaml:"clientcas,omitempty"` + MinimumTLS string `yaml:"minimumtls,omitempty"` + CipherSuites []string `yaml:"ciphersuites,omitempty"` + LetsEncrypt struct { CacheFile string `yaml:"cachefile,omitempty"` Email string `yaml:"email,omitempty"` Hosts []string `yaml:"hosts,omitempty"` diff --git a/docs/configuration.md b/docs/configuration.md index 23aaa81c8..75f52deaa 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -782,6 +782,10 @@ http: clientcas: - /path/to/ca.pem - /path/to/another/ca.pem + minimumtls: tls1.2 + ciphersuites: + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 letsencrypt: cachefile: /path/to/cache-file email: emailused@letsencrypt.com @@ -817,9 +821,49 @@ and proxy connections to the registry server. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| -| `certificate` | yes | Absolute path to the x509 certificate file. | -| `key` | yes | Absolute path to the x509 private key file. | -| `clientcas` | no | An array of absolute paths to x509 CA files. | +| `certificate` | yes | Absolute path to the x509 certificate file. | +| `key` | yes | Absolute path to the x509 private key file. | +| `clientcas` | no | An array of absolute paths to x509 CA files. | +| `minimumtls` | no | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2, tls1.3). Defaults to tls1.2 | +| `ciphersuites` | no | Cipher suites allowed. Please see below for allowed values and default. | + +Available cipher suites: +- TLS_RSA_WITH_RC4_128_SHA +- TLS_RSA_WITH_3DES_EDE_CBC_SHA +- TLS_RSA_WITH_AES_128_CBC_SHA +- TLS_RSA_WITH_AES_256_CBC_SHA +- TLS_RSA_WITH_AES_128_CBC_SHA256 +- TLS_RSA_WITH_AES_128_GCM_SHA256 +- TLS_RSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_ECDSA_WITH_RC4_128_SHA +- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA +- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA +- TLS_ECDHE_RSA_WITH_RC4_128_SHA +- TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA +- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA +- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA +- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 +- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 +- 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_CHACHA20_POLY1305_SHA256 +- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 +- TLS_AES_128_GCM_SHA256 +- TLS_AES_256_GCM_SHA384 +- TLS_CHACHA20_POLY1305_SHA256 + +Default cipher suites: +- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 +- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 +- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +- TLS_AES_128_GCM_SHA256 +- TLS_CHACHA20_POLY1305_SHA256 +- TLS_AES_256_GCM_SHA384 ### `letsencrypt` diff --git a/registry/registry.go b/registry/registry.go index 18698f5bf..16b85919a 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -31,6 +32,60 @@ import ( "github.com/yvasiyarov/gorelic" ) +// a map of TLS cipher suite names to constants in https://golang.org/pkg/crypto/tls/#pkg-constants +var cipherSuites = map[string]uint16{ + // TLS 1.0 - 1.2 cipher suites + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + // TLS 1.3 cipher suites + "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, + "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, + "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, +} + +// a list of default ciphersuites to utilize +var defaultCipherSuites = []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_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_256_GCM_SHA384, +} + +// maps tls version strings to constants +var defaultTLSVersionStr = "tls1.2" +var tlsVersions = map[string]uint16{ + // user specified values + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, + "tls1.2": tls.VersionTLS12, + "tls1.3": tls.VersionTLS13, +} + // this channel gets notified when process receives signal. It is global to ease unit testing var quit = make(chan os.Signal, 1) @@ -125,6 +180,35 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg }, nil } +// takes a list of cipher suites and converts it to a list of respective tls constants +// if an empty list is provided, then the defaults will be used +func getCipherSuites(names []string) ([]uint16, error) { + if len(names) == 0 { + return defaultCipherSuites, nil + } + cipherSuiteConsts := make([]uint16, len(names)) + for i, name := range names { + cipherSuiteConst, ok := cipherSuites[name] + if !ok { + return nil, fmt.Errorf("unknown TLS cipher suite '%s' specified for http.tls.cipherSuites", name) + } + cipherSuiteConsts[i] = cipherSuiteConst + } + return cipherSuiteConsts, nil +} + +// takes a list of cipher suite ids and converts it to a list of respective names +func getCipherSuiteNames(ids []uint16) []string { + if len(ids) == 0 { + return nil + } + names := make([]string, len(ids)) + for i, id := range ids { + names[i] = tls.CipherSuiteName(id) + } + return names +} + // ListenAndServe runs the registry's HTTP server. func (registry *Registry) ListenAndServe() error { config := registry.config @@ -135,19 +219,27 @@ func (registry *Registry) ListenAndServe() error { } if config.HTTP.TLS.Certificate != "" || config.HTTP.TLS.LetsEncrypt.CacheFile != "" { + if config.HTTP.TLS.MinimumTLS == "" { + config.HTTP.TLS.MinimumTLS = defaultTLSVersionStr + } + tlsMinVersion, ok := tlsVersions[config.HTTP.TLS.MinimumTLS] + if !ok { + return fmt.Errorf("unknown minimum TLS level '%s' specified for http.tls.minimumtls", config.HTTP.TLS.MinimumTLS) + } + dcontext.GetLogger(registry.app).Infof("restricting TLS version to %s or higher", config.HTTP.TLS.MinimumTLS) + + tlsCipherSuites, err := getCipherSuites(config.HTTP.TLS.CipherSuites) + if err != nil { + return err + } + dcontext.GetLogger(registry.app).Infof("restricting TLS cipher suites to: %s", strings.Join(getCipherSuiteNames(tlsCipherSuites), ",")) + tlsConf := &tls.Config{ ClientAuth: tls.NoClientCert, NextProtos: nextProtos(config), - MinVersion: tls.VersionTLS10, + MinVersion: tlsMinVersion, PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - }, + CipherSuites: tlsCipherSuites, } if config.HTTP.TLS.LetsEncrypt.CacheFile != "" { diff --git a/registry/registry_test.go b/registry/registry_test.go index d8deb35e1..c7eb85e46 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -3,12 +3,24 @@ package registry import ( "bufio" "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" "io/ioutil" + "math/big" "net" "net/http" "os" + "path" "reflect" + "strings" "testing" "time" @@ -38,18 +50,30 @@ func TestNextProtos(t *testing.T) { } } -func setupRegistry() (*Registry, error) { +type registryTLSConfig struct { + cipherSuites []string + certificatePath string + privateKeyPath string + certificate *tls.Certificate +} + +func setupRegistry(tlsCfg *registryTLSConfig, addr string) (*Registry, error) { config := &configuration.Configuration{} // TODO: this needs to change to something ephemeral as the test will fail if there is any server // already listening on port 5000 - config.HTTP.Addr = ":5000" + config.HTTP.Addr = addr config.HTTP.DrainTimeout = time.Duration(10) * time.Second + if tlsCfg != nil { + config.HTTP.TLS.CipherSuites = tlsCfg.cipherSuites + config.HTTP.TLS.Certificate = tlsCfg.certificatePath + config.HTTP.TLS.Key = tlsCfg.privateKeyPath + } config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} return NewRegistry(context.Background(), config) } func TestGracefulShutdown(t *testing.T) { - registry, err := setupRegistry() + registry, err := setupRegistry(nil, ":5000") if err != nil { t.Fatal(err) } @@ -98,3 +122,227 @@ func TestGracefulShutdown(t *testing.T) { t.Error("Body is not {}; ", string(body)) } } + +func TestGetCipherSuite(t *testing.T) { + resp, err := getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA"}) + if err != nil || len(resp) != 1 || resp[0] != tls.TLS_RSA_WITH_AES_128_CBC_SHA { + t.Errorf("expected cipher suite %q, got %q", + "TLS_RSA_WITH_AES_128_CBC_SHA", + strings.Join(getCipherSuiteNames(resp), ","), + ) + } + + resp, err = getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_AES_128_GCM_SHA256"}) + if err != nil || len(resp) != 2 || + resp[0] != tls.TLS_RSA_WITH_AES_128_CBC_SHA || resp[1] != tls.TLS_AES_128_GCM_SHA256 { + t.Errorf("expected cipher suites %q, got %q", + "TLS_RSA_WITH_AES_128_CBC_SHA,TLS_AES_128_GCM_SHA256", + strings.Join(getCipherSuiteNames(resp), ","), + ) + } + + _, err = getCipherSuites([]string{"TLS_RSA_WITH_AES_128_CBC_SHA", "bad_input"}) + if err == nil { + t.Error("did not return expected error about unknown cipher suite") + } +} + +func buildRegistryTLSConfig(name, keyType string, cipherSuites []string) (*registryTLSConfig, error) { + var priv interface{} + var pub crypto.PublicKey + var err error + switch keyType { + case "rsa": + priv, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, fmt.Errorf("failed to create rsa private key: %v", err) + } + rsaKey := priv.(*rsa.PrivateKey) + pub = rsaKey.Public() + case "ecdsa": + priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to create ecdsa private key: %v", err) + } + ecdsaKey := priv.(*ecdsa.PrivateKey) + pub = ecdsaKey.Public() + default: + return nil, fmt.Errorf("unsupported key type: %v", keyType) + } + + notBefore := time.Now() + notAfter := notBefore.Add(time.Minute) + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to create serial number: %v", err) + } + cert := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"registry_test"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + DNSNames: []string{"localhost"}, + IsCA: true, + } + derBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, pub, priv) + if err != nil { + return nil, fmt.Errorf("failed to create certificate: %v", err) + } + if _, err := os.Stat(os.TempDir()); os.IsNotExist(err) { + os.Mkdir(os.TempDir(), 1777) + } + + certPath := path.Join(os.TempDir(), name+".pem") + certOut, err := os.Create(certPath) + if err != nil { + return nil, fmt.Errorf("failed to create pem: %v", err) + } + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return nil, fmt.Errorf("failed to write data to %s: %v", certPath, err) + } + if err := certOut.Close(); err != nil { + return nil, fmt.Errorf("error closing %s: %v", certPath, err) + } + + keyPath := path.Join(os.TempDir(), name+".key") + keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return nil, fmt.Errorf("failed to open %s for writing: %v", keyPath, err) + } + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, fmt.Errorf("unable to marshal private key: %v", err) + } + if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { + return nil, fmt.Errorf("failed to write data to key.pem: %v", err) + } + if err := keyOut.Close(); err != nil { + return nil, fmt.Errorf("error closing %s: %v", keyPath, err) + } + + tlsCert := tls.Certificate{ + Certificate: [][]byte{derBytes}, + PrivateKey: priv, + } + + tlsTestCfg := registryTLSConfig{ + cipherSuites: cipherSuites, + certificatePath: certPath, + privateKeyPath: keyPath, + certificate: &tlsCert, + } + + return &tlsTestCfg, nil +} + +func TestRegistrySupportedCipherSuite(t *testing.T) { + name := "registry_test_server_supported_cipher" + cipherSuites := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + serverTLS, err := buildRegistryTLSConfig(name, "rsa", cipherSuites) + if err != nil { + t.Fatal(err) + } + + registry, err := setupRegistry(serverTLS, ":5001") + if err != nil { + t.Fatal(err) + } + + // run registry server + var errchan chan error + go func() { + errchan <- registry.ListenAndServe() + }() + select { + case err = <-errchan: + t.Fatalf("Error listening: %v", err) + default: + } + + // Wait for some unknown random time for server to start listening + time.Sleep(3 * time.Second) + + // send tls request with server supported cipher suite + clientCipherSuites, err := getCipherSuites(cipherSuites) + if err != nil { + t.Fatal(err) + } + clientTLS := tls.Config{ + InsecureSkipVerify: true, + CipherSuites: clientCipherSuites, + } + dialer := net.Dialer{ + Timeout: time.Second * 5, + } + conn, err := tls.DialWithDialer(&dialer, "tcp", "127.0.0.1:5001", &clientTLS) + if err != nil { + t.Fatal(err) + } + fmt.Fprintf(conn, "GET /v2/ HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") + + resp, err := http.ReadResponse(bufio.NewReader(conn), nil) + if err != nil { + t.Fatal(err) + } + if resp.Status != "200 OK" { + t.Error("response status is not 200 OK: ", resp.Status) + } + if body, err := ioutil.ReadAll(resp.Body); err != nil || string(body) != "{}" { + t.Error("Body is not {}; ", string(body)) + } + + // send stop signal + quit <- os.Interrupt + time.Sleep(100 * time.Millisecond) +} + +func TestRegistryUnsupportedCipherSuite(t *testing.T) { + name := "registry_test_server_unsupported_cipher" + serverTLS, err := buildRegistryTLSConfig(name, "rsa", []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA358"}) + if err != nil { + t.Fatal(err) + } + + registry, err := setupRegistry(serverTLS, ":5002") + if err != nil { + t.Fatal(err) + } + + // run registry server + var errchan chan error + go func() { + errchan <- registry.ListenAndServe() + }() + select { + case err = <-errchan: + t.Fatalf("Error listening: %v", err) + default: + } + + // Wait for some unknown random time for server to start listening + time.Sleep(3 * time.Second) + + // send tls request with server unsupported cipher suite + clientTLS := tls.Config{ + InsecureSkipVerify: true, + CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, + } + dialer := net.Dialer{ + Timeout: time.Second * 5, + } + _, err = tls.DialWithDialer(&dialer, "tcp", "127.0.0.1:5002", &clientTLS) + if err == nil { + t.Error("expected TLS connection to timeout") + } + + // send stop signal + quit <- os.Interrupt + time.Sleep(100 * time.Millisecond) +}