summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoffrey F <joffrey@docker.com>2016-03-29 17:08:31 -0700
committerJoffrey F <joffrey@docker.com>2016-03-29 17:08:31 -0700
commit0a5815bcad613fe445820e9f565381fa051e0e2c (patch)
treefd5d184bdb063169538b217ed89d80f5507e889e
parentb0e234eb0cf716c705f416534d39a91a06226233 (diff)
downloaddocker-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.py130
-rw-r--r--docker/ssladapter/ssladapter.py9
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.'''