2019-01-20
A few notes on using SSL in Python 3 client programs
I was recently writing a Python program to check whether a test account could log into our IMAP servers and to time how long it took (as part of our new Prometheus monitoring). I used Python because it's one of our standard languages and because it includes the imaplib module, which did all of the hard work for me. As is my usual habit, I read as little of the detailed module documentation as possible and used brute force, which means that my first code looked kind of like this:
try: m = imaplib.IMAP4_SSL(host=host) m.login(user, pw) m.logout() except ....: [...]
When I tried out this code, I discovered that it was perfectly
willing to connect to our IMAP servers using the wrong host name.
At one level this is sort of okay (we're verifying that the IMAP
TLS certificates are good through other checks), but at another
it's wrong. So I went and read the module documentation with a bit
more care, where it pointed me to the ssl module's "Security
considerations" section, which
told me that in modern Python, you want to supply a SSL context and you
should normally get that context from ssl.create_default_context()
.
The default SSL context is good for a client connecting to a server.
It does certificate verification, including hostname verification,
and has officially reasonable defaults, some of which you can see
in ctx.options
of a created context, and also ctx.get_ciphers()
(although the latter is rather verbose). Based on the module
documentation, Python 3 is not entirely relying on the defaults of
the underlying TLS library. However the underlying TLS library (and
its version) affects what module features are available; you need
OpenSSL 1.1.0g or later to get SSLContext.minimum_version
,
for example.
It's good that people who care can carefully select ciphers, TLS versions, and so on, but it's better that this seems to have good defaults (especially if we want to move away from the server dictating cipher order). I considered explicitly disabling TLSv1 in my checker, but decided that I didn't care enough to tune the settings here (and especially to keep them tuned). Note that explicitly setting a minimum version is a dangerous operation over the long term, because it means that someday you're lowering the minimum version instead of raising it.
(Today, for example, you might set the minimum version to TLS v1.2
and increase your security over the defaults. Then in five years,
the default version could change to TLS v1.3 and now your unchanged
code is worse than the defaults. Fortunately the TLS version constants
do compare properly so far, so you can write code that uses max()
to do it more or less right.)
Python 2.7 also has SSL contexts and ssl.create_default_context()
,
starting in 2.7.9. However, use of SSL contexts is less widespread
than it is in Python 3 (for instance the Python 2 imaplib
doesn't seem to
support them), so I think it's clear you want to use Python 3 here
if you have a choice.
(It seems a little bit odd to still be thinking about Python 2 now that it's less than a year to it being officially unsupported by the Python developers, but it's not going away any time soon and there are probably people writing new code in it.)