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