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