mercurial/sslutil.py
changeset 43076 2372284d9457
parent 42269 c8d55ff80da1
child 43077 687b865b95ad
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         )