1from __future__ import absolute_import
2
3import hashlib
4import hmac
5import os
6import sys
7import warnings
8from binascii import hexlify, unhexlify
9
10from ..exceptions import (
11 InsecurePlatformWarning,
12 ProxySchemeUnsupported,
13 SNIMissingWarning,
14 SSLError,
15)
16from ..packages import six
17from .url import BRACELESS_IPV6_ADDRZ_RE, IPV4_RE
18
19SSLContext = None
20SSLTransport = None
21HAS_SNI = False
22IS_PYOPENSSL = False
23IS_SECURETRANSPORT = False
24ALPN_PROTOCOLS = ["http/1.1"]
25
26# Maps the length of a digest to a possible hash function producing this digest
27HASHFUNC_MAP = {
28 length: getattr(hashlib, algorithm, None)
29 for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256"))
30}
31
32
33def _const_compare_digest_backport(a, b):
34 """
35 Compare two digests of equal length in constant time.
36
37 The digests must be of type str/bytes.
38 Returns True if the digests match, and False otherwise.
39 """
40 result = abs(len(a) - len(b))
41 for left, right in zip(bytearray(a), bytearray(b)):
42 result |= left ^ right
43 return result == 0
44
45
46_const_compare_digest = getattr(hmac, "compare_digest", _const_compare_digest_backport)
47
48try: # Test for SSL features
49 import ssl
50 from ssl import CERT_REQUIRED, wrap_socket
51except ImportError:
52 pass
53
54try:
55 from ssl import HAS_SNI # Has SNI?
56except ImportError:
57 pass
58
59try:
60 from .ssltransport import SSLTransport
61except ImportError:
62 pass
63
64
65try: # Platform-specific: Python 3.6
66 from ssl import PROTOCOL_TLS
67
68 PROTOCOL_SSLv23 = PROTOCOL_TLS
69except ImportError:
70 try:
71 from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS
72
73 PROTOCOL_SSLv23 = PROTOCOL_TLS
74 except ImportError:
75 PROTOCOL_SSLv23 = PROTOCOL_TLS = 2
76
77try:
78 from ssl import PROTOCOL_TLS_CLIENT
79except ImportError:
80 PROTOCOL_TLS_CLIENT = PROTOCOL_TLS
81
82
83try:
84 from ssl import OP_NO_COMPRESSION, OP_NO_SSLv2, OP_NO_SSLv3
85except ImportError:
86 OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000
87 OP_NO_COMPRESSION = 0x20000
88
89
90try: # OP_NO_TICKET was added in Python 3.6
91 from ssl import OP_NO_TICKET
92except ImportError:
93 OP_NO_TICKET = 0x4000
94
95
96# A secure default.
97# Sources for more information on TLS ciphers:
98#
99# - https://wiki.mozilla.org/Security/Server_Side_TLS
100# - https://www.ssllabs.com/projects/best-practices/index.html
101# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
102#
103# The general intent is:
104# - prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
105# - prefer ECDHE over DHE for better performance,
106# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and
107# security,
108# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,
109# - disable NULL authentication, MD5 MACs, DSS, and other
110# insecure ciphers for security reasons.
111# - NOTE: TLS 1.3 cipher suites are managed through a different interface
112# not exposed by CPython (yet!) and are enabled by default if they're available.
113DEFAULT_CIPHERS = ":".join(
114 [
115 "ECDHE+AESGCM",
116 "ECDHE+CHACHA20",
117 "DHE+AESGCM",
118 "DHE+CHACHA20",
119 "ECDH+AESGCM",
120 "DH+AESGCM",
121 "ECDH+AES",
122 "DH+AES",
123 "RSA+AESGCM",
124 "RSA+AES",
125 "!aNULL",
126 "!eNULL",
127 "!MD5",
128 "!DSS",
129 ]
130)
131
132try:
133 from ssl import SSLContext # Modern SSL?
134except ImportError:
135
136 class SSLContext(object): # Platform-specific: Python 2
137 def __init__(self, protocol_version):
138 self.protocol = protocol_version
139 # Use default values from a real SSLContext
140 self.check_hostname = False
141 self.verify_mode = ssl.CERT_NONE
142 self.ca_certs = None
143 self.options = 0
144 self.certfile = None
145 self.keyfile = None
146 self.ciphers = None
147
148 def load_cert_chain(self, certfile, keyfile):
149 self.certfile = certfile
150 self.keyfile = keyfile
151
152 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
153 self.ca_certs = cafile
154
155 if capath is not None:
156 raise SSLError("CA directories not supported in older Pythons")
157
158 if cadata is not None:
159 raise SSLError("CA data not supported in older Pythons")
160
161 def set_ciphers(self, cipher_suite):
162 self.ciphers = cipher_suite
163
164 def wrap_socket(self, socket, server_hostname=None, server_side=False):
165 warnings.warn(
166 "A true SSLContext object is not available. This prevents "
167 "urllib3 from configuring SSL appropriately and may cause "
168 "certain SSL connections to fail. You can upgrade to a newer "
169 "version of Python to solve this. For more information, see "
170 "https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html"
171 "#ssl-warnings",
172 InsecurePlatformWarning,
173 )
174 kwargs = {
175 "keyfile": self.keyfile,
176 "certfile": self.certfile,
177 "ca_certs": self.ca_certs,
178 "cert_reqs": self.verify_mode,
179 "ssl_version": self.protocol,
180 "server_side": server_side,
181 }
182 return wrap_socket(socket, ciphers=self.ciphers, **kwargs)
183
184
185def assert_fingerprint(cert, fingerprint):
186 """
187 Checks if given fingerprint matches the supplied certificate.
188
189 :param cert:
190 Certificate as bytes object.
191 :param fingerprint:
192 Fingerprint as string of hexdigits, can be interspersed by colons.
193 """
194
195 fingerprint = fingerprint.replace(":", "").lower()
196 digest_length = len(fingerprint)
197 if digest_length not in HASHFUNC_MAP:
198 raise SSLError("Fingerprint of invalid length: {0}".format(fingerprint))
199 hashfunc = HASHFUNC_MAP.get(digest_length)
200 if hashfunc is None:
201 raise SSLError(
202 "Hash function implementation unavailable for fingerprint length: {0}".format(
203 digest_length
204 )
205 )
206
207 # We need encode() here for py32; works on py2 and p33.
208 fingerprint_bytes = unhexlify(fingerprint.encode())
209
210 cert_digest = hashfunc(cert).digest()
211
212 if not _const_compare_digest(cert_digest, fingerprint_bytes):
213 raise SSLError(
214 'Fingerprints did not match. Expected "{0}", got "{1}".'.format(
215 fingerprint, hexlify(cert_digest)
216 )
217 )
218
219
220def resolve_cert_reqs(candidate):
221 """
222 Resolves the argument to a numeric constant, which can be passed to
223 the wrap_socket function/method from the ssl module.
224 Defaults to :data:`ssl.CERT_REQUIRED`.
225 If given a string it is assumed to be the name of the constant in the
226 :mod:`ssl` module or its abbreviation.
227 (So you can specify `REQUIRED` instead of `CERT_REQUIRED`.
228 If it's neither `None` nor a string we assume it is already the numeric
229 constant which can directly be passed to wrap_socket.
230 """
231 if candidate is None:
232 return CERT_REQUIRED
233
234 if isinstance(candidate, str):
235 res = getattr(ssl, candidate, None)
236 if res is None:
237 res = getattr(ssl, "CERT_" + candidate)
238 return res
239
240 return candidate
241
242
243def resolve_ssl_version(candidate):
244 """
245 like resolve_cert_reqs
246 """
247 if candidate is None:
248 return PROTOCOL_TLS
249
250 if isinstance(candidate, str):
251 res = getattr(ssl, candidate, None)
252 if res is None:
253 res = getattr(ssl, "PROTOCOL_" + candidate)
254 return res
255
256 return candidate
257
258
259def create_urllib3_context(
260 ssl_version=None, cert_reqs=None, options=None, ciphers=None
261):
262 """All arguments have the same meaning as ``ssl_wrap_socket``.
263
264 By default, this function does a lot of the same work that
265 ``ssl.create_default_context`` does on Python 3.4+. It:
266
267 - Disables SSLv2, SSLv3, and compression
268 - Sets a restricted set of server ciphers
269
270 If you wish to enable SSLv3, you can do::
271
272 from urllib3.util import ssl_
273 context = ssl_.create_urllib3_context()
274 context.options &= ~ssl_.OP_NO_SSLv3
275
276 You can do the same to enable compression (substituting ``COMPRESSION``
277 for ``SSLv3`` in the last line above).
278
279 :param ssl_version:
280 The desired protocol version to use. This will default to
281 PROTOCOL_SSLv23 which will negotiate the highest protocol that both
282 the server and your installation of OpenSSL support.
283 :param cert_reqs:
284 Whether to require the certificate verification. This defaults to
285 ``ssl.CERT_REQUIRED``.
286 :param options:
287 Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``,
288 ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``.
289 :param ciphers:
290 Which cipher suites to allow the server to select.
291 :returns:
292 Constructed SSLContext object with specified options
293 :rtype: SSLContext
294 """
295 # PROTOCOL_TLS is deprecated in Python 3.10
296 if not ssl_version or ssl_version == PROTOCOL_TLS:
297 ssl_version = PROTOCOL_TLS_CLIENT
298
299 context = SSLContext(ssl_version)
300
301 context.set_ciphers(ciphers or DEFAULT_CIPHERS)
302
303 # Setting the default here, as we may have no ssl module on import
304 cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs
305
306 if options is None:
307 options = 0
308 # SSLv2 is easily broken and is considered harmful and dangerous
309 options |= OP_NO_SSLv2
310 # SSLv3 has several problems and is now dangerous
311 options |= OP_NO_SSLv3
312 # Disable compression to prevent CRIME attacks for OpenSSL 1.0+
313 # (issue #309)
314 options |= OP_NO_COMPRESSION
315 # TLSv1.2 only. Unless set explicitly, do not request tickets.
316 # This may save some bandwidth on wire, and although the ticket is encrypted,
317 # there is a risk associated with it being on wire,
318 # if the server is not rotating its ticketing keys properly.
319 options |= OP_NO_TICKET
320
321 context.options |= options
322
323 # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is
324 # necessary for conditional client cert authentication with TLS 1.3.
325 # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older
326 # versions of Python. We only enable on Python 3.7.4+ or if certificate
327 # verification is enabled to work around Python issue #37428
328 # See: https://bugs.python.org/issue37428
329 if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr(
330 context, "post_handshake_auth", None
331 ) is not None:
332 context.post_handshake_auth = True
333
334 def disable_check_hostname():
335 if (
336 getattr(context, "check_hostname", None) is not None
337 ): # Platform-specific: Python 3.2
338 # We do our own verification, including fingerprints and alternative
339 # hostnames. So disable it here
340 context.check_hostname = False
341
342 # The order of the below lines setting verify_mode and check_hostname
343 # matter due to safe-guards SSLContext has to prevent an SSLContext with
344 # check_hostname=True, verify_mode=NONE/OPTIONAL. This is made even more
345 # complex because we don't know whether PROTOCOL_TLS_CLIENT will be used
346 # or not so we don't know the initial state of the freshly created SSLContext.
347 if cert_reqs == ssl.CERT_REQUIRED:
348 context.verify_mode = cert_reqs
349 disable_check_hostname()
350 else:
351 disable_check_hostname()
352 context.verify_mode = cert_reqs
353
354 # Enable logging of TLS session keys via defacto standard environment variable
355 # 'SSLKEYLOGFILE', if the feature is available (Python 3.8+). Skip empty values.
356 if hasattr(context, "keylog_filename"):
357 sslkeylogfile = os.environ.get("SSLKEYLOGFILE")
358 if sslkeylogfile:
359 context.keylog_filename = sslkeylogfile
360
361 return context
362
363
364def ssl_wrap_socket(
365 sock,
366 keyfile=None,
367 certfile=None,
368 cert_reqs=None,
369 ca_certs=None,
370 server_hostname=None,
371 ssl_version=None,
372 ciphers=None,
373 ssl_context=None,
374 ca_cert_dir=None,
375 key_password=None,
376 ca_cert_data=None,
377 tls_in_tls=False,
378):
379 """
380 All arguments except for server_hostname, ssl_context, and ca_cert_dir have
381 the same meaning as they do when using :func:`ssl.wrap_socket`.
382
383 :param server_hostname:
384 When SNI is supported, the expected hostname of the certificate
385 :param ssl_context:
386 A pre-made :class:`SSLContext` object. If none is provided, one will
387 be created using :func:`create_urllib3_context`.
388 :param ciphers:
389 A string of ciphers we wish the client to support.
390 :param ca_cert_dir:
391 A directory containing CA certificates in multiple separate files, as
392 supported by OpenSSL's -CApath flag or the capath argument to
393 SSLContext.load_verify_locations().
394 :param key_password:
395 Optional password if the keyfile is encrypted.
396 :param ca_cert_data:
397 Optional string containing CA certificates in PEM format suitable for
398 passing as the cadata parameter to SSLContext.load_verify_locations()
399 :param tls_in_tls:
400 Use SSLTransport to wrap the existing socket.
401 """
402 context = ssl_context
403 if context is None:
404 # Note: This branch of code and all the variables in it are no longer
405 # used by urllib3 itself. We should consider deprecating and removing
406 # this code.
407 context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers)
408
409 if ca_certs or ca_cert_dir or ca_cert_data:
410 try:
411 context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data)
412 except (IOError, OSError) as e:
413 raise SSLError(e)
414
415 elif ssl_context is None and hasattr(context, "load_default_certs"):
416 # try to load OS default certs; works well on Windows (require Python3.4+)
417 context.load_default_certs()
418
419 # Attempt to detect if we get the goofy behavior of the
420 # keyfile being encrypted and OpenSSL asking for the
421 # passphrase via the terminal and instead error out.
422 if keyfile and key_password is None and _is_key_file_encrypted(keyfile):
423 raise SSLError("Client private key is encrypted, password is required")
424
425 if certfile:
426 if key_password is None:
427 context.load_cert_chain(certfile, keyfile)
428 else:
429 context.load_cert_chain(certfile, keyfile, key_password)
430
431 try:
432 if hasattr(context, "set_alpn_protocols"):
433 context.set_alpn_protocols(ALPN_PROTOCOLS)
434 except NotImplementedError: # Defensive: in CI, we always have set_alpn_protocols
435 pass
436
437 # If we detect server_hostname is an IP address then the SNI
438 # extension should not be used according to RFC3546 Section 3.1
439 use_sni_hostname = server_hostname and not is_ipaddress(server_hostname)
440 # SecureTransport uses server_hostname in certificate verification.
441 send_sni = (use_sni_hostname and HAS_SNI) or (
442 IS_SECURETRANSPORT and server_hostname
443 )
444 # Do not warn the user if server_hostname is an invalid SNI hostname.
445 if not HAS_SNI and use_sni_hostname:
446 warnings.warn(
447 "An HTTPS request has been made, but the SNI (Server Name "
448 "Indication) extension to TLS is not available on this platform. "
449 "This may cause the server to present an incorrect TLS "
450 "certificate, which can cause validation failures. You can upgrade to "
451 "a newer version of Python to solve this. For more information, see "
452 "https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html"
453 "#ssl-warnings",
454 SNIMissingWarning,
455 )
456
457 if send_sni:
458 ssl_sock = _ssl_wrap_socket_impl(
459 sock, context, tls_in_tls, server_hostname=server_hostname
460 )
461 else:
462 ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls)
463 return ssl_sock
464
465
466def is_ipaddress(hostname):
467 """Detects whether the hostname given is an IPv4 or IPv6 address.
468 Also detects IPv6 addresses with Zone IDs.
469
470 :param str hostname: Hostname to examine.
471 :return: True if the hostname is an IP address, False otherwise.
472 """
473 if not six.PY2 and isinstance(hostname, bytes):
474 # IDN A-label bytes are ASCII compatible.
475 hostname = hostname.decode("ascii")
476 return bool(IPV4_RE.match(hostname) or BRACELESS_IPV6_ADDRZ_RE.match(hostname))
477
478
479def _is_key_file_encrypted(key_file):
480 """Detects if a key file is encrypted or not."""
481 with open(key_file, "r") as f:
482 for line in f:
483 # Look for Proc-Type: 4,ENCRYPTED
484 if "ENCRYPTED" in line:
485 return True
486
487 return False
488
489
490def _ssl_wrap_socket_impl(sock, ssl_context, tls_in_tls, server_hostname=None):
491 if tls_in_tls:
492 if not SSLTransport:
493 # Import error, ssl is not available.
494 raise ProxySchemeUnsupported(
495 "TLS in TLS requires support for the 'ssl' module"
496 )
497
498 SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context)
499 return SSLTransport(sock, ssl_context, server_hostname)
500
501 if server_hostname:
502 return ssl_context.wrap_socket(sock, server_hostname=server_hostname)
503 else:
504 return ssl_context.wrap_socket(sock)