Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit 27f5381

Browse files
committed
Proper verification on PyOpenSSL
1 parent 70d39f8 commit 27f5381

File tree

2 files changed

+10
-96
lines changed

2 files changed

+10
-96
lines changed

hyper/ssl_compat.py

Lines changed: 6 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import re
2020

2121
from OpenSSL import SSL as ossl
22+
from service_identity.pyopenssl import verify_hostname as _verify
2223

2324
CERT_NONE = ossl.VERIFY_NONE
2425
CERT_REQUIRED = ossl.VERIFY_PEER | ossl.VERIFY_FAIL_IF_NO_PEER_CERT
@@ -51,102 +52,11 @@ class CertificateError(SSLError):
5152
pass
5253

5354

54-
# lifted from the Python 3.4 stdlib
55-
def _dnsname_match(dn, hostname, max_wildcards=1):
55+
def verify_hostname(ssl_sock, server_hostname):
5656
"""
57-
Matching according to RFC 6125, section 6.4.3.
58-
59-
See http://tools.ietf.org/html/rfc6125#section-6.4.3
60-
"""
61-
pats = []
62-
if not dn:
63-
return False
64-
65-
parts = dn.split(r'.')
66-
leftmost = parts[0]
67-
remainder = parts[1:]
68-
69-
wildcards = leftmost.count('*')
70-
if wildcards > max_wildcards:
71-
# Issue #17980: avoid denials of service by refusing more
72-
# than one wildcard per fragment. A survery of established
73-
# policy among SSL implementations showed it to be a
74-
# reasonable choice.
75-
raise CertificateError(
76-
"too many wildcards in certificate DNS name: " + repr(dn))
77-
78-
# speed up common case w/o wildcards
79-
if not wildcards:
80-
return dn.lower() == hostname.lower()
81-
82-
# RFC 6125, section 6.4.3, subitem 1.
83-
# The client SHOULD NOT attempt to match a presented identifier in which
84-
# the wildcard character comprises a label other than the left-most label.
85-
if leftmost == '*':
86-
# When '*' is a fragment by itself, it matches a non-empty dotless
87-
# fragment.
88-
pats.append('[^.]+')
89-
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
90-
# RFC 6125, section 6.4.3, subitem 3.
91-
# The client SHOULD NOT attempt to match a presented identifier
92-
# where the wildcard character is embedded within an A-label or
93-
# U-label of an internationalized domain name.
94-
pats.append(re.escape(leftmost))
95-
else:
96-
# Otherwise, '*' matches any dotless string, e.g. www*
97-
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
98-
99-
# add the remaining fragments, ignore any wildcards
100-
for frag in remainder:
101-
pats.append(re.escape(frag))
102-
103-
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
104-
return pat.match(hostname)
105-
106-
107-
# lifted from the Python 3.4 stdlib
108-
def match_hostname(cert, hostname):
109-
"""
110-
Verify that ``cert`` (in decoded format as returned by
111-
``SSLSocket.getpeercert())`` matches the ``hostname``. RFC 2818 and RFC
112-
6125 rules are followed, but IP addresses are not accepted for ``hostname``.
113-
114-
``CertificateError`` is raised on failure. On success, the function returns
115-
nothing.
57+
A method nearly compatible with the stdlib's match_hostname.
11658
"""
117-
if not cert:
118-
raise ValueError("empty or no certificate, match_hostname needs a "
119-
"SSL socket or SSL context with either "
120-
"CERT_OPTIONAL or CERT_REQUIRED")
121-
dnsnames = []
122-
san = cert.get('subjectAltName', ())
123-
for key, value in san:
124-
if key == 'DNS':
125-
if _dnsname_match(value, hostname):
126-
return
127-
dnsnames.append(value)
128-
if not dnsnames:
129-
# The subject is only checked when there is no dNSName entry
130-
# in subjectAltName
131-
for sub in cert.get('subject', ()):
132-
for key, value in sub:
133-
# XXX according to RFC 2818, the most specific Common Name
134-
# must be used.
135-
if key == 'commonName':
136-
if _dnsname_match(value, hostname):
137-
return
138-
dnsnames.append(value)
139-
if len(dnsnames) > 1:
140-
raise CertificateError("hostname %r "
141-
"doesn't match either of %s"
142-
% (hostname, ', '.join(map(repr, dnsnames))))
143-
elif len(dnsnames) == 1:
144-
raise CertificateError("hostname %r "
145-
"doesn't match %r"
146-
% (hostname, dnsnames[0]))
147-
else:
148-
raise CertificateError("no appropriate commonName or "
149-
"subjectAltName fields were found")
59+
return _verify(ssl_sock._conn, server_hostname)
15060

15161

15262
class SSLSocket(object):
@@ -165,6 +75,7 @@ def __init__(self, conn, server_side, do_handshake_on_connect,
16575
else:
16676
if server_hostname:
16777
self._conn.set_tlsext_host_name(server_hostname.encode('utf-8'))
78+
self._server_hostname = server_hostname
16879
self._conn.set_connect_state() # FIXME does this override do_handshake_on_connect=False?
16980

17081
if self.connected and self._do_handshake_on_connect:
@@ -211,7 +122,7 @@ def connect(self, address):
211122
def do_handshake(self):
212123
self._safe_ssl_call(False, self._conn.do_handshake)
213124
if self._check_hostname:
214-
match_hostname(self.getpeercert(), self._conn.get_servername().decode('utf-8'))
125+
verify_hostname(self, self._server_hostname)
215126

216127
def recv(self, bufsize, flags=None):
217128
return self._safe_ssl_call(self._suppress_ragged_eofs, self._conn.recv,

hyper/tls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ def wrap_socket(sock, server_hostname):
3838
# post-handshake servername matches that of the certificate. We also need to
3939
# check that it matches the requested one.
4040
if _context.check_hostname: # pragma: no cover
41-
ssl.match_hostname(ssl_sock.getpeercert(), server_hostname)
41+
try:
42+
ssl.match_hostname(ssl_sock.getpeercert(), server_hostname)
43+
except AttributeError:
44+
ssl.verify_hostname(ssl_sock, server_hostname) # pyopenssl
4245

4346
proto = None
4447
with ignore_missing():

0 commit comments

Comments
 (0)