Skip to content

Commit 944c7d8

Browse files
authored
gh-94199: Remove ssl.match_hostname() function (#94224)
1 parent b528499 commit 944c7d8

File tree

5 files changed

+14
-313
lines changed

5 files changed

+14
-313
lines changed

Doc/library/ssl.rst

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -335,49 +335,6 @@ Certificate handling
335335

336336
import ssl
337337

338-
.. function:: match_hostname(cert, hostname)
339-
340-
Verify that *cert* (in decoded format as returned by
341-
:meth:`SSLSocket.getpeercert`) matches the given *hostname*. The rules
342-
applied are those for checking the identity of HTTPS servers as outlined
343-
in :rfc:`2818`, :rfc:`5280` and :rfc:`6125`. In addition to HTTPS, this
344-
function should be suitable for checking the identity of servers in
345-
various SSL-based protocols such as FTPS, IMAPS, POPS and others.
346-
347-
:exc:`CertificateError` is raised on failure. On success, the function
348-
returns nothing::
349-
350-
>>> cert = {'subject': ((('commonName', 'example.com'),),)}
351-
>>> ssl.match_hostname(cert, "example.com")
352-
>>> ssl.match_hostname(cert, "example.org")
353-
Traceback (most recent call last):
354-
File "<stdin>", line 1, in <module>
355-
File "/home/py3k/Lib/ssl.py", line 130, in match_hostname
356-
ssl.CertificateError: hostname 'example.org' doesn't match 'example.com'
357-
358-
.. versionadded:: 3.2
359-
360-
.. versionchanged:: 3.3.3
361-
The function now follows :rfc:`6125`, section 6.4.3 and does neither
362-
match multiple wildcards (e.g. ``*.*.com`` or ``*a*.example.org``) nor
363-
a wildcard inside an internationalized domain names (IDN) fragment.
364-
IDN A-labels such as ``www*.xn--pthon-kva.org`` are still supported,
365-
but ``x*.python.org`` no longer matches ``xn--tda.python.org``.
366-
367-
.. versionchanged:: 3.5
368-
Matching of IP addresses, when present in the subjectAltName field
369-
of the certificate, is now supported.
370-
371-
.. versionchanged:: 3.7
372-
The function is no longer used to TLS connections. Hostname matching
373-
is now performed by OpenSSL.
374-
375-
Allow wildcard when it is the leftmost and the only character
376-
in that segment. Partial wildcards like ``www*.example.com`` are no
377-
longer supported.
378-
379-
.. deprecated:: 3.7
380-
381338
.. function:: cert_time_to_seconds(cert_time)
382339

383340
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:
12511208
'subjectAltName': (('DNS', '*.eff.org'), ('DNS', 'eff.org')),
12521209
'version': 3}
12531210

1254-
.. note::
1255-
1256-
To validate a certificate for a particular service, you can use the
1257-
:func:`match_hostname` function.
1258-
12591211
If the ``binary_form`` parameter is :const:`True`, and a certificate was
12601212
provided, this method returns the DER-encoded form of the entire certificate
12611213
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:
12701222
:const:`None` if you used :const:`CERT_NONE` (rather than
12711223
:const:`CERT_OPTIONAL` or :const:`CERT_REQUIRED`).
12721224

1225+
See also :attr:`SSLContext.check_hostname`.
1226+
12731227
.. versionchanged:: 3.2
12741228
The returned dictionary includes additional items such as ``issuer``
12751229
and ``notBefore``.
@@ -2639,10 +2593,9 @@ Therefore, when in client mode, it is highly recommended to use
26392593
:const:`CERT_REQUIRED`. However, it is in itself not sufficient; you also
26402594
have to check that the server certificate, which can be obtained by calling
26412595
:meth:`SSLSocket.getpeercert`, matches the desired service. For many
2642-
protocols and applications, the service can be identified by the hostname;
2643-
in this case, the :func:`match_hostname` function can be used. This common
2644-
check is automatically performed when :attr:`SSLContext.check_hostname` is
2645-
enabled.
2596+
protocols and applications, the service can be identified by the hostname.
2597+
This common check is automatically performed when
2598+
:attr:`SSLContext.check_hostname` is enabled.
26462599

26472600
.. versionchanged:: 3.7
26482601
Hostname matchings is now performed by OpenSSL. Python no longer uses

Doc/whatsnew/3.12.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ Removed
224224
extension if it was not present.
225225
(Contributed by Victor Stinner in :gh:`94196`.)
226226

227+
* Remove the :func:`ssl.match_hostname` function. The
228+
:func:`ssl.match_hostname` was deprecated in Python 3.7. OpenSSL performs
229+
hostname matching since Python 3.7, Python no longer uses the
230+
:func:`ssl.match_hostname` function.
231+
(Contributed by Victor Stinner in :gh:`94199`.)
227232

228233
Porting to Python 3.12
229234
======================

Lib/ssl.py

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -373,68 +373,6 @@ def _ipaddress_match(cert_ipaddress, host_ip):
373373
return ip == host_ip
374374

375375

376-
def match_hostname(cert, hostname):
377-
"""Verify that *cert* (in decoded format as returned by
378-
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
379-
rules are followed.
380-
381-
The function matches IP addresses rather than dNSNames if hostname is a
382-
valid ipaddress string. IPv4 addresses are supported on all platforms.
383-
IPv6 addresses are supported on platforms with IPv6 support (AF_INET6
384-
and inet_pton).
385-
386-
CertificateError is raised on failure. On success, the function
387-
returns nothing.
388-
"""
389-
warnings.warn(
390-
"ssl.match_hostname() is deprecated",
391-
category=DeprecationWarning,
392-
stacklevel=2
393-
)
394-
if not cert:
395-
raise ValueError("empty or no certificate, match_hostname needs a "
396-
"SSL socket or SSL context with either "
397-
"CERT_OPTIONAL or CERT_REQUIRED")
398-
try:
399-
host_ip = _inet_paton(hostname)
400-
except ValueError:
401-
# Not an IP address (common case)
402-
host_ip = None
403-
dnsnames = []
404-
san = cert.get('subjectAltName', ())
405-
for key, value in san:
406-
if key == 'DNS':
407-
if host_ip is None and _dnsname_match(value, hostname):
408-
return
409-
dnsnames.append(value)
410-
elif key == 'IP Address':
411-
if host_ip is not None and _ipaddress_match(value, host_ip):
412-
return
413-
dnsnames.append(value)
414-
if not dnsnames:
415-
# The subject is only checked when there is no dNSName entry
416-
# in subjectAltName
417-
for sub in cert.get('subject', ()):
418-
for key, value in sub:
419-
# XXX according to RFC 2818, the most specific Common Name
420-
# must be used.
421-
if key == 'commonName':
422-
if _dnsname_match(value, hostname):
423-
return
424-
dnsnames.append(value)
425-
if len(dnsnames) > 1:
426-
raise CertificateError("hostname %r "
427-
"doesn't match either of %s"
428-
% (hostname, ', '.join(map(repr, dnsnames))))
429-
elif len(dnsnames) == 1:
430-
raise CertificateError("hostname %r "
431-
"doesn't match %r"
432-
% (hostname, dnsnames[0]))
433-
else:
434-
raise CertificateError("no appropriate commonName or "
435-
"subjectAltName fields were found")
436-
437-
438376
DefaultVerifyPaths = namedtuple("DefaultVerifyPaths",
439377
"cafile capath openssl_cafile_env openssl_cafile openssl_capath_env "
440378
"openssl_capath")

Lib/test/test_ssl.py

Lines changed: 0 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -681,205 +681,6 @@ def test_malformed_key(self):
681681
"""Wrapping with a badly formatted key (syntax error)"""
682682
self.bad_cert_test("badkey.pem")
683683

684-
@ignore_deprecation
685-
def test_match_hostname(self):
686-
def ok(cert, hostname):
687-
ssl.match_hostname(cert, hostname)
688-
def fail(cert, hostname):
689-
self.assertRaises(ssl.CertificateError,
690-
ssl.match_hostname, cert, hostname)
691-
692-
# -- Hostname matching --
693-
694-
cert = {'subject': ((('commonName', 'example.com'),),)}
695-
ok(cert, 'example.com')
696-
ok(cert, 'ExAmple.cOm')
697-
fail(cert, 'www.example.com')
698-
fail(cert, '.example.com')
699-
fail(cert, 'example.org')
700-
fail(cert, 'exampleXcom')
701-
702-
cert = {'subject': ((('commonName', '*.a.com'),),)}
703-
ok(cert, 'foo.a.com')
704-
fail(cert, 'bar.foo.a.com')
705-
fail(cert, 'a.com')
706-
fail(cert, 'Xa.com')
707-
fail(cert, '.a.com')
708-
709-
# only match wildcards when they are the only thing
710-
# in left-most segment
711-
cert = {'subject': ((('commonName', 'f*.com'),),)}
712-
fail(cert, 'foo.com')
713-
fail(cert, 'f.com')
714-
fail(cert, 'bar.com')
715-
fail(cert, 'foo.a.com')
716-
fail(cert, 'bar.foo.com')
717-
718-
# NULL bytes are bad, CVE-2013-4073
719-
cert = {'subject': ((('commonName',
720-
'null.python.org\x00example.org'),),)}
721-
ok(cert, 'null.python.org\x00example.org') # or raise an error?
722-
fail(cert, 'example.org')
723-
fail(cert, 'null.python.org')
724-
725-
# error cases with wildcards
726-
cert = {'subject': ((('commonName', '*.*.a.com'),),)}
727-
fail(cert, 'bar.foo.a.com')
728-
fail(cert, 'a.com')
729-
fail(cert, 'Xa.com')
730-
fail(cert, '.a.com')
731-
732-
cert = {'subject': ((('commonName', 'a.*.com'),),)}
733-
fail(cert, 'a.foo.com')
734-
fail(cert, 'a..com')
735-
fail(cert, 'a.com')
736-
737-
# wildcard doesn't match IDNA prefix 'xn--'
738-
idna = 'püthon.python.org'.encode("idna").decode("ascii")
739-
cert = {'subject': ((('commonName', idna),),)}
740-
ok(cert, idna)
741-
cert = {'subject': ((('commonName', 'x*.python.org'),),)}
742-
fail(cert, idna)
743-
cert = {'subject': ((('commonName', 'xn--p*.python.org'),),)}
744-
fail(cert, idna)
745-
746-
# wildcard in first fragment and IDNA A-labels in sequent fragments
747-
# are supported.
748-
idna = 'www*.pythön.org'.encode("idna").decode("ascii")
749-
cert = {'subject': ((('commonName', idna),),)}
750-
fail(cert, 'www.pythön.org'.encode("idna").decode("ascii"))
751-
fail(cert, 'www1.pythön.org'.encode("idna").decode("ascii"))
752-
fail(cert, 'ftp.pythön.org'.encode("idna").decode("ascii"))
753-
fail(cert, 'pythön.org'.encode("idna").decode("ascii"))
754-
755-
# Slightly fake real-world example
756-
cert = {'notAfter': 'Jun 26 21:41:46 2011 GMT',
757-
'subject': ((('commonName', 'linuxfrz.org'),),),
758-
'subjectAltName': (('DNS', 'linuxfr.org'),
759-
('DNS', 'linuxfr.com'),
760-
('othername', '<unsupported>'))}
761-
ok(cert, 'linuxfr.org')
762-
ok(cert, 'linuxfr.com')
763-
# Not a "DNS" entry
764-
fail(cert, '<unsupported>')
765-
# When there is a subjectAltName, commonName isn't used
766-
fail(cert, 'linuxfrz.org')
767-
768-
# A pristine real-world example
769-
cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT',
770-
'subject': ((('countryName', 'US'),),
771-
(('stateOrProvinceName', 'California'),),
772-
(('localityName', 'Mountain View'),),
773-
(('organizationName', 'Google Inc'),),
774-
(('commonName', 'mail.google.com'),))}
775-
ok(cert, 'mail.google.com')
776-
fail(cert, 'gmail.com')
777-
# Only commonName is considered
778-
fail(cert, 'California')
779-
780-
# -- IPv4 matching --
781-
cert = {'subject': ((('commonName', 'example.com'),),),
782-
'subjectAltName': (('DNS', 'example.com'),
783-
('IP Address', '10.11.12.13'),
784-
('IP Address', '14.15.16.17'),
785-
('IP Address', '127.0.0.1'))}
786-
ok(cert, '10.11.12.13')
787-
ok(cert, '14.15.16.17')
788-
# socket.inet_ntoa(socket.inet_aton('127.1')) == '127.0.0.1'
789-
fail(cert, '127.1')
790-
fail(cert, '14.15.16.17 ')
791-
fail(cert, '14.15.16.17 extra data')
792-
fail(cert, '14.15.16.18')
793-
fail(cert, 'example.net')
794-
795-
# -- IPv6 matching --
796-
if socket_helper.IPV6_ENABLED:
797-
cert = {'subject': ((('commonName', 'example.com'),),),
798-
'subjectAltName': (
799-
('DNS', 'example.com'),
800-
('IP Address', '2001:0:0:0:0:0:0:CAFE\n'),
801-
('IP Address', '2003:0:0:0:0:0:0:BABA\n'))}
802-
ok(cert, '2001::cafe')
803-
ok(cert, '2003::baba')
804-
fail(cert, '2003::baba ')
805-
fail(cert, '2003::baba extra data')
806-
fail(cert, '2003::bebe')
807-
fail(cert, 'example.net')
808-
809-
# -- Miscellaneous --
810-
811-
# Neither commonName nor subjectAltName
812-
cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT',
813-
'subject': ((('countryName', 'US'),),
814-
(('stateOrProvinceName', 'California'),),
815-
(('localityName', 'Mountain View'),),
816-
(('organizationName', 'Google Inc'),))}
817-
fail(cert, 'mail.google.com')
818-
819-
# No DNS entry in subjectAltName but a commonName
820-
cert = {'notAfter': 'Dec 18 23:59:59 2099 GMT',
821-
'subject': ((('countryName', 'US'),),
822-
(('stateOrProvinceName', 'California'),),
823-
(('localityName', 'Mountain View'),),
824-
(('commonName', 'mail.google.com'),)),
825-
'subjectAltName': (('othername', 'blabla'), )}
826-
ok(cert, 'mail.google.com')
827-
828-
# No DNS entry subjectAltName and no commonName
829-
cert = {'notAfter': 'Dec 18 23:59:59 2099 GMT',
830-
'subject': ((('countryName', 'US'),),
831-
(('stateOrProvinceName', 'California'),),
832-
(('localityName', 'Mountain View'),),
833-
(('organizationName', 'Google Inc'),)),
834-
'subjectAltName': (('othername', 'blabla'),)}
835-
fail(cert, 'google.com')
836-
837-
# Empty cert / no cert
838-
self.assertRaises(ValueError, ssl.match_hostname, None, 'example.com')
839-
self.assertRaises(ValueError, ssl.match_hostname, {}, 'example.com')
840-
841-
# Issue #17980: avoid denials of service by refusing more than one
842-
# wildcard per fragment.
843-
cert = {'subject': ((('commonName', 'a*b.example.com'),),)}
844-
with self.assertRaisesRegex(
845-
ssl.CertificateError,
846-
"partial wildcards in leftmost label are not supported"):
847-
ssl.match_hostname(cert, 'axxb.example.com')
848-
849-
cert = {'subject': ((('commonName', 'www.*.example.com'),),)}
850-
with self.assertRaisesRegex(
851-
ssl.CertificateError,
852-
"wildcard can only be present in the leftmost label"):
853-
ssl.match_hostname(cert, 'www.sub.example.com')
854-
855-
cert = {'subject': ((('commonName', 'a*b*.example.com'),),)}
856-
with self.assertRaisesRegex(
857-
ssl.CertificateError,
858-
"too many wildcards"):
859-
ssl.match_hostname(cert, 'axxbxxc.example.com')
860-
861-
cert = {'subject': ((('commonName', '*'),),)}
862-
with self.assertRaisesRegex(
863-
ssl.CertificateError,
864-
"sole wildcard without additional labels are not support"):
865-
ssl.match_hostname(cert, 'host')
866-
867-
cert = {'subject': ((('commonName', '*.com'),),)}
868-
with self.assertRaisesRegex(
869-
ssl.CertificateError,
870-
r"hostname 'com' doesn't match '\*.com'"):
871-
ssl.match_hostname(cert, 'com')
872-
873-
# extra checks for _inet_paton()
874-
for invalid in ['1', '', '1.2.3', '256.0.0.1', '127.0.0.1/24']:
875-
with self.assertRaises(ValueError):
876-
ssl._inet_paton(invalid)
877-
for ipaddr in ['127.0.0.1', '192.168.0.1']:
878-
self.assertTrue(ssl._inet_paton(ipaddr))
879-
if socket_helper.IPV6_ENABLED:
880-
for ipaddr in ['::1', '2001:db8:85a3::8a2e:370:7334']:
881-
self.assertTrue(ssl._inet_paton(ipaddr))
882-
883684
def test_server_side(self):
884685
# server_hostname doesn't work for server sockets
885686
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Remove the :func:`ssl.match_hostname` function. The
2+
:func:`ssl.match_hostname` was deprecated in Python 3.7. OpenSSL performs
3+
hostname matching since Python 3.7, Python no longer uses the
4+
:func:`ssl.match_hostname` function. Patch by Victor Stinner.

0 commit comments

Comments
 (0)