CherryPy missing documentation: Enabling client certificate verification

CherryPy is a convenient pure-Python web server that provides a good host for WSGI applications. It’s seemingly one of the few Python WSGI web servers which allows for SSL without depending on a separate web server, which is quite attractive for cross-platform apps. (I think Tornado is another notable example which does this, but Tornado is not the best platform for WSGI, despite technically supporting it.)

Getting basic SSL up and running is not particularly hard; it is easy enough to google the CherryPy docs and find this. However, deeper configuration is not exactly clear. One such option is client certificate verification. Indeed, I thought that due to the presence of this bug that it was not presently possible to enable this out of the box in CherryPy.

Thankfully I was wrong; indeed it is possible, it just does not appear to be documented. So, here’s the missing documentation.

This post assumes CherryPy 12.0.0 and cheroot 5.9.1.

First, let’s start with this example, copied nearly verbatim from the CherryPy docs:

import cherrypy


class HelloWorld(object):
    @cherrypy.expose
    def index(self):
        return "Hello world!\n"


if __name__ == '__main__':
   cherrypy.quickstart(HelloWorld())

This serves a single response via HTTP. First, how do we get this to speak HTTPS instead?

This is pretty easy, and is also in the docs. Before the class definition, add something like this:

MY_KEY = 'my.key'
MY_CERT = 'my.crt'
MY_CA_CHAIN = MY_CERT

cherrypy.server.socket_port = 1234
cherrypy.server.ssl_private_key = MY_KEY
cherrypy.server.ssl_certificate = MY_CERT
cherrypy.server.ssl_certificate_chain = MY_CA_CHAIN

Note that this assumes you’ve created an OpenSSL key and cert per the OpenSSL cookbook or the CherryPy documentation.

Note that I’ve also included a CA cert chain, although in this case I’m simply re-using my self-signed certificate as the CA chain. For this example I’m going to use the same key for both client and server. Note that I’m not recommending this as normal procedure; this is just to show how you can get this to work! (I’m not a security expert nor do I plan to pretend to be one here!)

Anyway, the above example will configure the server to run via HTTPS on port 1234. For production, you’d likely want to use the default HTTPS port of 443 instead.

You can run the above, and it’ll work! Except… client restrictions aren’t in effect yet.


vultaire@host:~/code/ssl_test$ curl --insecure --key my.key --cert my.crt https://localhost:1234
Hello world!
vultaire@host:~/code/ssl_test$ curl --insecure --key alt.key --cert alt.crt https://localhost:1234
Hello world!
vultaire@host:~/code/ssl_test$ curl --insecure https://localhost:1234
Hello world!

So, how do we get the client verification turned on here?

Now we need to go into undocumented stuff. Assuming a recent enough version of Python and use of the builtin ssl module rather than pyOpenSSL, you can set cherrypy.server.ssl_context to an ssl.SSLContext object. (You may be able to do similar with pyOpenSSL, but I have not tested this.)

It is suggested to use ssl.create_default_context() to create SSLContexts, so we’ll do that. But we’ll then tweak the context as we like. We’ll set verify_mode to CERT_REQUIRED, which is the key missing bit. We’ll also need to load our key, cert, and CA chain, as they do not automatically get loaded. Finally, as I want things to be as secure as possible in my case, I explicitly want to set the protocol to the latest TLS version I have support for.

The overall script ends up looking like this:

import ssl
import cherrypy

MY_KEY = 'my.key'
MY_CERT = 'my.crt'
MY_CA_CHAIN = MY_CERT

cherrypy.server.socket_port = 1234
cherrypy.server.ssl_private_key = MY_KEY
cherrypy.server.ssl_certificate = MY_CERT
cherrypy.server.ssl_certificate_chain = MY_CA_CHAIN  # This line seems unnecessary now, but leaving it in
cherrypy.server.ssl_context = ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.protocol = ssl.PROTOCOL_TLSv1_2
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.load_cert_chain(cherrypy.server.ssl_certificate, cherrypy.server.ssl_private_key)
ssl_context.load_verify_locations(cafile=MY_CA_CHAIN)


class HelloWorld(object):
    @cherrypy.expose
    def index(self):
        return "Hello world!\n"


if __name__ == '__main__':
   cherrypy.quickstart(HelloWorld())

And if you repeat the curl tests now, you’ll see that only the request using the correct key and cert gets a response; the rest will now be blocked.


vultaire@host:~/code/ssl_test$ curl --insecure --key my.key --cert my.crt https://localhost:1234
Hello world!
vultaire@host:~/code/ssl_test$ curl --insecure --key alt.key --cert alt.crt https://localhost:1234
curl: (35) error:14094418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca
vultaire@host:~/code/ssl_test$ curl --insecure https://localhost:1234
curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure

So, there you go! Client certificate verification while using CherryPy. Of course, from here it’s up to you to properly create and configure your keys, certs, and CA chains appropriately for your use case.

Hope this helps someone out!

Leave a Reply

Your email address will not be published. Required fields are marked *