Merge pull request #229 from cyli/tls-config-refactor

Factor out TLS configuration code for server and TLS
This commit is contained in:
Ying Li 2015-10-26 09:33:41 -07:00
commit bcdd375ce5
8 changed files with 475 additions and 51 deletions

View File

@ -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)

View File

@ -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{

View File

@ -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 != "" {

View File

@ -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...)

View File

@ -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 {

View File

@ -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 {

133
utils/tls_config.go Normal file
View File

@ -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
}

253
utils/tls_config_test.go Normal file
View File

@ -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)
}