diff options
author | Joffrey F <joffrey@docker.com> | 2016-03-29 17:08:31 -0700 |
---|---|---|
committer | Joffrey F <joffrey@docker.com> | 2016-03-29 17:08:31 -0700 |
commit | 0a5815bcad613fe445820e9f565381fa051e0e2c (patch) | |
tree | fd5d184bdb063169538b217ed89d80f5507e889e | |
parent | b0e234eb0cf716c705f416534d39a91a06226233 (diff) | |
download | docker-py-0a5815bcad613fe445820e9f565381fa051e0e2c.tar.gz |
Add match_hostname implementation and monkey-patch for py<3.5
Signed-off-by: Joffrey F <joffrey@docker.com>
-rw-r--r-- | docker/ssladapter/ssl_match_hostname.py | 130 | ||||
-rw-r--r-- | docker/ssladapter/ssladapter.py | 9 |
2 files changed, 139 insertions, 0 deletions
diff --git a/docker/ssladapter/ssl_match_hostname.py b/docker/ssladapter/ssl_match_hostname.py new file mode 100644 index 0000000..9de0c5f --- /dev/null +++ b/docker/ssladapter/ssl_match_hostname.py @@ -0,0 +1,130 @@ +# Slightly modified version of match_hostname in python's ssl library +# https://hg.python.org/cpython/file/tip/Lib/ssl.py +# Changed to make code python 2.x compatible (unicode strings for ip_address +# and 3.5-specific var assignment syntax) + +import ipaddress +import re + +try: + from ssl import CertificateError +except ImportError: + CertificateError = ValueError + +import six + + +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(six.text_type(ipname.rstrip())) + return ip == host_ip + + +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 = [] + if not dn: + return False + + split_dn = dn.split(r'.') + leftmost, remainder = split_dn[0], split_dn[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 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. + """ + if not cert: + 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(six.text_type(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 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: + # The subject is only checked when there is no dNSName entry + # in subjectAltName + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError( + "hostname %r doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError( + "hostname %r doesn't match %r" + % (hostname, dnsnames[0]) + ) + else: + raise CertificateError( + "no appropriate commonName or " + "subjectAltName fields were found" + ) diff --git a/docker/ssladapter/ssladapter.py b/docker/ssladapter/ssladapter.py index 5b43aa2..179510c 100644 --- a/docker/ssladapter/ssladapter.py +++ b/docker/ssladapter/ssladapter.py @@ -2,6 +2,8 @@ https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/ https://github.com/kennethreitz/requests/pull/799 """ +import sys + from distutils.version import StrictVersion from requests.adapters import HTTPAdapter @@ -10,8 +12,15 @@ try: except ImportError: import urllib3 + PoolManager = urllib3.poolmanager.PoolManager +# Monkey-patching match_hostname with a version that supports +# IP-address checking. Not necessary for Python 3.5 and above +if sys.version_info[0] < 3 or sys.version_info[1] < 5: + from .ssl_match_hostname import match_hostname + urllib3.connection.match_hostname = match_hostname + class SSLAdapter(HTTPAdapter): '''An HTTPS Transport Adapter that uses an arbitrary SSL version.''' |