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