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