[Python-checkins] gh-94199: Remove ssl.match_hostname() function (#94224)

tiran webhook-mailer at python.org
Sat Jun 25 06:54:15 EDT 2022


https://github.com/python/cpython/commit/944c7d8a8561d4b637af5c128df1d8d7570ccb46
commit: 944c7d8a8561d4b637af5c128df1d8d7570ccb46
branch: main
author: Victor Stinner <vstinner at python.org>
committer: tiran <christian at python.org>
date: 2022-06-25T12:54:11+02:00
summary:

gh-94199: Remove ssl.match_hostname() function (#94224)

files:
A Misc/NEWS.d/next/Library/2022-06-24-17-11-33.gh-issue-94199.7releN.rst
M Doc/library/ssl.rst
M Doc/whatsnew/3.12.rst
M Lib/ssl.py
M Lib/test/test_ssl.py

diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst
index 8df64ffa89afc..1bbcd7a1964ea 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 625790151f70c..f8de5f503e731 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
 ======================
diff --git a/Lib/ssl.py b/Lib/ssl.py
index 7c990417c4a1f..02359a18c01e3 100644
--- a/Lib/ssl.py
+++ b/Lib/ssl.py
@@ -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 0ba2d6b1efc72..b41ce98a6d994 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -681,205 +681,6 @@ def test_malformed_key(self):
         """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 0000000000000..68bd283b99074
--- /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.



More information about the Python-checkins mailing list