Permalink
Browse files

Remove backports.ssl dependency

  • Loading branch information...
mlorant committed Oct 5, 2017
1 parent 6cc77d7 commit 5a3f8d76f455ec908c6d44e228cd16d2db1c7b95
Showing with 31 additions and 227 deletions.
  1. +3 −3 doc/src/api.rst
  2. +16 −23 doc/src/concepts.rst
  3. +0 −5 doc/src/conf.py
  4. +3 −0 doc/src/releases.rst
  5. +2 −3 imapclient/config.py
  6. +2 −2 imapclient/imapclient.py
  7. +5 −185 imapclient/tls.py
  8. +0 −6 setup.py
View
@@ -46,9 +46,9 @@ malformed exception. In particular:
* socket.error
* socket.timeout: raised if a timeout was specified when creating the
IMAPClient instance and a network operation takes too long.
* backports.ssl.SSLError: the base class for network or SSL protocol
errors when ssl=True or starttls() is used.
* backports.ssl.CertificateError: raised when TLS certification
* ssl.SSLError: the base class for network or SSL protocol errors when
``ssl=True`` or ``starttls()`` is used.
* ssl.CertificateError: raised when TLS certification
verification fails. This is *not* a subclass of SSLError.
View
@@ -72,29 +72,22 @@ folder names are returned as str (Python 2) or bytes (Python 3).
TLS/SSL
~~~~~~~
IMAPClient established secure connections by default. It uses sensible TLS
parameters for encrypted connections and also allows for a high level of
control of TLS parameters if required, with the ``ssl`` module.
.. note::
For older versions of Python which do not provide a recent enough ``ssl``
module, IMAPClient installs `backports.ssl
<https://github.com/alekstorm/backports.ssl>`_ as a compatibility layer.
You are highly encouraged to use a recent version of Python if security is
of prime importance for you.
TLS parameters are controlled by passing an ``ssl.SSLContext`` (or
``backports.ssl.SSLContext`` when appropriate) when creating an IMAPClient
instance. When ``ssl=True`` is used without passing a SSLContext, a default
context is used. The default context avoids the use of known insecure ciphers
and SSL protocol versions, with certificate verification and hostname
verification turned on. The default context will use system installed
certificate authority trust chains, if available.
:py:func:`ssl.create_default_context()` returns a safe default context. When
constructing a custom context it is usually best to start with the default
context and modify it to suit your needs.
IMAPClient uses sensible TLS parameter defaults for encrypted
connections and also allows for a high level of control of TLS
parameters if required. It uses the built-in `ssl` package,
provided since Python 2.7.9 and 3.4.
TLS parameters are controlled by passing a ``ssl.SSLContext``
when creating an IMAPClient instance. When ``ssl=True`` is used
without passing a SSLContext, a default context is used. The default
context avoids the use of known insecure ciphers and SSL protocol
versions, with certificate verification and hostname verification
turned on. The default context will use system installed certificate
authority trust chains, if available.
When constructing a custom context it is usually best to start with
the default context, created by the ``ssl`` module, and modify it to
suit your needs.
The following example shows how to to disable certification
verification and certificate host name checks if required.
View
@@ -9,11 +9,6 @@
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..', '..')))
# Fake out backports(.ssl) so that it's not needed in order to build
# the docs (ReadTheDocs doesn't have backports.ssl installed).
MOCK_MODULES = ['backports']
sys.modules.update((mod_name, MagicMock()) for mod_name in MOCK_MODULES)
import imapclient
# -- General configuration -----------------------------------------------------
View
@@ -23,6 +23,9 @@ Changed
Other
-----
- Drop support of OAUTH(1)
- Drop ``imapclient.tls.create_default_context`` function. In case you were
using it, you can use the method with the same name available in the
built-in ``ssl`` module.
Python compatibility
View
@@ -5,15 +5,14 @@
from __future__ import unicode_literals
from os import environ, path
import ssl
from backports import ssl
from six import iteritems
from six.moves.configparser import SafeConfigParser, NoOptionError
from six.moves.urllib.request import urlopen
from six.moves.urllib.parse import urlencode
import imapclient
from .tls import create_default_context
try:
import json
@@ -159,7 +158,7 @@ def create_client_from_config(conf, login=True):
ssl_context = None
if conf.ssl:
ssl_context = create_default_context()
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
ssl_context.check_hostname = conf.ssl_check_hostname
if not conf.ssl_verify_cert:
ssl_context.verify_mode = ssl.CERT_NONE
View
@@ -241,8 +241,8 @@ def starttls(self, ssl_context=None):
"""Switch to an SSL encrypted connection by sending a STARTTLS command.
The *ssl_context* argument is optional and should be a
:py:class:`backports.ssl.SSLContext` object. If no SSL context
is given, a SSL context with reasonable default settings will be used.
:py:class:`ssl.SSLContext` object. If no SSL context is given, a SSL
context with reasonable default settings will be used.
You can enable checking of the hostname in the certificate presented
by the server against the hostname which was used for connecting, by
View
@@ -5,200 +5,20 @@
"""
This module contains IMAPClient's functionality related to Transport
Layer Security (TLS a.k.a. SSL).
It uses ``backports.ssl`` to provide consistent TLS functionality
across Python versions below 3.4 and 2.7.9, and uses the shipped
ssl module otherwise.
"""
import imaplib
import os
import socket
import sys
__all__ = ('create_default_context',)
# if Python versions are sufficient, wire up shipped modules
if sys.version_info[0] == 3 and sys.version_info[:2] >= (3, 4) or \
sys.version_info[0] == 2 and sys.version_info[:3] >= (2, 7, 9):
import ssl
def create_default_context(cafile=None, capath=None, cadata=None):
return ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH,
cafile=cafile,
capath=capath,
cadata=cadata)
def wrap_socket(sock, ssl_context, host):
if ssl_context is None:
ssl_context = create_default_context()
return ssl_context.wrap_socket(sock, server_hostname = host)
else:
# Explicitly check that the required pyOpenSSL is installed. On some
# systems (particularly OS X) the system installed version will be
# seen before any user installed version. Using a virtualenv is
# recommended to work around this.
def check_pyopenssl_version():
from distutils.version import LooseVersion as V
from OpenSSL import __version__ as installed_pyopenssl_version
from .version import min_pyopenssl_version
if V(installed_pyopenssl_version) < V(min_pyopenssl_version):
raise ImportError(
"pyOpenSSL version (%s) is too old. Need at least %s.\n"
"See http://imapclient.rtfd.io/#old-pyopenssl-versions"
% (installed_pyopenssl_version, min_pyopenssl_version))
if os.environ.get("READTHEDOCS") != "True":
check_pyopenssl_version()
try:
from backports import ssl
except ImportError:
raise ImportError("backports.ssl is not installed")
import warnings
warnings.warn("you are using an outdated ssl infrastructure "
"consider updating to a current Python 2 or 3 version", DeprecationWarning, 2)
_ossl = ssl.ossl
if sys.platform == "win32":
try:
from ssl import enum_certificates, Purpose
except ImportError:
enum_certificates = lambda x: []
# taken from Python 3.4 ssl module
_RESTRICTED_SERVER_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
'!eNULL:!MD5:!DSS:!RC4'
)
# TODO: get this into backports.ssl
def create_default_context(cafile=None, capath=None, cadata=None):
"""Return a backports.ssl.SSLContext object configured with sensible
default settings.
The optional *cafile* argument is path to a file of concatenated
CA certificates in PEM format.
The optional *capath* argument is a path to a directory containing
several CA certificates in PEM format, following an OpenSSL
specific layout.
The optional *cadata* argument is either an ASCII string of one or
more PEM-encoded certificates or a bytes-like object of
DER-encoded certificates.
If *cafile*, *capath* and *cadata* are all None then
system-installed CA certificates will be loaded (if available).
"""
# adapted from Python 3.4's ssl.create_default_context
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
# require certificate that matches the host name.
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
# SSLv2 considered harmful.
context.options |= _ossl.OP_NO_SSLv2
# SSLv3 has problematic security and is only required for really old
# clients such as IE6 on Windows XP
context.options |= _ossl.OP_NO_SSLv3
# disable compression to prevent CRIME attacks (OpenSSL 1.0+)
context.options |= getattr(_ossl, "OP_NO_COMPRESSION", 0)
# Prefer the server's ciphers by default so that we get stronger
# encryption
context.options |= getattr(_ossl, "OP_CIPHER_SERVER_PREFERENCE", 0)
# Use single use keys in order to improve forward secrecy
context.options |= getattr(_ossl, "OP_SINGLE_DH_USE", 0)
context.options |= getattr(_ossl, "OP_SINGLE_ECDH_USE", 0)
# disallow ciphers with known vulnerabilities
# TODO: backports.ssl.SSLContext is missing set_ciphers
context._ctx.set_cipher_list(_RESTRICTED_SERVER_CIPHERS)
if cafile or capath or cadata:
context.load_verify_locations(cafile, capath, cadata)
elif context.verify_mode != ssl.CERT_NONE:
# no explicit cafile, capath or cadata but the verify mode is
# CERT_OPTIONAL or CERT_REQUIRED. Let's try to load default system
# root CA certificates for the given purpose. This may fail silently.
if sys.platform == "win32":
certs = bytearray()
for storename in ("CA", "ROOT"):
for cert, encoding, trust in enum_certificates(storename):
# CA certs are never PKCS#7 encoded
if encoding == "x509_asn":
if trust is True or Purpose.SERVER_AUTH in trust:
certs.extend(cert)
if certs:
context.load_verify_locations(cadata=certs)
else:
context.set_default_verify_paths()
return context
def _wrap_socket(sock, ssl_context, hostname):
"""Wrap a socket and return an SSLSocket.
If *ssl_context* is `None`, a default context as returned by
`create_default_context` will be used.
If certificate validation fails, the socket will be shut down and
an Error raised.
"""
if not ssl_context:
ssl_context = create_default_context()
def killsock():
sock.shutdown(socket.SHUT_RDWR)
sock.close()
try:
newsock = ssl_context.wrap_socket(sock, server_hostname=hostname)
except ssl.CertificateError as err:
killsock()
raise imaplib.IMAP4.error("certificate error for %s: %s" % (hostname, str(err)))
except ssl.SSLError as err:
killsock()
raise imaplib.IMAP4.error("SSL error for %s: %s" % (hostname, err.args[-1]))
return _SSLSocketWithShutdown(newsock)
wrap_socket = lambda sock, context, host: _wrap_socket(sock, context, host)
# TODO: get shutdown added in backports.ssl.SSLSocket
class _SSLSocketWithShutdown(object):
import ssl
def __init__(self, sslsock):
self.sslsock = sslsock
def shutdown(self, how):
return self.sslsock._conn.sock_shutdown(how)
def wrap_socket(sock, ssl_context, host):
if ssl_context is None:
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
def __getattr__(self, name):
return getattr(self.sslsock, name)
return ssl_context.wrap_socket(sock, server_hostname=host)
# This implementation is shared across the different ssl interfaces
class IMAP4_TLS(imaplib.IMAP4):
"""IMAP4 client class for TLS/SSL connections.
View
@@ -69,12 +69,6 @@ def run_tests(self):
common_deps = ['six']
main_deps = common_deps[:]
# use shipped ssl module with Python >= (3.4, 2.7.9)
if IS_PY3 and IS_PY_33_OR_OLDER or IS_PY_278_OR_OLDER:
main_deps.append('backports.ssl>=0.0.9')
main_deps.append('pyopenssl>=' + info["min_pyopenssl_version"])
setup_deps = common_deps + ['sphinx']
test_deps = ['mock>=1.3.0']

0 comments on commit 5a3f8d7

Please sign in to comment.