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