]> granicus.if.org Git - python/commitdiff
Issue #23239: ssl.match_hostname() now supports matching of IP addresses.
authorAntoine Pitrou <solipsis@pitrou.net>
Sun, 15 Feb 2015 17:12:20 +0000 (18:12 +0100)
committerAntoine Pitrou <solipsis@pitrou.net>
Sun, 15 Feb 2015 17:12:20 +0000 (18:12 +0100)
Doc/library/ssl.rst
Lib/ssl.py
Lib/test/test_ssl.py
Misc/NEWS

index e7cf4250e2dc8eae43a4caba010ef74c0a72cfcb..254fc1fe18da3df130758a5b2adc39625ebf143f 100644 (file)
@@ -344,10 +344,9 @@ Certificate handling
    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` and :rfc:`6125`, except that IP addresses are not currently
-   supported. 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.
+   in :rfc:`2818` 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::
@@ -369,6 +368,10 @@ Certificate handling
       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.
+
 .. function:: cert_time_to_seconds(cert_time)
 
    Return the time in seconds since the Epoch, given the ``cert_time``
index 807e9f2896d19f346f78f5ede21cca1105255b3e..56bc38e5e1818dadd3b60dbe0228f8b379868c06 100644 (file)
@@ -87,6 +87,7 @@ ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE
 ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY
 """
 
+import ipaddress
 import textwrap
 import re
 import sys
@@ -242,6 +243,17 @@ def _dnsname_match(dn, hostname, max_wildcards=1):
     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):
     """Verify that *cert* (in decoded format as returned by
     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 "
                          "SSL socket or SSL context with either "
                          "CERT_OPTIONAL or CERT_REQUIRED")
+    try:
+        host_ip = ipaddress.ip_address(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 _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
             dnsnames.append(value)
     if not dnsnames:
index 2f2e739f77cd293ec3927bf1af4ce23676de235f..ce427b4db2c71ce52c8ea7c9b48ad80dda1d91fe 100644 (file)
@@ -383,6 +383,8 @@ class BasicSocketTests(unittest.TestCase):
             self.assertRaises(ssl.CertificateError,
                               ssl.match_hostname, cert, hostname)
 
+        # -- Hostname matching --
+
         cert = {'subject': ((('commonName', 'example.com'),),)}
         ok(cert, 'example.com')
         ok(cert, 'ExAmple.cOm')
@@ -468,6 +470,28 @@ class BasicSocketTests(unittest.TestCase):
         # 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'))}
+        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
         cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT',
                 'subject': ((('countryName', 'US'),),
index d44015146734a51cba0e55201f8e49bd32808463..3712a98431d546b8929a18fd1e8b9e72c148133d 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -13,6 +13,8 @@ Core and Builtins
 Library
 -------
 
+- Issue #23239: ssl.match_hostname() now supports matching of IP addresses.
+
 - Issue #23146: Fix mishandling of absolute Windows paths with forward
   slashes in pathlib.