Skip to content

Commit 0a5815b

Browse files
committed
Add match_hostname implementation and monkey-patch for py<3.5
Signed-off-by: Joffrey F <[email protected]>
1 parent b0e234e commit 0a5815b

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Slightly modified version of match_hostname in python's ssl library
2+
# https://hg.python.org/cpython/file/tip/Lib/ssl.py
3+
# Changed to make code python 2.x compatible (unicode strings for ip_address
4+
# and 3.5-specific var assignment syntax)
5+
6+
import ipaddress
7+
import re
8+
9+
try:
10+
from ssl import CertificateError
11+
except ImportError:
12+
CertificateError = ValueError
13+
14+
import six
15+
16+
17+
def _ipaddress_match(ipname, host_ip):
18+
"""Exact matching of IP addresses.
19+
20+
RFC 6125 explicitly doesn't define an algorithm for this
21+
(section 1.7.2 - "Out of Scope").
22+
"""
23+
# OpenSSL may add a trailing newline to a subjectAltName's IP address
24+
ip = ipaddress.ip_address(six.text_type(ipname.rstrip()))
25+
return ip == host_ip
26+
27+
28+
def _dnsname_match(dn, hostname, max_wildcards=1):
29+
"""Matching according to RFC 6125, section 6.4.3
30+
31+
http://tools.ietf.org/html/rfc6125#section-6.4.3
32+
"""
33+
pats = []
34+
if not dn:
35+
return False
36+
37+
split_dn = dn.split(r'.')
38+
leftmost, remainder = split_dn[0], split_dn[1:]
39+
40+
wildcards = leftmost.count('*')
41+
if wildcards > max_wildcards:
42+
# Issue #17980: avoid denials of service by refusing more
43+
# than one wildcard per fragment. A survey of established
44+
# policy among SSL implementations showed it to be a
45+
# reasonable choice.
46+
raise CertificateError(
47+
"too many wildcards in certificate DNS name: " + repr(dn))
48+
49+
# speed up common case w/o wildcards
50+
if not wildcards:
51+
return dn.lower() == hostname.lower()
52+
53+
# RFC 6125, section 6.4.3, subitem 1.
54+
# The client SHOULD NOT attempt to match a presented identifier in which
55+
# the wildcard character comprises a label other than the left-most label.
56+
if leftmost == '*':
57+
# When '*' is a fragment by itself, it matches a non-empty dotless
58+
# fragment.
59+
pats.append('[^.]+')
60+
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
61+
# RFC 6125, section 6.4.3, subitem 3.
62+
# The client SHOULD NOT attempt to match a presented identifier
63+
# where the wildcard character is embedded within an A-label or
64+
# U-label of an internationalized domain name.
65+
pats.append(re.escape(leftmost))
66+
else:
67+
# Otherwise, '*' matches any dotless string, e.g. www*
68+
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
69+
70+
# add the remaining fragments, ignore any wildcards
71+
for frag in remainder:
72+
pats.append(re.escape(frag))
73+
74+
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
75+
return pat.match(hostname)
76+
77+
78+
def match_hostname(cert, hostname):
79+
"""Verify that *cert* (in decoded format as returned by
80+
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
81+
rules are followed, but IP addresses are not accepted for *hostname*.
82+
83+
CertificateError is raised on failure. On success, the function
84+
returns nothing.
85+
"""
86+
if not cert:
87+
raise ValueError("empty or no certificate, match_hostname needs a "
88+
"SSL socket or SSL context with either "
89+
"CERT_OPTIONAL or CERT_REQUIRED")
90+
try:
91+
host_ip = ipaddress.ip_address(six.text_type(hostname))
92+
except ValueError:
93+
# Not an IP address (common case)
94+
host_ip = None
95+
dnsnames = []
96+
san = cert.get('subjectAltName', ())
97+
for key, value in san:
98+
if key == 'DNS':
99+
if host_ip is None and _dnsname_match(value, hostname):
100+
return
101+
dnsnames.append(value)
102+
elif key == 'IP Address':
103+
if host_ip is not None and _ipaddress_match(value, host_ip):
104+
return
105+
dnsnames.append(value)
106+
if not dnsnames:
107+
# The subject is only checked when there is no dNSName entry
108+
# in subjectAltName
109+
for sub in cert.get('subject', ()):
110+
for key, value in sub:
111+
# XXX according to RFC 2818, the most specific Common Name
112+
# must be used.
113+
if key == 'commonName':
114+
if _dnsname_match(value, hostname):
115+
return
116+
dnsnames.append(value)
117+
if len(dnsnames) > 1:
118+
raise CertificateError(
119+
"hostname %r doesn't match either of %s"
120+
% (hostname, ', '.join(map(repr, dnsnames))))
121+
elif len(dnsnames) == 1:
122+
raise CertificateError(
123+
"hostname %r doesn't match %r"
124+
% (hostname, dnsnames[0])
125+
)
126+
else:
127+
raise CertificateError(
128+
"no appropriate commonName or "
129+
"subjectAltName fields were found"
130+
)

docker/ssladapter/ssladapter.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
33
https://github.com/kennethreitz/requests/pull/799
44
"""
5+
import sys
6+
57
from distutils.version import StrictVersion
68
from requests.adapters import HTTPAdapter
79

@@ -10,8 +12,15 @@
1012
except ImportError:
1113
import urllib3
1214

15+
1316
PoolManager = urllib3.poolmanager.PoolManager
1417

18+
# Monkey-patching match_hostname with a version that supports
19+
# IP-address checking. Not necessary for Python 3.5 and above
20+
if sys.version_info[0] < 3 or sys.version_info[1] < 5:
21+
from .ssl_match_hostname import match_hostname
22+
urllib3.connection.match_hostname = match_hostname
23+
1524

1625
class SSLAdapter(HTTPAdapter):
1726
'''An HTTPS Transport Adapter that uses an arbitrary SSL version.'''

0 commit comments

Comments
 (0)