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