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,201 @@ 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+ get_verify_mode = getattr (context , 'get_verify_mode' , None )
180+ if (check_hostname and get_verify_mode is not None and
181+ get_verify_mode () == ssl_module .VERIFY_NONE ):
182+ context .set_verify (
183+ ssl_module .VERIFY_PEER ,
184+ callback = lambda _connection , _x509 , _errnum , _errdepth , ok : ok
185+ )
186+
187+
188+ def _build_pyopenssl_context_from_options (ssl_module , ssl_options ):
189+ ssl_options = ssl_options or {}
190+ context = ssl_module .Context (
191+ _pyopenssl_ssl_method_from_stdlib (ssl_module , ssl_options .get ('ssl_version' , None ))
192+ )
193+ if 'certfile' in ssl_options :
194+ context .use_certificate_file (ssl_options ['certfile' ])
195+ if 'keyfile' in ssl_options :
196+ context .use_privatekey_file (ssl_options ['keyfile' ])
197+ if 'ca_certs' in ssl_options :
198+ context .load_verify_locations (ssl_options ['ca_certs' ])
199+ cert_reqs = _pyopenssl_verify_mode_from_cert_reqs (
200+ ssl_module , ssl_options .get ('cert_reqs' , None ))
201+ if cert_reqs is None :
202+ cert_reqs = (ssl_module .VERIFY_PEER
203+ if ssl_options
204+ else ssl_module .VERIFY_NONE )
205+ elif ssl_options .get ('check_hostname' , False ) and cert_reqs == ssl_module .VERIFY_NONE :
206+ cert_reqs = ssl_module .VERIFY_PEER
207+ context .set_verify (
208+ cert_reqs ,
209+ callback = lambda _connection , _x509 , _errnum , _errdepth , ok : ok
210+ )
211+ ciphers = ssl_options .get ('ciphers' , None )
212+ if ciphers :
213+ if isinstance (ciphers , str ):
214+ ciphers = ciphers .encode ('ascii' )
215+ context .set_cipher_list (ciphers )
216+ return context
217+
218+
219+ def _normalized_hostname (hostname ):
220+ return hostname .rstrip ('.' ).lower ()
221+
222+
223+ def _dnsname_match (dn , hostname ):
224+ dn = _normalized_hostname (dn )
225+ hostname = _normalized_hostname (hostname )
226+
227+ if not dn or not hostname :
228+ return False
229+
230+ if '*' not in dn :
231+ return dn == hostname
232+
233+ dn_labels = dn .split ('.' )
234+ hostname_labels = hostname .split ('.' )
235+ if (len (dn_labels ) != len (hostname_labels ) or
236+ len (dn_labels ) < 2 or dn_labels [0 ] != '*' or
237+ not hostname_labels [0 ] or
238+ any ('*' in label for label in dn_labels [1 :])):
239+ return False
240+
241+ return dn_labels [1 :] == hostname_labels [1 :]
242+
243+
244+ def _ipaddress_match (cert_ip , hostname ):
245+ try :
246+ host_ip = ipaddress .ip_address (hostname )
247+ cert_ip = ipaddress .ip_address (cert_ip )
248+ except ValueError :
249+ return False
250+ return cert_ip == host_ip
251+
252+
253+ def _is_ip_address (hostname ):
254+ try :
255+ ipaddress .ip_address (hostname )
256+ except ValueError :
257+ return False
258+ return True
259+
260+
261+ def _decode_x509_name (value ):
262+ if isinstance (value , bytes ):
263+ return value .decode ('utf-8' )
264+ return value
265+
266+
267+ def _pyopenssl_cert_subject_alt_names (cert ):
268+ dns_names = []
269+ ip_addresses = []
270+
271+ for i in range (cert .get_extension_count ()):
272+ extension = cert .get_extension (i )
273+ if extension .get_short_name () != b'subjectAltName' :
274+ continue
275+ for item in str (extension ).split (',' ):
276+ item = item .strip ()
277+ if item .startswith ('DNS:' ):
278+ dns_names .append (item [4 :])
279+ elif item .startswith ('IP Address:' ):
280+ ip_addresses .append (item [11 :])
281+
282+ return dns_names , ip_addresses
283+
284+
285+ def _pyopenssl_cert_common_names (cert ):
286+ return [
287+ _decode_x509_name (value )
288+ for key , value in cert .get_subject ().get_components ()
289+ if key == b'CN'
290+ ]
291+
292+
293+ def _validate_pyopenssl_hostname (cert , hostname ):
294+ if cert is None :
295+ raise ssl .CertificateError (
296+ "peer did not present a certificate; cannot verify hostname %r" % (hostname ,))
297+
298+ san_dns_names , san_ip_addresses = _pyopenssl_cert_subject_alt_names (cert )
299+ san_names = san_dns_names + san_ip_addresses
300+ hostname_is_ip = _is_ip_address (hostname )
301+
302+ for cert_ip in san_ip_addresses :
303+ if _ipaddress_match (cert_ip , hostname ):
304+ return
305+ if hostname_is_ip and san_names :
306+ raise ssl .CertificateError (
307+ "hostname %r doesn't match certificate subjectAltName %r" %
308+ (hostname , san_names ))
309+
310+ for cert_hostname in san_dns_names :
311+ if _dnsname_match (cert_hostname , hostname ):
312+ return
313+ if san_names :
314+ raise ssl .CertificateError (
315+ "hostname %r doesn't match certificate subjectAltName %r" %
316+ (hostname , san_names ))
317+
318+ common_names = _pyopenssl_cert_common_names (cert )
319+ for common_name in common_names :
320+ if (_ipaddress_match (common_name , hostname ) if hostname_is_ip
321+ else _dnsname_match (common_name , hostname )):
322+ return
323+
324+ raise ssl .CertificateError (
325+ "hostname %r doesn't match certificate commonName %r" %
326+ (hostname , common_names ))
327+
328+
133329class EndPoint (object ):
134330 """
135331 Represents the information to connect to a cassandra node.
@@ -803,6 +999,7 @@ class Connection(object):
803999 endpoint = None
8041000 ssl_options = None
8051001 ssl_context = None
1002+ _ssl_options_explicit = False
8061003 last_error = None
8071004
8081005 # The current number of operations that are in flight. More precisely,
@@ -885,7 +1082,11 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
8851082 self .endpoint = host if isinstance (host , EndPoint ) else DefaultEndPoint (host , port )
8861083
8871084 self .authenticator = authenticator
888- self .ssl_options = ssl_options .copy () if ssl_options else {}
1085+ endpoint_ssl_options = self .endpoint .ssl_options
1086+ # Explicit ssl_options={} enables SSL with default options; omitted
1087+ # ssl_options=None leaves SSL disabled unless an endpoint supplies options.
1088+ self ._ssl_options_explicit = ssl_options is not None
1089+ self .ssl_options = ssl_options .copy () if ssl_options is not None else {}
8891090 self .ssl_context = ssl_context
8901091 self .sockopts = sockopts
8911092 self .compression = compression
@@ -905,10 +1106,13 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9051106 self ._on_orphaned_stream_released = on_orphaned_stream_released
9061107 self ._application_info = application_info
9071108
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
1109+ if ssl_options is not None :
1110+ self .ssl_options .update (endpoint_ssl_options or {})
1111+ elif endpoint_ssl_options is not None :
1112+ self ._ssl_options_explicit = True
1113+ self .ssl_options = endpoint_ssl_options
1114+ self ._check_hostname = bool (self .ssl_options .get ('check_hostname' , False ) or
1115+ getattr (self .ssl_context , 'check_hostname' , False ))
9121116
9131117 # PYTHON-1331
9141118 #
@@ -918,7 +1122,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9181122 #
9191123 # Note the use of pop() here; we are very deliberately removing these params from ssl_options if they're present. After this
9201124 # 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 :
1125+ if self .ssl_context is None and self ._ssl_options_explicit :
9221126 self .ssl_context = self ._build_ssl_context_from_options ()
9231127
9241128 self .max_request_id = min (self .max_in_flight - 1 , (2 ** 15 ) - 1 )
@@ -942,6 +1146,10 @@ def host(self):
9421146 def port (self ):
9431147 return self .endpoint .port
9441148
1149+ @property
1150+ def _ssl_enabled (self ):
1151+ return self .ssl_context is not None or self ._ssl_options_explicit
1152+
9451153 @classmethod
9461154 def initialize_reactor (cls ):
9471155 """
@@ -1000,10 +1208,18 @@ def _build_ssl_context_from_options(self):
10001208 # Python >= 3.10 requires either PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER so we'll get ahead of things by always
10011209 # being explicit
10021210 ssl_version = opts .get ('ssl_version' , None ) or ssl .PROTOCOL_TLS_CLIENT
1003- cert_reqs = opts .get ('cert_reqs' , None ) or ssl .CERT_REQUIRED
1211+ cert_reqs = opts .get ('cert_reqs' , None )
1212+ check_hostname = bool (opts .get ('check_hostname' , False ))
1213+ if cert_reqs is None :
1214+ cert_reqs = (ssl .CERT_REQUIRED
1215+ if self .ssl_options
1216+ else ssl .CERT_NONE )
1217+ elif check_hostname and cert_reqs == ssl .CERT_NONE :
1218+ cert_reqs = ssl .CERT_REQUIRED
10041219 rv = ssl .SSLContext (protocol = int (ssl_version ))
1005- rv .check_hostname = bool (opts .get ('check_hostname' , False ))
1006- rv .options = int (cert_reqs )
1220+ rv .check_hostname = False
1221+ rv .verify_mode = cert_reqs
1222+ rv .check_hostname = check_hostname
10071223
10081224 certfile = opts .get ('certfile' , None )
10091225 keyfile = opts .get ('keyfile' , None )
0 commit comments