Issue #23239: ssl.match_hostname() now supports matching of IP addresses.
This commit is contained in:
parent
2d07b85585
commit
c481bfb3f6
|
@ -344,10 +344,9 @@ Certificate handling
|
||||||
Verify that *cert* (in decoded format as returned by
|
Verify that *cert* (in decoded format as returned by
|
||||||
:meth:`SSLSocket.getpeercert`) matches the given *hostname*. The rules
|
:meth:`SSLSocket.getpeercert`) matches the given *hostname*. The rules
|
||||||
applied are those for checking the identity of HTTPS servers as outlined
|
applied are those for checking the identity of HTTPS servers as outlined
|
||||||
in :rfc:`2818` and :rfc:`6125`, except that IP addresses are not currently
|
in :rfc:`2818` and :rfc:`6125`. In addition to HTTPS, this function
|
||||||
supported. In addition to HTTPS, this function should be suitable for
|
should be suitable for checking the identity of servers in various
|
||||||
checking the identity of servers in various SSL-based protocols such as
|
SSL-based protocols such as FTPS, IMAPS, POPS and others.
|
||||||
FTPS, IMAPS, POPS and others.
|
|
||||||
|
|
||||||
:exc:`CertificateError` is raised on failure. On success, the function
|
:exc:`CertificateError` is raised on failure. On success, the function
|
||||||
returns nothing::
|
returns nothing::
|
||||||
|
@ -369,6 +368,10 @@ Certificate handling
|
||||||
IDN A-labels such as ``www*.xn--pthon-kva.org`` are still supported,
|
IDN A-labels such as ``www*.xn--pthon-kva.org`` are still supported,
|
||||||
but ``x*.python.org`` no longer matches ``xn--tda.python.org``.
|
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.
|
||||||
|
|
||||||
.. function:: cert_time_to_seconds(cert_time)
|
.. function:: cert_time_to_seconds(cert_time)
|
||||||
|
|
||||||
Return the time in seconds since the Epoch, given the ``cert_time``
|
Return the time in seconds since the Epoch, given the ``cert_time``
|
||||||
|
|
23
Lib/ssl.py
23
Lib/ssl.py
|
@ -87,6 +87,7 @@ ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE
|
||||||
ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY
|
ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import textwrap
|
import textwrap
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
@ -242,6 +243,17 @@ def _dnsname_match(dn, hostname, max_wildcards=1):
|
||||||
return pat.match(hostname)
|
return pat.match(hostname)
|
||||||
|
|
||||||
|
|
||||||
|
def _ipaddress_match(ipname, host_ip):
|
||||||
|
"""Exact matching of IP addresses.
|
||||||
|
|
||||||
|
RFC 6125 explicitly doesn't define an algorithm for this
|
||||||
|
(section 1.7.2 - "Out of Scope").
|
||||||
|
"""
|
||||||
|
# OpenSSL may add a trailing newline to a subjectAltName's IP address
|
||||||
|
ip = ipaddress.ip_address(ipname.rstrip())
|
||||||
|
return ip == host_ip
|
||||||
|
|
||||||
|
|
||||||
def match_hostname(cert, hostname):
|
def match_hostname(cert, hostname):
|
||||||
"""Verify that *cert* (in decoded format as returned by
|
"""Verify that *cert* (in decoded format as returned by
|
||||||
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
|
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
|
||||||
|
@ -254,11 +266,20 @@ def match_hostname(cert, hostname):
|
||||||
raise ValueError("empty or no certificate, match_hostname needs a "
|
raise ValueError("empty or no certificate, match_hostname needs a "
|
||||||
"SSL socket or SSL context with either "
|
"SSL socket or SSL context with either "
|
||||||
"CERT_OPTIONAL or CERT_REQUIRED")
|
"CERT_OPTIONAL or CERT_REQUIRED")
|
||||||
|
try:
|
||||||
|
host_ip = ipaddress.ip_address(hostname)
|
||||||
|
except ValueError:
|
||||||
|
# Not an IP address (common case)
|
||||||
|
host_ip = None
|
||||||
dnsnames = []
|
dnsnames = []
|
||||||
san = cert.get('subjectAltName', ())
|
san = cert.get('subjectAltName', ())
|
||||||
for key, value in san:
|
for key, value in san:
|
||||||
if key == 'DNS':
|
if key == 'DNS':
|
||||||
if _dnsname_match(value, hostname):
|
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
|
return
|
||||||
dnsnames.append(value)
|
dnsnames.append(value)
|
||||||
if not dnsnames:
|
if not dnsnames:
|
||||||
|
|
|
@ -383,6 +383,8 @@ class BasicSocketTests(unittest.TestCase):
|
||||||
self.assertRaises(ssl.CertificateError,
|
self.assertRaises(ssl.CertificateError,
|
||||||
ssl.match_hostname, cert, hostname)
|
ssl.match_hostname, cert, hostname)
|
||||||
|
|
||||||
|
# -- Hostname matching --
|
||||||
|
|
||||||
cert = {'subject': ((('commonName', 'example.com'),),)}
|
cert = {'subject': ((('commonName', 'example.com'),),)}
|
||||||
ok(cert, 'example.com')
|
ok(cert, 'example.com')
|
||||||
ok(cert, 'ExAmple.cOm')
|
ok(cert, 'ExAmple.cOm')
|
||||||
|
@ -468,6 +470,28 @@ class BasicSocketTests(unittest.TestCase):
|
||||||
# Only commonName is considered
|
# Only commonName is considered
|
||||||
fail(cert, 'California')
|
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'))}
|
||||||
|
ok(cert, '10.11.12.13')
|
||||||
|
ok(cert, '14.15.16.17')
|
||||||
|
fail(cert, '14.15.16.18')
|
||||||
|
fail(cert, 'example.net')
|
||||||
|
|
||||||
|
# -- IPv6 matching --
|
||||||
|
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::bebe')
|
||||||
|
fail(cert, 'example.net')
|
||||||
|
|
||||||
|
# -- Miscellaneous --
|
||||||
|
|
||||||
# Neither commonName nor subjectAltName
|
# Neither commonName nor subjectAltName
|
||||||
cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT',
|
cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT',
|
||||||
'subject': ((('countryName', 'US'),),
|
'subject': ((('countryName', 'US'),),
|
||||||
|
|
|
@ -13,6 +13,8 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #23239: ssl.match_hostname() now supports matching of IP addresses.
|
||||||
|
|
||||||
- Issue #23146: Fix mishandling of absolute Windows paths with forward
|
- Issue #23146: Fix mishandling of absolute Windows paths with forward
|
||||||
slashes in pathlib.
|
slashes in pathlib.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue