Mercurial > public > mercurial-scm > hg-stable
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 ) |