f918a44184673b700cede3901008d60e9f0c9412
[cascardo/ipsilon.git] / ipsilon / tools / saml2metadata.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import datetime
21 from ipsilon.tools.certs import Certificate
22 from lxml import etree
23 import lasso
24
25
26 SAML2_NAMEID_MAP = {
27     'email': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL,
28     'encrypted': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED,
29     'entity': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENTITY,
30     'kerberos': lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS,
31     'persistent': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
32     'transient': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT,
33     'unspecified': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED,
34     'windows': lasso.SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS,
35     'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509,
36 }
37
38 SAML2_SERVICE_MAP = {
39     'sso-post': ('SingleSignOnService',
40                  lasso.SAML2_METADATA_BINDING_POST),
41     'sso-redirect': ('SingleSignOnService',
42                      lasso.SAML2_METADATA_BINDING_REDIRECT),
43     'logout-redirect': ('SingleLogoutService',
44                         lasso.SAML2_METADATA_BINDING_REDIRECT),
45     'response-post': ('AssertionConsumerService',
46                       lasso.SAML2_METADATA_BINDING_POST)
47 }
48
49 EDESC = '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF
50 NSMAP = {
51     'md': lasso.SAML2_METADATA_HREF,
52     'ds': lasso.DS_HREF
53 }
54
55 IDPDESC = 'IDPSSODescriptor'
56 SPDESC = 'SPSSODescriptor'
57
58 IDP_ROLE = 'idp'
59 SP_ROLE = 'sp'
60
61
62 # Expire metadata weekly by default
63 MIN_EXP_DEFAULT = 7
64
65
66 def mdElement(_parent, _tag, **kwargs):
67     tag = '{%s}%s' % (lasso.SAML2_METADATA_HREF, _tag)
68     return etree.SubElement(_parent, tag, **kwargs)
69
70
71 def dsElement(_parent, _tag, **kwargs):
72     tag = '{%s}%s' % (lasso.DS_HREF, _tag)
73     return etree.SubElement(_parent, tag, **kwargs)
74
75
76 class Metadata(object):
77
78     def __init__(self, role=None, expiration=None):
79         self.root = etree.Element(EDESC, nsmap=NSMAP)
80         self.entityid = None
81         self.role = None
82         self.set_role(role)
83         self.set_expiration(expiration)
84
85     def set_entity_id(self, url):
86         self.entityid = url
87         self.root.set('entityID', url)
88
89     def set_role(self, role):
90         if role is None:
91             return
92         elif role == IDP_ROLE:
93             description = IDPDESC
94         elif role == SP_ROLE:
95             description = SPDESC
96         else:
97             raise ValueError('invalid role: %s' % role)
98         self.role = mdElement(self.root, description)
99         self.role.set('protocolSupportEnumeration', lasso.SAML2_PROTOCOL_HREF)
100         return self.role
101
102     def set_expiration(self, exp):
103         if exp is None:
104             self.root.set('cacheDuration', "P%dD" % (MIN_EXP_DEFAULT))
105             return
106         elif isinstance(exp, datetime.date):
107             d = datetime.datetime.combine(exp, datetime.date.min.time())
108         elif isinstance(exp, datetime.datetime):
109             d = exp
110         elif isinstance(exp, datetime.timedelta):
111             d = datetime.datetime.now() + exp
112         else:
113             raise TypeError('Invalid expiration date type')
114
115         self.root.set('validUntil', d.isoformat())
116
117     def add_cert(self, certdata, use):
118         desc = mdElement(self.role, 'KeyDescriptor')
119         desc.set('use', use)
120         info = dsElement(desc, 'KeyInfo')
121         data = dsElement(info, 'X509Data')
122         cert = dsElement(data, 'X509Certificate')
123         cert.text = certdata
124
125     def add_certs(self, signcert=None, enccert=None):
126         if signcert:
127             self.add_cert(signcert.get_cert(), 'signing')
128         if enccert:
129             self.add_cert(enccert.get_cert(), 'encryption')
130
131     def add_service(self, service, location, **kwargs):
132         svc = mdElement(self.role, service[0])
133         svc.set('Binding', service[1])
134         svc.set('Location', location)
135         for key, value in kwargs.iteritems():
136             svc.set(key, value)
137
138     def add_allowed_name_format(self, name_format):
139         nameidfmt = mdElement(self.role, 'NameIDFormat')
140         nameidfmt.text = name_format
141
142     def output(self, path=None):
143         data = etree.tostring(self.root, xml_declaration=True,
144                               encoding='UTF-8', pretty_print=True)
145         if path is None:
146             return data
147         else:
148             with open(path, 'w') as f:
149                 f.write(data)
150
151
152 if __name__ == '__main__':
153     import tempfile
154     import shutil
155     import os
156
157     tmpdir = tempfile.mkdtemp()
158
159     try:
160         # Test IDP generation
161         sign_cert = Certificate(tmpdir)
162         sign_cert.generate('idp-signing-cert', 'idp.ipsilon.example.com')
163         enc_cert = Certificate(tmpdir)
164         enc_cert.generate('idp-encryption-cert', 'idp.ipsilon.example.com')
165         idp = Metadata()
166         idp.set_entity_id('https://ipsilon.example.com/idp/metadata')
167         idp.set_role(IDP_ROLE)
168         idp.add_certs(sign_cert, enc_cert)
169         idp.add_service(SAML2_SERVICE_MAP['sso-post'],
170                         'https://ipsilon.example.com/idp/saml2/POST')
171         idp.add_service(SAML2_SERVICE_MAP['sso-redirect'],
172                         'https://ipsilon.example.com/idp/saml2/Redirect')
173         for k in SAML2_NAMEID_MAP:
174             idp.add_allowed_name_format(SAML2_NAMEID_MAP[k])
175         md_file = os.path.join(tmpdir, 'metadata.xml')
176         idp.output(md_file)
177         with open(md_file) as fd:
178             text = fd.read()
179         print '==================== IDP ===================='
180         print text
181         print '============================================='
182
183         # Test SP generation
184         sign_cert = Certificate(tmpdir)
185         sign_cert.generate('sp-signing-cert', 'sp.ipsilon.example.com')
186         sp = Metadata()
187         sp.set_entity_id('https://ipsilon.example.com/samlsp/metadata')
188         sp.set_role(SP_ROLE)
189         sp.add_certs(sign_cert)
190         sp.add_service(SAML2_SERVICE_MAP['logout-redirect'],
191                        'https://ipsilon.example.com/samlsp/logout')
192         sp.add_service(SAML2_SERVICE_MAP['response-post'],
193                        'https://ipsilon.example.com/samlsp/postResponse')
194         md_file = os.path.join(tmpdir, 'metadata.xml')
195         sp.output(md_file)
196         with open(md_file) as fd:
197             text = fd.read()
198         print '===================== SP ===================='
199         print text
200         print '============================================='
201
202     finally:
203         shutil.rmtree(tmpdir)