Skip to content

Commit f119103

Browse files
committed
ssl: avoid insecure pyopenssl fallback
1 parent b1f4e6a commit f119103

7 files changed

Lines changed: 123 additions & 10 deletions

File tree

cassandra/datastax/cloud/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,17 +176,25 @@ def _ssl_context_from_cert(ca_cert_location, cert_location, key_location):
176176
return ssl_context
177177

178178

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+
179187
def _pyopenssl_context_from_cert(ca_cert_location, cert_location, key_location):
180188
try:
181189
from OpenSSL import SSL
182190
except ImportError as e:
183191
raise ImportError(
184192
"PyOpenSSL must be installed to connect to Astra with the Eventlet or Twisted event loops")\
185193
.with_traceback(e.__traceback__)
186-
ssl_context = SSL.Context(SSL.TLSv1_METHOD)
194+
ssl_context = SSL.Context(_default_pyopenssl_ssl_method(SSL))
187195
ssl_context.set_verify(SSL.VERIFY_PEER, callback=lambda _1, _2, _3, _4, ok: ok)
188196
ssl_context.use_certificate_file(cert_location)
189197
ssl_context.use_privatekey_file(key_location)
190198
ssl_context.load_verify_locations(ca_cert_location)
191199

192-
return ssl_context
200+
return ssl_context

cassandra/io/eventletreactor.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@
3737

3838

3939
def _default_ssl_method():
40-
return getattr(SSL, 'TLS_CLIENT_METHOD',
41-
getattr(SSL, 'TLS_METHOD', SSL.TLSv1_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')
4245

4346

4447
def _check_pyopenssl():

cassandra/io/twistedreactor.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@
4141

4242

4343
def _default_ssl_method():
44-
return getattr(SSL, "TLS_CLIENT_METHOD",
45-
getattr(SSL, "TLS_METHOD", SSL.TLSv1_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")
4649

4750

4851
def _check_pyopenssl():

tests/unit/io/test_eventletreactor.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import os
1616
import unittest
1717
from datetime import datetime, timedelta, timezone
18+
from types import SimpleNamespace
1819

1920
from unittest.mock import Mock, patch
2021

@@ -85,12 +86,24 @@ def test_empty_ssl_options_default_to_negotiating_tls(self):
8586
context_mock.assert_called_once_with(eventletreactor.SSL.TLS_CLIENT_METHOD)
8687
assert context is context_mock.return_value
8788

89+
def test_default_ssl_method_falls_back_to_tls_method(self):
90+
tls_method = object()
91+
92+
with patch.object(eventletreactor, 'SSL', SimpleNamespace(TLS_METHOD=tls_method)):
93+
assert eventletreactor._default_ssl_method() is tls_method
94+
95+
def test_default_ssl_method_falls_back_to_tlsv1_2_method(self):
96+
tlsv1_2_method = object()
97+
98+
with patch.object(eventletreactor, 'SSL', SimpleNamespace(TLSv1_2_METHOD=tlsv1_2_method)):
99+
assert eventletreactor._default_ssl_method() is tlsv1_2_method
100+
88101
def test_ssl_version_option_is_preserved(self):
89102
with patch.object(eventletreactor.SSL, 'Context') as context_mock:
90103
eventletreactor._build_pyopenssl_context_from_options(
91-
{'ssl_version': eventletreactor.SSL.TLSv1_METHOD})
104+
{'ssl_version': eventletreactor.SSL.TLSv1_2_METHOD})
92105

93-
context_mock.assert_called_once_with(eventletreactor.SSL.TLSv1_METHOD)
106+
context_mock.assert_called_once_with(eventletreactor.SSL.TLSv1_2_METHOD)
94107

95108
def test_ca_certs_default_to_required_validation(self):
96109
conn = EventletConnection.__new__(EventletConnection)

tests/unit/io/test_twistedreactor.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import os
1616
import unittest
1717
from datetime import datetime, timedelta, timezone
18+
from types import SimpleNamespace
1819
from unittest.mock import Mock, patch
1920

2021
try:
@@ -76,12 +77,24 @@ def test_empty_ssl_options_default_to_negotiating_tls(self):
7677
context_mock.assert_called_once_with(twistedreactor.SSL.TLS_CLIENT_METHOD)
7778
assert context is context_mock.return_value
7879

80+
def test_default_ssl_method_falls_back_to_tls_method(self):
81+
tls_method = object()
82+
83+
with patch.object(twistedreactor, 'SSL', SimpleNamespace(TLS_METHOD=tls_method)):
84+
assert twistedreactor._default_ssl_method() is tls_method
85+
86+
def test_default_ssl_method_falls_back_to_tlsv1_2_method(self):
87+
tlsv1_2_method = object()
88+
89+
with patch.object(twistedreactor, 'SSL', SimpleNamespace(TLSv1_2_METHOD=tlsv1_2_method)):
90+
assert twistedreactor._default_ssl_method() is tlsv1_2_method
91+
7992
def test_ssl_version_option_is_preserved(self):
8093
with patch.object(twistedreactor.SSL, 'Context') as context_mock:
8194
twistedreactor._build_pyopenssl_context_from_options(
82-
{'ssl_version': twistedreactor.SSL.TLSv1_METHOD})
95+
{'ssl_version': twistedreactor.SSL.TLSv1_2_METHOD})
8396

84-
context_mock.assert_called_once_with(twistedreactor.SSL.TLSv1_METHOD)
97+
context_mock.assert_called_once_with(twistedreactor.SSL.TLSv1_2_METHOD)
8598

8699
def test_ca_certs_default_to_required_validation(self):
87100
context = twistedreactor._build_pyopenssl_context_from_options({'ca_certs': CA_CERTS})

tests/unit/test_cloud.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright DataStax, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from types import SimpleNamespace
16+
17+
import pytest
18+
19+
from cassandra.datastax import cloud
20+
21+
22+
def test_default_pyopenssl_ssl_method_prefers_tls_client_method():
23+
tls_client_method = object()
24+
25+
ssl_module = SimpleNamespace(
26+
TLS_CLIENT_METHOD=tls_client_method,
27+
TLS_METHOD=object(),
28+
TLSv1_2_METHOD=object())
29+
30+
assert cloud._default_pyopenssl_ssl_method(ssl_module) is tls_client_method
31+
32+
33+
def test_default_pyopenssl_ssl_method_falls_back_to_tls_method():
34+
tls_method = object()
35+
36+
ssl_module = SimpleNamespace(
37+
TLS_METHOD=tls_method,
38+
TLSv1_2_METHOD=object())
39+
40+
assert cloud._default_pyopenssl_ssl_method(ssl_module) is tls_method
41+
42+
43+
def test_default_pyopenssl_ssl_method_falls_back_to_tlsv1_2_method():
44+
tlsv1_2_method = object()
45+
46+
ssl_module = SimpleNamespace(TLSv1_2_METHOD=tlsv1_2_method)
47+
48+
assert cloud._default_pyopenssl_ssl_method(ssl_module) is tlsv1_2_method
49+
50+
51+
def test_default_pyopenssl_ssl_method_requires_secure_method():
52+
with pytest.raises(ImportError, match="secure TLS client method"):
53+
cloud._default_pyopenssl_ssl_method(SimpleNamespace())

tests/unit/test_cluster.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import unittest
15+
import warnings
1516

1617
from concurrent.futures import Future
1718
import logging
@@ -278,6 +279,25 @@ def test_connection_factory_passes_compression_kwarg(self):
278279
assert factory.call_args.kwargs['compression'] == expected
279280
assert cluster.compression == expected
280281

282+
def test_empty_ssl_options_are_rejected_with_cloud_config(self):
283+
with pytest.raises(ValueError) as exc:
284+
Cluster(cloud={'secure_connect_bundle': 'unused'}, ssl_options={})
285+
286+
assert "cannot be specified with a cloud configuration" in str(exc.value)
287+
288+
def test_empty_ssl_options_emit_deprecation_warning(self):
289+
with pytest.warns(DeprecationWarning, match="Using ssl_options without ssl_context"):
290+
cluster = Cluster(ssl_options={})
291+
292+
assert cluster.ssl_options == {}
293+
294+
def test_omitted_ssl_options_do_not_emit_deprecation_warning(self):
295+
with warnings.catch_warnings(record=True) as recorded_warnings:
296+
warnings.simplefilter("always")
297+
Cluster()
298+
299+
assert not [warning for warning in recorded_warnings if warning.category is DeprecationWarning]
300+
281301

282302
class SchedulerTest(unittest.TestCase):
283303
# TODO: this suite could be expanded; for now just adding a test covering a ticket

0 commit comments

Comments
 (0)