From 92d7ca3870249bf3b8ef78ff2ef845337a5c6d51 Mon Sep 17 00:00:00 2001 From: thomasDOTde Date: Sat, 20 Jan 2018 17:17:02 +0100 Subject: [PATCH] added application certificate handling for signed SAML messages --- app/__init__.py | 7 +++++-- app/lib/certutil.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ app/lib/utils.py | 22 ++++++++++++--------- app/views.py | 4 +++- config_template.py | 3 +++ 5 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 app/lib/certutil.py diff --git a/app/__init__.py b/app/__init__.py index 96e36d8..897db56 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -11,7 +11,6 @@ login_manager = LoginManager() login_manager.init_app(app) db = SQLAlchemy(app) - def enable_github_oauth(GITHUB_ENABLE): if not GITHUB_ENABLE: return None, None @@ -89,5 +88,9 @@ def enable_google_oauth(GOOGLE_ENABLE): google = enable_google_oauth(app.config.get('GOOGLE_OAUTH_ENABLE')) - from app import views, models + +if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'): + from app.lib import certutil + if not certutil.check_certificate(): + certutil.create_self_signed_cert() diff --git a/app/lib/certutil.py b/app/lib/certutil.py new file mode 100644 index 0000000..8f1b93b --- /dev/null +++ b/app/lib/certutil.py @@ -0,0 +1,48 @@ +from OpenSSL import crypto +from datetime import datetime +import pytz +import os +CRYPT_PATH = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../") +CERT_FILE = CRYPT_PATH + "/saml_cert.crt" +KEY_FILE = CRYPT_PATH + "/saml_cert.key" + + +def check_certificate(): + if not os.path.isfile(CERT_FILE): + return False + st_cert = open(CERT_FILE, 'rt').read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, st_cert) + now = datetime.now(pytz.utc) + begin = datetime.strptime(cert.get_notBefore(), "%Y%m%d%H%M%SZ").replace(tzinfo=pytz.UTC) + begin_ok = begin < now + end = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ").replace(tzinfo=pytz.UTC) + end_ok = end > now + if begin_ok and end_ok: + return True + return False + +def create_self_signed_cert(): + + # create a key pair + k = crypto.PKey() + k.generate_key(crypto.TYPE_RSA, 2048) + + # create a self-signed cert + cert = crypto.X509() + cert.get_subject().C = "DE" + cert.get_subject().ST = "NRW" + cert.get_subject().L = "Dortmund" + cert.get_subject().O = "Dummy Company Ltd" + cert.get_subject().OU = "Dummy Company Ltd" + cert.get_subject().CN = "PowerDNS-Admin" + cert.set_serial_number(1000) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(10*365*24*60*60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(k) + cert.sign(k, 'sha256') + + open(CERT_FILE, "wt").write( + crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + open(KEY_FILE, "wt").write( + crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) \ No newline at end of file diff --git a/app/lib/utils.py b/app/lib/utils.py index d7b6d4c..56fbf17 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -6,6 +6,7 @@ import urlparse import hashlib from app import app +from certutil import * from distutils.version import StrictVersion from datetime import datetime,timedelta from threading import Thread @@ -193,6 +194,7 @@ def email_to_gravatar_url(email, size=100): hash_string = hashlib.md5(email).hexdigest() return "https://s.gravatar.com/avatar/%s?s=%s" % (hash_string, size) + def prepare_flask_request(request): # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields url_data = urlparse.urlparse(request.url) @@ -204,7 +206,7 @@ def prepare_flask_request(request): 'get_data': request.args.copy(), 'post_data': request.form.copy(), # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 - # 'lowercase_urlencoding': True, + 'lowercase_urlencoding': True, 'query_string': request.query_string } @@ -220,8 +222,10 @@ def init_saml_auth(req): settings['sp'] = {} settings['sp']['NameIDFormat'] = idp_data['sp']['NameIDFormat'] settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID'] - settings['sp']['privateKey'] = '' - settings['sp']['x509cert'] = '' + cert = open(CERT_FILE, "r").readlines() + key = open(KEY_FILE, "r").readlines() + settings['sp']['privateKey'] = "".join(key) + settings['sp']['x509cert'] = "".join(cert) settings['sp']['assertionConsumerService'] = {} settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized' @@ -233,19 +237,19 @@ def init_saml_auth(req): settings['strict'] = True settings['debug'] = app.config['SAML_DEBUG'] settings['security'] = {} - settings['security']['digestAlgorithm'] = 'http://www.w3.org/2000/09/xmldsig#sha1' + settings['security']['digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' settings['security']['metadataCacheDuration'] = None settings['security']['metadataValidUntil'] = None settings['security']['requestedAuthnContext'] = True - settings['security']['signatureAlgorithm'] = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + settings['security']['signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' settings['security']['wantAssertionsEncrypted'] = False settings['security']['wantAttributeStatement'] = True settings['security']['wantNameId'] = True - settings['security']['authnRequestsSigned'] = False - settings['security']['logoutRequestSigned'] = False - settings['security']['logoutResponseSigned'] = False + settings['security']['authnRequestsSigned'] = app.config['SAML_SIGN_REQUEST'] + settings['security']['logoutRequestSigned'] = app.config['SAML_SIGN_REQUEST'] + settings['security']['logoutResponseSigned'] = app.config['SAML_SIGN_REQUEST'] settings['security']['nameIdEncrypted'] = False - settings['security']['signMetadata'] = False + settings['security']['signMetadata'] = True settings['security']['wantAssertionsSigned'] = True settings['security']['wantMessagesSigned'] = True settings['security']['wantNameIdEncrypted'] = False diff --git a/app/views.py b/app/views.py index ed6f791..a263d64 100644 --- a/app/views.py +++ b/app/views.py @@ -412,8 +412,10 @@ def saml_logout(): clear_session() if url is not None: return redirect(url) - elif app.config.get('SAML_LOGOUT_URL'): + elif app.config.get('SAML_LOGOUT_URL') is not None: return redirect(app.config.get('SAML_LOGOUT_URL')) + else: + return redirect(url_for('login')) else: return render_template('errors/SAML.html', errors=errors) diff --git a/config_template.py b/config_template.py index 73496fd..ed1ec68 100644 --- a/config_template.py +++ b/config_template.py @@ -92,6 +92,9 @@ SAML_METADATA_CACHE_LIFETIME = 1 SAML_SP_ENTITY_ID = 'http://' SAML_SP_CONTACT_NAME = '' SAML_SP_CONTACT_MAIL = '' +#Cofigures if SAML tokens should be encrypted. +#If enabled a new app certificate will be generated on restart +SAML_SIGN_REQUEST = False #Use SAML standard logout mechanism retreived from idp metadata #If configured false don't care about SAML session on logout. #Logout from PowerDNS-Admin only and keep SAML session authenticated.