##// END OF EJS Templates
sslutil: don't access message attribute in exception (issue5285)...
Gregory Szorc -
r29460:a7d1532b stable
parent child Browse files
Show More
@@ -1,382 +1,382 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import os
13 13 import re
14 14 import ssl
15 15 import sys
16 16
17 17 from .i18n import _
18 18 from . import (
19 19 error,
20 20 util,
21 21 )
22 22
23 23 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
24 24 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
25 25 # all exposed via the "ssl" module.
26 26 #
27 27 # Depending on the version of Python being used, SSL/TLS support is either
28 28 # modern/secure or legacy/insecure. Many operations in this module have
29 29 # separate code paths depending on support in Python.
30 30
31 31 hassni = getattr(ssl, 'HAS_SNI', False)
32 32
33 33 try:
34 34 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
35 35 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
36 36 except AttributeError:
37 37 OP_NO_SSLv2 = 0x1000000
38 38 OP_NO_SSLv3 = 0x2000000
39 39
40 40 try:
41 41 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
42 42 # SSL/TLS features are available.
43 43 SSLContext = ssl.SSLContext
44 44 modernssl = True
45 45 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
46 46 except AttributeError:
47 47 modernssl = False
48 48 _canloaddefaultcerts = False
49 49
50 50 # We implement SSLContext using the interface from the standard library.
51 51 class SSLContext(object):
52 52 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
53 53 _supportsciphers = sys.version_info >= (2, 7)
54 54
55 55 def __init__(self, protocol):
56 56 # From the public interface of SSLContext
57 57 self.protocol = protocol
58 58 self.check_hostname = False
59 59 self.options = 0
60 60 self.verify_mode = ssl.CERT_NONE
61 61
62 62 # Used by our implementation.
63 63 self._certfile = None
64 64 self._keyfile = None
65 65 self._certpassword = None
66 66 self._cacerts = None
67 67 self._ciphers = None
68 68
69 69 def load_cert_chain(self, certfile, keyfile=None, password=None):
70 70 self._certfile = certfile
71 71 self._keyfile = keyfile
72 72 self._certpassword = password
73 73
74 74 def load_default_certs(self, purpose=None):
75 75 pass
76 76
77 77 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
78 78 if capath:
79 79 raise error.Abort('capath not supported')
80 80 if cadata:
81 81 raise error.Abort('cadata not supported')
82 82
83 83 self._cacerts = cafile
84 84
85 85 def set_ciphers(self, ciphers):
86 86 if not self._supportsciphers:
87 87 raise error.Abort('setting ciphers not supported')
88 88
89 89 self._ciphers = ciphers
90 90
91 91 def wrap_socket(self, socket, server_hostname=None, server_side=False):
92 92 # server_hostname is unique to SSLContext.wrap_socket and is used
93 93 # for SNI in that context. So there's nothing for us to do with it
94 94 # in this legacy code since we don't support SNI.
95 95
96 96 args = {
97 97 'keyfile': self._keyfile,
98 98 'certfile': self._certfile,
99 99 'server_side': server_side,
100 100 'cert_reqs': self.verify_mode,
101 101 'ssl_version': self.protocol,
102 102 'ca_certs': self._cacerts,
103 103 }
104 104
105 105 if self._supportsciphers:
106 106 args['ciphers'] = self._ciphers
107 107
108 108 return ssl.wrap_socket(socket, **args)
109 109
110 110 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
111 111 ca_certs=None, serverhostname=None):
112 112 """Add SSL/TLS to a socket.
113 113
114 114 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
115 115 choices based on what security options are available.
116 116
117 117 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
118 118 the following additional arguments:
119 119
120 120 * serverhostname - The expected hostname of the remote server. If the
121 121 server (and client) support SNI, this tells the server which certificate
122 122 to use.
123 123 """
124 124 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
125 125 # that both ends support, including TLS protocols. On legacy stacks,
126 126 # the highest it likely goes in TLS 1.0. On modern stacks, it can
127 127 # support TLS 1.2.
128 128 #
129 129 # The PROTOCOL_TLSv* constants select a specific TLS version
130 130 # only (as opposed to multiple versions). So the method for
131 131 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
132 132 # disable protocols via SSLContext.options and OP_NO_* constants.
133 133 # However, SSLContext.options doesn't work unless we have the
134 134 # full/real SSLContext available to us.
135 135 #
136 136 # SSLv2 and SSLv3 are broken. We ban them outright.
137 137 if modernssl:
138 138 protocol = ssl.PROTOCOL_SSLv23
139 139 else:
140 140 protocol = ssl.PROTOCOL_TLSv1
141 141
142 142 # TODO use ssl.create_default_context() on modernssl.
143 143 sslcontext = SSLContext(protocol)
144 144
145 145 # This is a no-op on old Python.
146 146 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
147 147
148 148 # This still works on our fake SSLContext.
149 149 sslcontext.verify_mode = cert_reqs
150 150
151 151 if certfile is not None:
152 152 def password():
153 153 f = keyfile or certfile
154 154 return ui.getpass(_('passphrase for %s: ') % f, '')
155 155 sslcontext.load_cert_chain(certfile, keyfile, password)
156 156
157 157 if ca_certs is not None:
158 158 sslcontext.load_verify_locations(cafile=ca_certs)
159 159 else:
160 160 # This is a no-op on old Python.
161 161 sslcontext.load_default_certs()
162 162
163 163 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
164 164 # check if wrap_socket failed silently because socket had been
165 165 # closed
166 166 # - see http://bugs.python.org/issue13721
167 167 if not sslsocket.cipher():
168 168 raise error.Abort(_('ssl connection failed'))
169 169 return sslsocket
170 170
171 171 class wildcarderror(Exception):
172 172 """Represents an error parsing wildcards in DNS name."""
173 173
174 174 def _dnsnamematch(dn, hostname, maxwildcards=1):
175 175 """Match DNS names according RFC 6125 section 6.4.3.
176 176
177 177 This code is effectively copied from CPython's ssl._dnsname_match.
178 178
179 179 Returns a bool indicating whether the expected hostname matches
180 180 the value in ``dn``.
181 181 """
182 182 pats = []
183 183 if not dn:
184 184 return False
185 185
186 186 pieces = dn.split(r'.')
187 187 leftmost = pieces[0]
188 188 remainder = pieces[1:]
189 189 wildcards = leftmost.count('*')
190 190 if wildcards > maxwildcards:
191 191 raise wildcarderror(
192 192 _('too many wildcards in certificate DNS name: %s') % dn)
193 193
194 194 # speed up common case w/o wildcards
195 195 if not wildcards:
196 196 return dn.lower() == hostname.lower()
197 197
198 198 # RFC 6125, section 6.4.3, subitem 1.
199 199 # The client SHOULD NOT attempt to match a presented identifier in which
200 200 # the wildcard character comprises a label other than the left-most label.
201 201 if leftmost == '*':
202 202 # When '*' is a fragment by itself, it matches a non-empty dotless
203 203 # fragment.
204 204 pats.append('[^.]+')
205 205 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
206 206 # RFC 6125, section 6.4.3, subitem 3.
207 207 # The client SHOULD NOT attempt to match a presented identifier
208 208 # where the wildcard character is embedded within an A-label or
209 209 # U-label of an internationalized domain name.
210 210 pats.append(re.escape(leftmost))
211 211 else:
212 212 # Otherwise, '*' matches any dotless string, e.g. www*
213 213 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
214 214
215 215 # add the remaining fragments, ignore any wildcards
216 216 for frag in remainder:
217 217 pats.append(re.escape(frag))
218 218
219 219 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
220 220 return pat.match(hostname) is not None
221 221
222 222 def _verifycert(cert, hostname):
223 223 '''Verify that cert (in socket.getpeercert() format) matches hostname.
224 224 CRLs is not handled.
225 225
226 226 Returns error message if any problems are found and None on success.
227 227 '''
228 228 if not cert:
229 229 return _('no certificate received')
230 230
231 231 dnsnames = []
232 232 san = cert.get('subjectAltName', [])
233 233 for key, value in san:
234 234 if key == 'DNS':
235 235 try:
236 236 if _dnsnamematch(value, hostname):
237 237 return
238 238 except wildcarderror as e:
239 return e.message
239 return e.args[0]
240 240
241 241 dnsnames.append(value)
242 242
243 243 if not dnsnames:
244 244 # The subject is only checked when there is no DNS in subjectAltName.
245 245 for sub in cert.get('subject', []):
246 246 for key, value in sub:
247 247 # According to RFC 2818 the most specific Common Name must
248 248 # be used.
249 249 if key == 'commonName':
250 250 # 'subject' entries are unicide.
251 251 try:
252 252 value = value.encode('ascii')
253 253 except UnicodeEncodeError:
254 254 return _('IDN in certificate not supported')
255 255
256 256 try:
257 257 if _dnsnamematch(value, hostname):
258 258 return
259 259 except wildcarderror as e:
260 return e.message
260 return e.args[0]
261 261
262 262 dnsnames.append(value)
263 263
264 264 if len(dnsnames) > 1:
265 265 return _('certificate is for %s') % ', '.join(dnsnames)
266 266 elif len(dnsnames) == 1:
267 267 return _('certificate is for %s') % dnsnames[0]
268 268 else:
269 269 return _('no commonName or subjectAltName found in certificate')
270 270
271 271
272 272 # CERT_REQUIRED means fetch the cert from the server all the time AND
273 273 # validate it against the CA store provided in web.cacerts.
274 274
275 275 def _plainapplepython():
276 276 """return true if this seems to be a pure Apple Python that
277 277 * is unfrozen and presumably has the whole mercurial module in the file
278 278 system
279 279 * presumably is an Apple Python that uses Apple OpenSSL which has patches
280 280 for using system certificate store CAs in addition to the provided
281 281 cacerts file
282 282 """
283 283 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
284 284 return False
285 285 exe = os.path.realpath(sys.executable).lower()
286 286 return (exe.startswith('/usr/bin/python') or
287 287 exe.startswith('/system/library/frameworks/python.framework/'))
288 288
289 289 def _defaultcacerts():
290 290 """return path to CA certificates; None for system's store; ! to disable"""
291 291 if _plainapplepython():
292 292 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
293 293 if os.path.exists(dummycert):
294 294 return dummycert
295 295 if _canloaddefaultcerts:
296 296 return None
297 297 return '!'
298 298
299 299 def sslkwargs(ui, host):
300 300 kws = {'ui': ui}
301 301 hostfingerprint = ui.config('hostfingerprints', host)
302 302 if hostfingerprint:
303 303 return kws
304 304 cacerts = ui.config('web', 'cacerts')
305 305 if cacerts == '!':
306 306 pass
307 307 elif cacerts:
308 308 cacerts = util.expandpath(cacerts)
309 309 if not os.path.exists(cacerts):
310 310 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
311 311 else:
312 312 cacerts = _defaultcacerts()
313 313 if cacerts and cacerts != '!':
314 314 ui.debug('using %s to enable OS X system CA\n' % cacerts)
315 315 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
316 316 if cacerts != '!':
317 317 kws.update({'ca_certs': cacerts,
318 318 'cert_reqs': ssl.CERT_REQUIRED,
319 319 })
320 320 return kws
321 321
322 322 class validator(object):
323 323 def __init__(self, ui, host):
324 324 self.ui = ui
325 325 self.host = host
326 326
327 327 def __call__(self, sock, strict=False):
328 328 host = self.host
329 329
330 330 if not sock.cipher(): # work around http://bugs.python.org/issue13721
331 331 raise error.Abort(_('%s ssl connection error') % host)
332 332 try:
333 333 peercert = sock.getpeercert(True)
334 334 peercert2 = sock.getpeercert()
335 335 except AttributeError:
336 336 raise error.Abort(_('%s ssl connection error') % host)
337 337
338 338 if not peercert:
339 339 raise error.Abort(_('%s certificate error: '
340 340 'no certificate received') % host)
341 341
342 342 # If a certificate fingerprint is pinned, use it and only it to
343 343 # validate the remote cert.
344 344 hostfingerprints = self.ui.configlist('hostfingerprints', host)
345 345 peerfingerprint = util.sha1(peercert).hexdigest()
346 346 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
347 347 for x in xrange(0, len(peerfingerprint), 2)])
348 348 if hostfingerprints:
349 349 fingerprintmatch = False
350 350 for hostfingerprint in hostfingerprints:
351 351 if peerfingerprint.lower() == \
352 352 hostfingerprint.replace(':', '').lower():
353 353 fingerprintmatch = True
354 354 break
355 355 if not fingerprintmatch:
356 356 raise error.Abort(_('certificate for %s has unexpected '
357 357 'fingerprint %s') % (host, nicefingerprint),
358 358 hint=_('check hostfingerprint configuration'))
359 359 self.ui.debug('%s certificate matched fingerprint %s\n' %
360 360 (host, nicefingerprint))
361 361 return
362 362
363 363 # No pinned fingerprint. Establish trust by looking at the CAs.
364 364 cacerts = self.ui.config('web', 'cacerts')
365 365 if cacerts != '!':
366 366 msg = _verifycert(peercert2, host)
367 367 if msg:
368 368 raise error.Abort(_('%s certificate error: %s') % (host, msg),
369 369 hint=_('configure hostfingerprint %s or use '
370 370 '--insecure to connect insecurely') %
371 371 nicefingerprint)
372 372 self.ui.debug('%s certificate successfully verified\n' % host)
373 373 elif strict:
374 374 raise error.Abort(_('%s certificate with fingerprint %s not '
375 375 'verified') % (host, nicefingerprint),
376 376 hint=_('check hostfingerprints or web.cacerts '
377 377 'config setting'))
378 378 else:
379 379 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
380 380 'verified (check hostfingerprints or web.cacerts '
381 381 'config setting)\n') %
382 382 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now