Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/library/asyncio-eventloop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,11 @@ Opening network connections

For more information: https://tools.ietf.org/html/rfc6555

.. versionchanged:: next
Raises a ``DeprecationWarning`` if ``ssl.check_hostname`` is ``True``
and ``server_hostname`` is not supplied. In Python 3.13 and
later a ``ValueError`` is raised instead.

.. seealso::

The :func:`open_connection` function is a high-level alternative
Expand Down
13 changes: 12 additions & 1 deletion Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1901,7 +1901,11 @@ to speed up repeated connections from the same clients.
outgoing BIO.

The *server_side*, *server_hostname* and *session* parameters have the
same meaning as in :meth:`SSLContext.wrap_socket`.
same meaning as in :meth:`SSLContext.wrap_socket`, and are validated in
the same way: in particular a :exc:`DeprecationWarning` is raised when
:attr:`~SSLContext.check_hostname` is enabled but no *server_hostname* is
given, since there would be no name to match the peer's certificate
against. In Python 3.13 and later a ``ValueError`` is raised instead.

.. versionchanged:: 3.6
*session* argument was added.
Expand All @@ -1910,6 +1914,13 @@ to speed up repeated connections from the same clients.
The method returns on instance of :attr:`SSLContext.sslobject_class`
instead of hard-coded :class:`SSLObject`.

.. versionchanged:: next
The *server_side*, *server_hostname* and *session* parameters are now
validated as :meth:`SSLContext.wrap_socket` validates them. Previously
a context with :attr:`~SSLContext.check_hostname` enabled and no
*server_hostname* was accepted, and verified the certificate chain but
never the peer's identity.

.. attribute:: SSLContext.sslobject_class

The return type of :meth:`SSLContext.wrap_bio`, defaults to
Expand Down
13 changes: 13 additions & 0 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,19 @@ def __init__(self, *args, **kwargs):
@classmethod
def _create(cls, incoming, outgoing, server_side=False,
server_hostname=None, session=None, context=None):
if server_side:
if server_hostname:
raise ValueError("server_hostname can only be specified "
"in client mode")
if session is not None:
raise ValueError("session can only be specified in "
"client mode")
if context.check_hostname and server_hostname is None:
# Note: server_hostname='' is handled within _wrap_bio().
warnings.warn("check_hostname requires server_hostname",
category=DeprecationWarning,
stacklevel=3)

self = cls.__new__(cls)
sslobj = context._wrap_bio(
incoming, outgoing, server_side=server_side,
Expand Down
57 changes: 57 additions & 0 deletions Lib/test/test_asyncio/test_sslproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,63 @@ def test_handshake_timeout_negative(self):
sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter,
ssl_handshake_timeout=-10)

def test_check_hostname_accepts_server_hostname(self):
# Supplying a server_hostname succeeds with check_hostname enabled.
sslcontext = test_utils.simple_client_sslcontext(disable_verify=False)
sslcontext.check_hostname = True
app_proto = mock.Mock()
waiter = mock.Mock()

# No ValueError is raised from SSLProtocol with 'server_hostname'.
ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter,
server_hostname='example.org')
self.addCleanup(ssl_proto.connection_lost, None)
ssl_proto.connection_made(mock.Mock())

def test_check_hostname_requires_server_hostname(self):
# A caller-supplied context asking for hostname checking used to be
# taken through wrap_bio() with no name to check against, verifying
# the certificate chain but never the peer's identity.
# loop.start_tls() defaults server_hostname to None, and
# loop.create_connection() turns server_hostname='' into None here,
# so both reached that state.
sslcontext = test_utils.simple_client_sslcontext(disable_verify=False)
sslcontext.check_hostname = True
app_proto = mock.Mock()
waiter = mock.Mock()
server_hostname = None

# Supplying no server_hostname warns with check_hostname enabled.
with self.assertWarnsRegex(
DeprecationWarning,
'check_hostname requires server_hostname'):
ssl_proto = sslproto.SSLProtocol(
self.loop, app_proto, sslcontext, waiter)
self.addCleanup(ssl_proto.connection_lost, None)
ssl_proto.connection_made(mock.Mock())

with self.assertWarnsRegex(
DeprecationWarning,
'check_hostname requires server_hostname'):
ssl_proto = sslproto.SSLProtocol(
self.loop, app_proto, sslcontext, waiter,
server_hostname=server_hostname)
self.addCleanup(ssl_proto.connection_lost, None)
ssl_proto.connection_made(mock.Mock())

# Disabling check_hostname allows for an empty or unset server_hostname.
sslcontext.check_hostname = False

ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext, waiter)
self.addCleanup(ssl_proto.connection_lost, None)
ssl_proto.connection_made(mock.Mock())

ssl_proto = sslproto.SSLProtocol(self.loop, app_proto, sslcontext,
waiter,
server_hostname=server_hostname)
self.addCleanup(ssl_proto.connection_lost, None)
ssl_proto.connection_made(mock.Mock())

def test_eof_received_waiter(self):
waiter = self.loop.create_future()
ssl_proto = self.ssl_protocol(waiter=waiter)
Expand Down
93 changes: 93 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from test.support import socket_helper
from test.support import threading_helper
from test.support import warnings_helper
import contextlib
import re
import socket
import select
Expand Down Expand Up @@ -328,6 +329,34 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
return client_context, server_context, hostname


def connected_bio_pair(client_context, server_context, hostname, max_retry=5):
"""Handshake a client and a server SSLObject against each other.

Everything happens in memory, so this needs no socket and no thread.
Returns the two objects followed by their four BIOs, in the order
client, server, c_in, c_out, s_in, s_out.
"""
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
client = client_context.wrap_bio(c_in, c_out, server_hostname=hostname)
server = server_context.wrap_bio(s_in, s_out, server_side=True)

# Loop on the handshake for a bit to get it settled
for _ in range(max_retry):
with contextlib.suppress(ssl.SSLWantReadError):
client.do_handshake()
if c_out.pending:
s_in.write(c_out.read())
with contextlib.suppress(ssl.SSLWantReadError):
server.do_handshake()
if s_out.pending:
c_in.write(s_out.read())
# Now the handshakes should be complete (don't raise WantReadError)
client.do_handshake()
server.do_handshake()
return client, server, c_in, c_out, s_in, s_out


class BasicSocketTests(unittest.TestCase):

def test_constants(self):
Expand Down Expand Up @@ -1888,6 +1917,10 @@ def test_subclass(self):

def test_bad_server_hostname(self):
ctx = ssl.create_default_context()
# Omitting the name entirely is bad too: this context checks it.
with self.assertWarns(DeprecationWarning):
ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_hostname=None)
with self.assertRaises(ValueError):
ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_hostname="")
Expand Down Expand Up @@ -1968,6 +2001,66 @@ def test_private_init(self):
with self.assertRaisesRegex(TypeError, "public constructor"):
ssl.SSLObject(bio, bio)

def test_check_hostname_requires_server_hostname(self):
# wrap_bio() used to accept a context asking for hostname checking
# without a name to check against, and then verify the certificate
# chain but never the peer's identity without a warning. Now
# a warning is emitted in this scenario.
client_context, _, hostname = testing_context()
self.assertTrue(client_context.check_hostname)

server_hostname = None
with self.assertWarnsRegex(
DeprecationWarning,
"check_hostname requires server_hostname"):
client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_hostname=server_hostname)
# The sibling constructor refuses the very same call, but with
# a ValueError instead of DeprecationWarning.
with socket.socket() as sock:
with self.assertRaisesRegex(
ValueError,
"check_hostname requires server_hostname"):
client_context.wrap_socket(
sock, server_hostname=server_hostname)

# A name was all that was missing.
with warnings_helper.check_no_warnings(self):
client_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_hostname=hostname)

# Asking for no hostname check remains a way to say so explicitly.
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
self.assertFalse(context.check_hostname)
with warnings_helper.check_no_warnings(self):
context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO())

def test_server_side_bad_params(self):
# A server neither sends a hostname nor resumes a client's session,
# so wrap_bio() rejects both in server mode like wrap_socket()
client_context, server_context, hostname = testing_context()

with self.assertRaisesRegex(
ValueError,
"server_hostname can only be specified in client mode"):
server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_side=True,
server_hostname=hostname)

client, server, *_ = connected_bio_pair(
client_context, server_context, hostname)
session = client.session
self.assertIsNotNone(session)
with self.assertRaisesRegex(
ValueError, "session can only be specified in client mode"):
server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_side=True, session=session)

# Neither argument is what a server passes, so this still works.
server_context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_side=True)

def test_unwrap(self):
client_ctx, server_ctx, hostname = testing_context()
c_in = ssl.MemoryBIO()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:meth:`ssl.SSLContext.wrap_bio` now validates its *server_side*,
*server_hostname* and *session* arguments similar to
:meth:`ssl.SSLContext.wrap_socket`, but for backward compatiblity reasons
emits :exc:`DeprecationWarning` instead of :exc:`ValueError`.

In particular, a context with :attr:`~ssl.SSLContext.check_hostname` enabled
and no *server_hostname* passed to :meth:`!wrap_bio` now emits
:exc:`DeprecationWarning` to indicate the hostname wasn't checked.
(In Python 3.13 and later, this raises :exc:`ValueError`.)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:mod:`asyncio`: :meth:`loop.start_tls() <asyncio.loop.start_tls>` and
:meth:`loop.create_connection() <asyncio.loop.create_connection>` now
validate the *server_hostname* argument if an :class:`ssl.SSLContext` is
passed with *check_hostname* set to ``True``, emitting
:exc:`DeprecationWarning` if *server_hostname* is missing. (This will raise
:exc:`ValueError` in Python 3.13 and later.)
Loading