Handle secure connection with certificate

This commit is contained in:
Florian Mounier
2014-03-20 12:14:36 +01:00
parent 884eeb169a
commit 74700f5046
3 changed files with 128 additions and 61 deletions

View File

@@ -22,13 +22,11 @@ import tornado.ioloop
import tornado.httpserver
import uuid
import ssl
import getpass
import os
import stat
import sys
try:
input = raw_input
except NameError:
pass
tornado.options.define("debug", default=False, help="Debug mode")
tornado.options.define("more", default=False,
@@ -60,18 +58,46 @@ 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'
ssl_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'ssl')
if not os.path.exists(ssl_dir):
os.mkdir(ssl_dir)
def to_abs(file):
return os.path.join(ssl_dir, file)
ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [
'butterfly_ca.crt', 'butterfly_ca.key',
'butterfly.crt', 'butterfly.key',
'%s.p12'])
def write(file, content):
with open(file, 'wb') as fd:
fd.write(content)
print('Written %s' % file)
def read(file):
print('Reading %s' % file)
with open(file, 'rb') as fd:
return fd.read()
from butterfly import application
if tornado.options.options.generate_certs:
host = tornado.options.options.host
print('Generating certificates for %s (change it with --host)\n' % host)
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.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
@@ -79,17 +105,14 @@ if tornado.options.options.generate_certs:
ca_cert.set_pubkey(ca_pk)
ca_cert.sign(ca_pk, 'sha1')
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))
write(ca, crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert))
write(ca_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk))
os.chmod(ca_key, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms
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.get_subject().CN = host
server_cert.set_serial_number(200)
server_cert.gmtime_adj_notBefore(0) # From now
server_cert.gmtime_adj_notAfter(315360000) # to 10y
@@ -97,12 +120,12 @@ if tornado.options.options.generate_certs:
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))
write(cert, crypto.dump_certificate(crypto.FILETYPE_PEM, server_cert))
write(cert_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, server_pk))
os.chmod(cert_key, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms
with open(cert_key, "wb") as cf:
cf.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, server_pk))
print('Done')
print('\nNow you can run --generate_user_pkcs=user '
'to generate user certificate.')
sys.exit(0)
@@ -114,10 +137,8 @@ if tornado.options.options.generate_user_pkcs:
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())
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca))
ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key))
client_pk = crypto.PKey()
client_pk.generate_key(crypto.TYPE_RSA, 2048)
@@ -138,23 +159,29 @@ if tornado.options.options.generate_user_pkcs:
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)
while True:
password = getpass.getpass('\nPKCS12 Password (can be blank): ')
password2 = getpass.getpass('Verify Password (can be blank): ')
if password == password2:
break
print('Passwords do not match.')
write(pkcs12 % user, pfx.export(password.encode('utf-8')))
os.chmod(pkcs12 % user, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms
sys.exit(0)
if (tornado.options.options.unsecure or
tornado.options.options.host == '127.0.0.1'):
if tornado.options.options.unsecure:
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)
print("Unable to find butterfly certificate. "
"Can't run butterfly without certificate. "
"Either generate them using --generate-certs "
"or run as --unsecure "
"(NOT RECOMMENDED)")
sys.exit(1)
ssl_opts = {
'certfile': cert,

View File

@@ -22,6 +22,7 @@ import io
import struct
import fcntl
import termios
import tornado.web
import tornado.websocket
import tornado.process
import tornado.ioloop
@@ -71,8 +72,9 @@ R Y Y AFrom:R
! ! G%sX
'''
.replace('G', '\x1b[3%d;1m' % (
1 if tornado.options.options.unsecure else 2))
.replace('B', '\x1b[34;1m')
.replace('G', '\x1b[32;1m')
.replace('R', '\x1b[37;1m')
.replace('Z', '\x1b[33;1m')
.replace('A', '\x1b[37;0m')
@@ -86,6 +88,8 @@ R Y Y AFrom:R
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?')
class Index(Route):
def get(self, user, path):
if not tornado.options.options.unsecure and user:
raise tornado.web.HTTPError(400)
return self.render('index.html')
@@ -104,20 +108,26 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
self.communicate()
def shell(self):
if self.callee is None:
if self.callee is None and tornado.options.options.unsecure:
user = input('login: ')
try:
self.callee = utils.User(name=user)
except:
self.callee = utils.User(name='nobody')
assert self.callee is not None
try:
os.chdir(self.path or self.callee.dir)
except:
pass
env = os.environ
env.update(self.socket.env)
# If local and local user is the same as login user
# We set the env of the user from the browser
# Usefull when running as root
if self.caller == self.callee:
env.update(self.socket.env)
env["TERM"] = "xterm-256color"
env["COLORTERM"] = "butterfly"
env["HOME"] = self.callee.dir
@@ -127,23 +137,31 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', 'bin')), env.get("PATH"))
if self.socket.local:
# All users are the same -> launch shell
if self.caller == self.callee and server == self.callee:
args = [tornado.options.options.shell or self.callee.shell]
args.append('-i')
os.execvpe(args[0], args, env)
# This process has been replaced
return
if not tornado.options.options.unsecure or (
self.socket.local and
self.caller == self.callee and
server == self.callee):
# User has been auth with ssl or is the same user as server
try:
os.setuid(self.callee.uid)
except PermissionError:
print('The server must be run as root '
'if you want to log as different user\n')
sys.exit(1)
args = [tornado.options.options.shell or self.callee.shell]
args.append('-i')
os.execvpe(args[0], args, env)
# This process has been replaced
if server.root:
# Unsecure connection with su
if server.root:
if self.socket.local:
if self.callee != self.caller:
# Force password prompt by dropping rights
# to the daemon user
os.setuid(daemon.uid)
else:
# We are not local so we should always get a password prompt
if server.root:
else:
# We are not local so we should always get a password prompt
if self.callee == daemon:
# No logging from daemon
sys.exit(1)
@@ -201,28 +219,33 @@ 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 not tornado.options.options.unsecure:
cert = self.request.get_ssl_certificate()
if cert is not None:
for field in cert['subject']:
if field[0][0] == 'commonName':
self.user = self.callee = field[0][1]
# If local we have the user connecting
if self.socket.local and self.socket.user is not None:
self.caller = self.socket.user
if self.user:
if tornado.options.options.unsecure:
if self.user:
try:
self.callee = utils.User(name=self.user)
except LookupError:
self.callee = None
# If no user where given and we are local, keep the same user
# as the one who opened the socket
# ie: the one openning a terminal in borwser
if not self.callee and not self.user and self.socket.local:
self.callee = self.caller
else:
issuer, user = utils.parse_cert(self.request.get_ssl_certificate())
assert issuer == 'Butterfly CA', 'Invalid certificate issuer'
assert user, 'No user in certificate'
self.user = user
try:
self.callee = utils.User(name=self.user)
except LookupError:
self.callee = None
raise Exception('Invalid user in certificate')
# If no user where given and we are local, keep the same user
# as the one who opened the socket
# ie: the one openning a terminal in borwser
if not self.callee and not self.user and self.socket.local:
self.callee = self.caller
self.write_message(motd(self.socket))
self.pty()

View File

@@ -25,6 +25,23 @@ import re
log = getLogger('butterfly')
def parse_cert(cert):
issuer = None
user = None
for elt in cert['issuer']:
issuer = dict(elt).get('commonName', None)
if issuer:
break
for elt in cert['subject']:
user = dict(elt).get('commonName', None)
if user:
break
return issuer, user
class User(object):
def __init__(self, uid=None, name=None):
if uid is None and not name: