diff --git a/cmd/notary-signer/main.go b/cmd/notary-signer/main.go index 83dc413c2b..4a86dde90b 100644 --- a/cmd/notary-signer/main.go +++ b/cmd/notary-signer/main.go @@ -1,8 +1,6 @@ package main import ( - "crypto/rand" - "crypto/tls" "database/sql" "errors" _ "expvar" @@ -22,6 +20,7 @@ import ( "github.com/docker/notary/cryptoservice" "github.com/docker/notary/signer" "github.com/docker/notary/signer/api" + "github.com/docker/notary/utils" "github.com/docker/notary/version" "github.com/endophage/gotuf/data" _ "github.com/go-sql-driver/mysql" @@ -103,20 +102,13 @@ func main() { log.Fatalf("Certificate and key are mandatory") } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_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_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - tls.TLS_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_CBC_SHA}, + tlsConfig, err := utils.ConfigureServerTLS(&utils.ServerTLSOpts{ + ServerCertFile: certFile, + ServerKeyFile: keyFile, + }) + if err != nil { + logrus.Fatalf("Unable to set up TLS: %s", err.Error()) } - tlsConfig.Rand = rand.Reader cryptoServices := make(signer.CryptoServiceIndex) diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 40f5191ca5..b3cd58e348 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -3,8 +3,6 @@ package main import ( "bufio" "crypto/sha256" - "crypto/tls" - "crypto/x509" "fmt" "io/ioutil" "net" @@ -22,7 +20,7 @@ import ( "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/pkg/term" notaryclient "github.com/docker/notary/client" - "github.com/docker/notary/trustmanager" + "github.com/docker/notary/utils" "github.com/spf13/cobra" ) @@ -360,7 +358,6 @@ func (ps passwordStore) Basic(u *url.URL) (string, string) { func getTransport(gun string, readOnly bool) http.RoundTripper { // Attempt to get a root CA from the config file. Nil is the host defaults. - rootPool := x509.NewCertPool() rootCAFile := mainViper.GetString("remote_server.root_ca") if rootCAFile != "" { // If we haven't been given an Absolute path, we assume it's relative @@ -368,19 +365,18 @@ func getTransport(gun string, readOnly bool) http.RoundTripper { if !filepath.IsAbs(rootCAFile) { rootCAFile = filepath.Join(configPath, rootCAFile) } - rootCert, err := trustmanager.LoadCertFromFile(rootCAFile) - if err != nil { - fatalf("could not load root ca file. %s", err.Error()) - } - rootPool.AddCert(rootCert) } - // skipTLSVerify is false by default so verification will - // be performed. - tlsConfig := &tls.Config{ - InsecureSkipVerify: mainViper.GetBool("remote_server.skipTLSVerify"), - MinVersion: tls.VersionTLS10, - RootCAs: rootPool, + insecureSkipVerify := false + if mainViper.IsSet("remote_server.skipTLSVerify") { + insecureSkipVerify = mainViper.GetBool("remote_server.skipTLSVerify") + } + tlsConfig, err := utils.ConfigureClientTLS(&utils.ClientTLSOpts{ + RootCAFile: rootCAFile, + InsecureSkipVerify: insecureSkipVerify, + }) + if err != nil { + logrus.Fatal("Unable to configure TLS: ", err.Error()) } base := &http.Transport{ diff --git a/server/server.go b/server/server.go index bee1b7017d..cc8e29ded6 100644 --- a/server/server.go +++ b/server/server.go @@ -1,7 +1,6 @@ package server import ( - "crypto/rand" "crypto/tls" "fmt" "net" @@ -42,27 +41,13 @@ func Run(ctx context.Context, addr, tlsCertFile, tlsKeyFile string, trust signed } if tlsCertFile != "" && tlsKeyFile != "" { - keypair, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) + tlsConfig, err := utils.ConfigureServerTLS(&utils.ServerTLSOpts{ + ServerCertFile: tlsCertFile, + ServerKeyFile: tlsKeyFile, + }) if err != nil { return err } - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_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_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - tls.TLS_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, - }, - Certificates: []tls.Certificate{keypair}, - Rand: rand.Reader, - } - logrus.Info("Enabling TLS") lsnr = tls.NewListener(lsnr, tlsConfig) } else if tlsCertFile != "" || tlsKeyFile != "" { diff --git a/signer/signer_trust.go b/signer/signer_trust.go index 07ad56ecc0..448a1767da 100644 --- a/signer/signer_trust.go +++ b/signer/signer_trust.go @@ -7,6 +7,7 @@ import ( "github.com/Sirupsen/logrus" pb "github.com/docker/notary/proto" + "github.com/docker/notary/utils" "github.com/endophage/gotuf/data" "golang.org/x/net/context" "google.golang.org/grpc" @@ -30,10 +31,14 @@ type NotarySigner struct { func NewNotarySigner(hostname string, port string, tlscafile string) *NotarySigner { var opts []grpc.DialOption netAddr := net.JoinHostPort(hostname, port) - creds, err := credentials.NewClientTLSFromFile(tlscafile, hostname) + tlsConfig, err := utils.ConfigureClientTLS(&utils.ClientTLSOpts{ + RootCAFile: tlscafile, + ServerName: hostname, + }) if err != nil { - logrus.Fatal("fail to read: ", err) + logrus.Fatal("Unable to set up TLS: ", err) } + creds := credentials.NewTLS(tlsConfig) opts = append(opts, grpc.WithTransportCredentials(creds)) conn, err := grpc.Dial(netAddr, opts...) diff --git a/trustmanager/x509filestore.go b/trustmanager/x509filestore.go index 1df6233191..8417805583 100644 --- a/trustmanager/x509filestore.go +++ b/trustmanager/x509filestore.go @@ -260,6 +260,12 @@ func (s *X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, er return opts, nil } +// Empty returns true if there are no certificates in the X509FileStore, false +// otherwise. +func (s *X509FileStore) Empty() bool { + return len(s.fingerprintMap) == 0 +} + func fileName(cert *x509.Certificate) (string, CertID, error) { certID, err := fingerprintCert(cert) if err != nil { diff --git a/trustmanager/x509filestore_test.go b/trustmanager/x509filestore_test.go index 21b1a0b36b..58decfc748 100644 --- a/trustmanager/x509filestore_test.go +++ b/trustmanager/x509filestore_test.go @@ -5,6 +5,7 @@ import ( "encoding/pem" "io/ioutil" "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -22,6 +23,44 @@ func TestNewX509FileStore(t *testing.T) { } } +// NewX509FileStore loads any existing certs from the directory, and does +// not overwrite any of the. +func TestNewX509FileStoreLoadsExistingCerts(t *testing.T) { + tempDir, err := ioutil.TempDir("", "cert-test") + assert.NoError(t, err) + defer os.RemoveAll(tempDir) + + certBytes, err := ioutil.ReadFile("../fixtures/root-ca.crt") + assert.NoError(t, err) + out, err := os.Create(filepath.Join(tempDir, "root-ca.crt")) + assert.NoError(t, err) + + // to distinguish it from the canonical format + distinguishingBytes := []byte{'\n', '\n', '\n', '\n', '\n', '\n'} + nBytes, err := out.Write(distinguishingBytes) + assert.NoError(t, err) + assert.Len(t, distinguishingBytes, nBytes) + + nBytes, err = out.Write(certBytes) + assert.NoError(t, err) + assert.Len(t, certBytes, nBytes) + + err = out.Close() + assert.NoError(t, err) + + store, err := NewX509FileStore(tempDir) + assert.NoError(t, err) + + expectedCert, err := LoadCertFromFile("../fixtures/root-ca.crt") + assert.NoError(t, err) + assert.Equal(t, []*x509.Certificate{expectedCert}, store.GetCertificates()) + + outBytes, err := ioutil.ReadFile(filepath.Join(tempDir, "root-ca.crt")) + assert.NoError(t, err) + assert.Equal(t, distinguishingBytes, outBytes[:6], "original file overwritten") + assert.Equal(t, certBytes, outBytes[6:], "original file overwritten") +} + func TestAddCertX509FileStore(t *testing.T) { // Read certificate from file b, err := ioutil.ReadFile("../fixtures/root-ca.crt") @@ -82,6 +121,21 @@ func TestAddCertFromFileX509FileStore(t *testing.T) { } } +// TestNewX509FileStoreEmpty verifies the behavior of the Empty function +func TestNewX509FileStoreEmpty(t *testing.T) { + tempDir, err := ioutil.TempDir("", "cert-test") + assert.NoError(t, err) + defer os.RemoveAll(tempDir) + + store, err := NewX509FileStore(tempDir) + assert.NoError(t, err) + assert.True(t, store.Empty()) + + err = store.AddCertFromFile("../fixtures/root-ca.crt") + assert.NoError(t, err) + assert.False(t, store.Empty()) +} + func TestAddCertFromPEMX509FileStore(t *testing.T) { b, err := ioutil.ReadFile("../fixtures/root-ca.crt") if err != nil { diff --git a/utils/tls_config.go b/utils/tls_config.go new file mode 100644 index 0000000000..15050f84ae --- /dev/null +++ b/utils/tls_config.go @@ -0,0 +1,133 @@ +package utils + +import ( + "crypto/rand" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" +) + +// Client TLS cipher suites (dropping CBC ciphers for client preferred suite set) +var clientCipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, +} + +// Server TLS cipher suites +var serverCipherSuites = append(clientCipherSuites, []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, +}...) + +func poolFromFile(filename string) (*x509.CertPool, error) { + pemBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(pemBytes); !ok { + return nil, fmt.Errorf( + "Unable to parse certificates from %s", filename) + } + if len(pool.Subjects()) == 0 { + return nil, fmt.Errorf( + "No certificates parsed from %s", filename) + } + return pool, nil +} + +// ServerTLSOpts generates a tls configuration for servers using the +// provided parameters. +type ServerTLSOpts struct { + ServerCertFile string + ServerKeyFile string + RequireClientAuth bool + ClientCAFile string +} + +// ConfigureServerTLS specifies a set of ciphersuites, the server cert and key, +// and optionally client authentication. Note that a tls configuration is +// constructed that either requires and verifies client authentication or +// doesn't deal with client certs at all. Nothing in the middle. +// +// Also note that if the client CA file contains invalid data, behavior is not +// guaranteed. Currently (as of Go 1.5.1) only the valid certificates up to +// the bad data will be parsed and added the client CA pool. +func ConfigureServerTLS(opts *ServerTLSOpts) (*tls.Config, error) { + keypair, err := tls.LoadX509KeyPair( + opts.ServerCertFile, opts.ServerKeyFile) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + CipherSuites: serverCipherSuites, + Certificates: []tls.Certificate{keypair}, + Rand: rand.Reader, + } + + if opts.RequireClientAuth { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + + if opts.ClientCAFile != "" { + pool, err := poolFromFile(opts.ClientCAFile) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } + + return tlsConfig, nil +} + +// ClientTLSOpts is a struct that contains options to pass to +// ConfigureClientTLS +type ClientTLSOpts struct { + RootCAFile string + ServerName string + InsecureSkipVerify bool + ClientCertFile string + ClientKeyFile string +} + +// ConfigureClientTLS generates a tls configuration for clients using the +// provided parameters. +/// +// Note that if the root CA file contains invalid data, behavior is not +// guaranteed. Currently (as of Go 1.5.1) only the valid certificates up to +// the bad data will be parsed and added the root CA pool. +func ConfigureClientTLS(opts *ClientTLSOpts) (*tls.Config, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: opts.InsecureSkipVerify, + MinVersion: tls.VersionTLS12, + CipherSuites: clientCipherSuites, + ServerName: opts.ServerName, + } + + if opts.RootCAFile != "" { + pool, err := poolFromFile(opts.RootCAFile) + if err != nil { + return nil, err + } + tlsConfig.RootCAs = pool + } + + if opts.ClientCertFile != "" || opts.ClientKeyFile != "" { + keypair, err := tls.LoadX509KeyPair( + opts.ClientCertFile, opts.ClientKeyFile) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{keypair} + } + + return tlsConfig, nil +} diff --git a/utils/tls_config_test.go b/utils/tls_config_test.go new file mode 100644 index 0000000000..75c53e8809 --- /dev/null +++ b/utils/tls_config_test.go @@ -0,0 +1,253 @@ +package utils + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "io/ioutil" + "os" + "testing" + + "github.com/docker/notary/trustmanager" + "github.com/stretchr/testify/assert" +) + +const ( + ServerCert = "../fixtures/notary-server.crt" + ServerKey = "../fixtures/notary-server.key" + RootCA = "../fixtures/root-ca.crt" +) + +// generates a multiple-certificate file with both RSA and ECDSA certs and +// returns the filename so that cleanup can be deferred. +func generateMultiCert(t *testing.T) string { + tempFile, err := ioutil.TempFile("/tmp", "cert-test") + defer tempFile.Close() + assert.NoError(t, err) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + assert.NoError(t, err) + ecKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + assert.NoError(t, err) + template, err := trustmanager.NewCertificate("gun") + assert.NoError(t, err) + + for _, key := range []crypto.Signer{rsaKey, ecKey} { + derBytes, err := x509.CreateCertificate( + rand.Reader, template, template, key.Public(), key) + assert.NoError(t, err) + + cert, err := x509.ParseCertificate(derBytes) + assert.NoError(t, err) + + pemBytes := trustmanager.CertToPEM(cert) + nBytes, err := tempFile.Write(pemBytes) + assert.NoError(t, err) + assert.Equal(t, nBytes, len(pemBytes)) + } + return tempFile.Name() +} + +// If the cert files and directory are provided but are invalid, an error is +// returned. +func TestConfigServerTLSFailsIfUnableToLoadCerts(t *testing.T) { + for i := 0; i < 3; i++ { + files := []string{ServerCert, ServerKey, RootCA} + files[i] = "not-real-file" + + result, err := ConfigureServerTLS(&ServerTLSOpts{ + ServerCertFile: files[0], + ServerKeyFile: files[1], + RequireClientAuth: true, + ClientCAFile: files[2], + }) + assert.Nil(t, result) + assert.Error(t, err) + } +} + +// If server cert and key are provided, and client auth is disabled, then +// a valid tls.Config is returned with ClientAuth set to NoClientCert +func TestConfigServerTLSServerCertsOnly(t *testing.T) { + keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey) + assert.NoError(t, err) + + tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{ + ServerCertFile: ServerCert, + ServerKeyFile: ServerKey, + }) + assert.NoError(t, err) + assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates) + assert.True(t, tlsConfig.PreferServerCipherSuites) + assert.Equal(t, tls.NoClientCert, tlsConfig.ClientAuth) + assert.Nil(t, tlsConfig.ClientCAs) +} + +// If a valid client cert file is provided, but it contains no client +// certs, an error is returned. +func TestConfigServerTLSWithEmptyCACertFile(t *testing.T) { + tempFile, err := ioutil.TempFile("/tmp", "cert-test") + assert.NoError(t, err) + defer os.RemoveAll(tempFile.Name()) + tempFile.Close() + + tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{ + ServerCertFile: ServerCert, + ServerKeyFile: ServerKey, + ClientCAFile: tempFile.Name(), + }) + assert.Nil(t, tlsConfig) + assert.Error(t, err) +} + +// If server cert and key are provided, and client cert file is provided with +// one cert, a valid tls.Config is returned with the clientCAs set to that +// cert. +func TestConfigServerTLSWithOneCACert(t *testing.T) { + keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey) + assert.NoError(t, err) + + tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{ + ServerCertFile: ServerCert, + ServerKeyFile: ServerKey, + ClientCAFile: RootCA, + }) + assert.NoError(t, err) + assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates) + assert.True(t, tlsConfig.PreferServerCipherSuites) + assert.Equal(t, tls.NoClientCert, tlsConfig.ClientAuth) + assert.Len(t, tlsConfig.ClientCAs.Subjects(), 1) +} + +// If server cert and key are provided, and client cert file is provided with +// multiple certs, a valid tls.Config is returned with the clientCAs set to +// the valid cert. +func TestConfigServerTLSWithMultipleCACerts(t *testing.T) { + tempFilename := generateMultiCert(t) + defer os.RemoveAll(tempFilename) + + keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey) + assert.NoError(t, err) + + tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{ + ServerCertFile: ServerCert, + ServerKeyFile: ServerKey, + ClientCAFile: tempFilename, + }) + assert.NoError(t, err) + assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates) + assert.True(t, tlsConfig.PreferServerCipherSuites) + assert.Equal(t, tls.NoClientCert, tlsConfig.ClientAuth) + assert.Len(t, tlsConfig.ClientCAs.Subjects(), 2) +} + +// If server cert and key are provided, and client auth is disabled, then +// a valid tls.Config is returned with ClientAuth set to +// RequireAndVerifyClientCert +func TestConfigServerTLSClientAuthEnabled(t *testing.T) { + keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey) + assert.NoError(t, err) + + tlsConfig, err := ConfigureServerTLS(&ServerTLSOpts{ + ServerCertFile: ServerCert, + ServerKeyFile: ServerKey, + RequireClientAuth: true, + }) + assert.NoError(t, err) + assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates) + assert.True(t, tlsConfig.PreferServerCipherSuites) + assert.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth) + assert.Nil(t, tlsConfig.ClientCAs) +} + +// The skipVerify boolean gets set on the tls.Config's InsecureSkipBoolean +func TestConfigClientTLSNoVerify(t *testing.T) { + for _, skip := range []bool{true, false} { + tlsConfig, err := ConfigureClientTLS( + &ClientTLSOpts{InsecureSkipVerify: skip}) + assert.NoError(t, err) + assert.Nil(t, tlsConfig.Certificates) + assert.Equal(t, skip, tlsConfig.InsecureSkipVerify) + assert.Equal(t, "", tlsConfig.ServerName) + assert.Nil(t, tlsConfig.RootCAs) + } +} + +// The skipVerify boolean gets set on the tls.Config's InsecureSkipBoolean +func TestConfigClientServerName(t *testing.T) { + for _, name := range []string{"", "myname"} { + tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{ServerName: name}) + assert.NoError(t, err) + assert.Nil(t, tlsConfig.Certificates) + assert.Equal(t, false, tlsConfig.InsecureSkipVerify) + assert.Equal(t, name, tlsConfig.ServerName) + assert.Nil(t, tlsConfig.RootCAs) + } +} + +// The RootCA is set if the file provided has a single CA cert. +func TestConfigClientTLSRootCAFileWithOneCert(t *testing.T) { + tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{RootCAFile: RootCA}) + assert.NoError(t, err) + assert.Nil(t, tlsConfig.Certificates) + assert.Equal(t, false, tlsConfig.InsecureSkipVerify) + assert.Equal(t, "", tlsConfig.ServerName) + assert.Len(t, tlsConfig.RootCAs.Subjects(), 1) +} + +// If the root CA file provided has multiple CA certs, only the valid certs +// are read. +func TestConfigClientTLSRootCAFileMultipleCerts(t *testing.T) { + tempFilename := generateMultiCert(t) + defer os.RemoveAll(tempFilename) + + tlsConfig, err := ConfigureClientTLS( + &ClientTLSOpts{RootCAFile: tempFilename}) + assert.NoError(t, err) + assert.Nil(t, tlsConfig.Certificates) + assert.Equal(t, false, tlsConfig.InsecureSkipVerify) + assert.Equal(t, "", tlsConfig.ServerName) + assert.Len(t, tlsConfig.RootCAs.Subjects(), 2) +} + +// An error is returned if a root CA is provided but the file doesn't exist. +func TestConfigClientTLSNonexistentRootCAFile(t *testing.T) { + tlsConfig, err := ConfigureClientTLS( + &ClientTLSOpts{RootCAFile: "not-a-file"}) + assert.Error(t, err) + assert.Nil(t, tlsConfig) +} + +// An error is returned if either the client cert or the key are provided +// but invalid or blank. +func TestConfigClientTLSClientCertOrKeyInvalid(t *testing.T) { + for i := 0; i < 2; i++ { + for _, invalid := range []string{"not-a-file", ""} { + files := []string{ServerCert, ServerKey} + files[i] = invalid + tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{ + ClientCertFile: files[0], ClientKeyFile: files[1]}) + assert.Error(t, err) + assert.Nil(t, tlsConfig) + } + } +} + +// The certificate is set if the client cert and client key are provided and +// valid. +func TestConfigClientTLSValidClientCertAndKey(t *testing.T) { + keypair, err := tls.LoadX509KeyPair(ServerCert, ServerKey) + assert.NoError(t, err) + + tlsConfig, err := ConfigureClientTLS(&ClientTLSOpts{ + ClientCertFile: ServerCert, ClientKeyFile: ServerKey}) + assert.NoError(t, err) + assert.Equal(t, []tls.Certificate{keypair}, tlsConfig.Certificates) + assert.Equal(t, false, tlsConfig.InsecureSkipVerify) + assert.Equal(t, "", tlsConfig.ServerName) + assert.Nil(t, tlsConfig.RootCAs) +}