Commit 11a1493b authored by Christian Heimes's avatar Christian Heimes Committed by Nathaniel J. Smith

[bpo-28414] Make all hostnames in SSL module IDN A-labels (GH-5128)

Previously, the ssl module stored international domain names (IDNs)
as U-labels. This is problematic for a number of reasons -- for
example, it made it impossible for users to use a different version
of IDNA than the one built into Python.

After this change, we always convert to A-labels as soon as possible,
and use them for all internal processing. In particular, server_hostname
attribute is now an A-label, and on the server side there's a new
sni_callback that receives the SNI servername as an A-label rather than
a U-label.
parent 82ab13d7
...@@ -1268,6 +1268,12 @@ SSL sockets also have the following additional methods and attributes: ...@@ -1268,6 +1268,12 @@ SSL sockets also have the following additional methods and attributes:
.. versionadded:: 3.2 .. versionadded:: 3.2
.. versionchanged:: 3.7
The attribute is now always ASCII text. When ``server_hostname`` is
an internationalized domain name (IDN), this attribute now stores the
A-label form (``"xn--pythn-mua.org"``), rather than the U-label form
(``"pythön.org"``).
.. attribute:: SSLSocket.session .. attribute:: SSLSocket.session
The :class:`SSLSession` for this SSL connection. The session is available The :class:`SSLSession` for this SSL connection. The session is available
...@@ -1532,23 +1538,24 @@ to speed up repeated connections from the same clients. ...@@ -1532,23 +1538,24 @@ to speed up repeated connections from the same clients.
.. versionadded:: 3.3 .. versionadded:: 3.3
.. method:: SSLContext.set_servername_callback(server_name_callback) .. attribute:: SSLContext.sni_callback
Register a callback function that will be called after the TLS Client Hello Register a callback function that will be called after the TLS Client Hello
handshake message has been received by the SSL/TLS server when the TLS client handshake message has been received by the SSL/TLS server when the TLS client
specifies a server name indication. The server name indication mechanism specifies a server name indication. The server name indication mechanism
is specified in :rfc:`6066` section 3 - Server Name Indication. is specified in :rfc:`6066` section 3 - Server Name Indication.
Only one callback can be set per ``SSLContext``. If *server_name_callback* Only one callback can be set per ``SSLContext``. If *sni_callback*
is ``None`` then the callback is disabled. Calling this function a is set to ``None`` then the callback is disabled. Calling this function a
subsequent time will disable the previously registered callback. subsequent time will disable the previously registered callback.
The callback function, *server_name_callback*, will be called with three The callback function will be called with three
arguments; the first being the :class:`ssl.SSLSocket`, the second is a string arguments; the first being the :class:`ssl.SSLSocket`, the second is a string
that represents the server name that the client is intending to communicate that represents the server name that the client is intending to communicate
(or :const:`None` if the TLS Client Hello does not contain a server name) (or :const:`None` if the TLS Client Hello does not contain a server name)
and the third argument is the original :class:`SSLContext`. The server name and the third argument is the original :class:`SSLContext`. The server name
argument is the IDNA decoded server name. argument is text. For internationalized domain name, the server
name is an IDN A-label (``"xn--pythn-mua.org"``).
A typical use of this callback is to change the :class:`ssl.SSLSocket`'s A typical use of this callback is to change the :class:`ssl.SSLSocket`'s
:attr:`SSLSocket.context` attribute to a new object of type :attr:`SSLSocket.context` attribute to a new object of type
...@@ -1563,23 +1570,33 @@ to speed up repeated connections from the same clients. ...@@ -1563,23 +1570,33 @@ to speed up repeated connections from the same clients.
the TLS connection has progressed beyond the TLS Client Hello and therefore the TLS connection has progressed beyond the TLS Client Hello and therefore
will not contain return meaningful values nor can they be called safely. will not contain return meaningful values nor can they be called safely.
The *server_name_callback* function must return ``None`` to allow the The *sni_callback* function must return ``None`` to allow the
TLS negotiation to continue. If a TLS failure is required, a constant TLS negotiation to continue. If a TLS failure is required, a constant
:const:`ALERT_DESCRIPTION_* <ALERT_DESCRIPTION_INTERNAL_ERROR>` can be :const:`ALERT_DESCRIPTION_* <ALERT_DESCRIPTION_INTERNAL_ERROR>` can be
returned. Other return values will result in a TLS fatal error with returned. Other return values will result in a TLS fatal error with
:const:`ALERT_DESCRIPTION_INTERNAL_ERROR`. :const:`ALERT_DESCRIPTION_INTERNAL_ERROR`.
If there is an IDNA decoding error on the server name, the TLS connection If an exception is raised from the *sni_callback* function the TLS
will terminate with an :const:`ALERT_DESCRIPTION_INTERNAL_ERROR` fatal TLS
alert message to the client.
If an exception is raised from the *server_name_callback* function the TLS
connection will terminate with a fatal TLS alert message connection will terminate with a fatal TLS alert message
:const:`ALERT_DESCRIPTION_HANDSHAKE_FAILURE`. :const:`ALERT_DESCRIPTION_HANDSHAKE_FAILURE`.
This method will raise :exc:`NotImplementedError` if the OpenSSL library This method will raise :exc:`NotImplementedError` if the OpenSSL library
had OPENSSL_NO_TLSEXT defined when it was built. had OPENSSL_NO_TLSEXT defined when it was built.
.. versionadded:: 3.7
.. attribute:: SSLContext.set_servername_callback(server_name_callback)
This is a legacy API retained for backwards compatibility. When possible,
you should use :attr:`sni_callback` instead. The given *server_name_callback*
is similar to *sni_callback*, except that when the server hostname is an
IDN-encoded internationalized domain name, the *server_name_callback*
receives a decoded U-label (``"pythön.org"``).
If there is an decoding error on the server name, the TLS connection will
terminate with an :const:`ALERT_DESCRIPTION_INTERNAL_ERROR` fatal TLS
alert message to the client.
.. versionadded:: 3.4 .. versionadded:: 3.4
.. method:: SSLContext.load_dh_params(dhfile) .. method:: SSLContext.load_dh_params(dhfile)
......
...@@ -662,6 +662,14 @@ ciphers that have been blocked by OpenSSL security update. Default cipher ...@@ -662,6 +662,14 @@ ciphers that have been blocked by OpenSSL security update. Default cipher
suite selection can be configured on compile time. suite selection can be configured on compile time.
(Contributed by Christian Heimes in :issue:`31429`.) (Contributed by Christian Heimes in :issue:`31429`.)
Added support for validating server certificates containing
internationalized domain names (IDNs). As part of this change, the
:attr:`ssl.SSLSocket.server_hostname` attribute now stores the
expected hostname in A-label form (``"xn--pythn-mua.org"``), rather
than the U-label form (``"pythön.org"``). (Contributed by
Nathaniel J. Smith and Christian Heimes in :issue:`28414`.)
string string
------ ------
......
...@@ -355,13 +355,20 @@ class SSLContext(_SSLContext): ...@@ -355,13 +355,20 @@ class SSLContext(_SSLContext):
self = _SSLContext.__new__(cls, protocol) self = _SSLContext.__new__(cls, protocol)
return self return self
def __init__(self, protocol=PROTOCOL_TLS): def _encode_hostname(self, hostname):
self.protocol = protocol if hostname is None:
return None
elif isinstance(hostname, str):
return hostname.encode('idna').decode('ascii')
else:
return hostname.decode('ascii')
def wrap_socket(self, sock, server_side=False, def wrap_socket(self, sock, server_side=False,
do_handshake_on_connect=True, do_handshake_on_connect=True,
suppress_ragged_eofs=True, suppress_ragged_eofs=True,
server_hostname=None, session=None): server_hostname=None, session=None):
# SSLSocket class handles server_hostname encoding before it calls
# ctx._wrap_socket()
return self.sslsocket_class( return self.sslsocket_class(
sock=sock, sock=sock,
server_side=server_side, server_side=server_side,
...@@ -374,8 +381,12 @@ class SSLContext(_SSLContext): ...@@ -374,8 +381,12 @@ class SSLContext(_SSLContext):
def wrap_bio(self, incoming, outgoing, server_side=False, def wrap_bio(self, incoming, outgoing, server_side=False,
server_hostname=None, session=None): server_hostname=None, session=None):
sslobj = self._wrap_bio(incoming, outgoing, server_side=server_side, # Need to encode server_hostname here because _wrap_bio() can only
server_hostname=server_hostname) # handle ASCII str.
sslobj = self._wrap_bio(
incoming, outgoing, server_side=server_side,
server_hostname=self._encode_hostname(server_hostname)
)
return self.sslobject_class(sslobj, session=session) return self.sslobject_class(sslobj, session=session)
def set_npn_protocols(self, npn_protocols): def set_npn_protocols(self, npn_protocols):
...@@ -389,6 +400,19 @@ class SSLContext(_SSLContext): ...@@ -389,6 +400,19 @@ class SSLContext(_SSLContext):
self._set_npn_protocols(protos) self._set_npn_protocols(protos)
def set_servername_callback(self, server_name_callback):
if server_name_callback is None:
self.sni_callback = None
else:
if not callable(server_name_callback):
raise TypeError("not a callable object")
def shim_cb(sslobj, servername, sslctx):
servername = self._encode_hostname(servername)
return server_name_callback(sslobj, servername, sslctx)
self.sni_callback = shim_cb
def set_alpn_protocols(self, alpn_protocols): def set_alpn_protocols(self, alpn_protocols):
protos = bytearray() protos = bytearray()
for protocol in alpn_protocols: for protocol in alpn_protocols:
...@@ -447,6 +471,10 @@ class SSLContext(_SSLContext): ...@@ -447,6 +471,10 @@ class SSLContext(_SSLContext):
def hostname_checks_common_name(self): def hostname_checks_common_name(self):
return True return True
@property
def protocol(self):
return _SSLMethod(super().protocol)
@property @property
def verify_flags(self): def verify_flags(self):
return VerifyFlags(super().verify_flags) return VerifyFlags(super().verify_flags)
...@@ -749,7 +777,7 @@ class SSLSocket(socket): ...@@ -749,7 +777,7 @@ class SSLSocket(socket):
raise ValueError("check_hostname requires server_hostname") raise ValueError("check_hostname requires server_hostname")
self._session = _session self._session = _session
self.server_side = server_side self.server_side = server_side
self.server_hostname = server_hostname self.server_hostname = self._context._encode_hostname(server_hostname)
self.do_handshake_on_connect = do_handshake_on_connect self.do_handshake_on_connect = do_handshake_on_connect
self.suppress_ragged_eofs = suppress_ragged_eofs self.suppress_ragged_eofs = suppress_ragged_eofs
if sock is not None: if sock is not None:
...@@ -781,7 +809,7 @@ class SSLSocket(socket): ...@@ -781,7 +809,7 @@ class SSLSocket(socket):
# create the SSL object # create the SSL object
try: try:
sslobj = self._context._wrap_socket(self, server_side, sslobj = self._context._wrap_socket(self, server_side,
server_hostname) self.server_hostname)
self._sslobj = SSLObject(sslobj, owner=self, self._sslobj = SSLObject(sslobj, owner=self,
session=self._session) session=self._session)
if do_handshake_on_connect: if do_handshake_on_connect:
......
...@@ -1528,16 +1528,6 @@ class SSLErrorTests(unittest.TestCase): ...@@ -1528,16 +1528,6 @@ class SSLErrorTests(unittest.TestCase):
# For compatibility # For compatibility
self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_WANT_READ) self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_WANT_READ)
def test_bad_idna_in_server_hostname(self):
# Note: this test is testing some code that probably shouldn't exist
# in the first place, so if it starts failing at some point because
# you made the ssl module stop doing IDNA decoding then please feel
# free to remove it. The test was mainly added because this case used
# to cause memory corruption (see bpo-30594).
ctx = ssl.create_default_context()
with self.assertRaises(UnicodeError):
ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(),
server_hostname="xn--.com")
def test_bad_server_hostname(self): def test_bad_server_hostname(self):
ctx = ssl.create_default_context() ctx = ssl.create_default_context()
...@@ -2634,10 +2624,10 @@ class ThreadedTests(unittest.TestCase): ...@@ -2634,10 +2624,10 @@ class ThreadedTests(unittest.TestCase):
if support.verbose: if support.verbose:
sys.stdout.write("\n") sys.stdout.write("\n")
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS) server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(IDNSANSFILE) server_context.load_cert_chain(IDNSANSFILE)
context = ssl.SSLContext(ssl.PROTOCOL_TLS) context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.verify_mode = ssl.CERT_REQUIRED context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True context.check_hostname = True
context.load_verify_locations(SIGNING_CA) context.load_verify_locations(SIGNING_CA)
...@@ -2646,18 +2636,26 @@ class ThreadedTests(unittest.TestCase): ...@@ -2646,18 +2636,26 @@ class ThreadedTests(unittest.TestCase):
# different ways # different ways
idn_hostnames = [ idn_hostnames = [
('könig.idn.pythontest.net', ('könig.idn.pythontest.net',
'könig.idn.pythontest.net',), 'xn--knig-5qa.idn.pythontest.net'),
('xn--knig-5qa.idn.pythontest.net', ('xn--knig-5qa.idn.pythontest.net',
'xn--knig-5qa.idn.pythontest.net'), 'xn--knig-5qa.idn.pythontest.net'),
(b'xn--knig-5qa.idn.pythontest.net', (b'xn--knig-5qa.idn.pythontest.net',
b'xn--knig-5qa.idn.pythontest.net'), 'xn--knig-5qa.idn.pythontest.net'),
('königsgäßchen.idna2003.pythontest.net', ('königsgäßchen.idna2003.pythontest.net',
'königsgäßchen.idna2003.pythontest.net'), 'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'),
('xn--knigsgsschen-lcb0w.idna2003.pythontest.net', ('xn--knigsgsschen-lcb0w.idna2003.pythontest.net',
'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'), 'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'),
(b'xn--knigsgsschen-lcb0w.idna2003.pythontest.net', (b'xn--knigsgsschen-lcb0w.idna2003.pythontest.net',
b'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'), 'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'),
# ('königsgäßchen.idna2008.pythontest.net',
# 'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'),
('xn--knigsgchen-b4a3dun.idna2008.pythontest.net',
'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'),
(b'xn--knigsgchen-b4a3dun.idna2008.pythontest.net',
'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'),
] ]
for server_hostname, expected_hostname in idn_hostnames: for server_hostname, expected_hostname in idn_hostnames:
server = ThreadedEchoServer(context=server_context, chatty=True) server = ThreadedEchoServer(context=server_context, chatty=True)
...@@ -2676,16 +2674,6 @@ class ThreadedTests(unittest.TestCase): ...@@ -2676,16 +2674,6 @@ class ThreadedTests(unittest.TestCase):
s.getpeercert() s.getpeercert()
self.assertEqual(s.server_hostname, expected_hostname) self.assertEqual(s.server_hostname, expected_hostname)
# bug https://bugs.python.org/issue28414
# IDNA 2008 deviations are broken
idna2008 = 'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'
server = ThreadedEchoServer(context=server_context, chatty=True)
with server:
with self.assertRaises(UnicodeError):
with context.wrap_socket(socket.socket(),
server_hostname=idna2008) as s:
s.connect((HOST, server.port))
# incorrect hostname should raise an exception # incorrect hostname should raise an exception
server = ThreadedEchoServer(context=server_context, chatty=True) server = ThreadedEchoServer(context=server_context, chatty=True)
with server: with server:
......
The ssl module now allows users to perform their own IDN en/decoding when using SNI.
This diff is collapsed.
...@@ -650,19 +650,6 @@ PyDoc_STRVAR(_ssl__SSLContext_set_ecdh_curve__doc__, ...@@ -650,19 +650,6 @@ PyDoc_STRVAR(_ssl__SSLContext_set_ecdh_curve__doc__,
#endif /* !defined(OPENSSL_NO_ECDH) */ #endif /* !defined(OPENSSL_NO_ECDH) */
PyDoc_STRVAR(_ssl__SSLContext_set_servername_callback__doc__,
"set_servername_callback($self, method, /)\n"
"--\n"
"\n"
"Set a callback that will be called when a server name is provided by the SSL/TLS client in the SNI extension.\n"
"\n"
"If the argument is None then the callback is disabled. The method is called\n"
"with the SSLSocket, the server name as a string, and the SSLContext object.\n"
"See RFC 6066 for details of the SNI extension.");
#define _SSL__SSLCONTEXT_SET_SERVERNAME_CALLBACK_METHODDEF \
{"set_servername_callback", (PyCFunction)_ssl__SSLContext_set_servername_callback, METH_O, _ssl__SSLContext_set_servername_callback__doc__},
PyDoc_STRVAR(_ssl__SSLContext_cert_store_stats__doc__, PyDoc_STRVAR(_ssl__SSLContext_cert_store_stats__doc__,
"cert_store_stats($self, /)\n" "cert_store_stats($self, /)\n"
"--\n" "--\n"
...@@ -1168,4 +1155,4 @@ exit: ...@@ -1168,4 +1155,4 @@ exit:
#ifndef _SSL_ENUM_CRLS_METHODDEF #ifndef _SSL_ENUM_CRLS_METHODDEF
#define _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF
#endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */
/*[clinic end generated code: output=3d42305ed0ad162a input=a9049054013a1b77]*/ /*[clinic end generated code: output=84e1fd89aff9b0f7 input=a9049054013a1b77]*/
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment