Mercurial > public > mercurial-scm > hg
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. |