[python3/f19] Change behavior of ssl.match_hostname() to follow RFC 6125 (rhbz#1023742)

Matej Stuchlik mstuchli at fedoraproject.org
Fri Nov 8 13:31:10 UTC 2013


commit 92d5ecc93bd61eded7f1d092cd83117512f80251
Author: Matej Stuchlik <mstuchli at redhat.com>
Date:   Fri Nov 8 12:11:59 2013 +0100

    Change behavior of ssl.match_hostname() to follow RFC 6125 (rhbz#1023742)

 ...-change-match_hostname-to-follow-RFC-6125.patch |  188 ++++++++++++++++++++
 python3.spec                                       |   12 +-
 2 files changed, 198 insertions(+), 2 deletions(-)
---
diff --git a/00187-change-match_hostname-to-follow-RFC-6125.patch b/00187-change-match_hostname-to-follow-RFC-6125.patch
new file mode 100644
index 0000000..0f02276
--- /dev/null
+++ b/00187-change-match_hostname-to-follow-RFC-6125.patch
@@ -0,0 +1,188 @@
+--- a/Doc/library/ssl.rst
++++ b/Doc/library/ssl.rst
+@@ -283,10 +283,10 @@ 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`, 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`, 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.
+ 
+    :exc:`CertificateError` is raised on failure. On success, the function
+    returns nothing::
+@@ -301,6 +301,13 @@ Certificate handling
+ 
+    .. versionadded:: 3.2
+ 
++   .. versionchanged:: 3.3.3
++      The function now follows :rfc:`6125`, section 6.4.3 and does neither
++      match multiple wildcards (e.g. ``*.*.com`` or ``*a*.example.org``) nor
++      a wildcard inside an internationalized domain names (IDN) fragment.
++      IDN A-labels such as ``www*.xn--pthon-kva.org`` are still supported,
++      but ``x*.python.org`` no longer matches ``xn--tda.python.org``.
++
+ .. function:: cert_time_to_seconds(timestring)
+ 
+    Returns a floating-point value containing a normal seconds-after-the-epoch
+unchanged:
+--- a/Lib/ssl.py
++++ b/Lib/ssl.py
+@@ -129,25 +129,53 @@ 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 survery 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
++
++    leftmost, *remainder = dn.split(r'.')
++
++    wildcards = leftmost.count('*')
++    if wildcards > max_wildcards:
++        # Issue #17980: avoid denials of service by refusing more
++        # than one wildcard per fragment.  A survery 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):
+unchanged:
+--- a/Lib/test/test_ssl.py
++++ b/Lib/test/test_ssl.py
+@@ -307,11 +307,7 @@ class BasicSocketTests(unittest.TestCase
+         fail(cert, 'Xa.com')
+         fail(cert, '.a.com')
+ 
+-        cert = {'subject': ((('commonName', 'a.*.com'),),)}
+-        ok(cert, 'a.foo.com')
+-        fail(cert, 'a..com')
+-        fail(cert, 'a.com')
+-
++        # only match one left-most wildcard
+         cert = {'subject': ((('commonName', 'f*.com'),),)}
+         ok(cert, 'foo.com')
+         ok(cert, 'f.com')
+@@ -326,6 +322,36 @@ class BasicSocketTests(unittest.TestCase
+         fail(cert, 'example.org')
+         fail(cert, 'null.python.org')
+ 
++        # error cases with wildcards
++        cert = {'subject': ((('commonName', '*.*.a.com'),),)}
++        fail(cert, 'bar.foo.a.com')
++        fail(cert, 'a.com')
++        fail(cert, 'Xa.com')
++        fail(cert, '.a.com')
++
++        cert = {'subject': ((('commonName', 'a.*.com'),),)}
++        fail(cert, 'a.foo.com')
++        fail(cert, 'a..com')
++        fail(cert, 'a.com')
++
++        # wildcard doesn't match IDNA prefix 'xn--'
++        idna = 'püthon.python.org'.encode("idna").decode("ascii")
++        cert = {'subject': ((('commonName', idna),),)}
++        ok(cert, idna)
++        cert = {'subject': ((('commonName', 'x*.python.org'),),)}
++        fail(cert, idna)
++        cert = {'subject': ((('commonName', 'xn--p*.python.org'),),)}
++        fail(cert, idna)
++
++        # wildcard in first fragment and  IDNA A-labels in sequent fragments
++        # are supported.
++        idna = 'www*.pythön.org'.encode("idna").decode("ascii")
++        cert = {'subject': ((('commonName', idna),),)}
++        ok(cert, 'www.pythön.org'.encode("idna").decode("ascii"))
++        ok(cert, 'www1.pythön.org'.encode("idna").decode("ascii"))
++        fail(cert, 'ftp.pythön.org'.encode("idna").decode("ascii"))
++        fail(cert, 'pythön.org'.encode("idna").decode("ascii"))
++
+         # Slightly fake real-world example
+         cert = {'notAfter': 'Jun 26 21:41:46 2011 GMT',
+                 'subject': ((('commonName', 'linuxfrz.org'),),),
+@@ -386,7 +412,7 @@ class BasicSocketTests(unittest.TestCase
+         cert = {'subject': ((('commonName', 'a*b.com'),),)}
+         ok(cert, 'axxb.com')
+         cert = {'subject': ((('commonName', 'a*b.co*'),),)}
+-        ok(cert, 'axxb.com')
++        fail(cert, 'axxb.com')
+         cert = {'subject': ((('commonName', 'a*b*.com'),),)}
+         with self.assertRaises(ssl.CertificateError) as cm:
+             ssl.match_hostname(cert, 'axxbxxc.com')
+--- a/Lib/ssl.py
++++ b/Lib/ssl.py
+@@ -192,7 +192,7 @@ def match_hostname(cert, hostname):
+     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:
+@@ -203,7 +203,7 @@ def match_hostname(cert, hostname):
+                 # 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:
diff --git a/python3.spec b/python3.spec
index a824745..cd99a8a 100644
--- a/python3.spec
+++ b/python3.spec
@@ -126,7 +126,7 @@
 Summary: Version 3 of the Python programming language aka Python 3000
 Name: python3
 Version: %{pybasever}.2
-Release: 7%{?dist}
+Release: 8%{?dist}
 License: Python
 Group: Development/Languages
 
@@ -623,6 +623,11 @@ Patch185: 00185-CVE-2013-4238-hostname-check-bypass-in-SSL-module.patch
 # continuing bytecompilation for other files.
 Patch186: 00186-dont-raise-from-py_compile.patch
 
+# 00187 #
+# Fix for rhbz#1023742
+# Change behaviour of ssl.match_hostname() to follow RFC 6125
+# See http://bugs.python.org/issue17997#msg194950 for more.
+Patch187: 00187-change-match_hostname-to-follow-RFC-6125.patch
 
 
 # (New patches go here ^^^)
@@ -885,6 +890,7 @@ done
 %patch184 -p1
 %patch185 -p1
 %patch186 -p1
+%patch187 -p1
 
 # Currently (2010-01-15), http://docs.python.org/library is for 2.6, and there
 # are many differences between 2.6 and the Python 3 library.
@@ -1301,7 +1307,6 @@ sed \
 # ======================================================
 
 %check
-
 # first of all, check timestamps of bytecode files
 find %{buildroot} -type f -a -name "*.py" -print0 | \
     LD_LIBRARY_PATH="%{buildroot}%{dynload_dir}/:%{buildroot}%{_libdir}" \
@@ -1733,6 +1738,9 @@ rm -fr %{buildroot}
 # ======================================================
 
 %changelog
+* Fri Nov 08 2013 Matej Stuchlik <mstuchli at redhat.com> - 3.3.2-8
+- Changed behavior of ssl.match_hostname() to follow RFC 6125 (rhbz#1023742)
+
 * Wed Oct 30 2013 Bohuslav Kabrda <bkabrda at redhat.com> - 3.3.2-7
 - Bytecompile all *.py files properly during build (rhbz#1023607)
 


More information about the scm-commits mailing list