Search code examples
pythonpython-requeststls1.2pyopenssl

Python requests library using TLSv1 or TLSv1.1 despite upgrading to Python 2.7


I wanted to make sure that when using requests library to post to an HTTP server, it would refuse to communicate using TLSv1 or TLSv1.1. To do this, I configured my https server to force the SSL protocol to use either TLSv1 or TLSv1.1. I expected these versions to be rejected.

My python program is running on CentOs machine:

cat /etc/centos-release
CentOS release 6.7 (Final)

The default Python version is 2.6.6:

which python
/usr/bin/python
Python 2.6.6 (r266:84292, Aug 18 2016, 15:13:37) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-17)] on linux2

I installed python 2.7:

which python2.7
/usr/local/bin/python2.7
Python 2.7.6 (default, Jun  2 2017, 11:37:31) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-16)] on linux2
>>> import ssl
>>> ssl.OPENSSL_VERSION
'OpenSSL 1.0.1e-fips 11 Feb 2013'

openssl version
OpenSSL 1.0.1e-fips 11 Feb 2013

Using pip2.7, I installed the libraries required get requests to run. I did not make any changes to the openssl.

I sent two test alerts and it seems that my program negotiated down to TLSv1. I was under the impression that TLSv1 is deprecated. The program is executing using python 2.7 and not the system default of python 2.6.

At the top of the python program I embedded: #!/usr/local/bin/python2.7

Below are the 2 post alerts that shows TLSv1.1 and TLSv1 going through:

"POST /testpost HTTP/1.1" 200 43 TLSv1.1/ECDHE-RSA-AES256-SHA "-" "python-requests/2.5.1 CPython/2.6.6 Linux/2.6.32-573.22.1.el6.x86_64" 0.006 <"{for: test purposes}"

"POST /testpost HTTP/1.1" 200 43 TLSv1/ECDHE-RSA-AES256-SHA "-" "python-requests/2.5.1 CPython/2.6.6 Linux/2.6.32-573.22.1.el6.x86_64" 0.006 <"{for: test purposes}"

Any thoughts?


Solution

  • The documentation of the ssl module has a table which shows which protocol settings work together. Usually if a client and a server both using PROTOCOL_TLS (which is the same as PROTOCOL_SSLv23) connect they use the highest shared protocol version. If there is no compatible version (e.g. the server only speaks 1.1, the client only 1.0) then you'll get an error.

    The requests documentation shows how you can force a client to use a specific tls version, e.g. to force TLS 1.2 you could use (slightly modified example):

    import ssl
    
    from requests.adapters import HTTPAdapter
    from requests.packages.urllib3.poolmanager import PoolManager
    
    class Tls12Adapter(HTTPAdapter):
        """"Transport adapter that forces TLSv1.2"""
    
        def init_poolmanager(self, *pool_args, **pool_kwargs):
            self.poolmanager = PoolManager(
                *pool_args,
                ssl_version=ssl.PROTOCOL_TLSv1_2,
                **pool_kwargs)
    

    Recent versions of urllib3 (which requests uses for connections) allow to pass a SSLContext instead, which allows for more flexible configurations, e.g. blocking specific versions while allowing any newer version:

    import ssl
    import requests
    
    from requests.adapters import HTTPAdapter
    from requests.packages.urllib3.poolmanager import PoolManager
    from requests.packages.urllib3.util import ssl_
    
    
    class TlsAdapter(HTTPAdapter):
    
        def __init__(self, ssl_options=0, **kwargs):
            self.ssl_options = ssl_options
            super(TlsAdapter, self).__init__(**kwargs)
    
        def init_poolmanager(self, *pool_args, **pool_kwargs):
            ctx = ssl_.create_urllib3_context(ssl.PROTOCOL_TLS)
            # extend the default context options, which is to disable ssl2, ssl3
            # and ssl compression, see:
            # https://github.com/shazow/urllib3/blob/6a6cfe9/urllib3/util/ssl_.py#L241
            ctx.options |= self.ssl_options
            self.poolmanager = PoolManager(*pool_args,
                                           ssl_context=ctx,
                                           **pool_kwargs)
    
    session = requests.session()
    # disallow tls1.0 and tls1.1, allow only tls1.2 (and newer if suported by
    # the used openssl version)
    adapter = TlsAdapter(ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1)
    session.mount("https://", adapter)