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