Search code examples
python-3.xsslamazon-ec2opensslcherrypy

CherryPy server unable to perform proper SSL handshake


I have a CherryPy server listening on port 443 and using an SSL certificate provided by ComodoSSL. However, trying to connect through Chrome gives me "ERR_SSL_PROTOCOL_ERROR".

Note that I am using Python3.7 and CherryPy 18.8.0, and have tried both builtin and pyopenssl. Switching between the two didn't seem to change anything.

Here's how my conf file looks:

[global]
server.socket_host = '::'
server.socket_port = 443
server.thread_pool = 10
server.protocol_version = 'HTTP/1.1'
cherrypy.server.ssl_module = 'builtin'
#cherrypy.server.ssl_module = 'pyopenssl'
cherrypy.server.ssl_certificate = '/etc/pki/tls/certs/ca-bundle.crt'
cherrypy.server.ssl_certificate_chain = '/etc/pki/tls/certs/<ip_address>.ca-bundle', # keeping or removing this also doesn't seem to change anything
cherrypy.server.ssl_private_key = '/etc/pki/tls/private/custom.key',

Looking into it, I decided to verify that the server was indeed listening for https connections on port 443, and indeed it is, according to this command:

sudo lsof -i tcp:443

and its output:

python3   18078 root    4u  IPv4 1608562      0t0  TCP *:https (LISTEN)

Next, I tried to debug using openssl:

sudo openssl s_client -connect localhost:443 -debug

and got the following output:

CONNECTED(00000003)
[...bytes written to localhost...]
read from 0x1e8d000 [0x1f4f6f0] (7 bytes => 7 (0x7))
0000 - 48 54 54 50 2f 31 2e                              HTTP/1.
140621914351520:error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol:s23_clnt.c:794:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 289 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: 
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1687892227
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

From this, it became clear that the CherryPy server was not performing its side of the SSL handshake properly, but still no idea why.

So, I tried comparing the Apache server to the CherryPy server and found one notable difference running this command:

sudo lsof -i tcp:443

with my Apache server running yields an output that only specifies IPV6 for the TYPE, while as seen above with my CherryPy server running, the output only specifies IPV4 for the TYPE. So, I tried modifying the conf file's server.socket_host parameter to '::', which changed the TYPE to IPV6, but I still got the same output from the openssl command.

Last thing I tried was running curl from within the EC2 instance like so:

sudo curl -v https://localhost

which gave me this output:

*   Trying 127.0.0.1:443...
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN: offers h2,http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* OpenSSL/1.0.2k-fips: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol
* Closing connection 0
curl: (35) OpenSSL/1.0.2k-fips: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol

and then from outside the EC2 instance like so:

curl https://<ip_address>

which gave me this output:

curl: (35) LibreSSL/3.3.6: error:1404B42E:SSL routines:ST_CONNECT:tlsv1 alert protocol version

Interestingly, running

curl <ip_address>:443

gave me the expected output from the CherryPy server, whether calling it from within the EC2 instance or outside of it.

I'm not sure what else to try, so any help would be appreciated.

Edit: I just created a dummy server without a separate config file (using cherrypy.config.update()), and I was able to establish a secure connection to the server through Chrome. So, then I tried moving the conf to my main server, and that also worked. I'm wondering if there is a bug in CherryPy here. Anyway I can move on from this issue now.


Solution

  • Looking at

    read from 0x1e8d000 [0x1f4f6f0] (7 bytes => 7 (0x7))
    0000 - 48 54 54 50 2f 31 2e                              HTTP/1.
    

    it becomes obvious that CherryPy talks plain HTTP over that port, meaning there's some sort of a misconfiguration. This is visible from that HTTP/1. at the beginning — this is how a status line of a normal HTTP response looks. I bet if you were to run curl -v http://localhost (not https!), you'd see your content. Note that a web server can't speak two protocols on the same port — it's either HTTP or HTTPS.

    So why does CherryPy not speak TLS? This is because your config entries starting with cherrypy. are ignored, as CherryPy didn't recognize them. This prefix is not needed. Once you remove it, CherryPy will apply the configuration to the server object, which in turn will trigger using TLS on that port.

    A config like

    # config_with_tls_pyopenssl.conf 
    [global]
    server.socket_host: '::'
    server.socket_port: 443
    server.thread_pool: 10
    server.protocol_version: 'HTTP/1.1'
    server.ssl_module: 'builtin'
    server.ssl_certificate: 'certificate.pem'
    server.ssl_certificate_chain: 'certificate-chain.pem'
    server.ssl_private_key: 'certificate.key'
    

    should work if you load it via cherrypy.config.update('config_with_tls_pyopenssl.conf') or similar, provided you pass the right files to the right settings.

    I've made a small example for verifying TLS setup of a CherryPy app here: https://gist.github.com/webknjaz/56cfb9f28a05017ea465982328b71d10.