Skip to content

Commit 1ec0678

Browse files
committed
Fix IDN SAN handling
Signed-off-by: Christian Heimes <christian@python.org>
1 parent f985559 commit 1ec0678

File tree

5 files changed

+153
-110
lines changed

5 files changed

+153
-110
lines changed

Doc/library/ssl.rst

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,9 +1269,9 @@ SSL sockets also have the following additional methods and attributes:
12691269
.. versionadded:: 3.2
12701270

12711271
.. versionchanged:: 3.7
1272-
When ``server_hostname`` is an internationalized domain name
1273-
(IDN), this attribute now stores the A-label form
1274-
(``"xn--pythn-mua.org"``), rather than the U-label form
1272+
The attribute is now always ASCII text. When ``server_hostname`` is
1273+
an internationalized domain name (IDN), this attribute now stores the
1274+
A-label form (``"xn--pythn-mua.org"``), rather than the U-label form
12751275
(``"pythön.org"``).
12761276

12771277
.. attribute:: SSLSocket.session
@@ -1538,23 +1538,34 @@ to speed up repeated connections from the same clients.
15381538

15391539
.. versionadded:: 3.3
15401540

1541-
.. method:: SSLContext.set_servername_callback(server_name_callback)
1541+
.. attribute:: SSLContext.set_servername_callback(server_name_callback)
1542+
1543+
See :attr:`SSLContext.sni_callback`
1544+
1545+
If there is an decoding error on the server name, the TLS connection will
1546+
terminate with an :const:`ALERT_DESCRIPTION_INTERNAL_ERROR` fatal TLS
1547+
alert message to the client.
1548+
1549+
.. versionadded:: 3.4
1550+
1551+
.. attribute:: SSLContext.sni_callback
15421552

15431553
Register a callback function that will be called after the TLS Client Hello
15441554
handshake message has been received by the SSL/TLS server when the TLS client
15451555
specifies a server name indication. The server name indication mechanism
15461556
is specified in :rfc:`6066` section 3 - Server Name Indication.
15471557

1548-
Only one callback can be set per ``SSLContext``. If *server_name_callback*
1549-
is ``None`` then the callback is disabled. Calling this function a
1558+
Only one callback can be set per ``SSLContext``. If *sni_callback*
1559+
is set to ``None`` then the callback is disabled. Calling this function a
15501560
subsequent time will disable the previously registered callback.
15511561

1552-
The callback function, *server_name_callback*, will be called with three
1562+
The callback function, will be called with three
15531563
arguments; the first being the :class:`ssl.SSLSocket`, the second is a string
15541564
that represents the server name that the client is intending to communicate
15551565
(or :const:`None` if the TLS Client Hello does not contain a server name)
15561566
and the third argument is the original :class:`SSLContext`. The server name
1557-
argument is the IDNA decoded server name.
1567+
argument is text. For internationalized domain name, the server
1568+
name is an IDN A-label (``"xn--pythn-mua.org"``).
15581569

15591570
A typical use of this callback is to change the :class:`ssl.SSLSocket`'s
15601571
:attr:`SSLSocket.context` attribute to a new object of type
@@ -1575,18 +1586,14 @@ to speed up repeated connections from the same clients.
15751586
returned. Other return values will result in a TLS fatal error with
15761587
:const:`ALERT_DESCRIPTION_INTERNAL_ERROR`.
15771588

1578-
If there is an IDNA decoding error on the server name, the TLS connection
1579-
will terminate with an :const:`ALERT_DESCRIPTION_INTERNAL_ERROR` fatal TLS
1580-
alert message to the client.
1581-
15821589
If an exception is raised from the *server_name_callback* function the TLS
15831590
connection will terminate with a fatal TLS alert message
15841591
:const:`ALERT_DESCRIPTION_HANDSHAKE_FAILURE`.
15851592

15861593
This method will raise :exc:`NotImplementedError` if the OpenSSL library
15871594
had OPENSSL_NO_TLSEXT defined when it was built.
15881595

1589-
.. versionadded:: 3.4
1596+
.. versionadded:: 3.7
15901597

15911598
.. method:: SSLContext.load_dh_params(dhfile)
15921599

Lib/ssl.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -355,13 +355,20 @@ def __new__(cls, protocol=PROTOCOL_TLS, *args, **kwargs):
355355
self = _SSLContext.__new__(cls, protocol)
356356
return self
357357

358-
def __init__(self, protocol=PROTOCOL_TLS):
359-
self.protocol = protocol
358+
def _encode_hostname(self, hostname):
359+
if hostname is None:
360+
return None
361+
elif isinstance(hostname, str):
362+
return hostname.encode('idna').decode('ascii')
363+
else:
364+
return hostname.decode('ascii')
360365

361366
def wrap_socket(self, sock, server_side=False,
362367
do_handshake_on_connect=True,
363368
suppress_ragged_eofs=True,
364369
server_hostname=None, session=None):
370+
# SSLSocket class handles server_hostname encoding before it calls
371+
# ctx._wrap_socket()
365372
return self.sslsocket_class(
366373
sock=sock,
367374
server_side=server_side,
@@ -374,8 +381,12 @@ def wrap_socket(self, sock, server_side=False,
374381

375382
def wrap_bio(self, incoming, outgoing, server_side=False,
376383
server_hostname=None, session=None):
377-
sslobj = self._wrap_bio(incoming, outgoing, server_side=server_side,
378-
server_hostname=server_hostname)
384+
# Need to encode server_hostname here because _wrap_bio() can only
385+
# handle ASCII str.
386+
sslobj = self._wrap_bio(
387+
incoming, outgoing, server_side=server_side,
388+
server_hostname=self._encode_hostname(server_hostname)
389+
)
379390
return self.sslobject_class(sslobj, session=session)
380391

381392
def set_npn_protocols(self, npn_protocols):
@@ -389,6 +400,20 @@ def set_npn_protocols(self, npn_protocols):
389400

390401
self._set_npn_protocols(protos)
391402

403+
def set_servername_callback(self, server_name_callback):
404+
if server_name_callback is None:
405+
self.sni_callback = None
406+
else:
407+
if not hasattr(server_name_callback, '__call__'):
408+
raise TypeError("not a callable object")
409+
410+
def shim_cb(sslobj, servername, sslctx):
411+
if servername is not None:
412+
servername = servername.encode("ascii").decode("idna")
413+
return server_name_callback(sslobj, servername, sslctx)
414+
415+
self.sni_callback = shim_cb
416+
392417
def set_alpn_protocols(self, alpn_protocols):
393418
protos = bytearray()
394419
for protocol in alpn_protocols:
@@ -447,6 +472,10 @@ def hostname_checks_common_name(self, value):
447472
def hostname_checks_common_name(self):
448473
return True
449474

475+
@property
476+
def protocol(self):
477+
return _SSLMethod(super().protocol)
478+
450479
@property
451480
def verify_flags(self):
452481
return VerifyFlags(super().verify_flags)
@@ -749,7 +778,7 @@ def __init__(self, sock=None, keyfile=None, certfile=None,
749778
raise ValueError("check_hostname requires server_hostname")
750779
self._session = _session
751780
self.server_side = server_side
752-
self.server_hostname = server_hostname
781+
self.server_hostname = self._context._encode_hostname(server_hostname)
753782
self.do_handshake_on_connect = do_handshake_on_connect
754783
self.suppress_ragged_eofs = suppress_ragged_eofs
755784
if sock is not None:
@@ -781,7 +810,7 @@ def __init__(self, sock=None, keyfile=None, certfile=None,
781810
# create the SSL object
782811
try:
783812
sslobj = self._context._wrap_socket(self, server_side,
784-
server_hostname)
813+
self.server_hostname)
785814
self._sslobj = SSLObject(sslobj, owner=self,
786815
session=self._session)
787816
if do_handshake_on_connect:

Lib/test/test_ssl.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,6 @@ def data_file(*name):
119119
DHFILE = data_file("dh1024.pem")
120120
BYTES_DHFILE = os.fsencode(DHFILE)
121121

122-
# These were generated by doing 'pip install trustme', and then:
123-
# import trustme
124-
# ca = trustme.CA()
125-
# cert = ca.issue_server_cert("pythön.example.org")
126-
# ca.cert_pem.write_to_path("ssl-idn-ca.pem")
127-
# cert.private_key_and_cert_chain_pem.write_to_path("ssl-idn-cert.pem")
128-
IDN_CA = data_file("ssl-idn-ca.pem")
129-
IDN_CERT = data_file("ssl-idn-cert.pem")
130-
131122
# Not defined in all versions of OpenSSL
132123
OP_NO_COMPRESSION = getattr(ssl, "OP_NO_COMPRESSION", 0)
133124
OP_SINGLE_DH_USE = getattr(ssl, "OP_SINGLE_DH_USE", 0)
@@ -2634,7 +2625,7 @@ def test_check_hostname_idn(self):
26342625
sys.stdout.write("\n")
26352626

26362627
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2637-
server_context.load_cert_chain(IDN_CERT)
2628+
server_context.load_cert_chain(IDNSANSFILE)
26382629

26392630
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
26402631
context.verify_mode = ssl.CERT_REQUIRED
@@ -2645,18 +2636,26 @@ def test_check_hostname_idn(self):
26452636
# different ways
26462637
idn_hostnames = [
26472638
('könig.idn.pythontest.net',
2648-
'könig.idn.pythontest.net',),
2639+
'xn--knig-5qa.idn.pythontest.net'),
26492640
('xn--knig-5qa.idn.pythontest.net',
26502641
'xn--knig-5qa.idn.pythontest.net'),
26512642
(b'xn--knig-5qa.idn.pythontest.net',
2652-
b'xn--knig-5qa.idn.pythontest.net'),
2643+
'xn--knig-5qa.idn.pythontest.net'),
26532644

26542645
('königsgäßchen.idna2003.pythontest.net',
2655-
'königsgäßchen.idna2003.pythontest.net'),
2646+
'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'),
26562647
('xn--knigsgsschen-lcb0w.idna2003.pythontest.net',
26572648
'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'),
26582649
(b'xn--knigsgsschen-lcb0w.idna2003.pythontest.net',
2659-
b'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'),
2650+
'xn--knigsgsschen-lcb0w.idna2003.pythontest.net'),
2651+
2652+
# ('königsgäßchen.idna2008.pythontest.net',
2653+
# 'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'),
2654+
('xn--knigsgchen-b4a3dun.idna2008.pythontest.net',
2655+
'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'),
2656+
(b'xn--knigsgchen-b4a3dun.idna2008.pythontest.net',
2657+
'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'),
2658+
26602659
]
26612660
for server_hostname, expected_hostname in idn_hostnames:
26622661
server = ThreadedEchoServer(context=server_context, chatty=True)
@@ -2675,16 +2674,6 @@ def test_check_hostname_idn(self):
26752674
s.getpeercert()
26762675
self.assertEqual(s.server_hostname, expected_hostname)
26772676

2678-
# bug https://bugs.python.org/issue28414
2679-
# IDNA 2008 deviations are broken
2680-
idna2008 = 'xn--knigsgchen-b4a3dun.idna2008.pythontest.net'
2681-
server = ThreadedEchoServer(context=server_context, chatty=True)
2682-
with server:
2683-
with self.assertRaises(UnicodeError):
2684-
with context.wrap_socket(socket.socket(),
2685-
server_hostname=idna2008) as s:
2686-
s.connect((HOST, server.port))
2687-
26882677
# incorrect hostname should raise an exception
26892678
server = ThreadedEchoServer(context=server_context, chatty=True)
26902679
with server:

0 commit comments

Comments
 (0)