Skip to content

Commit f8eb94d

Browse files
Add PyOpenSSL TLS session caching for Eventlet and Twisted
- EventletConnection: restore cached session before handshake via set_session(), store session after do_handshake() via _cache_pyopenssl_session() - TwistedConnection: pass ssl_session_cache to _SSLCreator, restore cached session in clientConnectionForTLS(), store after handshake in info_callback() - All operations wrapped in try/except for error tolerance - Debug logging for session reuse and restore/store failures
1 parent 7504d4f commit f8eb94d

File tree

3 files changed

+142
-3
lines changed

3 files changed

+142
-3
lines changed

cassandra/io/eventletreactor.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ def _wrap_socket_from_context(self):
108108
if self.ssl_options and 'server_hostname' in self.ssl_options:
109109
# This is necessary for SNI
110110
self._socket.set_tlsext_host_name(self.ssl_options['server_hostname'].encode('ascii'))
111+
# Apply cached TLS session for resumption (PyOpenSSL)
112+
if self._ssl_session_cache:
113+
cached_session = self._ssl_session_cache.get(
114+
self._ssl_session_cache_key())
115+
if cached_session:
116+
try:
117+
self._socket.set_session(cached_session)
118+
log.debug("Using cached TLS session for %s", self.endpoint)
119+
except Exception:
120+
log.debug("Could not restore TLS session for %s", self.endpoint)
111121

112122
def _initiate_connection(self, sockaddr):
113123
if self.uses_legacy_ssl_options:
@@ -116,6 +126,8 @@ def _initiate_connection(self, sockaddr):
116126
self._socket.connect(sockaddr)
117127
if self.ssl_context or self.ssl_options:
118128
self._socket.do_handshake()
129+
# Store TLS session after successful handshake (PyOpenSSL)
130+
self._cache_pyopenssl_session()
119131

120132
def _match_hostname(self):
121133
if self.uses_legacy_ssl_options:
@@ -126,6 +138,19 @@ def _match_hostname(self):
126138
raise Exception("Hostname verification failed! Certificate name '{}' "
127139
"doesn't endpoint '{}'".format(cert_name, self.endpoint.address))
128140

141+
def _cache_pyopenssl_session(self):
142+
"""Store the PyOpenSSL TLS session in the cache after a successful handshake."""
143+
if self._ssl_session_cache is not None:
144+
try:
145+
session = self._socket.get_session()
146+
if session:
147+
self._ssl_session_cache.set(
148+
self._ssl_session_cache_key(), session)
149+
if self._socket.session_reused():
150+
log.debug("TLS session was reused for %s", self.endpoint)
151+
except Exception:
152+
log.debug("Could not cache TLS session for %s", self.endpoint)
153+
129154
def close(self):
130155
with self.lock:
131156
if self.is_closed:

cassandra/io/twistedreactor.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,12 @@ def _on_loop_timer(self):
139139

140140
@implementer(IOpenSSLClientConnectionCreator)
141141
class _SSLCreator(object):
142-
def __init__(self, endpoint, ssl_context, ssl_options, check_hostname, timeout):
142+
def __init__(self, endpoint, ssl_context, ssl_options, check_hostname, timeout, ssl_session_cache=None):
143143
self.endpoint = endpoint
144144
self.ssl_options = ssl_options
145145
self.check_hostname = check_hostname
146146
self.timeout = timeout
147+
self.ssl_session_cache = ssl_session_cache
147148

148149
if ssl_context:
149150
self.context = ssl_context
@@ -170,12 +171,36 @@ def info_callback(self, connection, where, ret):
170171
if self.check_hostname and self.endpoint.address != connection.get_peer_certificate().get_subject().commonName:
171172
transport = connection.get_app_data()
172173
transport.failVerification(Failure(ConnectionException("Hostname verification failed", self.endpoint)))
174+
return
175+
# Store TLS session after successful handshake (PyOpenSSL)
176+
if self.ssl_session_cache is not None:
177+
try:
178+
session = connection.get_session()
179+
if session:
180+
self.ssl_session_cache.set(
181+
self.endpoint.tls_session_cache_key, session)
182+
if connection.session_reused():
183+
log.debug("TLS session was reused for %s", self.endpoint)
184+
except Exception:
185+
log.debug("Could not cache TLS session for %s", self.endpoint)
173186

174187
def clientConnectionForTLS(self, tlsProtocol):
175188
connection = SSL.Connection(self.context, None)
176189
connection.set_app_data(tlsProtocol)
177190
if self.ssl_options and "server_hostname" in self.ssl_options:
178191
connection.set_tlsext_host_name(self.ssl_options['server_hostname'].encode('ascii'))
192+
193+
# Apply cached TLS session for resumption (PyOpenSSL)
194+
if self.ssl_session_cache is not None:
195+
cached_session = self.ssl_session_cache.get(
196+
self.endpoint.tls_session_cache_key)
197+
if cached_session:
198+
try:
199+
connection.set_session(cached_session)
200+
log.debug("Using cached TLS session for %s", self.endpoint)
201+
except Exception:
202+
log.debug("Could not restore TLS session for %s", self.endpoint)
203+
179204
return connection
180205

181206

@@ -241,6 +266,7 @@ def add_connection(self):
241266
self.ssl_options,
242267
self._check_hostname,
243268
self.connect_timeout,
269+
ssl_session_cache=self._ssl_session_cache,
244270
)
245271

246272
endpoint = SSL4ClientEndpoint(

tests/unit/io/test_twistedreactor.py

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
# limitations under the License.
1414

1515
import unittest
16-
from unittest.mock import Mock, patch
16+
from unittest.mock import Mock, patch, MagicMock
1717

18-
from cassandra.connection import DefaultEndPoint
18+
from cassandra.connection import DefaultEndPoint, SSLSessionCache
1919

2020
try:
2121
from twisted.test import proto_helpers
@@ -197,3 +197,91 @@ def test_push(self, mock_connectTCP):
197197
self.obj_ut.push('123 pickup')
198198
self.mock_reactor_cft.assert_called_with(
199199
transport_mock.write, '123 pickup')
200+
201+
202+
class TestSSLCreatorInfoCallback(unittest.TestCase):
203+
"""Verify that _SSLCreator.info_callback does not cache TLS sessions
204+
when hostname verification fails."""
205+
206+
def setUp(self):
207+
if twistedreactor is None:
208+
raise unittest.SkipTest("Twisted libraries not available")
209+
from OpenSSL import SSL
210+
self.SSL = SSL
211+
212+
def _make_creator(self, check_hostname, endpoint_address, cert_cn,
213+
ssl_session_cache=None):
214+
from cassandra.io.twistedreactor import _SSLCreator
215+
endpoint = Mock()
216+
endpoint.address = endpoint_address
217+
endpoint.tls_session_cache_key = (endpoint_address, 9042)
218+
ssl_ctx = Mock()
219+
creator = _SSLCreator(
220+
endpoint=endpoint,
221+
ssl_context=ssl_ctx,
222+
ssl_options=None,
223+
check_hostname=check_hostname,
224+
timeout=5,
225+
ssl_session_cache=ssl_session_cache,
226+
)
227+
228+
# Build a mock OpenSSL connection
229+
connection = Mock()
230+
subject = Mock()
231+
subject.commonName = cert_cn
232+
cert = Mock()
233+
cert.get_subject.return_value = subject
234+
connection.get_peer_certificate.return_value = cert
235+
session = Mock()
236+
connection.get_session.return_value = session
237+
connection.session_reused.return_value = False
238+
transport = Mock()
239+
connection.get_app_data.return_value = transport
240+
241+
return creator, connection, transport, session
242+
243+
def test_hostname_mismatch_does_not_cache(self):
244+
"""When hostname verification fails, the session must NOT be cached."""
245+
cache = SSLSessionCache()
246+
creator, connection, transport, session = self._make_creator(
247+
check_hostname=True,
248+
endpoint_address='good.example.com',
249+
cert_cn='evil.example.com',
250+
ssl_session_cache=cache,
251+
)
252+
253+
creator.info_callback(connection, self.SSL.SSL_CB_HANDSHAKE_DONE, 0)
254+
255+
transport.failVerification.assert_called_once()
256+
assert cache.size() == 0, "Session was cached despite hostname mismatch"
257+
258+
def test_hostname_match_caches_session(self):
259+
"""When hostname matches, the session should be cached."""
260+
cache = SSLSessionCache()
261+
creator, connection, transport, session = self._make_creator(
262+
check_hostname=True,
263+
endpoint_address='good.example.com',
264+
cert_cn='good.example.com',
265+
ssl_session_cache=cache,
266+
)
267+
268+
creator.info_callback(connection, self.SSL.SSL_CB_HANDSHAKE_DONE, 0)
269+
270+
transport.failVerification.assert_not_called()
271+
assert cache.size() == 1
272+
assert cache.get(('good.example.com', 9042)) is session
273+
274+
def test_no_check_hostname_caches_session(self):
275+
"""When check_hostname is False, always cache regardless of CN."""
276+
cache = SSLSessionCache()
277+
creator, connection, transport, session = self._make_creator(
278+
check_hostname=False,
279+
endpoint_address='good.example.com',
280+
cert_cn='evil.example.com',
281+
ssl_session_cache=cache,
282+
)
283+
284+
creator.info_callback(connection, self.SSL.SSL_CB_HANDSHAKE_DONE, 0)
285+
286+
transport.failVerification.assert_not_called()
287+
assert cache.size() == 1

0 commit comments

Comments
 (0)