Skip to main content

Influencing Python's choice of SSL/TLS cipher-suite

I'm debugging a fault being received by an application that uses the Python [v2.7] SOAP library (module name 'suds'). I want to look inside the SSL datastream, much as I can between SoapUI and the same server. SoapUI, being the typical Java client, doesn't default to a particularly high grade of cryptography, and so with the private key I can first record and then later inspect the cleartext data with Wireshark.

But Python, like many others, seems to default to enabling a lot of more secure cipher-suites that enable perfect-forward-secrecy. In order to snoop on those, you need to get the client to dump out the pre-master key somewhere.This post shows how you can you get Python's SSL module to use a different cipher specification string.

I've met a similar problem before when dealing with a flaky SSL implementation in an LDAP server; I needed to be able to get OpenLDAP client tools to use a particular [set of] ciphers, and was able to isolate those that exhibited a problem (which could have then be disabled). In that case I was able to use an environment variable LDAP_TLS_CIPHER_SUITE (IIRC)...

I needed something similar for Python, if it existed.

First, let's look at the stack at the call-graph of modules involved: suds > urllib2 > httplib > ssl > _ssl

The '_ssl' module is where it loads into the OpenSSL native libraries.

What I'm looking for is likely to exist either at the httplib module layer, or the ssl module layer.

Looking at httplib, we see the following

    import ssl
except ImportError:
    class HTTPSConnection(HTTPConnection):
        "This class allows communication via SSL."

        default_port = HTTPS_PORT

        def __init__(self, host, port=None, key_file=None, cert_file=None,
                     strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
            HTTPConnection.__init__(self, host, port, strict, timeout,
            self.key_file = key_file
            self.cert_file = cert_file

        def connect(self):
            "Connect to a host on a given (SSL) port."

            sock = socket.create_connection((, self.port),
                                            self.timeout, self.source_address)
            if self._tunnel_host:
                self.sock = sock
            self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file)

Hmm, there doesn't appear to expose any mechanism for influencing cipher choice, so let's go down the stack to the ssl module:

# Disable weak or insecure ciphers by default
# (OpenSSL's default setting is 'DEFAULT:!aNULL:!eNULL')

class SSLSocket(socket):

    """This class implements a subtype of socket.socket that wraps
    the underlying OS socket in an SSL context when necessary, and
    provides read and write methods over that channel."""

    def __init__(self, sock, keyfile=None, certfile=None,
                 server_side=False, cert_reqs=CERT_NONE,
                 ssl_version=PROTOCOL_SSLv23, ca_certs=None,
                 suppress_ragged_eofs=True, ciphers=None):
        socket.__init__(self, _sock=sock._sock)

        for attr in _delegate_methods:
                delattr(self, attr)
            except AttributeError:

        if ciphers is None and ssl_version != _SSLv2_IF_EXISTS:
            ciphers = _DEFAULT_CIPHERS

            # yes, create the SSL object
            self._connected = True
            self._sslobj = _ssl.sslwrap(self._sock, server_side,
                                        keyfile, certfile,
                                        cert_reqs, ssl_version, ca_certs,

Hmmm, so it seems that the default are entirely specified in the 'ssl' module, are there is no environment variable or similar to control it from outside the process.

So if we want to enable lower-grade crypto, we would need to change the _DEFAULT_CIPHERS (perhaps to match OpenSSL's default), or put in some more code to feed that down the layers all the way from 'suds' to 'ssl'. I suppose some keyword-arguments would be the tidiest way. But as a system administrator, I'd rather have the ability to do this for any [Python] application, not just the ones have considered this fairly minor use case.

I don't want to change _DEFAULT_CIPHERS, because that would be degrading security for all users of the module, which is clearly the wrong thing to do. I'd prefer to have this local to a process, not to a program. I think an environment variable would be the cleanest way for this. Since there doesn't appear to be a standard one, we'll have to invent on. How about the following:

  • 'PYTHON_SSL_CIPHER_SPEC', which would take the usual string of OpenSSL ciphers such as 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2', and
  • 'PYTHON_SSL_VERSION', which would take one of the _PROTOCOL_NAMES values from (currently 'TLSv1', 'SSLv23' and 'SSLv3')
So, planning the change, we would need to:
  1. import os
  2. In SSLSocket constructor, add code to see if 'PYTHON_SSL_CIPHER_SPEC' in os.environ, and if so, to set  'ciphers' before the test that sets it to the defaults. Note that the environment variable should, at least in this case, override any explicit setting.... although this is debatable, as if anything can set it, then it will generally have some way of adjusting it... perhaps.
  3. Similarly, look up any provided value for PYTHON_SSL_VERSION using a reverse lookup of the _PROTOCOL_NAMES dictionary

New version:

# At the top of the file
import os
class SSLSocket(socket):

    """This class implements a subtype of socket.socket that wraps
    the underlying OS socket in an SSL context when necessary, and
    provides read and write methods over that channel."""

    def __init__(self, sock, keyfile=None, certfile=None,
                 server_side=False, cert_reqs=CERT_NONE,
                 ssl_version=PROTOCOL_SSLv23, ca_certs=None,
                 suppress_ragged_eofs=True, ciphers=None):

        if 'PYTHON_SSL_CIPHER_SPEC' in os.environ:
            ciphers = os.environ['PYTHON_SSL_CIPHER_SPEC']

        if 'PYTHON_SSL_VERSION' in os.environ:
            matches = [ k for k,v in _PROTOCOL_NAMES.items() if v == os.environ['PYTHON_SSL_VERSION'] ]
            if len(matches) > 0:
                ssl_version = matches[0]

        if ciphers is None and ssl_version != _SSLv2_IF_EXISTS:
            ciphers = _DEFAULT_CIPHERS

To use it, start a wireshark capture, and run your command with the environment variable in place.


You should see the manual page for 'ciphers'. In this case, I used the same cipher that SoapUI was using (the manual-page has a useful table to turn this into OpenSSL's idea of the cipher.

If you've configured wireshark with the private key, you should now be able to see what's inside.

Right, now let the real debugging begin!...


Popular posts from this blog

Use IPTables NOTRACK to implement stateless rules and reduce packet loss.

I recently struck a performance problem with a high-volume Linux DNS server and found a very satisfying way to overcome it. This post is not about DNS specifically, but useful also to services with a high rate of connections/sessions (UDP or TCP), but it is especially useful for UDP-based traffic, as the stateful firewall doesn't really buy you much with UDP. It is also applicable to services such as HTTP/HTTPS or anything where you have a lot of connections...

We observed times when DNS would not respond, but retrying very soon after would generally work. For TCP, you may find that you get a a connection timeout (or possibly a connection reset? I haven't checked that recently).

Observing logs, you might the following in kernel logs:
kernel: nf_conntrack: table full, dropping packet. You might be inclined to increase net.netfilter.nf_conntrack_max and net.nf_conntrack_max, but a better response might be found by looking at what is actually taking up those entries in your conne…

ORA-12170: TNS:Connect timeout — resolved

If you're dealing with Oracle clients, you may be familiar with the error message
ERROR ORA-12170: TNS:Connect timed out occurred I was recently asked to investigate such a problem where an application server was having trouble talking to a database server. This issue was blocking progress on a number of projects in our development environment, and our developers' agile post-it note progress note board had a red post-it saying 'Waiting for Cameron', so I thought I should promote it to the front of my rather long list of things I needed to do... it probably also helped that the problem domain was rather interesting to me, and so it ended being a late-night productivity session where I wasn't interrupted and my experimentation wouldn't disrupt others. I think my colleagues are still getting used to seeing email from me at the wee hours of the morning.

This can masquerade as a number of other error strings as well. Here's what you might see in the sqlnet.log f…

Getting MySQL server to run with SSL

I needed to get an old version of MySQL server running with SSL. Thankfully, that support has been there for a long time, although on my previous try I found it rather frustrating and gave it over for some other job that needed doing.

If securing client connections to a database server is a non-negotiable requirement, I would suggest that MySQL is perhaps a poor-fit and other options, such as PostgreSQL -- according to common web-consensus and my interactions with developers would suggest -- should be first considered. While MySQL can do SSL connections, it does so in a rather poor way that leaves much to be desired.

UPDATED 2014-04-28 for MySQL 5.0 (on ancient Debian Etch).

Here is the fast guide to getting SSL on MySQL server. I'm doing this on a Debian 7 ("Wheezy") server. To complete things, I'll test connectivity from a 5.1 client as well as a reasonably up-to-date MySQL Workbench 5.2 CE, plus a Python 2.6 client; just to see what sort of pain awaits.

UPDATE: 2014-0…