comparison mercurial/sslutil.py @ 29605:519bb4f9d3a4 stable 3.9-rc

merge default into stable for 3.9 code freeze
author Matt Mackall <mpm@selenic.com>
date Mon, 18 Jul 2016 23:28:14 -0500
parents 6cff2ac0ccb9
children 2960ceee1948
comparison
equal deleted inserted replaced
29460:a7d1532b26a1 29605:519bb4f9d3a4
7 # This software may be used and distributed according to the terms of the 7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version. 8 # GNU General Public License version 2 or any later version.
9 9
10 from __future__ import absolute_import 10 from __future__ import absolute_import
11 11
12 import hashlib
12 import os 13 import os
13 import re 14 import re
14 import ssl 15 import ssl
15 import sys 16 import sys
16 17
26 # 27 #
27 # Depending on the version of Python being used, SSL/TLS support is either 28 # Depending on the version of Python being used, SSL/TLS support is either
28 # modern/secure or legacy/insecure. Many operations in this module have 29 # modern/secure or legacy/insecure. Many operations in this module have
29 # separate code paths depending on support in Python. 30 # separate code paths depending on support in Python.
30 31
32 configprotocols = set([
33 'tls1.0',
34 'tls1.1',
35 'tls1.2',
36 ])
37
31 hassni = getattr(ssl, 'HAS_SNI', False) 38 hassni = getattr(ssl, 'HAS_SNI', False)
32 39
33 try: 40 # TLS 1.1 and 1.2 may not be supported if the OpenSSL Python is compiled
34 OP_NO_SSLv2 = ssl.OP_NO_SSLv2 41 # against doesn't support them.
35 OP_NO_SSLv3 = ssl.OP_NO_SSLv3 42 supportedprotocols = set(['tls1.0'])
36 except AttributeError: 43 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_1'):
37 OP_NO_SSLv2 = 0x1000000 44 supportedprotocols.add('tls1.1')
38 OP_NO_SSLv3 = 0x2000000 45 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_2'):
46 supportedprotocols.add('tls1.2')
39 47
40 try: 48 try:
41 # ssl.SSLContext was added in 2.7.9 and presence indicates modern 49 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
42 # SSL/TLS features are available. 50 # SSL/TLS features are available.
43 SSLContext = ssl.SSLContext 51 SSLContext = ssl.SSLContext
74 def load_default_certs(self, purpose=None): 82 def load_default_certs(self, purpose=None):
75 pass 83 pass
76 84
77 def load_verify_locations(self, cafile=None, capath=None, cadata=None): 85 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
78 if capath: 86 if capath:
79 raise error.Abort('capath not supported') 87 raise error.Abort(_('capath not supported'))
80 if cadata: 88 if cadata:
81 raise error.Abort('cadata not supported') 89 raise error.Abort(_('cadata not supported'))
82 90
83 self._cacerts = cafile 91 self._cacerts = cafile
84 92
85 def set_ciphers(self, ciphers): 93 def set_ciphers(self, ciphers):
86 if not self._supportsciphers: 94 if not self._supportsciphers:
87 raise error.Abort('setting ciphers not supported') 95 raise error.Abort(_('setting ciphers in [hostsecurity] is not '
96 'supported by this version of Python'),
97 hint=_('remove the config option or run '
98 'Mercurial with a modern Python '
99 'version (preferred)'))
88 100
89 self._ciphers = ciphers 101 self._ciphers = ciphers
90 102
91 def wrap_socket(self, socket, server_hostname=None, server_side=False): 103 def wrap_socket(self, socket, server_hostname=None, server_side=False):
92 # server_hostname is unique to SSLContext.wrap_socket and is used 104 # server_hostname is unique to SSLContext.wrap_socket and is used
105 if self._supportsciphers: 117 if self._supportsciphers:
106 args['ciphers'] = self._ciphers 118 args['ciphers'] = self._ciphers
107 119
108 return ssl.wrap_socket(socket, **args) 120 return ssl.wrap_socket(socket, **args)
109 121
110 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE, 122 def _hostsettings(ui, hostname):
111 ca_certs=None, serverhostname=None): 123 """Obtain security settings for a hostname.
112 """Add SSL/TLS to a socket. 124
113 125 Returns a dict of settings relevant to that hostname.
114 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
115 choices based on what security options are available.
116
117 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
118 the following additional arguments:
119
120 * serverhostname - The expected hostname of the remote server. If the
121 server (and client) support SNI, this tells the server which certificate
122 to use.
123 """ 126 """
127 s = {
128 # Whether we should attempt to load default/available CA certs
129 # if an explicit ``cafile`` is not defined.
130 'allowloaddefaultcerts': True,
131 # List of 2-tuple of (hash algorithm, hash).
132 'certfingerprints': [],
133 # Path to file containing concatenated CA certs. Used by
134 # SSLContext.load_verify_locations().
135 'cafile': None,
136 # Whether certificate verification should be disabled.
137 'disablecertverification': False,
138 # Whether the legacy [hostfingerprints] section has data for this host.
139 'legacyfingerprint': False,
140 # PROTOCOL_* constant to use for SSLContext.__init__.
141 'protocol': None,
142 # ssl.CERT_* constant used by SSLContext.verify_mode.
143 'verifymode': None,
144 # Defines extra ssl.OP* bitwise options to set.
145 'ctxoptions': None,
146 # OpenSSL Cipher List to use (instead of default).
147 'ciphers': None,
148 }
149
150 # Allow minimum TLS protocol to be specified in the config.
151 def validateprotocol(protocol, key):
152 if protocol not in configprotocols:
153 raise error.Abort(
154 _('unsupported protocol from hostsecurity.%s: %s') %
155 (key, protocol),
156 hint=_('valid protocols: %s') %
157 ' '.join(sorted(configprotocols)))
158
159 # We default to TLS 1.1+ where we can because TLS 1.0 has known
160 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to
161 # TLS 1.0+ via config options in case a legacy server is encountered.
162 if 'tls1.1' in supportedprotocols:
163 defaultprotocol = 'tls1.1'
164 else:
165 # Let people know they are borderline secure.
166 # We don't document this config option because we want people to see
167 # the bold warnings on the web site.
168 # internal config: hostsecurity.disabletls10warning
169 if not ui.configbool('hostsecurity', 'disabletls10warning'):
170 ui.warn(_('warning: connecting to %s using legacy security '
171 'technology (TLS 1.0); see '
172 'https://mercurial-scm.org/wiki/SecureConnections for '
173 'more info\n') % hostname)
174 defaultprotocol = 'tls1.0'
175
176 key = 'minimumprotocol'
177 protocol = ui.config('hostsecurity', key, defaultprotocol)
178 validateprotocol(protocol, key)
179
180 key = '%s:minimumprotocol' % hostname
181 protocol = ui.config('hostsecurity', key, protocol)
182 validateprotocol(protocol, key)
183
184 s['protocol'], s['ctxoptions'] = protocolsettings(protocol)
185
186 ciphers = ui.config('hostsecurity', 'ciphers')
187 ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers)
188 s['ciphers'] = ciphers
189
190 # Look for fingerprints in [hostsecurity] section. Value is a list
191 # of <alg>:<fingerprint> strings.
192 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
193 [])
194 for fingerprint in fingerprints:
195 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
196 raise error.Abort(_('invalid fingerprint for %s: %s') % (
197 hostname, fingerprint),
198 hint=_('must begin with "sha1:", "sha256:", '
199 'or "sha512:"'))
200
201 alg, fingerprint = fingerprint.split(':', 1)
202 fingerprint = fingerprint.replace(':', '').lower()
203 s['certfingerprints'].append((alg, fingerprint))
204
205 # Fingerprints from [hostfingerprints] are always SHA-1.
206 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
207 fingerprint = fingerprint.replace(':', '').lower()
208 s['certfingerprints'].append(('sha1', fingerprint))
209 s['legacyfingerprint'] = True
210
211 # If a host cert fingerprint is defined, it is the only thing that
212 # matters. No need to validate CA certs.
213 if s['certfingerprints']:
214 s['verifymode'] = ssl.CERT_NONE
215 s['allowloaddefaultcerts'] = False
216
217 # If --insecure is used, don't take CAs into consideration.
218 elif ui.insecureconnections:
219 s['disablecertverification'] = True
220 s['verifymode'] = ssl.CERT_NONE
221 s['allowloaddefaultcerts'] = False
222
223 if ui.configbool('devel', 'disableloaddefaultcerts'):
224 s['allowloaddefaultcerts'] = False
225
226 # If both fingerprints and a per-host ca file are specified, issue a warning
227 # because users should not be surprised about what security is or isn't
228 # being performed.
229 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
230 if s['certfingerprints'] and cafile:
231 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
232 'fingerprints defined; using host fingerprints for '
233 'verification)\n') % hostname)
234
235 # Try to hook up CA certificate validation unless something above
236 # makes it not necessary.
237 if s['verifymode'] is None:
238 # Look at per-host ca file first.
239 if cafile:
240 cafile = util.expandpath(cafile)
241 if not os.path.exists(cafile):
242 raise error.Abort(_('path specified by %s does not exist: %s') %
243 ('hostsecurity.%s:verifycertsfile' % hostname,
244 cafile))
245 s['cafile'] = cafile
246 else:
247 # Find global certificates file in config.
248 cafile = ui.config('web', 'cacerts')
249
250 if cafile:
251 cafile = util.expandpath(cafile)
252 if not os.path.exists(cafile):
253 raise error.Abort(_('could not find web.cacerts: %s') %
254 cafile)
255 elif s['allowloaddefaultcerts']:
256 # CAs not defined in config. Try to find system bundles.
257 cafile = _defaultcacerts(ui)
258 if cafile:
259 ui.debug('using %s for CA file\n' % cafile)
260
261 s['cafile'] = cafile
262
263 # Require certificate validation if CA certs are being loaded and
264 # verification hasn't been disabled above.
265 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
266 s['verifymode'] = ssl.CERT_REQUIRED
267 else:
268 # At this point we don't have a fingerprint, aren't being
269 # explicitly insecure, and can't load CA certs. Connecting
270 # is insecure. We allow the connection and abort during
271 # validation (once we have the fingerprint to print to the
272 # user).
273 s['verifymode'] = ssl.CERT_NONE
274
275 assert s['protocol'] is not None
276 assert s['ctxoptions'] is not None
277 assert s['verifymode'] is not None
278
279 return s
280
281 def protocolsettings(protocol):
282 """Resolve the protocol and context options for a config value."""
283 if protocol not in configprotocols:
284 raise ValueError('protocol value not supported: %s' % protocol)
285
124 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol 286 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
125 # that both ends support, including TLS protocols. On legacy stacks, 287 # that both ends support, including TLS protocols. On legacy stacks,
126 # the highest it likely goes in TLS 1.0. On modern stacks, it can 288 # the highest it likely goes is TLS 1.0. On modern stacks, it can
127 # support TLS 1.2. 289 # support TLS 1.2.
128 # 290 #
129 # The PROTOCOL_TLSv* constants select a specific TLS version 291 # The PROTOCOL_TLSv* constants select a specific TLS version
130 # only (as opposed to multiple versions). So the method for 292 # only (as opposed to multiple versions). So the method for
131 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and 293 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
132 # disable protocols via SSLContext.options and OP_NO_* constants. 294 # disable protocols via SSLContext.options and OP_NO_* constants.
133 # However, SSLContext.options doesn't work unless we have the 295 # However, SSLContext.options doesn't work unless we have the
134 # full/real SSLContext available to us. 296 # full/real SSLContext available to us.
135 # 297 if supportedprotocols == set(['tls1.0']):
298 if protocol != 'tls1.0':
299 raise error.Abort(_('current Python does not support protocol '
300 'setting %s') % protocol,
301 hint=_('upgrade Python or disable setting since '
302 'only TLS 1.0 is supported'))
303
304 return ssl.PROTOCOL_TLSv1, 0
305
306 # WARNING: returned options don't work unless the modern ssl module
307 # is available. Be careful when adding options here.
308
136 # SSLv2 and SSLv3 are broken. We ban them outright. 309 # SSLv2 and SSLv3 are broken. We ban them outright.
137 if modernssl: 310 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
138 protocol = ssl.PROTOCOL_SSLv23 311
312 if protocol == 'tls1.0':
313 # Defaults above are to use TLS 1.0+
314 pass
315 elif protocol == 'tls1.1':
316 options |= ssl.OP_NO_TLSv1
317 elif protocol == 'tls1.2':
318 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
139 else: 319 else:
140 protocol = ssl.PROTOCOL_TLSv1 320 raise error.Abort(_('this should not happen'))
141 321
142 # TODO use ssl.create_default_context() on modernssl. 322 # Prevent CRIME.
143 sslcontext = SSLContext(protocol) 323 # There is no guarantee this attribute is defined on the module.
144 324 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
145 # This is a no-op on old Python. 325
146 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3 326 return ssl.PROTOCOL_SSLv23, options
327
328 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
329 """Add SSL/TLS to a socket.
330
331 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
332 choices based on what security options are available.
333
334 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
335 the following additional arguments:
336
337 * serverhostname - The expected hostname of the remote server. If the
338 server (and client) support SNI, this tells the server which certificate
339 to use.
340 """
341 if not serverhostname:
342 raise error.Abort(_('serverhostname argument is required'))
343
344 settings = _hostsettings(ui, serverhostname)
345
346 # We can't use ssl.create_default_context() because it calls
347 # load_default_certs() unless CA arguments are passed to it. We want to
348 # have explicit control over CA loading because implicitly loading
349 # CAs may undermine the user's intent. For example, a user may define a CA
350 # bundle with a specific CA cert removed. If the system/default CA bundle
351 # is loaded and contains that removed CA, you've just undone the user's
352 # choice.
353 sslcontext = SSLContext(settings['protocol'])
354
355 # This is a no-op unless using modern ssl.
356 sslcontext.options |= settings['ctxoptions']
147 357
148 # This still works on our fake SSLContext. 358 # This still works on our fake SSLContext.
149 sslcontext.verify_mode = cert_reqs 359 sslcontext.verify_mode = settings['verifymode']
360
361 if settings['ciphers']:
362 try:
363 sslcontext.set_ciphers(settings['ciphers'])
364 except ssl.SSLError as e:
365 raise error.Abort(_('could not set ciphers: %s') % e.args[0],
366 hint=_('change cipher string (%s) in config') %
367 settings['ciphers'])
150 368
151 if certfile is not None: 369 if certfile is not None:
152 def password(): 370 def password():
153 f = keyfile or certfile 371 f = keyfile or certfile
154 return ui.getpass(_('passphrase for %s: ') % f, '') 372 return ui.getpass(_('passphrase for %s: ') % f, '')
155 sslcontext.load_cert_chain(certfile, keyfile, password) 373 sslcontext.load_cert_chain(certfile, keyfile, password)
156 374
157 if ca_certs is not None: 375 if settings['cafile'] is not None:
158 sslcontext.load_verify_locations(cafile=ca_certs) 376 try:
159 else: 377 sslcontext.load_verify_locations(cafile=settings['cafile'])
378 except ssl.SSLError as e:
379 raise error.Abort(_('error loading CA file %s: %s') % (
380 settings['cafile'], e.args[1]),
381 hint=_('file is empty or malformed?'))
382 caloaded = True
383 elif settings['allowloaddefaultcerts']:
160 # This is a no-op on old Python. 384 # This is a no-op on old Python.
161 sslcontext.load_default_certs() 385 sslcontext.load_default_certs()
162 386 caloaded = True
163 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname) 387 else:
388 caloaded = False
389
390 try:
391 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
392 except ssl.SSLError as e:
393 # If we're doing certificate verification and no CA certs are loaded,
394 # that is almost certainly the reason why verification failed. Provide
395 # a hint to the user.
396 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
397 # only show this warning if modern ssl is available.
398 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
399 modernssl and not sslcontext.get_ca_certs()):
400 ui.warn(_('(an attempt was made to load CA certificates but none '
401 'were loaded; see '
402 'https://mercurial-scm.org/wiki/SecureConnections for '
403 'how to configure Mercurial to avoid this error)\n'))
404 # Try to print more helpful error messages for known failures.
405 if util.safehasattr(e, 'reason'):
406 if e.reason == 'UNSUPPORTED_PROTOCOL':
407 ui.warn(_('(could not negotiate a common protocol; see '
408 'https://mercurial-scm.org/wiki/SecureConnections '
409 'for how to configure Mercurial to avoid this '
410 'error)\n'))
411 raise
412
164 # check if wrap_socket failed silently because socket had been 413 # check if wrap_socket failed silently because socket had been
165 # closed 414 # closed
166 # - see http://bugs.python.org/issue13721 415 # - see http://bugs.python.org/issue13721
167 if not sslsocket.cipher(): 416 if not sslsocket.cipher():
168 raise error.Abort(_('ssl connection failed')) 417 raise error.Abort(_('ssl connection failed'))
418
419 sslsocket._hgstate = {
420 'caloaded': caloaded,
421 'hostname': serverhostname,
422 'settings': settings,
423 'ui': ui,
424 }
425
169 return sslsocket 426 return sslsocket
427
428 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
429 requireclientcert=False):
430 """Wrap a socket for use by servers.
431
432 ``certfile`` and ``keyfile`` specify the files containing the certificate's
433 public and private keys, respectively. Both keys can be defined in the same
434 file via ``certfile`` (the private key must come first in the file).
435
436 ``cafile`` defines the path to certificate authorities.
437
438 ``requireclientcert`` specifies whether to require client certificates.
439
440 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
441 """
442 protocol, options = protocolsettings('tls1.0')
443
444 # This config option is intended for use in tests only. It is a giant
445 # footgun to kill security. Don't define it.
446 exactprotocol = ui.config('devel', 'serverexactprotocol')
447 if exactprotocol == 'tls1.0':
448 protocol = ssl.PROTOCOL_TLSv1
449 elif exactprotocol == 'tls1.1':
450 if 'tls1.1' not in supportedprotocols:
451 raise error.Abort(_('TLS 1.1 not supported by this Python'))
452 protocol = ssl.PROTOCOL_TLSv1_1
453 elif exactprotocol == 'tls1.2':
454 if 'tls1.2' not in supportedprotocols:
455 raise error.Abort(_('TLS 1.2 not supported by this Python'))
456 protocol = ssl.PROTOCOL_TLSv1_2
457 elif exactprotocol:
458 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
459 exactprotocol)
460
461 if modernssl:
462 # We /could/ use create_default_context() here since it doesn't load
463 # CAs when configured for client auth. However, it is hard-coded to
464 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
465 sslcontext = SSLContext(protocol)
466 sslcontext.options |= options
467
468 # Improve forward secrecy.
469 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
470 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
471
472 # Use the list of more secure ciphers if found in the ssl module.
473 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
474 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
475 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
476 else:
477 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
478
479 if requireclientcert:
480 sslcontext.verify_mode = ssl.CERT_REQUIRED
481 else:
482 sslcontext.verify_mode = ssl.CERT_NONE
483
484 if certfile or keyfile:
485 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
486
487 if cafile:
488 sslcontext.load_verify_locations(cafile=cafile)
489
490 return sslcontext.wrap_socket(sock, server_side=True)
170 491
171 class wildcarderror(Exception): 492 class wildcarderror(Exception):
172 """Represents an error parsing wildcards in DNS name.""" 493 """Represents an error parsing wildcards in DNS name."""
173 494
174 def _dnsnamematch(dn, hostname, maxwildcards=1): 495 def _dnsnamematch(dn, hostname, maxwildcards=1):
266 elif len(dnsnames) == 1: 587 elif len(dnsnames) == 1:
267 return _('certificate is for %s') % dnsnames[0] 588 return _('certificate is for %s') % dnsnames[0]
268 else: 589 else:
269 return _('no commonName or subjectAltName found in certificate') 590 return _('no commonName or subjectAltName found in certificate')
270 591
271
272 # CERT_REQUIRED means fetch the cert from the server all the time AND
273 # validate it against the CA store provided in web.cacerts.
274
275 def _plainapplepython(): 592 def _plainapplepython():
276 """return true if this seems to be a pure Apple Python that 593 """return true if this seems to be a pure Apple Python that
277 * is unfrozen and presumably has the whole mercurial module in the file 594 * is unfrozen and presumably has the whole mercurial module in the file
278 system 595 system
279 * presumably is an Apple Python that uses Apple OpenSSL which has patches 596 * presumably is an Apple Python that uses Apple OpenSSL which has patches
284 return False 601 return False
285 exe = os.path.realpath(sys.executable).lower() 602 exe = os.path.realpath(sys.executable).lower()
286 return (exe.startswith('/usr/bin/python') or 603 return (exe.startswith('/usr/bin/python') or
287 exe.startswith('/system/library/frameworks/python.framework/')) 604 exe.startswith('/system/library/frameworks/python.framework/'))
288 605
289 def _defaultcacerts(): 606 _systemcacertpaths = [
290 """return path to CA certificates; None for system's store; ! to disable""" 607 # RHEL, CentOS, and Fedora
608 '/etc/pki/tls/certs/ca-bundle.trust.crt',
609 # Debian, Ubuntu, Gentoo
610 '/etc/ssl/certs/ca-certificates.crt',
611 ]
612
613 def _defaultcacerts(ui):
614 """return path to default CA certificates or None.
615
616 It is assumed this function is called when the returned certificates
617 file will actually be used to validate connections. Therefore this
618 function may print warnings or debug messages assuming this usage.
619
620 We don't print a message when the Python is able to load default
621 CA certs because this scenario is detected at socket connect time.
622 """
623 # The "certifi" Python package provides certificates. If it is installed,
624 # assume the user intends it to be used and use it.
625 try:
626 import certifi
627 certs = certifi.where()
628 ui.debug('using ca certificates from certifi\n')
629 return certs
630 except ImportError:
631 pass
632
633 # On Windows, only the modern ssl module is capable of loading the system
634 # CA certificates. If we're not capable of doing that, emit a warning
635 # because we'll get a certificate verification error later and the lack
636 # of loaded CA certificates will be the reason why.
637 # Assertion: this code is only called if certificates are being verified.
638 if os.name == 'nt':
639 if not _canloaddefaultcerts:
640 ui.warn(_('(unable to load Windows CA certificates; see '
641 'https://mercurial-scm.org/wiki/SecureConnections for '
642 'how to configure Mercurial to avoid this message)\n'))
643
644 return None
645
646 # Apple's OpenSSL has patches that allow a specially constructed certificate
647 # to load the system CA store. If we're running on Apple Python, use this
648 # trick.
291 if _plainapplepython(): 649 if _plainapplepython():
292 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem') 650 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
293 if os.path.exists(dummycert): 651 if os.path.exists(dummycert):
294 return dummycert 652 return dummycert
295 if _canloaddefaultcerts: 653
654 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
655 # load system certs, we're out of luck.
656 if sys.platform == 'darwin':
657 # FUTURE Consider looking for Homebrew or MacPorts installed certs
658 # files. Also consider exporting the keychain certs to a file during
659 # Mercurial install.
660 if not _canloaddefaultcerts:
661 ui.warn(_('(unable to load CA certificates; see '
662 'https://mercurial-scm.org/wiki/SecureConnections for '
663 'how to configure Mercurial to avoid this message)\n'))
296 return None 664 return None
297 return '!' 665
298 666 # / is writable on Windows. Out of an abundance of caution make sure
299 def sslkwargs(ui, host): 667 # we're not on Windows because paths from _systemcacerts could be installed
300 kws = {'ui': ui} 668 # by non-admin users.
301 hostfingerprint = ui.config('hostfingerprints', host) 669 assert os.name != 'nt'
302 if hostfingerprint: 670
303 return kws 671 # Try to find CA certificates in well-known locations. We print a warning
304 cacerts = ui.config('web', 'cacerts') 672 # when using a found file because we don't want too much silent magic
305 if cacerts == '!': 673 # for security settings. The expectation is that proper Mercurial
306 pass 674 # installs will have the CA certs path defined at install time and the
307 elif cacerts: 675 # installer/packager will make an appropriate decision on the user's
308 cacerts = util.expandpath(cacerts) 676 # behalf. We only get here and perform this setting as a feature of
309 if not os.path.exists(cacerts): 677 # last resort.
310 raise error.Abort(_('could not find web.cacerts: %s') % cacerts) 678 if not _canloaddefaultcerts:
311 else: 679 for path in _systemcacertpaths:
312 cacerts = _defaultcacerts() 680 if os.path.isfile(path):
313 if cacerts and cacerts != '!': 681 ui.warn(_('(using CA certificates from %s; if you see this '
314 ui.debug('using %s to enable OS X system CA\n' % cacerts) 682 'message, your Mercurial install is not properly '
315 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts') 683 'configured; see '
316 if cacerts != '!': 684 'https://mercurial-scm.org/wiki/SecureConnections '
317 kws.update({'ca_certs': cacerts, 685 'for how to configure Mercurial to avoid this '
318 'cert_reqs': ssl.CERT_REQUIRED, 686 'message)\n') % path)
319 }) 687 return path
320 return kws 688
321 689 ui.warn(_('(unable to load CA certificates; see '
322 class validator(object): 690 'https://mercurial-scm.org/wiki/SecureConnections for '
323 def __init__(self, ui, host): 691 'how to configure Mercurial to avoid this message)\n'))
324 self.ui = ui 692
325 self.host = host 693 return None
326 694
327 def __call__(self, sock, strict=False): 695 def validatesocket(sock):
328 host = self.host 696 """Validate a socket meets security requiremnets.
329 697
330 if not sock.cipher(): # work around http://bugs.python.org/issue13721 698 The passed socket must have been created with ``wrapsocket()``.
331 raise error.Abort(_('%s ssl connection error') % host) 699 """
332 try: 700 host = sock._hgstate['hostname']
333 peercert = sock.getpeercert(True) 701 ui = sock._hgstate['ui']
334 peercert2 = sock.getpeercert() 702 settings = sock._hgstate['settings']
335 except AttributeError: 703
336 raise error.Abort(_('%s ssl connection error') % host) 704 try:
337 705 peercert = sock.getpeercert(True)
338 if not peercert: 706 peercert2 = sock.getpeercert()
339 raise error.Abort(_('%s certificate error: ' 707 except AttributeError:
340 'no certificate received') % host) 708 raise error.Abort(_('%s ssl connection error') % host)
341 709
342 # If a certificate fingerprint is pinned, use it and only it to 710 if not peercert:
343 # validate the remote cert. 711 raise error.Abort(_('%s certificate error: '
344 hostfingerprints = self.ui.configlist('hostfingerprints', host) 712 'no certificate received') % host)
345 peerfingerprint = util.sha1(peercert).hexdigest() 713
346 nicefingerprint = ":".join([peerfingerprint[x:x + 2] 714 if settings['disablecertverification']:
347 for x in xrange(0, len(peerfingerprint), 2)]) 715 # We don't print the certificate fingerprint because it shouldn't
348 if hostfingerprints: 716 # be necessary: if the user requested certificate verification be
349 fingerprintmatch = False 717 # disabled, they presumably already saw a message about the inability
350 for hostfingerprint in hostfingerprints: 718 # to verify the certificate and this message would have printed the
351 if peerfingerprint.lower() == \ 719 # fingerprint. So printing the fingerprint here adds little to no
352 hostfingerprint.replace(':', '').lower(): 720 # value.
353 fingerprintmatch = True 721 ui.warn(_('warning: connection security to %s is disabled per current '
354 break 722 'settings; communication is susceptible to eavesdropping '
355 if not fingerprintmatch: 723 'and tampering\n') % host)
356 raise error.Abort(_('certificate for %s has unexpected ' 724 return
357 'fingerprint %s') % (host, nicefingerprint), 725
358 hint=_('check hostfingerprint configuration')) 726 # If a certificate fingerprint is pinned, use it and only it to
359 self.ui.debug('%s certificate matched fingerprint %s\n' % 727 # validate the remote cert.
360 (host, nicefingerprint)) 728 peerfingerprints = {
361 return 729 'sha1': hashlib.sha1(peercert).hexdigest(),
362 730 'sha256': hashlib.sha256(peercert).hexdigest(),
363 # No pinned fingerprint. Establish trust by looking at the CAs. 731 'sha512': hashlib.sha512(peercert).hexdigest(),
364 cacerts = self.ui.config('web', 'cacerts') 732 }
365 if cacerts != '!': 733
366 msg = _verifycert(peercert2, host) 734 def fmtfingerprint(s):
367 if msg: 735 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
368 raise error.Abort(_('%s certificate error: %s') % (host, msg), 736
369 hint=_('configure hostfingerprint %s or use ' 737 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
370 '--insecure to connect insecurely') % 738
371 nicefingerprint) 739 if settings['certfingerprints']:
372 self.ui.debug('%s certificate successfully verified\n' % host) 740 for hash, fingerprint in settings['certfingerprints']:
373 elif strict: 741 if peerfingerprints[hash].lower() == fingerprint:
374 raise error.Abort(_('%s certificate with fingerprint %s not ' 742 ui.debug('%s certificate matched fingerprint %s:%s\n' %
375 'verified') % (host, nicefingerprint), 743 (host, hash, fmtfingerprint(fingerprint)))
376 hint=_('check hostfingerprints or web.cacerts ' 744 return
377 'config setting')) 745
746 # Pinned fingerprint didn't match. This is a fatal error.
747 if settings['legacyfingerprint']:
748 section = 'hostfingerprint'
749 nice = fmtfingerprint(peerfingerprints['sha1'])
378 else: 750 else:
379 self.ui.warn(_('warning: %s certificate with fingerprint %s not ' 751 section = 'hostsecurity'
380 'verified (check hostfingerprints or web.cacerts ' 752 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
381 'config setting)\n') % 753 raise error.Abort(_('certificate for %s has unexpected '
382 (host, nicefingerprint)) 754 'fingerprint %s') % (host, nice),
755 hint=_('check %s configuration') % section)
756
757 # Security is enabled but no CAs are loaded. We can't establish trust
758 # for the cert so abort.
759 if not sock._hgstate['caloaded']:
760 raise error.Abort(
761 _('unable to verify security of %s (no loaded CA certificates); '
762 'refusing to connect') % host,
763 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
764 'how to configure Mercurial to avoid this error or set '
765 'hostsecurity.%s:fingerprints=%s to trust this server') %
766 (host, nicefingerprint))
767
768 msg = _verifycert(peercert2, host)
769 if msg:
770 raise error.Abort(_('%s certificate error: %s') % (host, msg),
771 hint=_('set hostsecurity.%s:certfingerprints=%s '
772 'config setting or use --insecure to connect '
773 'insecurely') %
774 (host, nicefingerprint))