Kaydet (Commit) 88790781 authored tarafından Joffrey F's avatar Joffrey F

Merge pull request #1013 from docker/hostname_ip_matching

Hostname IP matching
......@@ -4,11 +4,11 @@ MAINTAINER Joffrey F <joffrey@docker.com>
RUN mkdir /home/docker-py
WORKDIR /home/docker-py
ADD requirements.txt /home/docker-py/requirements.txt
COPY requirements.txt /home/docker-py/requirements.txt
RUN pip install -r requirements.txt
ADD test-requirements.txt /home/docker-py/test-requirements.txt
COPY test-requirements.txt /home/docker-py/test-requirements.txt
RUN pip install -r test-requirements.txt
ADD . /home/docker-py
COPY . /home/docker-py
RUN pip install .
......@@ -4,11 +4,11 @@ MAINTAINER Joffrey F <joffrey@docker.com>
RUN mkdir /home/docker-py
WORKDIR /home/docker-py
ADD requirements.txt /home/docker-py/requirements.txt
COPY requirements3.txt /home/docker-py/requirements.txt
RUN pip install -r requirements.txt
ADD test-requirements.txt /home/docker-py/test-requirements.txt
COPY test-requirements.txt /home/docker-py/test-requirements.txt
RUN pip install -r test-requirements.txt
ADD . /home/docker-py
COPY . /home/docker-py
RUN pip install .
# 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"
)
......@@ -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.'''
......
......@@ -31,7 +31,8 @@ RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class UnixHTTPConnection(httplib.HTTPConnection, object):
def __init__(self, base_url, unix_socket, timeout=60):
super(UnixHTTPConnection, self).__init__(
'localhost', timeout=timeout)
'localhost', timeout=timeout
)
self.base_url = base_url
self.unix_socket = unix_socket
self.timeout = timeout
......@@ -46,7 +47,8 @@ class UnixHTTPConnection(httplib.HTTPConnection, object):
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, base_url, socket_path, timeout=60):
super(UnixHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout)
'localhost', timeout=timeout
)
self.base_url = base_url
self.socket_path = socket_path
self.timeout = timeout
......
requests==2.5.3
six>=1.4.0
websocket-client==0.32.0
py2-ipaddress==3.4.1
\ No newline at end of file
requests==2.5.3
six>=1.4.0
websocket-client==0.32.0
\ No newline at end of file
......@@ -12,6 +12,9 @@ requirements = [
'websocket-client >= 0.32.0',
]
if sys.version_info[0] == 2:
requirements.append('py2-ipaddress >= 3.4.1')
exec(open('docker/version.py').read())
with open('./test-requirements.txt') as test_reqs_txt:
......
from docker.ssladapter import ssladapter
from docker.ssladapter.ssl_match_hostname import (
match_hostname, CertificateError
)
try:
from ssl import OP_NO_SSLv3, OP_NO_SSLv2, OP_NO_TLSv1
except ImportError:
OP_NO_SSLv2 = 0x1000000
OP_NO_SSLv3 = 0x2000000
OP_NO_TLSv1 = 0x4000000
from .. import base
class SSLAdapterTest(base.BaseTestCase):
def test_only_uses_tls(self):
ssl_context = ssladapter.urllib3.util.ssl_.create_urllib3_context()
assert ssl_context.options & OP_NO_SSLv3
assert ssl_context.options & OP_NO_SSLv2
assert not ssl_context.options & OP_NO_TLSv1
class MatchHostnameTest(base.BaseTestCase):
cert = {
'issuer': (
(('countryName', u'US'),),
(('stateOrProvinceName', u'California'),),
(('localityName', u'San Francisco'),),
(('organizationName', u'Docker Inc'),),
(('organizationalUnitName', u'Docker-Python'),),
(('commonName', u'localhost'),),
(('emailAddress', u'info@docker.com'),)
),
'notAfter': 'Mar 25 23:08:23 2030 GMT',
'notBefore': u'Mar 25 23:08:23 2016 GMT',
'serialNumber': u'BD5F894C839C548F',
'subject': (
(('countryName', u'US'),),
(('stateOrProvinceName', u'California'),),
(('localityName', u'San Francisco'),),
(('organizationName', u'Docker Inc'),),
(('organizationalUnitName', u'Docker-Python'),),
(('commonName', u'localhost'),),
(('emailAddress', u'info@docker.com'),)
),
'subjectAltName': (
('DNS', u'localhost'),
('DNS', u'*.gensokyo.jp'),
('IP Address', u'127.0.0.1'),
),
'version': 3
}
def test_match_ip_address_success(self):
assert match_hostname(self.cert, '127.0.0.1') is None
def test_match_localhost_success(self):
assert match_hostname(self.cert, 'localhost') is None
def test_match_dns_success(self):
assert match_hostname(self.cert, 'touhou.gensokyo.jp') is None
def test_match_ip_address_failure(self):
self.assertRaises(
CertificateError, match_hostname, self.cert, '192.168.0.25'
)
def test_match_dns_failure(self):
self.assertRaises(
CertificateError, match_hostname, self.cert, 'foobar.co.uk'
)
......@@ -12,17 +12,9 @@ import tempfile
import pytest
import six
try:
from ssl import OP_NO_SSLv3, OP_NO_SSLv2, OP_NO_TLSv1
except ImportError:
OP_NO_SSLv2 = 0x1000000
OP_NO_SSLv3 = 0x2000000
OP_NO_TLSv1 = 0x4000000
from docker.client import Client
from docker.constants import DEFAULT_DOCKER_API_VERSION
from docker.errors import DockerException, InvalidVersion
from docker.ssladapter import ssladapter
from docker.utils import (
parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
create_host_config, Ulimit, LogConfig, parse_bytes, parse_env_file,
......@@ -962,12 +954,3 @@ class TarTest(base.Cleanup, base.BaseTestCase):
self.assertEqual(
sorted(tar_data.getnames()), ['bar', 'bar/foo', 'foo']
)
class SSLAdapterTest(base.BaseTestCase):
def test_only_uses_tls(self):
ssl_context = ssladapter.urllib3.util.ssl_.create_urllib3_context()
assert ssl_context.options & OP_NO_SSLv3
assert ssl_context.options & OP_NO_SSLv2
assert not ssl_context.options & OP_NO_TLSv1
......@@ -8,7 +8,8 @@ commands =
py.test --cov=docker {posargs:tests/unit}
deps =
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
{py26,py27}: -r{toxinidir}/requirements.txt
{py33,py34}: -r{toxinidir}/requirements3.txt
[testenv:flake8]
commands = flake8 docker tests
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment