diff --git a/certgen/main.go b/certgen/main.go index f512a251c..4db9307f2 100644 --- a/certgen/main.go +++ b/certgen/main.go @@ -122,28 +122,49 @@ func main() { anHourAgo := now.Add(time.Hour * -1) // trustedRoot - // Trusted by drivers. trustedRoot.pem should be installed on driver Docker image among trusted + // Trusted by drivers. trustedRoot.pem should be installed in driver Docker image among trusted // root CAs. trustedRootCert, trustedRootKey, trustedRootDer := generateRoot(anHourAgo, tenYearsFromNow, "trustedRoot") writeKey(path.Join(basePath, "trustedRoot.key"), trustedRootKey) + // customRoot + customRoot2 + // Not trusted by the drivers by default. customRoot.crt should be installed in driver Docker image + // in some place. Testkit will only refer to it with its name (no path). It will be used to test custom CA driver settings. + customRootCert, customRootKey, customRootDer := generateRoot(anHourAgo, tenYearsFromNow, "customRoot") + writeKey(path.Join(basePath, "customRoot.key"), customRootKey) + customRoot2Cert, customRoot2Key, customRoot2Der := generateRoot(anHourAgo, tenYearsFromNow, "customRoot2") + writeKey(path.Join(basePath, "customRoot2.key"), customRoot2Key) // CRT files contains multiple certificates, as long as we only have one trusted in the driver it's // fine to just write it as this. // The name ca-certificates.crt assumes that driver Docker images are based on Debian and path // should be mounted as /etc/ssl/certs/ - writeCert(path.Join(basePath, "driver", "trustedRoot.crt"), trustedRootDer) + writeCert(path.Join(basePath, "driver", "trusted", "trustedRoot.crt"), trustedRootDer) + writeCert(path.Join(basePath, "driver", "custom", "customRoot.crt"), customRootDer) + writeCert(path.Join(basePath, "driver", "custom", "customRoot2.crt"), customRoot2Der) // trustedRoot_server1 - // Valid dates with hostname set to something that drivers can connect to from driver - // Docker container. + // Valid dates with hostname set to something that drivers can connect to from driver Docker container. trustedRoot_server1Key, trustedRoot_server1Der := generateServer(trustedRootCert, trustedRootKey, anHourAgo, tenYearsFromNow, "trustedRoot_thehost", "thehost") writeKey(path.Join(basePath, "server", "trustedRoot_thehost.key"), trustedRoot_server1Key) writeCert(path.Join(basePath, "server", "trustedRoot_thehost.pem"), trustedRoot_server1Der) + // customRoot_server1 + // now repeat the whole thing for the custom CAs + customRoot_server1Key, customRoot_server1Der := generateServer(customRootCert, customRootKey, anHourAgo, tenYearsFromNow, "customRoot_thehost", "thehost") + writeKey(path.Join(basePath, "server", "customRoot_thehost.key"), customRoot_server1Key) + writeCert(path.Join(basePath, "server", "customRoot_thehost.pem"), customRoot_server1Der) + customRoot2_server1Key, customRoot2_server1Der := generateServer(customRoot2Cert, customRoot2Key, anHourAgo, tenYearsFromNow, "customRoot2_thehost", "thehost") + writeKey(path.Join(basePath, "server", "customRoot2_thehost.key"), customRoot2_server1Key) + writeCert(path.Join(basePath, "server", "customRoot2_thehost.pem"), customRoot2_server1Der) // trustedRoot_server2 // Expired dates, otherwise same as server1. trustedRoot_server2Key, trustedRoot_server2Der := generateServer(trustedRootCert, trustedRootKey, tenYearsAgo, anHourAgo, "trustedRoot_thehost", "thehost") writeKey(path.Join(basePath, "server", "trustedRoot_thehost_expired.key"), trustedRoot_server2Key) writeCert(path.Join(basePath, "server", "trustedRoot_thehost_expired.pem"), trustedRoot_server2Der) + // customRoot_server2 + // now repeat the whole thing for the custom CA + customRoot_server2Key, customRoot_server2Der := generateServer(customRootCert, customRootKey, tenYearsAgo, anHourAgo, "customRoot_thehost", "thehost") + writeKey(path.Join(basePath, "server", "customRoot_thehost_expired.key"), customRoot_server2Key) + writeCert(path.Join(basePath, "server", "customRoot_thehost_expired.pem"), customRoot_server2Der) // untrustedRoot // Not trusted by drivers otherwise same as trustedRoot. diff --git a/driver.py b/driver.py index 929056e62..4da15398c 100644 --- a/driver.py +++ b/driver.py @@ -37,8 +37,13 @@ def _ensure_image(testkit_path, docker_image_path, branch_name, driver_name, cas_path = os.path.join(docker_image_path, "CAs") shutil.rmtree(cas_path, ignore_errors=True) cas_source_path = os.path.join(testkit_path, "tests", "tls", - "certs", "driver") + "certs", "driver", "trusted") shutil.copytree(cas_source_path, cas_path) + custom_cas_path = os.path.join(docker_image_path, "CustomCAs") + shutil.rmtree(custom_cas_path, ignore_errors=True) + custom_cas_source_path = os.path.join(testkit_path, "tests", "tls", + "certs", "driver", "custom") + shutil.copytree(custom_cas_source_path, custom_cas_path) # This will use the driver folder as build context. docker.build_and_tag(image_name, docker_image_path, diff --git a/nutkit/frontend/driver.py b/nutkit/frontend/driver.py index c7ac7e61d..1ffbe363a 100644 --- a/nutkit/frontend/driver.py +++ b/nutkit/frontend/driver.py @@ -6,7 +6,8 @@ class Driver: def __init__(self, backend, uri, auth_token, user_agent=None, resolver_fn=None, domain_name_resolver_fn=None, connection_timeout_ms=None, fetch_size=None, - max_tx_retry_time_ms=None): + max_tx_retry_time_ms=None, encrypted=None, + trusted_certificates=None): self._backend = backend self._resolver_fn = resolver_fn self._domain_name_resolver_fn = domain_name_resolver_fn @@ -15,7 +16,8 @@ def __init__(self, backend, uri, auth_token, user_agent=None, resolverRegistered=resolver_fn is not None, domainNameResolverRegistered=domain_name_resolver_fn is not None, connectionTimeoutMs=connection_timeout_ms, - fetchSize=fetch_size, maxTxRetryTimeMs=max_tx_retry_time_ms) + fetchSize=fetch_size, maxTxRetryTimeMs=max_tx_retry_time_ms, + encrypted=encrypted, trustedCertificates=trusted_certificates) res = backend.send_and_receive(req) if not isinstance(res, protocol.Driver): raise Exception("Should be Driver but was %s" % res) diff --git a/nutkit/protocol/feature.py b/nutkit/protocol/feature.py index ddce287b3..dbdc8d554 100644 --- a/nutkit/protocol/feature.py +++ b/nutkit/protocol/feature.py @@ -15,6 +15,15 @@ class Feature(Enum): # This methods asserts that exactly one record in left in the result # stream, else it will raise an exception. API_RESULT_SINGLE = "Feature:API:Result.Single" + # The driver implements explicit configuration options for SSL. + # - enable / disable SSL + # - verify signature against system store / custom cert / not at all + API_SSL_CONFIG = "Feature:API:SSLConfig" + # The driver understands bolt+s, bolt+ssc, neo4j+s, and neo4j+ssc schemes + # and will configure its ssl options automatically. + # ...+s: enforce SSL + verify server's signature with system's trust store + # ...+ssc: enforce SSL but do not verify the server's signature at all + API_SSL_SCHEMES = "Feature:API:SSLSchemes" # The driver supports single-sign-on (SSO) by providing a bearer auth token # API. AUTH_BEARER = "Feature:Auth:Bearer" diff --git a/nutkit/protocol/requests.py b/nutkit/protocol/requests.py index 691abff64..b70c71602 100644 --- a/nutkit/protocol/requests.py +++ b/nutkit/protocol/requests.py @@ -48,7 +48,8 @@ class NewDriver: def __init__( self, uri, authToken, userAgent=None, resolverRegistered=False, domainNameResolverRegistered=False, connectionTimeoutMs=None, - fetchSize=None, maxTxRetryTimeMs=None + fetchSize=None, maxTxRetryTimeMs=None, encrypted=None, + trustedCertificates=None ): # Neo4j URI to connect to self.uri = uri @@ -70,6 +71,19 @@ def __init__( assert hasattr(Feature, "TMP_DRIVER_MAX_TX_RETRY_TIME") if maxTxRetryTimeMs is not None: self.maxTxRetryTimeMs = maxTxRetryTimeMs + # (bool) whether to enable or disable encryption + # field missing in message: use driver default (should be False) + if encrypted is not None: + self.encrypted = encrypted + # None: trust system CAs + # [] (empty list): trust any certificate + # ["path", ...] (list of strings): custom CA certificates to trust + # field missing in message: use driver default (should be system CAs) + if trustedCertificates is not None: + if trustedCertificates == "None": + self.trustedCertificates = None + else: + self.trustedCertificates = trustedCertificates class AuthorizationToken: diff --git a/tests/shared.py b/tests/shared.py index 75c0b93ab..6f71dfa63 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -138,6 +138,11 @@ def get_driver_features(backend): protocol.Feature.BOLT_4_3, protocol.Feature.BOLT_4_4, )) + # TODO: remove this block once all drivers list this feature + # they all support the functionality already + if get_driver_name() in ["python", "java", "go", "dotnet"]: + assert protocol.Feature.API_SSL_SCHEMES not in features + features.add(protocol.Feature.API_SSL_SCHEMES) print("features", features) return features except (OSError, protocol.BaseError) as e: diff --git a/tests/tls/certs/customRoot.key b/tests/tls/certs/customRoot.key new file mode 100644 index 000000000..d1db4d898 --- /dev/null +++ b/tests/tls/certs/customRoot.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIB2DavQnjDSvTFbcQ8LAiOf0jdJw2vE5ws8BXimCubHOoAoGCCqGSM49 +AwEHoUQDQgAE7iYj3U966MS9xqfqn9hbG11Fe6+5rExRZNO3qqnosyFRMqaZ7U54 +hPwjVsqHOsvhMI7M71BXqrd6iOhkHsbFIA== +-----END EC PRIVATE KEY----- diff --git a/tests/tls/certs/customRoot2.key b/tests/tls/certs/customRoot2.key new file mode 100644 index 000000000..2d17d11c4 --- /dev/null +++ b/tests/tls/certs/customRoot2.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDK5fELEd3XFyNDsHpV32VcNtBmXWejBXDO/0wLIueHGoAoGCCqGSM49 +AwEHoUQDQgAEKtc4mzv0JEBevEtp6hDadUMTGhtl78i38IJXABB3fDUjeGdQQynB +7YaHtPFbxPOmR2VfgRybMhDhwMQxZcTsAw== +-----END EC PRIVATE KEY----- diff --git a/tests/tls/certs/driver/custom/customRoot.crt b/tests/tls/certs/driver/custom/customRoot.crt new file mode 100644 index 000000000..5d3b8ad74 --- /dev/null +++ b/tests/tls/certs/driver/custom/customRoot.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBfzCCASWgAwIBAgIQG6ywjQkkfBgCXAM3e21cMTAKBggqhkjOPQQDAjAVMRMw +EQYDVQQDEwpjdXN0b21Sb290MB4XDTIxMDkyNzExNDY1MFoXDTQxMDkyMjEyNDY1 +MFowFTETMBEGA1UEAxMKY3VzdG9tUm9vdDBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABO4mI91PeujEvcan6p/YWxtdRXuvuaxMUWTTt6qp6LMhUTKmme1OeIT8I1bK +hzrL4TCOzO9QV6q3eojoZB7GxSCjVzBVMA4GA1UdDwEB/wQEAwICBDATBgNVHSUE +DDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRGg7urqkqX +cidUaYZ7IhcpqW41lTAKBggqhkjOPQQDAgNIADBFAiBeFBloeaSr9fa6N94GmeaE +3qOrcRS4dqWf5A81OwmFegIhAOs9fcjfeHBmX2r07WN07RlbYXag4xlnl1BQUbAj +UMXk +-----END CERTIFICATE----- diff --git a/tests/tls/certs/driver/custom/customRoot2.crt b/tests/tls/certs/driver/custom/customRoot2.crt new file mode 100644 index 000000000..73afb47df --- /dev/null +++ b/tests/tls/certs/driver/custom/customRoot2.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBgDCCASegAwIBAgIQBFCs0ckOUAPEmdkVdEOa8DAKBggqhkjOPQQDAjAWMRQw +EgYDVQQDEwtjdXN0b21Sb290MjAeFw0yMTA5MjcxMTQ2NTBaFw00MTA5MjIxMjQ2 +NTBaMBYxFDASBgNVBAMTC2N1c3RvbVJvb3QyMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEKtc4mzv0JEBevEtp6hDadUMTGhtl78i38IJXABB3fDUjeGdQQynB7YaH +tPFbxPOmR2VfgRybMhDhwMQxZcTsA6NXMFUwDgYDVR0PAQH/BAQDAgIEMBMGA1Ud +JQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNDV2FCL +wDKV0dSdrhodOt+U0kmAMAoGCCqGSM49BAMCA0cAMEQCIAeUEL4P4gsY0gmOQ2i+ +/eXJvLH7iOxMmIW7RugnOc1ZAiACq1JA0BYyMZzDWl/8cn1Qs/0R35t/te+G5+K0 +VsqVbg== +-----END CERTIFICATE----- diff --git a/tests/tls/certs/driver/trustedRoot.crt b/tests/tls/certs/driver/trusted/trustedRoot.crt similarity index 100% rename from tests/tls/certs/driver/trustedRoot.crt rename to tests/tls/certs/driver/trusted/trustedRoot.crt diff --git a/tests/tls/certs/server/customRoot2_thehost.key b/tests/tls/certs/server/customRoot2_thehost.key new file mode 100644 index 000000000..5666873f0 --- /dev/null +++ b/tests/tls/certs/server/customRoot2_thehost.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICzYDTUBiNlCIngyqiTQ86fkG5KAvPPIAXbUW7kcMD9YoAoGCCqGSM49 +AwEHoUQDQgAESiRDcNXhPOKcT0HSLhNcVQsICPHYMgJcpz/nzhYWVAs5SGFyQM5i +vitjDQob2TkY5N8SUPVg3BzfNu08ny5mtw== +-----END EC PRIVATE KEY----- diff --git a/tests/tls/certs/server/customRoot2_thehost.pem b/tests/tls/certs/server/customRoot2_thehost.pem new file mode 100644 index 000000000..e8bb9183a --- /dev/null +++ b/tests/tls/certs/server/customRoot2_thehost.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBezCCASGgAwIBAgIQauOWHc5x54LVpRIRRzAPKTAKBggqhkjOPQQDAjAWMRQw +EgYDVQQDEwtjdXN0b21Sb290MjAeFw0yMTA5MjcxMTQ2NTBaFw00MTA5MjIxMjQ2 +NTBaMB4xHDAaBgNVBAMME2N1c3RvbVJvb3QyX3RoZWhvc3QwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARKJENw1eE84pxPQdIuE1xVCwgI8dgyAlynP+fOFhZUCzlI +YXJAzmK+K2MNChvZORjk3xJQ9WDcHN827TyfLma3o0kwRzAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADASBgNVHREECzAJ +ggd0aGVob3N0MAoGCCqGSM49BAMCA0gAMEUCIQDOh74w1gEy/CrjGmxfMPkrFIWz +I+bG9Ck28akdACUC0QIgRnidQd3BRR10bIaws7/QFB/s0070Ok+OJhYrvhZN55Q= +-----END CERTIFICATE----- diff --git a/tests/tls/certs/server/customRoot_thehost.key b/tests/tls/certs/server/customRoot_thehost.key new file mode 100644 index 000000000..73f07383e --- /dev/null +++ b/tests/tls/certs/server/customRoot_thehost.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMF+VyuuzaUjcum67tkMJnpcQWR8B/wnkNW1tkKSYUCroAoGCCqGSM49 +AwEHoUQDQgAEPbA6GMdGy7sIXMoAKbaNEdJZa0yGrOtS04UOuodCfIKPh099YT9t +D+kJ0sJtgiWvz75CJXvBE1ZVXG5DX8XssQ== +-----END EC PRIVATE KEY----- diff --git a/tests/tls/certs/server/customRoot_thehost.pem b/tests/tls/certs/server/customRoot_thehost.pem new file mode 100644 index 000000000..74306494f --- /dev/null +++ b/tests/tls/certs/server/customRoot_thehost.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBezCCASCgAwIBAgIRAPW09/lYACnO/SApHmOa+GEwCgYIKoZIzj0EAwIwFTET +MBEGA1UEAxMKY3VzdG9tUm9vdDAeFw0yMTA5MjcxMTQ2NTBaFw00MTA5MjIxMjQ2 +NTBaMB0xGzAZBgNVBAMMEmN1c3RvbVJvb3RfdGhlaG9zdDBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABD2wOhjHRsu7CFzKACm2jRHSWWtMhqzrUtOFDrqHQnyCj4dP +fWE/bQ/pCdLCbYIlr8++QiV7wRNWVVxuQ1/F7LGjSTBHMA4GA1UdDwEB/wQEAwIF +oDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBIGA1UdEQQLMAmC +B3RoZWhvc3QwCgYIKoZIzj0EAwIDSQAwRgIhAJqA5gax2RvjhcwXkMBvoBSwTTfl +huKrHFgTkf60swI7AiEA5fYtkckwZFk+6D/fUBIjc0UZgHF0KLKM0zcFWtuYyr8= +-----END CERTIFICATE----- diff --git a/tests/tls/certs/server/customRoot_thehost_expired.key b/tests/tls/certs/server/customRoot_thehost_expired.key new file mode 100644 index 000000000..fc6664c37 --- /dev/null +++ b/tests/tls/certs/server/customRoot_thehost_expired.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDm3744x0TZfnfTNQaxaN98clgGPS9iy7Blj6XGWTEDAoAoGCCqGSM49 +AwEHoUQDQgAEsYrhTVlHdnS6l8OtfK46y/hXW/3M6QX1DjPx/4epOXbAXk9XkmcI +phz6MV9p8YteCbDgKtAy4QuHv/dyHsLKVw== +-----END EC PRIVATE KEY----- diff --git a/tests/tls/certs/server/customRoot_thehost_expired.pem b/tests/tls/certs/server/customRoot_thehost_expired.pem new file mode 100644 index 000000000..4b0a259b8 --- /dev/null +++ b/tests/tls/certs/server/customRoot_thehost_expired.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBeDCCAR+gAwIBAgIQfYQ2BYaQwMOUjvubE4a8ZTAKBggqhkjOPQQDAjAVMRMw +EQYDVQQDEwpjdXN0b21Sb290MB4XDTAxMTAwMjEyNDY1MFoXDTIxMDkyNzExNDY1 +MFowHTEbMBkGA1UEAwwSY3VzdG9tUm9vdF90aGVob3N0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEsYrhTVlHdnS6l8OtfK46y/hXW/3M6QX1DjPx/4epOXbAXk9X +kmcIphz6MV9p8YteCbDgKtAy4QuHv/dyHsLKV6NJMEcwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwEgYDVR0RBAswCYIH +dGhlaG9zdDAKBggqhkjOPQQDAgNHADBEAiAEcKjBFoE1iYrdmoJ7WDndMEXyvmK+ +wBGbvdlapzEUlAIgGm0uf3OPPdg3O9bP95T1DSjwqy5208+IjRwO2MJwabc= +-----END CERTIFICATE----- diff --git a/tests/tls/certs/trustedRoot.key b/tests/tls/certs/trustedRoot.key index e42fb8254..d27e0047c 100644 --- a/tests/tls/certs/trustedRoot.key +++ b/tests/tls/certs/trustedRoot.key @@ -1,5 +1,5 @@ -----BEGIN EC PRIVATE KEY----- -MHcCAQEEIOoIWnYkga2JY0QUbauE5+4IYKnxrS2XxF83lyeA+2VkoAoGCCqGSM49 -AwEHoUQDQgAEjyaeSLCcHcEHz72b4w/q3k8l9VHe015DvwZ/7/B0nTtafwWvvL9D -qq/OJ6hgIYQpFyhZcLcFqQ8FLCzeH8GbRw== +MHcCAQEEIAvxrkdyTpeA1vWaCJxlQOZZsq6t2nu477SX/K0/o/RwoAoGCCqGSM49 +AwEHoUQDQgAE74l9pA6YhikKz49gjmlTnS0IdT/O3mh4eODQYDlzYCsjdgRCIKtj +Tej/SW9wFPyi5v52F5y7uLGrONDxR3U1Zg== -----END EC PRIVATE KEY----- diff --git a/tests/tls/securescheme.py b/tests/tls/securescheme.py deleted file mode 100644 index c870d9f3e..000000000 --- a/tests/tls/securescheme.py +++ /dev/null @@ -1,82 +0,0 @@ -from tests.shared import ( - get_driver_name, - TestkitTestCase, -) -from tests.tls.shared import ( - TlsServer, - try_connect, -) - -schemes = ["neo4j+s", "bolt+s"] - - -class TestSecureScheme(TestkitTestCase): - def setUp(self): - # Tests URL scheme neo4j+s/bolt+s where server is assumed to present - # a server certificate signed by a certificate authority recognized - # by the driver. - super().setUp() - self._server = None - self._driver = get_driver_name() - - def tearDown(self): - if self._server: - # If test raised an exception this will make sure that the stub - # server is killed and it's output is dumped for analysis. - self._server.reset() - self._server = None - super().tearDown() - - def test_trusted_ca_correct_hostname(self): - # Happy path, the server has a valid server certificate signed by a - # trusted certificate authority. - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("trustedRoot_thehost") - self.assertTrue(try_connect(self._backend, self._server, - scheme, "thehost")) - - def test_trusted_ca_expired_server_correct_hostname(self): - # The certificate authority is ok, hostname is ok but the server - # certificate has expired. Should not connect on expired certificate. - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("trustedRoot_thehost_expired") - self.assertFalse(try_connect(self._backend, self._server, - scheme, "thehost")) - - def test_trusted_ca_wrong_hostname(self): - # Verifies that driver rejects connect if hostnames doesn't match - - # TLS server is setup to serve under the name 'thehost' but driver will - # connect to this server using 'thehostbutwrong'. Note that the docker - # container must map this hostname to same IP as 'thehost', if this - # hasn't been done we won't connect (expected) but get a timeout - # instead since the TLS server hasn't received any connect attempt at - # all. - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("trustedRoot_thehost") - self.assertFalse(try_connect(self._backend, self._server, - scheme, "thehostbutwrong")) - - def test_untrusted_ca_correct_hostname(self): - # Verifies that driver rejects connect if hostnames match but CA isn't - # trusted - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("untrustedRoot_thehost") - self.assertFalse(try_connect(self._backend, self._server, - scheme, "thehost")) - - def test_unencrypted(self): - # Verify that driver doesn't connect when it has been configured for - # TLS connections but the server doesn't speak TLS - for scheme in schemes: - with self.subTest(scheme): - # The server cert doesn't really matter but set it to the one - # that would work if TLS happens to be on. - self._server = TlsServer("trustedRoot_thehost", - disable_tls=True) - self.assertFalse(try_connect(self._backend, self._server, - scheme, "thehost")) diff --git a/tests/tls/selfsignedscheme.py b/tests/tls/selfsignedscheme.py deleted file mode 100644 index 93789ddc0..000000000 --- a/tests/tls/selfsignedscheme.py +++ /dev/null @@ -1,96 +0,0 @@ -from tests.shared import ( - get_driver_name, - TestkitTestCase, -) -from tests.tls.shared import ( - TlsServer, - try_connect, -) - -schemes = ["neo4j+ssc", "bolt+ssc"] - - -class TestSelfSignedScheme(TestkitTestCase): - """Test URL scheme neo4j+ssc/bolt+ssc. - - The server is assumed to present a signed server certificate but not - necessarily signed by an authority recognized by the driver. - """ - - def setUp(self): - super().setUp() - self._server = None - self._driver = get_driver_name() - - def tearDown(self): - if self._server: - # If test raised an exception this will make sure that the stub - # server is killed and it's output is dumped for analysis. - self._server.reset() - self._server = None - super().tearDown() - - def test_trusted_ca_correct_hostname(self): - # A server certificate signed by a trusted CA should be accepted - # even when configured for self signed. - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("trustedRoot_thehost") - self.assertTrue(try_connect(self._backend, self._server, - scheme, "thehost")) - - def test_trusted_ca_expired_server_correct_hostname(self): - # A server certificate signed by a trusted CA but the certificate - # has expired. Go driver happily connects when InsecureSkipVerify is - # enabled, same for all drivers ? - - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("trustedRoot_thehost_expired") - self.assertTrue(try_connect(self._backend, self._server, - scheme, "thehost")) - - def test_trusted_ca_wrong_hostname(self): - # A server certificate signed by a trusted CA but with wrong - # hostname will still be accepted. - - # TLS server is setup to serve under the name 'thehost' but driver - # will connect to this server using 'thehostbutwrong'. Note that the - # docker container must map this hostname to same IP as 'thehost', - # if this hasn't been done we won't connect (expected) but get a - # timeout instead since the TLS server hasn't received any connect - # attempt at all. - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("trustedRoot_thehost") - self.assertTrue(try_connect(self._backend, self._server, - scheme, "thehostbutwrong")) - - def test_untrusted_ca_correct_hostname(self): - # Should connect - - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("untrustedRoot_thehost") - self.assertTrue(try_connect(self._backend, self._server, - scheme, "thehost")) - - def test_untrusted_ca_wrong_hostname(self): - # Should connect - for scheme in schemes: - with self.subTest(scheme): - self._server = TlsServer("untrustedRoot_thehost") - self.assertTrue(try_connect(self._backend, self._server, - scheme, "thehostbutwrong")) - - def test_unencrypted(self): - # Verifies that driver doesn't connect when it has been configured - # for TLS connections but the server doesn't speak TLS - for scheme in schemes: - with self.subTest(scheme): - # The server cert doesn't really matter but set it to the - # one that would work if TLS happens to be on. - self._server = TlsServer("untrustedRoot_thehost", - disable_tls=True) - self.assertFalse(try_connect(self._backend, self._server, - scheme, "thehost")) diff --git a/tests/tls/shared.py b/tests/tls/shared.py index 3621546c6..90f22c710 100644 --- a/tests/tls/shared.py +++ b/tests/tls/shared.py @@ -43,7 +43,7 @@ def __init__(self, server_cert, min_tls="0", max_tls="2", ) # Wait until something is written to know it started line = self._process.stdout.readline() - print(line) + print(line, end="") def _close_pipes(self): self._process.stdout.close() @@ -91,11 +91,11 @@ def reset(self): self._kill() -def try_connect(backend, server, scheme, host): +def try_connect(backend, server, scheme, host, **driver_config): url = "%s://%s:%d" % (scheme, host, 6666) # Doesn't really matter auth = AuthorizationToken("basic", principal="neo4j", credentials="pass") - driver = Driver(backend, url, auth) + driver = Driver(backend, url, auth, **driver_config) session = driver.session("r") try: session.run("RETURN 1 as n") diff --git a/tests/tls/suites.py b/tests/tls/suites.py index 9b74e991c..348a56488 100644 --- a/tests/tls/suites.py +++ b/tests/tls/suites.py @@ -9,19 +9,19 @@ get_test_result_class, ) from tests.tls import ( - securescheme, - selfsignedscheme, - tlsversions, - unsecurescheme, + test_secure_scheme, + test_self_signed_scheme, + test_tls_versions, + test_unsecure_scheme, ) loader = unittest.TestLoader() tls_suite = unittest.TestSuite() -tls_suite.addTests(loader.loadTestsFromModule(securescheme)) -tls_suite.addTests(loader.loadTestsFromModule(selfsignedscheme)) -tls_suite.addTests(loader.loadTestsFromModule(unsecurescheme)) -tls_suite.addTests(loader.loadTestsFromModule(tlsversions)) +tls_suite.addTests(loader.loadTestsFromModule(test_secure_scheme)) +tls_suite.addTests(loader.loadTestsFromModule(test_self_signed_scheme)) +tls_suite.addTests(loader.loadTestsFromModule(test_tls_versions)) +tls_suite.addTests(loader.loadTestsFromModule(test_unsecure_scheme)) if __name__ == "__main__": suite_name = "TLS tests" diff --git a/tests/tls/test_explicit_options.py b/tests/tls/test_explicit_options.py new file mode 100644 index 000000000..fe509aae6 --- /dev/null +++ b/tests/tls/test_explicit_options.py @@ -0,0 +1,48 @@ +from nutkit.frontend import Driver +import nutkit.protocol as types +from tests.shared import ( + driver_feature, + get_driver_name, + TestkitTestCase, +) +from tests.tls.shared import TlsServer + +schemes = ("bolt", "neo4j") + + +class TestExplicitSslOptions(TestkitTestCase): + def setUp(self): + super().setUp() + self._server = None + self._driver = get_driver_name() + + def tearDown(self): + if self._server: + # If test raised an exception this will make sure that the stub + # server is killed and its output is dumped for analysis. + self._server.reset() + self._server = None + super().tearDown() + + @driver_feature(types.Feature.API_SSL_SCHEMES, + types.Feature.API_SSL_CONFIG) + def test_explicit_config_and_scheme_config(self): + def _test(): + url = "%s://%s:%d" % (scheme, "thehost", 6666) + auth = types.AuthorizationToken(scheme="basic", principal="neo4j", + credentials="pass") + with self.assertRaises(types.DriverError) as exc: + Driver(self._backend, url, auth, encrypted=encrypted, + trusted_certificates=certs) + if get_driver_name() in ["javascript"]: + self.assertIs("encryption", exc.exception.msg.lower()) + self.assertIs("trust", exc.exception.msg.lower()) + else: + self.fail("Add expected error type for driver.") + + self._server = TlsServer("trustedRoot_thehost") + for scheme in ("neo4j+s", "neo4j+ssc", "bolt+s", "bolt+ssc"): + for encrypted in (True, False): + for certs in ("None", [], ["customRoot.crt"]): + with self.subTest("%s-%s-%s" % (scheme, encrypted, certs)): + _test() diff --git a/tests/tls/test_secure_scheme.py b/tests/tls/test_secure_scheme.py new file mode 100644 index 000000000..28eb4e14a --- /dev/null +++ b/tests/tls/test_secure_scheme.py @@ -0,0 +1,165 @@ +import nutkit.protocol as types +from tests.shared import ( + get_driver_name, + TestkitTestCase, +) +from tests.tls.shared import ( + TlsServer, + try_connect, +) + + +class TestSecureScheme(TestkitTestCase): + # Tests URL scheme neo4j+s/bolt+s where server is assumed to present a + # server certificate signed by a certificate authority recognized by the + # driver. + + def setUp(self): + super().setUp() + self._server = None + self._driver = get_driver_name() + + def tearDown(self): + if self._server: + # If test raised an exception this will make sure that the stub + # server is killed and it's output is dumped for analysis. + self._server.reset() + self._server = None + super().tearDown() + + schemes = "neo4j+s", "bolt+s" + feature_requirement = types.Feature.API_SSL_SCHEMES, + extra_driver_configs = {}, + cert_prefix = "trustedRoot_" + + def _try_connect(self, scheme, host, driver_config): + return try_connect(self._backend, self._server, scheme, host, + **driver_config) + + def _start_server(self, cert_suffix, **kwargs): + if "Root_" not in cert_suffix: + cert = self.cert_prefix + cert_suffix + else: + cert = cert_suffix + self._server = TlsServer(cert, **kwargs) + + def test_trusted_ca_correct_hostname(self): + # Happy path, the server has a valid server certificate signed by a + # trusted certificate authority. + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._start_server("thehost") + self.assertTrue(self._try_connect(scheme, "thehost", + driver_config)) + self._server.reset() + + def test_trusted_ca_expired_server_correct_hostname(self): + # The certificate authority is ok, hostname is ok but the server + # certificate has expired. Should not connect on expired certificate. + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._start_server("thehost_expired") + self.assertFalse(self._try_connect(scheme, "thehost", + driver_config)) + self._server.reset() + + def test_trusted_ca_wrong_hostname(self): + # Verifies that driver rejects connection if host name doesn't match + self.skip_if_missing_driver_features(*self.feature_requirement) + # TLS server is setup to serve under the name 'thehost' but driver will + # connect to this server using 'thehostbutwrong'. Note that the docker + # container must map this hostname to same IP as 'thehost', if this + # hasn't been done we won't connect (expected) but get a timeout + # instead since the TLS server hasn't received any connect attempt at + # all. + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._start_server("thehost") + self.assertFalse(self._try_connect(scheme, + "thehostbutwrong", + driver_config)) + self._server.reset() + + def test_untrusted_ca_correct_hostname(self): + # Verifies that driver rejects connection if host name doesn't match + # trusted + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._server = TlsServer("untrustedRoot_thehost") + self.assertFalse(self._try_connect(scheme, "thehost", + driver_config)) + self._server.reset() + + def test_unencrypted(self): + # Verifies that driver doesn't connect when it has been configured for + # TLS connections but the server doesn't speak TLS + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + # The server cert doesn't really matter but set it to the + # one that would work if TLS happens to be on. + self._start_server("thehost", disable_tls=True) + self.assertFalse(self._try_connect(scheme, "thehost", + driver_config)) + self._server.reset() + + +class TestTrustSystemCertsConfig(TestSecureScheme): + schemes = "neo4j", "bolt" + feature_requirement = types.Feature.API_SSL_CONFIG, + extra_driver_configs = ( + {"encrypted": True, "trusted_certificates": None}, + {"encrypted": True}, + ) + + def test_trusted_ca_correct_hostname(self): + super().test_trusted_ca_correct_hostname() + + def test_trusted_ca_expired_server_correct_hostname(self): + super().test_trusted_ca_expired_server_correct_hostname() + + def test_trusted_ca_wrong_hostname(self): + super().test_trusted_ca_wrong_hostname() + + def test_untrusted_ca_correct_hostname(self): + super().test_untrusted_ca_correct_hostname() + + def test_unencrypted(self): + super().test_unencrypted() + + +class TestTrustCustomCertsConfig(TestTrustSystemCertsConfig): + extra_driver_configs = ( + {"encrypted": True, "trusted_certificates": ["customRoot.crt"]}, + {"encrypted": True, + "trusted_certificates": ["customRoot2.crt", "customRoot.crt"]}, + ) + cert_prefix = "customRoot_" + + def test_trusted_ca_correct_hostname(self): + super().test_trusted_ca_correct_hostname() + + def test_trusted_ca_expired_server_correct_hostname(self): + super().test_trusted_ca_expired_server_correct_hostname() + + def test_trusted_ca_wrong_hostname(self): + super().test_trusted_ca_wrong_hostname() + + def test_untrusted_ca_correct_hostname(self): + super().test_untrusted_ca_correct_hostname() + + def test_unencrypted(self): + super().test_unencrypted() diff --git a/tests/tls/test_self_signed_scheme.py b/tests/tls/test_self_signed_scheme.py new file mode 100644 index 000000000..b3cb8dae1 --- /dev/null +++ b/tests/tls/test_self_signed_scheme.py @@ -0,0 +1,153 @@ +import nutkit.protocol as types +from tests.shared import ( + get_driver_name, + TestkitTestCase, +) +from tests.tls.shared import ( + TlsServer, + try_connect, +) + + +class TestSelfSignedScheme(TestkitTestCase): + # Tests URL scheme neo4j+ssc/bolt+ssc where server is assumed to present a + # signed server certificate but not necessarily signed by an authority + # recognized by the driver. + def setUp(self): + super().setUp() + self._server = None + self._driver = get_driver_name() + + def tearDown(self): + if self._server: + # If test raised an exception this will make sure that the stub + # server is killed and it's output is dumped for analysis. + self._server.reset() + self._server = None + super().tearDown() + + schemes = "neo4j+ssc", "bolt+ssc" + feature_requirement = types.Feature.API_SSL_SCHEMES, + extra_driver_configs = {}, + + def _try_connect(self, scheme, host, driver_config): + return try_connect(self._backend, self._server, scheme, host, + **driver_config) + + def test_trusted_ca_correct_hostname(self): + # A server certificate signed by a trusted CA should be accepted even + # when configured for self signed. + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._server = TlsServer("trustedRoot_thehost") + self.assertTrue(self._try_connect(scheme, "thehost", + driver_config)) + if self._server is not None: + self._server.reset() + + def test_trusted_ca_expired_server_correct_hostname(self): + # A server certificate signed by a trusted CA but the certificate has + # expired. Go driver happily connects when InsecureSkipVerify is + # enabled, same for all drivers ? + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._server = TlsServer("trustedRoot_thehost_expired") + self.assertTrue(self._try_connect(scheme, "thehost", + driver_config)) + if self._server is not None: + self._server.reset() + + def test_trusted_ca_wrong_hostname(self): + # A server certificate signed by a trusted CA but with wrong hostname + # will still be accepted. + # TLS server is setup to serve under the name 'thehost' but driver will + # connect to this server using 'thehostbutwrong'. Note that the docker + # container must map this hostname to same IP as 'thehost', if this + # hasn't been done we won't connect (expected) but get a timeout + # instead since the TLS server hasn't received any connect attempt at + # all. + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._server = TlsServer("trustedRoot_thehost") + self.assertTrue(self._try_connect(scheme, + "thehostbutwrong", + driver_config)) + if self._server is not None: + self._server.reset() + + def test_untrusted_ca_correct_hostname(self): + # Should connect + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._server = TlsServer("untrustedRoot_thehost") + self.assertTrue(self._try_connect(scheme, "thehost", + driver_config)) + if self._server is not None: + self._server.reset() + + def test_untrusted_ca_wrong_hostname(self): + # Should connect + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + self._server = TlsServer("untrustedRoot_thehost") + self.assertTrue(self._try_connect(scheme, + "thehostbutwrong", + driver_config)) + if self._server is not None: + self._server.reset() + + def test_unencrypted(self): + # Verifies that driver doesn't connect when it has been configured for + # TLS connections but the server doesn't speak TLS + self.skip_if_missing_driver_features(*self.feature_requirement) + for driver_config in self.extra_driver_configs: + for scheme in self.schemes: + with self.subTest(scheme + + "-" + str(driver_config)): + # The server cert doesn't really matter but set it to the + # one that would work if TLS happens to be on. + self._server = TlsServer("untrustedRoot_thehost", + disable_tls=True) + self.assertFalse(self._try_connect(scheme, "thehost", + driver_config)) + if self._server is not None: + self._server.reset() + + +class TestTrustAllCertsConfig(TestSelfSignedScheme): + schemes = "neo4j", "bolt" + feature_requirement = types.Feature.API_SSL_CONFIG, + extra_driver_configs = {"encrypted": True, "trusted_certificates": []}, + + def test_trusted_ca_correct_hostname(self): + super().test_trusted_ca_correct_hostname() + + def test_trusted_ca_expired_server_correct_hostname(self): + super().test_trusted_ca_expired_server_correct_hostname() + + def test_trusted_ca_wrong_hostname(self): + super().test_trusted_ca_wrong_hostname() + + def test_untrusted_ca_correct_hostname(self): + super().test_untrusted_ca_correct_hostname() + + def test_untrusted_ca_wrong_hostname(self): + super().test_untrusted_ca_wrong_hostname() + + def test_unencrypted(self): + super().test_unencrypted() diff --git a/tests/tls/tlsversions.py b/tests/tls/test_tls_versions.py similarity index 60% rename from tests/tls/tlsversions.py rename to tests/tls/test_tls_versions.py index faefd4c74..a1835a621 100644 --- a/tests/tls/tlsversions.py +++ b/tests/tls/test_tls_versions.py @@ -24,6 +24,18 @@ def tearDown(self): self._server = None super().tearDown() + def _try_connect(self): + if self.driver_supports_features(types.Feature.API_SSL_SCHEMES): + return try_connect(self._backend, self._server, + "neo4j+s", "thehost") + elif self.driver_supports_features(types.Feature.API_SSL_CONFIG): + return try_connect(self._backend, self._server, "neo4j", "thehost", + ) + self.skipTest("Needs support for either of %s" % ", ".join( + map(lambda f: f.value, + (types.Feature.API_SSL_SCHEMES, types.Feature.API_SSL_CONFIG)) + )) + def test_1_1(self): if self._driver in ["dotnet"]: self.skipTest("TLS 1.1 is not supported") @@ -31,28 +43,22 @@ def test_1_1(self): self._server = TlsServer("trustedRoot_thehost", min_tls="1", max_tls="1") if self.driver_supports_features(types.Feature.TLS_1_1): - self.assertTrue(try_connect(self._backend, self._server, - "neo4j+s", "thehost")) + self.assertTrue(self._try_connect()) else: - self.assertFalse(try_connect(self._backend, self._server, - "neo4j+s", "thehost")) + self.assertFalse(self._try_connect()) def test_1_2(self): self._server = TlsServer("trustedRoot_thehost", min_tls="2", max_tls="2") if self.driver_supports_features(types.Feature.TLS_1_2): - self.assertTrue(try_connect(self._backend, self._server, - "neo4j+s", "thehost")) + self.assertTrue(self._try_connect()) else: - self.assertFalse(try_connect(self._backend, self._server, - "neo4j+s", "thehost")) + self.assertFalse(self._try_connect()) def test_1_3(self): self._server = TlsServer("trustedRoot_thehost", min_tls="3", max_tls="3") if self.driver_supports_features(types.Feature.TLS_1_3): - self.assertTrue(try_connect(self._backend, self._server, - "neo4j+s", "thehost")) + self.assertTrue(self._try_connect()) else: - self.assertFalse(try_connect(self._backend, self._server, - "neo4j+s", "thehost")) + self.assertFalse(self._try_connect()) diff --git a/tests/tls/unsecurescheme.py b/tests/tls/test_unsecure_scheme.py similarity index 52% rename from tests/tls/unsecurescheme.py rename to tests/tls/test_unsecure_scheme.py index 34f0c9883..251510b4a 100644 --- a/tests/tls/unsecurescheme.py +++ b/tests/tls/test_unsecure_scheme.py @@ -1,4 +1,6 @@ +import nutkit.protocol as types from tests.shared import ( + driver_feature, get_driver_name, TestkitTestCase, ) @@ -11,13 +13,10 @@ class TestUnsecureScheme(TestkitTestCase): - """Test URL scheme neo4j/bolt where TLS is not used. - - The fact that driver can not connect to a TLS server with this - configuration is less interesting than the error handling when this - happens, the driver backend should "survive" (without special hacks in - it). - """ + # Tests URL scheme neo4j/bolt where TLS is not used. The fact that driver + # can not connect to a TLS server with this configuration is less + # interesting than the error handling when this happens, the driver backend + # should "survive" (without special hacks in it). def setUp(self): super().setUp() @@ -38,3 +37,14 @@ def test_secure_server(self): self._server = TlsServer("trustedRoot_thehost") self.assertFalse(try_connect(self._backend, self._server, scheme, "thehost")) + self._server.reset() + + @driver_feature(types.Feature.API_SSL_CONFIG) + def test_secure_server_explicitly_disabled_encryption(self): + for scheme in schemes: + with self.subTest(scheme): + self._server = TlsServer("trustedRoot_thehost") + self.assertFalse(try_connect(self._backend, self._server, + scheme, "thehost", + encrypted=False)) + self._server.reset()