Skip to content

[bpo-28414] In SSL module, store server_hostname as an A-label #3010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
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
6 changes: 6 additions & 0 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,12 @@ SSL sockets also have the following additional methods and attributes:

.. versionadded:: 3.2

.. versionchanged:: 3.7
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

The :class:`SSLSession` for this SSL connection. The session is available
Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,16 @@ can be set within the scope of a group.
``'^$'`` or ``(?=-)`` that matches an empty string.
(Contributed by Serhiy Storchaka in :issue:`25054`.)

ssl
---

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 in :issue:`28414`.)

string
------

Expand Down
15 changes: 15 additions & 0 deletions Lib/test/ssl-idn-ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICQDCCAamgAwIBAgIUBg5Z+vupJbrxjKmrYeJ6nb3xnQEwDQYJKoZIhvcNAQEL
BQAwQDEXMBUGA1UECgwOdHJ1c3RtZSB2MC40LjAxJTAjBgNVBAsMHFRlc3Rpbmcg
Q0EgI3QxM0tzY2dCQm8xQzZpTnUwIBcNMDAwMTAxMDAwMDAwWhgPMzAwMDAxMDEw
MDAwMDBaMEAxFzAVBgNVBAoMDnRydXN0bWUgdjAuNC4wMSUwIwYDVQQLDBxUZXN0
aW5nIENBICN0MTNLc2NnQkJvMUM2aU51MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDRWbKkz1+y31q1bu5P5J/XOjSwEac1ESG2G7W6hbYsVTn6OqqjXvebE3ex
+pNd/ciBHMv0SzlqKyo5l0BNLOjlth8C7j9LbUimddl4rpkpmtEuu4acwPT9pzts
jHxSPehJsF26ixReg8qi/E8Rsrri+3swFbI0pos6pQZo81HvjwIDAQABozUwMzAd
BgNVHQ4EFgQUYDMWkOTMwi9BwFK0blya/ou4r/YwEgYDVR0TAQH/BAgwBgEB/wIB
CTANBgkqhkiG9w0BAQsFAAOBgQC6BjPf9juAfJPNVqRX3qAWIf6wpOVWX1CO/Qtc
HSiCxxpTv2xAGX9ZAwK8liBKR4qGd0lDmujpKVLKAdsWlhFWJNgO3VyQgTkOYBzf
6fq2RE1oKXmqg2H7ndcku6TACNVCyFv4hbN4RISXO0Al/gR4h3lL9+05BXxT2eb5
dcUonA==
-----END CERTIFICATE-----
31 changes: 31 additions & 0 deletions Lib/test/ssl-idn-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCjbzNwZrxp50RAVF55jSw5M//KD5/kswwdpUePux00JS5WnHh3
SE98rnrWS34ryblBEMOEgIZPuYFjLs0fVN1XgmUHxs2cDPFUOpBrK2tf+nMDN0o9
AZG4V3e6wbwOKPyIybJyhkyCe1jj5oXjhrYTcDB0TIteAmtRkLU4nZlLrwIDAQAB
AoGAYhHyTfp4CRyLaga2gj3iUZkQXpGtorCGDqwFGwxu48GD4tkVuI4dlHWmpDy8
w03S6mZCzJnK/sAUEg4dbDWic1D9QyocWtpFFPJ3RyWKEuzN9Ka518dAzRtKya+9
oUovXCfCAiw3gwi2sO6QeADPbnScNgLlSiOZTOyTTJhydyECQQDPCnRxqXjLJjgT
Wpur6oSLiLmtitG1KsU80d7X2yqCnScTysw4IwYoOdq4BmiwSSnwV2Glha4pZKz1
trghZ9opAkEAyhT1vLNON4WDc6vpbWYCGFf3TaJRSqdd/hjaUmBeR9Wsa+bUccxS
6Jk2RcfP0dv8NT4ZY6bILNktCUr4V9liFwJBAJ+jTA2nwn/BRFOH9agk93YvQhvR
gcjS5anzmIOPdcOoMM1N/RD70G+LzF1Ac9AZWcD7X0slPBimi8YZ0PfQ/6ECQQCU
hAT6EvlIGsq6Jz0d1ptxkqzBFKsT559PkKpbYlHID4RxpKq7m8PPCFL3w9q7TCa2
ZpY4Q6nYNCBCNSQBRFUvAkBhGZjMj25DqZO3vsW2LqwlBVk+hIih0LrLHThCuzT1
rKdC2AdCvu3FZCbNg0lpjiYXEYCXvCP4c5TqS3H8uaC8
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICjTCCAfagAwIBAgIUHg5Af4BmKSy4Xyot/yAGeSkO+PswDQYJKoZIhvcNAQEL
BQAwQDEXMBUGA1UECgwOdHJ1c3RtZSB2MC40LjAxJTAjBgNVBAsMHFRlc3Rpbmcg
Q0EgI3QxM0tzY2dCQm8xQzZpTnUwIBcNMDAwMTAxMDAwMDAwWhgPMzAwMDAxMDEw
MDAwMDBaMEkxFzAVBgNVBAoMDnRydXN0bWUgdjAuNC4wMS4wLAYDVQQLDCVUZXN0
aW5nIHNlcnZlciBjZXJ0ICNaRlFmTEh1MDZaYU56UFYxMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQCjbzNwZrxp50RAVF55jSw5M//KD5/kswwdpUePux00JS5W
nHh3SE98rnrWS34ryblBEMOEgIZPuYFjLs0fVN1XgmUHxs2cDPFUOpBrK2tf+nMD
N0o9AZG4V3e6wbwOKPyIybJyhkyCe1jj5oXjhrYTcDB0TIteAmtRkLU4nZlLrwID
AQABo3kwdzAdBgNVHQ4EFgQU1BZqNqO4rmUwk8V015AXCb8+keQwDAYDVR0TAQH/
BAIwADAfBgNVHSMEGDAWgBRgMxaQ5MzCL0HAUrRuXJr+i7iv9jAnBgNVHREBAf8E
HTAbghl4bi0tcHl0aG4tbXVhLmV4YW1wbGUub3JnMA0GCSqGSIb3DQEBCwUAA4GB
ABJ4tUqfj9gHEYGxousPf7HSX/ZHF8e9HW+qpOX/urPRdGM0ObYrUlPgKJ1NIlA2
HSOPWGVQgvk6P84s0oBYLAJ0C2CrKg2AQsusFn9s8dAM9hlYNEK9rfTQILxrnCyz
vpg6hKEGXN0UjYPb5HBPFKsWF0DbbNaWrr0co32yH2L8
-----END CERTIFICATE-----
52 changes: 42 additions & 10 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ def data_file(*name):
DHFILE = data_file("dh1024.pem")
BYTES_DHFILE = os.fsencode(DHFILE)

# These were generated by doing 'pip install trustme', and then:
# import trustme
# ca = trustme.CA()
# cert = ca.issue_server_cert("pythön.example.org")
# ca.cert_pem.write_to_path("ssl-idn-ca.pem")
# cert.private_key_and_cert_chain_pem.write_to_path("ssl-idn-cert.pem")
IDN_CA = data_file("ssl-idn-ca.pem")
IDN_CERT = data_file("ssl-idn-cert.pem")

# Not defined in all versions of OpenSSL
OP_NO_COMPRESSION = getattr(ssl, "OP_NO_COMPRESSION", 0)
OP_SINGLE_DH_USE = getattr(ssl, "OP_SINGLE_DH_USE", 0)
Expand Down Expand Up @@ -1474,16 +1483,6 @@ def test_subclass(self):
# For compatibility
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")

class MemoryBIOTests(unittest.TestCase):

Expand Down Expand Up @@ -2522,6 +2521,39 @@ def test_check_hostname(self):
"check_hostname requires server_hostname"):
client_context.wrap_socket(s)

def test_check_hostname_idn(self):
if support.verbose:
sys.stdout.write("\n")

server_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
server_context.load_cert_chain(IDN_CERT)

context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
context.load_verify_locations(IDN_CA)

# correct hostname should verify, when specified in several
# different ways
for server_hostname in ["pythön.example.org",
"xn--pythn-mua.example.org",
b"xn--pythn-mua.example.org"]:
server = ThreadedEchoServer(context=server_context, chatty=True)
with server:
with context.wrap_socket(socket.socket(),
server_hostname=server_hostname) as s:
s.connect((HOST, server.port))
cert = s.getpeercert()
self.assertTrue(cert, "Can't get peer certificate.")

# incorrect hostname should raise an exception
server = ThreadedEchoServer(context=server_context, chatty=True)
with server:
with context.wrap_socket(socket.socket(),
server_hostname="python.example.org") as s:
with self.assertRaises(ssl.CertificateError):
s.connect((HOST, server.port))

def test_wrong_cert(self):
"""Connecting when the server rejects the client's certificate

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The ssl module can now validate hostnames that contain non-ASCII
characters (IDNs).
5 changes: 4 additions & 1 deletion Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,11 @@ newPySSLSocket(PySSLContext *sslctx, PySocketSockObject *sock,
self->owner = NULL;
self->server_hostname = NULL;
if (server_hostname != NULL) {
/* server_hostname was encoded to an A-label by our caller; put it
* back into a str object, but still as an A-label (bpo-28414)
*/
PyObject *hostname = PyUnicode_Decode(server_hostname, strlen(server_hostname),
"idna", "strict");
"ascii", "strict");
if (hostname == NULL) {
Py_DECREF(self);
return NULL;
Expand Down