1717import errno
1818from functools import wraps , partial , total_ordering
1919from heapq import heappush , heappop
20+ import ipaddress
2021import io
2122import logging
2223import socket
@@ -130,6 +131,220 @@ def decompress(byts):
130131frame_header_v3 = struct .Struct ('>BhBi' )
131132
132133
134+ def _default_pyopenssl_ssl_method (ssl_module ):
135+ for method_name in ('TLS_CLIENT_METHOD' , 'TLS_METHOD' , 'TLSv1_2_METHOD' ):
136+ method = getattr (ssl_module , method_name , None )
137+ if method is not None :
138+ return method
139+ raise ImportError ('pyOpenSSL does not expose a secure TLS client method' )
140+
141+
142+ def _pyopenssl_ssl_method_from_stdlib (ssl_module , ssl_version ):
143+ if ssl_version is None :
144+ return _default_pyopenssl_ssl_method (ssl_module )
145+
146+ protocol_method_names = (
147+ ('PROTOCOL_TLS_CLIENT' , ('TLS_CLIENT_METHOD' , 'TLS_METHOD' , 'TLSv1_2_METHOD' )),
148+ ('PROTOCOL_TLS' , ('TLS_METHOD' , 'TLS_CLIENT_METHOD' , 'TLSv1_2_METHOD' )),
149+ ('PROTOCOL_SSLv23' , ('TLS_METHOD' , 'TLS_CLIENT_METHOD' , 'TLSv1_2_METHOD' )),
150+ ('PROTOCOL_TLSv1_2' , ('TLSv1_2_METHOD' ,)),
151+ ('PROTOCOL_TLSv1_1' , ('TLSv1_1_METHOD' ,)),
152+ ('PROTOCOL_TLSv1' , ('TLSv1_METHOD' ,)),
153+ )
154+ for protocol_name , method_names in protocol_method_names :
155+ protocol = getattr (ssl , protocol_name , None )
156+ if (protocol is not None and
157+ ssl_version .__class__ is protocol .__class__ and
158+ ssl_version == protocol ):
159+ for method_name in method_names :
160+ method = getattr (ssl_module , method_name , None )
161+ if method is not None :
162+ return method
163+ raise ImportError ('pyOpenSSL does not expose a method for %s' % (protocol_name ,))
164+
165+ return ssl_version
166+
167+
168+ def _pyopenssl_verify_mode_from_cert_reqs (ssl_module , cert_reqs ):
169+ if cert_reqs is None :
170+ return None
171+ if cert_reqs == ssl .CERT_NONE :
172+ return ssl_module .VERIFY_NONE
173+ if cert_reqs in (ssl .CERT_OPTIONAL , ssl .CERT_REQUIRED ):
174+ return ssl_module .VERIFY_PEER
175+ return cert_reqs
176+
177+
178+ def _ensure_pyopenssl_context_requires_verification (ssl_module , context , check_hostname ):
179+ """
180+ Make hostname verification fail closed for a caller-supplied context.
181+
182+ When hostname checking is enabled, this may mutate the context's verify
183+ mode and callback because pyOpenSSL does not expose a non-mutating way to
184+ require peer verification.
185+ """
186+ if not check_hostname :
187+ return
188+
189+ get_verify_mode = getattr (context , 'get_verify_mode' , None )
190+ verify_mode = get_verify_mode () if get_verify_mode is not None else None
191+ if (verify_mode is not None and
192+ verify_mode & ssl_module .VERIFY_PEER ):
193+ return
194+
195+ log .warning (
196+ "check_hostname=True requires peer verification; mutating supplied "
197+ "pyOpenSSL context to use VERIFY_PEER and replacing its verification "
198+ "callback"
199+ )
200+ context .set_verify (
201+ ssl_module .VERIFY_PEER ,
202+ callback = lambda _connection , _x509 , _errnum , _errdepth , ok : ok
203+ )
204+
205+
206+ def _build_pyopenssl_context_from_options (ssl_module , ssl_options ):
207+ ssl_options = ssl_options or {}
208+ context = ssl_module .Context (
209+ _pyopenssl_ssl_method_from_stdlib (ssl_module , ssl_options .get ('ssl_version' , None ))
210+ )
211+ if 'certfile' in ssl_options :
212+ context .use_certificate_file (ssl_options ['certfile' ])
213+ if 'keyfile' in ssl_options :
214+ context .use_privatekey_file (ssl_options ['keyfile' ])
215+ if 'ca_certs' in ssl_options :
216+ context .load_verify_locations (ssl_options ['ca_certs' ])
217+ cert_reqs = _pyopenssl_verify_mode_from_cert_reqs (
218+ ssl_module , ssl_options .get ('cert_reqs' , None ))
219+ if cert_reqs is None :
220+ cert_reqs = (ssl_module .VERIFY_PEER
221+ if ssl_options
222+ else ssl_module .VERIFY_NONE )
223+ elif ssl_options .get ('check_hostname' , False ) and cert_reqs == ssl_module .VERIFY_NONE :
224+ cert_reqs = ssl_module .VERIFY_PEER
225+ context .set_verify (
226+ cert_reqs ,
227+ callback = lambda _connection , _x509 , _errnum , _errdepth , ok : ok
228+ )
229+ if (cert_reqs & ssl_module .VERIFY_PEER and
230+ not ssl_options .get ('ca_certs' )):
231+ set_default_verify_paths = getattr (context , 'set_default_verify_paths' , None )
232+ if set_default_verify_paths is not None :
233+ set_default_verify_paths ()
234+ ciphers = ssl_options .get ('ciphers' , None )
235+ if ciphers :
236+ if isinstance (ciphers , str ):
237+ ciphers = ciphers .encode ('ascii' )
238+ context .set_cipher_list (ciphers )
239+ return context
240+
241+
242+ def _normalized_hostname (hostname ):
243+ return hostname .rstrip ('.' ).lower ()
244+
245+
246+ def _dnsname_match (dn , hostname ):
247+ dn = _normalized_hostname (dn )
248+ hostname = _normalized_hostname (hostname )
249+
250+ if not dn or not hostname :
251+ return False
252+
253+ if '*' not in dn :
254+ return dn == hostname
255+
256+ dn_labels = dn .split ('.' )
257+ hostname_labels = hostname .split ('.' )
258+ if (len (dn_labels ) != len (hostname_labels ) or
259+ len (dn_labels ) < 2 or dn_labels [0 ] != '*' or
260+ not hostname_labels [0 ] or
261+ any ('*' in label for label in dn_labels [1 :])):
262+ return False
263+
264+ return dn_labels [1 :] == hostname_labels [1 :]
265+
266+
267+ def _ipaddress_match (cert_ip , hostname ):
268+ try :
269+ host_ip = ipaddress .ip_address (hostname )
270+ cert_ip = ipaddress .ip_address (cert_ip )
271+ except ValueError :
272+ return False
273+ return cert_ip == host_ip
274+
275+
276+ def _is_ip_address (hostname ):
277+ try :
278+ ipaddress .ip_address (hostname )
279+ except ValueError :
280+ return False
281+ return True
282+
283+
284+ def _pyopenssl_cert_subject_alt_names (cert ):
285+ # Imported lazily because cryptography is an optional driver dependency.
286+ # pyOpenSSL supplies it whenever this code path is used.
287+ from cryptography import x509
288+
289+ try :
290+ san = cert .to_cryptography ().extensions .get_extension_for_class (
291+ x509 .SubjectAlternativeName ).value
292+ except x509 .ExtensionNotFound :
293+ return [], []
294+
295+ return (
296+ san .get_values_for_type (x509 .DNSName ),
297+ [str (ip ) for ip in san .get_values_for_type (x509 .IPAddress )]
298+ )
299+
300+
301+ def _pyopenssl_cert_common_names (cert ):
302+ # Imported lazily for the same reason as the SAN types above.
303+ from cryptography .x509 .oid import NameOID
304+
305+ return [
306+ attribute .value
307+ for attribute in cert .to_cryptography ().subject .get_attributes_for_oid (
308+ NameOID .COMMON_NAME )
309+ ]
310+
311+
312+ def _validate_pyopenssl_hostname (cert , hostname ):
313+ if cert is None :
314+ raise ssl .CertificateError (
315+ "peer did not present a certificate; cannot verify hostname %r" % (hostname ,))
316+
317+ san_dns_names , san_ip_addresses = _pyopenssl_cert_subject_alt_names (cert )
318+ san_names = san_dns_names + san_ip_addresses
319+ hostname_is_ip = _is_ip_address (hostname )
320+
321+ for cert_ip in san_ip_addresses :
322+ if _ipaddress_match (cert_ip , hostname ):
323+ return
324+ if hostname_is_ip and san_names :
325+ raise ssl .CertificateError (
326+ "hostname %r doesn't match certificate subjectAltName %r" %
327+ (hostname , san_names ))
328+
329+ for cert_hostname in san_dns_names :
330+ if _dnsname_match (cert_hostname , hostname ):
331+ return
332+ if san_names :
333+ raise ssl .CertificateError (
334+ "hostname %r doesn't match certificate subjectAltName %r" %
335+ (hostname , san_names ))
336+
337+ common_names = _pyopenssl_cert_common_names (cert )
338+ for common_name in common_names :
339+ if (_ipaddress_match (common_name , hostname ) if hostname_is_ip
340+ else _dnsname_match (common_name , hostname )):
341+ return
342+
343+ raise ssl .CertificateError (
344+ "hostname %r doesn't match certificate commonName %r" %
345+ (hostname , common_names ))
346+
347+
133348class EndPoint (object ):
134349 """
135350 Represents the information to connect to a cassandra node.
@@ -803,6 +1018,7 @@ class Connection(object):
8031018 endpoint = None
8041019 ssl_options = None
8051020 ssl_context = None
1021+ _ssl_options_explicit = False
8061022 last_error = None
8071023
8081024 # The current number of operations that are in flight. More precisely,
@@ -885,7 +1101,11 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
8851101 self .endpoint = host if isinstance (host , EndPoint ) else DefaultEndPoint (host , port )
8861102
8871103 self .authenticator = authenticator
888- self .ssl_options = ssl_options .copy () if ssl_options else {}
1104+ endpoint_ssl_options = self .endpoint .ssl_options
1105+ # Explicit ssl_options={} enables SSL with default options; omitted
1106+ # ssl_options=None leaves SSL disabled unless an endpoint supplies options.
1107+ self ._ssl_options_explicit = ssl_options is not None
1108+ self .ssl_options = ssl_options .copy () if ssl_options is not None else {}
8891109 self .ssl_context = ssl_context
8901110 self .sockopts = sockopts
8911111 self .compression = compression
@@ -905,10 +1125,13 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9051125 self ._on_orphaned_stream_released = on_orphaned_stream_released
9061126 self ._application_info = application_info
9071127
908- if ssl_options :
909- self .ssl_options .update (self .endpoint .ssl_options or {})
910- elif self .endpoint .ssl_options :
911- self .ssl_options = self .endpoint .ssl_options
1128+ if ssl_options is not None :
1129+ self .ssl_options .update (endpoint_ssl_options or {})
1130+ elif endpoint_ssl_options is not None :
1131+ self ._ssl_options_explicit = True
1132+ self .ssl_options = endpoint_ssl_options
1133+ self ._check_hostname = bool (self .ssl_options .get ('check_hostname' , False ) or
1134+ getattr (self .ssl_context , 'check_hostname' , False ))
9121135
9131136 # PYTHON-1331
9141137 #
@@ -918,7 +1141,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9181141 #
9191142 # Note the use of pop() here; we are very deliberately removing these params from ssl_options if they're present. After this
9201143 # operation ssl_options should contain only args needed for the ssl_context.wrap_socket() call.
921- if not self .ssl_context and self .ssl_options :
1144+ if self .ssl_context is None and self ._ssl_options_explicit :
9221145 self .ssl_context = self ._build_ssl_context_from_options ()
9231146
9241147 self .max_request_id = min (self .max_in_flight - 1 , (2 ** 15 ) - 1 )
@@ -942,6 +1165,10 @@ def host(self):
9421165 def port (self ):
9431166 return self .endpoint .port
9441167
1168+ @property
1169+ def _ssl_enabled (self ):
1170+ return self .ssl_context is not None or self ._ssl_options_explicit
1171+
9451172 @classmethod
9461173 def initialize_reactor (cls ):
9471174 """
@@ -1000,10 +1227,18 @@ def _build_ssl_context_from_options(self):
10001227 # Python >= 3.10 requires either PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER so we'll get ahead of things by always
10011228 # being explicit
10021229 ssl_version = opts .get ('ssl_version' , None ) or ssl .PROTOCOL_TLS_CLIENT
1003- cert_reqs = opts .get ('cert_reqs' , None ) or ssl .CERT_REQUIRED
1230+ cert_reqs = opts .get ('cert_reqs' , None )
1231+ check_hostname = bool (opts .get ('check_hostname' , False ))
1232+ if cert_reqs is None :
1233+ cert_reqs = (ssl .CERT_REQUIRED
1234+ if self .ssl_options
1235+ else ssl .CERT_NONE )
1236+ elif check_hostname and cert_reqs == ssl .CERT_NONE :
1237+ cert_reqs = ssl .CERT_REQUIRED
10041238 rv = ssl .SSLContext (protocol = int (ssl_version ))
1005- rv .check_hostname = bool (opts .get ('check_hostname' , False ))
1006- rv .options = int (cert_reqs )
1239+ rv .check_hostname = False
1240+ rv .verify_mode = cert_reqs
1241+ rv .check_hostname = check_hostname
10071242
10081243 certfile = opts .get ('certfile' , None )
10091244 keyfile = opts .get ('keyfile' , None )
@@ -1012,6 +1247,10 @@ def _build_ssl_context_from_options(self):
10121247 ca_certs = opts .get ('ca_certs' , None )
10131248 if ca_certs :
10141249 rv .load_verify_locations (ca_certs )
1250+ elif cert_reqs != ssl .CERT_NONE :
1251+ load_default_certs = getattr (rv , 'load_default_certs' , None )
1252+ if load_default_certs is not None :
1253+ load_default_certs ()
10151254 ciphers = opts .get ('ciphers' , None )
10161255 if ciphers :
10171256 rv .set_ciphers (ciphers )
0 commit comments