diff --git a/.gitignore b/.gitignore index ef5840951050fad72e3b0d081c18f803fc1d21e9..703dff15f1ff4549653ce090ce43b1763a68d486 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ _trial_temp build dist .opennsa-test.json +.python-version diff --git a/README.md b/README.md index e6ba93497ca7092d4834f4dd0753c16fc5a0a305..5a0721345d93e0628aa77e3c59737becd4153aaa 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ NORDUnet License (3-clause BSD). See LICENSE for more details. #### Contact -* Henrik Thostrup Jensen htj <at> nordu.net +* Johannes Garm Houen - jgh @ nordu.net #### Copyright diff --git a/onsa b/onsa index c67f7fadd8b779fd65d2458d9547daa4ff146be1..b02f5583ccb4bc53386b412b79ff1fa4c70e4287 100755 --- a/onsa +++ b/onsa @@ -130,11 +130,11 @@ def doMain(): if public_key or private_key or certificate_dir: if public_key == '.' and private_key == '.': - from opennsa import ctxfactory - ctx_factory = ctxfactory.RequestContextFactory(certificate_dir, verify_cert) + from opennsa.opennsaTlsContext import opennsaTlsContext + ctx_factory = opennsaTlsContext(certificate_dir, verify_cert) elif public_key and private_key and certificate_dir: - from opennsa import ctxfactory - ctx_factory = ctxfactory.ContextFactory(private_key, public_key, certificate_dir, verify_cert) + from opennsa.opennsaTlsContext import opennsa2WayTlsContext + ctx_factory = opennsa2WayTlsContext(private_key, public_key, certificate_dir, verify_cert) elif tls: if not public_key: raise usage.UsageError('Cannot setup TLS. No public key defined') diff --git a/opennsa/ctxfactory.py b/opennsa/ctxfactory.py deleted file mode 100644 index d33285ec77e2eca7564c688b125800f051667559..0000000000000000000000000000000000000000 --- a/opennsa/ctxfactory.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -SSL/TLS context definition. - -Most of this code is borrowed from the SGAS 3.X LUTS codebase. -NORDUnet holds the copyright for SGAS 3.X LUTS and OpenNSA. -""" - -import os - -from OpenSSL import SSL - -from twisted.python import log - -LOG_SYSTEM = 'CTXFactory' - - - -class RequestContextFactory: - """ - Context Factory for issuing requests to SSL/TLS services without having - a client certificate. - """ - def __init__(self, certificate_dir, verify): - - self.certificate_dir = certificate_dir - self.verify = verify - - self.ctx = None - - - def getContext(self): - - if self.ctx is not None: - return self.ctx - else: - self.ctx = self._createContext() - return self.ctx - - - def _createContext(self): - - def verify_callback(conn, x509, error_number, error_depth, allowed): - # just return what openssl thinks is right - if self.verify: - return allowed # return what openssl thinks is right - else: - return 1 # allow everything which has a cert - - # The way to support tls 1.0 and forward is to use the SSLv23 method - # (which means everything) and then disable ssl2 and ssl3 - # Not pretty, but it works - ctx = SSL.Context(SSL.SSLv23_METHOD) - ctx.set_options(SSL.OP_NO_SSLv2) - ctx.set_options(SSL.OP_NO_SSLv3) - - # disable tls session id, as the twisted tls protocol seems to break on them - ctx.set_session_cache_mode(SSL.SESS_CACHE_OFF) - ctx.set_options(SSL.OP_NO_TICKET) - - ctx.set_verify(SSL.VERIFY_PEER, verify_callback) - - calist = [ ca for ca in os.listdir(self.certificate_dir) if ca.endswith('.0') ] - if len(calist) == 0 and self.verify: - log.msg('No certificiates loaded for CTX verificiation. CA verification will not work.', system=LOG_SYSTEM) - for ca in calist: - # openssl wants absolute paths - ca = os.path.join(self.certificate_dir, ca) - ctx.load_verify_locations(ca) - - return ctx - - - -class ContextFactory(RequestContextFactory): - """ - Full context factory with private key and cert. When running service - over SSL/TLS. - """ - def __init__(self, private_key_path, public_key_path, certificate_dir, verify): - - RequestContextFactory.__init__(self, certificate_dir, verify) - - self.private_key_path = private_key_path - self.public_key_path = public_key_path - - - def _createContext(self): - - ctx = RequestContextFactory._createContext(self) - - ctx.use_privatekey_file(self.private_key_path) - ctx.use_certificate_chain_file(self.public_key_path) - ctx.check_privatekey() # sanity check - - return ctx - diff --git a/opennsa/opennsaTlsContext.py b/opennsa/opennsaTlsContext.py new file mode 100755 index 0000000000000000000000000000000000000000..7397f247f6848eebd914c304ac9d1f1546bb5f2c --- /dev/null +++ b/opennsa/opennsaTlsContext.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +""" +SSL/TLS context definition. + +Most of this code is borrowed from the SGAS 3.X LUTS codebase. +NORDUnet holds the copyright for SGAS 3.X LUTS and OpenNSA. + +With contributions from Hans Trompert (SURF BV) +""" + +from OpenSSL import crypto, SSL +from os import listdir, path +from sys import stdout +from twisted.internet import ssl +from twisted.python import log +from twisted.python.filepath import FilePath + +LOG_SYSTEM = 'opennsaTlsContext' + + +class opennsaTlsContext: + """ + Context to be used while issuing requests to SSL/TLS services without having + a client certificate. + """ + def __init__(self, certificate_dir, verify): + + self.certificate_dir = certificate_dir + self.verify = verify + self._trustRoot = self._createTrustRootFromCADirectory(certificate_dir) + self._extraCertificateOptions = { + 'enableSessions': False, + 'enableSessionTickets': False, + 'raiseMinimumTo': ssl.TLSVersion.TLSv1_2, + 'fixBrokenPeers': True + } + + def _createTrustRootFromCADirectory(self, certificate_dir): + CACertificates = [] + for CAFilename in listdir(certificate_dir): + if not CAFilename.endswith('.0'): + continue + CAFileContent = FilePath(certificate_dir).child(CAFilename).getContent() + try: + CACertificates.append(ssl.Certificate.loadPEM(CAFileContent)) + except crypto.Error as error: + log.msg('Cannot load CA certificate from %s: %s' % (CAFilename, error), system = LOG_SYSTEM) + else: + log.msg('Loaded CA certificate commonName %s' % (str(CACertificates[-1].getSubject().commonName)), system = LOG_SYSTEM) + if len(CACertificates) == 0: + print('No certificiates loaded for CTX verificiation. CA verification will not work.') + return ssl.trustRootFromCertificates(CACertificates) + + def getTrustRoot(self): + return self._trustRoot + + def getExtraCertificateOptions(self): + return self._extraCertificateOptions + + def getClientTLSOptions(self, hostname): + if(not self.verify): + log.msg('httpClient ignores verify=false, WILL verify certificate chain for %s against certdir' % (hostname), system = LOG_SYSTEM) + return ssl.optionsForClientTLS(hostname, trustRoot=self._trustRoot, extraCertificateOptions=self._extraCertificateOptions) + + def getContext(self): + if self.ctx is None: + self.ctx = self.createOpenSSLContext() + return self.ctx + + def createOpenSSLContext(self): + + log.msg('creating OpenSSL SSL Context ...', system=LOG_SYSTEM) + + def verify_callback(conn, x509, error_number, error_depth, allowed): + # just return what openssl thinks is right + if self.verify: + return allowed # return what openssl thinks is right + else: + return 1 # allow everything which has a cert + + # The way to support tls 1.0 and forward is to use the SSLv23 method + # (which means everything) and then disable ssl2 and ssl3 + # Not pretty, but it works + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.set_options(SSL.OP_NO_SSLv2) + ctx.set_options(SSL.OP_NO_SSLv3) + + # disable tls session id, as the twisted tls protocol seems to break on them + ctx.set_session_cache_mode(SSL.SESS_CACHE_OFF) + ctx.set_options(SSL.OP_NO_TICKET) + + ctx.set_verify(SSL.VERIFY_PEER, verify_callback) + + calist = [ ca for ca in listdir(self.certificate_dir) if ca.endswith('.0') ] + if len(calist) == 0 and self.verify: + log.msg('No certificiates loaded for CTX verificiation. CA verification will not work.', system=LOG_SYSTEM) + for ca in calist: + # openssl wants absolute paths + ca = path.join(self.certificate_dir, ca) + ctx.load_verify_locations(ca) + + return ctx + + +class opennsa2WayTlsContext(opennsaTlsContext): + """ + Full context with private key and certificate when running service + over SSL/TLS. + """ + def __init__(self, private_key_path, public_key_path, certificate_dir, verify): + + self.private_key_path = private_key_path + self.public_key_path = public_key_path + self.ctx = None + + opennsaTlsContext.__init__(self, certificate_dir, verify) + + keyContent = FilePath(private_key_path).getContent() + certificateContent = FilePath(public_key_path).getContent() + self._clientCertificate = ssl.PrivateCertificate.loadPEM(keyContent + certificateContent) + + def getClientCertificate(self): + return self._clientCertificate + + def getPrivateKey(self): + return self.getClientCertificate().privateKey.original + + def getCertificate(self): + return self.getClientCertificate().original + + def getClientTLSOptions(self, hostname): + if(not self.verify): + log.msg('httpClient ignores verify=false, WILL verify certificate chain for %s against certdir' % (hostname), system = LOG_SYSTEM) + return ssl.optionsForClientTLS(hostname, trustRoot=self._trustRoot, clientCertificate=self._clientCertificate, extraCertificateOptions=self._extraCertificateOptions) + + def getContext(self): + if self.ctx is None: + self.ctx = self.createOpenSSLContext() + return self.ctx + + def createOpenSSLContext(self): + + self.ctx = opennsaTlsContext.createOpenSSLContext(self) + + log.msg('adding key and certificate to OpenSSL SSL Context ...', system=LOG_SYSTEM) + self.ctx.use_privatekey_file(self.private_key_path) + self.ctx.use_certificate_chain_file(self.public_key_path) + self.ctx.check_privatekey() # sanity check + + return self.ctx + + +def main(): + log.startLogging(stdout) + opennsaContext = opennsa2WayTlsContext('server.key', 'server.crt', 'trusted_ca_s', False) + log.msg('trustRoot = %s' % opennsaContext.getTrustRoot(), system = LOG_SYSTEM) + log.msg('extraCertificateOptions = %s' % opennsaContext.getExtraCertificateOptions(), system = LOG_SYSTEM) + log.msg('clientCertificate = %s' % opennsaContext.getClientCertificate().getSubject(), system = LOG_SYSTEM) + log.msg('OpenSSLContext = %s' % opennsaContext.getContext(), system = LOG_SYSTEM) + log.msg('ClientTLSOptions = %s' % opennsaContext.getClientTLSOptions('some.hostname'), system = LOG_SYSTEM) + + +if __name__ == "__main__": + main() diff --git a/opennsa/protocols/shared/httpclient.py b/opennsa/protocols/shared/httpclient.py index 4da2b8d9c8299779d205f0a5e291ae4ef6d6ce18..bfd1b696827cb18e38300a8625ca88fdcae8d9b6 100644 --- a/opennsa/protocols/shared/httpclient.py +++ b/opennsa/protocols/shared/httpclient.py @@ -78,7 +78,7 @@ def httpRequest(url, payload, headers, method=b'POST', timeout=DEFAULT_TIMEOUT, if scheme == b'https': if ctx_factory is None: return defer.fail(HTTPRequestError('Cannot perform https request without context factory')) - reactor.connectSSL(host, port, factory, ctx_factory) + reactor.connectSSL(host, port, factory, ctx_factory.getClientTLSOptions(host.decode())) else: reactor.connectTCP(host, port, factory) diff --git a/opennsa/setup.py b/opennsa/setup.py index dbe5ec47e9296a380141c9b5eed67e9e75dc5f9d..4f35462eb38d816717f42a0a568a130e1e149ece 100644 --- a/opennsa/setup.py +++ b/opennsa/setup.py @@ -103,14 +103,14 @@ def setupTLSContext(vc): # ssl/tls contxt if vc[config.TLS]: - from opennsa import ctxfactory - ctx_factory = ctxfactory.ContextFactory(vc[config.KEY], vc[config.CERTIFICATE], vc[config.CERTIFICATE_DIR], vc[config.VERIFY_CERT]) + from opennsa.opennsaTlsContext import opennsa2WayTlsContext + ctx_factory = opennsa2WayTlsContext(vc[config.KEY], vc[config.CERTIFICATE], vc[config.CERTIFICATE_DIR], vc[config.VERIFY_CERT]) elif vc[config.CERTIFICATE_DIR]: # create a context so we can verify https urls if not os.path.isdir(vc[config.CERTIFICATE_DIR]): raise config.ConfigurationError('certdir value {} is not a directory'.format(vc[config.CERTIFICATE_DIR])) - from opennsa import ctxfactory - ctx_factory = ctxfactory.RequestContextFactory(vc[config.CERTIFICATE_DIR], vc[config.VERIFY_CERT]) + from opennsa.opennsaTlsContext import opennsaTlsContext + ctx_factory = opennsaTlsContext(vc[config.CERTIFICATE_DIR], vc[config.VERIFY_CERT]) else: ctx_factory = None diff --git a/requirements.txt b/requirements.txt index 3259eb073b2e06c95f89ca41d741c875c76d4190..d160fef4fb1d57aedd0a2156c8e3af5eeb4b401a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ twistar>=2.0 psycopg2>=2.7,<2.8 --no-binary psycopg2 pyOpenSSL>=17.5.0 python-dateutil +service_identity +idna