Issue #23239: ssl.match_hostname() now supports matching of IP addresses.

This commit is contained in:
Antoine Pitrou 2015-02-15 18:12:20 +01:00
parent 2d07b85585
commit c481bfb3f6
4 changed files with 55 additions and 5 deletions

View File

@ -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``

View File

@ -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:

View File

@ -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'),),

View File

@ -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.