1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00
pip/src/pip/_vendor/urllib3/contrib/pyopenssl.py

510 lines
16 KiB
Python
Raw Normal View History

"""
2020-11-19 15:20:52 +01:00
TLS with SNI_-support for Python 2. Follow these instructions if you would
like to verify TLS certificates in Python 2. Note, the default libraries do
2014-05-16 20:08:09 +02:00
*not* do certificate checking; you need to do additional work to validate
certificates yourself.
2013-08-15 15:33:47 +02:00
This needs the following packages installed:
2020-11-19 15:20:52 +01:00
* `pyOpenSSL`_ (tested with 16.0.0)
* `cryptography`_ (minimum 1.3.4, from pyopenssl)
* `idna`_ (minimum 2.0, from cryptography)
However, pyopenssl depends on cryptography, which depends on idna, so while we
use all three directly here we end up having relatively few packages required.
2013-08-15 15:33:47 +02:00
2014-05-16 20:08:09 +02:00
You can install them with the following command:
2020-11-19 15:20:52 +01:00
.. code-block:: bash
$ python -m pip install pyopenssl cryptography idna
2014-05-16 20:08:09 +02:00
To activate certificate checking, call
:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code
before you begin making HTTP requests. This can be done in a ``sitecustomize``
module, or at any other time before your application begins using ``urllib3``,
2020-11-19 15:20:52 +01:00
like this:
.. code-block:: python
2013-08-15 15:33:47 +02:00
try:
import urllib3.contrib.pyopenssl
urllib3.contrib.pyopenssl.inject_into_urllib3()
except ImportError:
pass
Now you can use :mod:`urllib3` as you normally would, and it will support SNI
when the required modules are installed.
Activating this module also has the positive side effect of disabling SSL/TLS
2014-12-02 00:48:38 +01:00
compression in Python 2 (see `CRIME attack`_).
.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication
.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)
2020-11-19 15:20:52 +01:00
.. _pyopenssl: https://www.pyopenssl.org
.. _cryptography: https://cryptography.io
.. _idna: https://github.com/kjd/idna
"""
2016-01-19 00:28:54 +01:00
from __future__ import absolute_import
2013-08-15 15:33:47 +02:00
import OpenSSL.SSL
from cryptography import x509
from cryptography.hazmat.backends.openssl import backend as openssl_backend
from cryptography.hazmat.backends.openssl.x509 import _Certificate
2019-10-09 16:16:37 +02:00
2018-06-21 15:08:05 +02:00
try:
from cryptography.x509 import UnsupportedExtension
except ImportError:
# UnsupportedExtension is gone in cryptography >= 2.1.0
class UnsupportedExtension(Exception):
pass
2019-10-09 16:16:37 +02:00
from io import BytesIO
2020-11-19 15:20:52 +01:00
from socket import error as SocketError
from socket import timeout
try: # Platform-specific: Python 2
from socket import _fileobject
except ImportError: # Platform-specific: Python 3
_fileobject = None
from ..packages.backports.makefile import backport_makefile
import logging
2013-08-15 15:33:47 +02:00
import ssl
import sys
2013-08-15 15:33:47 +02:00
from .. import util
2020-11-19 15:20:52 +01:00
from ..packages import six
2019-07-20 05:59:46 +02:00
2019-10-09 16:16:37 +02:00
__all__ = ["inject_into_urllib3", "extract_from_urllib3"]
2013-08-15 15:33:47 +02:00
# SNI always works.
HAS_SNI = True
2013-08-15 15:33:47 +02:00
# Map from urllib3 to PyOpenSSL compatible parameter-values.
_openssl_versions = {
2019-07-20 05:59:46 +02:00
util.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD,
2013-08-15 15:33:47 +02:00
ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
}
2014-12-02 00:48:38 +01:00
2019-10-09 16:16:37 +02:00
if hasattr(ssl, "PROTOCOL_SSLv3") and hasattr(OpenSSL.SSL, "SSLv3_METHOD"):
2019-07-20 05:59:46 +02:00
_openssl_versions[ssl.PROTOCOL_SSLv3] = OpenSSL.SSL.SSLv3_METHOD
2019-10-09 16:16:37 +02:00
if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"):
2016-01-19 00:28:54 +01:00
_openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD
2019-10-09 16:16:37 +02:00
if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"):
2016-01-19 00:28:54 +01:00
_openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD
2014-12-02 00:48:38 +01:00
_stdlib_to_openssl_verify = {
2013-08-15 15:33:47 +02:00
ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
2019-10-09 16:16:37 +02:00
ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER
+ OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
2013-08-15 15:33:47 +02:00
}
2019-10-09 16:16:37 +02:00
_openssl_to_stdlib_verify = dict((v, k) for k, v in _stdlib_to_openssl_verify.items())
2016-01-19 00:28:54 +01:00
# OpenSSL will only write 16K at a time
SSL_WRITE_BLOCKSIZE = 16384
2013-08-15 15:33:47 +02:00
orig_util_HAS_SNI = util.HAS_SNI
orig_util_SSLContext = util.ssl_.SSLContext
log = logging.getLogger(__name__)
2013-08-15 15:33:47 +02:00
def inject_into_urllib3():
2019-10-09 16:16:37 +02:00
"Monkey-patch urllib3 with PyOpenSSL-backed SSL-support."
2013-08-15 15:33:47 +02:00
_validate_dependencies_met()
2019-07-20 05:59:46 +02:00
util.SSLContext = PyOpenSSLContext
util.ssl_.SSLContext = PyOpenSSLContext
2013-08-15 15:33:47 +02:00
util.HAS_SNI = HAS_SNI
util.ssl_.HAS_SNI = HAS_SNI
util.IS_PYOPENSSL = True
util.ssl_.IS_PYOPENSSL = True
2013-08-15 15:33:47 +02:00
def extract_from_urllib3():
2019-10-09 16:16:37 +02:00
"Undo monkey-patching by :func:`inject_into_urllib3`."
2013-08-15 15:33:47 +02:00
2019-07-20 05:59:46 +02:00
util.SSLContext = orig_util_SSLContext
util.ssl_.SSLContext = orig_util_SSLContext
2013-08-15 15:33:47 +02:00
util.HAS_SNI = orig_util_HAS_SNI
util.ssl_.HAS_SNI = orig_util_HAS_SNI
util.IS_PYOPENSSL = False
util.ssl_.IS_PYOPENSSL = False
def _validate_dependencies_met():
"""
Verifies that PyOpenSSL's package-level dependencies have been met.
Throws `ImportError` if they are not met.
"""
# Method added in `cryptography==1.1`; not available in older versions
from cryptography.x509.extensions import Extensions
2019-10-09 16:16:37 +02:00
if getattr(Extensions, "get_extension_for_class", None) is None:
2019-10-09 16:16:37 +02:00
raise ImportError(
"'cryptography' module missing required functionality. "
"Try upgrading to v1.3.4 or newer."
)
# pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509
# attribute is only present on those versions.
from OpenSSL.crypto import X509
2019-10-09 16:16:37 +02:00
x509 = X509()
if getattr(x509, "_x509", None) is None:
2019-10-09 16:16:37 +02:00
raise ImportError(
"'pyOpenSSL' module missing required functionality. "
"Try upgrading to v0.14 or newer."
)
def _dnsname_to_stdlib(name):
"""
Converts a dNSName SubjectAlternativeName field to the form used by the
standard library on the given Python version.
Cryptography produces a dNSName as a unicode string that was idna-decoded
from ASCII bytes. We need to idna-encode that string to get it back, and
then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib
uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8).
2019-01-18 06:11:41 +01:00
If the name cannot be idna-encoded then we return None signalling that
the name given should be skipped.
"""
2019-10-09 16:16:37 +02:00
def idna_encode(name):
"""
Borrowed wholesale from the Python Cryptography Project. It turns out
that we can't just safely call `idna.encode`: it can explode for
wildcard names. This avoids that problem.
"""
2017-09-07 18:32:59 +02:00
from pip._vendor import idna
2019-01-18 06:11:41 +01:00
try:
2019-10-09 16:16:37 +02:00
for prefix in [u"*.", u"."]:
2019-01-18 06:11:41 +01:00
if name.startswith(prefix):
2019-10-09 16:16:37 +02:00
name = name[len(prefix) :]
return prefix.encode("ascii") + idna.encode(name)
2019-01-18 06:11:41 +01:00
return idna.encode(name)
except idna.core.IDNAError:
return None
2019-07-20 05:59:46 +02:00
# Don't send IPv6 addresses through the IDNA encoder.
2019-10-09 16:16:37 +02:00
if ":" in name:
2019-07-20 05:59:46 +02:00
return name
name = idna_encode(name)
2019-01-18 06:11:41 +01:00
if name is None:
return None
elif sys.version_info >= (3, 0):
2019-10-09 16:16:37 +02:00
name = name.decode("utf-8")
return name
2013-08-15 15:33:47 +02:00
def get_subj_alt_name(peer_cert):
"""
Given an PyOpenSSL certificate, provides all the subject alternative names.
"""
# Pass the cert to cryptography, which has much better APIs for this.
2017-09-07 18:32:59 +02:00
if hasattr(peer_cert, "to_cryptography"):
cert = peer_cert.to_cryptography()
else:
# This is technically using private APIs, but should work across all
# relevant versions before PyOpenSSL got a proper API for this.
cert = _Certificate(openssl_backend, peer_cert._x509)
# We want to find the SAN extension. Ask Cryptography to locate it (it's
# faster than looping in Python)
try:
2019-10-09 16:16:37 +02:00
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
except x509.ExtensionNotFound:
# No such extension, return the empty list.
return []
2019-10-09 16:16:37 +02:00
except (
x509.DuplicateExtension,
UnsupportedExtension,
x509.UnsupportedGeneralNameType,
UnicodeError,
) as e:
# A problem has been found with the quality of the certificate. Assume
# no SAN field is present.
log.warning(
"A problem was encountered with the certificate that prevented "
"urllib3 from finding the SubjectAlternativeName field. This can "
"affect certificate validation. The error was %s",
e,
)
return []
# We want to return dNSName and iPAddress fields. We need to cast the IPs
# back to strings because the match_hostname function wants them as
# strings.
# Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8
# decoded. This is pretty frustrating, but that's what the standard library
# does with certificates, and so we need to attempt to do the same.
2019-01-18 06:11:41 +01:00
# We also want to skip over names which cannot be idna encoded.
names = [
2019-10-09 16:16:37 +02:00
("DNS", name)
for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName))
2019-01-18 06:11:41 +01:00
if name is not None
]
names.extend(
2019-10-09 16:16:37 +02:00
("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress)
)
return names
2013-08-15 15:33:47 +02:00
class WrappedSocket(object):
2019-10-09 16:16:37 +02:00
"""API-compatibility wrapper for Python OpenSSL's Connection-class.
Note: _makefile_refs, _drop() and _reuse() are needed for the garbage
collector of pypy.
2019-10-09 16:16:37 +02:00
"""
2013-08-15 15:33:47 +02:00
def __init__(self, connection, socket, suppress_ragged_eofs=True):
2013-08-15 15:33:47 +02:00
self.connection = connection
self.socket = socket
self.suppress_ragged_eofs = suppress_ragged_eofs
self._makefile_refs = 0
self._closed = False
2013-08-15 15:33:47 +02:00
def fileno(self):
return self.socket.fileno()
# Copy-pasted from Python 3.5 source code
def _decref_socketios(self):
if self._makefile_refs > 0:
self._makefile_refs -= 1
if self._closed:
self.close()
def recv(self, *args, **kwargs):
try:
data = self.connection.recv(*args, **kwargs)
except OpenSSL.SSL.SysCallError as e:
2019-10-09 16:16:37 +02:00
if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"):
return b""
else:
raise SocketError(str(e))
2019-07-20 05:59:46 +02:00
except OpenSSL.SSL.ZeroReturnError:
2015-03-02 21:07:00 +01:00
if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
2019-10-09 16:16:37 +02:00
return b""
2015-03-02 21:07:00 +01:00
else:
raise
except OpenSSL.SSL.WantReadError:
2018-06-21 15:08:05 +02:00
if not util.wait_for_read(self.socket, self.socket.gettimeout()):
2019-10-09 16:16:37 +02:00
raise timeout("The read operation timed out")
else:
return self.recv(*args, **kwargs)
2019-07-20 05:59:46 +02:00
# TLS 1.3 post-handshake authentication
except OpenSSL.SSL.Error as e:
raise ssl.SSLError("read error: %r" % e)
else:
return data
2013-08-15 15:33:47 +02:00
def recv_into(self, *args, **kwargs):
try:
return self.connection.recv_into(*args, **kwargs)
except OpenSSL.SSL.SysCallError as e:
2019-10-09 16:16:37 +02:00
if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"):
return 0
else:
raise SocketError(str(e))
2019-07-20 05:59:46 +02:00
except OpenSSL.SSL.ZeroReturnError:
if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
return 0
else:
raise
except OpenSSL.SSL.WantReadError:
2018-06-21 15:08:05 +02:00
if not util.wait_for_read(self.socket, self.socket.gettimeout()):
2019-10-09 16:16:37 +02:00
raise timeout("The read operation timed out")
else:
return self.recv_into(*args, **kwargs)
2019-07-20 05:59:46 +02:00
# TLS 1.3 post-handshake authentication
except OpenSSL.SSL.Error as e:
raise ssl.SSLError("read error: %r" % e)
2013-08-15 15:33:47 +02:00
def settimeout(self, timeout):
return self.socket.settimeout(timeout)
2014-12-02 00:48:38 +01:00
def _send_until_done(self, data):
while True:
try:
return self.connection.send(data)
except OpenSSL.SSL.WantWriteError:
2018-06-21 15:08:05 +02:00
if not util.wait_for_write(self.socket, self.socket.gettimeout()):
2014-12-02 00:48:38 +01:00
raise timeout()
continue
2017-05-19 13:57:33 +02:00
except OpenSSL.SSL.SysCallError as e:
raise SocketError(str(e))
2014-12-02 00:48:38 +01:00
2013-08-15 15:33:47 +02:00
def sendall(self, data):
2016-01-19 00:28:54 +01:00
total_sent = 0
while total_sent < len(data):
2019-10-09 16:16:37 +02:00
sent = self._send_until_done(
data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE]
)
2016-01-19 00:28:54 +01:00
total_sent += sent
def shutdown(self):
# FIXME rethrow compatible exceptions should we ever use this
self.connection.shutdown()
2013-08-15 15:33:47 +02:00
def close(self):
if self._makefile_refs < 1:
2016-01-19 00:28:54 +01:00
try:
self._closed = True
2016-01-19 00:28:54 +01:00
return self.connection.close()
except OpenSSL.SSL.Error:
return
else:
self._makefile_refs -= 1
2013-08-15 15:33:47 +02:00
def getpeercert(self, binary_form=False):
x509 = self.connection.get_peer_certificate()
if not x509:
return x509
if binary_form:
2019-10-09 16:16:37 +02:00
return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509)
2013-08-15 15:33:47 +02:00
return {
2019-10-09 16:16:37 +02:00
"subject": ((("commonName", x509.get_subject().CN),),),
"subjectAltName": get_subj_alt_name(x509),
2013-08-15 15:33:47 +02:00
}
2019-07-20 05:59:46 +02:00
def version(self):
return self.connection.get_protocol_version_name()
def _reuse(self):
self._makefile_refs += 1
def _drop(self):
if self._makefile_refs < 1:
self.close()
else:
self._makefile_refs -= 1
2013-08-15 15:33:47 +02:00
if _fileobject: # Platform-specific: Python 2
2019-10-09 16:16:37 +02:00
def makefile(self, mode, bufsize=-1):
self._makefile_refs += 1
return _fileobject(self, mode, bufsize, close=True)
2019-10-09 16:16:37 +02:00
else: # Platform-specific: Python 3
makefile = backport_makefile
WrappedSocket.makefile = makefile
class PyOpenSSLContext(object):
"""
I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible
for translating the interface of the standard library ``SSLContext`` object
to calls into PyOpenSSL.
"""
2019-10-09 16:16:37 +02:00
def __init__(self, protocol):
self.protocol = _openssl_versions[protocol]
self._ctx = OpenSSL.SSL.Context(self.protocol)
self._options = 0
self.check_hostname = False
@property
def options(self):
return self._options
@options.setter
def options(self, value):
self._options = value
self._ctx.set_options(value)
@property
def verify_mode(self):
return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()]
@verify_mode.setter
def verify_mode(self, value):
2019-10-09 16:16:37 +02:00
self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback)
def set_default_verify_paths(self):
self._ctx.set_default_verify_paths()
def set_ciphers(self, ciphers):
if isinstance(ciphers, six.text_type):
2019-10-09 16:16:37 +02:00
ciphers = ciphers.encode("utf-8")
self._ctx.set_cipher_list(ciphers)
def load_verify_locations(self, cafile=None, capath=None, cadata=None):
if cafile is not None:
2019-10-09 16:16:37 +02:00
cafile = cafile.encode("utf-8")
if capath is not None:
2019-10-09 16:16:37 +02:00
capath = capath.encode("utf-8")
2020-07-22 01:40:07 +02:00
try:
self._ctx.load_verify_locations(cafile, capath)
if cadata is not None:
self._ctx.load_verify_locations(BytesIO(cadata))
except OpenSSL.SSL.Error as e:
raise ssl.SSLError("unable to load trusted certificates: %r" % e)
def load_cert_chain(self, certfile, keyfile=None, password=None):
2018-06-21 15:08:05 +02:00
self._ctx.use_certificate_chain_file(certfile)
if password is not None:
2019-07-20 05:59:46 +02:00
if not isinstance(password, six.binary_type):
2019-10-09 16:16:37 +02:00
password = password.encode("utf-8")
2019-07-20 05:59:46 +02:00
self._ctx.set_passwd_cb(lambda *_: password)
self._ctx.use_privatekey_file(keyfile or certfile)
2020-11-19 15:20:52 +01:00
def set_alpn_protocols(self, protocols):
protocols = [six.ensure_binary(p) for p in protocols]
return self._ctx.set_alpn_protos(protocols)
2019-10-09 16:16:37 +02:00
def wrap_socket(
self,
sock,
server_side=False,
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
server_hostname=None,
):
cnx = OpenSSL.SSL.Connection(self._ctx, sock)
if isinstance(server_hostname, six.text_type): # Platform-specific: Python 3
2019-10-09 16:16:37 +02:00
server_hostname = server_hostname.encode("utf-8")
if server_hostname is not None:
cnx.set_tlsext_host_name(server_hostname)
cnx.set_connect_state()
while True:
try:
cnx.do_handshake()
except OpenSSL.SSL.WantReadError:
2018-06-21 15:08:05 +02:00
if not util.wait_for_read(sock, sock.gettimeout()):
2019-10-09 16:16:37 +02:00
raise timeout("select timed out")
continue
except OpenSSL.SSL.Error as e:
2019-10-09 16:16:37 +02:00
raise ssl.SSLError("bad handshake: %r" % e)
break
2013-08-15 15:33:47 +02:00
return WrappedSocket(cnx, sock)
2013-08-15 15:33:47 +02:00
def _verify_callback(cnx, x509, err_no, err_depth, return_code):
return err_no == 0