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