##// END OF EJS Templates
sslutil: another use proper attribute to select python 3.7+...
Ondrej Pohorelsky -
r50278:de2e158c default
parent child Browse files
Show More
@@ -1,923 +1,923 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'
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, b'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 if util.safehasattr(ssl, 'PROTOCOL_TLS_SERVER'):
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'serverexactprotocol')
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 serverexactprotocol: %s') % exactprotocol
587 587 )
588 588 else:
589 589 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
590 590 # ends support, including TLS protocols. commonssloptions() restricts the
591 591 # set of allowed protocols.
592 592 protocol = ssl.PROTOCOL_SSLv23
593 593 options = commonssloptions(b'tls1.0')
594 594
595 595 # This config option is intended for use in tests only. It is a giant
596 596 # footgun to kill security. Don't define it.
597 597 exactprotocol = ui.config(b'devel', b'serverexactprotocol')
598 598 if exactprotocol == b'tls1.0':
599 599 if b'tls1.0' not in supportedprotocols:
600 600 raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
601 601 protocol = ssl.PROTOCOL_TLSv1
602 602 elif exactprotocol == b'tls1.1':
603 603 if b'tls1.1' not in supportedprotocols:
604 604 raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
605 605 protocol = ssl.PROTOCOL_TLSv1_1
606 606 elif exactprotocol == b'tls1.2':
607 607 if b'tls1.2' not in supportedprotocols:
608 608 raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
609 609 protocol = ssl.PROTOCOL_TLSv1_2
610 610 elif exactprotocol:
611 611 raise error.Abort(
612 612 _(b'invalid value for serverexactprotocol: %s') % exactprotocol
613 613 )
614 614
615 615 # We /could/ use create_default_context() here since it doesn't load
616 616 # CAs when configured for client auth. However, it is hard-coded to
617 617 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
618 618 sslcontext = ssl.SSLContext(protocol)
619 619 sslcontext.options |= options
620 620
621 621 # Improve forward secrecy.
622 622 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
623 623 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
624 624
625 625 # In tests, allow insecure ciphers
626 626 # Otherwise, use the list of more secure ciphers if found in the ssl module.
627 627 if exactprotocol:
628 628 sslcontext.set_ciphers('DEFAULT')
629 629 elif util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
630 630 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
631 631 # pytype: disable=module-attr
632 632 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
633 633 # pytype: enable=module-attr
634 634
635 635 if requireclientcert:
636 636 sslcontext.verify_mode = ssl.CERT_REQUIRED
637 637 else:
638 638 sslcontext.verify_mode = ssl.CERT_NONE
639 639
640 640 if certfile or keyfile:
641 641 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
642 642
643 643 if cafile:
644 644 sslcontext.load_verify_locations(cafile=cafile)
645 645
646 646 return sslcontext.wrap_socket(sock, server_side=True)
647 647
648 648
649 649 class wildcarderror(Exception):
650 650 """Represents an error parsing wildcards in DNS name."""
651 651
652 652
653 653 def _dnsnamematch(dn, hostname, maxwildcards=1):
654 654 """Match DNS names according RFC 6125 section 6.4.3.
655 655
656 656 This code is effectively copied from CPython's ssl._dnsname_match.
657 657
658 658 Returns a bool indicating whether the expected hostname matches
659 659 the value in ``dn``.
660 660 """
661 661 pats = []
662 662 if not dn:
663 663 return False
664 664 dn = pycompat.bytesurl(dn)
665 665 hostname = pycompat.bytesurl(hostname)
666 666
667 667 pieces = dn.split(b'.')
668 668 leftmost = pieces[0]
669 669 remainder = pieces[1:]
670 670 wildcards = leftmost.count(b'*')
671 671 if wildcards > maxwildcards:
672 672 raise wildcarderror(
673 673 _(b'too many wildcards in certificate DNS name: %s') % dn
674 674 )
675 675
676 676 # speed up common case w/o wildcards
677 677 if not wildcards:
678 678 return dn.lower() == hostname.lower()
679 679
680 680 # RFC 6125, section 6.4.3, subitem 1.
681 681 # The client SHOULD NOT attempt to match a presented identifier in which
682 682 # the wildcard character comprises a label other than the left-most label.
683 683 if leftmost == b'*':
684 684 # When '*' is a fragment by itself, it matches a non-empty dotless
685 685 # fragment.
686 686 pats.append(b'[^.]+')
687 687 elif leftmost.startswith(b'xn--') or hostname.startswith(b'xn--'):
688 688 # RFC 6125, section 6.4.3, subitem 3.
689 689 # The client SHOULD NOT attempt to match a presented identifier
690 690 # where the wildcard character is embedded within an A-label or
691 691 # U-label of an internationalized domain name.
692 692 pats.append(stringutil.reescape(leftmost))
693 693 else:
694 694 # Otherwise, '*' matches any dotless string, e.g. www*
695 695 pats.append(stringutil.reescape(leftmost).replace(br'\*', b'[^.]*'))
696 696
697 697 # add the remaining fragments, ignore any wildcards
698 698 for frag in remainder:
699 699 pats.append(stringutil.reescape(frag))
700 700
701 701 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
702 702 return pat.match(hostname) is not None
703 703
704 704
705 705 def _verifycert(cert, hostname):
706 706 """Verify that cert (in socket.getpeercert() format) matches hostname.
707 707 CRLs is not handled.
708 708
709 709 Returns error message if any problems are found and None on success.
710 710 """
711 711 if not cert:
712 712 return _(b'no certificate received')
713 713
714 714 dnsnames = []
715 715 san = cert.get('subjectAltName', [])
716 716 for key, value in san:
717 717 if key == 'DNS':
718 718 try:
719 719 if _dnsnamematch(value, hostname):
720 720 return
721 721 except wildcarderror as e:
722 722 return stringutil.forcebytestr(e.args[0])
723 723
724 724 dnsnames.append(value)
725 725
726 726 if not dnsnames:
727 727 # The subject is only checked when there is no DNS in subjectAltName.
728 728 for sub in cert.get('subject', []):
729 729 for key, value in sub:
730 730 # According to RFC 2818 the most specific Common Name must
731 731 # be used.
732 732 if key == 'commonName':
733 733 # 'subject' entries are unicode.
734 734 try:
735 735 value = value.encode('ascii')
736 736 except UnicodeEncodeError:
737 737 return _(b'IDN in certificate not supported')
738 738
739 739 try:
740 740 if _dnsnamematch(value, hostname):
741 741 return
742 742 except wildcarderror as e:
743 743 return stringutil.forcebytestr(e.args[0])
744 744
745 745 dnsnames.append(value)
746 746
747 747 dnsnames = [pycompat.bytesurl(d) for d in dnsnames]
748 748 if len(dnsnames) > 1:
749 749 return _(b'certificate is for %s') % b', '.join(dnsnames)
750 750 elif len(dnsnames) == 1:
751 751 return _(b'certificate is for %s') % dnsnames[0]
752 752 else:
753 753 return _(b'no commonName or subjectAltName found in certificate')
754 754
755 755
756 756 def _plainapplepython():
757 757 """return true if this seems to be a pure Apple Python that
758 758 * is unfrozen and presumably has the whole mercurial module in the file
759 759 system
760 760 * presumably is an Apple Python that uses Apple OpenSSL which has patches
761 761 for using system certificate store CAs in addition to the provided
762 762 cacerts file
763 763 """
764 764 if (
765 765 not pycompat.isdarwin
766 766 or resourceutil.mainfrozen()
767 767 or not pycompat.sysexecutable
768 768 ):
769 769 return False
770 770 exe = os.path.realpath(pycompat.sysexecutable).lower()
771 771 return exe.startswith(b'/usr/bin/python') or exe.startswith(
772 772 b'/system/library/frameworks/python.framework/'
773 773 )
774 774
775 775
776 776 def _defaultcacerts(ui):
777 777 """return path to default CA certificates or None.
778 778
779 779 It is assumed this function is called when the returned certificates
780 780 file will actually be used to validate connections. Therefore this
781 781 function may print warnings or debug messages assuming this usage.
782 782
783 783 We don't print a message when the Python is able to load default
784 784 CA certs because this scenario is detected at socket connect time.
785 785 """
786 786 # The "certifi" Python package provides certificates. If it is installed
787 787 # and usable, assume the user intends it to be used and use it.
788 788 try:
789 789 import certifi
790 790
791 791 certs = certifi.where()
792 792 if os.path.exists(certs):
793 793 ui.debug(b'using ca certificates from certifi\n')
794 794 return pycompat.fsencode(certs)
795 795 except (ImportError, AttributeError):
796 796 pass
797 797
798 798 # Apple's OpenSSL has patches that allow a specially constructed certificate
799 799 # to load the system CA store. If we're running on Apple Python, use this
800 800 # trick.
801 801 if _plainapplepython():
802 802 dummycert = os.path.join(
803 803 os.path.dirname(pycompat.fsencode(__file__)), b'dummycert.pem'
804 804 )
805 805 if os.path.exists(dummycert):
806 806 return dummycert
807 807
808 808 return None
809 809
810 810
811 811 def validatesocket(sock):
812 812 """Validate a socket meets security requirements.
813 813
814 814 The passed socket must have been created with ``wrapsocket()``.
815 815 """
816 816 shost = sock._hgstate[b'hostname']
817 817 host = pycompat.bytesurl(shost)
818 818 ui = sock._hgstate[b'ui']
819 819 settings = sock._hgstate[b'settings']
820 820
821 821 try:
822 822 peercert = sock.getpeercert(True)
823 823 peercert2 = sock.getpeercert()
824 824 except AttributeError:
825 825 raise error.SecurityError(_(b'%s ssl connection error') % host)
826 826
827 827 if not peercert:
828 828 raise error.SecurityError(
829 829 _(b'%s certificate error: no certificate received') % host
830 830 )
831 831
832 832 if settings[b'disablecertverification']:
833 833 # We don't print the certificate fingerprint because it shouldn't
834 834 # be necessary: if the user requested certificate verification be
835 835 # disabled, they presumably already saw a message about the inability
836 836 # to verify the certificate and this message would have printed the
837 837 # fingerprint. So printing the fingerprint here adds little to no
838 838 # value.
839 839 ui.warn(
840 840 _(
841 841 b'warning: connection security to %s is disabled per current '
842 842 b'settings; communication is susceptible to eavesdropping '
843 843 b'and tampering\n'
844 844 )
845 845 % host
846 846 )
847 847 return
848 848
849 849 # If a certificate fingerprint is pinned, use it and only it to
850 850 # validate the remote cert.
851 851 peerfingerprints = {
852 852 b'sha1': hex(hashutil.sha1(peercert).digest()),
853 853 b'sha256': hex(hashlib.sha256(peercert).digest()),
854 854 b'sha512': hex(hashlib.sha512(peercert).digest()),
855 855 }
856 856
857 857 def fmtfingerprint(s):
858 858 return b':'.join([s[x : x + 2] for x in range(0, len(s), 2)])
859 859
860 860 nicefingerprint = b'sha256:%s' % fmtfingerprint(peerfingerprints[b'sha256'])
861 861
862 862 if settings[b'certfingerprints']:
863 863 for hash, fingerprint in settings[b'certfingerprints']:
864 864 if peerfingerprints[hash].lower() == fingerprint:
865 865 ui.debug(
866 866 b'%s certificate matched fingerprint %s:%s\n'
867 867 % (host, hash, fmtfingerprint(fingerprint))
868 868 )
869 869 if settings[b'legacyfingerprint']:
870 870 ui.warn(
871 871 _(
872 872 b'(SHA-1 fingerprint for %s found in legacy '
873 873 b'[hostfingerprints] section; '
874 874 b'if you trust this fingerprint, remove the old '
875 875 b'SHA-1 fingerprint from [hostfingerprints] and '
876 876 b'add the following entry to the new '
877 877 b'[hostsecurity] section: %s:fingerprints=%s)\n'
878 878 )
879 879 % (host, host, nicefingerprint)
880 880 )
881 881 return
882 882
883 883 # Pinned fingerprint didn't match. This is a fatal error.
884 884 if settings[b'legacyfingerprint']:
885 885 section = b'hostfingerprint'
886 886 nice = fmtfingerprint(peerfingerprints[b'sha1'])
887 887 else:
888 888 section = b'hostsecurity'
889 889 nice = b'%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
890 890 raise error.SecurityError(
891 891 _(b'certificate for %s has unexpected fingerprint %s')
892 892 % (host, nice),
893 893 hint=_(b'check %s configuration') % section,
894 894 )
895 895
896 896 # Security is enabled but no CAs are loaded. We can't establish trust
897 897 # for the cert so abort.
898 898 if not sock._hgstate[b'caloaded']:
899 899 raise error.SecurityError(
900 900 _(
901 901 b'unable to verify security of %s (no loaded CA certificates); '
902 902 b'refusing to connect'
903 903 )
904 904 % host,
905 905 hint=_(
906 906 b'see https://mercurial-scm.org/wiki/SecureConnections for '
907 907 b'how to configure Mercurial to avoid this error or set '
908 908 b'hostsecurity.%s:fingerprints=%s to trust this server'
909 909 )
910 910 % (host, nicefingerprint),
911 911 )
912 912
913 913 msg = _verifycert(peercert2, shost)
914 914 if msg:
915 915 raise error.SecurityError(
916 916 _(b'%s certificate error: %s') % (host, msg),
917 917 hint=_(
918 918 b'set hostsecurity.%s:certfingerprints=%s '
919 919 b'config setting or use --insecure to connect '
920 920 b'insecurely'
921 921 )
922 922 % (host, nicefingerprint),
923 923 )
General Comments 0
You need to be logged in to leave comments. Login now