comparison mercurial/sslutil.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents c8d55ff80da1
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
110 r'ca_certs': self._cacerts, 110 r'ca_certs': self._cacerts,
111 r'ciphers': self._ciphers, 111 r'ciphers': self._ciphers,
112 } 112 }
113 113
114 return ssl.wrap_socket(socket, **args) 114 return ssl.wrap_socket(socket, **args)
115
115 116
116 def _hostsettings(ui, hostname): 117 def _hostsettings(ui, hostname):
117 """Obtain security settings for a hostname. 118 """Obtain security settings for a hostname.
118 119
119 Returns a dict of settings relevant to that hostname. 120 Returns a dict of settings relevant to that hostname.
147 148
148 # Allow minimum TLS protocol to be specified in the config. 149 # Allow minimum TLS protocol to be specified in the config.
149 def validateprotocol(protocol, key): 150 def validateprotocol(protocol, key):
150 if protocol not in configprotocols: 151 if protocol not in configprotocols:
151 raise error.Abort( 152 raise error.Abort(
152 _('unsupported protocol from hostsecurity.%s: %s') % 153 _('unsupported protocol from hostsecurity.%s: %s')
153 (key, protocol), 154 % (key, protocol),
154 hint=_('valid protocols: %s') % 155 hint=_('valid protocols: %s')
155 ' '.join(sorted(configprotocols))) 156 % ' '.join(sorted(configprotocols)),
157 )
156 158
157 # We default to TLS 1.1+ where we can because TLS 1.0 has known 159 # We default to TLS 1.1+ where we can because TLS 1.0 has known
158 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to 160 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to
159 # TLS 1.0+ via config options in case a legacy server is encountered. 161 # TLS 1.0+ via config options in case a legacy server is encountered.
160 if 'tls1.1' in supportedprotocols: 162 if 'tls1.1' in supportedprotocols:
163 # Let people know they are borderline secure. 165 # Let people know they are borderline secure.
164 # We don't document this config option because we want people to see 166 # We don't document this config option because we want people to see
165 # the bold warnings on the web site. 167 # the bold warnings on the web site.
166 # internal config: hostsecurity.disabletls10warning 168 # internal config: hostsecurity.disabletls10warning
167 if not ui.configbool('hostsecurity', 'disabletls10warning'): 169 if not ui.configbool('hostsecurity', 'disabletls10warning'):
168 ui.warn(_('warning: connecting to %s using legacy security ' 170 ui.warn(
169 'technology (TLS 1.0); see ' 171 _(
170 'https://mercurial-scm.org/wiki/SecureConnections for ' 172 'warning: connecting to %s using legacy security '
171 'more info\n') % bhostname) 173 'technology (TLS 1.0); see '
174 'https://mercurial-scm.org/wiki/SecureConnections for '
175 'more info\n'
176 )
177 % bhostname
178 )
172 defaultprotocol = 'tls1.0' 179 defaultprotocol = 'tls1.0'
173 180
174 key = 'minimumprotocol' 181 key = 'minimumprotocol'
175 protocol = ui.config('hostsecurity', key, defaultprotocol) 182 protocol = ui.config('hostsecurity', key, defaultprotocol)
176 validateprotocol(protocol, key) 183 validateprotocol(protocol, key)
194 # Look for fingerprints in [hostsecurity] section. Value is a list 201 # Look for fingerprints in [hostsecurity] section. Value is a list
195 # of <alg>:<fingerprint> strings. 202 # of <alg>:<fingerprint> strings.
196 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % bhostname) 203 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % bhostname)
197 for fingerprint in fingerprints: 204 for fingerprint in fingerprints:
198 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))): 205 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
199 raise error.Abort(_('invalid fingerprint for %s: %s') % ( 206 raise error.Abort(
200 bhostname, fingerprint), 207 _('invalid fingerprint for %s: %s') % (bhostname, fingerprint),
201 hint=_('must begin with "sha1:", "sha256:", ' 208 hint=_('must begin with "sha1:", "sha256:", ' 'or "sha512:"'),
202 'or "sha512:"')) 209 )
203 210
204 alg, fingerprint = fingerprint.split(':', 1) 211 alg, fingerprint = fingerprint.split(':', 1)
205 fingerprint = fingerprint.replace(':', '').lower() 212 fingerprint = fingerprint.replace(':', '').lower()
206 s['certfingerprints'].append((alg, fingerprint)) 213 s['certfingerprints'].append((alg, fingerprint))
207 214
229 # If both fingerprints and a per-host ca file are specified, issue a warning 236 # If both fingerprints and a per-host ca file are specified, issue a warning
230 # because users should not be surprised about what security is or isn't 237 # because users should not be surprised about what security is or isn't
231 # being performed. 238 # being performed.
232 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % bhostname) 239 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % bhostname)
233 if s['certfingerprints'] and cafile: 240 if s['certfingerprints'] and cafile:
234 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host ' 241 ui.warn(
235 'fingerprints defined; using host fingerprints for ' 242 _(
236 'verification)\n') % bhostname) 243 '(hostsecurity.%s:verifycertsfile ignored when host '
244 'fingerprints defined; using host fingerprints for '
245 'verification)\n'
246 )
247 % bhostname
248 )
237 249
238 # Try to hook up CA certificate validation unless something above 250 # Try to hook up CA certificate validation unless something above
239 # makes it not necessary. 251 # makes it not necessary.
240 if s['verifymode'] is None: 252 if s['verifymode'] is None:
241 # Look at per-host ca file first. 253 # Look at per-host ca file first.
242 if cafile: 254 if cafile:
243 cafile = util.expandpath(cafile) 255 cafile = util.expandpath(cafile)
244 if not os.path.exists(cafile): 256 if not os.path.exists(cafile):
245 raise error.Abort(_('path specified by %s does not exist: %s') % 257 raise error.Abort(
246 ('hostsecurity.%s:verifycertsfile' % ( 258 _('path specified by %s does not exist: %s')
247 bhostname,), cafile)) 259 % ('hostsecurity.%s:verifycertsfile' % (bhostname,), cafile)
260 )
248 s['cafile'] = cafile 261 s['cafile'] = cafile
249 else: 262 else:
250 # Find global certificates file in config. 263 # Find global certificates file in config.
251 cafile = ui.config('web', 'cacerts') 264 cafile = ui.config('web', 'cacerts')
252 265
253 if cafile: 266 if cafile:
254 cafile = util.expandpath(cafile) 267 cafile = util.expandpath(cafile)
255 if not os.path.exists(cafile): 268 if not os.path.exists(cafile):
256 raise error.Abort(_('could not find web.cacerts: %s') % 269 raise error.Abort(
257 cafile) 270 _('could not find web.cacerts: %s') % cafile
271 )
258 elif s['allowloaddefaultcerts']: 272 elif s['allowloaddefaultcerts']:
259 # CAs not defined in config. Try to find system bundles. 273 # CAs not defined in config. Try to find system bundles.
260 cafile = _defaultcacerts(ui) 274 cafile = _defaultcacerts(ui)
261 if cafile: 275 if cafile:
262 ui.debug('using %s for CA file\n' % cafile) 276 ui.debug('using %s for CA file\n' % cafile)
278 assert s['protocol'] is not None 292 assert s['protocol'] is not None
279 assert s['ctxoptions'] is not None 293 assert s['ctxoptions'] is not None
280 assert s['verifymode'] is not None 294 assert s['verifymode'] is not None
281 295
282 return s 296 return s
297
283 298
284 def protocolsettings(protocol): 299 def protocolsettings(protocol):
285 """Resolve the protocol for a config value. 300 """Resolve the protocol for a config value.
286 301
287 Returns a 3-tuple of (protocol, options, ui value) where the first 302 Returns a 3-tuple of (protocol, options, ui value) where the first
302 # disable protocols via SSLContext.options and OP_NO_* constants. 317 # disable protocols via SSLContext.options and OP_NO_* constants.
303 # However, SSLContext.options doesn't work unless we have the 318 # However, SSLContext.options doesn't work unless we have the
304 # full/real SSLContext available to us. 319 # full/real SSLContext available to us.
305 if supportedprotocols == {'tls1.0'}: 320 if supportedprotocols == {'tls1.0'}:
306 if protocol != 'tls1.0': 321 if protocol != 'tls1.0':
307 raise error.Abort(_('current Python does not support protocol ' 322 raise error.Abort(
308 'setting %s') % protocol, 323 _('current Python does not support protocol ' 'setting %s')
309 hint=_('upgrade Python or disable setting since ' 324 % protocol,
310 'only TLS 1.0 is supported')) 325 hint=_(
326 'upgrade Python or disable setting since '
327 'only TLS 1.0 is supported'
328 ),
329 )
311 330
312 return ssl.PROTOCOL_TLSv1, 0, 'tls1.0' 331 return ssl.PROTOCOL_TLSv1, 0, 'tls1.0'
313 332
314 # WARNING: returned options don't work unless the modern ssl module 333 # WARNING: returned options don't work unless the modern ssl module
315 # is available. Be careful when adding options here. 334 # is available. Be careful when adding options here.
331 # There is no guarantee this attribute is defined on the module. 350 # There is no guarantee this attribute is defined on the module.
332 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0) 351 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
333 352
334 return ssl.PROTOCOL_SSLv23, options, protocol 353 return ssl.PROTOCOL_SSLv23, options, protocol
335 354
355
336 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None): 356 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
337 """Add SSL/TLS to a socket. 357 """Add SSL/TLS to a socket.
338 358
339 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane 359 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
340 choices based on what security options are available. 360 choices based on what security options are available.
350 raise error.Abort(_('serverhostname argument is required')) 370 raise error.Abort(_('serverhostname argument is required'))
351 371
352 if b'SSLKEYLOGFILE' in encoding.environ: 372 if b'SSLKEYLOGFILE' in encoding.environ:
353 try: 373 try:
354 import sslkeylog 374 import sslkeylog
355 sslkeylog.set_keylog(pycompat.fsdecode( 375
356 encoding.environ[b'SSLKEYLOGFILE'])) 376 sslkeylog.set_keylog(
377 pycompat.fsdecode(encoding.environ[b'SSLKEYLOGFILE'])
378 )
357 ui.warn( 379 ui.warn(
358 b'sslkeylog enabled by SSLKEYLOGFILE environment variable\n') 380 b'sslkeylog enabled by SSLKEYLOGFILE environment variable\n'
381 )
359 except ImportError: 382 except ImportError:
360 ui.warn(b'sslkeylog module missing, ' 383 ui.warn(
361 b'but SSLKEYLOGFILE set in environment\n') 384 b'sslkeylog module missing, '
385 b'but SSLKEYLOGFILE set in environment\n'
386 )
362 387
363 for f in (keyfile, certfile): 388 for f in (keyfile, certfile):
364 if f and not os.path.exists(f): 389 if f and not os.path.exists(f):
365 raise error.Abort( 390 raise error.Abort(
366 _('certificate file (%s) does not exist; cannot connect to %s') 391 _('certificate file (%s) does not exist; cannot connect to %s')
367 % (f, pycompat.bytesurl(serverhostname)), 392 % (f, pycompat.bytesurl(serverhostname)),
368 hint=_('restore missing file or fix references ' 393 hint=_(
369 'in Mercurial config')) 394 'restore missing file or fix references '
395 'in Mercurial config'
396 ),
397 )
370 398
371 settings = _hostsettings(ui, serverhostname) 399 settings = _hostsettings(ui, serverhostname)
372 400
373 # We can't use ssl.create_default_context() because it calls 401 # We can't use ssl.create_default_context() because it calls
374 # load_default_certs() unless CA arguments are passed to it. We want to 402 # load_default_certs() unless CA arguments are passed to it. We want to
390 sslcontext.set_ciphers(pycompat.sysstr(settings['ciphers'])) 418 sslcontext.set_ciphers(pycompat.sysstr(settings['ciphers']))
391 except ssl.SSLError as e: 419 except ssl.SSLError as e:
392 raise error.Abort( 420 raise error.Abort(
393 _('could not set ciphers: %s') 421 _('could not set ciphers: %s')
394 % stringutil.forcebytestr(e.args[0]), 422 % stringutil.forcebytestr(e.args[0]),
395 hint=_('change cipher string (%s) in config') % 423 hint=_('change cipher string (%s) in config')
396 settings['ciphers']) 424 % settings['ciphers'],
425 )
397 426
398 if certfile is not None: 427 if certfile is not None:
428
399 def password(): 429 def password():
400 f = keyfile or certfile 430 f = keyfile or certfile
401 return ui.getpass(_('passphrase for %s: ') % f, '') 431 return ui.getpass(_('passphrase for %s: ') % f, '')
432
402 sslcontext.load_cert_chain(certfile, keyfile, password) 433 sslcontext.load_cert_chain(certfile, keyfile, password)
403 434
404 if settings['cafile'] is not None: 435 if settings['cafile'] is not None:
405 try: 436 try:
406 sslcontext.load_verify_locations(cafile=settings['cafile']) 437 sslcontext.load_verify_locations(cafile=settings['cafile'])
407 except ssl.SSLError as e: 438 except ssl.SSLError as e:
408 if len(e.args) == 1: # pypy has different SSLError args 439 if len(e.args) == 1: # pypy has different SSLError args
409 msg = e.args[0] 440 msg = e.args[0]
410 else: 441 else:
411 msg = e.args[1] 442 msg = e.args[1]
412 raise error.Abort(_('error loading CA file %s: %s') % ( 443 raise error.Abort(
413 settings['cafile'], stringutil.forcebytestr(msg)), 444 _('error loading CA file %s: %s')
414 hint=_('file is empty or malformed?')) 445 % (settings['cafile'], stringutil.forcebytestr(msg)),
446 hint=_('file is empty or malformed?'),
447 )
415 caloaded = True 448 caloaded = True
416 elif settings['allowloaddefaultcerts']: 449 elif settings['allowloaddefaultcerts']:
417 # This is a no-op on old Python. 450 # This is a no-op on old Python.
418 sslcontext.load_default_certs() 451 sslcontext.load_default_certs()
419 caloaded = True 452 caloaded = True
431 # The exception handler is here to handle bugs around cert attributes: 464 # The exception handler is here to handle bugs around cert attributes:
432 # https://bugs.python.org/issue20916#msg213479. (See issues5313.) 465 # https://bugs.python.org/issue20916#msg213479. (See issues5313.)
433 # When the main 20916 bug occurs, 'sslcontext.get_ca_certs()' is a 466 # When the main 20916 bug occurs, 'sslcontext.get_ca_certs()' is a
434 # non-empty list, but the following conditional is otherwise True. 467 # non-empty list, but the following conditional is otherwise True.
435 try: 468 try:
436 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and 469 if (
437 modernssl and not sslcontext.get_ca_certs()): 470 caloaded
438 ui.warn(_('(an attempt was made to load CA certificates but ' 471 and settings['verifymode'] == ssl.CERT_REQUIRED
439 'none were loaded; see ' 472 and modernssl
440 'https://mercurial-scm.org/wiki/SecureConnections ' 473 and not sslcontext.get_ca_certs()
441 'for how to configure Mercurial to avoid this ' 474 ):
442 'error)\n')) 475 ui.warn(
476 _(
477 '(an attempt was made to load CA certificates but '
478 'none were loaded; see '
479 'https://mercurial-scm.org/wiki/SecureConnections '
480 'for how to configure Mercurial to avoid this '
481 'error)\n'
482 )
483 )
443 except ssl.SSLError: 484 except ssl.SSLError:
444 pass 485 pass
445 486
446 # Try to print more helpful error messages for known failures. 487 # Try to print more helpful error messages for known failures.
447 if util.safehasattr(e, 'reason'): 488 if util.safehasattr(e, 'reason'):
457 # the likely scenario is either the client or the server 498 # the likely scenario is either the client or the server
458 # is really old. (e.g. server doesn't support TLS 1.0+ or 499 # is really old. (e.g. server doesn't support TLS 1.0+ or
459 # client doesn't support modern TLS versions introduced 500 # client doesn't support modern TLS versions introduced
460 # several years from when this comment was written). 501 # several years from when this comment was written).
461 if supportedprotocols != {'tls1.0'}: 502 if supportedprotocols != {'tls1.0'}:
462 ui.warn(_( 503 ui.warn(
463 '(could not communicate with %s using security ' 504 _(
464 'protocols %s; if you are using a modern Mercurial ' 505 '(could not communicate with %s using security '
465 'version, consider contacting the operator of this ' 506 'protocols %s; if you are using a modern Mercurial '
466 'server; see ' 507 'version, consider contacting the operator of this '
467 'https://mercurial-scm.org/wiki/SecureConnections ' 508 'server; see '
468 'for more info)\n') % ( 509 'https://mercurial-scm.org/wiki/SecureConnections '
510 'for more info)\n'
511 )
512 % (
469 pycompat.bytesurl(serverhostname), 513 pycompat.bytesurl(serverhostname),
470 ', '.join(sorted(supportedprotocols)))) 514 ', '.join(sorted(supportedprotocols)),
515 )
516 )
471 else: 517 else:
472 ui.warn(_( 518 ui.warn(
473 '(could not communicate with %s using TLS 1.0; the ' 519 _(
474 'likely cause of this is the server no longer ' 520 '(could not communicate with %s using TLS 1.0; the '
475 'supports TLS 1.0 because it has known security ' 521 'likely cause of this is the server no longer '
476 'vulnerabilities; see ' 522 'supports TLS 1.0 because it has known security '
477 'https://mercurial-scm.org/wiki/SecureConnections ' 523 'vulnerabilities; see '
478 'for more info)\n') % 524 'https://mercurial-scm.org/wiki/SecureConnections '
479 pycompat.bytesurl(serverhostname)) 525 'for more info)\n'
526 )
527 % pycompat.bytesurl(serverhostname)
528 )
480 else: 529 else:
481 # We attempted TLS 1.1+. We can only get here if the client 530 # We attempted TLS 1.1+. We can only get here if the client
482 # supports the configured protocol. So the likely reason is 531 # supports the configured protocol. So the likely reason is
483 # the client wants better security than the server can 532 # the client wants better security than the server can
484 # offer. 533 # offer.
485 ui.warn(_( 534 ui.warn(
486 '(could not negotiate a common security protocol (%s+) ' 535 _(
487 'with %s; the likely cause is Mercurial is configured ' 536 '(could not negotiate a common security protocol (%s+) '
488 'to be more secure than the server can support)\n') % ( 537 'with %s; the likely cause is Mercurial is configured '
489 settings['protocolui'], 538 'to be more secure than the server can support)\n'
490 pycompat.bytesurl(serverhostname))) 539 )
491 ui.warn(_('(consider contacting the operator of this ' 540 % (
492 'server and ask them to support modern TLS ' 541 settings['protocolui'],
493 'protocol versions; or, set ' 542 pycompat.bytesurl(serverhostname),
494 'hostsecurity.%s:minimumprotocol=tls1.0 to allow ' 543 )
495 'use of legacy, less secure protocols when ' 544 )
496 'communicating with this server)\n') % 545 ui.warn(
497 pycompat.bytesurl(serverhostname)) 546 _(
498 ui.warn(_( 547 '(consider contacting the operator of this '
499 '(see https://mercurial-scm.org/wiki/SecureConnections ' 548 'server and ask them to support modern TLS '
500 'for more info)\n')) 549 'protocol versions; or, set '
501 550 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
502 elif (e.reason == r'CERTIFICATE_VERIFY_FAILED' and 551 'use of legacy, less secure protocols when '
503 pycompat.iswindows): 552 'communicating with this server)\n'
504 553 )
505 ui.warn(_('(the full certificate chain may not be available ' 554 % pycompat.bytesurl(serverhostname)
506 'locally; see "hg help debugssl")\n')) 555 )
556 ui.warn(
557 _(
558 '(see https://mercurial-scm.org/wiki/SecureConnections '
559 'for more info)\n'
560 )
561 )
562
563 elif (
564 e.reason == r'CERTIFICATE_VERIFY_FAILED' and pycompat.iswindows
565 ):
566
567 ui.warn(
568 _(
569 '(the full certificate chain may not be available '
570 'locally; see "hg help debugssl")\n'
571 )
572 )
507 raise 573 raise
508 574
509 # check if wrap_socket failed silently because socket had been 575 # check if wrap_socket failed silently because socket had been
510 # closed 576 # closed
511 # - see http://bugs.python.org/issue13721 577 # - see http://bugs.python.org/issue13721
519 'ui': ui, 585 'ui': ui,
520 } 586 }
521 587
522 return sslsocket 588 return sslsocket
523 589
524 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None, 590
525 requireclientcert=False): 591 def wrapserversocket(
592 sock, ui, certfile=None, keyfile=None, cafile=None, requireclientcert=False
593 ):
526 """Wrap a socket for use by servers. 594 """Wrap a socket for use by servers.
527 595
528 ``certfile`` and ``keyfile`` specify the files containing the certificate's 596 ``certfile`` and ``keyfile`` specify the files containing the certificate's
529 public and private keys, respectively. Both keys can be defined in the same 597 public and private keys, respectively. Both keys can be defined in the same
530 file via ``certfile`` (the private key must come first in the file). 598 file via ``certfile`` (the private key must come first in the file).
537 """ 605 """
538 # This function is not used much by core Mercurial, so the error messaging 606 # This function is not used much by core Mercurial, so the error messaging
539 # doesn't have to be as detailed as for wrapsocket(). 607 # doesn't have to be as detailed as for wrapsocket().
540 for f in (certfile, keyfile, cafile): 608 for f in (certfile, keyfile, cafile):
541 if f and not os.path.exists(f): 609 if f and not os.path.exists(f):
542 raise error.Abort(_('referenced certificate file (%s) does not ' 610 raise error.Abort(
543 'exist') % f) 611 _('referenced certificate file (%s) does not ' 'exist') % f
612 )
544 613
545 protocol, options, _protocolui = protocolsettings('tls1.0') 614 protocol, options, _protocolui = protocolsettings('tls1.0')
546 615
547 # This config option is intended for use in tests only. It is a giant 616 # This config option is intended for use in tests only. It is a giant
548 # footgun to kill security. Don't define it. 617 # footgun to kill security. Don't define it.
556 elif exactprotocol == 'tls1.2': 625 elif exactprotocol == 'tls1.2':
557 if 'tls1.2' not in supportedprotocols: 626 if 'tls1.2' not in supportedprotocols:
558 raise error.Abort(_('TLS 1.2 not supported by this Python')) 627 raise error.Abort(_('TLS 1.2 not supported by this Python'))
559 protocol = ssl.PROTOCOL_TLSv1_2 628 protocol = ssl.PROTOCOL_TLSv1_2
560 elif exactprotocol: 629 elif exactprotocol:
561 raise error.Abort(_('invalid value for serverexactprotocol: %s') % 630 raise error.Abort(
562 exactprotocol) 631 _('invalid value for serverexactprotocol: %s') % exactprotocol
632 )
563 633
564 if modernssl: 634 if modernssl:
565 # We /could/ use create_default_context() here since it doesn't load 635 # We /could/ use create_default_context() here since it doesn't load
566 # CAs when configured for client auth. However, it is hard-coded to 636 # CAs when configured for client auth. However, it is hard-coded to
567 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here. 637 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
590 if cafile: 660 if cafile:
591 sslcontext.load_verify_locations(cafile=cafile) 661 sslcontext.load_verify_locations(cafile=cafile)
592 662
593 return sslcontext.wrap_socket(sock, server_side=True) 663 return sslcontext.wrap_socket(sock, server_side=True)
594 664
665
595 class wildcarderror(Exception): 666 class wildcarderror(Exception):
596 """Represents an error parsing wildcards in DNS name.""" 667 """Represents an error parsing wildcards in DNS name."""
668
597 669
598 def _dnsnamematch(dn, hostname, maxwildcards=1): 670 def _dnsnamematch(dn, hostname, maxwildcards=1):
599 """Match DNS names according RFC 6125 section 6.4.3. 671 """Match DNS names according RFC 6125 section 6.4.3.
600 672
601 This code is effectively copied from CPython's ssl._dnsname_match. 673 This code is effectively copied from CPython's ssl._dnsname_match.
613 leftmost = pieces[0] 685 leftmost = pieces[0]
614 remainder = pieces[1:] 686 remainder = pieces[1:]
615 wildcards = leftmost.count('*') 687 wildcards = leftmost.count('*')
616 if wildcards > maxwildcards: 688 if wildcards > maxwildcards:
617 raise wildcarderror( 689 raise wildcarderror(
618 _('too many wildcards in certificate DNS name: %s') % dn) 690 _('too many wildcards in certificate DNS name: %s') % dn
691 )
619 692
620 # speed up common case w/o wildcards 693 # speed up common case w/o wildcards
621 if not wildcards: 694 if not wildcards:
622 return dn.lower() == hostname.lower() 695 return dn.lower() == hostname.lower()
623 696
643 pats.append(stringutil.reescape(frag)) 716 pats.append(stringutil.reescape(frag))
644 717
645 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE) 718 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
646 return pat.match(hostname) is not None 719 return pat.match(hostname) is not None
647 720
721
648 def _verifycert(cert, hostname): 722 def _verifycert(cert, hostname):
649 '''Verify that cert (in socket.getpeercert() format) matches hostname. 723 '''Verify that cert (in socket.getpeercert() format) matches hostname.
650 CRLs is not handled. 724 CRLs is not handled.
651 725
652 Returns error message if any problems are found and None on success. 726 Returns error message if any problems are found and None on success.
693 elif len(dnsnames) == 1: 767 elif len(dnsnames) == 1:
694 return _('certificate is for %s') % dnsnames[0] 768 return _('certificate is for %s') % dnsnames[0]
695 else: 769 else:
696 return _('no commonName or subjectAltName found in certificate') 770 return _('no commonName or subjectAltName found in certificate')
697 771
772
698 def _plainapplepython(): 773 def _plainapplepython():
699 """return true if this seems to be a pure Apple Python that 774 """return true if this seems to be a pure Apple Python that
700 * is unfrozen and presumably has the whole mercurial module in the file 775 * is unfrozen and presumably has the whole mercurial module in the file
701 system 776 system
702 * presumably is an Apple Python that uses Apple OpenSSL which has patches 777 * presumably is an Apple Python that uses Apple OpenSSL which has patches
703 for using system certificate store CAs in addition to the provided 778 for using system certificate store CAs in addition to the provided
704 cacerts file 779 cacerts file
705 """ 780 """
706 if (not pycompat.isdarwin or procutil.mainfrozen() or 781 if (
707 not pycompat.sysexecutable): 782 not pycompat.isdarwin
783 or procutil.mainfrozen()
784 or not pycompat.sysexecutable
785 ):
708 return False 786 return False
709 exe = os.path.realpath(pycompat.sysexecutable).lower() 787 exe = os.path.realpath(pycompat.sysexecutable).lower()
710 return (exe.startswith('/usr/bin/python') or 788 return exe.startswith('/usr/bin/python') or exe.startswith(
711 exe.startswith('/system/library/frameworks/python.framework/')) 789 '/system/library/frameworks/python.framework/'
790 )
791
712 792
713 _systemcacertpaths = [ 793 _systemcacertpaths = [
714 # RHEL, CentOS, and Fedora 794 # RHEL, CentOS, and Fedora
715 '/etc/pki/tls/certs/ca-bundle.trust.crt', 795 '/etc/pki/tls/certs/ca-bundle.trust.crt',
716 # Debian, Ubuntu, Gentoo 796 # Debian, Ubuntu, Gentoo
717 '/etc/ssl/certs/ca-certificates.crt', 797 '/etc/ssl/certs/ca-certificates.crt',
718 ] 798 ]
799
719 800
720 def _defaultcacerts(ui): 801 def _defaultcacerts(ui):
721 """return path to default CA certificates or None. 802 """return path to default CA certificates or None.
722 803
723 It is assumed this function is called when the returned certificates 804 It is assumed this function is called when the returned certificates
729 """ 810 """
730 # The "certifi" Python package provides certificates. If it is installed 811 # The "certifi" Python package provides certificates. If it is installed
731 # and usable, assume the user intends it to be used and use it. 812 # and usable, assume the user intends it to be used and use it.
732 try: 813 try:
733 import certifi 814 import certifi
815
734 certs = certifi.where() 816 certs = certifi.where()
735 if os.path.exists(certs): 817 if os.path.exists(certs):
736 ui.debug('using ca certificates from certifi\n') 818 ui.debug('using ca certificates from certifi\n')
737 return pycompat.fsencode(certs) 819 return pycompat.fsencode(certs)
738 except (ImportError, AttributeError): 820 except (ImportError, AttributeError):
743 # because we'll get a certificate verification error later and the lack 825 # because we'll get a certificate verification error later and the lack
744 # of loaded CA certificates will be the reason why. 826 # of loaded CA certificates will be the reason why.
745 # Assertion: this code is only called if certificates are being verified. 827 # Assertion: this code is only called if certificates are being verified.
746 if pycompat.iswindows: 828 if pycompat.iswindows:
747 if not _canloaddefaultcerts: 829 if not _canloaddefaultcerts:
748 ui.warn(_('(unable to load Windows CA certificates; see ' 830 ui.warn(
749 'https://mercurial-scm.org/wiki/SecureConnections for ' 831 _(
750 'how to configure Mercurial to avoid this message)\n')) 832 '(unable to load Windows CA certificates; see '
833 'https://mercurial-scm.org/wiki/SecureConnections for '
834 'how to configure Mercurial to avoid this message)\n'
835 )
836 )
751 837
752 return None 838 return None
753 839
754 # Apple's OpenSSL has patches that allow a specially constructed certificate 840 # Apple's OpenSSL has patches that allow a specially constructed certificate
755 # to load the system CA store. If we're running on Apple Python, use this 841 # to load the system CA store. If we're running on Apple Python, use this
756 # trick. 842 # trick.
757 if _plainapplepython(): 843 if _plainapplepython():
758 dummycert = os.path.join( 844 dummycert = os.path.join(
759 os.path.dirname(pycompat.fsencode(__file__)), 'dummycert.pem') 845 os.path.dirname(pycompat.fsencode(__file__)), 'dummycert.pem'
846 )
760 if os.path.exists(dummycert): 847 if os.path.exists(dummycert):
761 return dummycert 848 return dummycert
762 849
763 # The Apple OpenSSL trick isn't available to us. If Python isn't able to 850 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
764 # load system certs, we're out of luck. 851 # load system certs, we're out of luck.
765 if pycompat.isdarwin: 852 if pycompat.isdarwin:
766 # FUTURE Consider looking for Homebrew or MacPorts installed certs 853 # FUTURE Consider looking for Homebrew or MacPorts installed certs
767 # files. Also consider exporting the keychain certs to a file during 854 # files. Also consider exporting the keychain certs to a file during
768 # Mercurial install. 855 # Mercurial install.
769 if not _canloaddefaultcerts: 856 if not _canloaddefaultcerts:
770 ui.warn(_('(unable to load CA certificates; see ' 857 ui.warn(
771 'https://mercurial-scm.org/wiki/SecureConnections for ' 858 _(
772 'how to configure Mercurial to avoid this message)\n')) 859 '(unable to load CA certificates; see '
860 'https://mercurial-scm.org/wiki/SecureConnections for '
861 'how to configure Mercurial to avoid this message)\n'
862 )
863 )
773 return None 864 return None
774 865
775 # / is writable on Windows. Out of an abundance of caution make sure 866 # / is writable on Windows. Out of an abundance of caution make sure
776 # we're not on Windows because paths from _systemcacerts could be installed 867 # we're not on Windows because paths from _systemcacerts could be installed
777 # by non-admin users. 868 # by non-admin users.
785 # behalf. We only get here and perform this setting as a feature of 876 # behalf. We only get here and perform this setting as a feature of
786 # last resort. 877 # last resort.
787 if not _canloaddefaultcerts: 878 if not _canloaddefaultcerts:
788 for path in _systemcacertpaths: 879 for path in _systemcacertpaths:
789 if os.path.isfile(path): 880 if os.path.isfile(path):
790 ui.warn(_('(using CA certificates from %s; if you see this ' 881 ui.warn(
791 'message, your Mercurial install is not properly ' 882 _(
792 'configured; see ' 883 '(using CA certificates from %s; if you see this '
793 'https://mercurial-scm.org/wiki/SecureConnections ' 884 'message, your Mercurial install is not properly '
794 'for how to configure Mercurial to avoid this ' 885 'configured; see '
795 'message)\n') % path) 886 'https://mercurial-scm.org/wiki/SecureConnections '
887 'for how to configure Mercurial to avoid this '
888 'message)\n'
889 )
890 % path
891 )
796 return path 892 return path
797 893
798 ui.warn(_('(unable to load CA certificates; see ' 894 ui.warn(
799 'https://mercurial-scm.org/wiki/SecureConnections for ' 895 _(
800 'how to configure Mercurial to avoid this message)\n')) 896 '(unable to load CA certificates; see '
897 'https://mercurial-scm.org/wiki/SecureConnections for '
898 'how to configure Mercurial to avoid this message)\n'
899 )
900 )
801 901
802 return None 902 return None
903
803 904
804 def validatesocket(sock): 905 def validatesocket(sock):
805 """Validate a socket meets security requirements. 906 """Validate a socket meets security requirements.
806 907
807 The passed socket must have been created with ``wrapsocket()``. 908 The passed socket must have been created with ``wrapsocket()``.
816 peercert2 = sock.getpeercert() 917 peercert2 = sock.getpeercert()
817 except AttributeError: 918 except AttributeError:
818 raise error.Abort(_('%s ssl connection error') % host) 919 raise error.Abort(_('%s ssl connection error') % host)
819 920
820 if not peercert: 921 if not peercert:
821 raise error.Abort(_('%s certificate error: ' 922 raise error.Abort(
822 'no certificate received') % host) 923 _('%s certificate error: ' 'no certificate received') % host
924 )
823 925
824 if settings['disablecertverification']: 926 if settings['disablecertverification']:
825 # We don't print the certificate fingerprint because it shouldn't 927 # We don't print the certificate fingerprint because it shouldn't
826 # be necessary: if the user requested certificate verification be 928 # be necessary: if the user requested certificate verification be
827 # disabled, they presumably already saw a message about the inability 929 # disabled, they presumably already saw a message about the inability
828 # to verify the certificate and this message would have printed the 930 # to verify the certificate and this message would have printed the
829 # fingerprint. So printing the fingerprint here adds little to no 931 # fingerprint. So printing the fingerprint here adds little to no
830 # value. 932 # value.
831 ui.warn(_('warning: connection security to %s is disabled per current ' 933 ui.warn(
832 'settings; communication is susceptible to eavesdropping ' 934 _(
833 'and tampering\n') % host) 935 'warning: connection security to %s is disabled per current '
936 'settings; communication is susceptible to eavesdropping '
937 'and tampering\n'
938 )
939 % host
940 )
834 return 941 return
835 942
836 # If a certificate fingerprint is pinned, use it and only it to 943 # If a certificate fingerprint is pinned, use it and only it to
837 # validate the remote cert. 944 # validate the remote cert.
838 peerfingerprints = { 945 peerfingerprints = {
840 'sha256': node.hex(hashlib.sha256(peercert).digest()), 947 'sha256': node.hex(hashlib.sha256(peercert).digest()),
841 'sha512': node.hex(hashlib.sha512(peercert).digest()), 948 'sha512': node.hex(hashlib.sha512(peercert).digest()),
842 } 949 }
843 950
844 def fmtfingerprint(s): 951 def fmtfingerprint(s):
845 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)]) 952 return ':'.join([s[x : x + 2] for x in range(0, len(s), 2)])
846 953
847 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256']) 954 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
848 955
849 if settings['certfingerprints']: 956 if settings['certfingerprints']:
850 for hash, fingerprint in settings['certfingerprints']: 957 for hash, fingerprint in settings['certfingerprints']:
851 if peerfingerprints[hash].lower() == fingerprint: 958 if peerfingerprints[hash].lower() == fingerprint:
852 ui.debug('%s certificate matched fingerprint %s:%s\n' % 959 ui.debug(
853 (host, hash, fmtfingerprint(fingerprint))) 960 '%s certificate matched fingerprint %s:%s\n'
961 % (host, hash, fmtfingerprint(fingerprint))
962 )
854 if settings['legacyfingerprint']: 963 if settings['legacyfingerprint']:
855 ui.warn(_('(SHA-1 fingerprint for %s found in legacy ' 964 ui.warn(
856 '[hostfingerprints] section; ' 965 _(
857 'if you trust this fingerprint, remove the old ' 966 '(SHA-1 fingerprint for %s found in legacy '
858 'SHA-1 fingerprint from [hostfingerprints] and ' 967 '[hostfingerprints] section; '
859 'add the following entry to the new ' 968 'if you trust this fingerprint, remove the old '
860 '[hostsecurity] section: %s:fingerprints=%s)\n') % 969 'SHA-1 fingerprint from [hostfingerprints] and '
861 (host, host, nicefingerprint)) 970 'add the following entry to the new '
971 '[hostsecurity] section: %s:fingerprints=%s)\n'
972 )
973 % (host, host, nicefingerprint)
974 )
862 return 975 return
863 976
864 # Pinned fingerprint didn't match. This is a fatal error. 977 # Pinned fingerprint didn't match. This is a fatal error.
865 if settings['legacyfingerprint']: 978 if settings['legacyfingerprint']:
866 section = 'hostfingerprint' 979 section = 'hostfingerprint'
867 nice = fmtfingerprint(peerfingerprints['sha1']) 980 nice = fmtfingerprint(peerfingerprints['sha1'])
868 else: 981 else:
869 section = 'hostsecurity' 982 section = 'hostsecurity'
870 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash])) 983 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
871 raise error.Abort(_('certificate for %s has unexpected ' 984 raise error.Abort(
872 'fingerprint %s') % (host, nice), 985 _('certificate for %s has unexpected ' 'fingerprint %s')
873 hint=_('check %s configuration') % section) 986 % (host, nice),
987 hint=_('check %s configuration') % section,
988 )
874 989
875 # Security is enabled but no CAs are loaded. We can't establish trust 990 # Security is enabled but no CAs are loaded. We can't establish trust
876 # for the cert so abort. 991 # for the cert so abort.
877 if not sock._hgstate['caloaded']: 992 if not sock._hgstate['caloaded']:
878 raise error.Abort( 993 raise error.Abort(
879 _('unable to verify security of %s (no loaded CA certificates); ' 994 _(
880 'refusing to connect') % host, 995 'unable to verify security of %s (no loaded CA certificates); '
881 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for ' 996 'refusing to connect'
882 'how to configure Mercurial to avoid this error or set ' 997 )
883 'hostsecurity.%s:fingerprints=%s to trust this server') % 998 % host,
884 (host, nicefingerprint)) 999 hint=_(
1000 'see https://mercurial-scm.org/wiki/SecureConnections for '
1001 'how to configure Mercurial to avoid this error or set '
1002 'hostsecurity.%s:fingerprints=%s to trust this server'
1003 )
1004 % (host, nicefingerprint),
1005 )
885 1006
886 msg = _verifycert(peercert2, shost) 1007 msg = _verifycert(peercert2, shost)
887 if msg: 1008 if msg:
888 raise error.Abort(_('%s certificate error: %s') % (host, msg), 1009 raise error.Abort(
889 hint=_('set hostsecurity.%s:certfingerprints=%s ' 1010 _('%s certificate error: %s') % (host, msg),
890 'config setting or use --insecure to connect ' 1011 hint=_(
891 'insecurely') % 1012 'set hostsecurity.%s:certfingerprints=%s '
892 (host, nicefingerprint)) 1013 'config setting or use --insecure to connect '
1014 'insecurely'
1015 )
1016 % (host, nicefingerprint),
1017 )