##// END OF EJS Templates
sslutil: work around SSLContext.get_ca_certs bug on Windows (issue5313)...
Gregory Szorc -
r29631:387bdd53 stable
parent child Browse files
Show More
@@ -1,834 +1,840 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import ssl
16 16 import sys
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 error,
21 21 util,
22 22 )
23 23
24 24 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
25 25 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
26 26 # all exposed via the "ssl" module.
27 27 #
28 28 # Depending on the version of Python being used, SSL/TLS support is either
29 29 # modern/secure or legacy/insecure. Many operations in this module have
30 30 # separate code paths depending on support in Python.
31 31
32 32 configprotocols = set([
33 33 'tls1.0',
34 34 'tls1.1',
35 35 'tls1.2',
36 36 ])
37 37
38 38 hassni = getattr(ssl, 'HAS_SNI', False)
39 39
40 40 # TLS 1.1 and 1.2 may not be supported if the OpenSSL Python is compiled
41 41 # against doesn't support them.
42 42 supportedprotocols = set(['tls1.0'])
43 43 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_1'):
44 44 supportedprotocols.add('tls1.1')
45 45 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_2'):
46 46 supportedprotocols.add('tls1.2')
47 47
48 48 try:
49 49 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
50 50 # SSL/TLS features are available.
51 51 SSLContext = ssl.SSLContext
52 52 modernssl = True
53 53 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
54 54 except AttributeError:
55 55 modernssl = False
56 56 _canloaddefaultcerts = False
57 57
58 58 # We implement SSLContext using the interface from the standard library.
59 59 class SSLContext(object):
60 60 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
61 61 _supportsciphers = sys.version_info >= (2, 7)
62 62
63 63 def __init__(self, protocol):
64 64 # From the public interface of SSLContext
65 65 self.protocol = protocol
66 66 self.check_hostname = False
67 67 self.options = 0
68 68 self.verify_mode = ssl.CERT_NONE
69 69
70 70 # Used by our implementation.
71 71 self._certfile = None
72 72 self._keyfile = None
73 73 self._certpassword = None
74 74 self._cacerts = None
75 75 self._ciphers = None
76 76
77 77 def load_cert_chain(self, certfile, keyfile=None, password=None):
78 78 self._certfile = certfile
79 79 self._keyfile = keyfile
80 80 self._certpassword = password
81 81
82 82 def load_default_certs(self, purpose=None):
83 83 pass
84 84
85 85 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
86 86 if capath:
87 87 raise error.Abort(_('capath not supported'))
88 88 if cadata:
89 89 raise error.Abort(_('cadata not supported'))
90 90
91 91 self._cacerts = cafile
92 92
93 93 def set_ciphers(self, ciphers):
94 94 if not self._supportsciphers:
95 95 raise error.Abort(_('setting ciphers in [hostsecurity] is not '
96 96 'supported by this version of Python'),
97 97 hint=_('remove the config option or run '
98 98 'Mercurial with a modern Python '
99 99 'version (preferred)'))
100 100
101 101 self._ciphers = ciphers
102 102
103 103 def wrap_socket(self, socket, server_hostname=None, server_side=False):
104 104 # server_hostname is unique to SSLContext.wrap_socket and is used
105 105 # for SNI in that context. So there's nothing for us to do with it
106 106 # in this legacy code since we don't support SNI.
107 107
108 108 args = {
109 109 'keyfile': self._keyfile,
110 110 'certfile': self._certfile,
111 111 'server_side': server_side,
112 112 'cert_reqs': self.verify_mode,
113 113 'ssl_version': self.protocol,
114 114 'ca_certs': self._cacerts,
115 115 }
116 116
117 117 if self._supportsciphers:
118 118 args['ciphers'] = self._ciphers
119 119
120 120 return ssl.wrap_socket(socket, **args)
121 121
122 122 def _hostsettings(ui, hostname):
123 123 """Obtain security settings for a hostname.
124 124
125 125 Returns a dict of settings relevant to that hostname.
126 126 """
127 127 s = {
128 128 # Whether we should attempt to load default/available CA certs
129 129 # if an explicit ``cafile`` is not defined.
130 130 'allowloaddefaultcerts': True,
131 131 # List of 2-tuple of (hash algorithm, hash).
132 132 'certfingerprints': [],
133 133 # Path to file containing concatenated CA certs. Used by
134 134 # SSLContext.load_verify_locations().
135 135 'cafile': None,
136 136 # Whether certificate verification should be disabled.
137 137 'disablecertverification': False,
138 138 # Whether the legacy [hostfingerprints] section has data for this host.
139 139 'legacyfingerprint': False,
140 140 # PROTOCOL_* constant to use for SSLContext.__init__.
141 141 'protocol': None,
142 142 # String representation of minimum protocol to be used for UI
143 143 # presentation.
144 144 'protocolui': None,
145 145 # ssl.CERT_* constant used by SSLContext.verify_mode.
146 146 'verifymode': None,
147 147 # Defines extra ssl.OP* bitwise options to set.
148 148 'ctxoptions': None,
149 149 # OpenSSL Cipher List to use (instead of default).
150 150 'ciphers': None,
151 151 }
152 152
153 153 # Allow minimum TLS protocol to be specified in the config.
154 154 def validateprotocol(protocol, key):
155 155 if protocol not in configprotocols:
156 156 raise error.Abort(
157 157 _('unsupported protocol from hostsecurity.%s: %s') %
158 158 (key, protocol),
159 159 hint=_('valid protocols: %s') %
160 160 ' '.join(sorted(configprotocols)))
161 161
162 162 # We default to TLS 1.1+ where we can because TLS 1.0 has known
163 163 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to
164 164 # TLS 1.0+ via config options in case a legacy server is encountered.
165 165 if 'tls1.1' in supportedprotocols:
166 166 defaultprotocol = 'tls1.1'
167 167 else:
168 168 # Let people know they are borderline secure.
169 169 # We don't document this config option because we want people to see
170 170 # the bold warnings on the web site.
171 171 # internal config: hostsecurity.disabletls10warning
172 172 if not ui.configbool('hostsecurity', 'disabletls10warning'):
173 173 ui.warn(_('warning: connecting to %s using legacy security '
174 174 'technology (TLS 1.0); see '
175 175 'https://mercurial-scm.org/wiki/SecureConnections for '
176 176 'more info\n') % hostname)
177 177 defaultprotocol = 'tls1.0'
178 178
179 179 key = 'minimumprotocol'
180 180 protocol = ui.config('hostsecurity', key, defaultprotocol)
181 181 validateprotocol(protocol, key)
182 182
183 183 key = '%s:minimumprotocol' % hostname
184 184 protocol = ui.config('hostsecurity', key, protocol)
185 185 validateprotocol(protocol, key)
186 186
187 187 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
188 188 # We always print a "connection security to %s is disabled..." message when
189 189 # --insecure is used. So no need to print anything more here.
190 190 if ui.insecureconnections:
191 191 protocol = 'tls1.0'
192 192
193 193 s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol)
194 194
195 195 ciphers = ui.config('hostsecurity', 'ciphers')
196 196 ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers)
197 197 s['ciphers'] = ciphers
198 198
199 199 # Look for fingerprints in [hostsecurity] section. Value is a list
200 200 # of <alg>:<fingerprint> strings.
201 201 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
202 202 [])
203 203 for fingerprint in fingerprints:
204 204 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
205 205 raise error.Abort(_('invalid fingerprint for %s: %s') % (
206 206 hostname, fingerprint),
207 207 hint=_('must begin with "sha1:", "sha256:", '
208 208 'or "sha512:"'))
209 209
210 210 alg, fingerprint = fingerprint.split(':', 1)
211 211 fingerprint = fingerprint.replace(':', '').lower()
212 212 s['certfingerprints'].append((alg, fingerprint))
213 213
214 214 # Fingerprints from [hostfingerprints] are always SHA-1.
215 215 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
216 216 fingerprint = fingerprint.replace(':', '').lower()
217 217 s['certfingerprints'].append(('sha1', fingerprint))
218 218 s['legacyfingerprint'] = True
219 219
220 220 # If a host cert fingerprint is defined, it is the only thing that
221 221 # matters. No need to validate CA certs.
222 222 if s['certfingerprints']:
223 223 s['verifymode'] = ssl.CERT_NONE
224 224 s['allowloaddefaultcerts'] = False
225 225
226 226 # If --insecure is used, don't take CAs into consideration.
227 227 elif ui.insecureconnections:
228 228 s['disablecertverification'] = True
229 229 s['verifymode'] = ssl.CERT_NONE
230 230 s['allowloaddefaultcerts'] = False
231 231
232 232 if ui.configbool('devel', 'disableloaddefaultcerts'):
233 233 s['allowloaddefaultcerts'] = False
234 234
235 235 # If both fingerprints and a per-host ca file are specified, issue a warning
236 236 # because users should not be surprised about what security is or isn't
237 237 # being performed.
238 238 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
239 239 if s['certfingerprints'] and cafile:
240 240 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
241 241 'fingerprints defined; using host fingerprints for '
242 242 'verification)\n') % hostname)
243 243
244 244 # Try to hook up CA certificate validation unless something above
245 245 # makes it not necessary.
246 246 if s['verifymode'] is None:
247 247 # Look at per-host ca file first.
248 248 if cafile:
249 249 cafile = util.expandpath(cafile)
250 250 if not os.path.exists(cafile):
251 251 raise error.Abort(_('path specified by %s does not exist: %s') %
252 252 ('hostsecurity.%s:verifycertsfile' % hostname,
253 253 cafile))
254 254 s['cafile'] = cafile
255 255 else:
256 256 # Find global certificates file in config.
257 257 cafile = ui.config('web', 'cacerts')
258 258
259 259 if cafile:
260 260 cafile = util.expandpath(cafile)
261 261 if not os.path.exists(cafile):
262 262 raise error.Abort(_('could not find web.cacerts: %s') %
263 263 cafile)
264 264 elif s['allowloaddefaultcerts']:
265 265 # CAs not defined in config. Try to find system bundles.
266 266 cafile = _defaultcacerts(ui)
267 267 if cafile:
268 268 ui.debug('using %s for CA file\n' % cafile)
269 269
270 270 s['cafile'] = cafile
271 271
272 272 # Require certificate validation if CA certs are being loaded and
273 273 # verification hasn't been disabled above.
274 274 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
275 275 s['verifymode'] = ssl.CERT_REQUIRED
276 276 else:
277 277 # At this point we don't have a fingerprint, aren't being
278 278 # explicitly insecure, and can't load CA certs. Connecting
279 279 # is insecure. We allow the connection and abort during
280 280 # validation (once we have the fingerprint to print to the
281 281 # user).
282 282 s['verifymode'] = ssl.CERT_NONE
283 283
284 284 assert s['protocol'] is not None
285 285 assert s['ctxoptions'] is not None
286 286 assert s['verifymode'] is not None
287 287
288 288 return s
289 289
290 290 def protocolsettings(protocol):
291 291 """Resolve the protocol for a config value.
292 292
293 293 Returns a 3-tuple of (protocol, options, ui value) where the first
294 294 2 items are values used by SSLContext and the last is a string value
295 295 of the ``minimumprotocol`` config option equivalent.
296 296 """
297 297 if protocol not in configprotocols:
298 298 raise ValueError('protocol value not supported: %s' % protocol)
299 299
300 300 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
301 301 # that both ends support, including TLS protocols. On legacy stacks,
302 302 # the highest it likely goes is TLS 1.0. On modern stacks, it can
303 303 # support TLS 1.2.
304 304 #
305 305 # The PROTOCOL_TLSv* constants select a specific TLS version
306 306 # only (as opposed to multiple versions). So the method for
307 307 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
308 308 # disable protocols via SSLContext.options and OP_NO_* constants.
309 309 # However, SSLContext.options doesn't work unless we have the
310 310 # full/real SSLContext available to us.
311 311 if supportedprotocols == set(['tls1.0']):
312 312 if protocol != 'tls1.0':
313 313 raise error.Abort(_('current Python does not support protocol '
314 314 'setting %s') % protocol,
315 315 hint=_('upgrade Python or disable setting since '
316 316 'only TLS 1.0 is supported'))
317 317
318 318 return ssl.PROTOCOL_TLSv1, 0, 'tls1.0'
319 319
320 320 # WARNING: returned options don't work unless the modern ssl module
321 321 # is available. Be careful when adding options here.
322 322
323 323 # SSLv2 and SSLv3 are broken. We ban them outright.
324 324 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
325 325
326 326 if protocol == 'tls1.0':
327 327 # Defaults above are to use TLS 1.0+
328 328 pass
329 329 elif protocol == 'tls1.1':
330 330 options |= ssl.OP_NO_TLSv1
331 331 elif protocol == 'tls1.2':
332 332 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
333 333 else:
334 334 raise error.Abort(_('this should not happen'))
335 335
336 336 # Prevent CRIME.
337 337 # There is no guarantee this attribute is defined on the module.
338 338 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
339 339
340 340 return ssl.PROTOCOL_SSLv23, options, protocol
341 341
342 342 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
343 343 """Add SSL/TLS to a socket.
344 344
345 345 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
346 346 choices based on what security options are available.
347 347
348 348 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
349 349 the following additional arguments:
350 350
351 351 * serverhostname - The expected hostname of the remote server. If the
352 352 server (and client) support SNI, this tells the server which certificate
353 353 to use.
354 354 """
355 355 if not serverhostname:
356 356 raise error.Abort(_('serverhostname argument is required'))
357 357
358 358 settings = _hostsettings(ui, serverhostname)
359 359
360 360 # We can't use ssl.create_default_context() because it calls
361 361 # load_default_certs() unless CA arguments are passed to it. We want to
362 362 # have explicit control over CA loading because implicitly loading
363 363 # CAs may undermine the user's intent. For example, a user may define a CA
364 364 # bundle with a specific CA cert removed. If the system/default CA bundle
365 365 # is loaded and contains that removed CA, you've just undone the user's
366 366 # choice.
367 367 sslcontext = SSLContext(settings['protocol'])
368 368
369 369 # This is a no-op unless using modern ssl.
370 370 sslcontext.options |= settings['ctxoptions']
371 371
372 372 # This still works on our fake SSLContext.
373 373 sslcontext.verify_mode = settings['verifymode']
374 374
375 375 if settings['ciphers']:
376 376 try:
377 377 sslcontext.set_ciphers(settings['ciphers'])
378 378 except ssl.SSLError as e:
379 379 raise error.Abort(_('could not set ciphers: %s') % e.args[0],
380 380 hint=_('change cipher string (%s) in config') %
381 381 settings['ciphers'])
382 382
383 383 if certfile is not None:
384 384 def password():
385 385 f = keyfile or certfile
386 386 return ui.getpass(_('passphrase for %s: ') % f, '')
387 387 sslcontext.load_cert_chain(certfile, keyfile, password)
388 388
389 389 if settings['cafile'] is not None:
390 390 try:
391 391 sslcontext.load_verify_locations(cafile=settings['cafile'])
392 392 except ssl.SSLError as e:
393 393 raise error.Abort(_('error loading CA file %s: %s') % (
394 394 settings['cafile'], e.args[1]),
395 395 hint=_('file is empty or malformed?'))
396 396 caloaded = True
397 397 elif settings['allowloaddefaultcerts']:
398 398 # This is a no-op on old Python.
399 399 sslcontext.load_default_certs()
400 400 caloaded = True
401 401 else:
402 402 caloaded = False
403 403
404 404 try:
405 405 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
406 406 except ssl.SSLError as e:
407 407 # If we're doing certificate verification and no CA certs are loaded,
408 408 # that is almost certainly the reason why verification failed. Provide
409 409 # a hint to the user.
410 410 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
411 411 # only show this warning if modern ssl is available.
412 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
413 modernssl and not sslcontext.get_ca_certs()):
414 ui.warn(_('(an attempt was made to load CA certificates but none '
415 'were loaded; see '
416 'https://mercurial-scm.org/wiki/SecureConnections for '
417 'how to configure Mercurial to avoid this error)\n'))
412 # The exception handler is here because of
413 # https://bugs.python.org/issue20916.
414 try:
415 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
416 modernssl and not sslcontext.get_ca_certs()):
417 ui.warn(_('(an attempt was made to load CA certificates but '
418 'none were loaded; see '
419 'https://mercurial-scm.org/wiki/SecureConnections '
420 'for how to configure Mercurial to avoid this '
421 'error)\n'))
422 except ssl.SSLError:
423 pass
418 424 # Try to print more helpful error messages for known failures.
419 425 if util.safehasattr(e, 'reason'):
420 426 # This error occurs when the client and server don't share a
421 427 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
422 428 # outright. Hopefully the reason for this error is that we require
423 429 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
424 430 # reason, try to emit an actionable warning.
425 431 if e.reason == 'UNSUPPORTED_PROTOCOL':
426 432 # We attempted TLS 1.0+.
427 433 if settings['protocolui'] == 'tls1.0':
428 434 # We support more than just TLS 1.0+. If this happens,
429 435 # the likely scenario is either the client or the server
430 436 # is really old. (e.g. server doesn't support TLS 1.0+ or
431 437 # client doesn't support modern TLS versions introduced
432 438 # several years from when this comment was written).
433 439 if supportedprotocols != set(['tls1.0']):
434 440 ui.warn(_(
435 441 '(could not communicate with %s using security '
436 442 'protocols %s; if you are using a modern Mercurial '
437 443 'version, consider contacting the operator of this '
438 444 'server; see '
439 445 'https://mercurial-scm.org/wiki/SecureConnections '
440 446 'for more info)\n') % (
441 447 serverhostname,
442 448 ', '.join(sorted(supportedprotocols))))
443 449 else:
444 450 ui.warn(_(
445 451 '(could not communicate with %s using TLS 1.0; the '
446 452 'likely cause of this is the server no longer '
447 453 'supports TLS 1.0 because it has known security '
448 454 'vulnerabilities; see '
449 455 'https://mercurial-scm.org/wiki/SecureConnections '
450 456 'for more info)\n') % serverhostname)
451 457 else:
452 458 # We attempted TLS 1.1+. We can only get here if the client
453 459 # supports the configured protocol. So the likely reason is
454 460 # the client wants better security than the server can
455 461 # offer.
456 462 ui.warn(_(
457 463 '(could not negotiate a common security protocol (%s+) '
458 464 'with %s; the likely cause is Mercurial is configured '
459 465 'to be more secure than the server can support)\n') % (
460 466 settings['protocolui'], serverhostname))
461 467 ui.warn(_('(consider contacting the operator of this '
462 468 'server and ask them to support modern TLS '
463 469 'protocol versions; or, set '
464 470 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
465 471 'use of legacy, less secure protocols when '
466 472 'communicating with this server)\n') %
467 473 serverhostname)
468 474 ui.warn(_(
469 475 '(see https://mercurial-scm.org/wiki/SecureConnections '
470 476 'for more info)\n'))
471 477 raise
472 478
473 479 # check if wrap_socket failed silently because socket had been
474 480 # closed
475 481 # - see http://bugs.python.org/issue13721
476 482 if not sslsocket.cipher():
477 483 raise error.Abort(_('ssl connection failed'))
478 484
479 485 sslsocket._hgstate = {
480 486 'caloaded': caloaded,
481 487 'hostname': serverhostname,
482 488 'settings': settings,
483 489 'ui': ui,
484 490 }
485 491
486 492 return sslsocket
487 493
488 494 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
489 495 requireclientcert=False):
490 496 """Wrap a socket for use by servers.
491 497
492 498 ``certfile`` and ``keyfile`` specify the files containing the certificate's
493 499 public and private keys, respectively. Both keys can be defined in the same
494 500 file via ``certfile`` (the private key must come first in the file).
495 501
496 502 ``cafile`` defines the path to certificate authorities.
497 503
498 504 ``requireclientcert`` specifies whether to require client certificates.
499 505
500 506 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
501 507 """
502 508 protocol, options, _protocolui = protocolsettings('tls1.0')
503 509
504 510 # This config option is intended for use in tests only. It is a giant
505 511 # footgun to kill security. Don't define it.
506 512 exactprotocol = ui.config('devel', 'serverexactprotocol')
507 513 if exactprotocol == 'tls1.0':
508 514 protocol = ssl.PROTOCOL_TLSv1
509 515 elif exactprotocol == 'tls1.1':
510 516 if 'tls1.1' not in supportedprotocols:
511 517 raise error.Abort(_('TLS 1.1 not supported by this Python'))
512 518 protocol = ssl.PROTOCOL_TLSv1_1
513 519 elif exactprotocol == 'tls1.2':
514 520 if 'tls1.2' not in supportedprotocols:
515 521 raise error.Abort(_('TLS 1.2 not supported by this Python'))
516 522 protocol = ssl.PROTOCOL_TLSv1_2
517 523 elif exactprotocol:
518 524 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
519 525 exactprotocol)
520 526
521 527 if modernssl:
522 528 # We /could/ use create_default_context() here since it doesn't load
523 529 # CAs when configured for client auth. However, it is hard-coded to
524 530 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
525 531 sslcontext = SSLContext(protocol)
526 532 sslcontext.options |= options
527 533
528 534 # Improve forward secrecy.
529 535 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
530 536 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
531 537
532 538 # Use the list of more secure ciphers if found in the ssl module.
533 539 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
534 540 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
535 541 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
536 542 else:
537 543 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
538 544
539 545 if requireclientcert:
540 546 sslcontext.verify_mode = ssl.CERT_REQUIRED
541 547 else:
542 548 sslcontext.verify_mode = ssl.CERT_NONE
543 549
544 550 if certfile or keyfile:
545 551 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
546 552
547 553 if cafile:
548 554 sslcontext.load_verify_locations(cafile=cafile)
549 555
550 556 return sslcontext.wrap_socket(sock, server_side=True)
551 557
552 558 class wildcarderror(Exception):
553 559 """Represents an error parsing wildcards in DNS name."""
554 560
555 561 def _dnsnamematch(dn, hostname, maxwildcards=1):
556 562 """Match DNS names according RFC 6125 section 6.4.3.
557 563
558 564 This code is effectively copied from CPython's ssl._dnsname_match.
559 565
560 566 Returns a bool indicating whether the expected hostname matches
561 567 the value in ``dn``.
562 568 """
563 569 pats = []
564 570 if not dn:
565 571 return False
566 572
567 573 pieces = dn.split(r'.')
568 574 leftmost = pieces[0]
569 575 remainder = pieces[1:]
570 576 wildcards = leftmost.count('*')
571 577 if wildcards > maxwildcards:
572 578 raise wildcarderror(
573 579 _('too many wildcards in certificate DNS name: %s') % dn)
574 580
575 581 # speed up common case w/o wildcards
576 582 if not wildcards:
577 583 return dn.lower() == hostname.lower()
578 584
579 585 # RFC 6125, section 6.4.3, subitem 1.
580 586 # The client SHOULD NOT attempt to match a presented identifier in which
581 587 # the wildcard character comprises a label other than the left-most label.
582 588 if leftmost == '*':
583 589 # When '*' is a fragment by itself, it matches a non-empty dotless
584 590 # fragment.
585 591 pats.append('[^.]+')
586 592 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
587 593 # RFC 6125, section 6.4.3, subitem 3.
588 594 # The client SHOULD NOT attempt to match a presented identifier
589 595 # where the wildcard character is embedded within an A-label or
590 596 # U-label of an internationalized domain name.
591 597 pats.append(re.escape(leftmost))
592 598 else:
593 599 # Otherwise, '*' matches any dotless string, e.g. www*
594 600 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
595 601
596 602 # add the remaining fragments, ignore any wildcards
597 603 for frag in remainder:
598 604 pats.append(re.escape(frag))
599 605
600 606 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
601 607 return pat.match(hostname) is not None
602 608
603 609 def _verifycert(cert, hostname):
604 610 '''Verify that cert (in socket.getpeercert() format) matches hostname.
605 611 CRLs is not handled.
606 612
607 613 Returns error message if any problems are found and None on success.
608 614 '''
609 615 if not cert:
610 616 return _('no certificate received')
611 617
612 618 dnsnames = []
613 619 san = cert.get('subjectAltName', [])
614 620 for key, value in san:
615 621 if key == 'DNS':
616 622 try:
617 623 if _dnsnamematch(value, hostname):
618 624 return
619 625 except wildcarderror as e:
620 626 return e.args[0]
621 627
622 628 dnsnames.append(value)
623 629
624 630 if not dnsnames:
625 631 # The subject is only checked when there is no DNS in subjectAltName.
626 632 for sub in cert.get('subject', []):
627 633 for key, value in sub:
628 634 # According to RFC 2818 the most specific Common Name must
629 635 # be used.
630 636 if key == 'commonName':
631 637 # 'subject' entries are unicide.
632 638 try:
633 639 value = value.encode('ascii')
634 640 except UnicodeEncodeError:
635 641 return _('IDN in certificate not supported')
636 642
637 643 try:
638 644 if _dnsnamematch(value, hostname):
639 645 return
640 646 except wildcarderror as e:
641 647 return e.args[0]
642 648
643 649 dnsnames.append(value)
644 650
645 651 if len(dnsnames) > 1:
646 652 return _('certificate is for %s') % ', '.join(dnsnames)
647 653 elif len(dnsnames) == 1:
648 654 return _('certificate is for %s') % dnsnames[0]
649 655 else:
650 656 return _('no commonName or subjectAltName found in certificate')
651 657
652 658 def _plainapplepython():
653 659 """return true if this seems to be a pure Apple Python that
654 660 * is unfrozen and presumably has the whole mercurial module in the file
655 661 system
656 662 * presumably is an Apple Python that uses Apple OpenSSL which has patches
657 663 for using system certificate store CAs in addition to the provided
658 664 cacerts file
659 665 """
660 666 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
661 667 return False
662 668 exe = os.path.realpath(sys.executable).lower()
663 669 return (exe.startswith('/usr/bin/python') or
664 670 exe.startswith('/system/library/frameworks/python.framework/'))
665 671
666 672 _systemcacertpaths = [
667 673 # RHEL, CentOS, and Fedora
668 674 '/etc/pki/tls/certs/ca-bundle.trust.crt',
669 675 # Debian, Ubuntu, Gentoo
670 676 '/etc/ssl/certs/ca-certificates.crt',
671 677 ]
672 678
673 679 def _defaultcacerts(ui):
674 680 """return path to default CA certificates or None.
675 681
676 682 It is assumed this function is called when the returned certificates
677 683 file will actually be used to validate connections. Therefore this
678 684 function may print warnings or debug messages assuming this usage.
679 685
680 686 We don't print a message when the Python is able to load default
681 687 CA certs because this scenario is detected at socket connect time.
682 688 """
683 689 # The "certifi" Python package provides certificates. If it is installed,
684 690 # assume the user intends it to be used and use it.
685 691 try:
686 692 import certifi
687 693 certs = certifi.where()
688 694 ui.debug('using ca certificates from certifi\n')
689 695 return certs
690 696 except ImportError:
691 697 pass
692 698
693 699 # On Windows, only the modern ssl module is capable of loading the system
694 700 # CA certificates. If we're not capable of doing that, emit a warning
695 701 # because we'll get a certificate verification error later and the lack
696 702 # of loaded CA certificates will be the reason why.
697 703 # Assertion: this code is only called if certificates are being verified.
698 704 if os.name == 'nt':
699 705 if not _canloaddefaultcerts:
700 706 ui.warn(_('(unable to load Windows CA certificates; see '
701 707 'https://mercurial-scm.org/wiki/SecureConnections for '
702 708 'how to configure Mercurial to avoid this message)\n'))
703 709
704 710 return None
705 711
706 712 # Apple's OpenSSL has patches that allow a specially constructed certificate
707 713 # to load the system CA store. If we're running on Apple Python, use this
708 714 # trick.
709 715 if _plainapplepython():
710 716 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
711 717 if os.path.exists(dummycert):
712 718 return dummycert
713 719
714 720 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
715 721 # load system certs, we're out of luck.
716 722 if sys.platform == 'darwin':
717 723 # FUTURE Consider looking for Homebrew or MacPorts installed certs
718 724 # files. Also consider exporting the keychain certs to a file during
719 725 # Mercurial install.
720 726 if not _canloaddefaultcerts:
721 727 ui.warn(_('(unable to load CA certificates; see '
722 728 'https://mercurial-scm.org/wiki/SecureConnections for '
723 729 'how to configure Mercurial to avoid this message)\n'))
724 730 return None
725 731
726 732 # / is writable on Windows. Out of an abundance of caution make sure
727 733 # we're not on Windows because paths from _systemcacerts could be installed
728 734 # by non-admin users.
729 735 assert os.name != 'nt'
730 736
731 737 # Try to find CA certificates in well-known locations. We print a warning
732 738 # when using a found file because we don't want too much silent magic
733 739 # for security settings. The expectation is that proper Mercurial
734 740 # installs will have the CA certs path defined at install time and the
735 741 # installer/packager will make an appropriate decision on the user's
736 742 # behalf. We only get here and perform this setting as a feature of
737 743 # last resort.
738 744 if not _canloaddefaultcerts:
739 745 for path in _systemcacertpaths:
740 746 if os.path.isfile(path):
741 747 ui.warn(_('(using CA certificates from %s; if you see this '
742 748 'message, your Mercurial install is not properly '
743 749 'configured; see '
744 750 'https://mercurial-scm.org/wiki/SecureConnections '
745 751 'for how to configure Mercurial to avoid this '
746 752 'message)\n') % path)
747 753 return path
748 754
749 755 ui.warn(_('(unable to load CA certificates; see '
750 756 'https://mercurial-scm.org/wiki/SecureConnections for '
751 757 'how to configure Mercurial to avoid this message)\n'))
752 758
753 759 return None
754 760
755 761 def validatesocket(sock):
756 762 """Validate a socket meets security requiremnets.
757 763
758 764 The passed socket must have been created with ``wrapsocket()``.
759 765 """
760 766 host = sock._hgstate['hostname']
761 767 ui = sock._hgstate['ui']
762 768 settings = sock._hgstate['settings']
763 769
764 770 try:
765 771 peercert = sock.getpeercert(True)
766 772 peercert2 = sock.getpeercert()
767 773 except AttributeError:
768 774 raise error.Abort(_('%s ssl connection error') % host)
769 775
770 776 if not peercert:
771 777 raise error.Abort(_('%s certificate error: '
772 778 'no certificate received') % host)
773 779
774 780 if settings['disablecertverification']:
775 781 # We don't print the certificate fingerprint because it shouldn't
776 782 # be necessary: if the user requested certificate verification be
777 783 # disabled, they presumably already saw a message about the inability
778 784 # to verify the certificate and this message would have printed the
779 785 # fingerprint. So printing the fingerprint here adds little to no
780 786 # value.
781 787 ui.warn(_('warning: connection security to %s is disabled per current '
782 788 'settings; communication is susceptible to eavesdropping '
783 789 'and tampering\n') % host)
784 790 return
785 791
786 792 # If a certificate fingerprint is pinned, use it and only it to
787 793 # validate the remote cert.
788 794 peerfingerprints = {
789 795 'sha1': hashlib.sha1(peercert).hexdigest(),
790 796 'sha256': hashlib.sha256(peercert).hexdigest(),
791 797 'sha512': hashlib.sha512(peercert).hexdigest(),
792 798 }
793 799
794 800 def fmtfingerprint(s):
795 801 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
796 802
797 803 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
798 804
799 805 if settings['certfingerprints']:
800 806 for hash, fingerprint in settings['certfingerprints']:
801 807 if peerfingerprints[hash].lower() == fingerprint:
802 808 ui.debug('%s certificate matched fingerprint %s:%s\n' %
803 809 (host, hash, fmtfingerprint(fingerprint)))
804 810 return
805 811
806 812 # Pinned fingerprint didn't match. This is a fatal error.
807 813 if settings['legacyfingerprint']:
808 814 section = 'hostfingerprint'
809 815 nice = fmtfingerprint(peerfingerprints['sha1'])
810 816 else:
811 817 section = 'hostsecurity'
812 818 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
813 819 raise error.Abort(_('certificate for %s has unexpected '
814 820 'fingerprint %s') % (host, nice),
815 821 hint=_('check %s configuration') % section)
816 822
817 823 # Security is enabled but no CAs are loaded. We can't establish trust
818 824 # for the cert so abort.
819 825 if not sock._hgstate['caloaded']:
820 826 raise error.Abort(
821 827 _('unable to verify security of %s (no loaded CA certificates); '
822 828 'refusing to connect') % host,
823 829 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
824 830 'how to configure Mercurial to avoid this error or set '
825 831 'hostsecurity.%s:fingerprints=%s to trust this server') %
826 832 (host, nicefingerprint))
827 833
828 834 msg = _verifycert(peercert2, host)
829 835 if msg:
830 836 raise error.Abort(_('%s certificate error: %s') % (host, msg),
831 837 hint=_('set hostsecurity.%s:certfingerprints=%s '
832 838 'config setting or use --insecure to connect '
833 839 'insecurely') %
834 840 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now