From 01e871579ea499432c7ec55209a3f5de9171162d Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Mon, 28 Sep 2015 15:07:54 -0700 Subject: [PATCH 01/18] Add a transport library. The transport library provides tools for building TLS-secured client and server connections. It is designed to minimise the number of knobs and switches that are presented to end users, and supports features such as auto-updating of certificates. --- transport/ca/cert_provider.go | 16 ++ transport/client.go | 239 ++++++++++++++++++ transport/core/config.go | 45 ++++ transport/core/defs.go | 38 +++ transport/core/roots.go | 58 +++++ transport/doc.go | 34 +++ transport/kp/key_provider.go | 387 ++++++++++++++++++++++++++++++ transport/kp/key_provider_test.go | 89 +++++++ transport/listener.go | 150 ++++++++++++ transport/transport_test.go | 262 ++++++++++++++++++++ 10 files changed, 1318 insertions(+) create mode 100644 transport/ca/cert_provider.go create mode 100644 transport/client.go create mode 100644 transport/core/config.go create mode 100644 transport/core/defs.go create mode 100644 transport/core/roots.go create mode 100644 transport/doc.go create mode 100644 transport/kp/key_provider.go create mode 100644 transport/kp/key_provider_test.go create mode 100644 transport/listener.go create mode 100644 transport/transport_test.go diff --git a/transport/ca/cert_provider.go b/transport/ca/cert_provider.go new file mode 100644 index 000000000..cbf9f7532 --- /dev/null +++ b/transport/ca/cert_provider.go @@ -0,0 +1,16 @@ +// Package ca provides the CertificateAuthority interface for the +// transport package, which provide an interface to get a CSR signed +// by some certificate authority. +package ca + +// A CertificateAuthority is capable of signing certificates given +// certificate signing requests. +type CertificateAuthority interface { + // SignCSR submits a PKCS #10 certificate signing request to a + // CA for signing. + SignCSR(csrPEM []byte) (cert []byte, err error) + + // CACertificate returns the certificate authority's + // certificate. + CACertificate() (cert []byte, err error) +} diff --git a/transport/client.go b/transport/client.go new file mode 100644 index 000000000..a30aeb798 --- /dev/null +++ b/transport/client.go @@ -0,0 +1,239 @@ +package transport + +import ( + "crypto/tls" + "crypto/x509" + "net" + "os" + "time" + + "github.com/cloudflare/cfssl/csr" + "github.com/cloudflare/cfssl/log" + "github.com/cloudflare/cfssl/transport/ca" + "github.com/cloudflare/cfssl/transport/core" + "github.com/cloudflare/cfssl/transport/kp" +) + +func envOrDefault(key, def string) string { + val := os.Getenv(key) + if val == "" { + return def + } + return val +} + +var ( + // NewKeyProvider is the function used to build key providers + // from some identity. + NewKeyProvider = func(id *core.Identity) (kp.KeyProvider, error) { + return kp.NewStandardProvider(id) + } + + // NewCA is used to load a configuration for a certificate + // authority. + NewCA = func(id *core.Identity) (ca.CertificateAuthority, error) { + return ca.NewCFSSLProvider(id, nil) + } +) + +// A Transport is capable of providing transport-layer security using +// TLS. +type Transport struct { + // Before defines how long before the certificate expires the + // transport should start attempting to refresh the + // certificate. For example, if this is 24h, then 24 hours + // before the certificate expires the Transport will start + // attempting to replace it. + Before time.Duration + + // Provider contains a key management provider. + Provider kp.KeyProvider + + // CA contains a mechanism for obtaining signed certificates. + CA ca.CertificateAuthority + + // Roots contains the pool of X.509 certificates that are + // valid for authenticating peers. + Roots *x509.CertPool + + // Identity contains information about the entity that will be + // used to construct certificates. + Identity *core.Identity +} + +// NewTransport builds a new transport from the default +func NewTransport(before time.Duration, identity *core.Identity) (*Transport, error) { + var tr = &Transport{ + Before: before, + Roots: core.SystemRoots, + Identity: identity, + } + + var err error + tr.Provider, err = NewKeyProvider(identity) + if err != nil { + return nil, err + } + + tr.CA, err = NewCA(identity) + if err != nil { + return nil, err + } + + return tr, nil +} + +// Lifespan returns how much time is left before the transport's +// certificate expires, or 0 if the certificate is not present or +// expired. +func (tr *Transport) Lifespan() time.Duration { + cert := tr.Provider.Certificate() + if cert == nil { + return 0 + } + + now := time.Now() + if now.After(cert.NotAfter) { + return 0 + } + + now = now.Add(tr.Before) + ls := cert.NotAfter.Sub(now) + if ls < 0 { + return 0 + } + return ls +} + +// RefreshKeys will make sure the Transport has loaded keys and has a +// valid certificate. It will handle any persistence, check that the +// certificate is valid (i.e. that its expiry date is within the +// Before date), and handle certificate reissuance as needed. +func (tr *Transport) RefreshKeys() (err error) { + if !tr.Provider.Ready() { + log.Debug("key and certificate aren't ready, loading") + err = tr.Provider.Load() + if err != nil && err != kp.ErrCertificateUnavailable { + log.Debugf("failed to load keypair: %v", err) + kr := tr.Identity.Request.KeyRequest + if kr == nil { + kr = csr.NewBasicKeyRequest() + } + + err = tr.Provider.Generate(kr.Algo(), kr.Size()) + if err != nil { + log.Debugf("failed to generate key: %v", err) + return + } + } + } + + lifespan := tr.Lifespan() + if lifespan < tr.Before { + log.Debugf("transport's certificate is out of date (lifespan %s)", lifespan) + req, err := tr.Provider.CertificateRequest(tr.Identity.Request) + if err != nil { + log.Debugf("couldn't get a CSR: %v", err) + return err + } + + log.Debug("requesting certificate from CA") + cert, err := tr.CA.SignCSR(req) + if err != nil { + log.Debugf("failed to get the certificate signed: %v", err) + return err + } + + log.Debug("giving the certificate to the provider") + err = tr.Provider.SetCertificatePEM(cert) + if err != nil { + log.Debugf("failed to set the provider's certificate: %v", err) + return err + } + + log.Debug("storing the certificate") + err = tr.Provider.Store() + if err != nil { + log.Debugf("the provider failed to store the certificate: %v", err) + return err + } + } + + return nil +} + +func (tr *Transport) getCertificate() (cert tls.Certificate, err error) { + if !tr.Provider.Ready() { + log.Debug("transport isn't ready; attempting to refresh keypair") + err = tr.RefreshKeys() + if err != nil { + log.Debugf("transport couldn't get a certificate: %v", err) + return + } + } + + cert, err = tr.Provider.X509KeyPair() + if err != nil { + log.Debugf("couldn't generate an X.509 keypair: %v", err) + } + + return +} + +// Dial initiates a TLS connection to an outbound server. It returns a +// TLS connection to the server. +func Dial(address string, tr *Transport) (*tls.Conn, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + // Assume address is a hostname, and that it should + // use the HTTPS port number. + host = address + address = net.JoinHostPort(address, "443") + } + + cert, err := tr.getCertificate() + if err != nil { + return nil, err + } + + cfg := core.TLSClientAuthClientConfig(cert, host) + return tls.Dial("tcp", address, cfg) +} + +// AutoUpdate will automatically update the listener. If a non-nil +// certUpdates chan is provided, it will receive timestamps for +// reissued certificates. If errChan is non-nil, any errors that occur +// in the updater will be passed along. +func (tr *Transport) AutoUpdate(certUpdates chan time.Time, errChan chan error) { + for { + // Wait until it's time to update the certificate. + target := time.Now().Add(tr.Lifespan()) + if PollInterval == 0 { + <-time.After(tr.Lifespan()) + } else { + pollWait(target) + } + + // Keep trying to update the certificate until it's + // ready. + for { + log.Debugf("attempting to refresh keypair") + err := tr.RefreshKeys() + if err == nil { + break + } + + log.Debug("failed to update certificate, will try again in 5 minutes") + if errChan != nil { + errChan <- err + } + + <-time.After(5 * time.Minute) + } + + log.Debugf("certificate updated") + if certUpdates != nil { + certUpdates <- time.Now() + } + } +} diff --git a/transport/core/config.go b/transport/core/config.go new file mode 100644 index 000000000..fbac2b96d --- /dev/null +++ b/transport/core/config.go @@ -0,0 +1,45 @@ +package core + +import ( + "crypto/tls" + "crypto/x509" +) + +// TLSClientAuthClientConfig returns a new client authentication TLS +// configuration that can be used for a client using client auth +// connecting to the named host. +func TLSClientAuthClientConfig(cert tls.Certificate, host string) *tls.Config { + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: SystemRoots, + ServerName: host, + CipherSuites: CipherSuites, + MinVersion: tls.VersionTLS12, + } +} + +// TLSClientAuthServerConfig returns a new client authentication TLS +// configuration for servers expecting mutually authenticated +// clients. The clientAuth parameter should contain the root pool used +// to authenticate clients. +func TLSClientAuthServerConfig(cert tls.Certificate, clientAuth *x509.CertPool) *tls.Config { + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: SystemRoots, + ClientCAs: clientAuth, + ClientAuth: tls.RequireAndVerifyClientCert, + CipherSuites: CipherSuites, + MinVersion: tls.VersionTLS12, + } +} + +// TLSServerConfig is a general server configuration that should be +// used for non-client authentication purposes, such as HTTPS. +func TLSServerConfig(cert tls.Certificate) *tls.Config { + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: SystemRoots, + CipherSuites: CipherSuites, + MinVersion: tls.VersionTLS12, + } +} diff --git a/transport/core/defs.go b/transport/core/defs.go new file mode 100644 index 000000000..cee44ed4b --- /dev/null +++ b/transport/core/defs.go @@ -0,0 +1,38 @@ +// Package core contains core definitions for the transport package, +// the most salient of which is likely the Identity type. This type is +// used to build a Transport instance. +// +// The TLS configurations provided here are designed for three +// scenarios: mutual authentication for a clients, mutual +// authentication for servers, and a general-purpose server +// configuration applicable where mutual authentication is not +// appropriate. +// +package core + +import ( + "crypto/tls" + "time" + + "github.com/cloudflare/cfssl/csr" +) + +// Identity is used to store information about a particular transport. +type Identity struct { + // Request contains metadata for constructing certificate requests. + Request *csr.CertificateRequest `json:"request"` + + // Profiles contains a dictionary of names to dictionaries; + // this is intended to allow flexibility in supporting + // multiple configurations. + Profiles map[string]map[string]string `json:"profiles"` +} + +// A sensible default is to regenerate certificates the day before they expire. +var DefaultBefore = 24 * time.Hour + +// CipherSuites are the TLS cipher suites that should be used by CloudFlare programs. +var CipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, +} diff --git a/transport/core/roots.go b/transport/core/roots.go new file mode 100644 index 000000000..7a5f99cca --- /dev/null +++ b/transport/core/roots.go @@ -0,0 +1,58 @@ +package core + +import ( + "crypto/x509" + "io/ioutil" +) + +// SystemRoots is the certificate pool containing the system roots. If +// custom roots are needed, they can be loaded with LoadSystemRoots. The +// default value of nil uses the default crypto/x509 system roots. +var SystemRoots *x509.CertPool + +// LoadSystemRoots returns a new certificate pool loaded in manner +// similar to how the system roots in the crypto/x509 package are +// loaded. If certFiles is not empty, it should be a list of paths to a +// PEM-encoded file containing trusted CA roots. The first such file +// that is found will be used. If certDirs is not empty, it should +// contain a list of directories to scan for root certificates. Scanning +// will stop after first directory where at least one certificate is +// loaded. Finally, any additional roots are added to the pool. +func LoadSystemRoots(certFiles, certDirs []string, additional []*x509.Certificate) *x509.CertPool { + roots := x509.NewCertPool() + rootsAdded := false + for _, file := range certFiles { + data, err := ioutil.ReadFile(file) + if err == nil { + roots.AppendCertsFromPEM(data) + rootsAdded = true + break + } + } + + for _, directory := range certDirs { + fis, err := ioutil.ReadDir(directory) + if err != nil { + continue + } + + for _, fi := range fis { + data, err := ioutil.ReadFile(directory + "/" + fi.Name()) + if err == nil && roots.AppendCertsFromPEM(data) { + rootsAdded = true + } + } + + if rootsAdded { + break + } + } + + if rootsAdded { + for _, root := range additional { + roots.AddCert(root) + } + } + + return nil +} diff --git a/transport/doc.go b/transport/doc.go new file mode 100644 index 000000000..4704762ae --- /dev/null +++ b/transport/doc.go @@ -0,0 +1,34 @@ +// Package transport implements functions for facilitating proper TLS-secured +// communications for clients and servers. +// +// Clients should build an identity (of the core.identity) type, such as +// +// var id = &core.Identity{ +// Request: &csr.CertificateRequest{ +// CN: "localhost test certificate", +// }, +// Profiles: map[string]map[string]string{ +// "paths": map[string]string{ +// "private_key": "client.key", +// "certificate": "client.pem", +// }, +// "cfssl": { +// "label": "", +// "profile": "client-ca", +// "remote": "ca.example.net", +// "auth-type": "standard", +// "auth-key": "000102030405060708090a0b0c0d0e0f", +// }, +// }, +// } +// +// +// +// The NewTransport function will return a transport built using the +// NewKeyProvider and NewCA functions. These functions may be changed +// by other packages to provide common key provider and CA +// configurations. Clients can then use Rekey (or launch AutoUpdate in +// a goroutine) to ensure the certificate and key are loaded and +// correct. The Listen and Dial functions then provide the necessary +// connection support. +package transport diff --git a/transport/kp/key_provider.go b/transport/kp/key_provider.go new file mode 100644 index 000000000..53296b27f --- /dev/null +++ b/transport/kp/key_provider.go @@ -0,0 +1,387 @@ +// KeyProviders are used by clients and servers as a mechanism for +// providing keys and signing CSRs. It is a mechanism designed to +// allow switching out how private keys and their associated +// certificates are managed, such as supporting PKCS #11. The +// StandardProvider provides disk-backed PEM-encoded certificates and +// private keys. DiskFallback is a provider that will attempt to +// retrieve the certificate from a CA first, falling back to a +// disk-backed pair. This is useful for test a CA while providing a +// failover solution. +package kp + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + "strings" + + "github.com/cloudflare/cfssl/csr" + "github.com/cloudflare/cfssl/helpers" + "github.com/cloudflare/cfssl/transport/core" +) + +const ( + curveP256 = 256 + curveP384 = 384 + curveP521 = 521 +) + +// A KeyProvider provides some mechanism for managing private keys and +// certificates. It is not required to store the crypto.Signer itself. +type KeyProvider interface { + // Certificate returns the associated certificate, or nil if + // one isn't ready. + Certificate() *x509.Certificate + + // Given some metadata about a certificate request, the + // provider should be able to generate a new CSR. + CertificateRequest(*csr.CertificateRequest) ([]byte, error) + + // Check returns an error if the provider has an invalid setup. + Check() error + + // Generate should trigger the creation of a new private + // key. This will invalidate any certificates stored in the + // key provider. + Generate(algo string, size int) error + + // Load causes a private key and certificate associated with + // this provider to be loaded into memory and be prepared for + // use. + Load() error + + // Persistent returns true if the provider keeps state on disk. + Persistent() bool + + // Ready returns true if the provider has a key and + // certificate. + Ready() bool + + // SetCertificatePEM takes a PEM-encoded certificate and + // associates it with this key provider. + SetCertificatePEM([]byte) error + + // SignCSR allows a templated CSR to be signed. + SignCSR(csr *x509.CertificateRequest) ([]byte, error) + + // Store should perform whatever actions are necessary such + // that a call to Load later will reload the key and + // certificate associated with this provider. + Store() error + + // X509KeyPair returns a tls.Certficate. The returns + // tls.Certificate should have a parsed Leaf certificate. + X509KeyPair() (tls.Certificate, error) +} + +// StandardPaths contains a path to a key file and certificate file. +type StandardPaths struct { + KeyFile string `json:"private_key"` + CertFile string `json:"certificate"` +} + +// StandardProvider provides unencrypted PEM-encoded certificates and +// private keys. If paths are provided, the key and certificate will +// be stored on disk. +type StandardProvider struct { + Paths StandardPaths `json:"paths"` + internal struct { + priv crypto.Signer + cert *x509.Certificate + + // The PEM-encoded private key and certificate. This + // is stored alongside the crypto.Signer and + // x509.Certificate for convenience in marshaling and + // calling tls.X509KeyPair directly. + keyPEM []byte + certPEM []byte + } +} + +// NewStandardProvider sets up new StandardProvider from the +// information contained in an Identity. +func NewStandardProvider(id *core.Identity) (*StandardProvider, error) { + if id == nil { + return nil, errors.New("transport: the identity hasn't been initialised. Has it been loaded from disk?") + } + + paths := id.Profiles["paths"] + if paths == nil { + return &StandardProvider{}, nil + } + + sp := &StandardProvider{ + Paths: StandardPaths{ + KeyFile: paths["private_key"], + CertFile: paths["certificate"], + }, + } + + err := sp.Check() + if err != nil { + return nil, err + } + + return sp, nil +} + +func (sp *StandardProvider) resetCert() { + sp.internal.cert = nil + sp.internal.certPEM = nil +} + +func (sp *StandardProvider) resetKey() { + sp.internal.priv = nil + sp.internal.keyPEM = nil +} + +var ( + // ErrMissingKeyPath is returned if the StandardProvider has + // specified a certificate path but not a key path. + ErrMissingKeyPath = errors.New("transport: standard provider is missing a private key path to accompany the certificate path") + + // ErrMissingCertPath is returned if the StandardProvider has + // specified a private key path but not a certificate path. + ErrMissingCertPath = errors.New("transport: standard provider is missing a certificate path to accompany the certificate path") +) + +// Check ensures that the paths are valid for the provider. +func (sp *StandardProvider) Check() error { + if sp.Paths.KeyFile == "" && sp.Paths.CertFile == "" { + return nil + } + + if sp.Paths.KeyFile == "" { + return ErrMissingKeyPath + } + + if sp.Paths.CertFile == "" { + return ErrMissingCertPath + } + + return nil +} + +// Persistent returns true if the key and certificate will be stored +// on disk. +func (sp *StandardProvider) Persistent() bool { + return sp.Paths.KeyFile != "" && sp.Paths.CertFile != "" +} + +// Generate generates a new private key. +func (sp *StandardProvider) Generate(algo string, size int) (err error) { + sp.resetKey() + sp.resetCert() + + algo = strings.ToLower(algo) + switch algo { + case "rsa": + var priv *rsa.PrivateKey + if size < 2048 { + return errors.New("transport: RSA keys must be at least 2048 bits") + } + + priv, err = rsa.GenerateKey(rand.Reader, size) + if err != nil { + return err + } + + keyPEM := x509.MarshalPKCS1PrivateKey(priv) + p := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: keyPEM, + } + sp.internal.keyPEM = pem.EncodeToMemory(p) + sp.internal.priv = priv + case "ecdsa": + var priv *ecdsa.PrivateKey + var curve elliptic.Curve + switch size { + case curveP256: + curve = elliptic.P256() + case curveP384: + curve = elliptic.P384() + case curveP521: + curve = elliptic.P521() + default: + return errors.New("transport: invalid elliptic curve key size; only 256-, 384-, and 521-bit keys are accepted") + } + + priv, err = ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return err + } + + var keyPEM []byte + keyPEM, err = x509.MarshalECPrivateKey(priv) + if err != nil { + return err + } + + p := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyPEM, + } + sp.internal.keyPEM = pem.EncodeToMemory(p) + + sp.internal.priv = priv + default: + return errors.New("transport: invalid key algorithm; only RSA and ECDSA are supported") + } + + return nil +} + +// Certificate returns the associated certificate, or nil if +// one isn't ready. +func (sp *StandardProvider) Certificate() *x509.Certificate { + return sp.internal.cert +} + +// CertificateRequest takes some metadata about a certificate request, +// and attempts to produce a certificate signing request suitable for +// sending to a certificate authority. +func (sp *StandardProvider) CertificateRequest(req *csr.CertificateRequest) ([]byte, error) { + return csr.Generate(sp.internal.priv, req) +} + +// ErrCertificateUnavailable is returned when a key is available, but +// there is no accompanying certificate. +var ErrCertificateUnavailable = errors.New("transport: certificate unavailable") + +// Load a private key and certificate from disk. +func (sp *StandardProvider) Load() (err error) { + if !sp.Persistent() { + return + } + + var clearKey = true + defer func() { + if err != nil { + if clearKey { + sp.resetKey() + } + sp.resetCert() + } + }() + + sp.internal.keyPEM, err = ioutil.ReadFile(sp.Paths.KeyFile) + if err != nil { + return + } + + sp.internal.priv, err = helpers.ParsePrivateKeyPEM(sp.internal.keyPEM) + if err != nil { + return + } + + clearKey = false + + sp.internal.certPEM, err = ioutil.ReadFile(sp.Paths.CertFile) + if err != nil { + return ErrCertificateUnavailable + } + + sp.internal.cert, err = helpers.ParseCertificatePEM(sp.internal.certPEM) + if err != nil { + err = errors.New("transport: invalid certificate") + return + } + + p, _ := pem.Decode(sp.internal.keyPEM) + + switch sp.internal.cert.PublicKey.(type) { + case *rsa.PublicKey: + if p.Type != "RSA PRIVATE KEY" { + err = errors.New("transport: PEM type " + p.Type + " is invalid for an RSA key") + return + } + case *ecdsa.PublicKey: + if p.Type != "EC PRIVATE KEY" { + err = errors.New("transport: PEM type " + p.Type + " is invalid for an ECDSA key") + return + } + default: + err = errors.New("transport: invalid public key type") + } + + if err != nil { + clearKey = true + return + } + + return nil +} + +// Ready returns true if the provider has a key and certificate +// loaded. The certificate should be checked by the end user for +// validity. +func (sp *StandardProvider) Ready() bool { + switch { + case sp.internal.priv == nil: + return false + case sp.internal.cert == nil: + return false + case sp.internal.keyPEM == nil: + return false + case sp.internal.certPEM == nil: + return false + default: + return true + } +} + +// SetCertificatePEM receives a PEM-encoded certificate and loads it +// into the provider. +func (sp *StandardProvider) SetCertificatePEM(certPEM []byte) error { + cert, err := helpers.ParseCertificatePEM(certPEM) + if err != nil { + return errors.New("transport: invalid certificate") + } + + sp.internal.certPEM = certPEM + sp.internal.cert = cert + return nil +} + +// SignCSR takes a template certificate request and signs it. +func (sp *StandardProvider) SignCSR(tpl *x509.CertificateRequest) ([]byte, error) { + return x509.CreateCertificateRequest(rand.Reader, tpl, sp.internal.priv) +} + +// Store writes the key and certificate to disk, if necessary. +func (sp *StandardProvider) Store() error { + if !sp.Ready() { + return errors.New("transport: provider does not have a key and certificate") + } + + err := ioutil.WriteFile(sp.Paths.CertFile, sp.internal.certPEM, 0644) + if err != nil { + return err + } + + return ioutil.WriteFile(sp.Paths.KeyFile, sp.internal.keyPEM, 0600) +} + +// X509KeyPair returns a tls.Certificate for the provider. +func (sp *StandardProvider) X509KeyPair() (tls.Certificate, error) { + cert, err := tls.X509KeyPair(sp.internal.certPEM, sp.internal.keyPEM) + if err != nil { + return tls.Certificate{}, err + } + + if cert.Leaf == nil { + cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return tls.Certificate{}, err + } + } + return cert, nil +} diff --git a/transport/kp/key_provider_test.go b/transport/kp/key_provider_test.go new file mode 100644 index 000000000..d0c0306ce --- /dev/null +++ b/transport/kp/key_provider_test.go @@ -0,0 +1,89 @@ +package kp + +import ( + "os" + "testing" + + "github.com/cloudflare/cfssl/csr" + "github.com/cloudflare/cfssl/transport/core" +) + +const ( + testKey = "testdata/test.key" + testCert = "testdata/test.pem" +) + +var testIdentity = &core.Identity{ + Request: &csr.CertificateRequest{ + CN: "localhost test certificate", + }, + Profiles: map[string]map[string]string{ + "paths": map[string]string{ + "private_key": testKey, + "certificate": testCert, + }, + }, +} + +func removeIfPresent(path string) error { + if _, err := os.Stat(path); !os.IsNotExist(err) { + return os.Remove(path) + } + return nil +} + +func TestMain(m *testing.M) { + exitCode := m.Run() + + err := removeIfPresent(testKey) + if err == nil { + err = removeIfPresent(testCert) + } + + if err != nil { + os.Exit(1) + } + os.Exit(exitCode) +} + +var kp KeyProvider + +func TestNewStandardProvider(t *testing.T) { + var err error + kp, err = NewStandardProvider(testIdentity) + if err != nil { + t.Fatalf("%v", err) + } + + if kp.Ready() { + t.Fatalf("key provider should not be ready yet") + } + + if err = kp.Check(); err != nil { + t.Fatalf("calling check should return no error") + } + + if nil != kp.Certificate() { + t.Fatal("key provider should not have a certificate yet") + } + + if kp.Ready() { + t.Fatal("key provider should not be ready") + } + + if !kp.Persistent() { + t.Fatal("key provider should be persistent") + } +} + +func TestGenerate(t *testing.T) { + err := kp.Load() + if err == nil { + t.Fatal("key provider shouldn't have a key yet") + } + + err = kp.Generate("ecdsa", 256) + if err != nil { + t.Fatalf("key provider couldn't generate key: %v", err) + } +} diff --git a/transport/listener.go b/transport/listener.go new file mode 100644 index 000000000..000987506 --- /dev/null +++ b/transport/listener.go @@ -0,0 +1,150 @@ +package transport + +import ( + "crypto/tls" + "errors" + "net" + "time" + + "github.com/cloudflare/cfssl/log" + "github.com/cloudflare/cfssl/transport/core" +) + +// A Listener is a TCP network listener for TLS-secured connections. +type Listener struct { + *Transport + config *tls.Config + address string + listener net.Listener +} + +// PollInterval is how often to check whether a new certificate has +// been found. +var PollInterval = 30 * time.Second + +// Listen sets up a new server. If an error is returned, it means +// the server isn't ready to begin listening. +func Listen(address string, tr *Transport) (*Listener, error) { + l := &Listener{ + Transport: tr, + address: address, + } + + var err error + l.config, err = l.getConfig() + if err != nil { + return nil, err + } + + l.listener, err = tls.Listen("tcp", l.address, l.config) + if err != nil { + return nil, err + } + + log.Debug("listener ready") + return l, nil +} + +func pollWait(target time.Time) { + for { + <-time.After(PollInterval) + if time.Now().After(target) { + break + } + } +} + +// AutoUpdate will automatically update the listener. If a non-nil +// certUpdates chan is provided, it will receive timestamps for +// reissued certificates. If errChan is non-nil, any errors that occur +// in the updater will be passed along. +func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { + for { + // Wait until it's time to update the certificate. + target := time.Now().Add(l.Transport.Lifespan()) + if PollInterval == 0 { + <-time.After(l.Transport.Lifespan()) + } else { + pollWait(target) + } + + // Keep trying to update the certificate until it's + // ready. + for { + log.Debug("refreshing certificate") + err := l.Transport.RefreshKeys() + if err == nil { + break + } + + log.Debug("failed to update certificate, will try again in 5 minutes") + if errChan != nil { + errChan <- err + } + + <-time.After(5 * time.Minute) + } + + if certUpdates != nil { + certUpdates <- time.Now() + } + + var err error + l.config, err = l.getConfig() + if err != nil { + log.Debug("immediately after getting a new certificate, the Transport is reporting errors: %v", err) + if errChan != nil { + errChan <- err + } + } + + cert := l.Transport.Provider.Certificate() + log.Debug("listener: auto update of certificate complete") + } +} + +func (l *Listener) getConfig() (*tls.Config, error) { + cert, err := l.Transport.getCertificate() + if err != nil { + return nil, err + } + + if l.Transport.Roots != nil { + return core.TLSClientAuthServerConfig(cert, l.Transport.Roots), nil + } + return core.TLSServerConfig(cert), nil +} + +// Addr returns the server's address. +func (l *Listener) Addr() string { + return l.address +} + +// Close shuts down the listener. +func (l *Listener) Close() error { + l.config = nil + err := l.listener.Close() + l.listener = nil + return err +} + +// Accept waits for and returns the next connection to the listener. +func (l *Listener) Accept() (net.Conn, error) { + if l.config == nil { + log.Debug("listener needs a TLS config") + return nil, errors.New("transport: listener isn't active") + } + + if l.listener == nil { + log.Debug("listener isn't listening") + return nil, errors.New("transport: listener isn't active") + } + + conn, err := l.listener.Accept() + if err != nil { + return nil, err + } + + conn = tls.Server(conn, l.config) + return conn, nil +} diff --git a/transport/transport_test.go b/transport/transport_test.go new file mode 100644 index 000000000..ae55501a1 --- /dev/null +++ b/transport/transport_test.go @@ -0,0 +1,262 @@ +package transport + +import ( + "crypto/x509" + "encoding/json" + "flag" + "os" + "testing" + "time" + + "github.com/cloudflare/cfssl/api/client" + "github.com/cloudflare/cfssl/csr" + "github.com/cloudflare/cfssl/info" + "github.com/cloudflare/cfssl/log" + "github.com/cloudflare/cfssl/transport/core" +) + +var ( + testRemote = envOrDefault("CFSSL_REMOTE", "127.0.0.1:8888") + testLabel = envOrDefault("CFSSL_LABEL", "") + testProfile = envOrDefault("CFSSL_PROFILE", "transport-test") + disableTests bool +) + +func cfsslIsAvailable() bool { + defaultRemote := client.NewServer(testRemote) + + infoReq := info.Req{ + Profile: testProfile, + Label: testLabel, + } + + out, err := json.Marshal(infoReq) + if err != nil { + return false + } + + _, err = defaultRemote.Info(out) + if err != nil { + log.Debug("CFSSL remote is unavailable, skipping tests") + return false + } + + return true +} + +func removeIfPresent(path string) error { + if _, err := os.Stat(path); !os.IsNotExist(err) { + return os.Remove(path) + } + return nil +} + +func TestMain(m *testing.M) { + flag.IntVar(&log.Level, "loglevel", log.LevelInfo, "log level (0 = DEBUG, 4 = ERROR)") + flag.Parse() + if fi, err := os.Stat("testdata"); os.IsNotExist(err) { + err = os.Mkdir("testdata", 0755) + if err != nil { + log.Fatalf("unable to setup testdata directory: %v", err) + } + } else if fi != nil && !fi.Mode().IsDir() { + log.Fatalf("testdata exists but isn't a directory") + } else if err != nil { + log.Fatalf("%v", err) + } + + var exitCode int + if cfsslIsAvailable() { + exitCode = m.Run() + } + + err := removeIfPresent(testKey) + if err == nil { + err = removeIfPresent(testCert) + } + if err != nil { + os.Exit(1) + } + + err = removeIfPresent(testLKey) + if err == nil { + err = removeIfPresent(testLCert) + } + if err != nil { + os.Exit(1) + } + + os.Exit(exitCode) +} + +var ( + tr *Transport + testKey = "testdata/test.key" + testCert = "testdata/test.pem" + testIdentity = &core.Identity{ + Request: &csr.CertificateRequest{ + CN: "localhost test certificate", + }, + Profiles: map[string]map[string]string{ + "paths": map[string]string{ + "private_key": testKey, + "certificate": testCert, + }, + "cfssl": { + "label": testLabel, + "profile": testProfile, + "remote": testRemote, + }, + }, + } +) + +func TestTransportSetup(t *testing.T) { + var before = 55 * time.Second + var err error + + tr, err = NewTransport(before, testIdentity) + if err != nil { + t.Fatalf("failed to set up transport: %v", err) + } +} + +func TestRefreshKeys(t *testing.T) { + err := tr.RefreshKeys() + if err != nil { + t.Fatalf("%v", err) + } +} + +func TestAutoUpdate(t *testing.T) { + // To force a refresh, make sure that the certificate is + // updated 5 seconds from now. + cert := tr.Provider.Certificate() + if cert == nil { + t.Fatal("no certificate from provider") + } + + certUpdates := make(chan time.Time, 0) + errUpdates := make(chan error, 0) + oldBefore := tr.Before + before := cert.NotAfter.Sub(time.Now()) + before -= 5 * time.Second + tr.Before = before + defer func() { + tr.Before = oldBefore + PollInterval = 30 * time.Second + }() + + PollInterval = 2 * time.Second + + go tr.AutoUpdate(certUpdates, errUpdates) + log.Debugf("waiting for certificate update or error from auto updater") + select { + case <-certUpdates: + // Nothing needs to be done + case err := <-errUpdates: + t.Fatalf("%v", err) + case <-time.After(15 * time.Second): + t.Fatal("timeout waiting for update") + } +} + +var ( + l *Listener + testLKey = "testdata/server.key" + testLCert = "testdata/server.pem" + testLIdentity = &core.Identity{ + Request: &csr.CertificateRequest{ + CN: "localhost test certificate", + Hosts: []string{"127.0.0.1"}, + }, + Profiles: map[string]map[string]string{ + "paths": map[string]string{ + "private_key": testLKey, + "certificate": testLCert, + }, + "cfssl": { + "label": testLabel, + "profile": testProfile, + "remote": testRemote, + }, + }, + } +) + +func testListen(t *testing.T) { + log.Debug("listener waiting for connection") + conn, err := l.Accept() + if err != nil { + t.Fatalf("%v", err) + } + + log.Debugf("client has connected") + conn.Write([]byte("hello")) + + conn.Close() +} + +func TestListener(t *testing.T) { + var before = 55 * time.Second + + trl, err := NewTransport(before, testLIdentity) + if err != nil { + t.Fatalf("failed to set up transport: %v", err) + } + + trl.Identity.Request.CN = "localhost test server" + + err = trl.RefreshKeys() + if err != nil { + t.Fatal("%v", err) + } + + core.SystemRoots = x509.NewCertPool() + + caCert, err := tr.CA.CACertificate() + if err != nil { + t.Fatalf("%v", err) + } + if !core.SystemRoots.AppendCertsFromPEM(caCert) { + t.Fatal("no certificates could be added to system roots") + } + + core.SystemRoots.AddCert(tr.Provider.Certificate()) + + l, err = Listen("127.0.0.1:8765", trl) + if err != nil { + t.Fatalf("%v", err) + } + + errChan := make(chan error, 0) + go func() { + err := <-errChan + if err != nil { + t.Fatalf("listener auto update failed: %v", err) + } + }() + + cert := trl.Provider.Certificate() + before = cert.NotAfter.Sub(time.Now()) + before -= 5 * time.Second + + trl.Before = before + go l.AutoUpdate(nil, errChan) + go testListen(t) + + <-time.After(1 * time.Second) + log.Debug("dialer making connection") + conn, err := Dial(tr, "127.0.0.1:8765") + if err != nil { + log.Debugf("certificate time: %s-%s / %s", + trl.Provider.Certificate().NotBefore, + trl.Provider.Certificate().NotAfter, + time.Now().UTC()) + log.Debugf("%#v", trl.Provider.Certificate()) + t.Fatalf("%v", err) + } + log.Debugf("client connected to server") + + conn.Close() +} From b64ce01ec57890ce64fb559a4445baa8252084cf Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 1 Oct 2015 14:18:42 -0700 Subject: [PATCH 02/18] Manually specify ciphersuites. The SHA384 GCM ciphersuites aren't supported in Go 1.4; instead of having version-specified suites, include them as manually-specified suites. The TLS package won't send ciphersuites it doesn't actually support, and there is no compile-time generation of the ciphersuite list. --- transport/core/defs.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/transport/core/defs.go b/transport/core/defs.go index cee44ed4b..d64e629b3 100644 --- a/transport/core/defs.go +++ b/transport/core/defs.go @@ -11,7 +11,6 @@ package core import ( - "crypto/tls" "time" "github.com/cloudflare/cfssl/csr" @@ -33,6 +32,11 @@ var DefaultBefore = 24 * time.Hour // CipherSuites are the TLS cipher suites that should be used by CloudFlare programs. var CipherSuites = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + // These are manually specified because the SHA384 suites are + // not specified in Go 1.4; in Go 1.4, they won't actually + // be sent. + 0xc030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + 0xc02c, // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + 0xc02f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + 0xc02b, // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 } From 503d06f7bd30e1c3b12586dc8c1ce710b115cb43 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 1 Oct 2015 14:44:45 -0700 Subject: [PATCH 03/18] Fix ignored file issue. --- .gitignore | 3 - transport/ca/cfssl_provider.go | 294 +++++++++++++++++++++++++++++++++ transport/kp/key_provider.go | 3 + transport/listener.go | 1 - transport/transport_test.go | 4 +- 5 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 transport/ca/cfssl_provider.go diff --git a/.gitignore b/.gitignore index 9da1c25e0..372849cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -cfssl_* -*-amd64 -*-386 dist/* cli/serve/static.rice-box.go .coverprofile diff --git a/transport/ca/cfssl_provider.go b/transport/ca/cfssl_provider.go new file mode 100644 index 000000000..6d08a3582 --- /dev/null +++ b/transport/ca/cfssl_provider.go @@ -0,0 +1,294 @@ +package ca + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "net" + "path/filepath" + + "github.com/cloudflare/cfssl/api/client" + "github.com/cloudflare/cfssl/auth" + "github.com/cloudflare/cfssl/config" + "github.com/cloudflare/cfssl/info" + "github.com/cloudflare/cfssl/signer" + "github.com/cloudflare/cfssl/transport/core" +) + +type authError struct { + authType string +} + +func (err *authError) Error() string { + return fmt.Sprintf("transport: unsupported CFSSL authentication method %s", err.authType) +} + +// This approach allows us to quickly add other providers later, such +// as the TPM. +var authTypes = map[string]func(config.AuthKey, []byte) (auth.Provider, error){ + "standard": newStandardProvider, +} + +// Create a standard provider without providing any additional data. +func newStandardProvider(ak config.AuthKey, ad []byte) (auth.Provider, error) { + return auth.New(ak.Key, ad) +} + +// Create a new provider from an authentication key and possibly +// additional data. +func newProvider(ak config.AuthKey, ad []byte) (auth.Provider, error) { + // If no auth key was provided, use unauthenticated + // requests. This is useful when a local CFSSL is being used. + if ak.Type == "" && ak.Key == "" { + return nil, nil + } + + f, ok := authTypes[ak.Type] + if !ok { + return nil, &authError{authType: ak.Type} + } + + return f(ak, ad) +} + +// ErrNoAuth is returned when a client is talking to a CFSSL remote +// that is not on a loopback address and doesn't have an +// authentication provider set. +var ErrNoAuth = errors.New("transport: authentication is required for non-local remotes") + +var v4Loopback = net.IPNet{ + IP: net.IP{127, 0, 0, 1}, + Mask: net.IPv4Mask(255, 0, 0, 0), +} + +func ipIsLocal(ip net.IP) bool { + if ip.To4() == nil { + return ip.Equal(net.IPv6loopback) + } + + return v4Loopback.Contains(ip) +} + +// The only time a client should be doing unauthenticated requests is +// when a local CFSSL is being used. +func (cap *CFSSL) validateAuth() error { + // The client is using some form of authentication, and the best way + // to figure out that the auth is invalid is when it's used. Therefore, + // we'll elide checking the credentials until that time. + if cap.provider != nil { + return nil + } + + hosts := cap.remote.Hosts() + for i := range hosts { + ips, err := net.LookupIP(hosts[i]) + if err != nil { + return err + } + + for _, ip := range ips { + if !ipIsLocal(ip) { + return ErrNoAuth + } + } + } + + return nil +} + +var cfsslConfigDirs = []string{ + "/usr/local/cfssl", + "/etc/cfssl", + "/state/etc/cfssl", +} + +// The CFKS standard is to have a configuration file for a label as +// .label. +func findLabel(label string) *config.Config { + for _, dir := range cfsslConfigDirs { + cfgFile := filepath.Join(dir, label+".json") + cfg, err := config.LoadFile(cfgFile) + if err == nil { + return cfg + } + } + + return nil +} + +func getProfile(cfg *config.Config, profileName string) (*config.SigningProfile, bool) { + if cfg == nil || cfg.Signing == nil || cfg.Signing.Default == nil { + return nil, false + } + + var ok bool + profile := cfg.Signing.Default + if profileName != "" { + if cfg.Signing.Profiles == nil { + return nil, false + } + + profile, ok = cfg.Signing.Profiles[profileName] + if !ok { + return nil, false + } + } + + return profile, true +} + +// loadAuth loads an authentication provider from the client config's +// explicitly set auth key. +func (cap *CFSSL) loadAuth() error { + var err error + cap.provider, err = newProvider(cap.DefaultAuth, nil) + return err +} + +func getRemote(cfg *config.Config, profile *config.SigningProfile) (string, bool) { + // NB: Loading the config will validate that the remote is + // present in the config's remote section. + if profile.RemoteServer != "" { + return profile.RemoteServer, true + } + + return "", false +} + +// The client's remote should be set as follows: +// +// 1. If the remote has been explicitly set, honour that. +// +// 2. If the config specifies a remote, honour that. +// +// 3. Use CFKS. +// +// While setting the remote, the authentication provider should be set. +func (cap *CFSSL) setRemoteAndAuth() error { + if cap.Label != "" { + cfsslConfig := findLabel(cap.Label) + profile, ok := getProfile(cfsslConfig, cap.Profile) + if ok { + remote, ok := getRemote(cfsslConfig, profile) + if ok { + cap.remote = client.NewServer(remote) + cap.provider = profile.Provider + return nil + } + + // The profile may not have a remote set, but + // it may have an authentication provider. + cap.provider = profile.Provider + } + } + + cap.remote = cap.DefaultRemote + if cap.provider != nil { + return nil + } + return cap.loadAuth() +} + +// CFSSL provides support for signing certificates via +// CFSSL. +type CFSSL struct { + remote client.Remote + provider auth.Provider + Profile string + Label string + DefaultRemote client.Remote + DefaultAuth config.AuthKey +} + +// SignCSR requests a certificate from a CFSSL signer. +func (cap *CFSSL) SignCSR(csrPEM []byte) (cert []byte, err error) { + p, _ := pem.Decode(csrPEM) + if p == nil || p.Type != "CERTIFICATE REQUEST" { + return nil, errors.New("transport: invalid PEM-encoded certificate signing request") + } + + csr, err := x509.ParseCertificateRequest(p.Bytes) + if err != nil { + return nil, err + } + + hosts := make([]string, 0, len(csr.DNSNames)+len(csr.IPAddresses)) + copy(hosts, csr.DNSNames) + + for i := range csr.IPAddresses { + hosts = append(hosts, csr.IPAddresses[i].String()) + } + + sreq := &signer.SignRequest{ + Hosts: hosts, + Request: string(csrPEM), + Profile: cap.Profile, + Label: cap.Label, + } + + out, err := json.Marshal(sreq) + if err != nil { + return nil, err + } + + if cap.provider != nil { + return cap.remote.AuthSign(out, nil, cap.provider) + } + + return cap.remote.Sign(out) +} + +// CACertificate returns the certificate for a CFSSL CA. +func (cap *CFSSL) CACertificate() ([]byte, error) { + req := &info.Req{ + Label: cap.Label, + Profile: cap.Profile, + } + out, err := json.Marshal(req) + if err != nil { + return nil, err + } + + resp, err := cap.remote.Info(out) + if err != nil { + return nil, err + } + + return []byte(resp.Certificate), nil +} + +// NewCFSSLProvider takes the configuration information from an +// Identity (and an optional default remote), returning a CFSSL +// instance. There should be a profile in id called "cfssl", which +// should contain label and profile fields as needed. +func NewCFSSLProvider(id *core.Identity, defaultRemote client.Remote) (*CFSSL, error) { + if id == nil { + return nil, errors.New("transport: the identity hasn't been initialised. Has it been loaded from disk?") + } + + cap := &CFSSL{ + DefaultRemote: defaultRemote, + } + + cfssl := id.Profiles["cfssl"] + if cfssl != nil { + cap.Label = cfssl["label"] + cap.Profile = cfssl["profile"] + + if cap.DefaultRemote == nil { + cap.DefaultRemote = client.NewServer(cfssl["remote"]) + } + + cap.DefaultAuth.Type = cfssl["auth-type"] + cap.DefaultAuth.Key = cfssl["auth-key"] + } + + err := cap.setRemoteAndAuth() + if err != nil { + return nil, err + } + + return cap, nil +} diff --git a/transport/kp/key_provider.go b/transport/kp/key_provider.go index 53296b27f..5e3cd8274 100644 --- a/transport/kp/key_provider.go +++ b/transport/kp/key_provider.go @@ -1,3 +1,6 @@ +// Package kp describes transport key providers and provides a reference +// implementation. +// // KeyProviders are used by clients and servers as a mechanism for // providing keys and signing CSRs. It is a mechanism designed to // allow switching out how private keys and their associated diff --git a/transport/listener.go b/transport/listener.go index 000987506..03eb98733 100644 --- a/transport/listener.go +++ b/transport/listener.go @@ -98,7 +98,6 @@ func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { } } - cert := l.Transport.Provider.Certificate() log.Debug("listener: auto update of certificate complete") } } diff --git a/transport/transport_test.go b/transport/transport_test.go index ae55501a1..c59326168 100644 --- a/transport/transport_test.go +++ b/transport/transport_test.go @@ -209,7 +209,7 @@ func TestListener(t *testing.T) { err = trl.RefreshKeys() if err != nil { - t.Fatal("%v", err) + t.Fatalf("%v", err) } core.SystemRoots = x509.NewCertPool() @@ -247,7 +247,7 @@ func TestListener(t *testing.T) { <-time.After(1 * time.Second) log.Debug("dialer making connection") - conn, err := Dial(tr, "127.0.0.1:8765") + conn, err := Dial("127.0.0.1:8765", tr) if err != nil { log.Debugf("certificate time: %s-%s / %s", trl.Provider.Certificate().NotBefore, From 0c8d6d3f27236fa97164cc7c9025a9631d554970 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 21 Oct 2015 16:48:11 -0700 Subject: [PATCH 04/18] Implement TrustStore. TrustStore provides a mechanism for obtaining trusted roots. By default, it will use the system roots; otherwise, it will attempt to load certificates from a set of specified roots. --- transport/ca/cfssl_provider.go | 9 - transport/client.go | 84 +++++++- transport/core/config.go | 3 - transport/core/defs.go | 17 ++ transport/core/roots.go | 58 ------ transport/listener.go | 12 +- transport/roots/cfssl.go | 37 ++++ transport/roots/provider.go | 103 ++++++++++ transport/roots/system/root.go | 47 +++++ transport/roots/system/root_bsd.go | 14 ++ transport/roots/system/root_cgo_darwin.go | 82 ++++++++ transport/roots/system/root_darwin.go | 27 +++ transport/roots/system/root_darwin_arm_gen.go | 192 ++++++++++++++++++ transport/roots/system/root_darwin_test.go | 62 ++++++ transport/roots/system/root_linux.go | 13 ++ transport/roots/system/root_nacl.go | 8 + transport/roots/system/root_nocgo_darwin.go | 14 ++ transport/roots/system/root_plan9.go | 31 +++ transport/roots/system/root_solaris.go | 12 ++ transport/roots/system/root_unix.go | 53 +++++ transport/roots/system/root_windows.go | 145 +++++++++++++ 21 files changed, 936 insertions(+), 87 deletions(-) delete mode 100644 transport/core/roots.go create mode 100644 transport/roots/cfssl.go create mode 100644 transport/roots/provider.go create mode 100644 transport/roots/system/root.go create mode 100644 transport/roots/system/root_bsd.go create mode 100644 transport/roots/system/root_cgo_darwin.go create mode 100644 transport/roots/system/root_darwin.go create mode 100644 transport/roots/system/root_darwin_arm_gen.go create mode 100644 transport/roots/system/root_darwin_test.go create mode 100644 transport/roots/system/root_linux.go create mode 100644 transport/roots/system/root_nacl.go create mode 100644 transport/roots/system/root_nocgo_darwin.go create mode 100644 transport/roots/system/root_plan9.go create mode 100644 transport/roots/system/root_solaris.go create mode 100644 transport/roots/system/root_unix.go create mode 100644 transport/roots/system/root_windows.go diff --git a/transport/ca/cfssl_provider.go b/transport/ca/cfssl_provider.go index 6d08a3582..1b4480de5 100644 --- a/transport/ca/cfssl_provider.go +++ b/transport/ca/cfssl_provider.go @@ -157,15 +157,6 @@ func getRemote(cfg *config.Config, profile *config.SigningProfile) (string, bool return "", false } -// The client's remote should be set as follows: -// -// 1. If the remote has been explicitly set, honour that. -// -// 2. If the config specifies a remote, honour that. -// -// 3. Use CFKS. -// -// While setting the remote, the authentication provider should be set. func (cap *CFSSL) setRemoteAndAuth() error { if cap.Label != "" { cfsslConfig := findLabel(cap.Label) diff --git a/transport/client.go b/transport/client.go index a30aeb798..ba17442f9 100644 --- a/transport/client.go +++ b/transport/client.go @@ -2,7 +2,6 @@ package transport import ( "crypto/tls" - "crypto/x509" "net" "os" "time" @@ -12,6 +11,7 @@ import ( "github.com/cloudflare/cfssl/transport/ca" "github.com/cloudflare/cfssl/transport/core" "github.com/cloudflare/cfssl/transport/kp" + "github.com/cloudflare/cfssl/transport/roots" ) func envOrDefault(key, def string) string { @@ -52,24 +52,93 @@ type Transport struct { // CA contains a mechanism for obtaining signed certificates. CA ca.CertificateAuthority - // Roots contains the pool of X.509 certificates that are - // valid for authenticating peers. - Roots *x509.CertPool + // TrustStore contains the certificates trusted by this + // transport. + TrustStore *roots.TrustStore + + // ClientTrustStore contains the certificate authorities to + // use in verifying client authentication certificates. + ClientTrustStore *roots.TrustStore // Identity contains information about the entity that will be // used to construct certificates. Identity *core.Identity } +// TLSClientAuthClientConfig returns a new client authentication TLS +// configuration that can be used for a client using client auth +// connecting to the named host. +func (tr *Transport) TLSClientAuthClientConfig(host string) (*tls.Config, error) { + cert, err := tr.getCertificate() + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: tr.TrustStore.Pool(), + ServerName: host, + CipherSuites: core.CipherSuites, + MinVersion: tls.VersionTLS12, + }, nil +} + +// TLSClientAuthServerConfig returns a new client authentication TLS +// configuration for servers expecting mutually authenticated +// clients. The clientAuth parameter should contain the root pool used +// to authenticate clients. +func (tr *Transport) TLSClientAuthServerConfig() (*tls.Config, error) { + cert, err := tr.getCertificate() + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: tr.TrustStore.Pool(), + ClientCAs: tr.ClientTrustStore.Pool(), + ClientAuth: tls.RequireAndVerifyClientCert, + CipherSuites: core.CipherSuites, + MinVersion: tls.VersionTLS12, + }, nil +} + +// TLSServerConfig is a general server configuration that should be +// used for non-client authentication purposes, such as HTTPS. +func (tr *Transport) TLSServerConfig() (*tls.Config, error) { + cert, err := tr.getCertificate() + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + CipherSuites: core.CipherSuites, + MinVersion: tls.VersionTLS12, + }, nil +} + // NewTransport builds a new transport from the default func NewTransport(before time.Duration, identity *core.Identity) (*Transport, error) { var tr = &Transport{ Before: before, - Roots: core.SystemRoots, Identity: identity, } - var err error + store, err := roots.New(identity.Roots) + if err != nil { + return nil, err + } + tr.TrustStore = store + + if len(identity.ClientRoots) > 0 { + store, err = roots.New(identity.ClientRoots) + if err != nil { + return nil, err + } + tr.ClientTrustStore = store + } + tr.Provider, err = NewKeyProvider(identity) if err != nil { return nil, err @@ -191,12 +260,11 @@ func Dial(address string, tr *Transport) (*tls.Conn, error) { address = net.JoinHostPort(address, "443") } - cert, err := tr.getCertificate() + cfg, err := tr.TLSClientAuthClientConfig(host) if err != nil { return nil, err } - cfg := core.TLSClientAuthClientConfig(cert, host) return tls.Dial("tcp", address, cfg) } diff --git a/transport/core/config.go b/transport/core/config.go index fbac2b96d..8af3d57a1 100644 --- a/transport/core/config.go +++ b/transport/core/config.go @@ -11,7 +11,6 @@ import ( func TLSClientAuthClientConfig(cert tls.Certificate, host string) *tls.Config { return &tls.Config{ Certificates: []tls.Certificate{cert}, - RootCAs: SystemRoots, ServerName: host, CipherSuites: CipherSuites, MinVersion: tls.VersionTLS12, @@ -25,7 +24,6 @@ func TLSClientAuthClientConfig(cert tls.Certificate, host string) *tls.Config { func TLSClientAuthServerConfig(cert tls.Certificate, clientAuth *x509.CertPool) *tls.Config { return &tls.Config{ Certificates: []tls.Certificate{cert}, - RootCAs: SystemRoots, ClientCAs: clientAuth, ClientAuth: tls.RequireAndVerifyClientCert, CipherSuites: CipherSuites, @@ -38,7 +36,6 @@ func TLSClientAuthServerConfig(cert tls.Certificate, clientAuth *x509.CertPool) func TLSServerConfig(cert tls.Certificate) *tls.Config { return &tls.Config{ Certificates: []tls.Certificate{cert}, - RootCAs: SystemRoots, CipherSuites: CipherSuites, MinVersion: tls.VersionTLS12, } diff --git a/transport/core/defs.go b/transport/core/defs.go index d64e629b3..b896d36c9 100644 --- a/transport/core/defs.go +++ b/transport/core/defs.go @@ -16,11 +16,28 @@ import ( "github.com/cloudflare/cfssl/csr" ) +// A Root stores information about a trusted root. +type Root struct { + // Type should contain a string identifier for the type. + Type string `json:"type"` + + // Metadata contains the information needed to load the + // root(s). + Metadata map[string]string `json:"metadata"` +} + // Identity is used to store information about a particular transport. type Identity struct { // Request contains metadata for constructing certificate requests. Request *csr.CertificateRequest `json:"request"` + // Roots contains a list of sources for trusted roots. + Roots []*Root + + // ClientRoots contains a list of sources for trusted client + // certificates. + ClientRoots []*Root + // Profiles contains a dictionary of names to dictionaries; // this is intended to allow flexibility in supporting // multiple configurations. diff --git a/transport/core/roots.go b/transport/core/roots.go deleted file mode 100644 index 7a5f99cca..000000000 --- a/transport/core/roots.go +++ /dev/null @@ -1,58 +0,0 @@ -package core - -import ( - "crypto/x509" - "io/ioutil" -) - -// SystemRoots is the certificate pool containing the system roots. If -// custom roots are needed, they can be loaded with LoadSystemRoots. The -// default value of nil uses the default crypto/x509 system roots. -var SystemRoots *x509.CertPool - -// LoadSystemRoots returns a new certificate pool loaded in manner -// similar to how the system roots in the crypto/x509 package are -// loaded. If certFiles is not empty, it should be a list of paths to a -// PEM-encoded file containing trusted CA roots. The first such file -// that is found will be used. If certDirs is not empty, it should -// contain a list of directories to scan for root certificates. Scanning -// will stop after first directory where at least one certificate is -// loaded. Finally, any additional roots are added to the pool. -func LoadSystemRoots(certFiles, certDirs []string, additional []*x509.Certificate) *x509.CertPool { - roots := x509.NewCertPool() - rootsAdded := false - for _, file := range certFiles { - data, err := ioutil.ReadFile(file) - if err == nil { - roots.AppendCertsFromPEM(data) - rootsAdded = true - break - } - } - - for _, directory := range certDirs { - fis, err := ioutil.ReadDir(directory) - if err != nil { - continue - } - - for _, fi := range fis { - data, err := ioutil.ReadFile(directory + "/" + fi.Name()) - if err == nil && roots.AppendCertsFromPEM(data) { - rootsAdded = true - } - } - - if rootsAdded { - break - } - } - - if rootsAdded { - for _, root := range additional { - roots.AddCert(root) - } - } - - return nil -} diff --git a/transport/listener.go b/transport/listener.go index 03eb98733..d9082805a 100644 --- a/transport/listener.go +++ b/transport/listener.go @@ -7,7 +7,6 @@ import ( "time" "github.com/cloudflare/cfssl/log" - "github.com/cloudflare/cfssl/transport/core" ) // A Listener is a TCP network listener for TLS-secured connections. @@ -103,15 +102,10 @@ func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { } func (l *Listener) getConfig() (*tls.Config, error) { - cert, err := l.Transport.getCertificate() - if err != nil { - return nil, err - } - - if l.Transport.Roots != nil { - return core.TLSClientAuthServerConfig(cert, l.Transport.Roots), nil + if l.Transport.ClientTrustStore != nil { + return l.Transport.TLSClientAuthServerConfig() } - return core.TLSServerConfig(cert), nil + return l.Transport.TLSServerConfig() } // Addr returns the server's address. diff --git a/transport/roots/cfssl.go b/transport/roots/cfssl.go new file mode 100644 index 000000000..c8bfce4da --- /dev/null +++ b/transport/roots/cfssl.go @@ -0,0 +1,37 @@ +package roots + +import ( + "crypto/x509" + "encoding/json" + "errors" + + "github.com/cloudflare/cfssl/api/client" + "github.com/cloudflare/cfssl/helpers" + "github.com/cloudflare/cfssl/info" +) + +// This package contains CFSSL integration. + +// NewCFSSL produces a new CFSSL root. +func NewCFSSL(metadata map[string]string) ([]*x509.Certificate, error) { + host, ok := metadata["host"] + if !ok { + return nil, errors.New("transport: CFSSL root provider requires a host") + } + + label := metadata["label"] + profile := metadata["profile"] + + srv := client.NewServer(host) + data, err := json.Marshal(info.Req{Label: label, Profile: profile}) + if err != nil { + return nil, err + } + + resp, err := srv.Info(data) + if err != nil { + return nil, err + } + + return helpers.ParseCertificatesPEM([]byte(resp.Certificate)) +} diff --git a/transport/roots/provider.go b/transport/roots/provider.go new file mode 100644 index 000000000..cbaffccf6 --- /dev/null +++ b/transport/roots/provider.go @@ -0,0 +1,103 @@ +// Package roots includes support for loading trusted roots from +// various sources. +package roots + +import ( + "crypto/sha256" + "crypto/x509" + "errors" + + "github.com/cloudflare/cfssl/transport/core" + "github.com/cloudflare/cfssl/transport/roots/system" +) + +var Providers = map[string]func(map[string]string) ([]*x509.Certificate, error){ + "system-roots": system.New, + "cfssl": NewCFSSL, +} + +// A TrustStore contains a pool of certificate that are trusted for a +// given TLS configuration. +type TrustStore struct { + roots map[string]*x509.Certificate +} + +// Pool returns a certificate pool containing the certificates +// loaded into the provider. +func (ts *TrustStore) Pool() *x509.CertPool { + var pool = x509.NewCertPool() + for _, cert := range ts.roots { + pool.AddCert(cert) + } + return pool +} + +// Certificates returns a slice of the loaded certificates. +func (ts *TrustStore) Certificates() []*x509.Certificate { + var roots = make([]*x509.Certificate, 0, len(ts.roots)) + for _, cert := range ts.roots { + roots = append(roots, cert) + } + return roots +} + +func (ts *TrustStore) addCerts(certs []*x509.Certificate) { + if ts.roots == nil { + ts.roots = map[string]*x509.Certificate{} + } + + for _, cert := range certs { + digest := sha256.Sum256(cert.Raw) + ts.roots[string(digest[:])] = cert + } +} + +type Trusted interface { + // Certificates returns a slice containing the certificates + // that are loaded into the provider. + Certificates() []*x509.Certificate + + // AddCert adds a new certificate into the certificate pool. + AddCert(cert *x509.Certificate) + + // AddPEM adds a one or more PEM-encoded certificates into the + // certificate pool. + AddPEM(cert []byte) bool +} + +// New produces a new trusted root provider from a collection of +// roots. If there are no roots, the system roots will be used. +func New(rootDefs []*core.Root) (*TrustStore, error) { + var err error + + var store = &TrustStore{} + var roots []*x509.Certificate + + if len(rootDefs) == 0 { + roots, err = system.New(nil) + if err != nil { + return nil, err + } + + store.addCerts(roots) + return store, nil + } + + err = errors.New("transport: no supported root providers found") + for _, root := range rootDefs { + pfn, ok := Providers[root.Type] + if ok { + roots, err = pfn(root.Metadata) + if err != nil { + break + } + + store.addCerts(roots) + } + } + + if err != nil { + store = nil + } + return store, err +} diff --git a/transport/roots/system/root.go b/transport/roots/system/root.go new file mode 100644 index 000000000..7c463affb --- /dev/null +++ b/transport/roots/system/root.go @@ -0,0 +1,47 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package system + +import ( + "crypto/x509" + "encoding/pem" + "errors" +) + +func appendPEM(roots []*x509.Certificate, pemCerts []byte) ([]*x509.Certificate, bool) { + var ok bool + + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + continue + } + + roots = append(roots, cert) + ok = true + } + + return roots, ok +} + +// New returns a new certificate pool loaded with the system +// roots. The provided argument is not used; it is included for +// compatibility with other functions. +func New(metadata map[string]string) ([]*x509.Certificate, error) { + roots := initSystemRoots() + if len(roots) == 0 { + return nil, errors.New("transport: unable to find system roots") + } + return roots, nil +} diff --git a/transport/roots/system/root_bsd.go b/transport/roots/system/root_bsd.go new file mode 100644 index 000000000..a3b5bc0e7 --- /dev/null +++ b/transport/roots/system/root_bsd.go @@ -0,0 +1,14 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build dragonfly freebsd netbsd openbsd + +package system + +// Possible certificate files; stop after finding one. +var certFiles = []string{ + "/usr/local/share/certs/ca-root-nss.crt", // FreeBSD/DragonFly + "/etc/ssl/cert.pem", // OpenBSD + "/etc/openssl/certs/ca-certificates.crt", // NetBSD +} diff --git a/transport/roots/system/root_cgo_darwin.go b/transport/roots/system/root_cgo_darwin.go new file mode 100644 index 000000000..fba091593 --- /dev/null +++ b/transport/roots/system/root_cgo_darwin.go @@ -0,0 +1,82 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build cgo,!arm,!arm64,!ios + +package system + +/* +#cgo CFLAGS: -mmacosx-version-min=10.6 -D__MAC_OS_X_VERSION_MAX_ALLOWED=1060 +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include + +// FetchPEMRoots fetches the system's list of trusted X.509 root certificates. +// +// On success it returns 0 and fills pemRoots with a CFDataRef that contains the extracted root +// certificates of the system. On failure, the function returns -1. +// +// Note: The CFDataRef returned in pemRoots must be released (using CFRelease) after +// we've consumed its content. +int FetchPEMRoots(CFDataRef *pemRoots) { + if (pemRoots == NULL) { + return -1; + } + + CFArrayRef certs = NULL; + OSStatus err = SecTrustCopyAnchorCertificates(&certs); + if (err != noErr) { + return -1; + } + + CFMutableDataRef combinedData = CFDataCreateMutable(kCFAllocatorDefault, 0); + int i, ncerts = CFArrayGetCount(certs); + for (i = 0; i < ncerts; i++) { + CFDataRef data = NULL; + SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(certs, i); + if (cert == NULL) { + continue; + } + + // Note: SecKeychainItemExport is deprecated as of 10.7 in favor of SecItemExport. + // Once we support weak imports via cgo we should prefer that, and fall back to this + // for older systems. + err = SecKeychainItemExport(cert, kSecFormatX509Cert, kSecItemPemArmour, NULL, &data); + if (err != noErr) { + continue; + } + + if (data != NULL) { + CFDataAppendBytes(combinedData, CFDataGetBytePtr(data), CFDataGetLength(data)); + CFRelease(data); + } + } + + CFRelease(certs); + + *pemRoots = combinedData; + return 0; +} +*/ +import "C" +import ( + "crypto/x509" + "unsafe" +) + +func initSystemRoots() []*x509.Certificate { + var roots []*x509.Certificate + + var data C.CFDataRef = nil + err := C.FetchPEMRoots(&data) + if err == -1 { + return nil + } + + defer C.CFRelease(C.CFTypeRef(data)) + buf := C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(data)), C.int(C.CFDataGetLength(data))) + roots, _ = appendPEM(roots, buf) + return roots +} diff --git a/transport/roots/system/root_darwin.go b/transport/roots/system/root_darwin.go new file mode 100644 index 000000000..f81301174 --- /dev/null +++ b/transport/roots/system/root_darwin.go @@ -0,0 +1,27 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:generate go run root_darwin_arm_gen.go -output root_darwin_armx.go + +package system + +import ( + "crypto/x509" + "errors" + "os/exec" +) + +func execSecurityRoots() ([]*x509.Certificate, error) { + cmd := exec.Command("/usr/bin/security", "find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain") + data, err := cmd.Output() + if err != nil { + return nil, err + } + + roots, ok := appendPEM(nil, data) + if !ok { + return nil, errors.New("transport: no system roots found") + } + return roots, nil +} diff --git a/transport/roots/system/root_darwin_arm_gen.go b/transport/roots/system/root_darwin_arm_gen.go new file mode 100644 index 000000000..2cdbdff7a --- /dev/null +++ b/transport/roots/system/root_darwin_arm_gen.go @@ -0,0 +1,192 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ignore + +// Generates root_darwin_armx.go. +// +// As of iOS 8, there is no API for querying the system trusted X.509 root +// certificates. We could use SecTrustEvaluate to verify that a trust chain +// exists for a certificate, but the x509 API requires returning the entire +// chain. +// +// Apple publishes the list of trusted root certificates for iOS on +// support.apple.com. So we parse the list and extract the certificates from +// an OS X machine and embed them into the x509 package. +package main + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "flag" + "fmt" + "go/format" + "io/ioutil" + "log" + "math/big" + "net/http" + "os/exec" + "strings" +) + +var output = flag.String("output", "root_darwin_armx.go", "file name to write") + +func main() { + certs, err := selectCerts() + if err != nil { + log.Fatal(err) + } + + buf := new(bytes.Buffer) + + fmt.Fprintf(buf, "// Created by root_darwin_arm_gen --output %s; DO NOT EDIT\n", *output) + fmt.Fprintf(buf, "%s", header) + + fmt.Fprintf(buf, "const systemRootsPEM = `\n") + for _, cert := range certs { + b := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + if err := pem.Encode(buf, b); err != nil { + log.Fatal(err) + } + } + fmt.Fprintf(buf, "`") + + source, err := format.Source(buf.Bytes()) + if err != nil { + log.Fatal("source format error:", err) + } + if err := ioutil.WriteFile(*output, source, 0644); err != nil { + log.Fatal(err) + } +} + +func selectCerts() ([]*x509.Certificate, error) { + ids, err := fetchCertIDs() + if err != nil { + return nil, err + } + + scerts, err := sysCerts() + if err != nil { + return nil, err + } + + var certs []*x509.Certificate + for _, id := range ids { + sn, ok := big.NewInt(0).SetString(id.serialNumber, 0) // 0x prefix selects hex + if !ok { + return nil, fmt.Errorf("invalid serial number: %q", id.serialNumber) + } + ski, ok := big.NewInt(0).SetString(id.subjectKeyID, 0) + if !ok { + return nil, fmt.Errorf("invalid Subject Key ID: %q", id.subjectKeyID) + } + + for _, cert := range scerts { + if sn.Cmp(cert.SerialNumber) != 0 { + continue + } + cski := big.NewInt(0).SetBytes(cert.SubjectKeyId) + if ski.Cmp(cski) != 0 { + continue + } + certs = append(certs, cert) + break + } + } + return certs, nil +} + +func sysCerts() (certs []*x509.Certificate, err error) { + cmd := exec.Command("/usr/bin/security", "find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain") + data, err := cmd.Output() + if err != nil { + return nil, err + } + for len(data) > 0 { + var block *pem.Block + block, data = pem.Decode(data) + if block == nil { + break + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + continue + } + certs = append(certs, cert) + } + return certs, nil +} + +type certID struct { + serialNumber string + subjectKeyID string +} + +// fetchCertIDs fetches IDs of iOS X509 certificates from apple.com. +func fetchCertIDs() ([]certID, error) { + resp, err := http.Get("https://support.apple.com/en-us/HT204132") + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + text := string(body) + text = text[strings.Index(text, "
")] + + lines := strings.Split(text, "\n") + var ids []certID + var id certID + for i, ln := range lines { + if i == len(lines)-1 { + break + } + const sn = "Serial Number:" + if ln == sn { + id.serialNumber = "0x" + strings.Replace(strings.TrimSpace(lines[i+1]), ":", "", -1) + continue + } + if strings.HasPrefix(ln, sn) { + // extract hex value from parentheses. + id.serialNumber = ln[strings.Index(ln, "(")+1 : len(ln)-1] + continue + } + if strings.TrimSpace(ln) == "X509v3 Subject Key Identifier:" { + id.subjectKeyID = "0x" + strings.Replace(strings.TrimSpace(lines[i+1]), ":", "", -1) + ids = append(ids, id) + id = certID{} + } + } + return ids, nil +} + +const header = ` +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build cgo +// +build darwin +// +build arm arm64 + +package system + +func initSystemRoots() []*x509.Certificate { + var roots []*x509.Certificate + roots, _ = appendPEM([]byte(systemRootsPEM)) + return roots +} +` diff --git a/transport/roots/system/root_darwin_test.go b/transport/roots/system/root_darwin_test.go new file mode 100644 index 000000000..445f68424 --- /dev/null +++ b/transport/roots/system/root_darwin_test.go @@ -0,0 +1,62 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package system + +import ( + "runtime" + "testing" +) + +func TestSystemRoots(t *testing.T) { + switch runtime.GOARCH { + case "arm", "arm64": + t.Skipf("skipping on %s/%s, no system root", runtime.GOOS, runtime.GOARCH) + } + + sysRoots := systemRootsPool() // actual system roots + execRoots, err := execSecurityRoots() // non-cgo roots + + if err != nil { + t.Fatalf("failed to read system roots: %v", err) + } + + for _, tt := range []*CertPool{sysRoots, execRoots} { + if tt == nil { + t.Fatal("no system roots") + } + // On Mavericks, there are 212 bundled certs; require only + // 150 here, since this is just a sanity check, and the + // exact number will vary over time. + if want, have := 150, len(tt.certs); have < want { + t.Fatalf("want at least %d system roots, have %d", want, have) + } + } + + // Check that the two cert pools are roughly the same; + // |A∩B| > max(|A|, |B|) / 2 should be a reasonably robust check. + + isect := make(map[string]bool, len(sysRoots.certs)) + for _, c := range sysRoots.certs { + isect[string(c.Raw)] = true + } + + have := 0 + for _, c := range execRoots.certs { + if isect[string(c.Raw)] { + have++ + } + } + + var want int + if nsys, nexec := len(sysRoots.certs), len(execRoots.certs); nsys > nexec { + want = nsys / 2 + } else { + want = nexec / 2 + } + + if have < want { + t.Errorf("insufficent overlap between cgo and non-cgo roots; want at least %d, have %d", want, have) + } +} diff --git a/transport/roots/system/root_linux.go b/transport/roots/system/root_linux.go new file mode 100644 index 000000000..7892e12e5 --- /dev/null +++ b/transport/roots/system/root_linux.go @@ -0,0 +1,13 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package system + +// Possible certificate files; stop after finding one. +var certFiles = []string{ + "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. + "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL + "/etc/ssl/ca-bundle.pem", // OpenSUSE + "/etc/pki/tls/cacert.pem", // OpenELEC +} diff --git a/transport/roots/system/root_nacl.go b/transport/roots/system/root_nacl.go new file mode 100644 index 000000000..b1e278af0 --- /dev/null +++ b/transport/roots/system/root_nacl.go @@ -0,0 +1,8 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package system + +// Possible certificate files; stop after finding one. +var certFiles = []string{} diff --git a/transport/roots/system/root_nocgo_darwin.go b/transport/roots/system/root_nocgo_darwin.go new file mode 100644 index 000000000..c0a976e9b --- /dev/null +++ b/transport/roots/system/root_nocgo_darwin.go @@ -0,0 +1,14 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !cgo + +package system + +import "crypto/x509" + +func initSystemRoots() []*x509.Certificate { + roots, _ := execSecurityRoots() + return roots +} diff --git a/transport/roots/system/root_plan9.go b/transport/roots/system/root_plan9.go new file mode 100644 index 000000000..d6e511c8f --- /dev/null +++ b/transport/roots/system/root_plan9.go @@ -0,0 +1,31 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build plan9 + +package system + +import ( + "crypto/x509" + "io/ioutil" +) + +// Possible certificate files; stop after finding one. +var certFiles = []string{ + "/sys/lib/tls/ca.pem", +} + +func initSystemRoots() (roots []*x509.Certificate) { + for _, file := range certFiles { + data, err := ioutil.ReadFile(file) + if err == nil { + roots, _ = appendPEM(roots, data) + return + } + } + + // All of the files failed to load. systemRoots will be nil which will + // trigger a specific error at verification time. + return nil +} diff --git a/transport/roots/system/root_solaris.go b/transport/roots/system/root_solaris.go new file mode 100644 index 000000000..35451111f --- /dev/null +++ b/transport/roots/system/root_solaris.go @@ -0,0 +1,12 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package system + +// Possible certificate files; stop after finding one. +var certFiles = []string{ + "/etc/certs/ca-certificates.crt", // Solaris 11.2+ + "/etc/ssl/certs/ca-certificates.crt", // Joyent SmartOS + "/etc/ssl/cacert.pem", // OmniOS +} diff --git a/transport/roots/system/root_unix.go b/transport/roots/system/root_unix.go new file mode 100644 index 000000000..76dd8551e --- /dev/null +++ b/transport/roots/system/root_unix.go @@ -0,0 +1,53 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build dragonfly freebsd linux nacl netbsd openbsd solaris + +package system + +import ( + "crypto/x509" + "io/ioutil" +) + +// Possible directories with certificate files; stop after successfully +// reading at least one file from a directory. +var certDirectories = []string{ + "/system/etc/security/cacerts", // Android +} + +func initSystemRoots() []*x509.Certificate { + var roots []*x509.Certificate + for _, file := range certFiles { + data, err := ioutil.ReadFile(file) + if err == nil { + roots, _ = appendPEM(roots, data) + return roots + } + } + + for _, directory := range certDirectories { + fis, err := ioutil.ReadDir(directory) + if err != nil { + continue + } + rootsAdded := false + for _, fi := range fis { + var ok bool + data, err := ioutil.ReadFile(directory + "/" + fi.Name()) + if err == nil { + if roots, ok = appendPEM(roots, data); ok { + rootsAdded = true + } + } + } + if rootsAdded { + return roots + } + } + + // All of the files failed to load. systemRoots will be nil which will + // trigger a specific error at verification time. + return nil +} diff --git a/transport/roots/system/root_windows.go b/transport/roots/system/root_windows.go new file mode 100644 index 000000000..0ff5aab3b --- /dev/null +++ b/transport/roots/system/root_windows.go @@ -0,0 +1,145 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package system + +import ( + "crypto/x509" + "errors" + "syscall" + "unsafe" +) + +// Creates a new *syscall.CertContext representing the leaf certificate in an in-memory +// certificate store containing itself and all of the intermediate certificates specified +// in the opts.Intermediates CertPool. +// +// A pointer to the in-memory store is available in the returned CertContext's Store field. +// The store is automatically freed when the CertContext is freed using +// syscall.CertFreeCertificateContext. +func createStoreContext(leaf *Certificate, opts *VerifyOptions) (*syscall.CertContext, error) { + var storeCtx *syscall.CertContext + + leafCtx, err := syscall.CertCreateCertificateContext(syscall.X509_ASN_ENCODING|syscall.PKCS_7_ASN_ENCODING, &leaf.Raw[0], uint32(len(leaf.Raw))) + if err != nil { + return nil, err + } + defer syscall.CertFreeCertificateContext(leafCtx) + + handle, err := syscall.CertOpenStore(syscall.CERT_STORE_PROV_MEMORY, 0, 0, syscall.CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG, 0) + if err != nil { + return nil, err + } + defer syscall.CertCloseStore(handle, 0) + + err = syscall.CertAddCertificateContextToStore(handle, leafCtx, syscall.CERT_STORE_ADD_ALWAYS, &storeCtx) + if err != nil { + return nil, err + } + + if opts.Intermediates != nil { + for _, intermediate := range opts.Intermediates.certs { + ctx, err := syscall.CertCreateCertificateContext(syscall.X509_ASN_ENCODING|syscall.PKCS_7_ASN_ENCODING, &intermediate.Raw[0], uint32(len(intermediate.Raw))) + if err != nil { + return nil, err + } + + err = syscall.CertAddCertificateContextToStore(handle, ctx, syscall.CERT_STORE_ADD_ALWAYS, nil) + syscall.CertFreeCertificateContext(ctx) + if err != nil { + return nil, err + } + } + } + + return storeCtx, nil +} + +// extractSimpleChain extracts the final certificate chain from a CertSimpleChain. +func extractSimpleChain(simpleChain **syscall.CertSimpleChain, count int) (chain []*Certificate, err error) { + if simpleChain == nil || count == 0 { + return nil, errors.New("x509: invalid simple chain") + } + + simpleChains := (*[1 << 20]*syscall.CertSimpleChain)(unsafe.Pointer(simpleChain))[:] + lastChain := simpleChains[count-1] + elements := (*[1 << 20]*syscall.CertChainElement)(unsafe.Pointer(lastChain.Elements))[:] + for i := 0; i < int(lastChain.NumElements); i++ { + // Copy the buf, since ParseCertificate does not create its own copy. + cert := elements[i].CertContext + encodedCert := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:] + buf := make([]byte, cert.Length) + copy(buf, encodedCert[:]) + parsedCert, err := ParseCertificate(buf) + if err != nil { + return nil, err + } + chain = append(chain, parsedCert) + } + + return chain, nil +} + +// checkChainTrustStatus checks the trust status of the certificate chain, translating +// any errors it finds into Go errors in the process. +func checkChainTrustStatus(c *Certificate, chainCtx *syscall.CertChainContext) error { + if chainCtx.TrustStatus.ErrorStatus != syscall.CERT_TRUST_NO_ERROR { + status := chainCtx.TrustStatus.ErrorStatus + switch status { + case syscall.CERT_TRUST_IS_NOT_TIME_VALID: + return CertificateInvalidError{c, Expired} + default: + return UnknownAuthorityError{c, nil, nil} + } + } + return nil +} + +// checkChainSSLServerPolicy checks that the certificate chain in chainCtx is valid for +// use as a certificate chain for a SSL/TLS server. +func checkChainSSLServerPolicy(c *Certificate, chainCtx *syscall.CertChainContext, opts *VerifyOptions) error { + servernamep, err := syscall.UTF16PtrFromString(opts.DNSName) + if err != nil { + return err + } + sslPara := &syscall.SSLExtraCertChainPolicyPara{ + AuthType: syscall.AUTHTYPE_SERVER, + ServerName: servernamep, + } + sslPara.Size = uint32(unsafe.Sizeof(*sslPara)) + + para := &syscall.CertChainPolicyPara{ + ExtraPolicyPara: uintptr(unsafe.Pointer(sslPara)), + } + para.Size = uint32(unsafe.Sizeof(*para)) + + status := syscall.CertChainPolicyStatus{} + err = syscall.CertVerifyCertificateChainPolicy(syscall.CERT_CHAIN_POLICY_SSL, chainCtx, para, &status) + if err != nil { + return err + } + + // TODO(mkrautz): use the lChainIndex and lElementIndex fields + // of the CertChainPolicyStatus to provide proper context, instead + // using c. + if status.Error != 0 { + switch status.Error { + case syscall.CERT_E_EXPIRED: + return CertificateInvalidError{c, Expired} + case syscall.CERT_E_CN_NO_MATCH: + return HostnameError{c, opts.DNSName} + case syscall.CERT_E_UNTRUSTEDROOT: + return UnknownAuthorityError{c, nil, nil} + default: + return UnknownAuthorityError{c, nil, nil} + } + } + + return nil +} + +// Note(kyle): not sure how this works on windows, or if this does. +func initSystemRoots() []*x509.Certificate { + return nil +} From dde1e3669fb2b3e30daedba1a4fc4c24e56ebf3b Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 21 Oct 2015 17:00:25 -0700 Subject: [PATCH 05/18] Update test to match new TrustStore. --- transport/roots/provider.go | 4 ++-- transport/transport_test.go | 39 ++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/transport/roots/provider.go b/transport/roots/provider.go index cbaffccf6..0af26f33f 100644 --- a/transport/roots/provider.go +++ b/transport/roots/provider.go @@ -12,8 +12,8 @@ import ( ) var Providers = map[string]func(map[string]string) ([]*x509.Certificate, error){ - "system-roots": system.New, - "cfssl": NewCFSSL, + "system": system.New, + "cfssl": NewCFSSL, } // A TrustStore contains a pool of certificate that are trusted for a diff --git a/transport/transport_test.go b/transport/transport_test.go index c59326168..17ad2a58e 100644 --- a/transport/transport_test.go +++ b/transport/transport_test.go @@ -1,7 +1,6 @@ package transport import ( - "crypto/x509" "encoding/json" "flag" "os" @@ -97,6 +96,19 @@ var ( Request: &csr.CertificateRequest{ CN: "localhost test certificate", }, + Roots: []*core.Root{ + &core.Root{ + Type: "system", + }, + &core.Root{ + Type: "cfssl", + Metadata: map[string]string{ + "host": testRemote, + "label": testLabel, + "profile": testProfile, + }, + }, + }, Profiles: map[string]map[string]string{ "paths": map[string]string{ "private_key": testKey, @@ -181,6 +193,19 @@ var ( "remote": testRemote, }, }, + Roots: []*core.Root{ + &core.Root{ + Type: "system", + }, + &core.Root{ + Type: "cfssl", + Metadata: map[string]string{ + "host": testRemote, + "label": testLabel, + "profile": testProfile, + }, + }, + }, } ) @@ -212,18 +237,6 @@ func TestListener(t *testing.T) { t.Fatalf("%v", err) } - core.SystemRoots = x509.NewCertPool() - - caCert, err := tr.CA.CACertificate() - if err != nil { - t.Fatalf("%v", err) - } - if !core.SystemRoots.AppendCertsFromPEM(caCert) { - t.Fatal("no certificates could be added to system roots") - } - - core.SystemRoots.AddCert(tr.Provider.Certificate()) - l, err = Listen("127.0.0.1:8765", trl) if err != nil { t.Fatalf("%v", err) From acae57aac95ce7918ab6c85b4280a3b8b7dc1eec Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 23 Oct 2015 16:49:33 -0700 Subject: [PATCH 06/18] Initial transport examples. --- api/client/client.go | 4 +- transport/client.go | 3 +- transport/core/defs.go | 4 +- transport/example/ca.json | 15 +++++ transport/example/config.json | 25 +++++++ transport/example/genca.sh | 4 ++ transport/example/maclient/client.go | 70 ++++++++++++++++++++ transport/example/maclient/client.json | 28 ++++++++ transport/example/maserver/server.go | 90 ++++++++++++++++++++++++++ transport/example/maserver/server.json | 26 ++++++++ transport/listener.go | 2 + 11 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 transport/example/ca.json create mode 100644 transport/example/config.json create mode 100755 transport/example/genca.sh create mode 100644 transport/example/maclient/client.go create mode 100644 transport/example/maclient/client.json create mode 100644 transport/example/maserver/server.go create mode 100644 transport/example/maserver/server.json diff --git a/api/client/client.go b/api/client/client.go index 6e133ff93..c34d2d8f5 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -202,10 +202,10 @@ func (srv *server) Info(jsonData []byte) (*info.Resp, error) { info.Certificate = val.(string) } var usages []interface{} - if val, ok := res["usages"]; ok { + if val, ok := res["usages"]; ok && val != nil { usages = val.([]interface{}) } - if val, ok := res["expiry"]; ok { + if val, ok := res["expiry"]; ok && val != nil { info.ExpiryString = val.(string) } diff --git a/transport/client.go b/transport/client.go index ba17442f9..e51e9289f 100644 --- a/transport/client.go +++ b/transport/client.go @@ -80,6 +80,7 @@ func (tr *Transport) TLSClientAuthClientConfig(host string) (*tls.Config, error) ServerName: host, CipherSuites: core.CipherSuites, MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequireAndVerifyClientCert, }, nil } @@ -119,7 +120,7 @@ func (tr *Transport) TLSServerConfig() (*tls.Config, error) { } // NewTransport builds a new transport from the default -func NewTransport(before time.Duration, identity *core.Identity) (*Transport, error) { +func New(before time.Duration, identity *core.Identity) (*Transport, error) { var tr = &Transport{ Before: before, Identity: identity, diff --git a/transport/core/defs.go b/transport/core/defs.go index b896d36c9..aebd9c897 100644 --- a/transport/core/defs.go +++ b/transport/core/defs.go @@ -32,11 +32,11 @@ type Identity struct { Request *csr.CertificateRequest `json:"request"` // Roots contains a list of sources for trusted roots. - Roots []*Root + Roots []*Root `json:"roots"` // ClientRoots contains a list of sources for trusted client // certificates. - ClientRoots []*Root + ClientRoots []*Root `json:"client_roots"` // Profiles contains a dictionary of names to dictionaries; // this is intended to allow flexibility in supporting diff --git a/transport/example/ca.json b/transport/example/ca.json new file mode 100644 index 000000000..551b7452a --- /dev/null +++ b/transport/example/ca.json @@ -0,0 +1,15 @@ +{ + "hosts": [ + "dropsonde.net" + ], + "key": { + "algo": "rsa", + "size": 4096 + }, + "names": [{ + "C": "US", + "L": "San Francisco", + "OU": "Dropsonde Certificate Authority", + "ST": "California" + }] +} diff --git a/transport/example/config.json b/transport/example/config.json new file mode 100644 index 000000000..a4edf1728 --- /dev/null +++ b/transport/example/config.json @@ -0,0 +1,25 @@ +{ + "signing": { + "default": { + "expiry": "168h" + }, + "profiles": { + "maclient": { + "expiry": "1h", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "server": { + "expiry": "8760h", + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + } + } + } +} diff --git a/transport/example/genca.sh b/transport/example/genca.sh new file mode 100755 index 000000000..44fa79ba4 --- /dev/null +++ b/transport/example/genca.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cfssl gencert -initca ca.json | cfssljson -bare ca + diff --git a/transport/example/maclient/client.go b/transport/example/maclient/client.go new file mode 100644 index 000000000..5a05a83d0 --- /dev/null +++ b/transport/example/maclient/client.go @@ -0,0 +1,70 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/cloudflare/cfssl/transport" + "github.com/cloudflare/cfssl/transport/core" +) + +// maclient is a mutual-authentication client, meant to demonstrate +// using the client-side mutual authentication side of the transport +// package. + +var progname = filepath.Base(os.Args[0]) +var before = 5 * time.Minute + +// Err displays a formatting error message to standard error, +// appending the error string, and exits with the status code from +// `exit`, à la err(3). +func Err(exit int, err error, format string, a ...interface{}) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += ": %v\n" + a = append(a, err) + fmt.Fprintf(os.Stderr, format, a...) + os.Exit(exit) +} + +func main() { + var addr, conf string + flag.StringVar(&addr, "a", "127.0.0.1:9876", "`address` of server") + flag.StringVar(&conf, "f", "client.json", "config `file` to use") + flag.Parse() + + var id = new(core.Identity) + data, err := ioutil.ReadFile(conf) + if err != nil { + Err(1, err, "reading config file") + } + + err = json.Unmarshal(data, id) + if err != nil { + Err(1, err, "parsing config file") + } + + tr, err := transport.New(before, id) + if err != nil { + Err(1, err, "creating transport") + } + + conn, err := transport.Dial(addr, tr) + if err != nil { + Err(1, err, "dialing %s", addr) + } + + _, err = conn.Write([]byte("hello, world")) + if err != nil { + Err(1, err, "writing on socket") + } + + <-time.After(3 * time.Second) + + fmt.Println("OK") + conn.Close() +} diff --git a/transport/example/maclient/client.json b/transport/example/maclient/client.json new file mode 100644 index 000000000..ab41d6b08 --- /dev/null +++ b/transport/example/maclient/client.json @@ -0,0 +1,28 @@ +{ + "request": { + "CN": "test client", + "hosts": ["127.0.0.1"] + }, + "profiles": { + "paths": { + "private_key": "client.key", + "certificate": "client.pem" + }, + "cfssl": { + "profile": "maclient", + "remote": "127.0.0.1:8888" + } + }, + "roots": [ + { + "type": "system" + }, + { + "type": "cfssl", + "metadata": { + "host": "127.0.0.1:8888", + "profile": "server" + } + } + ] +} diff --git a/transport/example/maserver/server.go b/transport/example/maserver/server.go new file mode 100644 index 000000000..cf8770d7c --- /dev/null +++ b/transport/example/maserver/server.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "time" + + "github.com/cloudflare/cfssl/transport" + "github.com/cloudflare/cfssl/transport/core" +) + +// maclient is a mutual-authentication server, meant to demonstrate +// using the client-side mutual authentication side of the transport +// package. + +var progname = filepath.Base(os.Args[0]) +var before = 5 * time.Minute + +// Err displays a formatting error message to standard error, +// appending the error string, and exits with the status code from +// `exit`, à la err(3). +func Err(exit int, err error, format string, a ...interface{}) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += ": %v\n" + a = append(a, err) + fmt.Fprintf(os.Stderr, format, a...) + os.Exit(exit) +} + +// Warn displays a formatted error message to standard output, +// appending the error string, à la warn(3). +func Warn(err error, format string, a ...interface{}) (int, error) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += ": %v\n" + a = append(a, err) + return fmt.Fprintf(os.Stderr, format, a...) +} + +func main() { + var addr, conf string + flag.StringVar(&addr, "a", "127.0.0.1:9876", "`address` of server") + flag.StringVar(&conf, "f", "server.json", "config `file` to use") + flag.Parse() + + var id = new(core.Identity) + data, err := ioutil.ReadFile(conf) + if err != nil { + Err(1, err, "reading config file") + } + + err = json.Unmarshal(data, id) + if err != nil { + Err(1, err, "parsing config file") + } + + tr, err := transport.New(before, id) + if err != nil { + Err(1, err, "creating transport") + } + + l, err := transport.Listen(addr, tr) + if err != nil { + Err(1, err, "setting up listener") + } + + log.Println("setting up auto-update") + go l.AutoUpdate(nil, nil) + + log.Println("listening on", addr) + for { + conn, err := l.Accept() + if err != nil { + Warn(err, "client connection failed") + } else { + var buf = make([]byte, 256) + n, err := conn.Read(buf) + if err != nil { + Warn(err, "reading from client") + } else { + log.Printf("received %d-byte message: %s", n, buf[:n]) + } + conn.Close() + } + } +} diff --git a/transport/example/maserver/server.json b/transport/example/maserver/server.json new file mode 100644 index 000000000..0e092c843 --- /dev/null +++ b/transport/example/maserver/server.json @@ -0,0 +1,26 @@ +{ + "request": { + "CN": "test server", + "hosts": ["127.0.0.1"] + }, + "profiles": { + "paths": { + "private_key": "server.key", + "certificate": "server.pem" + }, + "cfssl": { + "profile": "server", + "remote": "127.0.0.1:8888" + } + }, + "roots": [{ + "type": "system" + }], + "client_roots": [{ + "type": "cfssl", + "metadata": { + "host": "127.0.0.1:8888", + "profile": "maclient" + } + }] +} diff --git a/transport/listener.go b/transport/listener.go index d9082805a..49f429a88 100644 --- a/transport/listener.go +++ b/transport/listener.go @@ -103,8 +103,10 @@ func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { func (l *Listener) getConfig() (*tls.Config, error) { if l.Transport.ClientTrustStore != nil { + log.Info("using client auth") return l.Transport.TLSClientAuthServerConfig() } + log.Info("not using client auth") return l.Transport.TLSServerConfig() } From cc1eb64d8701712c6363a91665b7e68c0ba9c5c1 Mon Sep 17 00:00:00 2001 From: "Jacob H. Haven" Date: Tue, 27 Oct 2015 12:46:13 -0700 Subject: [PATCH 07/18] Update Listener struct This commit simplifies the listener structure, and ensures that it satisfies the net.Listener interface. --- transport/example/maclient/client.go | 3 +- transport/example/maserver/server.go | 22 ++++--- transport/listener.go | 87 +++++++--------------------- 3 files changed, 37 insertions(+), 75 deletions(-) diff --git a/transport/example/maclient/client.go b/transport/example/maclient/client.go index 5a05a83d0..468cab22e 100644 --- a/transport/example/maclient/client.go +++ b/transport/example/maclient/client.go @@ -58,8 +58,7 @@ func main() { Err(1, err, "dialing %s", addr) } - _, err = conn.Write([]byte("hello, world")) - if err != nil { + if _, err := fmt.Fprint(conn, "hello world!"); err != nil { Err(1, err, "writing on socket") } diff --git a/transport/example/maserver/server.go b/transport/example/maserver/server.go index cf8770d7c..808b3c817 100644 --- a/transport/example/maserver/server.go +++ b/transport/example/maserver/server.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "log" + "net" "os" "path/filepath" "time" @@ -72,19 +73,26 @@ func main() { go l.AutoUpdate(nil, nil) log.Println("listening on", addr) + Warn(serve(l), "serving listener") +} + +func serve(l net.Listener) error { + defer l.Close() for { conn, err := l.Accept() if err != nil { Warn(err, "client connection failed") - } else { - var buf = make([]byte, 256) - n, err := conn.Read(buf) + continue + } + go func(conn net.Conn) { + defer conn.Close() + buf, err := ioutil.ReadAll(conn) if err != nil { Warn(err, "reading from client") - } else { - log.Printf("received %d-byte message: %s", n, buf[:n]) + return } - conn.Close() - } + + log.Printf("received %d-byte message: %s", len(buf), buf) + }(conn) } } diff --git a/transport/listener.go b/transport/listener.go index 49f429a88..5a8a91571 100644 --- a/transport/listener.go +++ b/transport/listener.go @@ -2,7 +2,6 @@ package transport import ( "crypto/tls" - "errors" "net" "time" @@ -12,38 +11,37 @@ import ( // A Listener is a TCP network listener for TLS-secured connections. type Listener struct { *Transport - config *tls.Config - address string - listener net.Listener + net.Listener + config *tls.Config } -// PollInterval is how often to check whether a new certificate has -// been found. -var PollInterval = 30 * time.Second - // Listen sets up a new server. If an error is returned, it means // the server isn't ready to begin listening. func Listen(address string, tr *Transport) (*Listener, error) { - l := &Listener{ - Transport: tr, - address: address, - } - var err error - l.config, err = l.getConfig() + l := &Listener{Transport: tr} + l.config, err = tr.getConfig() if err != nil { return nil, err } - l.listener, err = tls.Listen("tcp", l.address, l.config) - if err != nil { - return nil, err - } + l.Listener, err = tls.Listen("tcp", address, l.config) + return l, err +} - log.Debug("listener ready") - return l, nil +func (tr *Transport) getConfig() (*tls.Config, error) { + if tr.ClientTrustStore != nil { + log.Info("using client auth") + return tr.TLSClientAuthServerConfig() + } + log.Info("not using client auth") + return tr.TLSServerConfig() } +// PollInterval is how often to check whether a new certificate has +// been found. +var PollInterval = 30 * time.Second + func pollWait(target time.Time) { for { <-time.After(PollInterval) @@ -60,9 +58,9 @@ func pollWait(target time.Time) { func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { for { // Wait until it's time to update the certificate. - target := time.Now().Add(l.Transport.Lifespan()) + target := time.Now().Add(l.Lifespan()) if PollInterval == 0 { - <-time.After(l.Transport.Lifespan()) + <-time.After(l.Lifespan()) } else { pollWait(target) } @@ -71,7 +69,7 @@ func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { // ready. for { log.Debug("refreshing certificate") - err := l.Transport.RefreshKeys() + err := l.RefreshKeys() if err == nil { break } @@ -100,46 +98,3 @@ func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { log.Debug("listener: auto update of certificate complete") } } - -func (l *Listener) getConfig() (*tls.Config, error) { - if l.Transport.ClientTrustStore != nil { - log.Info("using client auth") - return l.Transport.TLSClientAuthServerConfig() - } - log.Info("not using client auth") - return l.Transport.TLSServerConfig() -} - -// Addr returns the server's address. -func (l *Listener) Addr() string { - return l.address -} - -// Close shuts down the listener. -func (l *Listener) Close() error { - l.config = nil - err := l.listener.Close() - l.listener = nil - return err -} - -// Accept waits for and returns the next connection to the listener. -func (l *Listener) Accept() (net.Conn, error) { - if l.config == nil { - log.Debug("listener needs a TLS config") - return nil, errors.New("transport: listener isn't active") - } - - if l.listener == nil { - log.Debug("listener isn't listening") - return nil, errors.New("transport: listener isn't active") - } - - conn, err := l.listener.Accept() - if err != nil { - return nil, err - } - - conn = tls.Server(conn, l.config) - return conn, nil -} From d03a1ca0587b885815a839832838bc9756b0a71f Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 27 Oct 2015 13:57:48 -0700 Subject: [PATCH 08/18] Clean up example server and client. * Move common functions to an example library. * Various clean ups: splitting things into separate functions and making it easier to extend the examples later. * Client sends a few messages this time, and the server will acknowledge them. In the future, a more extensive example might be useful. --- transport/example/exlib/exlib.go | 78 +++++++++++++++++++++++++ transport/example/maclient/client.go | 50 ++++++++-------- transport/example/maserver/server.go | 86 +++++++++++++--------------- 3 files changed, 143 insertions(+), 71 deletions(-) create mode 100644 transport/example/exlib/exlib.go diff --git a/transport/example/exlib/exlib.go b/transport/example/exlib/exlib.go new file mode 100644 index 000000000..a2fd53db2 --- /dev/null +++ b/transport/example/exlib/exlib.go @@ -0,0 +1,78 @@ +// Package exlib contains common library code for the examples. +package exlib + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "time" +) + +var progname = filepath.Base(os.Args[0]) +var Before = 5 * time.Minute + +// Err displays a formatting error message to standard error, +// appending the error string, and exits with the status code from +// `exit`, à la err(3). +func Err(exit int, err error, format string, a ...interface{}) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += ": %v\n" + a = append(a, err) + fmt.Fprintf(os.Stderr, format, a...) + os.Exit(exit) +} + +// Errx displays a formatted error message to standard error and exits +// with the status code from `exit`, à la errx(3). +func Errx(exit int, format string, a ...interface{}) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += "\n" + fmt.Fprintf(os.Stderr, format, a...) + os.Exit(exit) +} + +// Warn displays a formatted error message to standard output, +// appending the error string, à la warn(3). +func Warn(err error, format string, a ...interface{}) (int, error) { + format = fmt.Sprintf("[%s] %s", progname, format) + format += ": %v\n" + a = append(a, err) + return fmt.Fprintf(os.Stderr, format, a...) +} + +// Unpack reads a message from an io.Reader. +func Unpack(r io.Reader) ([]byte, error) { + var bl [2]byte + + _, err := io.ReadFull(r, bl[:]) + if err != nil { + return nil, err + } + + n := binary.LittleEndian.Uint16(bl[:]) + buf := make([]byte, int(n)) + _, err = io.ReadFull(r, buf) + return buf, err +} + +const messageMax = 1 << 16 + +// Pack writes a message to an io.Writer. +func Pack(w io.Writer, buf []byte) error { + if len(buf) > messageMax { + return errors.New("message is too large") + } + + var bl [2]byte + binary.LittleEndian.PutUint16(bl[:], uint16(len(buf))) + _, err := w.Write(bl[:]) + if err != nil { + return err + } + + _, err = w.Write(buf) + return err +} diff --git a/transport/example/maclient/client.go b/transport/example/maclient/client.go index 468cab22e..2318f6ffd 100644 --- a/transport/example/maclient/client.go +++ b/transport/example/maclient/client.go @@ -1,35 +1,22 @@ package main import ( + "bytes" "encoding/json" "flag" "fmt" "io/ioutil" - "os" - "path/filepath" - "time" "github.com/cloudflare/cfssl/transport" "github.com/cloudflare/cfssl/transport/core" + "github.com/cloudflare/cfssl/transport/example/exlib" ) // maclient is a mutual-authentication client, meant to demonstrate // using the client-side mutual authentication side of the transport // package. -var progname = filepath.Base(os.Args[0]) -var before = 5 * time.Minute - -// Err displays a formatting error message to standard error, -// appending the error string, and exits with the status code from -// `exit`, à la err(3). -func Err(exit int, err error, format string, a ...interface{}) { - format = fmt.Sprintf("[%s] %s", progname, format) - format += ": %v\n" - a = append(a, err) - fmt.Fprintf(os.Stderr, format, a...) - os.Exit(exit) -} +var messages = []string{"hello world", "hello", "world"} func main() { var addr, conf string @@ -40,29 +27,44 @@ func main() { var id = new(core.Identity) data, err := ioutil.ReadFile(conf) if err != nil { - Err(1, err, "reading config file") + exlib.Err(1, err, "reading config file") } err = json.Unmarshal(data, id) if err != nil { - Err(1, err, "parsing config file") + exlib.Err(1, err, "parsing config file") } - tr, err := transport.New(before, id) + tr, err := transport.New(exlib.Before, id) if err != nil { - Err(1, err, "creating transport") + exlib.Err(1, err, "creating transport") } conn, err := transport.Dial(addr, tr) if err != nil { - Err(1, err, "dialing %s", addr) + exlib.Err(1, err, "dialing %s", addr) } - if _, err := fmt.Fprint(conn, "hello world!"); err != nil { - Err(1, err, "writing on socket") + for _, msg := range messages { + if err = exlib.Pack(conn, []byte(msg)); err != nil { + exlib.Err(1, err, "sending message") + } + + var resp []byte + resp, err = exlib.Unpack(conn) + if err != nil { + exlib.Err(1, err, "receiving message") + } + + if !bytes.Equal(resp, []byte("OK")) { + exlib.Errx(1, "server didn't send an OK message; received '%s'", resp) + } } - <-time.After(3 * time.Second) + err = exlib.Pack(conn, []byte{}) + if err != nil { + exlib.Err(1, err, "sending shutdown message failed") + } fmt.Println("OK") conn.Close() diff --git a/transport/example/maserver/server.go b/transport/example/maserver/server.go index 808b3c817..2bc8cfe60 100644 --- a/transport/example/maserver/server.go +++ b/transport/example/maserver/server.go @@ -3,45 +3,19 @@ package main import ( "encoding/json" "flag" - "fmt" "io/ioutil" - "log" "net" - "os" - "path/filepath" - "time" + "github.com/cloudflare/cfssl/log" "github.com/cloudflare/cfssl/transport" "github.com/cloudflare/cfssl/transport/core" + "github.com/cloudflare/cfssl/transport/example/exlib" ) // maclient is a mutual-authentication server, meant to demonstrate // using the client-side mutual authentication side of the transport // package. -var progname = filepath.Base(os.Args[0]) -var before = 5 * time.Minute - -// Err displays a formatting error message to standard error, -// appending the error string, and exits with the status code from -// `exit`, à la err(3). -func Err(exit int, err error, format string, a ...interface{}) { - format = fmt.Sprintf("[%s] %s", progname, format) - format += ": %v\n" - a = append(a, err) - fmt.Fprintf(os.Stderr, format, a...) - os.Exit(exit) -} - -// Warn displays a formatted error message to standard output, -// appending the error string, à la warn(3). -func Warn(err error, format string, a ...interface{}) (int, error) { - format = fmt.Sprintf("[%s] %s", progname, format) - format += ": %v\n" - a = append(a, err) - return fmt.Fprintf(os.Stderr, format, a...) -} - func main() { var addr, conf string flag.StringVar(&addr, "a", "127.0.0.1:9876", "`address` of server") @@ -51,29 +25,54 @@ func main() { var id = new(core.Identity) data, err := ioutil.ReadFile(conf) if err != nil { - Err(1, err, "reading config file") + exlib.Err(1, err, "reading config file") } err = json.Unmarshal(data, id) if err != nil { - Err(1, err, "parsing config file") + exlib.Err(1, err, "parsing config file") } - tr, err := transport.New(before, id) + tr, err := transport.New(exlib.Before, id) if err != nil { - Err(1, err, "creating transport") + exlib.Err(1, err, "creating transport") } l, err := transport.Listen(addr, tr) if err != nil { - Err(1, err, "setting up listener") + exlib.Err(1, err, "setting up listener") } - log.Println("setting up auto-update") + log.Info("setting up auto-update") go l.AutoUpdate(nil, nil) - log.Println("listening on", addr) - Warn(serve(l), "serving listener") + log.Info("listening on ", addr) + exlib.Warn(serve(l), "serving listener") +} + +func connHandler(conn net.Conn) { + defer conn.Close() + + for { + buf, err := exlib.Unpack(conn) + if err != nil { + exlib.Warn(err, "unpack message") + return + } + + if len(buf) == 0 { + log.Info(conn.RemoteAddr(), " sent empty record, closing connection") + return + } + + log.Infof("received %d-byte message: %s", len(buf), buf) + + err = exlib.Pack(conn, []byte("OK")) + if err != nil { + exlib.Warn(err, "pack message") + return + } + } } func serve(l net.Listener) error { @@ -81,18 +80,11 @@ func serve(l net.Listener) error { for { conn, err := l.Accept() if err != nil { - Warn(err, "client connection failed") + exlib.Warn(err, "client connection failed") continue } - go func(conn net.Conn) { - defer conn.Close() - buf, err := ioutil.ReadAll(conn) - if err != nil { - Warn(err, "reading from client") - return - } - - log.Printf("received %d-byte message: %s", len(buf), buf) - }(conn) + + log.Info("connection from ", conn.RemoteAddr()) + go connHandler(conn) } } From 6b3d51bf49def6366fbb85dc5676a3e7bdb372d7 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 27 Oct 2015 14:09:48 -0700 Subject: [PATCH 09/18] Add a transport example README. This README explains how to bootstrap a CFSSL for use in the examples, how to run the server, and how to run the clients. --- transport/example/README.md | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 transport/example/README.md diff --git a/transport/example/README.md b/transport/example/README.md new file mode 100644 index 000000000..2e73eee46 --- /dev/null +++ b/transport/example/README.md @@ -0,0 +1,79 @@ +# Transport Package Examples + +`maserver` and `maclient` are a mutually authenticated server and client; +the client will connect to the server and send a few messages. + +## Set up + +A running CFSSL is needed. The `genca.sh` script should generate +everything that's needed to run CFSSL locally. In a terminal for the +CA: + +``` +$ basename $(pwd) +example +$ ./genca.sh +2015/10/27 14:00:29 [INFO] generating a new CA key and certificate from CSR +2015/10/27 14:00:29 [INFO] generate received request +2015/10/27 14:00:29 [INFO] received CSR +2015/10/27 14:00:29 [INFO] generating key: rsa-4096 +2015/10/27 14:00:32 [INFO] encoded CSR +2015/10/27 14:00:33 [INFO] signed certificate with serial number 2940131150448804266 +$ cfssl serve -ca ca.pem -ca-key ca-key.pem -config config.json +... +2015/10/27 14:00:35 [INFO] Setting up '/api/v1/cfssl/sign' endpoint +``` + +The providing `config.json` contains the CFSSL configuration; the +`client.json` and `server.json` configurations are based on this +config. + +## Running the server + +The server expects a `server.json` in the same directory containing +the configuration. One is provided in the server source: + +``` +$ basename $(pwd) +example +$ cd maserver/ +$ go run server.go -a 127.0.0.1:9876 +$ go run server.go -a 127.0.0.1:9876 +2015/10/27 14:05:47 [INFO] using client auth +2015/10/27 14:05:47 [DEBUG] transport isn't ready; attempting to refresh keypair +2015/10/27 14:05:47 [DEBUG] key and certificate aren't ready, loading +2015/10/27 14:05:47 [DEBUG] failed to load keypair: open server.key: no such file or directory +2015/10/27 14:05:47 [DEBUG] transport's certificate is out of date (lifespan 0) +2015/10/27 14:05:47 [INFO] encoded CSR +2015/10/27 14:05:47 [DEBUG] requesting certificate from CA +2015/10/27 14:05:47 [DEBUG] giving the certificate to the provider +2015/10/27 14:05:47 [DEBUG] storing the certificate +2015/10/27 14:05:47 [INFO] setting up auto-update +2015/10/27 14:05:47 [INFO] listening on 127.0.0.1:9876 +``` + +At this point, the clients can start talking to the server. + +## Running a client + +At this point, clients just connect and send a few messages, ensuring +the server acknowledges the messages. The client also expects a +`client.json` configuration in the same directory; once is provided in +the source directory. + +``` +$ basename $(pwd) +example +$ go run client.go +2015/10/27 14:08:34 [DEBUG] transport isn't ready; attempting to refresh keypair +2015/10/27 14:08:34 [DEBUG] key and certificate aren't ready, loading +2015/10/27 14:08:34 [DEBUG] failed to load keypair: open client.key: no such file or directory +2015/10/27 14:08:34 [DEBUG] transport's certificate is out of date (lifespan 0) +2015/10/27 14:08:34 [INFO] encoded CSR +2015/10/27 14:08:34 [DEBUG] requesting certificate from CA +2015/10/27 14:08:34 [DEBUG] giving the certificate to the provider +2015/10/27 14:08:34 [DEBUG] storing the certificate +OK +$ +``` + From ff27854a40de9d42b5687f7fa8eed0523c8b206d Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 27 Oct 2015 14:25:57 -0700 Subject: [PATCH 10/18] Add authenticated CFSSL example. Add configurations to demonstrate the case where the remote CFSSL requires authentication. --- transport/example/README.md | 17 ++++++++-- transport/example/config_auth.json | 37 +++++++++++++++++++++ transport/example/maclient/client.json | 2 +- transport/example/maclient/client_auth.json | 30 +++++++++++++++++ transport/example/maserver/server_auth.json | 28 ++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 transport/example/config_auth.json create mode 100644 transport/example/maclient/client_auth.json create mode 100644 transport/example/maserver/server_auth.json diff --git a/transport/example/README.md b/transport/example/README.md index 2e73eee46..a77bdb8c0 100644 --- a/transport/example/README.md +++ b/transport/example/README.md @@ -31,7 +31,8 @@ config. ## Running the server The server expects a `server.json` in the same directory containing -the configuration. One is provided in the server source: +the configuration. One is provided in the server source, or it may be +overridden using the `-f` command line flag. ``` $ basename $(pwd) @@ -59,7 +60,8 @@ At this point, the clients can start talking to the server. At this point, clients just connect and send a few messages, ensuring the server acknowledges the messages. The client also expects a `client.json` configuration in the same directory; once is provided in -the source directory. +the source directory, or it may be overridden using the `-f` command +line flag. ``` $ basename $(pwd) @@ -77,3 +79,14 @@ OK $ ``` +## Auth Examples + +The CA, server, and client ship with a `_auth.json` configuration file +that will use an authenticated CFSSL. The commands change to: + +``` +$ cfssl serve -ca ca.pem -ca-key ca-key.pem -config config_auth.json +$ go run server.go -a 127.0.0.1:9876 -f server_auth.json +$ go run client.go -f client_auth.json +``` + diff --git a/transport/example/config_auth.json b/transport/example/config_auth.json new file mode 100644 index 000000000..fbe75a2d1 --- /dev/null +++ b/transport/example/config_auth.json @@ -0,0 +1,37 @@ +{ + "auth_keys": { + "client": { + "type": "standard", + "key": "52abb3ac91971bb72bce17e7a289cd04476490b19e0d8eb7810dc42d4ac16c41" + }, + "server": { + "type": "standard", + "key": "4f4f26686209f672e0ec7b19cbbc8b6d94fdd12cc0b20326f9005d5f234e6e3e" + } + }, + "signing": { + "default": { + "expiry": "168h" + }, + "profiles": { + "client": { + "auth_key": "client", + "expiry": "1h", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "server": { + "auth_key": "server", + "expiry": "8760h", + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + } + } + } +} diff --git a/transport/example/maclient/client.json b/transport/example/maclient/client.json index ab41d6b08..42cf58965 100644 --- a/transport/example/maclient/client.json +++ b/transport/example/maclient/client.json @@ -9,7 +9,7 @@ "certificate": "client.pem" }, "cfssl": { - "profile": "maclient", + "profile": "client", "remote": "127.0.0.1:8888" } }, diff --git a/transport/example/maclient/client_auth.json b/transport/example/maclient/client_auth.json new file mode 100644 index 000000000..4c110705c --- /dev/null +++ b/transport/example/maclient/client_auth.json @@ -0,0 +1,30 @@ +{ + "request": { + "CN": "test client", + "hosts": ["127.0.0.1"] + }, + "profiles": { + "paths": { + "private_key": "client.key", + "certificate": "client.pem" + }, + "cfssl": { + "profile": "client", + "remote": "127.0.0.1:8888", + "auth-type": "standard", + "auth-key": "52abb3ac91971bb72bce17e7a289cd04476490b19e0d8eb7810dc42d4ac16c41" + } + }, + "roots": [ + { + "type": "system" + }, + { + "type": "cfssl", + "metadata": { + "host": "127.0.0.1:8888", + "profile": "server" + } + } + ] +} diff --git a/transport/example/maserver/server_auth.json b/transport/example/maserver/server_auth.json new file mode 100644 index 000000000..70c4b8d4b --- /dev/null +++ b/transport/example/maserver/server_auth.json @@ -0,0 +1,28 @@ +{ + "request": { + "CN": "test server", + "hosts": ["127.0.0.1"] + }, + "profiles": { + "paths": { + "private_key": "server.key", + "certificate": "server.pem" + }, + "cfssl": { + "profile": "server", + "remote": "127.0.0.1:8888", + "auth-type": "standard", + "auth-key": "4f4f26686209f672e0ec7b19cbbc8b6d94fdd12cc0b20326f9005d5f234e6e3e" + } + }, + "roots": [{ + "type": "system" + }], + "client_roots": [{ + "type": "cfssl", + "metadata": { + "host": "127.0.0.1:8888", + "profile": "client" + } + }] +} From 65c12ae95589f7ca50344535ccf951c3c874ff1c Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 27 Oct 2015 14:27:22 -0700 Subject: [PATCH 11/18] Address Zi's comment. --- transport/ca/cfssl_provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/ca/cfssl_provider.go b/transport/ca/cfssl_provider.go index 1b4480de5..ca82240d2 100644 --- a/transport/ca/cfssl_provider.go +++ b/transport/ca/cfssl_provider.go @@ -105,7 +105,7 @@ var cfsslConfigDirs = []string{ } // The CFKS standard is to have a configuration file for a label as -// .label. +// .json. func findLabel(label string) *config.Config { for _, dir := range cfsslConfigDirs { cfgFile := filepath.Join(dir, label+".json") From 1209e80b1c14f6d81b194bdb781dc9578d778599 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 27 Oct 2015 14:35:30 -0700 Subject: [PATCH 12/18] golint changes. This is mostly commentary adjustments via https://travis-ci.org/cloudflare/cfssl/jobs/87769654. --- transport/client.go | 4 +++- transport/core/defs.go | 3 ++- transport/example/exlib/exlib.go | 3 +++ transport/roots/provider.go | 3 +++ transport/transport_test.go | 4 ++-- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/transport/client.go b/transport/client.go index e51e9289f..b43ef178a 100644 --- a/transport/client.go +++ b/transport/client.go @@ -119,7 +119,9 @@ func (tr *Transport) TLSServerConfig() (*tls.Config, error) { }, nil } -// NewTransport builds a new transport from the default +// New builds a new transport from an identity and a before time. The +// before time tells the transport how long before the certificate +// expires to start attempting to update when auto-updating. func New(before time.Duration, identity *core.Identity) (*Transport, error) { var tr = &Transport{ Before: before, diff --git a/transport/core/defs.go b/transport/core/defs.go index aebd9c897..bcb6c47e1 100644 --- a/transport/core/defs.go +++ b/transport/core/defs.go @@ -44,7 +44,8 @@ type Identity struct { Profiles map[string]map[string]string `json:"profiles"` } -// A sensible default is to regenerate certificates the day before they expire. +// DefaultBefore is a sensible default; attempt to regenerate certificates the +// day before they expire. var DefaultBefore = 24 * time.Hour // CipherSuites are the TLS cipher suites that should be used by CloudFlare programs. diff --git a/transport/example/exlib/exlib.go b/transport/example/exlib/exlib.go index a2fd53db2..01c1a71a0 100644 --- a/transport/example/exlib/exlib.go +++ b/transport/example/exlib/exlib.go @@ -12,6 +12,9 @@ import ( ) var progname = filepath.Base(os.Args[0]) + +// Before set to 5 minutes; certificates will attempt to auto-update 5 +// minutes before they expire. var Before = 5 * time.Minute // Err displays a formatting error message to standard error, diff --git a/transport/roots/provider.go b/transport/roots/provider.go index 0af26f33f..5aef23e24 100644 --- a/transport/roots/provider.go +++ b/transport/roots/provider.go @@ -11,6 +11,8 @@ import ( "github.com/cloudflare/cfssl/transport/roots/system" ) +// Providers is a mapping of supported providers and the functions +// that can build them. var Providers = map[string]func(map[string]string) ([]*x509.Certificate, error){ "system": system.New, "cfssl": NewCFSSL, @@ -52,6 +54,7 @@ func (ts *TrustStore) addCerts(certs []*x509.Certificate) { } } +// Trusted contains a store of trusted certificates. type Trusted interface { // Certificates returns a slice containing the certificates // that are loaded into the provider. diff --git a/transport/transport_test.go b/transport/transport_test.go index 17ad2a58e..417e77d63 100644 --- a/transport/transport_test.go +++ b/transport/transport_test.go @@ -127,7 +127,7 @@ func TestTransportSetup(t *testing.T) { var before = 55 * time.Second var err error - tr, err = NewTransport(before, testIdentity) + tr, err = New(before, testIdentity) if err != nil { t.Fatalf("failed to set up transport: %v", err) } @@ -225,7 +225,7 @@ func testListen(t *testing.T) { func TestListener(t *testing.T) { var before = 55 * time.Second - trl, err := NewTransport(before, testLIdentity) + trl, err := New(before, testLIdentity) if err != nil { t.Fatalf("failed to set up transport: %v", err) } From b035c5e11cbd933975bbe5666c7246742c032d38 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 28 Oct 2015 16:56:09 -0700 Subject: [PATCH 13/18] Fix notes from Nick. * Remove unused core/config.go file. Users should just use the `Dial` and `Listen` functions in the `transport` package. * Fix spelling and grammar issues. * Clean up formatting on JSON file. --- transport/ca/cert_provider.go | 2 +- transport/ca/cfssl_provider.go | 4 +-- transport/core/config.go | 42 ------------------------ transport/example/maclient/client.go | 2 +- transport/example/maserver/server.json | 44 +++++++++++++++----------- 5 files changed, 29 insertions(+), 65 deletions(-) delete mode 100644 transport/core/config.go diff --git a/transport/ca/cert_provider.go b/transport/ca/cert_provider.go index cbf9f7532..e83af4e8e 100644 --- a/transport/ca/cert_provider.go +++ b/transport/ca/cert_provider.go @@ -1,5 +1,5 @@ // Package ca provides the CertificateAuthority interface for the -// transport package, which provide an interface to get a CSR signed +// transport package, which provides an interface to get a CSR signed // by some certificate authority. package ca diff --git a/transport/ca/cfssl_provider.go b/transport/ca/cfssl_provider.go index ca82240d2..8f085adcd 100644 --- a/transport/ca/cfssl_provider.go +++ b/transport/ca/cfssl_provider.go @@ -76,7 +76,7 @@ func ipIsLocal(ip net.IP) bool { func (cap *CFSSL) validateAuth() error { // The client is using some form of authentication, and the best way // to figure out that the auth is invalid is when it's used. Therefore, - // we'll elide checking the credentials until that time. + // we'll delay checking the credentials until that time. if cap.provider != nil { return nil } @@ -104,7 +104,7 @@ var cfsslConfigDirs = []string{ "/state/etc/cfssl", } -// The CFKS standard is to have a configuration file for a label as +// The CFSSL standard is to have a configuration file for a label as // .json. func findLabel(label string) *config.Config { for _, dir := range cfsslConfigDirs { diff --git a/transport/core/config.go b/transport/core/config.go deleted file mode 100644 index 8af3d57a1..000000000 --- a/transport/core/config.go +++ /dev/null @@ -1,42 +0,0 @@ -package core - -import ( - "crypto/tls" - "crypto/x509" -) - -// TLSClientAuthClientConfig returns a new client authentication TLS -// configuration that can be used for a client using client auth -// connecting to the named host. -func TLSClientAuthClientConfig(cert tls.Certificate, host string) *tls.Config { - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - ServerName: host, - CipherSuites: CipherSuites, - MinVersion: tls.VersionTLS12, - } -} - -// TLSClientAuthServerConfig returns a new client authentication TLS -// configuration for servers expecting mutually authenticated -// clients. The clientAuth parameter should contain the root pool used -// to authenticate clients. -func TLSClientAuthServerConfig(cert tls.Certificate, clientAuth *x509.CertPool) *tls.Config { - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientCAs: clientAuth, - ClientAuth: tls.RequireAndVerifyClientCert, - CipherSuites: CipherSuites, - MinVersion: tls.VersionTLS12, - } -} - -// TLSServerConfig is a general server configuration that should be -// used for non-client authentication purposes, such as HTTPS. -func TLSServerConfig(cert tls.Certificate) *tls.Config { - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - CipherSuites: CipherSuites, - MinVersion: tls.VersionTLS12, - } -} diff --git a/transport/example/maclient/client.go b/transport/example/maclient/client.go index 2318f6ffd..786a7c245 100644 --- a/transport/example/maclient/client.go +++ b/transport/example/maclient/client.go @@ -44,6 +44,7 @@ func main() { if err != nil { exlib.Err(1, err, "dialing %s", addr) } + defer conn.Close() for _, msg := range messages { if err = exlib.Pack(conn, []byte(msg)); err != nil { @@ -67,5 +68,4 @@ func main() { } fmt.Println("OK") - conn.Close() } diff --git a/transport/example/maserver/server.json b/transport/example/maserver/server.json index 0e092c843..975b3247f 100644 --- a/transport/example/maserver/server.json +++ b/transport/example/maserver/server.json @@ -1,26 +1,32 @@ { "request": { - "CN": "test server", - "hosts": ["127.0.0.1"] + "CN": "test server", + "hosts": [ + "127.0.0.1" + ] }, "profiles": { - "paths": { - "private_key": "server.key", - "certificate": "server.pem" - }, - "cfssl": { - "profile": "server", - "remote": "127.0.0.1:8888" - } + "paths": { + "private_key": "server.key", + "certificate": "server.pem" + }, + "cfssl": { + "profile": "server", + "remote": "127.0.0.1:8888" + } }, - "roots": [{ - "type": "system" - }], - "client_roots": [{ - "type": "cfssl", + "roots": [ + { + "type": "system" + } + ], + "client_roots": [ + { + "type": "cfssl", "metadata": { - "host": "127.0.0.1:8888", - "profile": "maclient" - } - }] + "host": "127.0.0.1:8888", + "profile": "maclient" + } + } + ] } From 04733ab3e5f15f6877d63b8bfc1de8253b3053d3 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 29 Oct 2015 12:59:17 -0700 Subject: [PATCH 14/18] Address @lmb's comments. * Notation change in specifying local IPv4 network. * Explicitly mark AutoUpdate channels as write-only. * Add a recovery and associated critical log message in the event an autoupdate goroutine panics. --- transport/ca/cfssl_provider.go | 2 +- transport/client.go | 8 +++++++- transport/listener.go | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/transport/ca/cfssl_provider.go b/transport/ca/cfssl_provider.go index 8f085adcd..d20501c41 100644 --- a/transport/ca/cfssl_provider.go +++ b/transport/ca/cfssl_provider.go @@ -59,7 +59,7 @@ func newProvider(ak config.AuthKey, ad []byte) (auth.Provider, error) { var ErrNoAuth = errors.New("transport: authentication is required for non-local remotes") var v4Loopback = net.IPNet{ - IP: net.IP{127, 0, 0, 1}, + IP: net.IP{127, 0, 0, 0}, Mask: net.IPv4Mask(255, 0, 0, 0), } diff --git a/transport/client.go b/transport/client.go index b43ef178a..15380a037 100644 --- a/transport/client.go +++ b/transport/client.go @@ -275,7 +275,13 @@ func Dial(address string, tr *Transport) (*tls.Conn, error) { // certUpdates chan is provided, it will receive timestamps for // reissued certificates. If errChan is non-nil, any errors that occur // in the updater will be passed along. -func (tr *Transport) AutoUpdate(certUpdates chan time.Time, errChan chan error) { +func (tr *Transport) AutoUpdate(certUpdates chan<- time.Time, errChan chan<- error) { + defer func() { + if r := recover(); r != nil { + log.Criticalf("AutoUpdate panicked: %v", r) + } + }() + for { // Wait until it's time to update the certificate. target := time.Now().Add(tr.Lifespan()) diff --git a/transport/listener.go b/transport/listener.go index 5a8a91571..006791754 100644 --- a/transport/listener.go +++ b/transport/listener.go @@ -55,7 +55,13 @@ func pollWait(target time.Time) { // certUpdates chan is provided, it will receive timestamps for // reissued certificates. If errChan is non-nil, any errors that occur // in the updater will be passed along. -func (l *Listener) AutoUpdate(certUpdates chan time.Time, errChan chan error) { +func (l *Listener) AutoUpdate(certUpdates chan<- time.Time, errChan chan<- error) { + defer func() { + if r := recover(); r != nil { + log.Criticalf("AutoUpdate panicked: %v", r) + } + }() + for { // Wait until it's time to update the certificate. target := time.Now().Add(l.Lifespan()) From b910c794767132fde9578bcf762f676332593375 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 29 Oct 2015 13:04:28 -0700 Subject: [PATCH 15/18] Clean up example code. * Use "client" as the profile name for consistency with the authenticated version. * Add an error channel to server to demonstrate its use. --- transport/example/config.json | 4 ++-- transport/example/maserver/server.go | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/transport/example/config.json b/transport/example/config.json index a4edf1728..5fb78e1c9 100644 --- a/transport/example/config.json +++ b/transport/example/config.json @@ -4,7 +4,7 @@ "expiry": "168h" }, "profiles": { - "maclient": { + "client": { "expiry": "1h", "usages": [ "signing", @@ -13,7 +13,7 @@ ] }, "server": { - "expiry": "8760h", + "expiry": "1h", "usages": [ "signing", "key encipherment", diff --git a/transport/example/maserver/server.go b/transport/example/maserver/server.go index 2bc8cfe60..3939102b6 100644 --- a/transport/example/maserver/server.go +++ b/transport/example/maserver/server.go @@ -43,8 +43,20 @@ func main() { exlib.Err(1, err, "setting up listener") } + var errChan = make(chan error, 0) + go func(ec <-chan error) { + for { + err, ok := <-ec + if !ok { + log.Warning("error channel closed, future errors will not be reported") + break + } + log.Errorf("auto update error: %v", err) + } + }(errChan) + log.Info("setting up auto-update") - go l.AutoUpdate(nil, nil) + go l.AutoUpdate(nil, errChan) log.Info("listening on ", addr) exlib.Warn(serve(l), "serving listener") From 08fb74a3bc435ac0e164df1e0bb6a45c4a76efd5 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 29 Oct 2015 13:37:00 -0700 Subject: [PATCH 16/18] Add support for loading roots from file. This was requested by @jkroll. Also adds package documentation. --- transport/roots/doc.go | 15 +++++++++++++++ transport/roots/provider.go | 21 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 transport/roots/doc.go diff --git a/transport/roots/doc.go b/transport/roots/doc.go new file mode 100644 index 000000000..a079bf445 --- /dev/null +++ b/transport/roots/doc.go @@ -0,0 +1,15 @@ +// Package roots includes support for loading trusted roots from +// various sources. +// +// The following are supported trusted roout sources provided: +// +// The "system" type does not take any metadata. It will use the +// default system certificates provided by the operating system. +// +// The "cfssl" provider takes keys for the CFSSL "host", "label", and +// "profile", and loads the returned certificate into the trust store. +// +// The "file" provider takes a source file (specified under the +// "source" key) that contains one or more certificates and adds +// them into the source tree. +package roots diff --git a/transport/roots/provider.go b/transport/roots/provider.go index 5aef23e24..4c3f599e8 100644 --- a/transport/roots/provider.go +++ b/transport/roots/provider.go @@ -1,12 +1,12 @@ -// Package roots includes support for loading trusted roots from -// various sources. package roots import ( "crypto/sha256" "crypto/x509" "errors" + "io/ioutil" + "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/transport/core" "github.com/cloudflare/cfssl/transport/roots/system" ) @@ -16,6 +16,7 @@ import ( var Providers = map[string]func(map[string]string) ([]*x509.Certificate, error){ "system": system.New, "cfssl": NewCFSSL, + "file": TrustPEM, } // A TrustStore contains a pool of certificate that are trusted for a @@ -104,3 +105,19 @@ func New(rootDefs []*core.Root) (*TrustStore, error) { } return store, err } + +// TrustPEM takes a source file containing one or more certificates +// and adds them to the trust store. +func TrustPEM(metadata map[string]string) ([]*x509.Certificate, error) { + sourceFile, ok := metadata["source"] + if !ok { + return nil, errors.New("transport: PEM source requires a source file") + } + + in, err := ioutil.ReadFile(sourceFile) + if err != nil { + return nil, err + } + + return helpers.ParseCertificatesPEM(in) +} From 39a7c5a3c392fa215a47258fcdc679120235cf26 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 29 Oct 2015 15:36:34 -0700 Subject: [PATCH 17/18] Update docs based on feedback from @lmb. Question #1: "What happens if before > lifetime of a freshly issued cert?" Question #2: "Questions on certificate rollover, all in the context of very long running connections: * Does a server have to react to an updated cert by resetting connections? * Does a client have to AutoUpdate? * If yes, should a client restart connections as well on a new certificate?" Rekey was renamed to RefreshKeys, and the documentation now reflects this. --- transport/client.go | 4 +++- transport/doc.go | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/transport/client.go b/transport/client.go index 15380a037..025cf1343 100644 --- a/transport/client.go +++ b/transport/client.go @@ -121,7 +121,9 @@ func (tr *Transport) TLSServerConfig() (*tls.Config, error) { // New builds a new transport from an identity and a before time. The // before time tells the transport how long before the certificate -// expires to start attempting to update when auto-updating. +// expires to start attempting to update when auto-updating. If before +// is longer than the certificate's lifetime, every update check will +// trigger a new certificate to be generated. func New(before time.Duration, identity *core.Identity) (*Transport, error) { var tr = &Transport{ Before: before, diff --git a/transport/doc.go b/transport/doc.go index 4704762ae..d9296e618 100644 --- a/transport/doc.go +++ b/transport/doc.go @@ -24,11 +24,20 @@ // // // -// The NewTransport function will return a transport built using the +// The New function will return a transport built using the // NewKeyProvider and NewCA functions. These functions may be changed // by other packages to provide common key provider and CA -// configurations. Clients can then use Rekey (or launch AutoUpdate in -// a goroutine) to ensure the certificate and key are loaded and -// correct. The Listen and Dial functions then provide the necessary -// connection support. +// configurations. Clients can then use RefreshKeys (or launch +// AutoUpdate in a goroutine) to ensure the certificate and key are +// loaded and correct. The Listen and Dial functions then provide the +// necessary connection support. +// +// The AutoUpdate function will handle automatic certificate +// issuance. Servers and clients are not required to take any special +// action when the certificate is updated: the key and certificate are +// only used when establishing a connection, and therefore existing +// connections are not affected---there is no need to reset or restart +// any existing connections. Clients should run AutoUpdate if they +// plan on making multiple connections or will be reconnecting; for a +// one-off connection, it isn't necessary. package transport From 51e1ac76731f0fe62a9dad99551820578bff177b Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Mon, 2 Nov 2015 19:53:05 -0800 Subject: [PATCH 18/18] Add exponential backoff with jitter to AutoUpdate. --- transport/client.go | 11 +++++- transport/core/backoff.go | 82 +++++++++++++++++++++++++++++++++++++++ transport/core/rand.go | 36 +++++++++++++++++ transport/listener.go | 5 ++- 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 transport/core/backoff.go create mode 100644 transport/core/rand.go diff --git a/transport/client.go b/transport/client.go index 025cf1343..36a235175 100644 --- a/transport/client.go +++ b/transport/client.go @@ -63,6 +63,11 @@ type Transport struct { // Identity contains information about the entity that will be // used to construct certificates. Identity *core.Identity + + // Backoff is used to control the behaviour of a Transport + // when it is attempting to automatically update a certificate + // as part of AutoUpdate. + Backoff *core.Backoff } // TLSClientAuthClientConfig returns a new client authentication TLS @@ -128,6 +133,7 @@ func New(before time.Duration, identity *core.Identity) (*Transport, error) { var tr = &Transport{ Before: before, Identity: identity, + Backoff: &core.Backoff{}, } store, err := roots.New(identity.Roots) @@ -302,12 +308,13 @@ func (tr *Transport) AutoUpdate(certUpdates chan<- time.Time, errChan chan<- err break } - log.Debug("failed to update certificate, will try again in 5 minutes") + delay := tr.Backoff.Duration() + log.Debugf("failed to update certificate, will try again in %s", delay) if errChan != nil { errChan <- err } - <-time.After(5 * time.Minute) + <-time.After(delay) } log.Debugf("certificate updated") diff --git a/transport/core/backoff.go b/transport/core/backoff.go new file mode 100644 index 000000000..111505069 --- /dev/null +++ b/transport/core/backoff.go @@ -0,0 +1,82 @@ +package core + +// http://www.awsarchitectureblog.com/2015/03/backoff.html + +import ( + "math" + mrand "math/rand" + "sync" + "time" +) + +// DefaultInterval is used when a Backoff is initialised with a +// zero-value Interval. +var DefaultInterval = 5 * time.Minute + +// DefaultMaxDuration is maximum amount of time that the backoff will +// delay for. +var DefaultMaxDuration = 6 * time.Hour + +// A Backoff contains the information needed to intelligently backoff +// and retry operations using an exponential backoff algorithm. It may +// be initialised with all zero values and it will behave sanely. +type Backoff struct { + // MaxDuration is the largest possible duration that can be + // returned from a call to Duration. + MaxDuration time.Duration + + // Interval controls the time step for backing off. + Interval time.Duration + + // Jitter controls whether to use the "Full Jitter" + // improvement to attempt to smooth out spikes in a high + // contention scenario. + Jitter bool + + tries int + lock *sync.Mutex // lock guards tries +} + +func (b *Backoff) setup() { + if b.Interval == 0 { + b.Interval = DefaultInterval + } + + if b.MaxDuration == 0 { + b.MaxDuration = DefaultMaxDuration + } + + if b.lock == nil { + b.lock = new(sync.Mutex) + } +} + +// Duration returns a time.Duration appropriate for the backoff, +// incrementing the attempt counter. +func (b *Backoff) Duration() time.Duration { + b.setup() + b.lock.Lock() + defer b.lock.Unlock() + + pow := 1 << uint(b.tries) + + // MaxInt16 is an arbitrary choice on an upper bound; the + // implication is that every 16 tries, the counter resets. + if pow > math.MaxInt16 { + b.tries = 0 + pow = 1 + } + + t := time.Duration(pow) + b.tries++ + t = b.Interval * t + if t > b.MaxDuration { + t = b.MaxDuration + } + + if b.Jitter { + t = time.Duration(mrand.Int63n(int64(t))) + } + + return t +} diff --git a/transport/core/rand.go b/transport/core/rand.go new file mode 100644 index 000000000..d4b24bfed --- /dev/null +++ b/transport/core/rand.go @@ -0,0 +1,36 @@ +package core + +import ( + "crypto/rand" + "encoding/binary" + "io" + mrand "math/rand" + + "github.com/cloudflare/cfssl/log" +) + +var seeded bool + +func seed() error { + if seeded { + return nil + } + + var buf [8]byte + _, err := io.ReadFull(rand.Reader, buf[:]) + if err != nil { + return err + } + + n := int64(binary.LittleEndian.Uint64(buf[:])) + mrand.Seed(n) + seeded = true + return nil +} + +func init() { + err := seed() + if err != nil { + log.Errorf("seeding mrand failed: %v", err) + } +} diff --git a/transport/listener.go b/transport/listener.go index 006791754..ce8a557d0 100644 --- a/transport/listener.go +++ b/transport/listener.go @@ -80,12 +80,13 @@ func (l *Listener) AutoUpdate(certUpdates chan<- time.Time, errChan chan<- error break } - log.Debug("failed to update certificate, will try again in 5 minutes") + delay := l.Transport.Backoff.Duration() + log.Debugf("failed to update certificate, will try again in %s", delay) if errChan != nil { errChan <- err } - <-time.After(5 * time.Minute) + <-time.After(delay) } if certUpdates != nil {