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