From: Simo Sorce Date: Wed, 9 Apr 2014 18:02:08 +0000 (-0400) Subject: Move accessory functions to a generic tools module X-Git-Tag: v0.2.2~32 X-Git-Url: http://git.cascardo.info/?p=cascardo%2Fipsilon.git;a=commitdiff_plain;h=277ed07e8810dbd0adbbf213f56246394753f452;hp=904898b83d90d3d7f83c574b27a79b98a23e3734 Move accessory functions to a generic tools module This will allow to easly share the module with install tools, without the need to install server side modules in clients Signed-off-by: Simo Sorce --- diff --git a/ipsilon/providers/saml2/certs.py b/ipsilon/providers/saml2/certs.py deleted file mode 100755 index dc08e08..0000000 --- a/ipsilon/providers/saml2/certs.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2014 Simo Sorce -# -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from subprocess import Popen -import os -import string - - -class Certificate(object): - - def __init__(self, path=None): - self.subject = None - self.path = path - self.key = None - self.cert = None - - def generate(self, prefix, subject): - self.key = '%s.key' % prefix - self.cert = '%s.pem' % prefix - self.subject = '/CN=%s' % subject - command = ['openssl', - 'req', '-x509', '-batch', '-days', '1825', - '-newkey', 'rsa:2048', '-nodes', '-subj', self.subject, - '-keyout', os.path.join(self.path, self.key), - '-out', os.path.join(self.path, self.cert)] - proc = Popen(command) - proc.wait() - - def get_cert(self): - if not self.cert: - raise NameError('Invalid certificate name: %s' % self.cert) - with open(os.path.join(self.path, self.cert), 'r') as f: - cert = f.readlines() - - #poor man stripping of BEGIN/END lines - if cert[0] == '-----BEGIN CERTIFICATE-----\n': - cert = cert[1:] - if cert[-1] == '-----END CERTIFICATE-----\n': - cert = cert[:-1] - - return string.join(cert) diff --git a/ipsilon/providers/saml2/metadata.py b/ipsilon/providers/saml2/metadata.py deleted file mode 100755 index 0effd4c..0000000 --- a/ipsilon/providers/saml2/metadata.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/python -# -# Copyright (C) 2014 Simo Sorce -# -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from ipsilon.providers.saml2.certs import Certificate -from lxml import etree -import lasso - - -EDESC = '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF -NSMAP = { - 'md': lasso.SAML2_METADATA_HREF, - 'ds': lasso.DS_HREF -} - -IDPDESC = 'IDPSSODescriptor' -SPDESC = 'SPSSODescriptor' - -IDP_ROLE = 'idp' -SP_ROLE = 'sp' - -SSO_SERVICE = 'SingleSignOnService' -LOGOUT_SERVICE = 'SingleLogoutService' -ASSERTION_SERVICE = 'AssertionConsumerService' - - -def mdElement(_parent, _tag, **kwargs): - tag = '{%s}%s' % (lasso.SAML2_METADATA_HREF, _tag) - return etree.SubElement(_parent, tag, **kwargs) - - -def dsElement(_parent, _tag, **kwargs): - tag = '{%s}%s' % (lasso.DS_HREF, _tag) - return etree.SubElement(_parent, tag, **kwargs) - - -class Metadata(object): - - def __init__(self, role=None): - self.root = etree.Element(EDESC, nsmap=NSMAP) - self.entityid = None - self.role = None - self.set_role(role) - - def set_entity_id(self, url): - self.entityid = url - self.root.set('entityID', url) - - def set_role(self, role): - if role is None: - return - elif role == IDP_ROLE: - description = IDPDESC - elif role == SP_ROLE: - description = SPDESC - else: - raise ValueError('invalid role: %s' % role) - self.role = mdElement(self.root, description) - self.role.set('protocolSupportEnumeration', lasso.SAML2_PROTOCOL_HREF) - return self.role - - def add_cert(self, certdata, use): - desc = mdElement(self.role, 'KeyDescriptor') - desc.set('use', use) - info = dsElement(desc, 'KeyInfo') - data = dsElement(info, 'X509Data') - cert = dsElement(data, 'X509Certificate') - cert.text = certdata - - def add_certs(self, signcert=None, enccert=None): - if signcert: - self.add_cert(signcert.get_cert(), 'signing') - if enccert: - self.add_cert(enccert.get_cert(), 'encryption') - - def add_service(self, svctype, binding, location): - svc = mdElement(self.role, svctype) - svc.set('Binding', binding) - svc.set('Location', location) - - def add_allowed_name_format(self, name_format): - nameidfmt = mdElement(self.role, 'NameIDFormat') - nameidfmt.text = name_format - - def output(self, path): - data = etree.tostring(self.root, xml_declaration=True, - encoding='UTF-8', pretty_print=True) - with open(path, 'w') as f: - f.write(data) - - -if __name__ == '__main__': - from ipsilon.providers.saml2.provider import NAMEID_MAP - import tempfile - import shutil - import os - - tmpdir = tempfile.mkdtemp() - - try: - # Test IDP generation - sign_cert = Certificate(tmpdir) - sign_cert.generate('idp-signing-cert', 'idp.ipsilon.example.com') - enc_cert = Certificate(tmpdir) - enc_cert.generate('idp-encryption-cert', 'idp.ipsilon.example.com') - idp = Metadata() - idp.set_entity_id('https://ipsilon.example.com/idp/metadata') - idp.set_role(IDP_ROLE) - idp.add_certs(sign_cert, enc_cert) - idp.add_service(SSO_SERVICE, lasso.SAML2_METADATA_BINDING_POST, - 'https://ipsilon.example.com/idp/saml2/POST') - idp.add_service(SSO_SERVICE, lasso.SAML2_METADATA_BINDING_REDIRECT, - 'https://ipsilon.example.com/idp/saml2/Redirect') - for k in NAMEID_MAP: - idp.add_allowed_name_format(NAMEID_MAP[k]) - md_file = os.path.join(tmpdir, 'metadata.xml') - idp.output(md_file) - with open(md_file) as fd: - text = fd.read() - print '==================== IDP ====================' - print text - print '=============================================' - - # Test SP generation - sign_cert = Certificate(tmpdir) - sign_cert.generate('sp-signing-cert', 'sp.ipsilon.example.com') - sp = Metadata() - sp.set_entity_id('https://ipsilon.example.com/samlsp/metadata') - sp.set_role(SP_ROLE) - sp.add_certs(sign_cert) - sp.add_service(LOGOUT_SERVICE, lasso.SAML2_METADATA_BINDING_REDIRECT, - 'https://ipsilon.example.com/samlsp/logout') - sp.add_service(ASSERTION_SERVICE, lasso.SAML2_METADATA_BINDING_POST, - 'https://ipsilon.example.com/samlsp/postResponse') - md_file = os.path.join(tmpdir, 'metadata.xml') - sp.output(md_file) - with open(md_file) as fd: - text = fd.read() - print '===================== SP ====================' - print text - print '=============================================' - - finally: - shutil.rmtree(tmpdir) diff --git a/ipsilon/providers/saml2/provider.py b/ipsilon/providers/saml2/provider.py index 73ff005..7d47363 100755 --- a/ipsilon/providers/saml2/provider.py +++ b/ipsilon/providers/saml2/provider.py @@ -18,23 +18,11 @@ # along with this program. If not, see . from ipsilon.providers.common import ProviderException +from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP import cherrypy import lasso -NAMEID_MAP = { - 'email': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL, - 'encrypted': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED, - 'entity': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENTITY, - 'kerberos': lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS, - 'persistent': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, - 'transient': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT, - 'unspecified': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED, - 'windows': lasso.SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS, - 'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509, -} - - class InvalidProviderId(ProviderException): def __init__(self, code): @@ -129,14 +117,14 @@ class ServiceProvider(object): def get_valid_nameid(self, nip): self._debug('Requested NameId [%s]' % (nip.format,)) if nip.format is None: - return NAMEID_MAP[self.default_nameid] + return SAML2_NAMEID_MAP[self.default_nameid] elif nip.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED: - return NAMEID_MAP[self.default_nameid] + return SAML2_NAMEID_MAP[self.default_nameid] else: allowed = self.allowed_nameids self._debug('Allowed NameIds %s' % (repr(allowed))) for nameid in allowed: - if nip.format == NAMEID_MAP[nameid]: + if nip.format == SAML2_NAMEID_MAP[nameid]: return nip.format raise NameIdNotAllowed(nip.format) diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index 1922c53..87cc7f6 100755 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -21,9 +21,9 @@ from ipsilon.providers.common import ProviderBase, ProviderPageBase from ipsilon.providers.common import FACILITY from ipsilon.providers.saml2.auth import AuthenticateRequest from ipsilon.providers.saml2.admin import AdminPage -from ipsilon.providers.saml2.certs import Certificate from ipsilon.providers.saml2.provider import IdentityProvider -from ipsilon.providers.saml2 import metadata +from ipsilon.tools.certs import Certificate +from ipsilon.tools import saml2metadata as metadata from ipsilon.util.user import UserSession from ipsilon.util.plugin import PluginObject import cherrypy diff --git a/ipsilon/tools/__init__.py b/ipsilon/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ipsilon/tools/certs.py b/ipsilon/tools/certs.py new file mode 100755 index 0000000..dc08e08 --- /dev/null +++ b/ipsilon/tools/certs.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Simo Sorce +# +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from subprocess import Popen +import os +import string + + +class Certificate(object): + + def __init__(self, path=None): + self.subject = None + self.path = path + self.key = None + self.cert = None + + def generate(self, prefix, subject): + self.key = '%s.key' % prefix + self.cert = '%s.pem' % prefix + self.subject = '/CN=%s' % subject + command = ['openssl', + 'req', '-x509', '-batch', '-days', '1825', + '-newkey', 'rsa:2048', '-nodes', '-subj', self.subject, + '-keyout', os.path.join(self.path, self.key), + '-out', os.path.join(self.path, self.cert)] + proc = Popen(command) + proc.wait() + + def get_cert(self): + if not self.cert: + raise NameError('Invalid certificate name: %s' % self.cert) + with open(os.path.join(self.path, self.cert), 'r') as f: + cert = f.readlines() + + #poor man stripping of BEGIN/END lines + if cert[0] == '-----BEGIN CERTIFICATE-----\n': + cert = cert[1:] + if cert[-1] == '-----END CERTIFICATE-----\n': + cert = cert[:-1] + + return string.join(cert) diff --git a/ipsilon/tools/saml2metadata.py b/ipsilon/tools/saml2metadata.py new file mode 100755 index 0000000..fc2e02c --- /dev/null +++ b/ipsilon/tools/saml2metadata.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Simo Sorce +# +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from ipsilon.tools.certs import Certificate +from lxml import etree +import lasso + + +SAML2_NAMEID_MAP = { + 'email': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL, + 'encrypted': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED, + 'entity': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENTITY, + 'kerberos': lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS, + 'persistent': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, + 'transient': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT, + 'unspecified': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED, + 'windows': lasso.SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS, + 'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509, +} + + +EDESC = '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF +NSMAP = { + 'md': lasso.SAML2_METADATA_HREF, + 'ds': lasso.DS_HREF +} + +IDPDESC = 'IDPSSODescriptor' +SPDESC = 'SPSSODescriptor' + +IDP_ROLE = 'idp' +SP_ROLE = 'sp' + +SSO_SERVICE = 'SingleSignOnService' +LOGOUT_SERVICE = 'SingleLogoutService' +ASSERTION_SERVICE = 'AssertionConsumerService' + + +def mdElement(_parent, _tag, **kwargs): + tag = '{%s}%s' % (lasso.SAML2_METADATA_HREF, _tag) + return etree.SubElement(_parent, tag, **kwargs) + + +def dsElement(_parent, _tag, **kwargs): + tag = '{%s}%s' % (lasso.DS_HREF, _tag) + return etree.SubElement(_parent, tag, **kwargs) + + +class Metadata(object): + + def __init__(self, role=None): + self.root = etree.Element(EDESC, nsmap=NSMAP) + self.entityid = None + self.role = None + self.set_role(role) + + def set_entity_id(self, url): + self.entityid = url + self.root.set('entityID', url) + + def set_role(self, role): + if role is None: + return + elif role == IDP_ROLE: + description = IDPDESC + elif role == SP_ROLE: + description = SPDESC + else: + raise ValueError('invalid role: %s' % role) + self.role = mdElement(self.root, description) + self.role.set('protocolSupportEnumeration', lasso.SAML2_PROTOCOL_HREF) + return self.role + + def add_cert(self, certdata, use): + desc = mdElement(self.role, 'KeyDescriptor') + desc.set('use', use) + info = dsElement(desc, 'KeyInfo') + data = dsElement(info, 'X509Data') + cert = dsElement(data, 'X509Certificate') + cert.text = certdata + + def add_certs(self, signcert=None, enccert=None): + if signcert: + self.add_cert(signcert.get_cert(), 'signing') + if enccert: + self.add_cert(enccert.get_cert(), 'encryption') + + def add_service(self, svctype, binding, location): + svc = mdElement(self.role, svctype) + svc.set('Binding', binding) + svc.set('Location', location) + + def add_allowed_name_format(self, name_format): + nameidfmt = mdElement(self.role, 'NameIDFormat') + nameidfmt.text = name_format + + def output(self, path): + data = etree.tostring(self.root, xml_declaration=True, + encoding='UTF-8', pretty_print=True) + with open(path, 'w') as f: + f.write(data) + + +if __name__ == '__main__': + import tempfile + import shutil + import os + + tmpdir = tempfile.mkdtemp() + + try: + # Test IDP generation + sign_cert = Certificate(tmpdir) + sign_cert.generate('idp-signing-cert', 'idp.ipsilon.example.com') + enc_cert = Certificate(tmpdir) + enc_cert.generate('idp-encryption-cert', 'idp.ipsilon.example.com') + idp = Metadata() + idp.set_entity_id('https://ipsilon.example.com/idp/metadata') + idp.set_role(IDP_ROLE) + idp.add_certs(sign_cert, enc_cert) + idp.add_service(SSO_SERVICE, lasso.SAML2_METADATA_BINDING_POST, + 'https://ipsilon.example.com/idp/saml2/POST') + idp.add_service(SSO_SERVICE, lasso.SAML2_METADATA_BINDING_REDIRECT, + 'https://ipsilon.example.com/idp/saml2/Redirect') + for k in SAML2_NAMEID_MAP: + idp.add_allowed_name_format(SAML2_NAMEID_MAP[k]) + md_file = os.path.join(tmpdir, 'metadata.xml') + idp.output(md_file) + with open(md_file) as fd: + text = fd.read() + print '==================== IDP ====================' + print text + print '=============================================' + + # Test SP generation + sign_cert = Certificate(tmpdir) + sign_cert.generate('sp-signing-cert', 'sp.ipsilon.example.com') + sp = Metadata() + sp.set_entity_id('https://ipsilon.example.com/samlsp/metadata') + sp.set_role(SP_ROLE) + sp.add_certs(sign_cert) + sp.add_service(LOGOUT_SERVICE, lasso.SAML2_METADATA_BINDING_REDIRECT, + 'https://ipsilon.example.com/samlsp/logout') + sp.add_service(ASSERTION_SERVICE, lasso.SAML2_METADATA_BINDING_POST, + 'https://ipsilon.example.com/samlsp/postResponse') + md_file = os.path.join(tmpdir, 'metadata.xml') + sp.output(md_file) + with open(md_file) as fd: + text = fd.read() + print '===================== SP ====================' + print text + print '=============================================' + + finally: + shutil.rmtree(tmpdir) diff --git a/setup.py b/setup.py index 846698b..3de7faa 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,8 @@ setup( version = '0.1', license = 'GPLv3+', packages = ['ipsilon', 'ipsilon.admin', 'ipsilon.login', 'ipsilon.util', - 'ipsilon.providers', 'ipsilon.providers.saml2'], + 'ipsilon.providers', 'ipsilon.providers.saml2', + 'ipsilon.tools'], data_files = [('share/man/man7', ["man/ipsilon.7"]), ('share/doc/ipsilon', ['COPYING']), ('share/doc/ipsilon/examples', ['examples/ipsilon.conf',