diff --git a/api/types/types_config.go b/api/types/types_config.go index 5e7e506c..a566f0ab 100644 --- a/api/types/types_config.go +++ b/api/types/types_config.go @@ -99,6 +99,12 @@ const ( // ConfigTLSServerName is a config key. ConfigTLSServerName = ConfigTLS + ".serverName" + // ConfigTLSKnownHosts is a config key. + ConfigTLSKnownHosts = ConfigTLS + ".knownHosts" + + // ConfigTLSVerifyPeers is a config key. + ConfigTLSVerifyPeers = ConfigTLS + ".verifyPeers" + // ConfigTLSClientCertRequired is a config key. ConfigTLSClientCertRequired = ConfigTLS + ".clientCertRequired" diff --git a/api/types/types_paths.go b/api/types/types_paths.go index 63fcc304..33d91cdd 100644 --- a/api/types/types_paths.go +++ b/api/types/types_paths.go @@ -109,6 +109,22 @@ const ( // LSX is the path to the libStorage executor. LSX + // DefaultTLSCertFile is the default path to the TLS cert file, + // libstorage.crt. + DefaultTLSCertFile + + // DefaultTLSKeyFile is the default path to the TLS key file, + // libstorage.key. + DefaultTLSKeyFile + + // DefaultTLSTrustedRootsFile is the default path to the TLS trusted roots + // file, cacerts. + DefaultTLSTrustedRootsFile + + // DefaultTLSKnownHosts is the default path to the TLS known hosts file, + // known_hosts file. + DefaultTLSKnownHosts + maxFileKey ) @@ -167,6 +183,10 @@ func (k fileKey) parent() fileKey { return Etc case LSX: return Lib + case DefaultTLSCertFile, + DefaultTLSKeyFile, + DefaultTLSTrustedRootsFile: + return TLS default: return Home } @@ -195,6 +215,14 @@ func (k fileKey) key() string { return "tls" case LSX: return "lsx" + case DefaultTLSCertFile: + return "crt" + case DefaultTLSKeyFile: + return "key" + case DefaultTLSTrustedRootsFile: + return "tca" + case DefaultTLSKnownHosts: + return "hst" } return "" } @@ -239,6 +267,14 @@ func (k fileKey) defaultVal() string { default: return fmt.Sprintf("lsx-%s", runtime.GOOS) } + case DefaultTLSCertFile: + return "libstorage.crt" + case DefaultTLSKeyFile: + return "libstorage.key" + case DefaultTLSTrustedRootsFile: + return "cacerts" + case DefaultTLSKnownHosts: + return "known_hosts" } return "" } diff --git a/api/types/types_paths_test.go b/api/types/types_paths_test.go index 60fdacb1..50188b7c 100644 --- a/api/types/types_paths_test.go +++ b/api/types/types_paths_test.go @@ -25,6 +25,11 @@ func TestPaths(t *testing.T) { t.Logf("%5[1]s %[2]s", Home.key(), Home) t.Logf("%5[1]s %[2]s", Etc.key(), Etc) t.Logf("%5[1]s %[2]s", TLS.key(), TLS) + t.Logf("%5[1]s %[2]s", DefaultTLSCertFile.key(), DefaultTLSCertFile) + t.Logf("%5[1]s %[2]s", DefaultTLSKeyFile.key(), DefaultTLSKeyFile) + t.Logf("%5[1]s %[2]s", + DefaultTLSTrustedRootsFile.key(), DefaultTLSTrustedRootsFile) + t.Logf("%5[1]s %[2]s", DefaultTLSKnownHosts.key(), DefaultTLSKnownHosts) t.Logf("%5[1]s %[2]s", Lib.key(), Lib) t.Logf("%5[1]s %[2]s", Log.key(), Log) t.Logf("%5[1]s %[2]s", Run.key(), Run) diff --git a/api/types/types_tls.go b/api/types/types_tls.go index 0252c52f..55b5e692 100644 --- a/api/types/types_tls.go +++ b/api/types/types_tls.go @@ -7,6 +7,16 @@ import "crypto/tls" type TLSConfig struct { tls.Config + // VerifyPeers is a flag that indicates whether peer certificates + // should be validated against a PeerFingerprint or known hosts files. + VerifyPeers bool + + // SysKnownHosts is the path to the system's known_hosts file. + SysKnownHosts string + + // UsrKnownHosts is the path to the user's known_hosts file. + UsrKnownHosts string + // PeerFingerprint is the expected SHA256 fingerprint of a peer certificate. PeerFingerprint []byte } diff --git a/api/utils/utils_tls.go b/api/utils/utils_tls.go index 5843c5f7..6eb762e4 100644 --- a/api/utils/utils_tls.go +++ b/api/utils/utils_tls.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "io/ioutil" "os" + "path" "regexp" "strings" @@ -17,12 +18,29 @@ import ( "github.com/codedellemc/libstorage/api/types" ) +var knownHostRX = regexp.MustCompile(`(?i)^([^\s]+?)\s([^\s]+?)\s(.+)$`) + +// ParseKnownHost parses a known host line that's in the expected format: +// "host algorithm fingerprint". +func ParseKnownHost(text string) (string, string, []byte, bool, error) { + m := knownHostRX.FindStringSubmatch(text) + if len(m) == 0 { + return "", "", nil, false, nil + } + buf, err := hex.DecodeString(m[3]) + if err != nil { + return "", "", nil, false, goof.WithError( + "error decoding known host fingerprint", err) + } + return m[1], m[2], buf, true, nil +} + // ParseTLSConfig returns a new TLS configuration. func ParseTLSConfig( ctx types.Context, config gofig.Config, fields log.Fields, - roots ...string) (*types.TLSConfig, error) { + roots ...string) (tlsConfig *types.TLSConfig, tlsErr error) { ctx.Debug("parsing tls config") @@ -33,6 +51,162 @@ func ParseTLSConfig( fields[k] = v } + // defer the parsing of the cert, key, and cacerts files so that no + // matter how tls is configured these files might be loaded. This behavior + // is to accomodate the fact that the files can be placed in default + // locations and thus there is no reason not to use them if they are + // placed in their known locations + defer func() { + if tlsConfig == nil { + return + } + + defer func() { + if tlsErr != nil { + tlsConfig = nil + ctx.Error(tlsErr) + } + }() + + // always check for the user's known_hosts file + func() { + khFile := path.Join(gotil.HomeDir(), ".libstorage", "known_hosts") + if gotil.FileExists(khFile) { + tlsConfig.UsrKnownHosts = khFile + tlsConfig.VerifyPeers = true + } + }() + + // always check for the system's known_hosts file + if tlsErr = func() error { + if !isSet(config, types.ConfigTLSKnownHosts, roots...) { + return nil + } + khFile := getString(config, types.ConfigTLSKnownHosts, roots...) + + // is the known_hosts file the same as the default known_hosts + // file? It's not possible to use os.SameFile as the files may not + // yet exist + isDefKH := strings.EqualFold( + khFile, types.DefaultTLSKnownHosts.Path()) + + if !gotil.FileExists(khFile) { + if !isDefKH { + return goof.WithField( + "path", khFile, "invalid known_hosts file") + } + return nil + } + + f(types.ConfigTLSKnownHosts, khFile) + + tlsConfig.SysKnownHosts = khFile + tlsConfig.VerifyPeers = true + + return nil + }(); tlsErr != nil { + return + } + + // always check for the cacerts file + if tlsErr = func() error { + if !isSet(config, types.ConfigTLSTrustedCertsFile, roots...) { + return nil + } + + caCerts := getString( + config, types.ConfigTLSTrustedCertsFile, roots...) + + // is the key file the same as the default cacerts file? It's not + // possible to use os.SameFile as the files may not yet exist + isDefCA := strings.EqualFold( + caCerts, types.DefaultTLSTrustedRootsFile.Path()) + + if !gotil.FileExists(caCerts) { + if !isDefCA { + return goof.WithField( + "path", caCerts, "invalid cacerts file") + } + return nil + } + + f(types.ConfigTLSTrustedCertsFile, caCerts) + + buf, err := func() ([]byte, error) { + f, err := os.Open(caCerts) + if err != nil { + return nil, goof.WithFieldE( + "path", caCerts, "error opening cacerts file", err) + } + defer f.Close() + buf, err := ioutil.ReadAll(f) + if err != nil { + return nil, goof.WithFieldE( + "path", caCerts, "error reading cacerts file", err) + } + return buf, nil + }() + if err != nil { + return err + } + + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(buf) + tlsConfig.RootCAs = certPool + tlsConfig.ClientCAs = certPool + + return nil + }(); tlsErr != nil { + return + } + + // always check for the cert and key files + tlsErr = func() error { + if !isSet(config, types.ConfigTLSKeyFile, roots...) { + return nil + } + keyFile := getString(config, types.ConfigTLSKeyFile, roots...) + + // is the key file the same as the default key file? It's not + // possible to use os.SameFile as the files may not yet exist + isDefKF := strings.EqualFold( + keyFile, types.DefaultTLSKeyFile.Path()) + + if !gotil.FileExists(keyFile) { + if !isDefKF { + return goof.WithField( + "path", keyFile, "invalid key file") + } + return nil + } + + f(types.ConfigTLSKeyFile, keyFile) + + crtFile := getString(config, types.ConfigTLSCertFile, roots...) + + // is the key file the same as the default cert file? It's not + // possible to use os.SameFile as the files may not yet exist + isDefCF := strings.EqualFold( + crtFile, types.DefaultTLSCertFile.Path()) + + if !gotil.FileExists(crtFile) { + if !isDefCF { + return goof.WithField( + "path", crtFile, "invalid crt file") + } + return nil + } + + f(types.ConfigTLSCertFile, crtFile) + cer, err := tls.LoadX509KeyPair(crtFile, keyFile) + if err != nil { + return goof.WithError("error loading x509 pair", err) + } + tlsConfig.Certificates = []tls.Certificate{cer} + return nil + }() + }() + if !isSet(config, types.ConfigTLS, roots...) { ctx.Info("tls not configured") return nil, nil @@ -53,7 +227,7 @@ func ParseTLSConfig( } if v := getString(config, types.ConfigTLS, roots...); v != "" { - // check to see if TLS is enabled with a simple insecure value + // check to see if TLS is enabled with insecure if strings.EqualFold(v, "insecure") { f(types.ConfigTLS, "insecure") ctx.WithField(types.ConfigTLS, "insecure").Info("tls enabled") @@ -62,25 +236,32 @@ func ParseTLSConfig( }, nil } + // check to see if TLS is enabled with peers + if strings.EqualFold(v, "verifyPeers") { + f(types.ConfigTLS, "verifyPeers") + ctx.WithField(types.ConfigTLS, "verifyPeers").Info("tls enabled") + return &types.TLSConfig{ + Config: tls.Config{InsecureSkipVerify: true}, + VerifyPeers: true, + }, nil + } + // check to see if TLS is enabled with an expected sha256 fingerprint - shaRX := regexp.MustCompile(`^(?i)sha256:(.+)$`) - if m := shaRX.FindStringSubmatch(v); len(m) > 0 { + if _, _, buf, ok, err := ParseKnownHost(v); err != nil { + ctx.Error(err) + return nil, err + } else if ok { ctx.WithField(types.ConfigTLS, v).Info("tls enabled") - s := strings.Join(strings.Split(m[1], ":"), "") - buf, err := hex.DecodeString(s) - if err != nil { - ctx.WithError(err).Error("error decoding tls cert fingerprint") - return nil, err - } return &types.TLSConfig{ Config: tls.Config{InsecureSkipVerify: true}, + VerifyPeers: true, PeerFingerprint: buf, }, nil } } // tls is enabled; figure out its configuration - tlsConfig := &types.TLSConfig{Config: tls.Config{}} + tlsConfig = &types.TLSConfig{Config: tls.Config{}} // if the tls config is set to insecure, then mark it as so insecure := getBool(config, types.ConfigTLSInsecure, roots...) @@ -89,22 +270,11 @@ func ParseTLSConfig( tlsConfig.InsecureSkipVerify = true } - if isSet(config, types.ConfigTLSKeyFile, roots...) { - keyFile := getString(config, types.ConfigTLSKeyFile, roots...) - if !gotil.FileExists(keyFile) { - return nil, goof.WithField("path", keyFile, "invalid key file") - } - f(types.ConfigTLSKeyFile, keyFile) - certFile := getString(config, types.ConfigTLSCertFile, roots...) - if !gotil.FileExists(certFile) { - return nil, goof.WithField("path", certFile, "invalid cert file") - } - f(types.ConfigTLSCertFile, certFile) - cer, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cer} + verifyPeers := getBool(config, types.ConfigTLSVerifyPeers, roots...) + if verifyPeers { + f(types.ConfigTLSVerifyPeers, true) + tlsConfig.VerifyPeers = true + tlsConfig.InsecureSkipVerify = true } if isSet(config, types.ConfigTLSServerName, roots...) { @@ -122,38 +292,5 @@ func ParseTLSConfig( f(types.ConfigTLSClientCertRequired, clientCertRequired) } - if isSet(config, types.ConfigTLSTrustedCertsFile, roots...) { - trustedCertsFile := getString( - config, types.ConfigTLSTrustedCertsFile, roots...) - - if !gotil.FileExists(trustedCertsFile) { - return nil, goof.WithField( - "path", trustedCertsFile, "invalid trust file") - } - - f(types.ConfigTLSTrustedCertsFile, trustedCertsFile) - - buf, err := func() ([]byte, error) { - f, err := os.Open(trustedCertsFile) - if err != nil { - return nil, err - } - defer f.Close() - buf, err := ioutil.ReadAll(f) - if err != nil { - return nil, err - } - return buf, nil - }() - if err != nil { - return nil, err - } - - certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(buf) - tlsConfig.RootCAs = certPool - tlsConfig.ClientCAs = certPool - } - return tlsConfig, nil } diff --git a/drivers/storage/libstorage/libstorage_client.go b/drivers/storage/libstorage/libstorage_client.go index 15b29319..b0dd92a9 100644 --- a/drivers/storage/libstorage/libstorage_client.go +++ b/drivers/storage/libstorage/libstorage_client.go @@ -21,6 +21,7 @@ type client struct { types.APIClient ctx types.Context config gofig.Config + tlsConfig *types.TLSConfig clientType types.ClientType lsxCache *lss serviceCache *lss diff --git a/drivers/storage/libstorage/libstorage_driver.go b/drivers/storage/libstorage/libstorage_driver.go index 16eaa68f..5ea49329 100644 --- a/drivers/storage/libstorage/libstorage_driver.go +++ b/drivers/storage/libstorage/libstorage_driver.go @@ -1,11 +1,7 @@ package libstorage import ( - "bytes" - "crypto/sha256" "crypto/tls" - "encoding/hex" - "errors" "io/ioutil" "net" "net/http" @@ -41,8 +37,6 @@ func newDriver() types.StorageDriver { return &driver{} } -var errServerFingerprint = errors.New("invalid server fingerprint") - func (d *driver) Init(ctx types.Context, config gofig.Config) error { logFields := log.Fields{} @@ -91,7 +85,12 @@ func (d *driver) Init(ctx types.Context, config gofig.Config) error { httpTransport := &http.Transport{ Dial: func(string, string) (net.Conn, error) { if tlsConfig == nil { - return net.Dial(proto, lAddr) + conn, err := net.Dial(proto, lAddr) + if err != nil { + return nil, err + } + d.ctx.Debug("successful connection") + return conn, nil } conn, err := tls.Dial(proto, lAddr, &tlsConfig.Config) @@ -99,31 +98,28 @@ func (d *driver) Init(ctx types.Context, config gofig.Config) error { return nil, err } - if len(tlsConfig.PeerFingerprint) > 0 { - peerCerts := conn.ConnectionState().PeerCertificates - matchedFingerprint := false - expectedFP := hex.EncodeToString(tlsConfig.PeerFingerprint) - for _, cert := range peerCerts { - h := sha256.New() - h.Write(cert.Raw) - certFP := h.Sum(nil) - actualFP := hex.EncodeToString(certFP) - d.ctx.WithFields(log.Fields{ - "actualFingerprint": actualFP, - "expectedFingerprint": expectedFP, - }).Debug("comparing tls fingerprints") - if bytes.EqualFold(tlsConfig.PeerFingerprint, certFP) { - matchedFingerprint = true - d.ctx.WithFields(log.Fields{ - "actualFingerprint": actualFP, - "expectedFingerprint": expectedFP, - }).Debug("matched tls fingerprints") - break - } - } - if !matchedFingerprint { - return nil, errServerFingerprint - } + if !tlsConfig.VerifyPeers { + d.ctx.Debug("successful tls connection; not verifying peers") + return conn, nil + } + + if err := verifyPeerFingerprint( + d.ctx, + conn.ConnectionState().PeerCertificates, + tlsConfig.PeerFingerprint); err != nil { + + d.ctx.WithError(err).Error("error matching peer fingerprint") + return nil, err + } + + if err := verifyKnownHosts( + d.ctx, + conn.ConnectionState().PeerCertificates, + tlsConfig.UsrKnownHosts, + tlsConfig.SysKnownHosts); err != nil { + + d.ctx.WithError(err).Error("error matching known host") + return nil, err } return conn, nil @@ -146,6 +142,7 @@ func (d *driver) Init(ctx types.Context, config gofig.Config) error { APIClient: apiClient, ctx: d.ctx, config: config, + tlsConfig: tlsConfig, clientType: cliType, serviceCache: &lss{Store: utils.NewStore()}, } diff --git a/drivers/storage/libstorage/libstorage_driver_tls.go b/drivers/storage/libstorage/libstorage_driver_tls.go new file mode 100644 index 00000000..9cc8a5f5 --- /dev/null +++ b/drivers/storage/libstorage/libstorage_driver_tls.go @@ -0,0 +1,124 @@ +package libstorage + +import ( + "bufio" + "bytes" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "errors" + "os" + + "github.com/codedellemc/libstorage/api/types" + "github.com/codedellemc/libstorage/api/utils" +) + +var errServerFingerprint = errors.New("invalid server fingerprint") +var errKnownHost = errors.New("not found in known_hosts file(s)") + +func verifyPeerFingerprint( + ctx types.Context, + peerCerts []*x509.Certificate, + peerFingerprint []byte) error { + + if len(peerFingerprint) == 0 { + return nil + } + + expectedFP := hex.EncodeToString(peerFingerprint) + for _, cert := range peerCerts { + h := sha256.New() + h.Write(cert.Raw) + certFP := h.Sum(nil) + actualFP := hex.EncodeToString(certFP) + ctx.WithFields(map[string]interface{}{ + "actualFingerprint": actualFP, + "expectedFingerprint": expectedFP, + }).Debug("comparing tls fingerprints") + if bytes.EqualFold(peerFingerprint, certFP) { + ctx.WithFields(map[string]interface{}{ + "actualFingerprint": actualFP, + "expectedFingerprint": expectedFP, + }).Debug("matched tls fingerprints") + return nil + } + } + return errServerFingerprint +} + +func verifyKnownHosts( + ctx types.Context, + peerCerts []*x509.Certificate, + usrKnownHostsFilePath, + sysKnownHostsFilePath string) error { + + if len(usrKnownHostsFilePath) == 0 && len(sysKnownHostsFilePath) == 0 { + return nil + } + + if len(usrKnownHostsFilePath) > 0 { + err := verifyKnownHostsFile(ctx, peerCerts, usrKnownHostsFilePath) + if err == nil { + return nil + } + if err != errKnownHost { + return err + } + } + + if len(sysKnownHostsFilePath) > 0 { + return verifyKnownHostsFile(ctx, peerCerts, sysKnownHostsFilePath) + } + + return errKnownHost +} + +func verifyKnownHostsFile( + ctx types.Context, + peerCerts []*x509.Certificate, + knownHostsFilePath string) error { + + r, err := os.Open(knownHostsFilePath) + if err != nil { + ctx.WithField("path", knownHostsFilePath).Error( + "error opening known_hosts file") + return err + } + defer r.Close() + + ctx.WithField("path", knownHostsFilePath).Debug("opened known_hosts file") + + scn := bufio.NewScanner(r) + for scn.Scan() { + // TODO compare host + _, _, sig, ok, err := utils.ParseKnownHost(scn.Text()) + if err != nil { + ctx.WithField("path", knownHostsFilePath).Error( + "error scanning known_hosts file") + return err + } + if !ok { + continue + } + expectedFP := hex.EncodeToString(sig) + for _, cert := range peerCerts { + h := sha256.New() + h.Write(cert.Raw) + certFP := h.Sum(nil) + actualFP := hex.EncodeToString(certFP) + ctx.WithFields(map[string]interface{}{ + "actualFingerprint": actualFP, + "expectedFingerprint": expectedFP, + }).Debug("comparing known_hosts fingerprints") + if bytes.EqualFold(sig, certFP) { + ctx.WithFields(map[string]interface{}{ + "actualFingerprint": actualFP, + "expectedFingerprint": expectedFP, + }).Debug("matched known_hosts fingerprints") + return nil + } + } + } + + return errKnownHost +} diff --git a/imports/config/imports_config_99_gofig.go b/imports/config/imports_config_99_gofig.go index fa25355e..0bcb3f23 100644 --- a/imports/config/imports_config_99_gofig.go +++ b/imports/config/imports_config_99_gofig.go @@ -83,6 +83,32 @@ func init() { rk(gofig.String, "0s", "", types.ConfigServerTasksLogTimeout) rk(gofig.Bool, false, "", types.ConfigServerParseRequestOpts) + // tls config + rk( + gofig.String, + types.DefaultTLSCertFile.Path(), + "", + types.ConfigTLSCertFile) + rk( + gofig.String, + types.DefaultTLSKeyFile.Path(), + "", + types.ConfigTLSKeyFile) + rk( + gofig.String, + types.DefaultTLSTrustedRootsFile.Path(), + "", + types.ConfigTLSTrustedCertsFile) + rk( + gofig.String, + types.DefaultTLSKnownHosts.Path(), + "", + types.ConfigTLSKnownHosts) + rk(gofig.String, "", "", types.ConfigTLSServerName) + rk(gofig.Bool, false, "", types.ConfigTLSDisabled) + rk(gofig.Bool, false, "", types.ConfigTLSInsecure) + rk(gofig.Bool, false, "", types.ConfigTLSClientCertRequired) + // auth config - client rk(gofig.String, "", "", types.ConfigClientAuthToken)