mirror of
https://github.com/python/cpython.git
synced 2024-11-29 00:56:12 +01:00
gh-94199: Remove ssl.match_hostname() function (#94224)
This commit is contained in:
parent
b528499052
commit
944c7d8a85
@ -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
|
||||
|
@ -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
|
||||
======================
|
||||
|
62
Lib/ssl.py
62
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")
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user