Mercurial > public > mercurial-scm > hg-stable
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 |