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) |
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 |
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. |
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 |
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 ) |