Commits

Anonymous committed abe0367

Fix for https://bitbucket.org/brandon/backports.ssl_match_hostname/issue/2 This
update uses even newer code in the python-3.3.3 and python-3.4.0 hg repository
to deal with another security flaw

  • Participants
  • Parent commits ebfbe23

Comments (0)

Files changed (2)

 
-The ssl.match_hostname() function from Python 3.2
+The ssl.match_hostname() function from Python 3.4
 =================================================
 
 The Secure Sockets layer is only actually *secure*
 But the matching logic, defined in `RFC2818`_,
 can be a bit tricky to implement on your own.
 So the ``ssl`` package in the Standard Library of Python 3.2
-now includes a ``match_hostname()`` function
+and greater now includes a ``match_hostname()`` function
 for performing this check instead of requiring every application
 to implement the check separately.
 
 from the Python Package Index to use code like that shown above.
 
 Brandon Craig Rhodes is merely the packager of this distribution;
-the actual code inside comes verbatim from Python 3.4a1.
+the actual code inside comes verbatim from Python 3.4.
+
+History
+-------
+* This function was introduced in python-3.2
+* It was updated for python-3.4a1 for a CVE 
+  (backports-ssl_match_hostname-3.4.0.1)
+* It was updated from RFC2818 to RFC 6125 compliance in order to fix another
+  security flaw for python-3.3.3 and python-3.4a5
+  (backports-ssl_match_hostname-3.4.0.2)
+
 
 .. _RFC2818: http://tools.ietf.org/html/rfc2818.html
 
-"""The match_hostname() function from Python 3.4a1, essential when using SSL."""
+"""The match_hostname() function from Python 3.3.3, essential when using SSL."""
 
 import re
 
-__version__ = '3.4.0.1'
+__version__ = '3.4.0.2'
 
 class CertificateError(ValueError):
     pass
 
-def _dnsname_to_pat(dn, max_wildcards=1):
+
+def _dnsname_match(dn, hostname, max_wildcards=1):
+    """Matching according to RFC 6125, section 6.4.3
+
+    http://tools.ietf.org/html/rfc6125#section-6.4.3
+    """
     pats = []
-    for frag in dn.split(r'.'):
-        if frag.count('*') > max_wildcards:
-            # Issue #17980: avoid denials of service by refusing more
-            # than one wildcard per fragment.  A survey of established
-            # policy among SSL implementations showed it to be a
-            # reasonable choice.
-            raise CertificateError(
-                "too many wildcards in certificate DNS name: " + repr(dn))
-        if frag == '*':
-            # When '*' is a fragment by itself, it matches a non-empty dotless
-            # fragment.
-            pats.append('[^.]+')
-        else:
-            # Otherwise, '*' matches any dotless fragment.
-            frag = re.escape(frag)
-            pats.append(frag.replace(r'\*', '[^.]*'))
-    return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
+    if not dn:
+        return False
+
+    # Ported from python3-syntax:
+    # leftmost, *remainder = dn.split(r'.')
+    parts = dn.split(r'.')
+    leftmost = parts[0]
+    remainder = parts[1:]
+
+    wildcards = leftmost.count('*')
+    if wildcards > max_wildcards:
+        # Issue #17980: avoid denials of service by refusing more
+        # than one wildcard per fragment.  A survey of established
+        # policy among SSL implementations showed it to be a
+        # reasonable choice.
+        raise CertificateError(
+            "too many wildcards in certificate DNS name: " + repr(dn))
+
+    # speed up common case w/o wildcards
+    if not wildcards:
+        return dn.lower() == hostname.lower()
+
+    # RFC 6125, section 6.4.3, subitem 1.
+    # The client SHOULD NOT attempt to match a presented identifier in which
+    # the wildcard character comprises a label other than the left-most label.
+    if leftmost == '*':
+        # When '*' is a fragment by itself, it matches a non-empty dotless
+        # fragment.
+        pats.append('[^.]+')
+    elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
+        # RFC 6125, section 6.4.3, subitem 3.
+        # The client SHOULD NOT attempt to match a presented identifier
+        # where the wildcard character is embedded within an A-label or
+        # U-label of an internationalized domain name.
+        pats.append(re.escape(leftmost))
+    else:
+        # Otherwise, '*' matches any dotless string, e.g. www*
+        pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
+
+    # add the remaining fragments, ignore any wildcards
+    for frag in remainder:
+        pats.append(re.escape(frag))
+
+    pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
+    return pat.match(hostname)
+
 
 def match_hostname(cert, hostname):
     """Verify that *cert* (in decoded format as returned by
-    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 rules
-    are mostly followed, but IP addresses are not accepted for *hostname*.
+    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 and RFC 6125
+    rules are followed, but IP addresses are not accepted for *hostname*.
 
     CertificateError is raised on failure. On success, the function
     returns nothing.
     san = cert.get('subjectAltName', ())
     for key, value in san:
         if key == 'DNS':
-            if _dnsname_to_pat(value).match(hostname):
+            if _dnsname_match(value, hostname):
                 return
             dnsnames.append(value)
     if not dnsnames:
                 # XXX according to RFC 2818, the most specific Common Name
                 # must be used.
                 if key == 'commonName':
-                    if _dnsname_to_pat(value).match(hostname):
+                    if _dnsname_match(value, hostname):
                         return
                     dnsnames.append(value)
     if len(dnsnames) > 1: