##// END OF EJS Templates
ssl: handle a difference in SSLError with pypy (issue5348)...
Pierre-Yves David -
r29927:799e3674 default
parent child Browse files
Show More
@@ -1,840 +1,844 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 hashlib
13 13 import os
14 14 import re
15 15 import ssl
16 16 import sys
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 error,
21 21 util,
22 22 )
23 23
24 24 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
25 25 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
26 26 # all exposed via the "ssl" module.
27 27 #
28 28 # Depending on the version of Python being used, SSL/TLS support is either
29 29 # modern/secure or legacy/insecure. Many operations in this module have
30 30 # separate code paths depending on support in Python.
31 31
32 32 configprotocols = set([
33 33 'tls1.0',
34 34 'tls1.1',
35 35 'tls1.2',
36 36 ])
37 37
38 38 hassni = getattr(ssl, 'HAS_SNI', False)
39 39
40 40 # TLS 1.1 and 1.2 may not be supported if the OpenSSL Python is compiled
41 41 # against doesn't support them.
42 42 supportedprotocols = set(['tls1.0'])
43 43 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_1'):
44 44 supportedprotocols.add('tls1.1')
45 45 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_2'):
46 46 supportedprotocols.add('tls1.2')
47 47
48 48 try:
49 49 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
50 50 # SSL/TLS features are available.
51 51 SSLContext = ssl.SSLContext
52 52 modernssl = True
53 53 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
54 54 except AttributeError:
55 55 modernssl = False
56 56 _canloaddefaultcerts = False
57 57
58 58 # We implement SSLContext using the interface from the standard library.
59 59 class SSLContext(object):
60 60 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
61 61 _supportsciphers = sys.version_info >= (2, 7)
62 62
63 63 def __init__(self, protocol):
64 64 # From the public interface of SSLContext
65 65 self.protocol = protocol
66 66 self.check_hostname = False
67 67 self.options = 0
68 68 self.verify_mode = ssl.CERT_NONE
69 69
70 70 # Used by our implementation.
71 71 self._certfile = None
72 72 self._keyfile = None
73 73 self._certpassword = None
74 74 self._cacerts = None
75 75 self._ciphers = None
76 76
77 77 def load_cert_chain(self, certfile, keyfile=None, password=None):
78 78 self._certfile = certfile
79 79 self._keyfile = keyfile
80 80 self._certpassword = password
81 81
82 82 def load_default_certs(self, purpose=None):
83 83 pass
84 84
85 85 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
86 86 if capath:
87 87 raise error.Abort(_('capath not supported'))
88 88 if cadata:
89 89 raise error.Abort(_('cadata not supported'))
90 90
91 91 self._cacerts = cafile
92 92
93 93 def set_ciphers(self, ciphers):
94 94 if not self._supportsciphers:
95 95 raise error.Abort(_('setting ciphers in [hostsecurity] is not '
96 96 'supported by this version of Python'),
97 97 hint=_('remove the config option or run '
98 98 'Mercurial with a modern Python '
99 99 'version (preferred)'))
100 100
101 101 self._ciphers = ciphers
102 102
103 103 def wrap_socket(self, socket, server_hostname=None, server_side=False):
104 104 # server_hostname is unique to SSLContext.wrap_socket and is used
105 105 # for SNI in that context. So there's nothing for us to do with it
106 106 # in this legacy code since we don't support SNI.
107 107
108 108 args = {
109 109 'keyfile': self._keyfile,
110 110 'certfile': self._certfile,
111 111 'server_side': server_side,
112 112 'cert_reqs': self.verify_mode,
113 113 'ssl_version': self.protocol,
114 114 'ca_certs': self._cacerts,
115 115 }
116 116
117 117 if self._supportsciphers:
118 118 args['ciphers'] = self._ciphers
119 119
120 120 return ssl.wrap_socket(socket, **args)
121 121
122 122 def _hostsettings(ui, hostname):
123 123 """Obtain security settings for a hostname.
124 124
125 125 Returns a dict of settings relevant to that hostname.
126 126 """
127 127 s = {
128 128 # Whether we should attempt to load default/available CA certs
129 129 # if an explicit ``cafile`` is not defined.
130 130 'allowloaddefaultcerts': True,
131 131 # List of 2-tuple of (hash algorithm, hash).
132 132 'certfingerprints': [],
133 133 # Path to file containing concatenated CA certs. Used by
134 134 # SSLContext.load_verify_locations().
135 135 'cafile': None,
136 136 # Whether certificate verification should be disabled.
137 137 'disablecertverification': False,
138 138 # Whether the legacy [hostfingerprints] section has data for this host.
139 139 'legacyfingerprint': False,
140 140 # PROTOCOL_* constant to use for SSLContext.__init__.
141 141 'protocol': None,
142 142 # String representation of minimum protocol to be used for UI
143 143 # presentation.
144 144 'protocolui': None,
145 145 # ssl.CERT_* constant used by SSLContext.verify_mode.
146 146 'verifymode': None,
147 147 # Defines extra ssl.OP* bitwise options to set.
148 148 'ctxoptions': None,
149 149 # OpenSSL Cipher List to use (instead of default).
150 150 'ciphers': None,
151 151 }
152 152
153 153 # Allow minimum TLS protocol to be specified in the config.
154 154 def validateprotocol(protocol, key):
155 155 if protocol not in configprotocols:
156 156 raise error.Abort(
157 157 _('unsupported protocol from hostsecurity.%s: %s') %
158 158 (key, protocol),
159 159 hint=_('valid protocols: %s') %
160 160 ' '.join(sorted(configprotocols)))
161 161
162 162 # We default to TLS 1.1+ where we can because TLS 1.0 has known
163 163 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to
164 164 # TLS 1.0+ via config options in case a legacy server is encountered.
165 165 if 'tls1.1' in supportedprotocols:
166 166 defaultprotocol = 'tls1.1'
167 167 else:
168 168 # Let people know they are borderline secure.
169 169 # We don't document this config option because we want people to see
170 170 # the bold warnings on the web site.
171 171 # internal config: hostsecurity.disabletls10warning
172 172 if not ui.configbool('hostsecurity', 'disabletls10warning'):
173 173 ui.warn(_('warning: connecting to %s using legacy security '
174 174 'technology (TLS 1.0); see '
175 175 'https://mercurial-scm.org/wiki/SecureConnections for '
176 176 'more info\n') % hostname)
177 177 defaultprotocol = 'tls1.0'
178 178
179 179 key = 'minimumprotocol'
180 180 protocol = ui.config('hostsecurity', key, defaultprotocol)
181 181 validateprotocol(protocol, key)
182 182
183 183 key = '%s:minimumprotocol' % hostname
184 184 protocol = ui.config('hostsecurity', key, protocol)
185 185 validateprotocol(protocol, key)
186 186
187 187 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
188 188 # We always print a "connection security to %s is disabled..." message when
189 189 # --insecure is used. So no need to print anything more here.
190 190 if ui.insecureconnections:
191 191 protocol = 'tls1.0'
192 192
193 193 s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol)
194 194
195 195 ciphers = ui.config('hostsecurity', 'ciphers')
196 196 ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers)
197 197 s['ciphers'] = ciphers
198 198
199 199 # Look for fingerprints in [hostsecurity] section. Value is a list
200 200 # of <alg>:<fingerprint> strings.
201 201 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
202 202 [])
203 203 for fingerprint in fingerprints:
204 204 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
205 205 raise error.Abort(_('invalid fingerprint for %s: %s') % (
206 206 hostname, fingerprint),
207 207 hint=_('must begin with "sha1:", "sha256:", '
208 208 'or "sha512:"'))
209 209
210 210 alg, fingerprint = fingerprint.split(':', 1)
211 211 fingerprint = fingerprint.replace(':', '').lower()
212 212 s['certfingerprints'].append((alg, fingerprint))
213 213
214 214 # Fingerprints from [hostfingerprints] are always SHA-1.
215 215 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
216 216 fingerprint = fingerprint.replace(':', '').lower()
217 217 s['certfingerprints'].append(('sha1', fingerprint))
218 218 s['legacyfingerprint'] = True
219 219
220 220 # If a host cert fingerprint is defined, it is the only thing that
221 221 # matters. No need to validate CA certs.
222 222 if s['certfingerprints']:
223 223 s['verifymode'] = ssl.CERT_NONE
224 224 s['allowloaddefaultcerts'] = False
225 225
226 226 # If --insecure is used, don't take CAs into consideration.
227 227 elif ui.insecureconnections:
228 228 s['disablecertverification'] = True
229 229 s['verifymode'] = ssl.CERT_NONE
230 230 s['allowloaddefaultcerts'] = False
231 231
232 232 if ui.configbool('devel', 'disableloaddefaultcerts'):
233 233 s['allowloaddefaultcerts'] = False
234 234
235 235 # If both fingerprints and a per-host ca file are specified, issue a warning
236 236 # because users should not be surprised about what security is or isn't
237 237 # being performed.
238 238 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
239 239 if s['certfingerprints'] and cafile:
240 240 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
241 241 'fingerprints defined; using host fingerprints for '
242 242 'verification)\n') % hostname)
243 243
244 244 # Try to hook up CA certificate validation unless something above
245 245 # makes it not necessary.
246 246 if s['verifymode'] is None:
247 247 # Look at per-host ca file first.
248 248 if cafile:
249 249 cafile = util.expandpath(cafile)
250 250 if not os.path.exists(cafile):
251 251 raise error.Abort(_('path specified by %s does not exist: %s') %
252 252 ('hostsecurity.%s:verifycertsfile' % hostname,
253 253 cafile))
254 254 s['cafile'] = cafile
255 255 else:
256 256 # Find global certificates file in config.
257 257 cafile = ui.config('web', 'cacerts')
258 258
259 259 if cafile:
260 260 cafile = util.expandpath(cafile)
261 261 if not os.path.exists(cafile):
262 262 raise error.Abort(_('could not find web.cacerts: %s') %
263 263 cafile)
264 264 elif s['allowloaddefaultcerts']:
265 265 # CAs not defined in config. Try to find system bundles.
266 266 cafile = _defaultcacerts(ui)
267 267 if cafile:
268 268 ui.debug('using %s for CA file\n' % cafile)
269 269
270 270 s['cafile'] = cafile
271 271
272 272 # Require certificate validation if CA certs are being loaded and
273 273 # verification hasn't been disabled above.
274 274 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
275 275 s['verifymode'] = ssl.CERT_REQUIRED
276 276 else:
277 277 # At this point we don't have a fingerprint, aren't being
278 278 # explicitly insecure, and can't load CA certs. Connecting
279 279 # is insecure. We allow the connection and abort during
280 280 # validation (once we have the fingerprint to print to the
281 281 # user).
282 282 s['verifymode'] = ssl.CERT_NONE
283 283
284 284 assert s['protocol'] is not None
285 285 assert s['ctxoptions'] is not None
286 286 assert s['verifymode'] is not None
287 287
288 288 return s
289 289
290 290 def protocolsettings(protocol):
291 291 """Resolve the protocol for a config value.
292 292
293 293 Returns a 3-tuple of (protocol, options, ui value) where the first
294 294 2 items are values used by SSLContext and the last is a string value
295 295 of the ``minimumprotocol`` config option equivalent.
296 296 """
297 297 if protocol not in configprotocols:
298 298 raise ValueError('protocol value not supported: %s' % protocol)
299 299
300 300 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
301 301 # that both ends support, including TLS protocols. On legacy stacks,
302 302 # the highest it likely goes is TLS 1.0. On modern stacks, it can
303 303 # support TLS 1.2.
304 304 #
305 305 # The PROTOCOL_TLSv* constants select a specific TLS version
306 306 # only (as opposed to multiple versions). So the method for
307 307 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
308 308 # disable protocols via SSLContext.options and OP_NO_* constants.
309 309 # However, SSLContext.options doesn't work unless we have the
310 310 # full/real SSLContext available to us.
311 311 if supportedprotocols == set(['tls1.0']):
312 312 if protocol != 'tls1.0':
313 313 raise error.Abort(_('current Python does not support protocol '
314 314 'setting %s') % protocol,
315 315 hint=_('upgrade Python or disable setting since '
316 316 'only TLS 1.0 is supported'))
317 317
318 318 return ssl.PROTOCOL_TLSv1, 0, 'tls1.0'
319 319
320 320 # WARNING: returned options don't work unless the modern ssl module
321 321 # is available. Be careful when adding options here.
322 322
323 323 # SSLv2 and SSLv3 are broken. We ban them outright.
324 324 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
325 325
326 326 if protocol == 'tls1.0':
327 327 # Defaults above are to use TLS 1.0+
328 328 pass
329 329 elif protocol == 'tls1.1':
330 330 options |= ssl.OP_NO_TLSv1
331 331 elif protocol == 'tls1.2':
332 332 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
333 333 else:
334 334 raise error.Abort(_('this should not happen'))
335 335
336 336 # Prevent CRIME.
337 337 # There is no guarantee this attribute is defined on the module.
338 338 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
339 339
340 340 return ssl.PROTOCOL_SSLv23, options, protocol
341 341
342 342 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
343 343 """Add SSL/TLS to a socket.
344 344
345 345 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
346 346 choices based on what security options are available.
347 347
348 348 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
349 349 the following additional arguments:
350 350
351 351 * serverhostname - The expected hostname of the remote server. If the
352 352 server (and client) support SNI, this tells the server which certificate
353 353 to use.
354 354 """
355 355 if not serverhostname:
356 356 raise error.Abort(_('serverhostname argument is required'))
357 357
358 358 settings = _hostsettings(ui, serverhostname)
359 359
360 360 # We can't use ssl.create_default_context() because it calls
361 361 # load_default_certs() unless CA arguments are passed to it. We want to
362 362 # have explicit control over CA loading because implicitly loading
363 363 # CAs may undermine the user's intent. For example, a user may define a CA
364 364 # bundle with a specific CA cert removed. If the system/default CA bundle
365 365 # is loaded and contains that removed CA, you've just undone the user's
366 366 # choice.
367 367 sslcontext = SSLContext(settings['protocol'])
368 368
369 369 # This is a no-op unless using modern ssl.
370 370 sslcontext.options |= settings['ctxoptions']
371 371
372 372 # This still works on our fake SSLContext.
373 373 sslcontext.verify_mode = settings['verifymode']
374 374
375 375 if settings['ciphers']:
376 376 try:
377 377 sslcontext.set_ciphers(settings['ciphers'])
378 378 except ssl.SSLError as e:
379 379 raise error.Abort(_('could not set ciphers: %s') % e.args[0],
380 380 hint=_('change cipher string (%s) in config') %
381 381 settings['ciphers'])
382 382
383 383 if certfile is not None:
384 384 def password():
385 385 f = keyfile or certfile
386 386 return ui.getpass(_('passphrase for %s: ') % f, '')
387 387 sslcontext.load_cert_chain(certfile, keyfile, password)
388 388
389 389 if settings['cafile'] is not None:
390 390 try:
391 391 sslcontext.load_verify_locations(cafile=settings['cafile'])
392 392 except ssl.SSLError as e:
393 if len(e.args) == 1: # pypy has different SSLError args
394 msg = e.args[0]
395 else:
396 msg = e.args[1]
393 397 raise error.Abort(_('error loading CA file %s: %s') % (
394 settings['cafile'], e.args[1]),
398 settings['cafile'], msg),
395 399 hint=_('file is empty or malformed?'))
396 400 caloaded = True
397 401 elif settings['allowloaddefaultcerts']:
398 402 # This is a no-op on old Python.
399 403 sslcontext.load_default_certs()
400 404 caloaded = True
401 405 else:
402 406 caloaded = False
403 407
404 408 try:
405 409 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
406 410 except ssl.SSLError as e:
407 411 # If we're doing certificate verification and no CA certs are loaded,
408 412 # that is almost certainly the reason why verification failed. Provide
409 413 # a hint to the user.
410 414 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
411 415 # only show this warning if modern ssl is available.
412 416 # The exception handler is here because of
413 417 # https://bugs.python.org/issue20916.
414 418 try:
415 419 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
416 420 modernssl and not sslcontext.get_ca_certs()):
417 421 ui.warn(_('(an attempt was made to load CA certificates but '
418 422 'none were loaded; see '
419 423 'https://mercurial-scm.org/wiki/SecureConnections '
420 424 'for how to configure Mercurial to avoid this '
421 425 'error)\n'))
422 426 except ssl.SSLError:
423 427 pass
424 428 # Try to print more helpful error messages for known failures.
425 429 if util.safehasattr(e, 'reason'):
426 430 # This error occurs when the client and server don't share a
427 431 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
428 432 # outright. Hopefully the reason for this error is that we require
429 433 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
430 434 # reason, try to emit an actionable warning.
431 435 if e.reason == 'UNSUPPORTED_PROTOCOL':
432 436 # We attempted TLS 1.0+.
433 437 if settings['protocolui'] == 'tls1.0':
434 438 # We support more than just TLS 1.0+. If this happens,
435 439 # the likely scenario is either the client or the server
436 440 # is really old. (e.g. server doesn't support TLS 1.0+ or
437 441 # client doesn't support modern TLS versions introduced
438 442 # several years from when this comment was written).
439 443 if supportedprotocols != set(['tls1.0']):
440 444 ui.warn(_(
441 445 '(could not communicate with %s using security '
442 446 'protocols %s; if you are using a modern Mercurial '
443 447 'version, consider contacting the operator of this '
444 448 'server; see '
445 449 'https://mercurial-scm.org/wiki/SecureConnections '
446 450 'for more info)\n') % (
447 451 serverhostname,
448 452 ', '.join(sorted(supportedprotocols))))
449 453 else:
450 454 ui.warn(_(
451 455 '(could not communicate with %s using TLS 1.0; the '
452 456 'likely cause of this is the server no longer '
453 457 'supports TLS 1.0 because it has known security '
454 458 'vulnerabilities; see '
455 459 'https://mercurial-scm.org/wiki/SecureConnections '
456 460 'for more info)\n') % serverhostname)
457 461 else:
458 462 # We attempted TLS 1.1+. We can only get here if the client
459 463 # supports the configured protocol. So the likely reason is
460 464 # the client wants better security than the server can
461 465 # offer.
462 466 ui.warn(_(
463 467 '(could not negotiate a common security protocol (%s+) '
464 468 'with %s; the likely cause is Mercurial is configured '
465 469 'to be more secure than the server can support)\n') % (
466 470 settings['protocolui'], serverhostname))
467 471 ui.warn(_('(consider contacting the operator of this '
468 472 'server and ask them to support modern TLS '
469 473 'protocol versions; or, set '
470 474 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
471 475 'use of legacy, less secure protocols when '
472 476 'communicating with this server)\n') %
473 477 serverhostname)
474 478 ui.warn(_(
475 479 '(see https://mercurial-scm.org/wiki/SecureConnections '
476 480 'for more info)\n'))
477 481 raise
478 482
479 483 # check if wrap_socket failed silently because socket had been
480 484 # closed
481 485 # - see http://bugs.python.org/issue13721
482 486 if not sslsocket.cipher():
483 487 raise error.Abort(_('ssl connection failed'))
484 488
485 489 sslsocket._hgstate = {
486 490 'caloaded': caloaded,
487 491 'hostname': serverhostname,
488 492 'settings': settings,
489 493 'ui': ui,
490 494 }
491 495
492 496 return sslsocket
493 497
494 498 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
495 499 requireclientcert=False):
496 500 """Wrap a socket for use by servers.
497 501
498 502 ``certfile`` and ``keyfile`` specify the files containing the certificate's
499 503 public and private keys, respectively. Both keys can be defined in the same
500 504 file via ``certfile`` (the private key must come first in the file).
501 505
502 506 ``cafile`` defines the path to certificate authorities.
503 507
504 508 ``requireclientcert`` specifies whether to require client certificates.
505 509
506 510 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
507 511 """
508 512 protocol, options, _protocolui = protocolsettings('tls1.0')
509 513
510 514 # This config option is intended for use in tests only. It is a giant
511 515 # footgun to kill security. Don't define it.
512 516 exactprotocol = ui.config('devel', 'serverexactprotocol')
513 517 if exactprotocol == 'tls1.0':
514 518 protocol = ssl.PROTOCOL_TLSv1
515 519 elif exactprotocol == 'tls1.1':
516 520 if 'tls1.1' not in supportedprotocols:
517 521 raise error.Abort(_('TLS 1.1 not supported by this Python'))
518 522 protocol = ssl.PROTOCOL_TLSv1_1
519 523 elif exactprotocol == 'tls1.2':
520 524 if 'tls1.2' not in supportedprotocols:
521 525 raise error.Abort(_('TLS 1.2 not supported by this Python'))
522 526 protocol = ssl.PROTOCOL_TLSv1_2
523 527 elif exactprotocol:
524 528 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
525 529 exactprotocol)
526 530
527 531 if modernssl:
528 532 # We /could/ use create_default_context() here since it doesn't load
529 533 # CAs when configured for client auth. However, it is hard-coded to
530 534 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
531 535 sslcontext = SSLContext(protocol)
532 536 sslcontext.options |= options
533 537
534 538 # Improve forward secrecy.
535 539 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
536 540 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
537 541
538 542 # Use the list of more secure ciphers if found in the ssl module.
539 543 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
540 544 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
541 545 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
542 546 else:
543 547 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
544 548
545 549 if requireclientcert:
546 550 sslcontext.verify_mode = ssl.CERT_REQUIRED
547 551 else:
548 552 sslcontext.verify_mode = ssl.CERT_NONE
549 553
550 554 if certfile or keyfile:
551 555 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
552 556
553 557 if cafile:
554 558 sslcontext.load_verify_locations(cafile=cafile)
555 559
556 560 return sslcontext.wrap_socket(sock, server_side=True)
557 561
558 562 class wildcarderror(Exception):
559 563 """Represents an error parsing wildcards in DNS name."""
560 564
561 565 def _dnsnamematch(dn, hostname, maxwildcards=1):
562 566 """Match DNS names according RFC 6125 section 6.4.3.
563 567
564 568 This code is effectively copied from CPython's ssl._dnsname_match.
565 569
566 570 Returns a bool indicating whether the expected hostname matches
567 571 the value in ``dn``.
568 572 """
569 573 pats = []
570 574 if not dn:
571 575 return False
572 576
573 577 pieces = dn.split(r'.')
574 578 leftmost = pieces[0]
575 579 remainder = pieces[1:]
576 580 wildcards = leftmost.count('*')
577 581 if wildcards > maxwildcards:
578 582 raise wildcarderror(
579 583 _('too many wildcards in certificate DNS name: %s') % dn)
580 584
581 585 # speed up common case w/o wildcards
582 586 if not wildcards:
583 587 return dn.lower() == hostname.lower()
584 588
585 589 # RFC 6125, section 6.4.3, subitem 1.
586 590 # The client SHOULD NOT attempt to match a presented identifier in which
587 591 # the wildcard character comprises a label other than the left-most label.
588 592 if leftmost == '*':
589 593 # When '*' is a fragment by itself, it matches a non-empty dotless
590 594 # fragment.
591 595 pats.append('[^.]+')
592 596 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
593 597 # RFC 6125, section 6.4.3, subitem 3.
594 598 # The client SHOULD NOT attempt to match a presented identifier
595 599 # where the wildcard character is embedded within an A-label or
596 600 # U-label of an internationalized domain name.
597 601 pats.append(re.escape(leftmost))
598 602 else:
599 603 # Otherwise, '*' matches any dotless string, e.g. www*
600 604 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
601 605
602 606 # add the remaining fragments, ignore any wildcards
603 607 for frag in remainder:
604 608 pats.append(re.escape(frag))
605 609
606 610 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
607 611 return pat.match(hostname) is not None
608 612
609 613 def _verifycert(cert, hostname):
610 614 '''Verify that cert (in socket.getpeercert() format) matches hostname.
611 615 CRLs is not handled.
612 616
613 617 Returns error message if any problems are found and None on success.
614 618 '''
615 619 if not cert:
616 620 return _('no certificate received')
617 621
618 622 dnsnames = []
619 623 san = cert.get('subjectAltName', [])
620 624 for key, value in san:
621 625 if key == 'DNS':
622 626 try:
623 627 if _dnsnamematch(value, hostname):
624 628 return
625 629 except wildcarderror as e:
626 630 return e.args[0]
627 631
628 632 dnsnames.append(value)
629 633
630 634 if not dnsnames:
631 635 # The subject is only checked when there is no DNS in subjectAltName.
632 636 for sub in cert.get('subject', []):
633 637 for key, value in sub:
634 638 # According to RFC 2818 the most specific Common Name must
635 639 # be used.
636 640 if key == 'commonName':
637 641 # 'subject' entries are unicide.
638 642 try:
639 643 value = value.encode('ascii')
640 644 except UnicodeEncodeError:
641 645 return _('IDN in certificate not supported')
642 646
643 647 try:
644 648 if _dnsnamematch(value, hostname):
645 649 return
646 650 except wildcarderror as e:
647 651 return e.args[0]
648 652
649 653 dnsnames.append(value)
650 654
651 655 if len(dnsnames) > 1:
652 656 return _('certificate is for %s') % ', '.join(dnsnames)
653 657 elif len(dnsnames) == 1:
654 658 return _('certificate is for %s') % dnsnames[0]
655 659 else:
656 660 return _('no commonName or subjectAltName found in certificate')
657 661
658 662 def _plainapplepython():
659 663 """return true if this seems to be a pure Apple Python that
660 664 * is unfrozen and presumably has the whole mercurial module in the file
661 665 system
662 666 * presumably is an Apple Python that uses Apple OpenSSL which has patches
663 667 for using system certificate store CAs in addition to the provided
664 668 cacerts file
665 669 """
666 670 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
667 671 return False
668 672 exe = os.path.realpath(sys.executable).lower()
669 673 return (exe.startswith('/usr/bin/python') or
670 674 exe.startswith('/system/library/frameworks/python.framework/'))
671 675
672 676 _systemcacertpaths = [
673 677 # RHEL, CentOS, and Fedora
674 678 '/etc/pki/tls/certs/ca-bundle.trust.crt',
675 679 # Debian, Ubuntu, Gentoo
676 680 '/etc/ssl/certs/ca-certificates.crt',
677 681 ]
678 682
679 683 def _defaultcacerts(ui):
680 684 """return path to default CA certificates or None.
681 685
682 686 It is assumed this function is called when the returned certificates
683 687 file will actually be used to validate connections. Therefore this
684 688 function may print warnings or debug messages assuming this usage.
685 689
686 690 We don't print a message when the Python is able to load default
687 691 CA certs because this scenario is detected at socket connect time.
688 692 """
689 693 # The "certifi" Python package provides certificates. If it is installed,
690 694 # assume the user intends it to be used and use it.
691 695 try:
692 696 import certifi
693 697 certs = certifi.where()
694 698 ui.debug('using ca certificates from certifi\n')
695 699 return certs
696 700 except ImportError:
697 701 pass
698 702
699 703 # On Windows, only the modern ssl module is capable of loading the system
700 704 # CA certificates. If we're not capable of doing that, emit a warning
701 705 # because we'll get a certificate verification error later and the lack
702 706 # of loaded CA certificates will be the reason why.
703 707 # Assertion: this code is only called if certificates are being verified.
704 708 if os.name == 'nt':
705 709 if not _canloaddefaultcerts:
706 710 ui.warn(_('(unable to load Windows CA certificates; see '
707 711 'https://mercurial-scm.org/wiki/SecureConnections for '
708 712 'how to configure Mercurial to avoid this message)\n'))
709 713
710 714 return None
711 715
712 716 # Apple's OpenSSL has patches that allow a specially constructed certificate
713 717 # to load the system CA store. If we're running on Apple Python, use this
714 718 # trick.
715 719 if _plainapplepython():
716 720 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
717 721 if os.path.exists(dummycert):
718 722 return dummycert
719 723
720 724 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
721 725 # load system certs, we're out of luck.
722 726 if sys.platform == 'darwin':
723 727 # FUTURE Consider looking for Homebrew or MacPorts installed certs
724 728 # files. Also consider exporting the keychain certs to a file during
725 729 # Mercurial install.
726 730 if not _canloaddefaultcerts:
727 731 ui.warn(_('(unable to load CA certificates; see '
728 732 'https://mercurial-scm.org/wiki/SecureConnections for '
729 733 'how to configure Mercurial to avoid this message)\n'))
730 734 return None
731 735
732 736 # / is writable on Windows. Out of an abundance of caution make sure
733 737 # we're not on Windows because paths from _systemcacerts could be installed
734 738 # by non-admin users.
735 739 assert os.name != 'nt'
736 740
737 741 # Try to find CA certificates in well-known locations. We print a warning
738 742 # when using a found file because we don't want too much silent magic
739 743 # for security settings. The expectation is that proper Mercurial
740 744 # installs will have the CA certs path defined at install time and the
741 745 # installer/packager will make an appropriate decision on the user's
742 746 # behalf. We only get here and perform this setting as a feature of
743 747 # last resort.
744 748 if not _canloaddefaultcerts:
745 749 for path in _systemcacertpaths:
746 750 if os.path.isfile(path):
747 751 ui.warn(_('(using CA certificates from %s; if you see this '
748 752 'message, your Mercurial install is not properly '
749 753 'configured; see '
750 754 'https://mercurial-scm.org/wiki/SecureConnections '
751 755 'for how to configure Mercurial to avoid this '
752 756 'message)\n') % path)
753 757 return path
754 758
755 759 ui.warn(_('(unable to load CA certificates; see '
756 760 'https://mercurial-scm.org/wiki/SecureConnections for '
757 761 'how to configure Mercurial to avoid this message)\n'))
758 762
759 763 return None
760 764
761 765 def validatesocket(sock):
762 766 """Validate a socket meets security requiremnets.
763 767
764 768 The passed socket must have been created with ``wrapsocket()``.
765 769 """
766 770 host = sock._hgstate['hostname']
767 771 ui = sock._hgstate['ui']
768 772 settings = sock._hgstate['settings']
769 773
770 774 try:
771 775 peercert = sock.getpeercert(True)
772 776 peercert2 = sock.getpeercert()
773 777 except AttributeError:
774 778 raise error.Abort(_('%s ssl connection error') % host)
775 779
776 780 if not peercert:
777 781 raise error.Abort(_('%s certificate error: '
778 782 'no certificate received') % host)
779 783
780 784 if settings['disablecertverification']:
781 785 # We don't print the certificate fingerprint because it shouldn't
782 786 # be necessary: if the user requested certificate verification be
783 787 # disabled, they presumably already saw a message about the inability
784 788 # to verify the certificate and this message would have printed the
785 789 # fingerprint. So printing the fingerprint here adds little to no
786 790 # value.
787 791 ui.warn(_('warning: connection security to %s is disabled per current '
788 792 'settings; communication is susceptible to eavesdropping '
789 793 'and tampering\n') % host)
790 794 return
791 795
792 796 # If a certificate fingerprint is pinned, use it and only it to
793 797 # validate the remote cert.
794 798 peerfingerprints = {
795 799 'sha1': hashlib.sha1(peercert).hexdigest(),
796 800 'sha256': hashlib.sha256(peercert).hexdigest(),
797 801 'sha512': hashlib.sha512(peercert).hexdigest(),
798 802 }
799 803
800 804 def fmtfingerprint(s):
801 805 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
802 806
803 807 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
804 808
805 809 if settings['certfingerprints']:
806 810 for hash, fingerprint in settings['certfingerprints']:
807 811 if peerfingerprints[hash].lower() == fingerprint:
808 812 ui.debug('%s certificate matched fingerprint %s:%s\n' %
809 813 (host, hash, fmtfingerprint(fingerprint)))
810 814 return
811 815
812 816 # Pinned fingerprint didn't match. This is a fatal error.
813 817 if settings['legacyfingerprint']:
814 818 section = 'hostfingerprint'
815 819 nice = fmtfingerprint(peerfingerprints['sha1'])
816 820 else:
817 821 section = 'hostsecurity'
818 822 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
819 823 raise error.Abort(_('certificate for %s has unexpected '
820 824 'fingerprint %s') % (host, nice),
821 825 hint=_('check %s configuration') % section)
822 826
823 827 # Security is enabled but no CAs are loaded. We can't establish trust
824 828 # for the cert so abort.
825 829 if not sock._hgstate['caloaded']:
826 830 raise error.Abort(
827 831 _('unable to verify security of %s (no loaded CA certificates); '
828 832 'refusing to connect') % host,
829 833 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
830 834 'how to configure Mercurial to avoid this error or set '
831 835 'hostsecurity.%s:fingerprints=%s to trust this server') %
832 836 (host, nicefingerprint))
833 837
834 838 msg = _verifycert(peercert2, host)
835 839 if msg:
836 840 raise error.Abort(_('%s certificate error: %s') % (host, msg),
837 841 hint=_('set hostsecurity.%s:certfingerprints=%s '
838 842 'config setting or use --insecure to connect '
839 843 'insecurely') %
840 844 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now