Skip to content

Commit 84771bf

Browse files
committed
ssl: share pyopenssl context option handling
1 parent 74a9b82 commit 84771bf

10 files changed

Lines changed: 303 additions & 102 deletions

File tree

cassandra/cluster.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1350,7 +1350,7 @@ def __init__(self,
13501350
# connections go through NLB proxies whose addresses won't match
13511351
# server certificates.
13521352
_check_hostname_enabled = False
1353-
if ssl_context is not None and ssl_context.check_hostname:
1353+
if ssl_context is not None and getattr(ssl_context, 'check_hostname', False):
13541354
_check_hostname_enabled = True
13551355
if ssl_options is not None and ssl_options.get('check_hostname', False):
13561356
_check_hostname_enabled = True

cassandra/connection.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,86 @@ def _ipaddress_match(pattern, host_ip):
154154
return False
155155

156156

157+
def _default_pyopenssl_ssl_method(ssl_module):
158+
for method_name in ('TLS_CLIENT_METHOD', 'TLS_METHOD', 'TLSv1_2_METHOD'):
159+
method = getattr(ssl_module, method_name, None)
160+
if method is not None:
161+
return method
162+
raise ImportError('pyOpenSSL does not expose a secure TLS client method')
163+
164+
165+
def _pyopenssl_ssl_method_from_ssl_version(ssl_version, ssl_module):
166+
if not isinstance(ssl_version, type(ssl.PROTOCOL_TLS)):
167+
return ssl_version
168+
169+
protocol_method_names = (
170+
('PROTOCOL_TLS', None),
171+
('PROTOCOL_SSLv23', None),
172+
('PROTOCOL_TLS_CLIENT', None),
173+
('PROTOCOL_TLS_SERVER', ('TLS_SERVER_METHOD', 'TLS_METHOD')),
174+
('PROTOCOL_TLSv1', ('TLSv1_METHOD',)),
175+
('PROTOCOL_TLSv1_1', ('TLSv1_1_METHOD',)),
176+
('PROTOCOL_TLSv1_2', ('TLSv1_2_METHOD',)),
177+
)
178+
for protocol_name, method_names in protocol_method_names:
179+
if getattr(ssl, protocol_name, None) is ssl_version:
180+
if method_names is None:
181+
return _default_pyopenssl_ssl_method(ssl_module)
182+
for method_name in method_names:
183+
method = getattr(ssl_module, method_name, None)
184+
if method is not None:
185+
return method
186+
raise ValueError('pyOpenSSL does not expose a method for {}'.format(protocol_name))
187+
188+
return ssl_version
189+
190+
191+
def _pyopenssl_verify_mode_from_cert_reqs(cert_reqs, ssl_module):
192+
if not isinstance(cert_reqs, type(ssl.CERT_NONE)):
193+
return cert_reqs
194+
if cert_reqs == ssl.CERT_NONE:
195+
return ssl_module.VERIFY_NONE
196+
if cert_reqs in (ssl.CERT_OPTIONAL, ssl.CERT_REQUIRED):
197+
return ssl_module.VERIFY_PEER
198+
return cert_reqs
199+
200+
201+
def _build_pyopenssl_context_from_options(ssl_options, ssl_module):
202+
ssl_version = (ssl_options['ssl_version']
203+
if 'ssl_version' in ssl_options else _default_pyopenssl_ssl_method(ssl_module))
204+
ssl_version = _pyopenssl_ssl_method_from_ssl_version(ssl_version, ssl_module)
205+
context = ssl_module.Context(ssl_version)
206+
certfile = ssl_options.get('certfile', None)
207+
keyfile = ssl_options.get('keyfile', None)
208+
if certfile:
209+
use_certificate_chain_file = getattr(context, 'use_certificate_chain_file', None)
210+
if use_certificate_chain_file:
211+
use_certificate_chain_file(certfile)
212+
else:
213+
context.use_certificate_file(certfile)
214+
context.use_privatekey_file(keyfile or certfile)
215+
context.check_privatekey()
216+
elif keyfile:
217+
context.use_privatekey_file(keyfile)
218+
ca_certs = ssl_options.get('ca_certs', None)
219+
if ca_certs:
220+
context.load_verify_locations(ca_certs)
221+
ciphers = ssl_options.get('ciphers', None)
222+
if ciphers:
223+
context.set_cipher_list(ciphers)
224+
cert_reqs = ssl_options.get('cert_reqs', None)
225+
if cert_reqs is None:
226+
cert_reqs = (ssl_module.VERIFY_PEER
227+
if (ssl_options.get('ca_certs', None) or ssl_options.get('check_hostname', False))
228+
else ssl_module.VERIFY_NONE)
229+
cert_reqs = _pyopenssl_verify_mode_from_cert_reqs(cert_reqs, ssl_module)
230+
context.set_verify(
231+
cert_reqs,
232+
callback=lambda _connection, _x509, _errnum, _errdepth, ok: ok
233+
)
234+
return context
235+
236+
157237
segment_codec_no_compression = SegmentCodec()
158238
segment_codec_lz4 = None
159239

cassandra/datastax/cloud/__init__.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from zipfile import BadZipfile as BadZipFile
3535

3636
from cassandra import DriverException
37+
from cassandra.connection import _build_pyopenssl_context_from_options
3738

3839
log = logging.getLogger(__name__)
3940

@@ -89,7 +90,7 @@ def get_cloud_config(cloud_config, create_pyopenssl_context=False):
8990

9091
config = read_metadata_info(config, cloud_config)
9192
if create_pyopenssl_context:
92-
config.ssl_context = config.pyopenssl_context
93+
config.ssl_context = getattr(config, 'pyopenssl_context', config.ssl_context)
9394
return config
9495

9596

@@ -112,19 +113,19 @@ def parse_cloud_config(path, cloud_config, create_pyopenssl_context):
112113

113114
config = CloudConfig.from_dict(data)
114115
config_dir = os.path.dirname(path)
116+
ca_cert_location = os.path.join(config_dir, 'ca.crt')
117+
cert_location = os.path.join(config_dir, 'cert')
118+
key_location = os.path.join(config_dir, 'key')
115119

116120
if 'ssl_context' in cloud_config:
117121
config.ssl_context = cloud_config['ssl_context']
118122
else:
119123
# Load the ssl_context before we delete the temporary directory
120-
ca_cert_location = os.path.join(config_dir, 'ca.crt')
121-
cert_location = os.path.join(config_dir, 'cert')
122-
key_location = os.path.join(config_dir, 'key')
123124
# Regardless of if we create a pyopenssl context, we still need the builtin one
124125
# to connect to the metadata service
125126
config.ssl_context = _ssl_context_from_cert(ca_cert_location, cert_location, key_location)
126-
if create_pyopenssl_context:
127-
config.pyopenssl_context = _pyopenssl_context_from_cert(ca_cert_location, cert_location, key_location)
127+
if create_pyopenssl_context:
128+
config.pyopenssl_context = _pyopenssl_context_from_cert(ca_cert_location, cert_location, key_location)
128129

129130
return config
130131

@@ -176,25 +177,19 @@ def _ssl_context_from_cert(ca_cert_location, cert_location, key_location):
176177
return ssl_context
177178

178179

179-
def _default_pyopenssl_ssl_method(ssl_module):
180-
for method_name in ('TLS_CLIENT_METHOD', 'TLS_METHOD', 'TLSv1_2_METHOD'):
181-
method = getattr(ssl_module, method_name, None)
182-
if method is not None:
183-
return method
184-
raise ImportError('pyOpenSSL does not expose a secure TLS client method')
185-
186-
187180
def _pyopenssl_context_from_cert(ca_cert_location, cert_location, key_location):
188181
try:
189182
from OpenSSL import SSL
190183
except ImportError as e:
191184
raise ImportError(
192185
"PyOpenSSL must be installed to connect to Astra with the Eventlet or Twisted event loops")\
193186
.with_traceback(e.__traceback__)
194-
ssl_context = SSL.Context(_default_pyopenssl_ssl_method(SSL))
195-
ssl_context.set_verify(SSL.VERIFY_PEER, callback=lambda _1, _2, _3, _4, ok: ok)
196-
ssl_context.use_certificate_file(cert_location)
197-
ssl_context.use_privatekey_file(key_location)
198-
ssl_context.load_verify_locations(ca_cert_location)
199-
200-
return ssl_context
187+
return _build_pyopenssl_context_from_options(
188+
{
189+
'ca_certs': ca_cert_location,
190+
'certfile': cert_location,
191+
'keyfile': key_location,
192+
'cert_reqs': SSL.VERIFY_PEER,
193+
},
194+
SSL
195+
)

cassandra/io/eventletreactor.py

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import time
2525

2626
from cassandra.connection import (Connection, ConnectionShutdown, Timer, TimerManager,
27+
_build_pyopenssl_context_from_options,
2728
_validate_pyopenssl_hostname)
2829
try:
2930
from eventlet.green.OpenSSL import SSL
@@ -36,14 +37,6 @@
3637
log = logging.getLogger(__name__)
3738

3839

39-
def _default_ssl_method():
40-
for method_name in ('TLS_CLIENT_METHOD', 'TLS_METHOD', 'TLSv1_2_METHOD'):
41-
method = getattr(SSL, method_name, None)
42-
if method is not None:
43-
return method
44-
raise ImportError('pyOpenSSL does not expose a secure TLS client method')
45-
46-
4740
def _check_pyopenssl():
4841
if not _PYOPENSSL:
4942
raise ImportError(
@@ -52,28 +45,6 @@ def _check_pyopenssl():
5245
)
5346

5447

55-
def _build_pyopenssl_context_from_options(ssl_options):
56-
ssl_version = (ssl_options['ssl_version']
57-
if 'ssl_version' in ssl_options else _default_ssl_method())
58-
context = SSL.Context(ssl_version)
59-
if 'certfile' in ssl_options:
60-
context.use_certificate_file(ssl_options['certfile'])
61-
if 'keyfile' in ssl_options:
62-
context.use_privatekey_file(ssl_options['keyfile'])
63-
if 'ca_certs' in ssl_options:
64-
context.load_verify_locations(ssl_options['ca_certs'])
65-
cert_reqs = ssl_options.get('cert_reqs', None)
66-
if cert_reqs is None:
67-
cert_reqs = (SSL.VERIFY_PEER
68-
if (ssl_options.get('ca_certs', None) or ssl_options.get('check_hostname', False))
69-
else SSL.VERIFY_NONE)
70-
context.set_verify(
71-
cert_reqs,
72-
callback=lambda _connection, _x509, _errnum, _errdepth, ok: ok
73-
)
74-
return context
75-
76-
7748
class EventletConnection(Connection):
7849
"""
7950
An implementation of :class:`.Connection` that utilizes ``eventlet``.
@@ -158,7 +129,7 @@ def _validate_hostname(self):
158129

159130
def _build_ssl_context_from_options(self):
160131
_check_pyopenssl()
161-
return _build_pyopenssl_context_from_options(self.ssl_options)
132+
return _build_pyopenssl_context_from_options(self.ssl_options, SSL)
162133

163134
def close(self):
164135
with self.lock:

cassandra/io/twistedreactor.py

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from zope.interface import implementer
3030

3131
from cassandra.connection import (Connection, ConnectionShutdown, Timer, TimerManager, ConnectionException,
32+
_build_pyopenssl_context_from_options,
3233
_validate_pyopenssl_hostname)
3334

3435
try:
@@ -40,14 +41,6 @@
4041
log = logging.getLogger(__name__)
4142

4243

43-
def _default_ssl_method():
44-
for method_name in ("TLS_CLIENT_METHOD", "TLS_METHOD", "TLSv1_2_METHOD"):
45-
method = getattr(SSL, method_name, None)
46-
if method is not None:
47-
return method
48-
raise ImportError("pyOpenSSL does not expose a secure TLS client method")
49-
50-
5144
def _check_pyopenssl():
5245
if not _HAS_SSL:
5346
raise ImportError(
@@ -56,28 +49,6 @@ def _check_pyopenssl():
5649
)
5750

5851

59-
def _build_pyopenssl_context_from_options(ssl_options):
60-
ssl_version = (ssl_options["ssl_version"]
61-
if "ssl_version" in ssl_options else _default_ssl_method())
62-
context = SSL.Context(ssl_version)
63-
if "certfile" in ssl_options:
64-
context.use_certificate_file(ssl_options["certfile"])
65-
if "keyfile" in ssl_options:
66-
context.use_privatekey_file(ssl_options["keyfile"])
67-
if "ca_certs" in ssl_options:
68-
context.load_verify_locations(ssl_options["ca_certs"])
69-
cert_reqs = ssl_options.get("cert_reqs", None)
70-
if cert_reqs is None:
71-
cert_reqs = (SSL.VERIFY_PEER
72-
if (ssl_options.get("ca_certs", None) or ssl_options.get("check_hostname", False))
73-
else SSL.VERIFY_NONE)
74-
context.set_verify(
75-
cert_reqs,
76-
callback=lambda _connection, _x509, _errnum, _errdepth, ok: ok
77-
)
78-
return context
79-
80-
8152
def _cleanup(cleanup_weakref):
8253
try:
8354
cleanup_weakref()._cleanup()
@@ -187,8 +158,7 @@ def __init__(self, endpoint, ssl_context, ssl_options, check_hostname, timeout):
187158
if ssl_context is not None:
188159
self.context = ssl_context
189160
else:
190-
self.context = _build_pyopenssl_context_from_options(self.ssl_options)
191-
self.context.set_info_callback(self.info_callback)
161+
self.context = _build_pyopenssl_context_from_options(self.ssl_options, SSL)
192162

193163
def verify_callback(self, connection, x509, errnum, errdepth, ok):
194164
return ok
@@ -208,6 +178,8 @@ def info_callback(self, connection, where, ret):
208178
def clientConnectionForTLS(self, tlsProtocol):
209179
connection = SSL.Connection(self.context, None)
210180
connection.set_app_data(tlsProtocol)
181+
if self.check_hostname:
182+
connection.set_info_callback(self.info_callback)
211183
if self.ssl_options and "server_hostname" in self.ssl_options:
212184
connection.set_tlsext_host_name(self.ssl_options['server_hostname'].encode('ascii'))
213185
return connection
@@ -256,7 +228,7 @@ def _check_pyopenssl(self):
256228

257229
def _build_ssl_context_from_options(self):
258230
_check_pyopenssl()
259-
return _build_pyopenssl_context_from_options(self.ssl_options)
231+
return _build_pyopenssl_context_from_options(self.ssl_options, SSL)
260232

261233
def add_connection(self):
262234
"""

tests/unit/io/test_eventletreactor.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
eventletreactor = None
4242
EventletConnection = None # noqa
4343

44-
from cassandra.connection import Connection, DefaultEndPoint
44+
from cassandra.connection import Connection, DefaultEndPoint, _default_pyopenssl_ssl_method
4545

4646
CA_CERTS = os.path.abspath(os.path.join(
4747
os.path.dirname(__file__), '..', '..', 'integration', 'long', 'ssl', 'rootCa.crt'))
@@ -82,7 +82,7 @@ def test_empty_ssl_options_build_pyopenssl_context(self):
8282

8383
def test_empty_ssl_options_default_to_negotiating_tls(self):
8484
with patch.object(eventletreactor.SSL, 'Context') as context_mock:
85-
context = eventletreactor._build_pyopenssl_context_from_options({})
85+
context = eventletreactor._build_pyopenssl_context_from_options({}, eventletreactor.SSL)
8686

8787
context_mock.assert_called_once_with(eventletreactor.SSL.TLS_CLIENT_METHOD)
8888
assert context is context_mock.return_value
@@ -91,21 +91,28 @@ def test_default_ssl_method_falls_back_to_tls_method(self):
9191
tls_method = object()
9292

9393
with patch.object(eventletreactor, 'SSL', SimpleNamespace(TLS_METHOD=tls_method)):
94-
assert eventletreactor._default_ssl_method() is tls_method
94+
assert _default_pyopenssl_ssl_method(eventletreactor.SSL) is tls_method
9595

9696
def test_default_ssl_method_falls_back_to_tlsv1_2_method(self):
9797
tlsv1_2_method = object()
9898

9999
with patch.object(eventletreactor, 'SSL', SimpleNamespace(TLSv1_2_METHOD=tlsv1_2_method)):
100-
assert eventletreactor._default_ssl_method() is tlsv1_2_method
100+
assert _default_pyopenssl_ssl_method(eventletreactor.SSL) is tlsv1_2_method
101101

102102
def test_ssl_version_option_is_preserved(self):
103103
with patch.object(eventletreactor.SSL, 'Context') as context_mock:
104104
eventletreactor._build_pyopenssl_context_from_options(
105-
{'ssl_version': eventletreactor.SSL.TLSv1_2_METHOD})
105+
{'ssl_version': eventletreactor.SSL.TLSv1_2_METHOD}, eventletreactor.SSL)
106106

107107
context_mock.assert_called_once_with(eventletreactor.SSL.TLSv1_2_METHOD)
108108

109+
def test_stdlib_ssl_version_option_is_translated(self):
110+
with patch.object(eventletreactor.SSL, 'Context') as context_mock:
111+
eventletreactor._build_pyopenssl_context_from_options(
112+
{'ssl_version': ssl.PROTOCOL_TLS}, eventletreactor.SSL)
113+
114+
context_mock.assert_called_once_with(eventletreactor.SSL.TLS_CLIENT_METHOD)
115+
109116
def test_ca_certs_default_to_required_validation(self):
110117
conn = EventletConnection.__new__(EventletConnection)
111118

0 commit comments

Comments
 (0)