]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
netutil: Use newer ssl APIs
authorBen Darnell <ben@bendarnell.com>
Wed, 29 Dec 2021 00:44:19 +0000 (19:44 -0500)
committerBen Darnell <ben@bendarnell.com>
Sun, 16 Jan 2022 21:49:19 +0000 (16:49 -0500)
Pass a client/server flag to all SSL context creations. This is required
to avoid deprecations in Python 3.10. This is a behavior change for
client-side connections: certificate verification and hostname checks
are now enabled in more situations (previously, the high-level
interfaces would generally enforce these requirements but the low-level
interfaces like SSLIOStream did not).

tornado/iostream.py
tornado/netutil.py
tornado/test/httpserver_test.py
tornado/test/iostream_test.py
tornado/test/simple_httpclient_test.py

index 930f73d61aa31dbe5735a43165c2a0ecc4a64d27..b9955daf86b41c3caa5b9a440068dd5e8152727a 100644 (file)
@@ -1530,6 +1530,7 @@ class SSLIOStream(IOStream):
             self._ssl_options,
             server_hostname=self._server_hostname,
             do_handshake_on_connect=False,
+            server_side=False,
         )
         self._add_io_state(old_state)
 
index 15eea9704fe0dfbb2ed976ff4d6aed9d9d396407..4a1135405a7392df4fcab654f5f585d507f9f5e9 100644 (file)
@@ -559,7 +559,8 @@ _SSL_CONTEXT_KEYWORDS = frozenset(
 
 
 def ssl_options_to_context(
-    ssl_options: Union[Dict[str, Any], ssl.SSLContext]
+    ssl_options: Union[Dict[str, Any], ssl.SSLContext],
+    server_side: Optional[bool] = None,
 ) -> ssl.SSLContext:
     """Try to convert an ``ssl_options`` dictionary to an
     `~ssl.SSLContext` object.
@@ -570,19 +571,34 @@ def ssl_options_to_context(
     `~ssl.SSLContext` equivalent, and may be used when a component which
     accepts both forms needs to upgrade to the `~ssl.SSLContext` version
     to use features like SNI or NPN.
+
+    .. versionchanged:: 6.2
+
+       Added server_side argument. Omitting this argument will
+       result in a DeprecationWarning on Python 3.10.
+
     """
     if isinstance(ssl_options, ssl.SSLContext):
         return ssl_options
     assert isinstance(ssl_options, dict)
     assert all(k in _SSL_CONTEXT_KEYWORDS for k in ssl_options), ssl_options
-    # Can't use create_default_context since this interface doesn't
-    # tell us client vs server.
-    context = ssl.SSLContext(ssl_options.get("ssl_version", ssl.PROTOCOL_SSLv23))
+    # TODO: Now that we have the server_side argument, can we switch to
+    # create_default_context or would that change behavior?
+    default_version = ssl.PROTOCOL_TLS
+    if server_side:
+        default_version = ssl.PROTOCOL_TLS_SERVER
+    elif server_side is not None:
+        default_version = ssl.PROTOCOL_TLS_CLIENT
+    context = ssl.SSLContext(ssl_options.get("ssl_version", default_version))
     if "certfile" in ssl_options:
         context.load_cert_chain(
             ssl_options["certfile"], ssl_options.get("keyfile", None)
         )
     if "cert_reqs" in ssl_options:
+        if ssl_options["cert_reqs"] == ssl.CERT_NONE:
+            # This may have been set automatically by PROTOCOL_TLS_CLIENT but is
+            # incompatible with CERT_NONE so we must manually clear it.
+            context.check_hostname = False
         context.verify_mode = ssl_options["cert_reqs"]
     if "ca_certs" in ssl_options:
         context.load_verify_locations(ssl_options["ca_certs"])
@@ -601,6 +617,7 @@ def ssl_wrap_socket(
     socket: socket.socket,
     ssl_options: Union[Dict[str, Any], ssl.SSLContext],
     server_hostname: Optional[str] = None,
+    server_side: Optional[bool] = None,
     **kwargs: Any
 ) -> ssl.SSLSocket:
     """Returns an ``ssl.SSLSocket`` wrapping the given socket.
@@ -610,14 +627,23 @@ def ssl_wrap_socket(
     keyword arguments are passed to ``wrap_socket`` (either the
     `~ssl.SSLContext` method or the `ssl` module function as
     appropriate).
+
+    .. versionchanged:: 6.2
+
+       Added server_side argument. Omitting this argument will
+       result in a DeprecationWarning on Python 3.10.
     """
-    context = ssl_options_to_context(ssl_options)
+    context = ssl_options_to_context(ssl_options, server_side=server_side)
+    if server_side is None:
+        server_side = False
     if ssl.HAS_SNI:
         # In python 3.4, wrap_socket only accepts the server_hostname
         # argument if HAS_SNI is true.
         # TODO: add a unittest (python added server-side SNI support in 3.4)
         # In the meantime it can be manually tested with
         # python3 -m tornado.httpclient https://sni.velox.ch
-        return context.wrap_socket(socket, server_hostname=server_hostname, **kwargs)
+        return context.wrap_socket(
+            socket, server_hostname=server_hostname, server_side=server_side, **kwargs
+        )
     else:
-        return context.wrap_socket(socket, **kwargs)
+        return context.wrap_socket(socket, server_side=server_side, **kwargs)
index 58696e238e5756b954eb0f539bdc9676902e7b7d..146ed781ca7c01d5e61186f1ed59726c68784024 100644 (file)
@@ -181,7 +181,9 @@ class TLSv1Test(BaseSSLTest, SSLTestMixin):
 
 class SSLContextTest(BaseSSLTest, SSLTestMixin):
     def get_ssl_options(self):
-        context = ssl_options_to_context(AsyncHTTPSTestCase.get_ssl_options(self))
+        context = ssl_options_to_context(
+            AsyncHTTPSTestCase.get_ssl_options(self), server_side=True
+        )
         assert isinstance(context, ssl.SSLContext)
         return context
 
index 8e8e9f477a77783e9568705d56157623152da2f8..b098c9aacdd863d6b1acf8e578d14b7e4e68e0c4 100644 (file)
@@ -12,7 +12,7 @@ from tornado.iostream import (
 from tornado.httputil import HTTPHeaders
 from tornado.locks import Condition, Event
 from tornado.log import gen_log
-from tornado.netutil import ssl_wrap_socket
+from tornado.netutil import ssl_options_to_context, ssl_wrap_socket
 from tornado.platform.asyncio import AddThreadSelectorEventLoop
 from tornado.tcpserver import TCPServer
 from tornado.testing import (
@@ -23,7 +23,12 @@ from tornado.testing import (
     ExpectLog,
     gen_test,
 )
-from tornado.test.util import skipIfNonUnix, refusing_port, skipPypy3V58
+from tornado.test.util import (
+    skipIfNonUnix,
+    refusing_port,
+    skipPypy3V58,
+    ignore_deprecation,
+)
 from tornado.web import RequestHandler, Application
 import asyncio
 import errno
@@ -900,11 +905,11 @@ class TestIOStream(TestIOStreamMixin, AsyncTestCase):
 
 class TestIOStreamSSL(TestIOStreamMixin, AsyncTestCase):
     def _make_server_iostream(self, connection, **kwargs):
-        connection = ssl.wrap_socket(
+        ssl_ctx = ssl_options_to_context(_server_ssl_options(), server_side=True)
+        connection = ssl_ctx.wrap_socket(
             connection,
             server_side=True,
             do_handshake_on_connect=False,
-            **_server_ssl_options()
         )
         return SSLIOStream(connection, **kwargs)
 
@@ -919,7 +924,7 @@ class TestIOStreamSSL(TestIOStreamMixin, AsyncTestCase):
 # instead of an ssl_options dict to the SSLIOStream constructor.
 class TestIOStreamSSLContext(TestIOStreamMixin, AsyncTestCase):
     def _make_server_iostream(self, connection, **kwargs):
-        context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+        context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
         context.load_cert_chain(
             os.path.join(os.path.dirname(__file__), "test.crt"),
             os.path.join(os.path.dirname(__file__), "test.key"),
@@ -930,7 +935,9 @@ class TestIOStreamSSLContext(TestIOStreamMixin, AsyncTestCase):
         return SSLIOStream(connection, **kwargs)
 
     def _make_client_iostream(self, connection, **kwargs):
-        context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+        context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
+        context.check_hostname = False
+        context.verify_mode = ssl.CERT_NONE
         return SSLIOStream(connection, ssl_options=context, **kwargs)
 
 
@@ -1076,8 +1083,11 @@ class WaitForHandshakeTest(AsyncTestCase):
             # to openssl 1.1.c. Other platforms might be affected with
             # newer openssl too). Disable it until we figure out
             # what's up.
-            ssl_ctx.options |= getattr(ssl, "OP_NO_TLSv1_3", 0)
-            client = SSLIOStream(socket.socket(), ssl_options=ssl_ctx)
+            # Update 2021-12-28: Still happening with Python 3.10 on
+            # Windows. OP_NO_TLSv1_3 now raises a DeprecationWarning.
+            with ignore_deprecation():
+                ssl_ctx.options |= getattr(ssl, "OP_NO_TLSv1_3", 0)
+                client = SSLIOStream(socket.socket(), ssl_options=ssl_ctx)
             yield client.connect(("127.0.0.1", port))
             self.assertIsNotNone(client.socket.cipher())
         finally:
index eadd4ed3034cb0b0cbeeb540e7770df7a187fd31..62bd4830c8389c861ab3b75856052fc805c0a79f 100644 (file)
@@ -537,11 +537,14 @@ class SimpleHTTPSClientTestCase(SimpleHTTPClientTestMixin, AsyncHTTPSTestCase):
         )
 
     def test_ssl_options(self):
-        resp = self.fetch("/hello", ssl_options={})
+        resp = self.fetch("/hello", ssl_options={"cert_reqs": ssl.CERT_NONE})
         self.assertEqual(resp.body, b"Hello world!")
 
     def test_ssl_context(self):
-        resp = self.fetch("/hello", ssl_options=ssl.SSLContext(ssl.PROTOCOL_SSLv23))
+        ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
+        ssl_ctx.check_hostname = False
+        ssl_ctx.verify_mode = ssl.CERT_NONE
+        resp = self.fetch("/hello", ssl_options=ssl_ctx)
         self.assertEqual(resp.body, b"Hello world!")
 
     def test_ssl_options_handshake_fail(self):
@@ -555,8 +558,8 @@ class SimpleHTTPSClientTestCase(SimpleHTTPClientTestMixin, AsyncHTTPSTestCase):
 
     def test_ssl_context_handshake_fail(self):
         with ExpectLog(gen_log, "SSL Error|Uncaught exception"):
-            ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
-            ctx.verify_mode = ssl.CERT_REQUIRED
+            # CERT_REQUIRED is set by default.
+            ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
             with self.assertRaises(ssl.SSLError):
                 self.fetch("/hello", ssl_options=ctx, raise_error=True)