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