SSL
===
As a part of discussion of design decisions made in the test suite [6]_, Joseph Tate, another
CherryPy contributor expressed opinion that SSL support in CherryPy should be deprecated.
Basically the message is about how can we guarantee security of the implementation if we can't
even guarantee its functionality.
Later in discussion with Joseph when I stood on fact that CherryPy's SSL adapters are a wrapper on
wrapper, Python ``ssl`` or pyOpenSSL on OpenSSL, and we don't need a professional security expert
in the community or special maintenance, he relaxed his opinion.
Distros have patched OpenSSL to remove insecure stuff, so maybe this is not as big a concern
as I'm making it out to be, but the lack of an expert on this code is hindering its viability.
Also I want to inform Joseph, that his knowledge of CherryPy support for intermediate certificates
isn't up to date. ``cherrypy.server.ssl_certificate_chain`` is documented [14]_ and was there at
least since 2011. I've verified it works in default branch.
Anyway, I took Joseph's questions seriously and I'm sure **we should not in any way give our users
false sense of security**. Security here is in theoretical sense as the state of art. I'm neither
about zero-day breaches in algorithms and implementations, nor about fundamental problem of CA
trust, no-such-agencies and generally post-Snowden world we live in.
On the other hand I believe that having pure Python functional (possibly insecure in theoretical
sense) HTTPS server is beneficial for many practical use cases. There're various development tools
that are HTTPS-only unless you fight them. There're environments where you have Python but can't
install other software. There're use cases then any SSL, as an obfuscation, is fairly enough
secure.
CherryPy is described and known as a complete web server and such reduction would be a big
loss. And as I will show you below, although there're several issues it is possible to
workaround them and configure CherryPy to serve an application over HTTPS securely even today.
Experiment
----------
Recently there was a CherryPy StackOverflow question right about the topic [15]_. The guy asked
*how to block SSL protocols in favour of TLS*. It led me to coming to the following
quick-and-dirty solution.
.. sourcecode:: python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import ssl
import cherrypy
from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter
from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
from cherrypy import wsgiserver
if sys.version_info < (3, 0):
from cherrypy.wsgiserver.wsgiserver2 import ssl_adapters
else:
from cherrypy.wsgiserver.wsgiserver3 import ssl_adapters
try:
from OpenSSL import SSL
except ImportError:
pass
ciphers = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
'!eNULL:!MD5:!DSS:!RC4:!SSLv2'
)
bundle = os.path.join(os.path.dirname(cherrypy.__file__), 'test', 'test.pem')
config = {
'global' : {
'server.socket_host' : '127.0.0.1',
'server.socket_port' : 8443,
'server.thread_pool' : 8,
'server.ssl_module' : 'custom-pyopenssl',
'server.ssl_certificate' : bundle,
'server.ssl_private_key' : bundle,
}
}
class BuiltinSsl(BuiltinSSLAdapter):
'''Vulnerable, on py2 < 2.7.9, py3 < 3.3:
* POODLE (SSLv3), adding ``!SSLv3`` to cipher list makes it very incompatible
* can't disable TLS compression (CRIME)
* supports Secure Client-Initiated Renegotiation (DOS)
* no Forward Secrecy
Also session caching doesn't work. Some tweaks are posslbe, but don't really
change much. For example, it's possible to use ssl.PROTOCOL_TLSv1 instead of
ssl.PROTOCOL_SSLv23 with little worse compatiblity.
'''
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
try:
s = ssl.wrap_socket(
sock,
ciphers = ciphers, # the override is for this line
do_handshake_on_connect = True,
server_side = True,
certfile = self.certificate,
keyfile = self.private_key,
ssl_version = ssl.PROTOCOL_SSLv23
)
except ssl.SSLError:
e = sys.exc_info()[1]
if e.errno == ssl.SSL_ERROR_EOF:
# This is almost certainly due to the cherrypy engine
# 'pinging' the socket to assert it's connectable;
# the 'ping' isn't SSL.
return None, {}
elif e.errno == ssl.SSL_ERROR_SSL:
if e.args[1].endswith('http request'):
# The client is speaking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError
elif e.args[1].endswith('unknown protocol'):
# The client is speaking some non-HTTP protocol.
# Drop the conn.
return None, {}
raise
return s, self.get_environ(s)
ssl_adapters['custom-ssl'] = BuiltinSsl
class Pyopenssl(pyOpenSSLAdapter):
'''Mostly fine, except:
* Secure Client-Initiated Renegotiation
* no Forward Secrecy, SSL.OP_SINGLE_DH_USE could have helped but it didn't
'''
def get_context(self):
"""Return an SSL.Context from self attributes."""
c = SSL.Context(SSL.SSLv23_METHOD)
# override:
c.set_options(SSL.OP_NO_COMPRESSION | SSL.OP_SINGLE_DH_USE | SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
c.set_cipher_list(ciphers)
c.use_privatekey_file(self.private_key)
if self.certificate_chain:
c.load_verify_locations(self.certificate_chain)
c.use_certificate_file(self.certificate)
return c
ssl_adapters['custom-pyopenssl'] = Pyopenssl
class App:
@cherrypy.expose
def index(self):
return '<em>Is this secure?</em>'
if __name__ == '__main__':
cherrypy.quickstart(App(), '/', config)
After I regenerated our test certificate to:
* use longer than 1024-bit key
* not use MD5 and SHA1 (also deprecated [26]_) for signature
* use valid domain name for *common name*
Using the following versions, we have **A-** for the customised pyOpenSSL adapter
on Qualys SSL Server Test [34]_:
* Debian Wheezy (stable)
* Python 2.7.3-4+deb7u1, ``ssl.OPENSSL_VERSION == 'OpenSSL 1.0.1e 11 Feb 2013'``
* OpenSSL 1.0.1e-2+deb7u16
* pyOpenSSL 0.14
* CherryPy from default branch (pre 3.7)
There's no error, but the warnings:
* Secure Client-Initiated Renegotiation -- Supported, DoS DANGER [16]_
* Downgrade attack prevention -- No, TLS_FALLBACK_SCSV not supported [17]_
* Forward Secrecy -- No, WEAK [18]_
Here's what Wikipedia says [25]_ about *forward secrecy*, to inform you that it is not
something critical or required for immediate operation.
In cryptography, forward secrecy is a property of key-agreement protocols ensuring that a
session key derived from a set of long-term keys cannot be compromised if one of the
long-term keys is compromised in the future.
And here's what can be squeezed out of built-in ``ssl``, which is grievous.
Here goes the what-is-wrong list:
* POODLE (SSLv3) -- Vulnerable, INSECURE [19]_
* TLS compression -- Yes, INSECURE [20]_
There's also half-implemented *session resumption* -- *No (IDs assigned but not accepted)*. And
the same three warnings as for PyOpenSSL apply. ``ssl_builtin`` doesn't handle underlying ``ssl``
module exceptions correctly, as the log was flooded by exceptions originating mostly from
``do_handshake()``. PyOpenSSL's log was the way more quiet with just a couple of page accesses.
::
[27/Mar/2015:14:07:49] ENGINE Error in HTTPServer.tick
Traceback (most recent call last):
File "venv/local/lib/python2.7/site-packages/cherrypy/wsgiserver/wsgiserver2.py", line 1968, in start
self.tick()
File "venv/local/lib/python2.7/site-packages/cherrypy/wsgiserver/wsgiserver2.py", line 2035, in tick
s, ssl_env = self.ssl_adapter.wrap(s)
File "./test.py", line 53, in wrap
ssl_version = ssl.PROTOCOL_SSLv23
File "/usr/lib/python2.7/ssl.py", line 381, in wrap_socket
ciphers=ciphers)
File "/usr/lib/python2.7/ssl.py", line 143, in __init__
self.do_handshake()
File "/usr/lib/python2.7/ssl.py", line 305, in do_handshake
self._sslobj.do_handshake()
SSLError: [Errno 1] _ssl.c:504: error:1408A0C1:SSL routines:SSL3_GET_CLIENT_HELLO:no shared cipher
SSLError: [Errno 1] _ssl.c:504: error:14076102:SSL routines:SSL23_GET_CLIENT_HELLO:unsupported protocol
SSLError: [Errno 1] _ssl.c:504: error:1408F119:SSL routines:SSL3_GET_RECORD:decryption failed or bad record mac
SSLError: [Errno 1] _ssl.c:504: error:14094085:SSL routines:SSL3_READ_BYTES:ccs received early
But nothing is really that bad even with ``ssl_builtin``. Let's relax stability policy, make sure
you understand the consequences if you're going to do it on a production server, and add
Debain Jessie's (testing) source to ``/etc/apt/sources.list`` of the server (Debian Wheezy) and
see what next Debain has in its briefcase:
* Python 2.7.9-1, ``ssl.OPENSSL_VERSION == 'OpenSSL 1.0.1k 8 Jan 2015'``
* Python 3.4.2-2, ``ssl.OPENSSL_VERSION == 'OpenSSL 1.0.1k 8 Jan 2015'``
* OpenSSL 1.0.1k-3
* pyOpenSSL 0.15.1 (installed via ``pip``, probably has been updated since)
For you information, related excerpt from Python 2.7.9 release change log [21]_. It was in fact
a huge security update, not very well represented by the change of a patch version.
* The entirety of Python 3.4's ssl module has been backported for Python 2.7.9. See
PEP 466 [22]_ for justification.
* HTTPS certificate validation using the system's certificate store is now enabled by
default. See PEP 476 [23]_ for details.
* SSLv3 has been disabled by default in httplib and its reverse dependencies due to the
POODLE attack.
All errors have gone, and in spite of the warning it supports *forward secrecy* for most
clients. Here are just couple of warnings left:
* Secure Client-Initiated Renegotiation -- Supported, DoS DANGER [16]_
* Session resumption (caching) -- No (IDs assigned but not accepted)
The result is pretty good. If you run it on plain ``ssl_builtin``, is has **B**, because RC4
ciphers [24]_ aren't excluded by default. Python 3.4.2 test on the customised ``ssl_builtin``
is obviously the same **A-** because both underlying libraries are the same. Customised
``ssl_pyopenssl`` on Python 2.7.9 gives same **A-** -- again it has functional *session
resumption* but doesn't have *forward secrecy* for any client.
Problem
-------
After the experiment has taken place, it is time to generalise the results. Here's what is
wrong with CherryPy's SSL support now:
#. giving false sense of security
#. not flexible configuration: protocol, ciphers, options
#. not up to date with builit-in ``ssl`` (no SSL Context [27]_ support)
#. no support for pyOpenSSL for *py3*
#. no security assessment
Plan
----
I think that the problem is solvable with a reasonable effort. What has been said above may
already have convinced you that no special cryptography knowledge is required. There's a scalar
letter grade for A to F and several design decisions aimed at flexibility and compatibility.
Outright state of affairs
~~~~~~~~~~~~~~~~~~~~~~~~~
Now we have the following note in *SSL support* section [14]_ in *Deploy* documentation page.
You may want to test your server for SSL using the services from Qualys, Inc.
To me after doing this research it doesn't even look like a half-true thing expressed modestly.
It looks we lie to users, giving them false sense of security. It may look like we're so sure
that it's okay that we let them do a voluntary task of checking it once again, just in case.
But in fact it is to deploy their *py2* application and see it's vulnerable (I assume
Python 2.7.9 isn't going to updated to soon for various reasons). This is absolutely
redundant and we can tell that Python < 2.7.9 is a insecure platform right away. There should be
a warning in documentation, like "Please update to Python 2.7.9, install pyOpenSSL or start off
with Python 3.4+. Otherwise it will only obfuscate your channel".
Python < 2.7.9 should be considered an insecure platform with appropriate Python warning, see
below.
Configuration
~~~~~~~~~~~~~
``cherrypy.server`` in addition to ``ssl_certificate``, ``ssl_certificate_chain``,
``ssl_private_key`` should support:
* ``ssl_cipher_list`` available for all Python versions and both adapters. Mostly needed for
Python < 2.7.9.
* ``ssl_context`` available for Python 2.7.9+ and 3.3+. pyOpenSSL is almost there. SSL
Context [27]_ allows user to set: protocol, ciphers, options.
SSL Context [27]_ when not provided from user should be created with
``ssl.create_default_context`` which takes care of security defaults. It is available for
Python 2.7.9+ and 3.4+. For Python 3.3 context should be configured at CherryPy side
(the function is two dozen of lines [29]_).
``ssl_adapters`` should be available at ``cherrypy.wsgiserver`` for user be able to set
custom adapter (see the code above). Already implemented in my fork.
Built-in SSL adapter update
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The adapter should make use of PEP 466 [22]_. This way, inside it may be two adapters. One for
Python 2.7.9+ with SSL Context [27]_ support and one for Python < 2.7.9. The latter should be
explicitly documented as vulnerable and emit warning when used. Documentation and warning text
can be taken from ``urllib3``'s ``InsecurePlatformWarning`` [30]_.
The adapter should handle SSL exceptions according to the protocol, not flooding the log with
them.
Python 3 support for pyOpenSSL adapter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Purpose of pyOpenSSL adapter should be rethought as Python < 2.6 is no longer supported
and Python 2.6+ has ``ssl`` module built in. Thus the purpose is not to provide a fallback.
The purpose is to provide more flexibility for users to manage their own security. Python
binary that comes from a disto's repository have their own version of OpenSSL. When an OpenSSL
breach is discovered it is reasonable to expect patched OpenSSL package to be shipped first.
This gives an user ability to rebuild pyOpenSSL with latest OpenSSL as soon as possible in matter
of doing:
.. sourcecode:: bash
pip install -U pyOpenSSL
Embracing this purpose Python 3 support should be provided. Currently
``pyOpenSSLAdapter.makefile`` uses ``wsgiserver.CP_fileobject`` directly and uses
``wsgiserver.ssl_pyopenssl.SSL_fileobject`` which inherits from ``wsgiserver.CP_fileobject``.
``wsgiserver.CP_fileobject`` is available only in ``wsgiserver.wsgiserver2``. Therefore
``pyOpenSSLAdapter.makefile`` should use ``wsgiserver.CP_makefile`` in *py3* just like
``BuiltinSSLAdapter`` does. Here an advice or involvement of original authors is likely to
be needed because it's unclear in what circumstances ``pyOpenSSLAdapter.makefile`` should return
non-SSL socket.
Automated security assessment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Debian package of OpenSSL is described as:
[When] you need it to perform certain cryptographic actions like...
SSL/TLS client and server tests.
There are also things like sslyze [31]_, which is described as *fast and full-featured SSL
scanner*. I think at some extent this approach will give automated security test, but it's an
rough road. Basically, the quality of results will depend on our proficiency and maintenance of
the chosen tool. Much easier way is to you a service.
Luckily, in January Qualys announced SSL Labs API beta [32]_. At the dedicated page [33]_
there're links to official command like client and protocol manual. It makes sense to expect
the same behaviour from the API as the normal tester [34]_ has. If so it won't work on bare IP
address and on non 443 port.
To externalise this idea we have some prerequisite. If there's real CherryPy instance running
behind Nginx on cherrypy.org at WebFaction, then do we have access to it? Can we run several additional instances there? if not, can we run them somewhere else?
If think of instances for the following environments:
in distos' repositories for quite some time
Then we need to multiplex them to outer port 443 because likely it'll be running on single
server. Because Nginx doesn't seem to be able passthrough upstream SSL connections [35]_
we need to employ something else, maybe HAProxy [36]_ [37]_.
What's left to do thereafter is to implement SSL Labs API, run in some schedule and we're done.
It is able to give us detailed and actual reports on every environment that can be exposed to
users to make their decisions.
Questions
---------
Sylvain, can you update the documentation to explicitly express the state of our SSL support? I
think it is needed as soon as possible.
____