diff options
author | Victor Stinner <vstinner@python.org> | 2022-06-25 10:54:11 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-25 10:54:11 (GMT) |
commit | 944c7d8a8561d4b637af5c128df1d8d7570ccb46 (patch) | |
tree | 900315b515aadaf9297178d6ddc23aa14a03670f | |
parent | b52849905218582d067a875712af8c2bcc8789c4 (diff) | |
download | cpython-944c7d8a8561d4b637af5c128df1d8d7570ccb46.zip cpython-944c7d8a8561d4b637af5c128df1d8d7570ccb46.tar.gz cpython-944c7d8a8561d4b637af5c128df1d8d7570ccb46.tar.bz2 |
gh-94199: Remove ssl.match_hostname() function (#94224)
-rw-r--r-- | Doc/library/ssl.rst | 57 | ||||
-rw-r--r-- | Doc/whatsnew/3.12.rst | 5 | ||||
-rw-r--r-- | Lib/ssl.py | 62 | ||||
-rw-r--r-- | Lib/test/test_ssl.py | 199 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2022-06-24-17-11-33.gh-issue-94199.7releN.rst | 4 |
5 files changed, 14 insertions, 313 deletions
diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 8df64ff..1bbcd7a 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -335,49 +335,6 @@ Certificate handling import ssl -.. function:: match_hostname(cert, hostname) - - Verify that *cert* (in decoded format as returned by - :meth:`SSLSocket.getpeercert`) matches the given *hostname*. The rules - applied are those for checking the identity of HTTPS servers as outlined - in :rfc:`2818`, :rfc:`5280` and :rfc:`6125`. In addition to HTTPS, this - function should be suitable for checking the identity of servers in - various SSL-based protocols such as FTPS, IMAPS, POPS and others. - - :exc:`CertificateError` is raised on failure. On success, the function - returns nothing:: - - >>> cert = {'subject': ((('commonName', 'example.com'),),)} - >>> ssl.match_hostname(cert, "example.com") - >>> ssl.match_hostname(cert, "example.org") - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "/home/py3k/Lib/ssl.py", line 130, in match_hostname - ssl.CertificateError: hostname 'example.org' doesn't match 'example.com' - - .. versionadded:: 3.2 - - .. versionchanged:: 3.3.3 - The function now follows :rfc:`6125`, section 6.4.3 and does neither - match multiple wildcards (e.g. ``*.*.com`` or ``*a*.example.org``) nor - a wildcard inside an internationalized domain names (IDN) fragment. - IDN A-labels such as ``www*.xn--pthon-kva.org`` are still supported, - but ``x*.python.org`` no longer matches ``xn--tda.python.org``. - - .. versionchanged:: 3.5 - Matching of IP addresses, when present in the subjectAltName field - of the certificate, is now supported. - - .. versionchanged:: 3.7 - The function is no longer used to TLS connections. Hostname matching - is now performed by OpenSSL. - - Allow wildcard when it is the leftmost and the only character - in that segment. Partial wildcards like ``www*.example.com`` are no - longer supported. - - .. deprecated:: 3.7 - .. function:: cert_time_to_seconds(cert_time) Return the time in seconds since the Epoch, given the ``cert_time`` @@ -1251,11 +1208,6 @@ SSL sockets also have the following additional methods and attributes: 'subjectAltName': (('DNS', '*.eff.org'), ('DNS', 'eff.org')), 'version': 3} - .. note:: - - To validate a certificate for a particular service, you can use the - :func:`match_hostname` function. - If the ``binary_form`` parameter is :const:`True`, and a certificate was provided, this method returns the DER-encoded form of the entire certificate as a sequence of bytes, or :const:`None` if the peer did not provide a @@ -1270,6 +1222,8 @@ SSL sockets also have the following additional methods and attributes: :const:`None` if you used :const:`CERT_NONE` (rather than :const:`CERT_OPTIONAL` or :const:`CERT_REQUIRED`). + See also :attr:`SSLContext.check_hostname`. + .. versionchanged:: 3.2 The returned dictionary includes additional items such as ``issuer`` and ``notBefore``. @@ -2639,10 +2593,9 @@ Therefore, when in client mode, it is highly recommended to use :const:`CERT_REQUIRED`. However, it is in itself not sufficient; you also have to check that the server certificate, which can be obtained by calling :meth:`SSLSocket.getpeercert`, matches the desired service. For many -protocols and applications, the service can be identified by the hostname; -in this case, the :func:`match_hostname` function can be used. This common -check is automatically performed when :attr:`SSLContext.check_hostname` is -enabled. +protocols and applications, the service can be identified by the hostname. +This common check is automatically performed when +:attr:`SSLContext.check_hostname` is enabled. .. versionchanged:: 3.7 Hostname matchings is now performed by OpenSSL. Python no longer uses diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 6257901..f8de5f5 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -224,6 +224,11 @@ Removed extension if it was not present. (Contributed by Victor Stinner in :gh:`94196`.) +* Remove the :func:`ssl.match_hostname` function. The + :func:`ssl.match_hostname` was deprecated in Python 3.7. OpenSSL performs + hostname matching since Python 3.7, Python no longer uses the + :func:`ssl.match_hostname` function. + (Contributed by Victor Stinner in :gh:`94199`.) Porting to Python 3.12 ====================== @@ -373,68 +373,6 @@ def _ipaddress_match(cert_ipaddress, host_ip): return ip == host_ip -def match_hostname(cert, hostname): - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 - rules are followed. - - The function matches IP addresses rather than dNSNames if hostname is a - valid ipaddress string. IPv4 addresses are supported on all platforms. - IPv6 addresses are supported on platforms with IPv6 support (AF_INET6 - and inet_pton). - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - warnings.warn( - "ssl.match_hostname() is deprecated", - category=DeprecationWarning, - stacklevel=2 - ) - if not cert: - raise ValueError("empty or no certificate, match_hostname needs a " - "SSL socket or SSL context with either " - "CERT_OPTIONAL or CERT_REQUIRED") - try: - host_ip = _inet_paton(hostname) - except ValueError: - # Not an IP address (common case) - host_ip = None - dnsnames = [] - san = cert.get('subjectAltName', ()) - for key, value in san: - if key == 'DNS': - if host_ip is None and _dnsname_match(value, hostname): - return - dnsnames.append(value) - elif key == 'IP Address': - if host_ip is not None and _ipaddress_match(value, host_ip): - return - dnsnames.append(value) - if not dnsnames: - # The subject is only checked when there is no dNSName entry - # in subjectAltName - for sub in cert.get('subject', ()): - for key, value in sub: - # XXX according to RFC 2818, the most specific Common Name - # must be used. - if key == 'commonName': - if _dnsname_match(value, hostname): - return - dnsnames.append(value) - if len(dnsnames) > 1: - raise CertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) - elif len(dnsnames) == 1: - raise CertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) - else: - raise CertificateError("no appropriate commonName or " - "subjectAltName fields were found") - - DefaultVerifyPaths = namedtuple("DefaultVerifyPaths", "cafile capath openssl_cafile_env openssl_cafile openssl_capath_env " "openssl_capath") diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 0ba2d6b..b41ce98 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -681,205 +681,6 @@ class BasicSocketTests(unittest.TestCase): """Wrapping with a badly formatted key (syntax error)""" self.bad_cert_test("badkey.pem") - @ignore_deprecation - def test_match_hostname(self): - def ok(cert, hostname): - ssl.match_hostname(cert, hostname) - def fail(cert, hostname): - self.assertRaises(ssl.CertificateError, - ssl.match_hostname, cert, hostname) - - # -- Hostname matching -- - - cert = {'subject': ((('commonName', 'example.com'),),)} - ok(cert, 'example.com') - ok(cert, 'ExAmple.cOm') - fail(cert, 'www.example.com') - fail(cert, '.example.com') - fail(cert, 'example.org') - fail(cert, 'exampleXcom') - - cert = {'subject': ((('commonName', '*.a.com'),),)} - ok(cert, 'foo.a.com') - fail(cert, 'bar.foo.a.com') - fail(cert, 'a.com') - fail(cert, 'Xa.com') - fail(cert, '.a.com') - - # only match wildcards when they are the only thing - # in left-most segment - cert = {'subject': ((('commonName', 'f*.com'),),)} - fail(cert, 'foo.com') - fail(cert, 'f.com') - fail(cert, 'bar.com') - fail(cert, 'foo.a.com') - fail(cert, 'bar.foo.com') - - # NULL bytes are bad, CVE-2013-4073 - cert = {'subject': ((('commonName', - 'null.python.org\x00example.org'),),)} - ok(cert, 'null.python.org\x00example.org') # or raise an error? - fail(cert, 'example.org') - fail(cert, 'null.python.org') - - # error cases with wildcards - cert = {'subject': ((('commonName', '*.*.a.com'),),)} - fail(cert, 'bar.foo.a.com') - fail(cert, 'a.com') - fail(cert, 'Xa.com') - fail(cert, '.a.com') - - cert = {'subject': ((('commonName', 'a.*.com'),),)} - fail(cert, 'a.foo.com') - fail(cert, 'a..com') - fail(cert, 'a.com') - - # wildcard doesn't match IDNA prefix 'xn--' - idna = 'püthon.python.org'.encode("idna").decode("ascii") - cert = {'subject': ((('commonName', idna),),)} - ok(cert, idna) - cert = {'subject': ((('commonName', 'x*.python.org'),),)} - fail(cert, idna) - cert = {'subject': ((('commonName', 'xn--p*.python.org'),),)} - fail(cert, idna) - - # wildcard in first fragment and IDNA A-labels in sequent fragments - # are supported. - idna = 'www*.pythön.org'.encode("idna").decode("ascii") - cert = {'subject': ((('commonName', idna),),)} - fail(cert, 'www.pythön.org'.encode("idna").decode("ascii")) - fail(cert, 'www1.pythön.org'.encode("idna").decode("ascii")) - fail(cert, 'ftp.pythön.org'.encode("idna").decode("ascii")) - fail(cert, 'pythön.org'.encode("idna").decode("ascii")) - - # Slightly fake real-world example - cert = {'notAfter': 'Jun 26 21:41:46 2011 GMT', - 'subject': ((('commonName', 'linuxfrz.org'),),), - 'subjectAltName': (('DNS', 'linuxfr.org'), - ('DNS', 'linuxfr.com'), - ('othername', '<unsupported>'))} - ok(cert, 'linuxfr.org') - ok(cert, 'linuxfr.com') - # Not a "DNS" entry - fail(cert, '<unsupported>') - # When there is a subjectAltName, commonName isn't used - fail(cert, 'linuxfrz.org') - - # A pristine real-world example - cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT', - 'subject': ((('countryName', 'US'),), - (('stateOrProvinceName', 'California'),), - (('localityName', 'Mountain View'),), - (('organizationName', 'Google Inc'),), - (('commonName', 'mail.google.com'),))} - ok(cert, 'mail.google.com') - fail(cert, 'gmail.com') - # Only commonName is considered - fail(cert, 'California') - - # -- IPv4 matching -- - cert = {'subject': ((('commonName', 'example.com'),),), - 'subjectAltName': (('DNS', 'example.com'), - ('IP Address', '10.11.12.13'), - ('IP Address', '14.15.16.17'), - ('IP Address', '127.0.0.1'))} - ok(cert, '10.11.12.13') - ok(cert, '14.15.16.17') - # socket.inet_ntoa(socket.inet_aton('127.1')) == '127.0.0.1' - fail(cert, '127.1') - fail(cert, '14.15.16.17 ') - fail(cert, '14.15.16.17 extra data') - fail(cert, '14.15.16.18') - fail(cert, 'example.net') - - # -- IPv6 matching -- - if socket_helper.IPV6_ENABLED: - cert = {'subject': ((('commonName', 'example.com'),),), - 'subjectAltName': ( - ('DNS', 'example.com'), - ('IP Address', '2001:0:0:0:0:0:0:CAFE\n'), - ('IP Address', '2003:0:0:0:0:0:0:BABA\n'))} - ok(cert, '2001::cafe') - ok(cert, '2003::baba') - fail(cert, '2003::baba ') - fail(cert, '2003::baba extra data') - fail(cert, '2003::bebe') - fail(cert, 'example.net') - - # -- Miscellaneous -- - - # Neither commonName nor subjectAltName - cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT', - 'subject': ((('countryName', 'US'),), - (('stateOrProvinceName', 'California'),), - (('localityName', 'Mountain View'),), - (('organizationName', 'Google Inc'),))} - fail(cert, 'mail.google.com') - - # No DNS entry in subjectAltName but a commonName - cert = {'notAfter': 'Dec 18 23:59:59 2099 GMT', - 'subject': ((('countryName', 'US'),), - (('stateOrProvinceName', 'California'),), - (('localityName', 'Mountain View'),), - (('commonName', 'mail.google.com'),)), - 'subjectAltName': (('othername', 'blabla'), )} - ok(cert, 'mail.google.com') - - # No DNS entry subjectAltName and no commonName - cert = {'notAfter': 'Dec 18 23:59:59 2099 GMT', - 'subject': ((('countryName', 'US'),), - (('stateOrProvinceName', 'California'),), - (('localityName', 'Mountain View'),), - (('organizationName', 'Google Inc'),)), - 'subjectAltName': (('othername', 'blabla'),)} - fail(cert, 'google.com') - - # Empty cert / no cert - self.assertRaises(ValueError, ssl.match_hostname, None, 'example.com') - self.assertRaises(ValueError, ssl.match_hostname, {}, 'example.com') - - # Issue #17980: avoid denials of service by refusing more than one - # wildcard per fragment. - cert = {'subject': ((('commonName', 'a*b.example.com'),),)} - with self.assertRaisesRegex( - ssl.CertificateError, - "partial wildcards in leftmost label are not supported"): - ssl.match_hostname(cert, 'axxb.example.com') - - cert = {'subject': ((('commonName', 'www.*.example.com'),),)} - with self.assertRaisesRegex( - ssl.CertificateError, - "wildcard can only be present in the leftmost label"): - ssl.match_hostname(cert, 'www.sub.example.com') - - cert = {'subject': ((('commonName', 'a*b*.example.com'),),)} - with self.assertRaisesRegex( - ssl.CertificateError, - "too many wildcards"): - ssl.match_hostname(cert, 'axxbxxc.example.com') - - cert = {'subject': ((('commonName', '*'),),)} - with self.assertRaisesRegex( - ssl.CertificateError, - "sole wildcard without additional labels are not support"): - ssl.match_hostname(cert, 'host') - - cert = {'subject': ((('commonName', '*.com'),),)} - with self.assertRaisesRegex( - ssl.CertificateError, - r"hostname 'com' doesn't match '\*.com'"): - ssl.match_hostname(cert, 'com') - - # extra checks for _inet_paton() - for invalid in ['1', '', '1.2.3', '256.0.0.1', '127.0.0.1/24']: - with self.assertRaises(ValueError): - ssl._inet_paton(invalid) - for ipaddr in ['127.0.0.1', '192.168.0.1']: - self.assertTrue(ssl._inet_paton(ipaddr)) - if socket_helper.IPV6_ENABLED: - for ipaddr in ['::1', '2001:db8:85a3::8a2e:370:7334']: - self.assertTrue(ssl._inet_paton(ipaddr)) - def test_server_side(self): # server_hostname doesn't work for server sockets ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) diff --git a/Misc/NEWS.d/next/Library/2022-06-24-17-11-33.gh-issue-94199.7releN.rst b/Misc/NEWS.d/next/Library/2022-06-24-17-11-33.gh-issue-94199.7releN.rst new file mode 100644 index 0000000..68bd283 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-06-24-17-11-33.gh-issue-94199.7releN.rst @@ -0,0 +1,4 @@ +Remove the :func:`ssl.match_hostname` function. The +:func:`ssl.match_hostname` was deprecated in Python 3.7. OpenSSL performs +hostname matching since Python 3.7, Python no longer uses the +:func:`ssl.match_hostname` function. Patch by Victor Stinner. |