Cleanup of telnet-ssl, creating public/private/certs in code rather than

calling openssl in a subprocess. Also better handle errors and
reporting.
This commit is contained in:
Griatch 2018-01-27 19:50:50 +01:00
parent 5e08ca3579
commit 948d27cd92
3 changed files with 161 additions and 128 deletions

View file

@ -10,14 +10,11 @@ by game/evennia.py).
from __future__ import print_function
from builtins import object
import time
import sys
import os
from twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.internet.task import LoopingCall
from twisted.web import server
import django
django.setup()
from django.conf import settings
@ -228,9 +225,9 @@ if TELNET_ENABLED:
if SSL_ENABLED:
# Start SSL game connection (requires PyOpenSSL).
# Start Telnet SSL game connection (requires PyOpenSSL).
from evennia.server.portal import ssl
from evennia.server.portal import telnet_ssl
for interface in SSL_INTERFACES:
ifacestr = ""
@ -241,15 +238,20 @@ if SSL_ENABLED:
factory = protocol.ServerFactory()
factory.noisy = False
factory.sessionhandler = PORTAL_SESSIONS
factory.protocol = ssl.SSLProtocol
ssl_service = internet.SSLServer(port,
factory,
ssl.getSSLContext(),
interface=interface)
ssl_service.setName('EvenniaSSL%s' % pstring)
PORTAL.services.addService(ssl_service)
factory.protocol = telnet_ssl.SSLProtocol
print(" ssl%s: %s" % (ifacestr, port))
ssl_context = telnet_ssl.getSSLContext()
if ssl_context:
ssl_service = internet.SSLServer(port,
factory,
telnet_ssl.getSSLContext(),
interface=interface)
ssl_service.setName('EvenniaSSL%s' % pstring)
PORTAL.services.addService(ssl_service)
print(" ssl%s: %s" % (ifacestr, port))
else:
print(" ssl%s: %s (deactivated - keys/certificate unset)" % (ifacestr, port))
if SSH_ENABLED:

View file

@ -1,115 +0,0 @@
"""
This is a simple context factory for auto-creating
SSL keys and certificates.
"""
from __future__ import print_function
import os
import sys
try:
import OpenSSL
from twisted.internet import ssl as twisted_ssl
except ImportError as error:
errstr = """
{err}
SSL requires the PyOpenSSL library and dependencies:
pip install pyopenssl pycrypto enum pyasn1 service_identity
Stop and start Evennia again. If no certificate can be generated, you'll
get a suggestion for a (linux) command to generate this locally.
"""
raise ImportError(errstr.format(err=error))
from django.conf import settings
from evennia.server.portal.telnet import TelnetProtocol
_GAME_DIR = settings.GAME_DIR
# messages
NO_AUTOGEN = """
{err}
Evennia could not auto-generate the SSL private key. If this error
persists, create {keyfile} yourself using third-party tools.
"""
NO_AUTOCERT = """
{err}
Evennia's SSL context factory could not automatically, create an SSL
certificate {certfile}.
A private key {keyfile} was already created. Please create {certfile}
manually using the commands valid for your operating system, for
example (linux, using the openssl program):
{exestring}
"""
class SSLProtocol(TelnetProtocol):
"""
Communication is the same as telnet, except data transfer
is done with encryption.
"""
def __init__(self, *args, **kwargs):
super(SSLProtocol, self).__init__(*args, **kwargs)
self.protocol_name = "ssl"
def verify_SSL_key_and_cert(keyfile, certfile):
"""
This function looks for RSA key and certificate in the current
directory. If files ssl.key and ssl.cert does not exist, they
are created.
"""
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
# key/cert does not exist. Create.
import subprocess
from Crypto.PublicKey import RSA
from twisted.conch.ssh.keys import Key
print(" Creating SSL key and certificate ... ", end=' ')
try:
# create the RSA key and store it.
KEY_LENGTH = 1024
rsaKey = Key(RSA.generate(KEY_LENGTH))
keyString = rsaKey.toString(type="OPENSSH")
file(keyfile, 'w+b').write(keyString)
except Exception as err:
print(NO_AUTOGEN.format(err=err, keyfile=keyfile))
sys.exit(5)
# try to create the certificate
CERT_EXPIRE = 365 * 20 # twenty years validity
# default:
# openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300
exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (keyfile, certfile, CERT_EXPIRE)
try:
subprocess.call(exestring)
except OSError as err:
raise OSError(NO_AUTOCERT.format(err=err, certfile=certfile, keyfile=keyfile, exestring=exestring))
print("done.")
def getSSLContext():
"""
This is called by the portal when creating the SSL context
server-side.
Returns:
ssl_context (tuple): A key and certificate that is either
existing previously or or created on the fly.
"""
keyfile = os.path.join(_GAME_DIR, "server", "ssl.key")
certfile = os.path.join(_GAME_DIR, "server", "ssl.cert")
verify_SSL_key_and_cert(keyfile, certfile)
return twisted_ssl.DefaultOpenSSLContextFactory(keyfile, certfile)

View file

@ -0,0 +1,146 @@
"""
This allows for running the telnet communication over an encrypted SSL tunnel. To use it, requires a
client supporting Telnet SSL.
The protocol will try to automatically create the private key and certificate on the server side
when starting and will warn if this was not possible. These will appear as files ssl.key and
ssl.cert in mygame/server/.
"""
from __future__ import print_function
import os
try:
from OpenSSL import crypto
from twisted.internet import ssl as twisted_ssl
except ImportError as error:
errstr = """
{err}
Telnet-SSL requires the PyOpenSSL library and dependencies:
pip install pyopenssl pycrypto enum pyasn1 service_identity
Stop and start Evennia again. If no certificate can be generated, you'll
get a suggestion for a (linux) command to generate this locally.
"""
raise ImportError(errstr.format(err=error))
from django.conf import settings
from evennia.server.portal.telnet import TelnetProtocol
_GAME_DIR = settings.GAME_DIR
_PRIVATE_KEY_LENGTH = 2048
_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl.key")
_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl_public.key")
_CERTIFICATE_FILE = os.path.join(_GAME_DIR, "server", "ssl.cert")
_CERTIFICATE_EXPIRE = 365 * 24 * 60 * 60 * 20 # 20 years
_CERTIFICATE_ISSUER = {"C": "EV", "ST": "Evennia", "L": "Evennia", "O":
"Evennia Security", "OU": "Evennia Department", "CN": "evennia"}
# messages
NO_AUTOGEN = """
Evennia could not auto-generate the SSL private- and public keys ({{err}}).
If this error persists, create them manually (using the tools for your OS). The files
should be placed and named like this:
{}
{}
""".format(_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE)
NO_AUTOCERT = """
Evennia's could not auto-generate the SSL certificate ({{err}}).
The private key already exists here:
{}
If this error persists, create the certificate manually (using the private key and
the tools for your OS). The file should be placed and named like this:
{}
""".format(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE)
class SSLProtocol(TelnetProtocol):
"""
Communication is the same as telnet, except data transfer
is done with encryption set up by the portal at start time.
"""
def __init__(self, *args, **kwargs):
super(SSLProtocol, self).__init__(*args, **kwargs)
self.protocol_name = "ssl"
def verify_or_create_SSL_key_and_cert(keyfile, certfile):
"""
Verify or create new key/certificate files.
Args:
keyfile (str): Path to ssl.key file.
certfile (str): Parth to ssl.cert file.
Notes:
If files don't already exist, they are created.
"""
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
# key/cert does not exist. Create.
try:
# generate the keypair
keypair = crypto.PKey()
keypair.generate_key(crypto.TYPE_RSA, _PRIVATE_KEY_LENGTH)
with open(_PRIVATE_KEY_FILE, 'wt') as pfile:
pfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair))
print("Created SSL private key in '{}'.".format(_PRIVATE_KEY_FILE))
with open(_PUBLIC_KEY_FILE, 'wt') as pfile:
pfile.write(crypto.dump_publickey(crypto.FILETYPE_PEM, keypair))
print("Created SSL public key in '{}'.".format(_PRIVATE_KEY_FILE))
except Exception as err:
print(NO_AUTOGEN.format(err=err))
return False
else:
try:
# create certificate
cert = crypto.X509()
subj = cert.get_subject()
for key, value in _CERTIFICATE_ISSUER.items():
setattr(subj, key, value)
cert.set_issuer(subj)
cert.set_serial_number(1000)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(_CERTIFICATE_EXPIRE)
cert.set_pubkey(keypair)
cert.sign(keypair, 'sha1')
with open(_CERTIFICATE_FILE, 'wt') as cfile:
cfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
print("Created SSL certificate in '{}'.".format(_PRIVATE_KEY_FILE))
except Exception as err:
print(NO_AUTOCERT.format(err=err))
return False
return True
def getSSLContext():
"""
This is called by the portal when creating the SSL context
server-side.
Returns:
ssl_context (tuple): A key and certificate that is either
existing previously or created on the fly.
"""
if verify_or_create_SSL_key_and_cert(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE):
return twisted_ssl.DefaultOpenSSLContextFactory(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE)
else:
return None