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