From b1163645d6e905bb684090690141c2ee2b507cf2 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 6 Aug 2017 14:23:43 -0700 Subject: [PATCH 1/6] [bpo-28414] In SSL module, store server_hostname as an A-label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Historically, our handling of international domain names (IDNs) in the ssl module has been very broken. The flow went like: 1. User passes server_hostname= to the SSLSocket/SSLObject constructor. This gets normalized to an A-label by using the PyArg_Parse "et" mode: bytes objects get passed through unchanged (assumed to already be A-labels); str objects get run through .encode("idna") to convert them into A-labels. 2. newPySSLSocket takes this A-label, and for some reason decodes it *back* to a U-label, and stores that as the object's server_hostname attribute. 3. Later, this U-label server_hostname attribute gets passed to match_hostname, to compare against the hostname seen in the certificate. But certificates contain A-labels, and match_hostname expects to be passed an A-label, so this doesn't work at all. This PR fixes the problem by removing the pointless decoding at step 2, so that internally we always use A-labels, which matches how internet protocols are designed in general: A-labels are used everywhere internally and on-the-wire, and U-labels are basically just for user interfaces. This also matches the general advice to handle encoding/decoding once at the edges, though for backwards-compatibility we continue to use 'str' objects to store A-labels, even though they're now always ASCII. Technically there is a minor compatibility break here: if a user examines the .server_hostname attribute of an ssl-wrapped socket, then previously they would have seen a U-label like "pythön.org", and now they'll see an A-label like "xn--pythn-mua.org". But this only affects non-ASCII domain names, which have never worked in the first place, so it seems unlikely that anyone is relying on the old behavior. This PR also adds an end-to-end test for IDN hostname validation. Previously there were no tests for this functionality. Fixes bpo-28414. --- Doc/library/ssl.rst | 6 ++++ Doc/whatsnew/3.7.rst | 8 +++++ Lib/test/ssl-idn-ca.pem | 15 +++++++++ Lib/test/ssl-idn-cert.pem | 31 +++++++++++++++++++ Lib/test/test_ssl.py | 25 +++++++-------- .../2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst | 2 ++ Modules/_ssl.c | 5 ++- 7 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 Lib/test/ssl-idn-ca.pem create mode 100644 Lib/test/ssl-idn-cert.pem create mode 100644 Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 21da4f6387135b..3dbd87272eb2e4 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1268,6 +1268,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 diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 50c9238e4aa8a2..c924f6ddd4715e 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -662,6 +662,14 @@ ciphers that have been blocked by OpenSSL security update. Default cipher suite selection can be configured on compile time. (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 ------ diff --git a/Lib/test/ssl-idn-ca.pem b/Lib/test/ssl-idn-ca.pem new file mode 100644 index 00000000000000..0034eb8b2e607f --- /dev/null +++ b/Lib/test/ssl-idn-ca.pem @@ -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----- diff --git a/Lib/test/ssl-idn-cert.pem b/Lib/test/ssl-idn-cert.pem new file mode 100644 index 00000000000000..26ae6853a74771 --- /dev/null +++ b/Lib/test/ssl-idn-cert.pem @@ -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----- diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index a253f51d2a440a..a9f789016f9e58 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -119,6 +119,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) @@ -1528,16 +1537,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") def test_bad_server_hostname(self): ctx = ssl.create_default_context() @@ -2634,10 +2633,10 @@ 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(IDNSANSFILE) + server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + server_context.load_cert_chain(IDN_CERT) - context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.verify_mode = ssl.CERT_REQUIRED context.check_hostname = True context.load_verify_locations(SIGNING_CA) diff --git a/Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst b/Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst new file mode 100644 index 00000000000000..8423c1852b8825 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst @@ -0,0 +1,2 @@ +The ssl module can now validate hostnames that contain non-ASCII +characters (IDNs). diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 7545e91babdb3f..2b24c39b37ba02 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -4014,7 +4014,10 @@ _servername_callback(SSL *s, int *al, void *args) PyErr_WriteUnraisable((PyObject *) ssl_ctx); goto error; } - servername_idna = PyUnicode_FromEncodedObject(servername_o, "idna", 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) + */ + servername_idna = PyUnicode_FromEncodedObject(servername_o, "ascii", NULL); if (servername_idna == NULL) { PyErr_WriteUnraisable(servername_o); Py_DECREF(servername_o); From f9855596511f82ab30d031a36234bf7069627133 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sun, 7 Jan 2018 16:17:52 +0100 Subject: [PATCH 2/6] Drop trustme certs All test certs must be generated by CPython's own test helper. Signed-off-by: Christian Heimes --- Lib/test/ssl-idn-ca.pem | 15 --------------- Lib/test/ssl-idn-cert.pem | 31 ------------------------------- 2 files changed, 46 deletions(-) delete mode 100644 Lib/test/ssl-idn-ca.pem delete mode 100644 Lib/test/ssl-idn-cert.pem diff --git a/Lib/test/ssl-idn-ca.pem b/Lib/test/ssl-idn-ca.pem deleted file mode 100644 index 0034eb8b2e607f..00000000000000 --- a/Lib/test/ssl-idn-ca.pem +++ /dev/null @@ -1,15 +0,0 @@ ------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----- diff --git a/Lib/test/ssl-idn-cert.pem b/Lib/test/ssl-idn-cert.pem deleted file mode 100644 index 26ae6853a74771..00000000000000 --- a/Lib/test/ssl-idn-cert.pem +++ /dev/null @@ -1,31 +0,0 @@ ------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----- From 1ec067804bbcf40ed1ec1dd61643a1b78dd70b56 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sun, 7 Jan 2018 16:36:48 +0100 Subject: [PATCH 3/6] Fix IDN SAN handling Signed-off-by: Christian Heimes --- Doc/library/ssl.rst | 33 ++++++---- Lib/ssl.py | 41 ++++++++++-- Lib/test/test_ssl.py | 37 ++++------- Modules/_ssl.c | 137 ++++++++++++++++++++++++---------------- Modules/clinic/_ssl.c.h | 15 +---- 5 files changed, 153 insertions(+), 110 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 3dbd87272eb2e4..4d9110f75fcee3 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1269,9 +1269,9 @@ 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 + 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 @@ -1538,23 +1538,34 @@ to speed up repeated connections from the same clients. .. versionadded:: 3.3 -.. method:: SSLContext.set_servername_callback(server_name_callback) +.. attribute:: SSLContext.set_servername_callback(server_name_callback) + + See :attr:`SSLContext.sni_callback` + + 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 + +.. attribute:: SSLContext.sni_callback 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 specifies a server name indication. The server name indication mechanism is specified in :rfc:`6066` section 3 - Server Name Indication. - Only one callback can be set per ``SSLContext``. If *server_name_callback* - is ``None`` then the callback is disabled. Calling this function a + Only one callback can be set per ``SSLContext``. If *sni_callback* + is set to ``None`` then the callback is disabled. Calling this function a 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 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) 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 :attr:`SSLSocket.context` attribute to a new object of type @@ -1575,10 +1586,6 @@ to speed up repeated connections from the same clients. returned. Other return values will result in a TLS fatal error with :const:`ALERT_DESCRIPTION_INTERNAL_ERROR`. - If there is an IDNA 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. - If an exception is raised from the *server_name_callback* function the TLS connection will terminate with a fatal TLS alert message :const:`ALERT_DESCRIPTION_HANDSHAKE_FAILURE`. @@ -1586,7 +1593,7 @@ to speed up repeated connections from the same clients. This method will raise :exc:`NotImplementedError` if the OpenSSL library had OPENSSL_NO_TLSEXT defined when it was built. - .. versionadded:: 3.4 + .. versionadded:: 3.7 .. method:: SSLContext.load_dh_params(dhfile) diff --git a/Lib/ssl.py b/Lib/ssl.py index b6161d0f178d95..6477ddd60a7583 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -355,13 +355,20 @@ def __new__(cls, protocol=PROTOCOL_TLS, *args, **kwargs): self = _SSLContext.__new__(cls, protocol) return self - def __init__(self, protocol=PROTOCOL_TLS): - self.protocol = protocol + def _encode_hostname(self, hostname): + 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, do_handshake_on_connect=True, suppress_ragged_eofs=True, server_hostname=None, session=None): + # SSLSocket class handles server_hostname encoding before it calls + # ctx._wrap_socket() return self.sslsocket_class( sock=sock, server_side=server_side, @@ -374,8 +381,12 @@ def wrap_socket(self, sock, server_side=False, def wrap_bio(self, incoming, outgoing, server_side=False, server_hostname=None, session=None): - sslobj = self._wrap_bio(incoming, outgoing, server_side=server_side, - server_hostname=server_hostname) + # Need to encode server_hostname here because _wrap_bio() can only + # 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) def set_npn_protocols(self, npn_protocols): @@ -389,6 +400,20 @@ def set_npn_protocols(self, npn_protocols): 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 hasattr(server_name_callback, '__call__'): + raise TypeError("not a callable object") + + def shim_cb(sslobj, servername, sslctx): + if servername is not None: + servername = servername.encode("ascii").decode("idna") + return server_name_callback(sslobj, servername, sslctx) + + self.sni_callback = shim_cb + def set_alpn_protocols(self, alpn_protocols): protos = bytearray() for protocol in alpn_protocols: @@ -447,6 +472,10 @@ def hostname_checks_common_name(self, value): def hostname_checks_common_name(self): return True + @property + def protocol(self): + return _SSLMethod(super().protocol) + @property def verify_flags(self): return VerifyFlags(super().verify_flags) @@ -749,7 +778,7 @@ def __init__(self, sock=None, keyfile=None, certfile=None, raise ValueError("check_hostname requires server_hostname") self._session = _session 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.suppress_ragged_eofs = suppress_ragged_eofs if sock is not None: @@ -781,7 +810,7 @@ def __init__(self, sock=None, keyfile=None, certfile=None, # create the SSL object try: sslobj = self._context._wrap_socket(self, server_side, - server_hostname) + self.server_hostname) self._sslobj = SSLObject(sslobj, owner=self, session=self._session) if do_handshake_on_connect: diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index a9f789016f9e58..a48eb890da43c4 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -119,15 +119,6 @@ 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) @@ -2634,7 +2625,7 @@ def test_check_hostname_idn(self): sys.stdout.write("\n") server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - server_context.load_cert_chain(IDN_CERT) + server_context.load_cert_chain(IDNSANSFILE) context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.verify_mode = ssl.CERT_REQUIRED @@ -2645,18 +2636,26 @@ def test_check_hostname_idn(self): # different ways idn_hostnames = [ ('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'), (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'), + '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'), + '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: server = ThreadedEchoServer(context=server_context, chatty=True) @@ -2675,16 +2674,6 @@ def test_check_hostname_idn(self): s.getpeercert() 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 server = ThreadedEchoServer(context=server_context, chatty=True) with server: diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 2b24c39b37ba02..353629eecdbf47 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -337,13 +337,14 @@ typedef struct { unsigned int alpn_protocols_len; #endif #ifndef OPENSSL_NO_TLSEXT - PyObject *set_hostname; + PyObject *set_sni_cb; #endif int check_hostname; /* OpenSSL has no API to get hostflags from X509_VERIFY_PARAM* struct. * We have to maintain our own copy. OpenSSL's hostflags default to 0. */ unsigned int hostflags; + int protocol; } PySSLContext; typedef struct { @@ -408,7 +409,7 @@ class _ssl.SSLSession "PySSLSession *" "&PySSLSession_Type" static int PySSL_select(PySocketSockObject *s, int writing, _PyTime_t timeout); -#define PySSLContext_Check(v) (Py_TYPE(v) == &PySSLContext_Type) +#define PySSLContext_Check(v) PyObject_IsInstance((v), (PyObject*)&PySSLContext_Type) #define PySSLSocket_Check(v) (Py_TYPE(v) == &PySSLSocket_Type) #define PySSLMemoryBIO_Check(v) (Py_TYPE(v) == &PySSLMemoryBIO_Type) #define PySSLSession_Check(v) (Py_TYPE(v) == &PySSLSession_Type) @@ -761,7 +762,7 @@ _ssl_configure_hostname(PySSLSocket *self, const char* server_hostname) ERR_clear_error(); } - hostname = PyUnicode_Decode(server_hostname, len, "idna", "strict"); + hostname = PyUnicode_Decode(server_hostname, len, "ascii", "strict"); if (hostname == NULL) { goto error; } @@ -1992,7 +1993,7 @@ PyDoc_STRVAR(PySSL_set_context_doc, "_setter_context(ctx)\n\ \ This changes the context associated with the SSLSocket. This is typically\n\ -used from within a callback function set by the set_servername_callback\n\ +used from within a callback function set by the sni_callback\n\ on the SSLContext to change the certificate information associated with the\n\ SSLSocket before the cryptographic exchange handshake messages\n"); @@ -2850,6 +2851,7 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) } self->ctx = ctx; self->hostflags = X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS; + self->protocol = proto_version; #if defined(OPENSSL_NPN_NEGOTIATED) && !defined(OPENSSL_NO_NEXTPROTONEG) self->npn_protocols = NULL; #endif @@ -2857,7 +2859,7 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) self->alpn_protocols = NULL; #endif #ifndef OPENSSL_NO_TLSEXT - self->set_hostname = NULL; + self->set_sni_cb = NULL; #endif /* Don't check host name by default */ if (proto_version == PY_SSL_VERSION_TLS_CLIENT) { @@ -2968,7 +2970,7 @@ static int context_traverse(PySSLContext *self, visitproc visit, void *arg) { #ifndef OPENSSL_NO_TLSEXT - Py_VISIT(self->set_hostname); + Py_VISIT(self->set_sni_cb); #endif return 0; } @@ -2977,7 +2979,7 @@ static int context_clear(PySSLContext *self) { #ifndef OPENSSL_NO_TLSEXT - Py_CLEAR(self->set_hostname); + Py_CLEAR(self->set_sni_cb); #endif return 0; } @@ -3354,6 +3356,10 @@ set_check_hostname(PySSLContext *self, PyObject *arg, void *c) return 0; } +static PyObject * +get_protocol(PySSLContext *self, void *c) { + return PyLong_FromLong(self->protocol); +} typedef struct { PyThreadState *thread_state; @@ -3818,9 +3824,9 @@ _ssl__SSLContext__wrap_socket_impl(PySSLContext *self, PyObject *sock, PyObject *res; /* server_hostname is either None (or absent), or to be encoded - using the idna encoding. */ + as IDN A-label (ASCII str). */ if (hostname_obj != Py_None) { - if (!PyArg_Parse(hostname_obj, "et", "idna", &hostname)) + if (!PyArg_Parse(hostname_obj, "es", "ascii", &hostname)) return NULL; } @@ -3851,9 +3857,9 @@ _ssl__SSLContext__wrap_bio_impl(PySSLContext *self, PySSLMemoryBIO *incoming, PyObject *res; /* server_hostname is either None (or absent), or to be encoded - using the idna encoding. */ + as IDN A-label (ASCII str). */ if (hostname_obj != Py_None) { - if (!PyArg_Parse(hostname_obj, "et", "idna", &hostname)) + if (!PyArg_Parse(hostname_obj, "es", "ascii", &hostname)) return NULL; } @@ -3967,15 +3973,13 @@ _servername_callback(SSL *s, int *al, void *args) int ret; PySSLContext *ssl_ctx = (PySSLContext *) args; PySSLSocket *ssl; - PyObject *servername_o; - PyObject *servername_idna; PyObject *result; /* The high-level ssl.SSLSocket object */ PyObject *ssl_socket; const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); PyGILState_STATE gstate = PyGILState_Ensure(); - if (ssl_ctx->set_hostname == NULL) { + if (ssl_ctx->set_sni_cb == NULL) { /* remove race condition in this the call back while if removing the * callback is in progress */ PyGILState_Release(gstate); @@ -4005,38 +4009,55 @@ _servername_callback(SSL *s, int *al, void *args) goto error; if (servername == NULL) { - result = PyObject_CallFunctionObjArgs(ssl_ctx->set_hostname, ssl_socket, + result = PyObject_CallFunctionObjArgs(ssl_ctx->set_sni_cb, ssl_socket, Py_None, ssl_ctx, NULL); } else { - servername_o = PyBytes_FromString(servername); - if (servername_o == NULL) { + PyObject *servername_bytes; + PyObject *servername_str; + + servername_bytes = PyBytes_FromString(servername); + if (servername_bytes == NULL) { PyErr_WriteUnraisable((PyObject *) ssl_ctx); goto error; } /* 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) */ - servername_idna = PyUnicode_FromEncodedObject(servername_o, "ascii", NULL); - if (servername_idna == NULL) { - PyErr_WriteUnraisable(servername_o); - Py_DECREF(servername_o); + servername_str = PyUnicode_FromEncodedObject(servername_bytes, "ascii", NULL); + Py_DECREF(servername_bytes); + if (servername_str == NULL) { + PyErr_WriteUnraisable(servername_bytes); goto error; } - Py_DECREF(servername_o); - result = PyObject_CallFunctionObjArgs(ssl_ctx->set_hostname, ssl_socket, - servername_idna, ssl_ctx, NULL); - Py_DECREF(servername_idna); + result = PyObject_CallFunctionObjArgs( + ssl_ctx->set_sni_cb, ssl_socket, servername_str, + ssl_ctx, NULL); + Py_DECREF(servername_str); } Py_DECREF(ssl_socket); if (result == NULL) { - PyErr_WriteUnraisable(ssl_ctx->set_hostname); + PyErr_WriteUnraisable(ssl_ctx->set_sni_cb); *al = SSL_AD_HANDSHAKE_FAILURE; ret = SSL_TLSEXT_ERR_ALERT_FATAL; } else { - if (result != Py_None) { + /* Result may be None, a SSLContext or an integer + * None and SSLContext are OK, integer or other values are an error. + */ + if (result == Py_None) { + ret = SSL_TLSEXT_ERR_OK; + } + else if (PySSLContext_Check(result)) { + if (ssl->ctx != (PySSLContext *)result) { + /* if result is not sock.context: socket.context = result */ + Py_INCREF(result); + Py_SETREF(ssl->ctx, (PySSLContext *)result); + SSL_set_SSL_CTX(ssl->ssl, ssl->ctx->ctx); + } + ret = SSL_TLSEXT_ERR_OK; + } else { *al = (int) PyLong_AsLong(result); if (PyErr_Occurred()) { PyErr_WriteUnraisable(result); @@ -4044,9 +4065,6 @@ _servername_callback(SSL *s, int *al, void *args) } ret = SSL_TLSEXT_ERR_ALERT_FATAL; } - else { - ret = SSL_TLSEXT_ERR_OK; - } Py_DECREF(result); } @@ -4062,49 +4080,59 @@ _servername_callback(SSL *s, int *al, void *args) } #endif -/*[clinic input] -_ssl._SSLContext.set_servername_callback - method as cb: object - / - -Set a callback that will be called when a server name is provided by the SSL/TLS client in the SNI extension. - -If the argument is None then the callback is disabled. The method is called -with the SSLSocket, the server name as a string, and the SSLContext object. -See RFC 6066 for details of the SNI extension. -[clinic start generated code]*/ - static PyObject * -_ssl__SSLContext_set_servername_callback(PySSLContext *self, PyObject *cb) -/*[clinic end generated code: output=3439a1b2d5d3b7ea input=a2a83620197d602b]*/ +get_sni_callback(PySSLContext *self, void *c) { + PyObject *cb = self->set_sni_cb; + if (cb == NULL) { + Py_RETURN_NONE; + } + Py_INCREF(cb); + return cb; +} + +static int +set_sni_callback(PySSLContext *self, PyObject *arg, void *c) +{ + if (self->protocol == PY_SSL_VERSION_TLS_CLIENT) { + PyErr_SetString(PyExc_ValueError, + "sni_callback cannot be set on TLS_CLIENT context"); + return -1; + } #if HAVE_SNI && !defined(OPENSSL_NO_TLSEXT) - Py_CLEAR(self->set_hostname); - if (cb == Py_None) { + Py_CLEAR(self->set_sni_cb); + if (arg == Py_None) { SSL_CTX_set_tlsext_servername_callback(self->ctx, NULL); } else { - if (!PyCallable_Check(cb)) { + if (!PyCallable_Check(arg)) { SSL_CTX_set_tlsext_servername_callback(self->ctx, NULL); PyErr_SetString(PyExc_TypeError, "not a callable object"); - return NULL; + return -1; } - Py_INCREF(cb); - self->set_hostname = cb; + Py_INCREF(arg); + self->set_sni_cb = arg; SSL_CTX_set_tlsext_servername_callback(self->ctx, _servername_callback); SSL_CTX_set_tlsext_servername_arg(self->ctx, self); } - Py_RETURN_NONE; + return 0; #else PyErr_SetString(PyExc_NotImplementedError, "The TLS extension servername callback, " "SSL_CTX_set_tlsext_servername_callback, " "is not in the current OpenSSL library."); - return NULL; + return -1; #endif } +PyDoc_STRVAR(PySSLContext_sni_callback_doc, +"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."); + /*[clinic input] _ssl._SSLContext.cert_store_stats @@ -4220,8 +4248,12 @@ static PyGetSetDef context_getsetlist[] = { (setter) set_check_hostname, NULL}, {"_host_flags", (getter) get_host_flags, (setter) set_host_flags, NULL}, + {"sni_callback", (getter) get_sni_callback, + (setter) set_sni_callback, PySSLContext_sni_callback_doc}, {"options", (getter) get_options, (setter) set_options, NULL}, + {"protocol", (getter) get_protocol, + NULL, NULL}, {"verify_flags", (getter) get_verify_flags, (setter) set_verify_flags, NULL}, {"verify_mode", (getter) get_verify_mode, @@ -4241,7 +4273,6 @@ static struct PyMethodDef context_methods[] = { _SSL__SSLCONTEXT_SESSION_STATS_METHODDEF _SSL__SSLCONTEXT_SET_DEFAULT_VERIFY_PATHS_METHODDEF _SSL__SSLCONTEXT_SET_ECDH_CURVE_METHODDEF - _SSL__SSLCONTEXT_SET_SERVERNAME_CALLBACK_METHODDEF _SSL__SSLCONTEXT_CERT_STORE_STATS_METHODDEF _SSL__SSLCONTEXT_GET_CA_CERTS_METHODDEF _SSL__SSLCONTEXT_GET_CIPHERS_METHODDEF diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 32743e710f590f..d1a9afcd1d1e5d 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -650,19 +650,6 @@ PyDoc_STRVAR(_ssl__SSLContext_set_ecdh_curve__doc__, #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__, "cert_store_stats($self, /)\n" "--\n" @@ -1168,4 +1155,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _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]*/ From a0e42d4576f5a5a9fc0b05352acb47c76918032f Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 22 Feb 2018 17:14:26 -0800 Subject: [PATCH 4/6] Small doc updates --- Doc/library/ssl.rst | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 4d9110f75fcee3..4cad9f667c48d0 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1538,16 +1538,6 @@ to speed up repeated connections from the same clients. .. versionadded:: 3.3 -.. attribute:: SSLContext.set_servername_callback(server_name_callback) - - See :attr:`SSLContext.sni_callback` - - 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 - .. attribute:: SSLContext.sni_callback Register a callback function that will be called after the TLS Client Hello @@ -1559,7 +1549,7 @@ to speed up repeated connections from the same clients. is set to ``None`` then the callback is disabled. Calling this function a subsequent time will disable the previously registered callback. - The callback function, 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 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) @@ -1580,13 +1570,13 @@ to speed up repeated connections from the same clients. the TLS connection has progressed beyond the TLS Client Hello and therefore 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 :const:`ALERT_DESCRIPTION_* ` can be returned. Other return values will result in a TLS fatal error with :const:`ALERT_DESCRIPTION_INTERNAL_ERROR`. - If an exception is raised from the *server_name_callback* function the TLS + If an exception is raised from the *sni_callback* function the TLS connection will terminate with a fatal TLS alert message :const:`ALERT_DESCRIPTION_HANDSHAKE_FAILURE`. @@ -1595,6 +1585,20 @@ to speed up repeated connections from the same clients. .. 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 + .. method:: SSLContext.load_dh_params(dhfile) Load the key generation parameters for Diffie-Helman (DH) key exchange. From e5073b5b87d4e82a68cf53baca2e6cfb3abf69a6 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 22 Feb 2018 17:20:45 -0800 Subject: [PATCH 5/6] Update blurb to match what this PR does now --- .../next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst b/Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst index 8423c1852b8825..06528c93ee19f6 100644 --- a/Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst +++ b/Misc/NEWS.d/next/Security/2017-08-06-14-43-45.bpo-28414.mzZ6vD.rst @@ -1,2 +1 @@ -The ssl module can now validate hostnames that contain non-ASCII -characters (IDNs). +The ssl module now allows users to perform their own IDN en/decoding when using SNI. From 8e3c14f685a0e0aff66407de17242a13601d6ea1 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 23 Feb 2018 12:43:56 +0100 Subject: [PATCH 6/6] Address review comments Drop extra code for PEP 543 future compatibility in sni callback Use callable() and encode_hostname in shim for old SNI callback. Signed-off-by: Christian Heimes --- Lib/ssl.py | 5 ++--- Modules/_ssl.c | 11 ----------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Lib/ssl.py b/Lib/ssl.py index 6477ddd60a7583..f2537698d303df 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -404,12 +404,11 @@ def set_servername_callback(self, server_name_callback): if server_name_callback is None: self.sni_callback = None else: - if not hasattr(server_name_callback, '__call__'): + if not callable(server_name_callback): raise TypeError("not a callable object") def shim_cb(sslobj, servername, sslctx): - if servername is not None: - servername = servername.encode("ascii").decode("idna") + servername = self._encode_hostname(servername) return server_name_callback(sslobj, servername, sslctx) self.sni_callback = shim_cb diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 353629eecdbf47..a0f8c1cb324434 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -408,8 +408,6 @@ class _ssl.SSLSession "PySSLSession *" "&PySSLSession_Type" static int PySSL_select(PySocketSockObject *s, int writing, _PyTime_t timeout); - -#define PySSLContext_Check(v) PyObject_IsInstance((v), (PyObject*)&PySSLContext_Type) #define PySSLSocket_Check(v) (Py_TYPE(v) == &PySSLSocket_Type) #define PySSLMemoryBIO_Check(v) (Py_TYPE(v) == &PySSLMemoryBIO_Type) #define PySSLSession_Check(v) (Py_TYPE(v) == &PySSLSession_Type) @@ -4048,15 +4046,6 @@ _servername_callback(SSL *s, int *al, void *args) */ if (result == Py_None) { ret = SSL_TLSEXT_ERR_OK; - } - else if (PySSLContext_Check(result)) { - if (ssl->ctx != (PySSLContext *)result) { - /* if result is not sock.context: socket.context = result */ - Py_INCREF(result); - Py_SETREF(ssl->ctx, (PySSLContext *)result); - SSL_set_SSL_CTX(ssl->ssl, ssl->ctx->ctx); - } - ret = SSL_TLSEXT_ERR_OK; } else { *al = (int) PyLong_AsLong(result); if (PyErr_Occurred()) {