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