comparison mercurial/sslutil.py @ 29559:7dec5e441bf7

sslutil: config option to specify TLS protocol version Currently, Mercurial will use TLS 1.0 or newer when connecting to remote servers, selecting the highest TLS version supported by both peers. On older Pythons, only TLS 1.0 is available. On newer Pythons, TLS 1.1 and 1.2 should be available. Security-minded people may want to not take any risks running TLS 1.0 (or even TLS 1.1). This patch gives those people a config option to explicitly control which TLS versions Mercurial should use. By providing this option, one can require newer TLS versions before they are formally deprecated by Mercurial/Python/OpenSSL/etc and lower their security exposure. This option also provides an easy mechanism to change protocol policies in Mercurial. If there is a 0-day and TLS 1.0 is completely broken, we can act quickly without changing much code. Because setting the minimum TLS protocol is something you'll likely want to do globally, this patch introduces a global config option under [hostsecurity] for that purpose. wrapserversocket() has been taught a hidden config option to define the explicit protocol to use. This is queried in this function and not passed as an argument because I don't want to expose this dangerous option as part of the Python API. There is a risk someone could footgun themselves. But the config option is a devel option, has a warning comment, and I doubt most people are using `hg serve` to run a production HTTPS server (I would have something not Mercurial/Python handle TLS). If this is problematic, we can go back to using a custom extension in tests to coerce the server into bad behavior.
author Gregory Szorc <gregory.szorc@gmail.com>
date Thu, 14 Jul 2016 20:47:22 -0700
parents a935cd7d51a6
children 303e9300772a
comparison
equal deleted inserted replaced
29558:a935cd7d51a6 29559:7dec5e441bf7
27 # 27 #
28 # Depending on the version of Python being used, SSL/TLS support is either 28 # Depending on the version of Python being used, SSL/TLS support is either
29 # modern/secure or legacy/insecure. Many operations in this module have 29 # modern/secure or legacy/insecure. Many operations in this module have
30 # separate code paths depending on support in Python. 30 # separate code paths depending on support in Python.
31 31
32 configprotocols = set([
33 'tls1.0',
34 'tls1.1',
35 'tls1.2',
36 ])
37
32 hassni = getattr(ssl, 'HAS_SNI', False) 38 hassni = getattr(ssl, 'HAS_SNI', False)
33
34 try:
35 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
36 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
37 except AttributeError:
38 OP_NO_SSLv2 = 0x1000000
39 OP_NO_SSLv3 = 0x2000000
40 39
41 try: 40 try:
42 # ssl.SSLContext was added in 2.7.9 and presence indicates modern 41 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
43 # SSL/TLS features are available. 42 # SSL/TLS features are available.
44 SSLContext = ssl.SSLContext 43 SSLContext = ssl.SSLContext
134 'ctxoptions': None, 133 'ctxoptions': None,
135 } 134 }
136 135
137 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol 136 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
138 # that both ends support, including TLS protocols. On legacy stacks, 137 # that both ends support, including TLS protocols. On legacy stacks,
139 # the highest it likely goes in TLS 1.0. On modern stacks, it can 138 # the highest it likely goes is TLS 1.0. On modern stacks, it can
140 # support TLS 1.2. 139 # support TLS 1.2.
141 # 140 #
142 # The PROTOCOL_TLSv* constants select a specific TLS version 141 # The PROTOCOL_TLSv* constants select a specific TLS version
143 # only (as opposed to multiple versions). So the method for 142 # only (as opposed to multiple versions). So the method for
144 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and 143 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
145 # disable protocols via SSLContext.options and OP_NO_* constants. 144 # disable protocols via SSLContext.options and OP_NO_* constants.
146 # However, SSLContext.options doesn't work unless we have the 145 # However, SSLContext.options doesn't work unless we have the
147 # full/real SSLContext available to us. 146 # full/real SSLContext available to us.
148 if modernssl: 147
149 s['protocol'] = ssl.PROTOCOL_SSLv23 148 # Allow minimum TLS protocol to be specified in the config.
150 else: 149 def validateprotocol(protocol, key):
151 s['protocol'] = ssl.PROTOCOL_TLSv1 150 if protocol not in configprotocols:
152 151 raise error.Abort(
153 # SSLv2 and SSLv3 are broken. We ban them outright. 152 _('unsupported protocol from hostsecurity.%s: %s') %
154 # WARNING: ctxoptions doesn't have an effect unless the modern ssl module 153 (key, protocol),
155 # is available. Be careful when adding flags! 154 hint=_('valid protocols: %s') %
156 s['ctxoptions'] = OP_NO_SSLv2 | OP_NO_SSLv3 155 ' '.join(sorted(configprotocols)))
157 156
158 # Prevent CRIME. 157 key = 'minimumprotocol'
159 # There is no guarantee this attribute is defined on the module. 158 # Default to TLS 1.0+ as that is what browsers are currently doing.
160 s['ctxoptions'] |= getattr(ssl, 'OP_NO_COMPRESSION', 0) 159 protocol = ui.config('hostsecurity', key, 'tls1.0')
160 validateprotocol(protocol, key)
161
162 key = '%s:minimumprotocol' % hostname
163 protocol = ui.config('hostsecurity', key, protocol)
164 validateprotocol(protocol, key)
165
166 s['protocol'], s['ctxoptions'] = protocolsettings(protocol)
161 167
162 # Look for fingerprints in [hostsecurity] section. Value is a list 168 # Look for fingerprints in [hostsecurity] section. Value is a list
163 # of <alg>:<fingerprint> strings. 169 # of <alg>:<fingerprint> strings.
164 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname, 170 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
165 []) 171 [])
248 assert s['ctxoptions'] is not None 254 assert s['ctxoptions'] is not None
249 assert s['verifymode'] is not None 255 assert s['verifymode'] is not None
250 256
251 return s 257 return s
252 258
259 def protocolsettings(protocol):
260 """Resolve the protocol and context options for a config value."""
261 if protocol not in configprotocols:
262 raise ValueError('protocol value not supported: %s' % protocol)
263
264 # Legacy ssl module only supports up to TLS 1.0. Ideally we'd use
265 # PROTOCOL_SSLv23 and options to disable SSLv2 and SSLv3. However,
266 # SSLContext.options doesn't work in our implementation since we use
267 # a fake SSLContext on these Python versions.
268 if not modernssl:
269 if protocol != 'tls1.0':
270 raise error.Abort(_('current Python does not support protocol '
271 'setting %s') % protocol,
272 hint=_('upgrade Python or disable setting since '
273 'only TLS 1.0 is supported'))
274
275 return ssl.PROTOCOL_TLSv1, 0
276
277 # WARNING: returned options don't work unless the modern ssl module
278 # is available. Be careful when adding options here.
279
280 # SSLv2 and SSLv3 are broken. We ban them outright.
281 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
282
283 if protocol == 'tls1.0':
284 # Defaults above are to use TLS 1.0+
285 pass
286 elif protocol == 'tls1.1':
287 options |= ssl.OP_NO_TLSv1
288 elif protocol == 'tls1.2':
289 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
290 else:
291 raise error.Abort(_('this should not happen'))
292
293 # Prevent CRIME.
294 # There is no guarantee this attribute is defined on the module.
295 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
296
297 return ssl.PROTOCOL_SSLv23, options
298
253 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None): 299 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
254 """Add SSL/TLS to a socket. 300 """Add SSL/TLS to a socket.
255 301
256 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane 302 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
257 choices based on what security options are available. 303 choices based on what security options are available.
304 else: 350 else:
305 caloaded = False 351 caloaded = False
306 352
307 try: 353 try:
308 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname) 354 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
309 except ssl.SSLError: 355 except ssl.SSLError as e:
310 # If we're doing certificate verification and no CA certs are loaded, 356 # If we're doing certificate verification and no CA certs are loaded,
311 # that is almost certainly the reason why verification failed. Provide 357 # that is almost certainly the reason why verification failed. Provide
312 # a hint to the user. 358 # a hint to the user.
313 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can 359 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
314 # only show this warning if modern ssl is available. 360 # only show this warning if modern ssl is available.
316 modernssl and not sslcontext.get_ca_certs()): 362 modernssl and not sslcontext.get_ca_certs()):
317 ui.warn(_('(an attempt was made to load CA certificates but none ' 363 ui.warn(_('(an attempt was made to load CA certificates but none '
318 'were loaded; see ' 364 'were loaded; see '
319 'https://mercurial-scm.org/wiki/SecureConnections for ' 365 'https://mercurial-scm.org/wiki/SecureConnections for '
320 'how to configure Mercurial to avoid this error)\n')) 366 'how to configure Mercurial to avoid this error)\n'))
367 # Try to print more helpful error messages for known failures.
368 if util.safehasattr(e, 'reason'):
369 if e.reason == 'UNSUPPORTED_PROTOCOL':
370 ui.warn(_('(could not negotiate a common protocol; see '
371 'https://mercurial-scm.org/wiki/SecureConnections '
372 'for how to configure Mercurial to avoid this '
373 'error)\n'))
321 raise 374 raise
322 375
323 # check if wrap_socket failed silently because socket had been 376 # check if wrap_socket failed silently because socket had been
324 # closed 377 # closed
325 # - see http://bugs.python.org/issue13721 378 # - see http://bugs.python.org/issue13721
347 400
348 ``requireclientcert`` specifies whether to require client certificates. 401 ``requireclientcert`` specifies whether to require client certificates.
349 402
350 Typically ``cafile`` is only defined if ``requireclientcert`` is true. 403 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
351 """ 404 """
405 protocol, options = protocolsettings('tls1.0')
406
407 # This config option is intended for use in tests only. It is a giant
408 # footgun to kill security. Don't define it.
409 exactprotocol = ui.config('devel', 'serverexactprotocol')
410 if exactprotocol == 'tls1.0':
411 protocol = ssl.PROTOCOL_TLSv1
412 elif exactprotocol == 'tls1.1':
413 protocol = ssl.PROTOCOL_TLSv1_1
414 elif exactprotocol == 'tls1.2':
415 protocol = ssl.PROTOCOL_TLSv1_2
416 elif exactprotocol:
417 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
418 exactprotocol)
419
352 if modernssl: 420 if modernssl:
353 # We /could/ use create_default_context() here since it doesn't load 421 # We /could/ use create_default_context() here since it doesn't load
354 # CAs when configured for client auth. 422 # CAs when configured for client auth. However, it is hard-coded to
355 sslcontext = SSLContext(ssl.PROTOCOL_SSLv23) 423 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
356 # SSLv2 and SSLv3 are broken. Ban them outright. 424 sslcontext = SSLContext(protocol)
357 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3 425 sslcontext.options |= options
358 # Prevent CRIME 426
359 sslcontext.options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
360 # Improve forward secrecy. 427 # Improve forward secrecy.
361 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0) 428 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
362 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0) 429 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
363 430
364 # Use the list of more secure ciphers if found in the ssl module. 431 # Use the list of more secure ciphers if found in the ssl module.