From 8485e737a92f817dc2998112a76853a0904fafc0 Mon Sep 17 00:00:00 2001 From: akutz Date: Wed, 19 Apr 2017 00:01:42 -0500 Subject: [PATCH] Security Enhancements This patch enhances libStorage security: * If discovered in '$LIBSTORAGE_PATHS_TLS' the following files are automatically loaded: * `libstorage.crt` * `libstorage.key` * `cacerts` * If `$LIBSTORAGE_PATHS_ETC/known_hosts` exists it is automatically loaded unless the property `libstorage.tls.knownHosts` is explicitly defined. This is the system's `known_hosts` file. * If `$HOME/.libstorage/known_hosts` exists it is automatically used when TLS security is set to verify peer certificates. This is the user's `known_hosts` file. * The above `known_hosts` files are line-delimited with each line following the format: 'HOST ALGORITHM FINGERPRINT' * When matching a remote host's known host information the peer's host (derived from the certificate's Subject.CommonName) is also taken into account. Thus if a host is marked as trusted but later provides a different certificate during TLS negotiation the connection will fail. This is identical to SSH's known host logic. * The property `libstorage.tls.verifyPeers` is introduced. It's a boolean flag that indicates TLS connections should be verified against a known list of peer certificate fingerprints in the system's and user's `known_hosts` files. Enabling this property also sets `libstorage.tls.insecure` to `true`. The connection will be encrypted, but the certificate verification is disabled and deferred to the peer verification. * The property `libstorage.tls` can now be set to a simple string value of `verifyPeers` to indicate TLS connections should be verified against the system's and user's `known_hosts` files. --- .docs/user-guide/config.md | 205 +++++++++--- .tls/known_hosts | 1 + .travis.yml | 25 +- api/tests/tests.go | 116 +++++-- api/types/types_config.go | 6 + api/types/types_errors.go | 33 ++ api/types/types_paths.go | 49 ++- api/types/types_paths_test.go | 5 + api/types/types_tls.go | 28 +- api/utils/utils_config.go | 12 + api/utils/utils_tls.go | 301 +++++++++++++----- .../storage/libstorage/libstorage_client.go | 1 + .../storage/libstorage/libstorage_driver.go | 61 ++-- .../libstorage/libstorage_driver_tls.go | 155 +++++++++ drivers/storage/vfs/tests/vfs_test.go | 142 +++++++++ imports/config/imports_config_99_gofig.go | 26 ++ 16 files changed, 961 insertions(+), 205 deletions(-) create mode 100644 .tls/known_hosts create mode 100644 drivers/storage/libstorage/libstorage_driver_tls.go diff --git a/.docs/user-guide/config.md b/.docs/user-guide/config.md index 5a0785e9..744c130f 100644 --- a/.docs/user-guide/config.md +++ b/.docs/user-guide/config.md @@ -302,8 +302,64 @@ and a more verbose, `info` log level for just the server. The following sections detail every last aspect of how `libStorage` works and can be configured. +### Data Directories +The first time libStorage is executed it will create several directories if +they do not already exist: + +* `/etc/libstorage` +* `/etc/libstorage/tls` +* `/var/lib/libstorage` +* `/var/log/libstorage` +* `/var/run/libstorage` + +The above directories will contain configuration files, logs, PID files, and +mounted volumes. + +The location of these directories can be influenced in two ways. The first way +is via the environment variable `LIBSTORAGE_HOME`. When `LIBSTORAGE_HOME` is +defined, the normal, final token of the above paths is removed. Thus when +`LIBSTORAGE_HOME` is defined as `/opt/libstorage` the +above directory paths would be: + +* `/opt/libstorage/etc` +* `/opt/libstorage/etc/tls` +* `/opt/libstorage/var/lib` +* `/opt/libstorage/var/log` +* `/opt/libstorage/var/run` + +It's also possible to override any one of the above directory paths manually +using the following environment variables: + +* `LIBSTORAGE_HOME_ETC` +* `LIBSTORAGE_HOME_ETC_TLS` +* `LIBSTORAGE_HOME_LIB` +* `LIBSTORAGE_HOME_LOG` +* `LIBSTORAGE_HOME_RUN` + +Thus if `LIBSTORAGE_HOME` was set to `/opt/libstorage` and +`LIBSTORAGE_HOME_ETC` was set to `/etc/libstorage` the above paths would be: + +* `/etc/libstorage` +* `/etc/libstorage/tls` +* `/opt/libstorage/var/lib` +* `/opt/libstorage/var/log` +* `/opt/libstorage/var/run` + ### TLS Configuration -This section reviews the several supported TLS configuration options. +This section reviews the several supported TLS configuration options. The table +below lists the default locations of the TLS-related files. + +Directory | File | Property | Description +----------|-----|-----------|------------ +`$LIBSTORAGE_HOME_ETC_TLS` | `libstorage.crt` | `libstorage.tls.crtFile` | The public key. +| `libstorage.key` | `libstorage.tls.keyFile` | The private key. +| `cacerts` | `libstorage.tls.trustedCertsFile` | The trusted key ring. +| `known_hosts` | `libstorage.tls.knownHosts` | The system known hosts file. + +If libStorage detects any of the above files, the detected files are loaded +when necessary and without any explicit configuration. However, if a file's +related configuration property is set explicitly to some other, non-default +value, the default file will not be loaded even if it is present. #### Insecure TLS The following example illustrates how to configure the libStorage client to @@ -334,62 +390,120 @@ libstorage: certificate provided by the server. This is a security risk and should not ever be used in production. -#### Server Cert Fingerprint -While TLS should never be configured as insecure in production, a compromise -between no TLS and insecure TLS is specifying a server certificate's SHA256 -fingerprint. +#### Peer Verification +While TLS should never be configured as insecure in production, there is a +compromise that enables an encrypted connection while still providing some +measure of verification of the remote endpoint's identity -- peer verification. + +When peer verification mode is enabled, TLS is implicitly configured to operate +as insecure in order to disable server-side certificate verification. This +enables an encrypted transport while delegating the authenticity of the +server's identity to the peer verification process. + +The first step to configuring peer verification is to obtain the information +about the peer used to identify it. First, obtain the peer's certificate and +store it locally. This step can be omitted if the remote peer's certificate +is already available locally. -For example, the following command can be used to print google.com's SHA256 -fingerprint: +```bash +$ openssl s_client -connect google.com:443 2>/dev/null my.crt +``` + +Once the remote certificate is available locally it can be used to generate +its identifying information: ```bash -$ openssl s_client -connect google.com:443 + $ openssl s_client -connect google.com:443 2>/dev/null my.crt && \ + echo $(cat my.crt | openssl x509 -noout -subject | \ + awk 'BEGIN { FS = "/" }; { print $NF }' | \ + cut -c4-) sha256 $(cat my.crt | \ + openssl x509 -noout -fingerprint -sha256 | cut -c20-) && \ + rm -f my.crt + +##### Simple Peer Verification +With the remote peer's identifying information in hand it is possible to +enable peer verification on the client by setting the property +`libstorage.tls` to the remote peer's identifier string: ```yaml libstorage: - host: tcp://127.0.0.1:7979 client: - tls: "sha256:15:92:77:BE:6C:90:D3:FB:59:29:9C:51:A7:DB:5C:16:55:BD:B9:9E:E7:7E:C1:9B:30:C3:74:99:21:5F:08:99" - server: - tls: - certFile: /etc/libstorage/libstorage-server.crt - keyFile: /etc/libstorage/libstorage-server.key - services: - virtualbox: - driver: virtualbox - virtualbox: - endpoint: http://10.0.2.2:18083 - tls: false - volumePath: $HOME/VirtualBox/Volumes - controllerName: SATA + tls: *.google.com sha256 14:8F:93:BE:EA:AB:68:CE:C8:03:0D:0B:0D:54:C3:59:4C:18:55:5D:2D:7E:4E:8C:68:9E:D4:59:33:3C:68:96 ``` -With `DEBUG` level logging enabled, the libStorage client logs reflect the -matched fingerprint: +The above approach does result in the client attempting a TLS connection to +the configured, remote host. However, the peer verification will only be +valid for a single peer. + +##### Advanced Peer Verification +While simple peer verification works for a single, remote host, sometimes it +is necessary to enable peer verification for multiple remote hosts. This +configuration requires a _known hosts_ file. + +For people that use SSH the concept of a known hosts file should feel familiar. +In fact, libStorage copies the format of SSH's known hosts file entirely. The +file adheres to a line-delimited format: ```bash -DEBU[0000] comparing tls fingerprints actualFingerprint=159277be6c90d3fb59299c51a7db5c1655bdb99ee77ec19b30c37499215f0899 expectedFingerprint=159277be6c90d3fb59299c51a7db5c1655bdb99ee77ec19b30c37499215f0899 service=virtualbox storageDriver=libstorage time=1488821773182 -DEBU[0000] matched tls fingerprints actualFingerprint=159277be6c90d3fb59299c51a7db5c1655bdb99ee77ec19b30c37499215f0899 expectedFingerprint=159277be6c90d3fb59299c51a7db5c1655bdb99ee77ec19b30c37499215f0899 service=virtualbox storageDriver=libstorage time=1488821773182 +ls-svr-01 sha256 15:92:77:BE:6C:90:D3:FB:59:29:9C:51:A7:DB:5C:16:55:BD:B9:9E:E7:7E:C1:9B:30:C3:74:99:21:5F:08:6A +ls-svr-02 sha256 15:92:77:BE:6C:90:D3:FB:59:29:9C:51:A7:DB:5C:16:55:BD:B9:9E:E7:7E:C1:9B:30:C3:74:99:21:5F:08:6C +ls-svr-03 sha256 15:92:77:BE:6C:90:D3:FB:59:29:9C:51:A7:DB:5C:16:55:BD:B9:9E:E7:7E:C1:9B:30:C3:74:99:21:5F:08:6D ``` +The known hosts file can be specified via the property +`libstorage.tls.knownHosts`. This is the _system_ known hosts file. If this +property is not explicitly configured then libStorage checks for the file +`$LIBSTORAGE_HOME_ETC_TLS/known_hosts`. libStorage also looks for the _user_ +known hosts file at `$HOME/.libstorage/known_hosts`. + +Thus if a known hosts file is present at either of the default system or +user locations, it's possible to take advantage of them with a configuration +similar to the following: + +```yaml +libstorage: + client: + tls: verifyPeers +``` + +The above configuration snippet indicates that TLS is enabled with peer +verification. Because no known hosts file is specified the default paths are +checked for any known host files. To enable peer verification with a custom +system known hosts file the following configuration can be used: + +```yaml +libstorage: + client: + tls: + verifyPeers: true + knownHosts: /tmp/known_hosts +``` + +The above configuration snippet indicates that TLS is enabled and set to +peer verification mode and that the system known hosts file is located at +`/tmp/known_hosts`. + #### Trusted Certs File This TLS configuration example describes how to instruct the libStorage client to validate the provided server-side certificate using a custom trusted CA file. @@ -656,21 +770,6 @@ manage its own configuration and supply the embedded `libStorage` instance directly with a configuration object. In this scenario, the `libStorage` configuration files are ignored in deference to the embedding application. -### Data Directories -The first time `libStorage` is executed it will create several directories if -they do not already exist: - -* `/etc/libstorage` -* `/var/log/libstorage` -* `/var/run/libstorage` -* `/var/lib/libstorage` - -The above directories will contain configuration files, logs, PID files, and -mounted volumes. However, the location of these directories can also be -influenced with the environment variable `LIBSTORAGE_HOME`. All of the above -data directories will be placed in their same paths, but prefixed by the path -specified via `LIBSTORAGE_HOME`, if `LIBSTORAGE_HOME` is in fact specified. - ### Configuration Methods There are three ways to configure `libStorage`: diff --git a/.tls/known_hosts b/.tls/known_hosts new file mode 100644 index 00000000..5f87c9be --- /dev/null +++ b/.tls/known_hosts @@ -0,0 +1 @@ +libstorage-server sha256 52:C7:5D:00:1B:E7:33:66:14:3C:47:07:77:59:9C:94:F1:EA:76:00:41:B1:9D:71:0B:80:05:1F:F7:2D:6B:69 diff --git a/.travis.yml b/.travis.yml index ec1ee49e..2094f270 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,15 +12,37 @@ os: env: - BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - VFS_INSTANCEID_USE_FIELDS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS_PEERS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" - BUILD_TAGS="gofig pflag libstorage_integration_driver_linux" - BUILD_TAGS="" matrix: + fast_finish: true allow_failures: - go: 1.6.3 - go: 1.7.5 - go: tip - fast_finish: true + exclude: + - go: 1.6.3 + env: VFS_INSTANCEID_USE_FIELDS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: 1.6.3 + env: LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: 1.6.3 + env: LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS_PEERS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: 1.7.5 + env: VFS_INSTANCEID_USE_FIELDS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: 1.7.5 + env: LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: 1.7.5 + env: LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS_PEERS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: tip + env: VFS_INSTANCEID_USE_FIELDS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: tip + env: LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" + - go: tip + env: LIBSTORAGE_TEST_TCP=false LIBSTORAGE_TEST_TCP_TLS_PEERS=true BUILD_TAGS="gofig pflag libstorage_integration_driver_linux libstorage_storage_driver libstorage_storage_driver_vfs libstorage_storage_executor libstorage_storage_executor_vfs" before_install: - if [ "$BUILD_TAGS" = "$DEFAULT_BUILD_TAGS" ] && [ "$TRAVIS_GO_VERSION" = "$COVERED_GO_VERSION" ]; then echo coverage enabled; fi @@ -37,7 +59,6 @@ script: - make gometalinter-all - make -j build - if [ "$BUILD_TAGS" = "$DEFAULT_BUILD_TAGS" ] && [ "$TRAVIS_GO_VERSION" = "$COVERED_GO_VERSION" ]; then make -j test; fi - - if [ "$BUILD_TAGS" = "$DEFAULT_BUILD_TAGS" ] && [ "$TRAVIS_GO_VERSION" = "$COVERED_GO_VERSION" ]; then VFS_INSTANCEID_USE_FIELDS=true ./drivers/storage/vfs/tests/vfs.test; fi after_success: - if [ "$BUILD_TAGS" = "$DEFAULT_BUILD_TAGS" ] && [ "$TRAVIS_GO_VERSION" = "$COVERED_GO_VERSION" ]; then make -j cover; fi diff --git a/api/tests/tests.go b/api/tests/tests.go index 2223348c..7a9da939 100644 --- a/api/tests/tests.go +++ b/api/tests/tests.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path" "runtime" "strconv" "sync" @@ -34,8 +35,13 @@ var ( lsxDarwinInfo, _ = executors.ExecutorInfoInspect("lsx-darwin", false) // lsxWindowsInfo, _ = executors.ExecutorInfoInspect("lsx-windows.exe", false) - tcpTest bool - tcpTLSTest, _ = strconv.ParseBool(os.Getenv("LIBSTORAGE_TEST_TCP_TLS")) + tcpTest bool + + tcpTLSTest, _ = strconv.ParseBool( + os.Getenv("LIBSTORAGE_TEST_TCP_TLS")) + tcpTLSPeersTest, _ = strconv.ParseBool( + os.Getenv("LIBSTORAGE_TEST_TCP_TLS_PEERS")) + sockTest, _ = strconv.ParseBool(os.Getenv("LIBSTORAGE_TEST_SOCK")) sockTLSTest, _ = strconv.ParseBool(os.Getenv("LIBSTORAGE_TEST_SOCK_TLS")) @@ -59,13 +65,15 @@ func init() { } var ( - tlsPath = fmt.Sprintf( - "%s/src/github.com/codedellemc/libstorage/.tls", os.Getenv("GOPATH")) - serverCrt = fmt.Sprintf("%s/libstorage-server.crt", tlsPath) - serverKey = fmt.Sprintf("%s/libstorage-server.key", tlsPath) - clientCrt = fmt.Sprintf("%s/libstorage-client.crt", tlsPath) - clientKey = fmt.Sprintf("%s/libstorage-client.key", tlsPath) - trustedCerts = fmt.Sprintf("%s/libstorage-ca.crt", tlsPath) + tlsPath = path.Join( + os.Getenv("GOPATH"), + "/src/github.com/codedellemc/libstorage/.tls") + serverCrt = path.Join(tlsPath, "libstorage-server.crt") + serverKey = path.Join(tlsPath, "libstorage-server.key") + clientCrt = path.Join(tlsPath, "libstorage-client.crt") + clientKey = path.Join(tlsPath, "libstorage-client.key") + trustedCerts = path.Join(tlsPath, "libstorage-ca.crt") + knownHosts = path.Join(tlsPath, "known_hosts") ) var ( @@ -236,6 +244,7 @@ func (th *testHarness) run( wg.Add(1) go func(x int, config gofig.Config) { + defer wg.Done() server, errs, err := apiserver.Serve(nil, config) if err != nil { @@ -253,12 +262,10 @@ func (th *testHarness) run( th.servers = append(th.servers, server) c, err := client.New(nil, config) - if err != nil { - if onNewClientError != nil { - onNewClientError(err) - } else { - t.Fatal(err) - } + if onNewClientError != nil { + onNewClientError(err) + } else if err != nil { + t.Fatal(err) } else if !assert.NotNil(t, c) { t.FailNow() } else { @@ -295,19 +302,19 @@ func (th *testHarness) run( err := <-errs if err != nil { th.closeServers(t) - t.Fatalf("server (%s) error: %v", configNames[x], err) + t.Fatalf( + "server (%s) error: %v", + configNames[x], err) } }() th.servers = append(th.servers, server) c, err := client.New(nil, config) - if err != nil { - if onNewClientError != nil { - onNewClientError(err) - } else { - t.Fatal(err) - } + if onNewClientError != nil { + onNewClientError(err) + } else if err != nil { + t.Fatal(err) } else if !assert.NotNil(t, c) { t.FailNow() } else { @@ -404,19 +411,39 @@ func getTestConfigs( if tcpTest { configNames[len(configNames)] = "tcp" - configs = append(configs, config.Scope("libstorage.tests.tcp")) + configs = append(configs, config.Scope( + "libstorage.tests.tcp").Scope( + "testing")) } if tcpTLSTest { configNames[len(configNames)] = "tcpTLS" - configs = append(configs, config.Scope("libstorage.tests.tcpTLS")) + configs = append(configs, config.Scope( + "libstorage.tests.tcpTLS").Scope( + "test")) + } + if tcpTLSPeersTest { + configNames[len(configNames)] = "tcpTLSPeers" + configs = append(configs, config.Scope( + "libstorage.tests.tcpTLSPeers").Scope( + "test")) } if sockTest { configNames[len(configNames)] = "unix" - configs = append(configs, config.Scope("libstorage.tests.unix")) + configs = append(configs, config.Scope( + "libstorage.tests.unix").Scope( + "test")) + } + if sockTLSTest { + configNames[len(configNames)] = "unixTLS" + configs = append(configs, config.Scope( + "libstorage.tests.unixTLS").Scope( + "test")) } if sockTLSTest { configNames[len(configNames)] = "unixTLS" - configs = append(configs, config.Scope("libstorage.tests.unixTLS")) + configs = append(configs, config.Scope( + "libstorage.tests.unixTLSPeers").Scope( + "test")) } return configNames, configs @@ -439,7 +466,13 @@ func initTestConfigs(config map[string]interface{}) { unixHost := fmt.Sprintf("unix://%s", utils.GetTempSockFile()) unixTLSHost := fmt.Sprintf("unix://%s", utils.GetTempSockFile()) - clientTLSConfig := func() map[string]interface{} { + clientTLSConfig := func(peers bool) map[string]interface{} { + if peers { + return map[string]interface{}{ + "verifyPeers": true, + "knownHosts": knownHosts, + } + } return map[string]interface{}{ "serverName": "libstorage-server", "certFile": clientCrt, @@ -448,13 +481,13 @@ func initTestConfigs(config map[string]interface{}) { } } - serverTLSConfig := func() map[string]interface{} { + serverTLSConfig := func(clientCertRequired bool) map[string]interface{} { return map[string]interface{}{ "serverName": "libstorage-server", "certFile": serverCrt, "keyFile": serverKey, "trustedCertsFile": trustedCerts, - "clientCertRequired": true, + "clientCertRequired": clientCertRequired, } } @@ -480,12 +513,29 @@ func initTestConfigs(config map[string]interface{}) { "endpoints": map[string]interface{}{ "localhost": map[string]interface{}{ "address": tcpTLSHost, - "tls": serverTLSConfig(), + "tls": serverTLSConfig(true), + }, + }, + }, + "client": map[string]interface{}{ + "tls": clientTLSConfig(false), + }, + }, + }, + + "tcpTLSPeers": map[string]interface{}{ + "libstorage": map[string]interface{}{ + "host": tcpTLSHost, + "server": map[string]interface{}{ + "endpoints": map[string]interface{}{ + "localhost": map[string]interface{}{ + "address": tcpTLSHost, + "tls": serverTLSConfig(false), }, }, }, "client": map[string]interface{}{ - "tls": clientTLSConfig(), + "tls": clientTLSConfig(true), }, }, }, @@ -510,12 +560,12 @@ func initTestConfigs(config map[string]interface{}) { "endpoints": map[string]interface{}{ "localhost": map[string]interface{}{ "address": unixTLSHost, - "tls": serverTLSConfig(), + "tls": serverTLSConfig(true), }, }, }, "client": map[string]interface{}{ - "tls": clientTLSConfig(), + "tls": clientTLSConfig(false), }, }, }, 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_errors.go b/api/types/types_errors.go index 1bd2b8c6..8d7f03d6 100644 --- a/api/types/types_errors.go +++ b/api/types/types_errors.go @@ -82,3 +82,36 @@ type ErrSecTokInvalid struct { func (e *ErrSecTokInvalid) Error() string { return "invalid security token" } + +// ErrKnownHost occurs when the client's TLS dialer encounters a problem +// verifying the remote peer's certificate against a list of known host +// signatures. +type ErrKnownHost struct { + // PeerHost is the remote peer's host name. + PeerHost string + + // PeerAlg is algorithm used to calculate the remote peer's fingerprint. + PeerAlg string + + // PeerFingerprint is the remote peer's fingerprint. + PeerFingerprint []byte +} + +func (e *ErrKnownHost) Error() string { + return "error verifying the remote peer is a known host" +} + +// GetPeerHost returns the value of PeerHost. +func (e *ErrKnownHost) GetPeerHost() string { + return e.PeerHost +} + +// GetPeerAlg returns the value of PeerAlg. +func (e *ErrKnownHost) GetPeerAlg() string { + return e.PeerAlg +} + +// GetPeerFingerprint returns the value of PeerFingerprint. +func (e *ErrKnownHost) GetPeerFingerprint() []byte { + return e.PeerFingerprint +} diff --git a/api/types/types_paths.go b/api/types/types_paths.go index 63fcc304..d7ab13f8 100644 --- a/api/types/types_paths.go +++ b/api/types/types_paths.go @@ -45,22 +45,23 @@ func init() { libstorageHome = path.Join(gotil.HomeDir(), ".libstorage") } - if v := os.Getenv("LIBSTORAGE_PATHS_ETC"); v != "" && gotil.FileExists(v) { + if v := os.Getenv("LIBSTORAGE_HOME_ETC"); v != "" && gotil.FileExists(v) { etcEnvVarPath = v } - if v := os.Getenv("LIBSTORAGE_PATHS_LIB"); v != "" && gotil.FileExists(v) { + if v := os.Getenv("LIBSTORAGE_HOME_LIB"); v != "" && gotil.FileExists(v) { libEnvVarPath = v } - if v := os.Getenv("LIBSTORAGE_PATHS_LOG"); v != "" && gotil.FileExists(v) { + if v := os.Getenv("LIBSTORAGE_HOME_LOG"); v != "" && gotil.FileExists(v) { logEnvVarPath = v } - if v := os.Getenv("LIBSTORAGE_PATHS_RUN"); v != "" && gotil.FileExists(v) { + if v := os.Getenv("LIBSTORAGE_HOME_RUN"); v != "" && gotil.FileExists(v) { runEnvVarPath = v } - if v := os.Getenv("LIBSTORAGE_PATHS_TLS"); v != "" && gotil.FileExists(v) { + if v := os.Getenv( + "LIBSTORAGE_HOME_ETC_TLS"); v != "" && gotil.FileExists(v) { tlsEnvVarPath = v } - if v := os.Getenv("LIBSTORAGE_PATHS_LSX"); v != "" && gotil.FileExists(v) { + if v := os.Getenv("LIBSTORAGE_HOME_LSX"); v != "" && gotil.FileExists(v) { lsxEnvVarPath = v } @@ -109,6 +110,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 +184,10 @@ func (k fileKey) parent() fileKey { return Etc case LSX: return Lib + case DefaultTLSCertFile, + DefaultTLSKeyFile, + DefaultTLSTrustedRootsFile: + return TLS default: return Home } @@ -195,6 +216,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 +268,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..7982c362 100644 --- a/api/types/types_tls.go +++ b/api/types/types_tls.go @@ -7,6 +7,30 @@ import "crypto/tls" type TLSConfig struct { tls.Config - // PeerFingerprint is the expected SHA256 fingerprint of a peer certificate. - PeerFingerprint []byte + // 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 + + // KnownHost is the trusted, remote host information. + KnownHost *TLSKnownHost +} + +// TLSKnownHost contains the identifying information of trusted, remote peer. +type TLSKnownHost struct { + + // Host is the name of the known host. This value is derived from the + // CommonName value in the remote host's certiicate. + Host string + + // Alg is the cryptographic algorithm used to calculate the fingerprint. + Alg string + + // Fingerprint is known host's certificate's fingerprint. + Fingerprint []byte } diff --git a/api/utils/utils_config.go b/api/utils/utils_config.go index c1d4262c..5cf61ab2 100644 --- a/api/utils/utils_config.go +++ b/api/utils/utils_config.go @@ -23,11 +23,17 @@ func isSetPrefix( for _, r := range roots { rk := strings.Replace(key, prefix, fmt.Sprintf("%s.", r), 1) if config.IsSet(rk) { + /*if types.Debug { + fmt.Printf("isSet %s=true\n", rk) + }*/ return true } } if config.IsSet(key) { + /*if types.Debug { + fmt.Printf("isSet %s=true\n", key) + }*/ return true } @@ -52,12 +58,18 @@ func getStringPrefix( for _, r := range roots { rk := strings.Replace(key, prefix, fmt.Sprintf("%s.", r), 1) if val = config.GetString(rk); val != "" { + /*if types.Debug { + fmt.Printf("getString %s=%s\n", rk, val) + }*/ return val } } val = config.GetString(key) if val != "" { + /*if types.Debug { + fmt.Printf("getString %s=%s\n", key, val) + }*/ return val } diff --git a/api/utils/utils_tls.go b/api/utils/utils_tls.go index 5843c5f7..274eb859 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,43 @@ 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( + ctx types.Context, + text string) (*types.TLSKnownHost, error) { + + m := knownHostRX.FindStringSubmatch(text) + if len(m) == 0 { + return nil, nil + } + + ctx.WithFields(map[string]interface{}{ + "khHost": m[1], + "khAlg": m[2], + "khSig": m[3], + }).Debug("parsing known_hosts file fields") + + buf, err := hex.DecodeString(strings.Replace(m[3], ":", "", -1)) + if err != nil { + return nil, goof.WithError( + "error decoding known host fingerprint", err) + } + return &types.TLSKnownHost{ + Host: m[1], + Alg: m[2], + Fingerprint: buf, + }, 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") @@ -31,8 +63,165 @@ func ParseTLSConfig( return } fields[k] = v + ctx.WithField(k, v).Debug("tls field set") } + // 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 +242,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,97 +251,55 @@ 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 { + kh, err := ParseKnownHost(ctx, v) + if err != nil { + ctx.Error(err) + return nil, err + } + if kh != nil { 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}, - PeerFingerprint: buf, + Config: tls.Config{InsecureSkipVerify: true}, + VerifyPeers: true, + KnownHost: kh, }, 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...) - if insecure { - f(types.ConfigTLSInsecure, true) + if getBool(config, types.ConfigTLSInsecure, roots...) { tlsConfig.InsecureSkipVerify = true + f(types.ConfigTLSInsecure, 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} - } - - if isSet(config, types.ConfigTLSServerName, roots...) { - serverName := getString(config, types.ConfigTLSServerName, roots...) - tlsConfig.ServerName = serverName - f(types.ConfigTLSServerName, serverName) + if getBool(config, types.ConfigTLSVerifyPeers, roots...) { + tlsConfig.VerifyPeers = true + tlsConfig.InsecureSkipVerify = true + f(types.ConfigTLSVerifyPeers, true) } - if isSet(config, types.ConfigTLSClientCertRequired, roots...) { - clientCertRequired := getBool( - config, types.ConfigTLSClientCertRequired, roots...) - if clientCertRequired { - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - } - f(types.ConfigTLSClientCertRequired, clientCertRequired) + if getBool( + config, types.ConfigTLSClientCertRequired, roots...) { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + f(types.ConfigTLSClientCertRequired, true) } - 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 + if v := getString(config, types.ConfigTLSServerName, roots...); v != "" { + tlsConfig.ServerName = v + f(types.ConfigTLSServerName, v) } 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..8370b946 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 := verifyKnownHost( + d.ctx, + conn.ConnectionState().PeerCertificates, + tlsConfig.KnownHost); err != nil { + + d.ctx.WithError(err).Error("error matching peer fingerprint") + return nil, err + } + + if err := verifyKnownHostFiles( + 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..21e2843b --- /dev/null +++ b/drivers/storage/libstorage/libstorage_driver_tls.go @@ -0,0 +1,155 @@ +package libstorage + +import ( + "bufio" + "bytes" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "errors" + "os" + "strings" + + "github.com/codedellemc/libstorage/api/types" + "github.com/codedellemc/libstorage/api/utils" +) + +var errServerFingerprint = errors.New("invalid server fingerprint") + +func verifyKnownHost( + ctx types.Context, + peerCerts []*x509.Certificate, + knownHost *types.TLSKnownHost) error { + + if knownHost == nil { + return nil + } + + expectedFP := hex.EncodeToString(knownHost.Fingerprint) + 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, + "actualHost": cert.Subject.CommonName, + "expectedHost": knownHost.Host, + }).Debug("comparing tls known host information") + if bytes.EqualFold(knownHost.Fingerprint, certFP) && + strings.EqualFold(knownHost.Host, cert.Subject.CommonName) { + ctx.WithFields(map[string]interface{}{ + "actualFingerprint": actualFP, + "expectedFingerprint": expectedFP, + "actualHost": cert.Subject.CommonName, + "expectedHost": knownHost.Host, + }).Debug("matched tls known host information") + return nil + } + } + return errServerFingerprint +} + +func verifyKnownHostFiles( + 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 _, ok := err.(*types.ErrKnownHost); !ok { + return err + } + } + + if len(sysKnownHostsFilePath) > 0 { + return verifyKnownHostsFile(ctx, peerCerts, sysKnownHostsFilePath) + } + + return newErrKnownHost(peerCerts) +} + +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() { + l := scn.Text() + if len(l) == 0 { + continue + } + ctx.WithField("line", l).Debug("scanning known_hosts file") + kh, err := utils.ParseKnownHost(ctx, l) + if err != nil { + ctx.WithField("path", knownHostsFilePath).Error( + "error scanning known_hosts file") + return err + } + if kh == nil { + continue + } + expectedFP := hex.EncodeToString(kh.Fingerprint) + 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, + "actualHost": cert.Subject.CommonName, + "expectedHost": kh.Host, + }).Debug("comparing tls known host information") + if bytes.EqualFold(kh.Fingerprint, certFP) && + strings.EqualFold(kh.Host, cert.Subject.CommonName) { + ctx.WithFields(map[string]interface{}{ + "actualFingerprint": actualFP, + "expectedFingerprint": expectedFP, + "actualHost": cert.Subject.CommonName, + "expectedHost": kh.Host, + }).Debug("matched tls known host information") + return nil + } + } + } + + return newErrKnownHost(peerCerts) +} + +func newErrKnownHost(peerCerts []*x509.Certificate) error { + err := &types.ErrKnownHost{} + + if len(peerCerts) == 0 { + return err + } + + err.PeerHost = peerCerts[0].Subject.CommonName + err.PeerAlg = "sha256" + + h := sha256.New() + h.Write(peerCerts[0].Raw) + err.PeerFingerprint = h.Sum(nil) + + return err +} diff --git a/drivers/storage/vfs/tests/vfs_test.go b/drivers/storage/vfs/tests/vfs_test.go index 69406d48..1db12c12 100644 --- a/drivers/storage/vfs/tests/vfs_test.go +++ b/drivers/storage/vfs/tests/vfs_test.go @@ -5,9 +5,11 @@ package vfs import ( "bufio" "bytes" + "encoding/hex" "encoding/json" "fmt" "io/ioutil" + "net/url" "os" "path" "strconv" @@ -54,6 +56,146 @@ func TestClient(t *testing.T) { }) } +func TestClientKnownHostInvalidSig(t *testing.T) { + tcpTLSPeersTest, _ := strconv.ParseBool( + os.Getenv("LIBSTORAGE_TEST_TCP_TLS_PEERS")) + if !tcpTLSPeersTest { + t.SkipNow() + } + + tfile, err := ioutil.TempFile("", "") + if !assert.NoError(t, err) { + t.FailNow() + } + + const ( + host = "libstorage-server" + alg = "sha256" + fingerprint = `52:C7:5D:00:1B:E7:33:66:14:3C:47:07:77:59:9C:` + + `94:F1:EA:76:00:41:B1:9D:71:0B:80:05:1F:F7:2D:6B:6B` + knownHostEntry = host + " " + alg + " " + fingerprint + knownHostConfig = ` +test: + libstorage: + client: + tls: + knownHosts: %s +` + ) + + fmt.Fprint(tfile, knownHostEntry) + if !assert.NoError(t, tfile.Close()) { + t.FailNow() + } + defer func() { os.RemoveAll(tfile.Name()) }() + + buf := bytes.NewBuffer(newTestConfig(t)) + fmt.Fprintf(buf, knownHostConfig, tfile.Name()) + + tf := func(config gofig.Config, client types.Client, t *testing.T) { + // do nothing + } + + oce := func(err error) { + t.Log(err) + if !assert.Error(t, err) { + t.FailNow() + } + if !assert.IsType(t, &url.Error{}, err) { + t.FailNow() + } + uerr := err.(*url.Error) + if !assert.IsType(t, &types.ErrKnownHost{}, uerr.Err) { + t.FailNow() + } + terr := uerr.Err.(*types.ErrKnownHost) + if !assert.Equal(t, host, terr.PeerHost) { + t.FailNow() + } + if !assert.Equal(t, alg, terr.PeerAlg) { + t.FailNow() + } + if !assert.False(t, + strings.EqualFold( + strings.Replace(fingerprint, ":", "", -1), + hex.EncodeToString(terr.PeerFingerprint))) { + t.FailNow() + } + } + + apitests.RunWithOnClientError(t, oce, vfs.Name, buf.Bytes(), tf) +} + +func TestClientKnownHostInvalidHost(t *testing.T) { + tcpTLSPeersTest, _ := strconv.ParseBool( + os.Getenv("LIBSTORAGE_TEST_TCP_TLS_PEERS")) + if !tcpTLSPeersTest { + t.SkipNow() + } + + tfile, err := ioutil.TempFile("", "") + if !assert.NoError(t, err) { + t.FailNow() + } + + const ( + host = "libstorage-server2" + alg = "sha256" + fingerprint = `52:C7:5D:00:1B:E7:33:66:14:3C:47:07:77:59:9C:` + + `94:F1:EA:76:00:41:B1:9D:71:0B:80:05:1F:F7:2D:6B:69` + knownHostEntry = host + " " + alg + " " + fingerprint + knownHostConfig = ` +test: + libstorage: + client: + tls: + knownHosts: %s +` + ) + + fmt.Fprint(tfile, knownHostEntry) + if !assert.NoError(t, tfile.Close()) { + t.FailNow() + } + defer func() { os.RemoveAll(tfile.Name()) }() + + buf := bytes.NewBuffer(newTestConfig(t)) + fmt.Fprintf(buf, knownHostConfig, tfile.Name()) + + tf := func(config gofig.Config, client types.Client, t *testing.T) { + // do nothing + } + + oce := func(err error) { + t.Log(err) + if !assert.Error(t, err) { + t.FailNow() + } + if !assert.IsType(t, &url.Error{}, err) { + t.FailNow() + } + uerr := err.(*url.Error) + if !assert.IsType(t, &types.ErrKnownHost{}, uerr.Err) { + t.FailNow() + } + terr := uerr.Err.(*types.ErrKnownHost) + if !assert.Equal(t, alg, terr.PeerAlg) { + t.FailNow() + } + if !assert.True(t, + strings.EqualFold( + strings.Replace(fingerprint, ":", "", -1), + hex.EncodeToString(terr.PeerFingerprint))) { + t.FailNow() + } + if !assert.NotEqual(t, host, terr.PeerHost) { + t.FailNow() + } + } + + apitests.RunWithOnClientError(t, oce, vfs.Name, buf.Bytes(), tf) +} + func TestRoot(t *testing.T) { apitests.Run(t, vfs.Name, newTestConfig(t), apitests.TestRoot) } 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)