diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 2a02f1ac54..d0fc085b3a 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,10 @@ Features
Others
------
+* The deprecated ``Cluster.ssl_options`` parameter is retained as a migration
+ aid but can no longer configure TLS. Supplying or assigning it, including an
+ empty dictionary, raises ``ValueError`` with guidance for configuring an
+ ``ssl.SSLContext`` and passing it using ``ssl_context`` instead.
* ``PreparedStatement.result_metadata`` and ``PreparedStatement.result_metadata_id`` are
now read-only. They are replaced together by
``PreparedStatement.update_result_metadata()``, so a request can never observe a metadata
diff --git a/cassandra/cluster.py b/cassandra/cluster.py
index 88c8d2707a..50558c7849 100644
--- a/cassandra/cluster.py
+++ b/cassandra/cluster.py
@@ -874,45 +874,28 @@ def default_retry_policy(self, policy):
:const:`True`, else :const:`None`.
"""
- ssl_options = None
- """
- Using ssl_options without ssl_context is deprecated and will be removed in the
- next major release.
-
- An optional dict which will be used as kwargs for ``ssl.SSLContext.wrap_socket``
- when new sockets are created. This should be used when client encryption is enabled
- in Cassandra.
-
- The following documentation only applies when ssl_options is used without ssl_context.
-
- By default, a ``ca_certs`` value should be supplied (the value should be
- a string pointing to the location of the CA certs file), and you probably
- want to specify ``ssl_version`` as ``ssl.PROTOCOL_TLS`` to match
- Cassandra's default protocol.
-
- .. versionchanged:: 3.3.0
+ _ssl_options = None
- In addition to ``wrap_socket`` kwargs, clients may also specify ``'check_hostname': True`` to verify the cert hostname
- as outlined in RFC 2818 and RFC 6125. Note that this requires the certificate to be transferred, so
- should almost always require the option ``'cert_reqs': ssl.CERT_REQUIRED``. Note also that this functionality was not built into
- Python standard library until (2.7.9, 3.2). To enable this mechanism in earlier versions, patch ``ssl.match_hostname``
- with a custom or `back-ported function `_.
-
- .. versionchanged:: 3.29.0
+ @property
+ def ssl_options(self):
+ """
+ Deprecated TLS configuration option retained to provide migration
+ guidance. Passing or assigning a value raises :class:`ValueError`.
+ Configure an ``ssl.SSLContext`` and pass it using
+ :attr:`.ssl_context` instead.
+ """
+ return None
- ``ssl.match_hostname`` has been deprecated since Python 3.7 (and removed in Python 3.12). This functionality is now implemented
- via ``ssl.SSLContext.check_hostname``. All options specified above (including ``check_hostname``) should continue to behave in a
- way that is consistent with prior implementations.
- """
+ @ssl_options.setter
+ def ssl_options(self, value):
+ if value is not None:
+ self._raise_ssl_options_migration_error()
ssl_context = None
"""
An optional ``ssl.SSLContext`` instance which will be used when new sockets are created.
This should be used when client encryption is enabled in Cassandra.
- ``wrap_socket`` options can be set using :attr:`~Cluster.ssl_options`. ssl_options will
- be used as kwargs for ``ssl.SSLContext.wrap_socket``.
-
.. versionadded:: 3.17.0
"""
@@ -1277,6 +1260,9 @@ def __init__(self,
Any of the mutable Cluster attributes may be set as keyword arguments to the constructor.
"""
+ if ssl_options is not None:
+ self._raise_ssl_options_migration_error()
+
# Handle port passed as string
if isinstance(port, str):
if not port.isdigit():
@@ -1508,14 +1494,10 @@ def __init__(self,
self.metrics_enabled = metrics_enabled
- if ssl_options and not ssl_context:
- warn('Using ssl_options without ssl_context is '
- 'deprecated and will result in an error in '
- 'the next major release. Please use ssl_context '
- 'to prepare for that release.',
- DeprecationWarning)
-
- self.ssl_options = ssl_options
+ # Cloud configuration still carries internal per-endpoint TLS routing
+ # metadata. Keep it private so callers cannot accidentally reach the
+ # legacy public ssl_options path.
+ self._ssl_options = ssl_options
self.ssl_context = ssl_context
self.sockopts = sockopts
self.cql_version = cql_version
@@ -1762,7 +1744,7 @@ def _make_connection_kwargs(self, endpoint, kwargs_dict):
kwargs_dict.setdefault('port', self.port)
kwargs_dict.setdefault('compression', self.compression)
kwargs_dict.setdefault('sockopts', self.sockopts)
- kwargs_dict.setdefault('ssl_options', self.ssl_options)
+ kwargs_dict.setdefault('ssl_options', self._ssl_options)
kwargs_dict.setdefault('ssl_context', self.ssl_context)
kwargs_dict.setdefault('cql_version', self.cql_version)
kwargs_dict.setdefault('protocol_version', self.protocol_version)
@@ -1773,6 +1755,18 @@ def _make_connection_kwargs(self, endpoint, kwargs_dict):
return kwargs_dict
+ @staticmethod
+ def _raise_ssl_options_migration_error():
+ raise ValueError(
+ "ssl_options is deprecated and can no longer configure TLS. "
+ "Create an ssl.SSLContext and pass it using ssl_context instead. "
+ "For example, ca_certs maps to "
+ "SSLContext.load_verify_locations(), and certfile/keyfile map to "
+ "SSLContext.load_cert_chain(). Migration guide: "
+ "https://python-driver.docs.scylladb.com/stable/"
+ "security.html#ssl-options-migration"
+ )
+
def protocol_downgrade(self, host_endpoint, previous_version):
if self._protocol_version_explicit:
raise DriverException("ProtocolError returned from server while using explicitly set client protocol_version %d" % (previous_version,))
diff --git a/docs/security.rst b/docs/security.rst
index 5c8645e685..64a980640b 100644
--- a/docs/security.rst
+++ b/docs/security.rst
@@ -47,17 +47,124 @@ These docs will include some examples for how to achieve common configurations,
but the `ssl.SSLContext `_ documentation
gives a more complete description of what is possible.
-To enable SSL with version 3.17.0 and higher, you will need to set :attr:`.Cluster.ssl_context` to a
-``ssl.SSLContext`` instance to enable SSL. Optionally, you can also set :attr:`.Cluster.ssl_options`
-to a dict of options. These will be passed as kwargs to ``ssl.SSLContext.wrap_socket()``
-when new sockets are created.
+To enable SSL with version 3.17.0 and higher, set :attr:`.Cluster.ssl_context` to an
+``ssl.SSLContext`` instance. The legacy :attr:`.Cluster.ssl_options` argument remains
+in the API only to raise an error with migration guidance when it is used.
+
+.. _ssl-options-migration:
+
+Migrating from ``ssl_options``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Replace the legacy options dictionary with a context configured through the
+standard-library ``ssl`` API. For example, replace:
+
+.. code-block:: python
+
+ import ssl
+ from cassandra.cluster import Cluster
+
+ cluster = Cluster(
+ ['node.example.com'],
+ ssl_options={
+ 'ca_certs': '/path/to/rootca.pem',
+ 'cert_reqs': ssl.CERT_REQUIRED,
+ 'check_hostname': True,
+ },
+ )
+
+with:
+
+.. code-block:: python
+
+ context = ssl.create_default_context(cafile='/path/to/rootca.pem')
+ context.check_hostname = True
+
+ cluster = Cluster(
+ ['node.example.com'],
+ ssl_context=context,
+ )
+
+Use the following mappings for other legacy options:
+
+.. list-table:: ``ssl_options`` migration reference
+ :header-rows: 1
+ :widths: 22 38 40
+
+ * - Legacy option
+ - Standard-library context
+ - Twisted/Eventlet pyOpenSSL context
+ * - ``ca_certs``
+ - ``context.load_verify_locations(path)``
+ - ``context.load_verify_locations(path)``
+ * - ``certfile`` and ``keyfile``
+ - ``context.load_cert_chain(certfile, keyfile, password)``
+ - ``context.use_certificate_file(certfile)`` and
+ ``context.use_privatekey_file(keyfile)``
+ * - ``cert_reqs``
+ - ``context.verify_mode``
+ - ``context.set_verify(mode, callback)``
+ * - ``check_hostname``
+ - ``context.check_hostname``
+ - There is no direct context attribute. Configure hostname verification
+ in a vetted pyOpenSSL verification layer, or migrate to a
+ standard-library-backed reactor when hostname verification is required.
+ * - ``ciphers``
+ - ``context.set_ciphers(value)``
+ - ``context.set_cipher_list(value.encode('ascii'))``
+ * - ``ssl_version``
+ - Start with ``ssl.PROTOCOL_TLS_CLIENT`` and configure
+ ``minimum_version`` and ``maximum_version`` when pinning is required.
+ - Start with ``SSL.TLS_CLIENT_METHOD`` and configure protocol bounds on
+ the context when pinning is required.
+ * - ``server_hostname``
+ - The driver derives this from the endpoint. Use
+ :class:`~cassandra.connection.SniEndPoint` when explicit SNI routing is
+ required.
+ - The driver derives this from the endpoint. Use
+ :class:`~cassandra.connection.SniEndPoint` when explicit SNI routing is
+ required.
+ * - ``server_side``, ``do_handshake_on_connect``, and
+ ``suppress_ragged_eofs``
+ - No migration. The driver owns client-side socket creation and TLS
+ handshakes; these low-level overrides are no longer configurable.
+ - No migration. The driver owns client-side socket creation and TLS
+ handshakes; these low-level overrides are no longer configurable.
+
+An empty ``ssl_options={}`` previously left the intended verification policy
+ambiguous. Replace it with an explicit context. For an insecure development
+connection with no certificate verification:
+
+.. code-block:: python
+
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ cluster = Cluster(['127.0.0.1'], ssl_context=context)
+
+Do not use this configuration in production. Prefer
+``ssl.create_default_context()`` with a trusted CA and hostname verification.
+
+For mutual TLS, load the client certificate and private key on the context:
+
+.. code-block:: python
+
+ context = ssl.create_default_context(cafile='/path/to/rootca.pem')
+ context.load_cert_chain(
+ certfile='/path/to/client.crt',
+ keyfile='/path/to/client.key',
+ password='optional-key-password',
+ )
+ cluster = Cluster(['node.example.com'], ssl_context=context)
+
+Twisted and Eventlet require an ``OpenSSL.SSL.Context`` instead of a standard
+``ssl.SSLContext``. See `SSL with Twisted or Eventlet`_ below for a complete
+example. Cloud secure-connect bundles require no migration; the driver creates
+their TLS context internally.
If you create your SSLContext using `ssl.create_default_context `_,
-be aware that SSLContext.check_hostname is set to True by default, so the hostname validation will be done
-by Python and not the driver. For this reason, we need to set the server_hostname at best effort, which is the
-resolved ip address. If this validation needs to be done against the FQDN, consider enabling it using the ssl_options
-as described in the following examples or implement your own :class:`~.connection.EndPoint` and
-:class:`~.connection.EndPointFactory`.
+be aware that SSLContext.check_hostname is set to True by default, so hostname validation is done
+by Python rather than the driver. The driver uses the endpoint address as the TLS server name.
The following examples assume you have generated your Scylla certificate and
@@ -135,7 +242,7 @@ to `CERT_REQUIRED`. Otherwise, the loaded verify certificate will have no effect
cluster = Cluster(['127.0.0.1'], ssl_context=ssl_context)
session = cluster.connect()
-Additionally, you can also force the driver to verify the `hostname` of the server by passing additional options to `ssl_context.wrap_socket` via the `ssl_options` kwarg:
+To verify the hostname of the server, enable hostname checking on the context:
.. code-block:: python
@@ -146,9 +253,7 @@ Additionally, you can also force the driver to verify the `hostname` of the serv
ssl_context.load_verify_locations('/path/to/rootca.crt')
ssl_context.verify_mode = CERT_REQUIRED
ssl_context.check_hostname = True
- ssl_options = {'server_hostname': '127.0.0.1'}
-
- cluster = Cluster(['127.0.0.1'], ssl_context=ssl_context, ssl_options=ssl_options)
+ cluster = Cluster(['127.0.0.1'], ssl_context=ssl_context)
session = cluster.connect()
.. _ssl-server-verifies-client:
@@ -263,8 +368,7 @@ for more details about ``SSLContext`` configuration.
cluster = Cluster(
contact_points=['127.0.0.1'],
connection_class=TwistedConnection,
- ssl_context=ssl_context,
- ssl_options={'check_hostname': True}
+ ssl_context=ssl_context
)
session = cluster.connect()
diff --git a/tests/integration/long/test_ssl.py b/tests/integration/long/test_ssl.py
index 0170f56fa1..51026818d7 100644
--- a/tests/integration/long/test_ssl.py
+++ b/tests/integration/long/test_ssl.py
@@ -47,22 +47,53 @@
DRIVER_KEYFILE = os.path.abspath("tests/integration/long/ssl/client.key")
DRIVER_KEYFILE_ENCRYPTED = os.path.abspath("tests/integration/long/ssl/client_encrypted.key")
DRIVER_CERTFILE = os.path.abspath("tests/integration/long/ssl/client.crt_signed")
-DRIVER_CERTFILE_BAD = os.path.abspath("tests/integration/long/ssl/client_bad.key")
USES_PYOPENSSL = "twisted" in EVENT_LOOP_MANAGER or "eventlet" in EVENT_LOOP_MANAGER
-if "twisted" in EVENT_LOOP_MANAGER:
- import OpenSSL
- ssl_version = OpenSSL.SSL.TLS_METHOD
- verify_certs = {'cert_reqs': SSL.VERIFY_PEER,
- 'check_hostname': True}
-else:
- ssl_version = ssl.PROTOCOL_TLS
- verify_certs = {'cert_reqs': ssl.CERT_REQUIRED,
- 'check_hostname': True}
-def verify_callback(connection, x509, errnum, errdepth, ok):
- return ok
+def create_ssl_context(
+ ca_certs=CLIENT_CA_CERTS, certfile=None, keyfile=None,
+ password=None, hostname=None):
+ if USES_PYOPENSSL:
+ ssl_context = SSL.Context(SSL.TLS_CLIENT_METHOD)
+ if ca_certs:
+ ssl_context.load_verify_locations(ca_certs)
+
+ def verify_peer(_connection, certificate, _errnum, errdepth, ok):
+ if not ok:
+ return False
+ return (not hostname or errdepth != 0 or
+ certificate.get_subject().commonName == hostname)
+
+ ssl_context.set_verify(SSL.VERIFY_PEER, verify_peer)
+ if certfile:
+ ssl_context.use_certificate_file(certfile)
+ if keyfile:
+ if password:
+ with open(keyfile) as key_file:
+ key = crypto.load_privatekey(
+ crypto.FILETYPE_PEM,
+ key_file.read(),
+ password.encode('ascii'),
+ )
+ ssl_context.use_privatekey(key)
+ else:
+ ssl_context.use_privatekey_file(keyfile)
+ return ssl_context
+
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ssl_context.check_hostname = False
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
+ if ca_certs:
+ ssl_context.load_verify_locations(ca_certs)
+ if certfile:
+ ssl_context.load_cert_chain(
+ certfile=certfile,
+ keyfile=keyfile,
+ password=password,
+ )
+ ssl_context.check_hostname = hostname is not None
+ return ssl_context
def setup_cluster_ssl(client_auth=False):
@@ -90,44 +121,38 @@ def setup_cluster_ssl(client_auth=False):
start_cluster_wait_for_up(ccm_cluster)
-def validate_ssl_options(**kwargs):
- ssl_options = kwargs.get('ssl_options', None)
- ssl_context = kwargs.get('ssl_context', None)
- hostname = kwargs.get('hostname', '127.0.0.1')
-
- # find absolute path to client CA_CERTS
- tries = 0
- while True:
- if tries > 5:
- raise RuntimeError("Failed to connect to SSL cluster after 5 attempts")
- try:
- cluster = TestCluster(
- contact_points=[DefaultEndPoint(hostname)],
- ssl_options=ssl_options,
- ssl_context=ssl_context
- )
- session = cluster.connect(wait_for_all_pools=True)
- break
- except Exception:
- ex_type, ex, tb = sys.exc_info()
- log.warning("{0}: {1} Backtrace: {2}".format(ex_type.__name__, ex, traceback.extract_tb(tb)))
- del tb
- tries += 1
+def validate_ssl_context(ssl_context, hostname='127.0.0.1'):
+ tries = 0
+ while True:
+ if tries > 5:
+ raise RuntimeError("Failed to connect to SSL cluster after 5 attempts")
+ try:
+ cluster = TestCluster(
+ contact_points=[DefaultEndPoint(hostname)],
+ ssl_context=ssl_context
+ )
+ session = cluster.connect(wait_for_all_pools=True)
+ break
+ except Exception:
+ ex_type, ex, tb = sys.exc_info()
+ log.warning("{0}: {1} Backtrace: {2}".format(ex_type.__name__, ex, traceback.extract_tb(tb)))
+ del tb
+ tries += 1
+
+ # attempt a few simple commands.
+ insert_keyspace = """CREATE KEYSPACE ssltest
+ WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'}
+ """
+ statement = SimpleStatement(insert_keyspace)
+ statement.consistency_level = 3
+ session.execute(statement)
- # attempt a few simple commands.
- insert_keyspace = """CREATE KEYSPACE ssltest
- WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'}
- """
- statement = SimpleStatement(insert_keyspace)
- statement.consistency_level = 3
- session.execute(statement)
-
- drop_keyspace = "DROP KEYSPACE ssltest"
- statement = SimpleStatement(drop_keyspace)
- statement.consistency_level = ConsistencyLevel.ANY
- session.execute(statement)
+ drop_keyspace = "DROP KEYSPACE ssltest"
+ statement = SimpleStatement(drop_keyspace)
+ statement.consistency_level = ConsistencyLevel.ANY
+ session.execute(statement)
- cluster.shutdown()
+ cluster.shutdown()
class SSLConnectionTests(unittest.TestCase):
@@ -159,8 +184,7 @@ def test_can_connect_with_ssl_ca(self):
"""
# find absolute path to client CA_CERTS
- ssl_options = {'ca_certs': CLIENT_CA_CERTS,'ssl_version': ssl_version}
- validate_ssl_options(ssl_options=ssl_options)
+ validate_ssl_context(create_ssl_context())
def test_can_connect_with_ssl_long_running(self):
"""
@@ -174,15 +198,14 @@ def test_can_connect_with_ssl_long_running(self):
"""
# find absolute path to client CA_CERTS
- abs_path_ca_cert_path = os.path.abspath(CLIENT_CA_CERTS)
- ssl_options = {'ca_certs': abs_path_ca_cert_path,
- 'ssl_version': ssl_version}
+ ssl_context = create_ssl_context(
+ ca_certs=os.path.abspath(CLIENT_CA_CERTS))
tries = 0
while True:
if tries > 5:
raise RuntimeError("Failed to connect to SSL cluster after 5 attempts")
try:
- cluster = TestCluster(ssl_options=ssl_options)
+ cluster = TestCluster(ssl_context=ssl_context)
session = cluster.connect(wait_for_all_pools=True)
break
except Exception:
@@ -213,11 +236,7 @@ def test_can_connect_with_ssl_ca_host_match(self):
@test_category connection:ssl
"""
- ssl_options = {'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version}
- ssl_options.update(verify_certs)
-
- validate_ssl_options(ssl_options=ssl_options)
+ validate_ssl_context(create_ssl_context(hostname='127.0.0.1'))
class SSLConnectionAuthTests(unittest.TestCase):
@@ -246,11 +265,10 @@ def test_can_connect_with_ssl_client_auth(self):
@test_category connection:ssl
"""
- ssl_options = {'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version,
- 'keyfile': DRIVER_KEYFILE,
- 'certfile': DRIVER_CERTFILE}
- validate_ssl_options(ssl_options=ssl_options)
+ validate_ssl_context(create_ssl_context(
+ certfile=DRIVER_CERTFILE,
+ keyfile=DRIVER_KEYFILE,
+ ))
def test_can_connect_with_ssl_client_auth_host_name(self):
"""
@@ -267,13 +285,11 @@ def test_can_connect_with_ssl_client_auth_host_name(self):
@test_category connection:ssl
"""
- ssl_options = {'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version,
- 'keyfile': DRIVER_KEYFILE,
- 'certfile': DRIVER_CERTFILE}
- ssl_options.update(verify_certs)
-
- validate_ssl_options(ssl_options=ssl_options)
+ validate_ssl_context(create_ssl_context(
+ certfile=DRIVER_CERTFILE,
+ keyfile=DRIVER_KEYFILE,
+ hostname='127.0.0.1',
+ ))
def test_cannot_connect_without_client_auth(self):
"""
@@ -288,8 +304,7 @@ def test_cannot_connect_without_client_auth(self):
@test_category connection:ssl
"""
- cluster = TestCluster(ssl_options={'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version})
+ cluster = TestCluster(ssl_context=create_ssl_context())
with pytest.raises(NoHostAvailable):
cluster.connect()
@@ -309,33 +324,25 @@ def test_cannot_connect_with_bad_client_auth(self):
@test_category connection:ssl
"""
- ssl_options = {'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version,
- 'keyfile': DRIVER_KEYFILE}
-
- if not USES_PYOPENSSL:
- # I don't set the bad certfile for pyopenssl because it hangs
- ssl_options['certfile'] = DRIVER_CERTFILE_BAD
-
- cluster = TestCluster(
- ssl_options={'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version,
- 'keyfile': DRIVER_KEYFILE}
- )
+ # A context without a client certificate cannot authenticate to this
+ # cluster. This covers the same handshake failure without relying on
+ # legacy ssl_options parsing.
+ cluster = TestCluster(ssl_context=create_ssl_context())
with pytest.raises(NoHostAvailable):
cluster.connect()
cluster.shutdown()
def test_cannot_connect_with_invalid_hostname(self):
- ssl_options = {'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version,
- 'keyfile': DRIVER_KEYFILE,
- 'certfile': DRIVER_CERTFILE}
- ssl_options.update(verify_certs)
-
with pytest.raises(Exception):
- validate_ssl_options(ssl_options=ssl_options, hostname='localhost')
+ validate_ssl_context(
+ create_ssl_context(
+ certfile=DRIVER_CERTFILE,
+ keyfile=DRIVER_KEYFILE,
+ hostname='localhost',
+ ),
+ hostname='localhost',
+ )
class SSLSocketErrorTests(unittest.TestCase):
@@ -360,9 +367,7 @@ def test_ssl_want_write_errors_are_retried(self):
@test_category connection:ssl
"""
- ssl_options = {'ca_certs': CLIENT_CA_CERTS,
- 'ssl_version': ssl_version}
- cluster = TestCluster(ssl_options=ssl_options)
+ cluster = TestCluster(ssl_context=create_ssl_context())
session = cluster.connect(wait_for_all_pools=True)
try:
session.execute('drop keyspace ssl_error_test')
@@ -401,14 +406,7 @@ def test_can_connect_with_sslcontext_certificate(self):
@test_category connection:ssl
"""
- if USES_PYOPENSSL:
- ssl_context = SSL.Context(SSL.TLS_CLIENT_METHOD)
- ssl_context.load_verify_locations(CLIENT_CA_CERTS)
- else:
- ssl_context = ssl.SSLContext(ssl_version)
- ssl_context.load_verify_locations(CLIENT_CA_CERTS)
- ssl_context.verify_mode = ssl.CERT_REQUIRED
- validate_ssl_options(ssl_context=ssl_context)
+ validate_ssl_context(create_ssl_context())
def test_can_connect_with_ssl_client_auth_password_private_key(self):
"""
@@ -421,75 +419,35 @@ def test_can_connect_with_ssl_client_auth_password_private_key(self):
@test_category connection:ssl
"""
- abs_driver_keyfile = os.path.abspath(DRIVER_KEYFILE_ENCRYPTED)
- abs_driver_certfile = os.path.abspath(DRIVER_CERTFILE)
- ssl_options = {}
-
- if USES_PYOPENSSL:
- ssl_context = SSL.Context(SSL.TLS_CLIENT_METHOD)
- ssl_context.use_certificate_file(abs_driver_certfile)
- with open(abs_driver_keyfile) as keyfile:
- key = crypto.load_privatekey(crypto.FILETYPE_PEM, keyfile.read(), b'cassandra')
- ssl_context.use_privatekey(key)
- ssl_context.set_verify(SSL.VERIFY_NONE, verify_callback)
- else:
- ssl_context = ssl.SSLContext(ssl_version)
- ssl_context.load_cert_chain(certfile=abs_driver_certfile,
- keyfile=abs_driver_keyfile,
- password="cassandra")
- ssl_context.verify_mode = ssl.CERT_NONE
- validate_ssl_options(ssl_context=ssl_context, ssl_options=ssl_options)
+ validate_ssl_context(create_ssl_context(
+ certfile=os.path.abspath(DRIVER_CERTFILE),
+ keyfile=os.path.abspath(DRIVER_KEYFILE_ENCRYPTED),
+ password='cassandra',
+ ))
def test_can_connect_with_ssl_context_ca_host_match(self):
"""
Test to validate that we are able to connect to a cluster using a SSLContext
using client auth, an encrypted keyfile, and host matching
"""
- ssl_options = {}
- if USES_PYOPENSSL:
- ssl_context = SSL.Context(SSL.TLS_CLIENT_METHOD)
- ssl_context.use_certificate_file(DRIVER_CERTFILE)
- with open(DRIVER_KEYFILE_ENCRYPTED) as keyfile:
- key = crypto.load_privatekey(crypto.FILETYPE_PEM, keyfile.read(), b'cassandra')
- ssl_context.use_privatekey(key)
- ssl_context.load_verify_locations(CLIENT_CA_CERTS)
- ssl_options["check_hostname"] = True
- else:
- ssl_context = ssl.SSLContext(ssl_version)
- ssl_context.verify_mode = ssl.CERT_REQUIRED
- ssl_context.load_verify_locations(CLIENT_CA_CERTS)
- ssl_context.load_cert_chain(
- certfile=DRIVER_CERTFILE,
- keyfile=DRIVER_KEYFILE_ENCRYPTED,
- password="cassandra",
- )
- ssl_context.verify_mode = ssl.CERT_REQUIRED
- ssl_options["check_hostname"] = True
- validate_ssl_options(ssl_context=ssl_context, ssl_options=ssl_options)
+ validate_ssl_context(create_ssl_context(
+ certfile=DRIVER_CERTFILE,
+ keyfile=DRIVER_KEYFILE_ENCRYPTED,
+ password='cassandra',
+ hostname='127.0.0.1',
+ ))
def test_cannot_connect_ssl_context_with_invalid_hostname(self):
- ssl_options = {}
- if USES_PYOPENSSL:
- ssl_context = SSL.Context(SSL.TLS_CLIENT_METHOD)
- ssl_context.use_certificate_file(DRIVER_CERTFILE)
- with open(DRIVER_KEYFILE_ENCRYPTED) as keyfile:
- key = crypto.load_privatekey(crypto.FILETYPE_PEM, keyfile.read(), b"cassandra")
- ssl_context.use_privatekey(key)
- ssl_context.load_verify_locations(CLIENT_CA_CERTS)
- ssl_options["check_hostname"] = True
- else:
- ssl_context = ssl.SSLContext(ssl_version)
- ssl_context.verify_mode = ssl.CERT_REQUIRED
- ssl_context.load_verify_locations(CLIENT_CA_CERTS)
- ssl_context.load_cert_chain(
- certfile=DRIVER_CERTFILE,
- keyfile=DRIVER_KEYFILE_ENCRYPTED,
- password="cassandra",
- )
- ssl_context.verify_mode = ssl.CERT_REQUIRED
- ssl_options["check_hostname"] = True
with pytest.raises(Exception):
- validate_ssl_options(ssl_context=ssl_context, ssl_options=ssl_options, hostname="localhost")
+ validate_ssl_context(
+ create_ssl_context(
+ certfile=DRIVER_CERTFILE,
+ keyfile=DRIVER_KEYFILE_ENCRYPTED,
+ password='cassandra',
+ hostname='localhost',
+ ),
+ hostname='localhost',
+ )
@unittest.skipIf(USES_PYOPENSSL, "This test is for the built-in ssl.Context")
def test_can_connect_with_sslcontext_default_context(self):
@@ -499,4 +457,4 @@ def test_can_connect_with_sslcontext_default_context(self):
@test_category connection:ssl
"""
ssl_context = ssl.create_default_context(cafile=CLIENT_CA_CERTS)
- validate_ssl_options(ssl_context=ssl_context)
+ validate_ssl_context(ssl_context)
diff --git a/tests/unit/test_client_routes.py b/tests/unit/test_client_routes.py
index 0aa82fc76a..c0ac284ba4 100644
--- a/tests/unit/test_client_routes.py
+++ b/tests/unit/test_client_routes.py
@@ -437,7 +437,7 @@ def test_check_hostname_with_ssl_context_raises(self):
self.assertIn("check_hostname", str(cm.exception))
def test_check_hostname_with_ssl_options_raises(self):
- """Cluster should reject check_hostname=True in ssl_options with client_routes_config."""
+ """Cluster should reject ssl_options before configuring client routes."""
config = ClientRoutesConfig(
proxies=[ClientRouteProxy(str(uuid.uuid4()), "10.0.0.1")]
)
@@ -447,7 +447,8 @@ def test_check_hostname_with_ssl_options_raises(self):
ssl_options={'check_hostname': True},
client_routes_config=config,
)
- self.assertIn("check_hostname", str(cm.exception))
+ self.assertIn("ssl_options is deprecated", str(cm.exception))
+ self.assertIn("ssl_context", str(cm.exception))
def test_disabled_check_hostname_with_client_routes_ok(self):
"""Cluster should allow check_hostname=False with client_routes_config."""
diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py
index 3d55bc1860..f3e0b62f34 100644
--- a/tests/unit/test_cluster.py
+++ b/tests/unit/test_cluster.py
@@ -150,6 +150,33 @@ def test_backward_compat_positional(self):
class ClusterTest(unittest.TestCase):
+ def test_ssl_options_is_rejected(self):
+ for ssl_options in ({}, {'ca_certs': '/path/to/ca.pem'}):
+ with self.subTest(ssl_options=ssl_options):
+ with self.assertRaisesRegex(
+ ValueError,
+ "ssl_options is deprecated.*ssl_context.*"
+ "ssl-options-migration"):
+ Cluster(ssl_options=ssl_options)
+
+ def test_ssl_options_is_rejected_with_ssl_context(self):
+ with self.assertRaisesRegex(
+ ValueError,
+ "ssl_options is deprecated.*ssl_context.*"
+ "ssl-options-migration"):
+ Cluster(ssl_options={}, ssl_context=Mock())
+
+ def test_ssl_options_assignment_is_rejected(self):
+ cluster = Cluster()
+
+ with self.assertRaisesRegex(
+ ValueError,
+ "ssl_options is deprecated.*ssl_context.*"
+ "ssl-options-migration"):
+ cluster.ssl_options = {'ca_certs': '/path/to/ca.pem'}
+
+ assert cluster.ssl_options is None
+
def test_tuple_for_contact_points(self):
cluster = Cluster(contact_points=[('localhost', 9045), ('127.0.0.2', 9046), '127.0.0.3'], port=9999)
# Refactored for clarity