comparison mercurial/sslutil.py @ 29452:26a5d605b868 stable 3.8.4

sslutil: synchronize hostname matching logic with CPython sslutil contains its own hostname matching logic. CPython has code for the same intent. However, it is only available to Python 2.7.9+ (or distributions that have backported 2.7.9's ssl module improvements). This patch effectively imports CPython's hostname matching code from its ssl.py into sslutil.py. The hostname matching code itself is pretty similar. However, the DNS name matching code is much more robust and spec conformant. As the test changes show, this changes some behavior around wildcard handling and IDNA matching. The new behavior allows wildcards in the middle of words (e.g. 'f*.com' matches 'foo.com') This is spec compliant according to RFC 6125 Section 6.5.3 item 3. There is one test where the matcher is more strict. Before, '*.a.com' matched '.a.com'. Now it doesn't match. Strictly speaking this is a security vulnerability.
author Gregory Szorc <gregory.szorc@gmail.com>
date Sun, 26 Jun 2016 19:34:48 -0700
parents 693b856a4d45
children fd93b15b5c30 a7d1532b26a1
comparison
equal deleted inserted replaced
29451:676f4d0e3a7b 29452:26a5d605b868
8 # GNU General Public License version 2 or any later version. 8 # GNU General Public License version 2 or any later version.
9 9
10 from __future__ import absolute_import 10 from __future__ import absolute_import
11 11
12 import os 12 import os
13 import re
13 import ssl 14 import ssl
14 import sys 15 import sys
15 16
16 from .i18n import _ 17 from .i18n import _
17 from . import ( 18 from . import (
165 # - see http://bugs.python.org/issue13721 166 # - see http://bugs.python.org/issue13721
166 if not sslsocket.cipher(): 167 if not sslsocket.cipher():
167 raise error.Abort(_('ssl connection failed')) 168 raise error.Abort(_('ssl connection failed'))
168 return sslsocket 169 return sslsocket
169 170
171 class wildcarderror(Exception):
172 """Represents an error parsing wildcards in DNS name."""
173
174 def _dnsnamematch(dn, hostname, maxwildcards=1):
175 """Match DNS names according RFC 6125 section 6.4.3.
176
177 This code is effectively copied from CPython's ssl._dnsname_match.
178
179 Returns a bool indicating whether the expected hostname matches
180 the value in ``dn``.
181 """
182 pats = []
183 if not dn:
184 return False
185
186 pieces = dn.split(r'.')
187 leftmost = pieces[0]
188 remainder = pieces[1:]
189 wildcards = leftmost.count('*')
190 if wildcards > maxwildcards:
191 raise wildcarderror(
192 _('too many wildcards in certificate DNS name: %s') % dn)
193
194 # speed up common case w/o wildcards
195 if not wildcards:
196 return dn.lower() == hostname.lower()
197
198 # RFC 6125, section 6.4.3, subitem 1.
199 # The client SHOULD NOT attempt to match a presented identifier in which
200 # the wildcard character comprises a label other than the left-most label.
201 if leftmost == '*':
202 # When '*' is a fragment by itself, it matches a non-empty dotless
203 # fragment.
204 pats.append('[^.]+')
205 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
206 # RFC 6125, section 6.4.3, subitem 3.
207 # The client SHOULD NOT attempt to match a presented identifier
208 # where the wildcard character is embedded within an A-label or
209 # U-label of an internationalized domain name.
210 pats.append(re.escape(leftmost))
211 else:
212 # Otherwise, '*' matches any dotless string, e.g. www*
213 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
214
215 # add the remaining fragments, ignore any wildcards
216 for frag in remainder:
217 pats.append(re.escape(frag))
218
219 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
220 return pat.match(hostname) is not None
221
170 def _verifycert(cert, hostname): 222 def _verifycert(cert, hostname):
171 '''Verify that cert (in socket.getpeercert() format) matches hostname. 223 '''Verify that cert (in socket.getpeercert() format) matches hostname.
172 CRLs is not handled. 224 CRLs is not handled.
173 225
174 Returns error message if any problems are found and None on success. 226 Returns error message if any problems are found and None on success.
175 ''' 227 '''
176 if not cert: 228 if not cert:
177 return _('no certificate received') 229 return _('no certificate received')
178 dnsname = hostname.lower() 230
179 def matchdnsname(certname): 231 dnsnames = []
180 return (certname == dnsname or
181 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
182
183 san = cert.get('subjectAltName', []) 232 san = cert.get('subjectAltName', [])
184 if san: 233 for key, value in san:
185 certnames = [value.lower() for key, value in san if key == 'DNS'] 234 if key == 'DNS':
186 for name in certnames:
187 if matchdnsname(name):
188 return None
189 if certnames:
190 return _('certificate is for %s') % ', '.join(certnames)
191
192 # subject is only checked when subjectAltName is empty
193 for s in cert.get('subject', []):
194 key, value = s[0]
195 if key == 'commonName':
196 try: 235 try:
197 # 'subject' entries are unicode 236 if _dnsnamematch(value, hostname):
198 certname = value.lower().encode('ascii') 237 return
199 except UnicodeEncodeError: 238 except wildcarderror as e:
200 return _('IDN in certificate not supported') 239 return e.message
201 if matchdnsname(certname): 240
202 return None 241 dnsnames.append(value)
203 return _('certificate is for %s') % certname 242
204 return _('no commonName or subjectAltName found in certificate') 243 if not dnsnames:
244 # The subject is only checked when there is no DNS in subjectAltName.
245 for sub in cert.get('subject', []):
246 for key, value in sub:
247 # According to RFC 2818 the most specific Common Name must
248 # be used.
249 if key == 'commonName':
250 # 'subject' entries are unicide.
251 try:
252 value = value.encode('ascii')
253 except UnicodeEncodeError:
254 return _('IDN in certificate not supported')
255
256 try:
257 if _dnsnamematch(value, hostname):
258 return
259 except wildcarderror as e:
260 return e.message
261
262 dnsnames.append(value)
263
264 if len(dnsnames) > 1:
265 return _('certificate is for %s') % ', '.join(dnsnames)
266 elif len(dnsnames) == 1:
267 return _('certificate is for %s') % dnsnames[0]
268 else:
269 return _('no commonName or subjectAltName found in certificate')
205 270
206 271
207 # CERT_REQUIRED means fetch the cert from the server all the time AND 272 # CERT_REQUIRED means fetch the cert from the server all the time AND
208 # validate it against the CA store provided in web.cacerts. 273 # validate it against the CA store provided in web.cacerts.
209 274