Mercurial > hg
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)) |