How can I block SSL protocols in PyOpenSSL
in favour of TLS
? I'm using CentOS 7
and have these versions:
pyOpenSSL-0.13.1-3.el7.x86_64
openssl-1.0.1e-34.el7_0.7.x86_64
In my config file (this if for a CherryPy app) I have:
'server.ssl_module': 'pyopenssl',
This is really good question for CherryPy today. This month we started discussing SSL issues and overall maintainability of CherryPy's wrappers over py2.6+ ssl
and pyOpenSSL in CherryPy user group. I'm planning a topic about SSL issues there, so you can subscribe for the group to get more details later.
For now, here's what is possible. I had Debian Wheezy, Python 2.7.3-4+deb7u1, OpenSSL 1.0.1e-2+deb7u16. I've installed CherryPy from the repo (3.6 has broken SSL), and pyOpenSSL 0.14. I tried to override both CherryPy SSL adapters to gain some points in Qualys SSL labs test. It is very helpful and I strongly suggest you to test your deployment with it (whatever is your frontend, CherryPy or not).
As a result, ssl
-based adapter still has vulnerabilities which I don't see the way to workaround in py2 < 2.7.9 (massive SSL update) and py3 < 3.3. Because CherryPy ssl
adapter was written long before these changes, it needs a rewrite to support both old and new ways (mostly SSL Contexts). On the other hand with subclassed pyOpenSSL adapted it's mostly fine, except for:
- Enabled Secure Client-Initiated Renegotiation. It may be OpenSSL-dependent.
- no Forward Secrecy,
SSL.OP_SINGLE_DH_USE
could have helped but it didn't. May also depend on version of OpenSSL.
Here's the code.
#!/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)
Update
Here's the article and discussion where future of CherryPy's SSL support should be decided.
There are two ways to do it I am aware. One is a configuratio options, and the other is a runtime option.
Configuration Option
The configuration option is used when building OpenSSL. Its great for all applications because it applies your administrative policy and addresses applications which are not mindful to SSL/TLS related issues.
For this option, simply configure OpenSSL with no-ssl2 no-ssl3
. no-comp
is also often used because compression can leak information.
./Configure no-ssl2 no-ssl3 <other opts>
Other OpenSSL options are available, and you might want to visit Compilation and Installation on OpenSSL's wiki.
Runtime Option
In C, you have to (1) use the 2/3 method to get SSL 2/3 and above; and then (2) call SSL_CTX_set_options
(or SSL_set_options
) and (3) remove the SSL protocols. That leaves the TLS protocols:
SSL_CTX* ctx = SSL_CTX_new(SSLv23_method());
const long flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION;
SSL_CTX_set_options(ctx, flags);
In Python, you do it with OpenSSL.SSL.Context.set_options
.