| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 23afd17 commit bdebbf9
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -521,6 +521,11 @@ Opening network connections | |||
| 521 | 521 | ||
| 522 | 522 | Added the *ssl_shutdown_timeout* parameter. | |
| 523 | 523 | ||
| 524 | + .. versionchanged:: next | ||
| 525 | + Raises a ``DeprecationWarning`` if ``ssl.check_hostname`` is ``True`` | ||
| 526 | + and ``server_hostname`` is not supplied. In Python 3.13 and | ||
| 527 | + later a ``ValueError`` is raised instead. | ||
| 528 | + | ||
| 524 | 529 | .. seealso:: | |
| 525 | 530 | ||
| 526 | 531 | The :func:`open_connection` function is a high-level alternative | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -365,6 +365,12 @@ StreamWriter | |||
| 365 | 365 | ||
| 366 | 366 | .. versionadded:: 3.11 | |
| 367 | 367 | ||
| 368 | + .. versionchanged:: next | ||
| 369 | + Raises a ``DeprecationWarning`` if ``ssl.check_hostname`` is ``True`` | ||
| 370 | + and ``server_hostname`` is not supplied. In Python 3.13 and | ||
| 371 | + later a ``ValueError`` is raised instead. | ||
| 372 | + | ||
| 373 | + | ||
| 368 | 374 | .. method:: is_closing() | |
| 369 | 375 | ||
| 370 | 376 | Return ``True`` if the stream is closed or in the process of | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1902,7 +1902,11 @@ to speed up repeated connections from the same clients. | |||
| 1902 | 1902 | outgoing BIO. | |
| 1903 | 1903 | ||
| 1904 | 1904 | The *server_side*, *server_hostname* and *session* parameters have the | |
| 1905 | - same meaning as in :meth:`SSLContext.wrap_socket`. | ||
| 1905 | + same meaning as in :meth:`SSLContext.wrap_socket`, and are validated in | ||
| 1906 | + the same way: in particular a :exc:`DeprecationWarning` is raised when | ||
| 1907 | + :attr:`~SSLContext.check_hostname` is enabled but no *server_hostname* is | ||
| 1908 | + given, since there would be no name to match the peer's certificate | ||
| 1909 | + against. In Python 3.13 and later a ``ValueError`` is raised instead. | ||
| 1906 | 1910 | ||
| 1907 | 1911 | .. versionchanged:: 3.6 | |
| 1908 | 1912 | *session* argument was added. | |
@@ -1911,6 +1915,13 @@ to speed up repeated connections from the same clients. | |||
| 1911 | 1915 | The method returns an instance of :attr:`SSLContext.sslobject_class` | |
| 1912 | 1916 | instead of hard-coded :class:`SSLObject`. | |
| 1913 | 1917 | ||
| 1918 | + .. versionchanged:: next | ||
| 1919 | + The *server_side*, *server_hostname* and *session* parameters are now | ||
| 1920 | + validated as :meth:`SSLContext.wrap_socket` validates them. Previously | ||
| 1921 | + a context with :attr:`~SSLContext.check_hostname` enabled and no | ||
| 1922 | + *server_hostname* was accepted, and verified the certificate chain but | ||
| 1923 | + never the peer's identity. | ||
| 1924 | + | ||
| 1914 | 1925 | .. attribute:: SSLContext.sslobject_class | |
| 1915 | 1926 | ||
| 1916 | 1927 | The return type of :meth:`SSLContext.wrap_bio`, defaults to | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -866,6 +866,19 @@ def __init__(self, *args, **kwargs): | |||
| 866 | 866 | @classmethod | |
| 867 | 867 | def _create(cls, incoming, outgoing, server_side=False, | |
| 868 | 868 | server_hostname=None, session=None, context=None): | |
| 869 | + if server_side: | ||
| 870 | + if server_hostname: | ||
| 871 | + raise ValueError("server_hostname can only be specified " | ||
| 872 | + "in client mode") | ||
| 873 | + if session is not None: | ||
| 874 | + raise ValueError("session can only be specified in " | ||
| 875 | + "client mode") | ||
| 876 | + if context.check_hostname and server_hostname is None: | ||
| 877 | + # Note: server_hostname='' is handled within _wrap_bio(). | ||
| 878 | + warnings.warn("check_hostname requires server_hostname", | ||
| 879 | + category=DeprecationWarning, | ||
| 880 | + stacklevel=3) | ||
| 881 | + | ||
| 869 | 882 | self = cls.__new__(cls) | |
| 870 | 883 | sslobj = context._wrap_bio( | |
| 871 | 884 | incoming, outgoing, server_side=server_side, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -70,6 +70,56 @@ def test_handshake_timeout_negative(self): | |||
| 70 | 70 | sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter, | |
| 71 | 71 | ssl_handshake_timeout=-10) | |
| 72 | 72 | ||
| 73 | + def test_check_hostname_accepts_server_hostname(self): | ||
| 74 | + # Supplying a server_hostname succeeds with check_hostname enabled. | ||
| 75 | + sslcontext = test_utils.simple_client_sslcontext(disable_verify=False) | ||
| 76 | + sslcontext.check_hostname = True | ||
| 77 | + app_proto = mock.Mock() | ||
| 78 | + waiter = mock.Mock() | ||
| 79 | + | ||
| 80 | + # No ValueError is raised from SSLProtocol with 'server_hostname'. | ||
| 81 | + ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter, | ||
| 82 | + server_hostname='example.org') | ||
| 83 | + self.addCleanup(ssl_proto._app_transport.close) | ||
| 84 | + | ||
| 85 | + def test_check_hostname_requires_server_hostname(self): | ||
| 86 | + # A caller-supplied context asking for hostname checking used to be | ||
| 87 | + # taken through wrap_bio() with no name to check against, verifying | ||
| 88 | + # the certificate chain but never the peer's identity. | ||
| 89 | + # loop.start_tls() defaults server_hostname to None, and | ||
| 90 | + # loop.create_connection() turns server_hostname='' into None here, | ||
| 91 | + # so both reached that state. | ||
| 92 | + sslcontext = test_utils.simple_client_sslcontext(disable_verify=False) | ||
| 93 | + sslcontext.check_hostname = True | ||
| 94 | + app_proto = mock.Mock() | ||
| 95 | + waiter = mock.Mock() | ||
| 96 | + server_hostname = None | ||
| 97 | + | ||
| 98 | + # Supplying no server_hostname warns with check_hostname enabled. | ||
| 99 | + with self.assertWarnsRegex( | ||
| 100 | + DeprecationWarning, | ||
| 101 | + 'check_hostname requires server_hostname'): | ||
| 102 | + sslproto.SSLProtocol(self.loop, app_proto, sslcontext, | ||
| 103 | + waiter) | ||
| 104 | + | ||
| 105 | + with self.assertWarnsRegex( | ||
| 106 | + DeprecationWarning, | ||
| 107 | + 'check_hostname requires server_hostname'): | ||
| 108 | + sslproto.SSLProtocol(self.loop, app_proto, sslcontext, | ||
| 109 | + waiter, | ||
| 110 | + server_hostname=server_hostname) | ||
| 111 | + | ||
| 112 | + # Disabling check_hostname allows for an empty or unset server_hostname. | ||
| 113 | + sslcontext.check_hostname = False | ||
| 114 | + | ||
| 115 | + ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter) | ||
| 116 | + self.addCleanup(ssl_proto._app_transport.close) | ||
| 117 | + | ||
| 118 | + ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, | ||
| 119 | + waiter, | ||
| 120 | + server_hostname=server_hostname) | ||
| 121 | + self.addCleanup(ssl_proto._app_transport.close) | ||
| 122 | + | ||
| 73 | 123 | def test_eof_received_waiter(self): | |
| 74 | 124 | waiter = self.loop.create_future() | |
| 75 | 125 | ssl_proto = self.ssl_protocol(waiter=waiter) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,6 +9,7 @@ | |||
| 9 | 9 | from test.support import socket_helper | |
| 10 | 10 | from test.support import threading_helper | |
| 11 | 11 | from test.support import warnings_helper | |
| 12 | + import contextlib | ||
| 12 | 13 | import array | |
| 13 | 14 | import re | |
| 14 | 15 | import socket | |
@@ -328,6 +329,34 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True): | |||
| 328 | 329 | return client_context, server_context, hostname | |
| 329 | 330 | ||
| 330 | 331 | ||
| 332 | + def connected_bio_pair(client_context, server_context, hostname, max_retry=5): | ||
| 333 | + """Handshake a client and a server SSLObject against each other. | ||
| 334 | + | ||
| 335 | + Everything happens in memory, so this needs no socket and no thread. | ||
| 336 | + Returns the two objects followed by their four BIOs, in the order | ||
| 337 | + client, server, c_in, c_out, s_in, s_out. | ||
| 338 | + """ | ||
| 339 | + c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO() | ||
| 340 | + s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO() | ||
| 341 | + client = client_context.wrap_bio(c_in, c_out, server_hostname=hostname) | ||
| 342 | + server = server_context.wrap_bio(s_in, s_out, server_side=True) | ||
| 343 | + | ||
| 344 | + # Loop on the handshake for a bit to get it settled | ||
| 345 | + for _ in range(max_retry): | ||
| 346 | + with contextlib.suppress(ssl.SSLWantReadError): | ||
| 347 | + client.do_handshake() | ||
| 348 | + if c_out.pending: | ||
| 349 | + s_in.write(c_out.read()) | ||
| 350 | + with contextlib.suppress(ssl.SSLWantReadError): | ||
| 351 | + server.do_handshake() | ||
| 352 | + if s_out.pending: | ||
| 353 | + c_in.write(s_out.read()) | ||
| 354 | + # Now the handshakes should be complete (don't raise WantReadError) | ||
| 355 | + client.do_handshake() | ||
| 356 | + server.do_handshake() | ||
| 357 | + return client, server, c_in, c_out, s_in, s_out | ||
| 358 | + | ||
| 359 | + | ||
| 331 | 360 | class BasicSocketTests(unittest.TestCase): | |
| 332 | 361 | ||
| 333 | 362 | def test_constants(self): | |
@@ -1914,6 +1943,10 @@ def test_subclass(self): | |||
| 1914 | 1943 | ||
| 1915 | 1944 | def test_bad_server_hostname(self): | |
| 1916 | 1945 | ctx = ssl.create_default_context() | |
| 1946 | + # Omitting the name entirely is bad too: this context checks it. | ||
| 1947 | + with self.assertWarns(DeprecationWarning): | ||
| 1948 | + ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), | ||
| 1949 | + server_hostname=None) | ||
| 1917 | 1950 | with self.assertRaises(ValueError): | |
| 1918 | 1951 | ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), | |
| 1919 | 1952 | server_hostname="") | |
@@ -1998,6 +2031,66 @@ def test_private_init(self): | |||
| 1998 | 2031 | with self.assertRaisesRegex(TypeError, "public constructor"): | |
| 1999 | 2032 | ssl.SSLObject(bio, bio) | |
| 2000 | 2033 | ||
| 2034 | + def test_check_hostname_requires_server_hostname(self): | ||
| 2035 | + # wrap_bio() used to accept a context asking for hostname checking | ||
| 2036 | + # without a name to check against, and then verify the certificate | ||
| 2037 | + # chain but never the peer's identity without a warning. Now | ||
| 2038 | + # a warning is emitted in this scenario. | ||
| 2039 | + client_context, _, hostname = testing_context() | ||
| 2040 | + self.assertTrue(client_context.check_hostname) | ||
| 2041 | + | ||
| 2042 | + server_hostname = None | ||
| 2043 | + with self.assertWarnsRegex( | ||
| 2044 | + DeprecationWarning, | ||
| 2045 | + "check_hostname requires server_hostname"): | ||
| 2046 | + client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), | ||
| 2047 | + server_hostname=server_hostname) | ||
| 2048 | + # The sibling constructor refuses the very same call, but with | ||
| 2049 | + # a ValueError instead of DeprecationWarning. | ||
| 2050 | + with socket.socket() as sock: | ||
| 2051 | + with self.assertRaisesRegex( | ||
| 2052 | + ValueError, | ||
| 2053 | + "check_hostname requires server_hostname"): | ||
| 2054 | + client_context.wrap_socket( | ||
| 2055 | + sock, server_hostname=server_hostname) | ||
| 2056 | + | ||
| 2057 | + # A name was all that was missing. | ||
| 2058 | + with warnings_helper.check_no_warnings(self): | ||
| 2059 | + client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), | ||
| 2060 | + server_hostname=hostname) | ||
| 2061 | + | ||
| 2062 | + # Asking for no hostname check remains a way to say so explicitly. | ||
| 2063 | + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||
| 2064 | + context.check_hostname = False | ||
| 2065 | + self.assertFalse(context.check_hostname) | ||
| 2066 | + with warnings_helper.check_no_warnings(self): | ||
| 2067 | + context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO()) | ||
| 2068 | + | ||
| 2069 | + def test_server_side_bad_params(self): | ||
| 2070 | + # A server neither sends a hostname nor resumes a client's session, | ||
| 2071 | + # so wrap_bio() rejects both in server mode like wrap_socket() | ||
| 2072 | + client_context, server_context, hostname = testing_context() | ||
| 2073 | + | ||
| 2074 | + with self.assertRaisesRegex( | ||
| 2075 | + ValueError, | ||
| 2076 | + "server_hostname can only be specified in client mode"): | ||
| 2077 | + server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), | ||
| 2078 | + server_side=True, | ||
| 2079 | + server_hostname=hostname) | ||
| 2080 | + | ||
| 2081 | + client, server, *_ = connected_bio_pair( | ||
| 2082 | + client_context, server_context, hostname) | ||
| 2083 | + session = client.session | ||
| 2084 | + self.assertIsNotNone(session) | ||
| 2085 | + with self.assertRaisesRegex( | ||
| 2086 | + ValueError, "session can only be specified in client mode"): | ||
| 2087 | + server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), | ||
| 2088 | + server_side=True, session=session) | ||
| 2089 | + | ||
| 2090 | + # Neither argument is what a server passes, so this still works. | ||
| 2091 | + server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), | ||
| 2092 | + server_side=True) | ||
| 2093 | + | ||
| 2001 | 2094 | def test_unwrap(self): | |
| 2002 | 2095 | client_ctx, server_ctx, hostname = testing_context() | |
| 2003 | 2096 | c_in = ssl.MemoryBIO() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,9 @@ | |||
| 1 | + :meth:`ssl.SSLContext.wrap_bio` now validates its *server_side*, | ||
| 2 | + *server_hostname* and *session* arguments similar to | ||
| 3 | + :meth:`ssl.SSLContext.wrap_socket`, but for backward compatiblity reasons | ||
| 4 | + emits :exc:`DeprecationWarning` instead of :exc:`ValueError`. | ||
| 5 | + | ||
| 6 | + In particular, a context with :attr:`~ssl.SSLContext.check_hostname` enabled | ||
| 7 | + and no *server_hostname* passed to :meth:`!wrap_bio` now emits | ||
| 8 | + :exc:`DeprecationWarning` to indicate the hostname wasn't checked. | ||
| 9 | + (In Python 3.13 and later, this raises :exc:`ValueError`.) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,6 @@ | |||
| 1 | + :mod:`asyncio`: :meth:`loop.start_tls() <asyncio.loop.start_tls>` and | ||
| 2 | + :meth:`loop.create_connection() <asyncio.loop.create_connection>` now | ||
| 3 | + validate the *server_hostname* argument if an :class:`ssl.SSLContext` is | ||
| 4 | + passed with *check_hostname* set to ``True``, emitting | ||
| 5 | + :exc:`DeprecationWarning` if *server_hostname* is missing. (This will raise | ||
| 6 | + :exc:`ValueError` in Python 3.13 and later.) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments