203 lines
8.6 KiB
Python
203 lines
8.6 KiB
Python
# w.c.s. - web application for online forms
|
|
# Copyright (C) 2005-2010 Entr'ouvert
|
|
#
|
|
# 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 2 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 <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
|
|
from . import force_str, x509utils
|
|
|
|
def bool2xs(boolean):
|
|
'''Convert a boolean value to XSchema boolean representation'''
|
|
if boolean is True:
|
|
return 'true'
|
|
if boolean is False:
|
|
return 'false'
|
|
raise TypeError()
|
|
|
|
|
|
class Metadata(object):
|
|
__endpoints = {
|
|
'slo' : 'singleLogout',
|
|
'mni' : 'manageNameId',
|
|
'ac' : 'assertionConsumer' }
|
|
|
|
def __init__(self, publisher, provider_id, config):
|
|
self.publisher = publisher
|
|
self.provider_id = provider_id
|
|
self.config = config
|
|
|
|
def get_key_descriptor(self, keytype, key):
|
|
'''Format key as an XML Dsig KeyNode content'''
|
|
if keytype:
|
|
prologue = ' <KeyDescriptor use="%s">' % keytype
|
|
else:
|
|
prologue = ' <KeyDescriptor>'
|
|
if key and 'CERTIF' in key:
|
|
naked = x509utils.decapsulate_pem_file(key)
|
|
return prologue + '''
|
|
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
<ds:X509Data><ds:X509Certificate>%s</ds:X509Certificate></ds:X509Data>
|
|
</ds:KeyInfo>
|
|
</KeyDescriptor>
|
|
''' % naked
|
|
# FIXME: generate proper RSAKeyValue, but wait for support in Lasso
|
|
elif key and 'KEY' in key:
|
|
naked = x509utils.get_xmldsig_rsa_key_value(key)
|
|
return prologue + '''
|
|
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
%s
|
|
</ds:KeyInfo>
|
|
</KeyDescriptor>
|
|
''' % naked
|
|
else:
|
|
return ''
|
|
|
|
|
|
def get_key_descriptors(self, signing_pem_key, encryption_pem_key):
|
|
signing_pem_key, encryption_pem_key = \
|
|
self.get_new_or_old_keys(signing_pem_key, encryption_pem_key)
|
|
sp_key = {
|
|
'signing': self.get_key_descriptor('signing', signing_pem_key),
|
|
'encryption': self.get_key_descriptor('encryption', encryption_pem_key) }
|
|
if not sp_key['signing'] and sp_key['encryption']:
|
|
sp_key = {
|
|
'signing': '',
|
|
'encryption': self.get_key_descriptor('', encryption_pem_key) }
|
|
if sp_key['signing'] and not sp_key['encryption']:
|
|
sp_key = {
|
|
'signing': self.get_key_descriptor('', signing_pem_key),
|
|
'encryption': ''
|
|
}
|
|
return sp_key
|
|
|
|
def get_new_or_old_keys(self, signing_pem_key, encryption_pem_key):
|
|
'''Return new or earlier version of PEM keys'''
|
|
dir = self.publisher.app_dir
|
|
if not signing_pem_key and self.config.get('publickey'):
|
|
signing_pem_key = open(os.path.join(dir, 'public-key.pem'), 'r').read()
|
|
if not encryption_pem_key and self.config.get('encryption_publickey'):
|
|
encryption_pem_key = open(os.path.join(dir, 'encryption-public-key.pem'), 'r').read()
|
|
return (signing_pem_key, encryption_pem_key)
|
|
|
|
def get_spsso_descriptor(self, signing_pem_key, encryption_pem_key,
|
|
endpoints):
|
|
signing_pem_key, encryption_pem_key = \
|
|
self.get_new_or_old_keys(signing_pem_key, encryption_pem_key)
|
|
|
|
authnrequestsigned = bool2xs(self.config.get('authn-request-signed', True))
|
|
wantassertionsigned = bool2xs(self.config.get('want-assertion-signed', True))
|
|
prologue = ''' <SPSSODescriptor
|
|
AuthnRequestsSigned="%s" WantAssertionsSigned="%s"
|
|
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
''' % (authnrequestsigned, wantassertionsigned)
|
|
sp_key = self.get_key_descriptors(signing_pem_key, encryption_pem_key)
|
|
config = {}
|
|
config.update(self.config)
|
|
config.update(self.__endpoints)
|
|
config.update(endpoints)
|
|
return prologue + sp_key['signing'] + sp_key['encryption'] + '''
|
|
<SingleLogoutService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
Location="%(saml2_base_url)s/%(slo)s"
|
|
ResponseLocation="%(saml2_base_url)s/%(slo)sReturn" />
|
|
<SingleLogoutService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
|
Location="%(saml2_base_url)s/%(slo)sSOAP" />
|
|
<ManageNameIDService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
Location="%(saml2_base_url)s/%(mni)s"
|
|
ResponseLocation="%(saml2_base_url)s/%(mni)sReturn" />
|
|
<ManageNameIDService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
|
Location="%(saml2_base_url)s/%(mni)sSOAP" />
|
|
<AssertionConsumerService isDefault="true" index="0"
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
|
|
Location="%(saml2_base_url)s/%(ac)sArtifact" />
|
|
<AssertionConsumerService index="1"
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
|
Location="%(saml2_base_url)s/%(ac)sPost" />
|
|
<AssertionConsumerService index="2"
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
|
|
Location="%(saml2_base_url)s/%(ac)sSOAP" />
|
|
<AssertionConsumerService index="3"
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
Location="%(saml2_base_url)s/%(ac)sRedirect" />
|
|
</SPSSODescriptor>''' % config
|
|
|
|
def get_idpsso_descriptor(self, signing_pem_key, encryption_pem_key):
|
|
signing_pem_key, encryption_pem_key = \
|
|
self.get_new_or_old_keys(signing_pem_key, encryption_pem_key)
|
|
|
|
idp_key = self.get_key_descriptors(signing_pem_key, encryption_pem_key)
|
|
idp_head = """
|
|
<IDPSSODescriptor
|
|
WantAuthnRequestsSigned="%s"
|
|
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
""" % bool2xs(self.config.get('want-authn-request-signed', True))
|
|
idp_body = """
|
|
<ArtifactResolutionService isDefault="true" index="0"
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
|
Location="%(saml2_base_soap_url)s/artifact" />
|
|
<SingleLogoutService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
Location="%(saml2_base_url)s/singleLogout"
|
|
ResponseLocation="%(saml2_base_url)s/singleLogoutReturn" />
|
|
<SingleLogoutService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
|
Location="%(saml2_base_soap_url)s/singleLogoutSOAP" />
|
|
<ManageNameIDService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
Location="%(saml2_base_url)s/manageNameId"
|
|
ResponseLocation="%(saml2_base_url)s/manageNameIdReturn" />
|
|
<ManageNameIDService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
|
Location="%(saml2_base_soap_url)s/manageNameIdSOAP" />
|
|
<SingleSignOnService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
Location="%(saml2_base_url)s/singleSignOn" />
|
|
<SingleSignOnService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
|
Location="%(saml2_base_url)s/singleSignOn" />
|
|
<SingleSignOnService
|
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
|
Location="%(saml2_base_soap_url)s/singleSignOnSOAP" />
|
|
</IDPSSODescriptor>""" % self.config
|
|
return idp_head + idp_key['signing'] + idp_key['encryption'] + idp_body
|
|
|
|
def get_saml2_metadata(self, signing_pem_key = '', encryption_pem_key = '', do_idp = False, do_sp = False, endpoints = {}):
|
|
prologue = '''<?xml version="1.0"?>
|
|
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
|
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
|
|
entityID="%s">''' % self.provider_id
|
|
|
|
sp_descriptor = ''
|
|
if do_sp:
|
|
sp_descriptor = self.get_spsso_descriptor(signing_pem_key, encryption_pem_key, endpoints)
|
|
|
|
idp_descriptor = ''
|
|
if do_idp:
|
|
idp_descriptor = self.get_idpsso_descriptor(signing_pem_key, encryption_pem_key)
|
|
|
|
orga = ''
|
|
if self.config.get('organization_name'):
|
|
orga = '''<Organization>
|
|
<OrganizationName xml:lang="en">%s</OrganizationName>
|
|
</Organization>''' % force_str(self.config['organization_name'])
|
|
|
|
epilogue = '</EntityDescriptor>'
|
|
|
|
return '\n'.join([prologue, sp_descriptor, idp_descriptor, orga, epilogue])
|