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