Skip to content

Commit

Permalink
Validate SSL certs by default at the IOStream level.
Browse files Browse the repository at this point in the history
Use the system certificates instead of certifi when available.
Note that this does not change the behavior of simple_httpclient,
which always uses certifi but will be changing in a future commit.
  • Loading branch information
bdarnell committed Feb 15, 2015
1 parent d41d160 commit 308d872
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 15 deletions.
53 changes: 43 additions & 10 deletions tornado/iostream.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from __future__ import absolute_import, division, print_function, with_statement

import certifi
import collections
import errno
import numbers
Expand Down Expand Up @@ -82,7 +83,26 @@
if hasattr(errno, "WSAEINPROGRESS"):
_ERRNO_INPROGRESS += (errno.WSAEINPROGRESS,)

#######################################################
if hasattr(ssl, 'SSLContext'):
if hasattr(ssl, 'create_default_context'):
# Python 2.7.9+, 3.4+
# Note that the naming of ssl.Purpose is confusing; the purpose
# of a context is to authentiate the opposite side of the connection.
_client_ssl_defaults = ssl.create_default_context(
ssl.Purpose.SERVER_AUTH)
_server_ssl_defaults = ssl.create_default_context(
ssl.Purpose.CLIENT_AUTH)
else:
# Python 3.2-3.3
_client_ssl_defaults = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
_client_ssl_defaults.verify_mode = ssl.CERT_REQUIRED
_client_ssl_defaults.load_verify_locations(certifi.where())
_server_ssl_defaults = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
else:
# Python 2.6-2.7.8
_client_ssl_defaults = dict(cert_reqs=ssl.CERT_REQUIRED,
ca_certs=certifi.where())
_ssl_server_defaults = {}


class StreamClosedError(IOError):
Expand Down Expand Up @@ -1022,10 +1042,10 @@ class is recommended instead of calling this method directly.
returns a `.Future` (whose result after a successful
connection will be the stream itself).
If specified, the ``server_hostname`` parameter will be used
in SSL connections for certificate validation (if requested in
the ``ssl_options``) and SNI (if supported; requires
Python 2.7.9+).
In SSL mode, the ``server_hostname`` parameter will be used
for certificate validation (unless disabled in the
``ssl_options``) and SNI (if supported; requires Python
2.7.9+).
Note that it is safe to call `IOStream.write
<BaseIOStream.write>` while the connection is pending, in
Expand All @@ -1036,6 +1056,11 @@ class is recommended instead of calling this method directly.
.. versionchanged:: 4.0
If no callback is given, returns a `.Future`.
.. versionchanged:: 4.2
SSL certificates are validated by default; pass
``ssl_options=dict(cert_reqs=ssl.CERT_NONE)`` or a
suitably-configured `ssl.SSLContext` to the
`SSLIOStream` constructor to disable.
"""
self._connecting = True
if callback is not None:
Expand Down Expand Up @@ -1080,9 +1105,9 @@ def start_tls(self, server_side, ssl_options=None, server_hostname=None):
The ``ssl_options`` argument may be either an `ssl.SSLContext`
object or a dictionary of keyword arguments for the
`ssl.wrap_socket` function. If a ``server_hostname`` is
given, it will be used for certificate verification (as
configured in the ``ssl_options``).
`ssl.wrap_socket` function. The ``server_hostname`` argument
will be used for certificate validation unless disabled
in the ``ssl_options``.
This method returns a `.Future` whose result is the new
`SSLIOStream`. After this method has been called,
Expand All @@ -1092,6 +1117,11 @@ def start_tls(self, server_side, ssl_options=None, server_hostname=None):
transferred to the new stream.
.. versionadded:: 4.0
.. versionchanged:: 4.2
SSL certificates are validated by default; pass
``ssl_options=dict(cert_reqs=ssl.CERT_NONE)`` or a
suitably-configured `ssl.SSLContext` to disable.
"""
if (self._read_callback or self._read_future or
self._write_callback or self._write_future or
Expand All @@ -1100,7 +1130,10 @@ def start_tls(self, server_side, ssl_options=None, server_hostname=None):
self._read_buffer or self._write_buffer):
raise ValueError("IOStream is not idle; cannot convert to SSL")
if ssl_options is None:
ssl_options = {}
if server_side:
ssl_options = _server_ssl_defaults
else:
ssl_options = _client_ssl_defaults

socket = self.socket
self.io_loop.remove_handler(socket)
Expand Down Expand Up @@ -1184,7 +1217,7 @@ def __init__(self, *args, **kwargs):
`ssl.SSLContext` object or a dictionary of keywords arguments
for `ssl.wrap_socket`
"""
self._ssl_options = kwargs.pop('ssl_options', {})
self._ssl_options = kwargs.pop('ssl_options', _client_ssl_defaults)
super(SSLIOStream, self).__init__(*args, **kwargs)
self._ssl_accepting = True
self._handshake_reading = False
Expand Down
13 changes: 8 additions & 5 deletions tornado/test/iostream_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,8 @@ def _make_client_iostream(self):

class TestIOStreamWebHTTPS(TestIOStreamWebMixin, AsyncHTTPSTestCase):
def _make_client_iostream(self):
return SSLIOStream(socket.socket(), io_loop=self.io_loop)
return SSLIOStream(socket.socket(), io_loop=self.io_loop,
ssl_options=dict(cert_reqs=ssl.CERT_NONE))


class TestIOStream(TestIOStreamMixin, AsyncTestCase):
Expand All @@ -777,7 +778,9 @@ def _make_server_iostream(self, connection, **kwargs):
return SSLIOStream(connection, io_loop=self.io_loop, **kwargs)

def _make_client_iostream(self, connection, **kwargs):
return SSLIOStream(connection, io_loop=self.io_loop, **kwargs)
return SSLIOStream(connection, io_loop=self.io_loop,
ssl_options=dict(cert_reqs=ssl.CERT_NONE),
**kwargs)


# This will run some tests that are basically redundant but it's the
Expand Down Expand Up @@ -867,7 +870,7 @@ def test_start_tls_smtp(self):
yield self.server_send_line(b"250 STARTTLS\r\n")
yield self.client_send_line(b"STARTTLS\r\n")
yield self.server_send_line(b"220 Go ahead\r\n")
client_future = self.client_start_tls()
client_future = self.client_start_tls(dict(cert_reqs=ssl.CERT_NONE))
server_future = self.server_start_tls(_server_ssl_options())
self.client_stream = yield client_future
self.server_stream = yield server_future
Expand All @@ -879,8 +882,8 @@ def test_start_tls_smtp(self):
@gen_test
def test_handshake_fail(self):
server_future = self.server_start_tls(_server_ssl_options())
client_future = self.client_start_tls(
dict(cert_reqs=ssl.CERT_REQUIRED, ca_certs=certifi.where()))
# Certificates are verified with the default configuration.
client_future = self.client_start_tls(server_hostname="localhost")
with ExpectLog(gen_log, "SSL Error"):
with self.assertRaises(ssl.SSLError):
yield client_future
Expand Down

0 comments on commit 308d872

Please sign in to comment.