diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2013-11-03 08:12:40 -0800 |
---|---|---|
committer | Jason R. Coombs <jaraco@jaraco.com> | 2013-11-03 08:12:40 -0800 |
commit | 6360097ac51941b83b52b006eedce60ddcf312f9 (patch) | |
tree | 9388eefff0d01f3c62da03da0d4094283095e9fb | |
parent | 2644c5e54c7d5dae012a09f7d73d473d31aa2857 (diff) | |
parent | ad6bce6ab02836ea6d90e69e5c6f3b851532874a (diff) | |
download | external_python_setuptools-6360097ac51941b83b52b006eedce60ddcf312f9.tar.gz external_python_setuptools-6360097ac51941b83b52b006eedce60ddcf312f9.tar.bz2 external_python_setuptools-6360097ac51941b83b52b006eedce60ddcf312f9.zip |
Merge pull request #2 from abadger/feature/ssl-match-hostname-17997
There's apparently another security issue in the python3 match_hostname code. No CVE has been issued for it yet:
http://bugs.python.org/issue17997#msg194950
This merge includes two commits. The first updates the included match_hostname code to reflect what's in the python-3.3.3 and python-3.4 stdlib (with a minor change to preserve python2 compat). The second commit adds a check for the backports.ssl_match_hostname module from pypi: https://pypi.python.org/pypi/backports.ssl_match_hostname
If the stdlib doesn't have ssl_match_hostname but backports.ssl_match_hostname exists it uses that code. If neither one are present, then it uses the code included in setuptools.
Using backports.ssl_match_hostname helps system packagers and system admins to have a single place to maintain SSL support rather than every package that's copying the match_hostname code. On the other hand, it means that users won't get any fixes before they go into the backports.ssl_match_hostname module. Brandon Rhodes is the owner of that module and Toshio has done the last several releases to make sure that module is current with the match_hostname security issues.
-rw-r--r-- | setuptools/ssl_support.py | 85 |
1 files changed, 63 insertions, 22 deletions
diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py index 90359b2c..e1cf8040 100644 --- a/setuptools/ssl_support.py +++ b/setuptools/ssl_support.py @@ -85,33 +85,74 @@ except ImportError: try: from ssl import CertificateError, match_hostname except ImportError: + try: + from backports.ssl_match_hostname import CertificateError + from backports.ssl_match_hostname import match_hostname + except ImportError: + CertificateError = None + match_hostname = None + +if not CertificateError: class CertificateError(ValueError): pass - def _dnsname_to_pat(dn, max_wildcards=1): +if not match_hostname: + 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 + + # 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. @@ -122,7 +163,7 @@ except ImportError: 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: @@ -133,7 +174,7 @@ except ImportError: # 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: |