From 884eeb169a6d871ccb482e1cc1fc443f215552bd Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 19 Mar 2014 14:40:36 +0100 Subject: [PATCH] Implement client/server certificate generation + enable ssl required by default on non localhost hosts --- .gitignore | 3 + butterfly.server.py | 133 +++++++++++++++++++++++---- butterfly/routes.py | 6 +- butterfly/static/coffees/main.coffee | 1 + setup.py | 2 +- 5 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bed4b5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.crt +*.key +*.p12 diff --git a/butterfly.server.py b/butterfly.server.py index 427e936..22c2657 100644 --- a/butterfly.server.py +++ b/butterfly.server.py @@ -20,7 +20,15 @@ import tornado.options import tornado.ioloop import tornado.httpserver +import uuid import ssl +import os +import sys + +try: + input = raw_input +except NameError: + pass tornado.options.define("debug", default=False, help="Debug mode") tornado.options.define("more", default=False, @@ -28,13 +36,17 @@ tornado.options.define("more", default=False, tornado.options.define("host", default='127.0.0.1', help="Server host") tornado.options.define("port", default=57575, type=int, help="Server port") tornado.options.define("shell", help="Shell to execute at login") -tornado.options.define("secure", default=False, - help="Choose whether or not to use SSL") -tornado.options.define("reallysecure", default=False, - help="Require certificate authentication.") +tornado.options.define("unsecure", default=False, + help="Don't use ssl not recommended") + +tornado.options.define("generate_certs", default=False, + help="Generate butterfly certificates") +tornado.options.define("generate_user_pkcs", default='', + help="Generate user pfx for client authentication") tornado.options.parse_command_line() + import logging for logger in ('tornado.access', 'tornado.application', 'tornado.general', 'butterfly'): @@ -48,28 +60,117 @@ for logger in ('tornado.access', 'tornado.application', log = logging.getLogger('butterfly') log.info('Starting server') ioloop = tornado.ioloop.IOLoop.instance() +ca, ca_key = 'butterfly_ca.crt', 'butterfly_ca.key' +cert, cert_key = 'butterfly.crt', 'butterfly.key' from butterfly import application -if tornado.options.options.reallysecure: - tornado.options.options.secure = True - reqs = ssl.CERT_REQUIRED -elif tornado.options.options.secure: - reqs = ssl.CERT_OPTIONAL +if tornado.options.options.generate_certs: + from OpenSSL import crypto + ca_pk = crypto.PKey() + ca_pk.generate_key(crypto.TYPE_RSA, 2048) + ca_cert = crypto.X509() + ca_cert.get_subject().CN = 'butterfly ca' + ca_cert.set_serial_number(100) + ca_cert.gmtime_adj_notBefore(0) # From now + ca_cert.gmtime_adj_notAfter(315360000) # to 10y + ca_cert.set_issuer(ca_cert.get_subject()) # Self signed + ca_cert.set_pubkey(ca_pk) + ca_cert.sign(ca_pk, 'sha1') -ssl_opts = None -if tornado.options.options.secure: - ssl_opts = dict(certfile="butterfly.crt", keyfile="butterfly.key", - cert_reqs=reqs, ca_certs="butterflyca.crt") + with open(ca, "wb") as cf: + cf.write( + crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert)) + with open(ca_key, "wb") as cf: + cf.write( + crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk)) + + server_pk = crypto.PKey() + server_pk.generate_key(crypto.TYPE_RSA, 2048) + server_cert = crypto.X509() + server_cert.get_subject().CN = tornado.options.options.host + server_cert.set_serial_number(200) + server_cert.gmtime_adj_notBefore(0) # From now + server_cert.gmtime_adj_notAfter(315360000) # to 10y + server_cert.set_issuer(ca_cert.get_subject()) # Signed by ca + server_cert.set_pubkey(server_pk) + server_cert.sign(ca_pk, 'sha1') + + with open(cert, "wb") as cf: + cf.write(crypto.dump_certificate(crypto.FILETYPE_PEM, server_cert)) + + with open(cert_key, "wb") as cf: + cf.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, server_pk)) + print('Done') + sys.exit(0) + + +if tornado.options.options.generate_user_pkcs: + from OpenSSL import crypto + if not all(map(os.path.exists, + [cert, cert_key, ca, ca_key])): + print('Please generate certificates using --generate_certs before') + sys.exit(1) + + user = tornado.options.options.generate_user_pkcs + with open(ca, 'rb') as cf: + ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, cf.read()) + with open(ca_key, 'rb') as cf: + ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, cf.read()) + + client_pk = crypto.PKey() + client_pk.generate_key(crypto.TYPE_RSA, 2048) + + client_cert = crypto.X509() + client_cert.get_subject().CN = user + client_cert.set_serial_number(uuid.uuid4().int) + client_cert.gmtime_adj_notBefore(0) # From now + client_cert.gmtime_adj_notAfter(315360000) # to 10y + client_cert.set_issuer(ca_cert.get_subject()) # Signed by ca + client_cert.set_pubkey(client_pk) + client_cert.sign(client_pk, 'sha1') + client_cert.sign(ca_pk, 'sha1') + + pfx = crypto.PKCS12() + pfx.set_certificate(client_cert) + pfx.set_privatekey(client_pk) + pfx.set_ca_certificates([ca_cert]) + pfx.set_friendlyname(('%s cert for butterfly' % user).encode('utf-8')) + + with open('%s.p12' % user, "wb") as cf: + cf.write(pfx.export(b'')) + print('%s.p12 written.' % user) + sys.exit(0) + + +if (tornado.options.options.unsecure or + tornado.options.options.host == '127.0.0.1'): + ssl_opts = None +else: + if not all(map(os.path.exists, + [cert, cert_key, ca, ca_key])): + print("Unable to find butterfly certificate. " + "Can't run butterfly without certificate. " + "Either generate them or run as --unsecure " + "(NOT RECOMMENDED)") + sys.exit(1) + + ssl_opts = { + 'certfile': cert, + 'keyfile': cert_key, + 'ca_certs': ca, + 'cert_reqs': ssl.CERT_REQUIRED + } http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_opts) http_server.listen( tornado.options.options.port, address=tornado.options.options.host) -url = "http%s://%s:%d/*" % ("s" if tornado.options.options.secure else "", - tornado.options.options.host, - tornado.options.options.port) +url = "http%s://%s:%d/*" % ( + "s" if not tornado.options.options.unsecure else "", + tornado.options.options.host, + tornado.options.options.port) # This is for debugging purpose try: diff --git a/butterfly/routes.py b/butterfly/routes.py index 5ed4c1b..eea6c5a 100644 --- a/butterfly/routes.py +++ b/butterfly/routes.py @@ -122,7 +122,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): env["COLORTERM"] = "butterfly" env["HOME"] = self.callee.dir env["LOCATION"] = "http%s://%s:%d/" % ( - "s" if tornado.options.options.secure else "", + "s" if not tornado.options.options.unsecure else "", tornado.options.options.host, tornado.options.options.port) env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'bin')), env.get("PATH")) @@ -186,7 +186,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): def open(self, user, path): if self.request.headers['Origin'] != 'http%s://%s' % ( - "s" if tornado.options.options.secure else "", + "s" if not tornado.options.options.unsecure else "", self.request.headers['Host']): self.log.warning( 'Unauthorized connection attempt: from : %s to: %s' % ( @@ -201,7 +201,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler): self.path = path self.user = user.decode('utf-8') if user else None self.caller = self.callee = None - if tornado.options.options.secure: + if not tornado.options.options.unsecure: cert = self.request.get_ssl_certificate() if cert is not None: for field in cert['subject']: diff --git a/butterfly/static/coffees/main.coffee b/butterfly/static/coffees/main.coffee index af4e05f..8d9fafe 100644 --- a/butterfly/static/coffees/main.coffee +++ b/butterfly/static/coffees/main.coffee @@ -33,6 +33,7 @@ if location.protocol == 'https:' ws_url = 'wss://' else ws_url = 'ws://' + ws_url += document.location.host + '/ws' + location.pathname ws = new WebSocket ws_url diff --git a/setup.py b/setup.py index e06011f..3752507 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ options = dict( platforms="Any", scripts=['butterfly.server.py'], packages=['butterfly'], - install_requires=["tornado>=3.2"], + install_requires=["tornado>=3.2", "pyOpenSSL"], package_data={ 'butterfly': [ 'static/fonts/*',