1717import errno
1818from functools import wraps , partial , total_ordering
1919from heapq import heappush , heappop
20+ import ipaddress
2021import io
2122import logging
2223import socket
5556
5657log = logging .getLogger (__name__ )
5758
59+
5860segment_codec_no_compression = SegmentCodec ()
5961segment_codec_lz4 = None
6062
@@ -130,6 +132,185 @@ def decompress(byts):
130132frame_header_v3 = struct .Struct ('>BhBi' )
131133
132134
135+ def _default_pyopenssl_ssl_method (ssl_module ):
136+ for method_name in ('TLS_CLIENT_METHOD' , 'TLS_METHOD' , 'TLSv1_2_METHOD' ):
137+ method = getattr (ssl_module , method_name , None )
138+ if method is not None :
139+ return method
140+ raise ImportError ('pyOpenSSL does not expose a secure TLS client method' )
141+
142+
143+ def _pyopenssl_ssl_method_from_stdlib (ssl_module , ssl_version ):
144+ if ssl_version is None :
145+ return _default_pyopenssl_ssl_method (ssl_module )
146+
147+ protocol_method_names = (
148+ ('PROTOCOL_TLS_CLIENT' , ('TLS_CLIENT_METHOD' , 'TLS_METHOD' , 'TLSv1_2_METHOD' )),
149+ ('PROTOCOL_TLS' , ('TLS_METHOD' , 'TLS_CLIENT_METHOD' , 'TLSv1_2_METHOD' )),
150+ ('PROTOCOL_SSLv23' , ('TLS_METHOD' , 'TLS_CLIENT_METHOD' , 'TLSv1_2_METHOD' )),
151+ ('PROTOCOL_TLSv1_2' , ('TLSv1_2_METHOD' ,)),
152+ ('PROTOCOL_TLSv1_1' , ('TLSv1_1_METHOD' ,)),
153+ ('PROTOCOL_TLSv1' , ('TLSv1_METHOD' ,)),
154+ )
155+ for protocol_name , method_names in protocol_method_names :
156+ protocol = getattr (ssl , protocol_name , None )
157+ if (protocol is not None and
158+ ssl_version .__class__ is protocol .__class__ and
159+ ssl_version == protocol ):
160+ for method_name in method_names :
161+ method = getattr (ssl_module , method_name , None )
162+ if method is not None :
163+ return method
164+ raise ImportError ('pyOpenSSL does not expose a method for %s' % (protocol_name ,))
165+
166+ return ssl_version
167+
168+
169+ def _pyopenssl_verify_mode_from_cert_reqs (ssl_module , cert_reqs ):
170+ if cert_reqs is None :
171+ return None
172+ if cert_reqs .__class__ is not type (ssl .CERT_REQUIRED ):
173+ return cert_reqs
174+ if cert_reqs == ssl .CERT_NONE :
175+ return ssl_module .VERIFY_NONE
176+ if cert_reqs in (ssl .CERT_OPTIONAL , ssl .CERT_REQUIRED ):
177+ return ssl_module .VERIFY_PEER
178+ return cert_reqs
179+
180+
181+ def _build_pyopenssl_context_from_options (ssl_module , ssl_options ):
182+ ssl_options = ssl_options or {}
183+ context = ssl_module .Context (
184+ _pyopenssl_ssl_method_from_stdlib (ssl_module , ssl_options .get ('ssl_version' , None ))
185+ )
186+ if 'certfile' in ssl_options :
187+ context .use_certificate_file (ssl_options ['certfile' ])
188+ if 'keyfile' in ssl_options :
189+ context .use_privatekey_file (ssl_options ['keyfile' ])
190+ if 'ca_certs' in ssl_options :
191+ context .load_verify_locations (ssl_options ['ca_certs' ])
192+ cert_reqs = _pyopenssl_verify_mode_from_cert_reqs (
193+ ssl_module , ssl_options .get ('cert_reqs' , None ))
194+ if cert_reqs is None :
195+ cert_reqs = (ssl_module .VERIFY_PEER
196+ if (ssl_options .get ('ca_certs' , None ) or ssl_options .get ('check_hostname' , False ))
197+ else ssl_module .VERIFY_NONE )
198+ elif ssl_options .get ('check_hostname' , False ) and cert_reqs == ssl_module .VERIFY_NONE :
199+ cert_reqs = ssl_module .VERIFY_PEER
200+ context .set_verify (
201+ cert_reqs ,
202+ callback = lambda _connection , _x509 , _errnum , _errdepth , ok : ok
203+ )
204+ ciphers = ssl_options .get ('ciphers' , None )
205+ if ciphers :
206+ if isinstance (ciphers , str ):
207+ ciphers = ciphers .encode ('ascii' )
208+ context .set_cipher_list (ciphers )
209+ return context
210+
211+
212+ def _normalized_hostname (hostname ):
213+ return hostname .rstrip ('.' ).lower ()
214+
215+
216+ def _dnsname_match (dn , hostname ):
217+ dn = _normalized_hostname (dn )
218+ hostname = _normalized_hostname (hostname )
219+
220+ if '*' not in dn :
221+ return dn == hostname
222+
223+ dn_labels = dn .split ('.' )
224+ hostname_labels = hostname .split ('.' )
225+ if (len (dn_labels ) != len (hostname_labels ) or
226+ not dn_labels or dn_labels [0 ] != '*' or
227+ any ('*' in label for label in dn_labels [1 :])):
228+ return False
229+
230+ return dn_labels [1 :] == hostname_labels [1 :]
231+
232+
233+ def _ipaddress_match (cert_ip , hostname ):
234+ try :
235+ host_ip = ipaddress .ip_address (hostname )
236+ cert_ip = ipaddress .ip_address (cert_ip )
237+ except ValueError :
238+ return False
239+ return cert_ip == host_ip
240+
241+
242+ def _is_ip_address (hostname ):
243+ try :
244+ ipaddress .ip_address (hostname )
245+ except ValueError :
246+ return False
247+ return True
248+
249+
250+ def _decode_x509_name (value ):
251+ if isinstance (value , bytes ):
252+ return value .decode ('utf-8' )
253+ return value
254+
255+
256+ def _pyopenssl_cert_subject_alt_names (cert ):
257+ dns_names = []
258+ ip_addresses = []
259+
260+ for i in range (cert .get_extension_count ()):
261+ extension = cert .get_extension (i )
262+ if extension .get_short_name () != b'subjectAltName' :
263+ continue
264+ for item in str (extension ).split (',' ):
265+ item = item .strip ()
266+ if item .startswith ('DNS:' ):
267+ dns_names .append (item [4 :])
268+ elif item .startswith ('IP Address:' ):
269+ ip_addresses .append (item [11 :])
270+
271+ return dns_names , ip_addresses
272+
273+
274+ def _pyopenssl_cert_common_names (cert ):
275+ return [
276+ _decode_x509_name (value )
277+ for key , value in cert .get_subject ().get_components ()
278+ if key == b'CN'
279+ ]
280+
281+
282+ def _validate_pyopenssl_hostname (cert , hostname ):
283+ san_dns_names , san_ip_addresses = _pyopenssl_cert_subject_alt_names (cert )
284+ san_names = san_dns_names + san_ip_addresses
285+ hostname_is_ip = _is_ip_address (hostname )
286+
287+ for cert_ip in san_ip_addresses :
288+ if _ipaddress_match (cert_ip , hostname ):
289+ return
290+ if hostname_is_ip and san_names :
291+ raise ssl .CertificateError (
292+ "hostname %r doesn't match certificate subjectAltName %r" %
293+ (hostname , san_names ))
294+
295+ for cert_hostname in san_dns_names :
296+ if _dnsname_match (cert_hostname , hostname ):
297+ return
298+ if san_names :
299+ raise ssl .CertificateError (
300+ "hostname %r doesn't match certificate subjectAltName %r" %
301+ (hostname , san_names ))
302+
303+ common_names = _pyopenssl_cert_common_names (cert )
304+ for common_name in common_names :
305+ if (_ipaddress_match (common_name , hostname ) if hostname_is_ip
306+ else _dnsname_match (common_name , hostname )):
307+ return
308+
309+ raise ssl .CertificateError (
310+ "hostname %r doesn't match certificate commonName %r" %
311+ (hostname , common_names ))
312+
313+
133314class EndPoint (object ):
134315 """
135316 Represents the information to connect to a cassandra node.
@@ -803,6 +984,7 @@ class Connection(object):
803984 endpoint = None
804985 ssl_options = None
805986 ssl_context = None
987+ _ssl_options_explicit = False
806988 last_error = None
807989
808990 # The current number of operations that are in flight. More precisely,
@@ -885,7 +1067,11 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
8851067 self .endpoint = host if isinstance (host , EndPoint ) else DefaultEndPoint (host , port )
8861068
8871069 self .authenticator = authenticator
888- self .ssl_options = ssl_options .copy () if ssl_options else {}
1070+ endpoint_ssl_options = self .endpoint .ssl_options
1071+ # Explicit ssl_options={} enables SSL with default options; omitted
1072+ # ssl_options=None leaves SSL disabled unless an endpoint supplies options.
1073+ self ._ssl_options_explicit = ssl_options is not None
1074+ self .ssl_options = ssl_options .copy () if ssl_options is not None else {}
8891075 self .ssl_context = ssl_context
8901076 self .sockopts = sockopts
8911077 self .compression = compression
@@ -905,10 +1091,13 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9051091 self ._on_orphaned_stream_released = on_orphaned_stream_released
9061092 self ._application_info = application_info
9071093
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
1094+ if ssl_options is not None :
1095+ self .ssl_options .update (endpoint_ssl_options or {})
1096+ elif endpoint_ssl_options is not None :
1097+ self ._ssl_options_explicit = True
1098+ self .ssl_options = endpoint_ssl_options
1099+ self ._check_hostname = bool (self .ssl_options .get ('check_hostname' , False ) or
1100+ getattr (self .ssl_context , 'check_hostname' , False ))
9121101
9131102 # PYTHON-1331
9141103 #
@@ -918,7 +1107,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9181107 #
9191108 # Note the use of pop() here; we are very deliberately removing these params from ssl_options if they're present. After this
9201109 # 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 :
1110+ if self .ssl_context is None and self ._ssl_options_explicit :
9221111 self .ssl_context = self ._build_ssl_context_from_options ()
9231112
9241113 self .max_request_id = min (self .max_in_flight - 1 , (2 ** 15 ) - 1 )
@@ -942,6 +1131,10 @@ def host(self):
9421131 def port (self ):
9431132 return self .endpoint .port
9441133
1134+ @property
1135+ def _ssl_enabled (self ):
1136+ return self .ssl_context is not None or self ._ssl_options_explicit
1137+
9451138 @classmethod
9461139 def initialize_reactor (cls ):
9471140 """
@@ -1000,10 +1193,15 @@ def _build_ssl_context_from_options(self):
10001193 # Python >= 3.10 requires either PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER so we'll get ahead of things by always
10011194 # being explicit
10021195 ssl_version = opts .get ('ssl_version' , None ) or ssl .PROTOCOL_TLS_CLIENT
1003- cert_reqs = opts .get ('cert_reqs' , None ) or ssl .CERT_REQUIRED
1196+ cert_reqs = opts .get ('cert_reqs' , None )
1197+ if cert_reqs is None :
1198+ cert_reqs = (ssl .CERT_REQUIRED
1199+ if (opts .get ('ca_certs' , None ) or opts .get ('check_hostname' , False ))
1200+ else ssl .CERT_NONE )
10041201 rv = ssl .SSLContext (protocol = int (ssl_version ))
1202+ rv .check_hostname = False
1203+ rv .verify_mode = cert_reqs
10051204 rv .check_hostname = bool (opts .get ('check_hostname' , False ))
1006- rv .options = int (cert_reqs )
10071205
10081206 certfile = opts .get ('certfile' , None )
10091207 keyfile = opts .get ('keyfile' , None )
0 commit comments