Improvements to the XML based credential, still no verification
[sfa.git] / sfa / trust / credential.py
1 ##
2 # Implements SFA Credentials
3 #
4 # Credentials are layered on top of certificates, and are essentially a
5 # certificate that stores a tuple of parameters.
6 ##
7
8 ### $Id$
9 ### $URL$
10
11 import xmlrpclib
12 import random
13 import os
14
15
16 import xml.dom.minidom
17 from xml.dom.minidom import Document
18 from sfa.trust.credential_legacy import CredentialLegacy
19 from sfa.trust.certificate import Certificate
20 from sfa.trust.rights import *
21 from sfa.trust.gid import *
22 from sfa.util.faults import *
23 from sfa.util.sfalogging import *
24
25
26 # TODO:
27 # . Need to verify credentials
28 # . Need to add privileges (make PG and PL privs work together and add delegation per privelege instead of global)
29 # . Need to fix lifetime
30 # . Need to make sure delegation is fully supported
31 # . Need to test
32
33 signature_template = \
34 '''
35 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
36     <SignedInfo>
37       <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
38       <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
39       <Reference URI="#%s">
40       <Transforms>
41         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
42       </Transforms>
43       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
44       <DigestValue></DigestValue>
45       </Reference>
46     </SignedInfo>
47     <SignatureValue />
48       <KeyInfo>
49         <X509Data>
50           <X509SubjectName/>
51           <X509IssuerSerial/>
52           <X509Certificate/>
53         </X509Data>
54       <KeyValue />
55       </KeyInfo>
56     </Signature>
57 '''
58
59
60
61
62
63 ##
64 # Credential is a tuple:
65 #    (GIDCaller, GIDObject, LifeTime, Privileges, DelegateBit)
66 #
67 # These fields are encoded in one of two ways.  The legacy style places
68 # it in the subjectAltName of an X509 certificate.  The new credentials
69 # are placed in signed XML.
70
71
72 class Credential(object):
73     gidCaller = None
74     gidObject = None
75     lifeTime = None
76     privileges = None
77     delegate = False
78     issuer_privkey = None
79     issuer_gid = None
80     issuer_pubkey = None
81     parent = None
82     xml = None
83
84     ##
85     # Create a Credential object
86     #
87     # @param create If true, create a blank x509 certificate
88     # @param subject If subject!=None, create an x509 cert with the subject name
89     # @param string If string!=None, load the credential from the string
90     # @param filename If filename!=None, load the credential from the file
91
92     def __init__(self, create=False, subject=None, string=None, filename=None):
93
94         # Check if this is a legacy credential, translate it if so
95         if string or filename:
96             if string:                
97                 str = string
98             elif filename:
99                 str = file(filename).read()
100                 
101             if str.strip().startswith("-----"):
102                 self.translate_legacy(str)
103             else:
104                 self.xml = str
105                 # Let's not mess around with invalid credentials
106                 self.verify_chain()
107
108     ##
109     # Translate a legacy credential into a new one
110     #
111     # @param String of the legacy credential
112
113     def translate_legacy(self, str):
114         legacy = CredentialLegacy(False,string=str)
115         self.gidCaller = legacy.get_gid_caller()
116         self.gidObject = legacy.get_gid_object()
117         self.lifeTime = legacy.get_lifetime()
118         self.privileges = legacy.get_privileges()
119         self.delegate = legacy.get_delegate()
120
121     ##
122     # Need the issuer's private key and name
123     # @param key Keypair object containing the private key of the issuer
124     # @param gid GID of the issuing authority
125
126     def set_issuer_keys(self, privkey, gid):
127         self.issuer_privkey = privkey
128         self.issuer_gid = gid
129
130     def set_pubkey(self, pubkey):
131         self.issuer_pubkey = pubkey
132
133     def set_parent(self, cred):
134         self.parent = cred
135
136     ##
137     # set the GID of the caller
138     #
139     # @param gid GID object of the caller
140
141     def set_gid_caller(self, gid):
142         self.gidCaller = gid
143         # gid origin caller is the caller's gid by default
144         self.gidOriginCaller = gid
145
146     ##
147     # get the GID of the object
148
149     def get_gid_caller(self):
150         if not self.gidCaller:
151             self.decode()
152         return self.gidCaller
153
154     ##
155     # set the GID of the object
156     #
157     # @param gid GID object of the object
158
159     def set_gid_object(self, gid):
160         self.gidObject = gid
161
162     ##
163     # get the GID of the object
164
165     def get_gid_object(self):
166         if not self.gidObject:
167             self.decode()
168         return self.gidObject
169
170     ##
171     # set the lifetime of this credential
172     #
173     # @param lifetime lifetime of credential
174
175     def set_lifetime(self, lifeTime):
176         self.lifeTime = lifeTime
177
178     ##
179     # get the lifetime of the credential
180
181     def get_lifetime(self):
182         if not self.lifeTime:
183             self.decode()
184         return self.lifeTime
185
186     ##
187     # set the delegate bit
188     #
189     # @param delegate boolean (True or False)
190
191     def set_delegate(self, delegate):
192         self.delegate = delegate
193
194     ##
195     # get the delegate bit
196
197     def get_delegate(self):
198         if not self.delegate:
199             self.decode()
200         return self.delegate
201
202     ##
203     # set the privileges
204     #
205     # @param privs either a comma-separated list of privileges of a RightList object
206
207     def set_privileges(self, privs):
208         if isinstance(privs, str):
209             self.privileges = RightList(string = privs)
210         else:
211             self.privileges = privs
212
213     ##
214     # return the privileges as a RightList object
215
216     def get_privileges(self):
217         if not self.privileges:
218             self.decode()
219         return self.privileges
220
221     ##
222     # determine whether the credential allows a particular operation to be
223     # performed
224     #
225     # @param op_name string specifying name of operation ("lookup", "update", etc)
226
227     def can_perform(self, op_name):
228         rights = self.get_privileges()
229         if not rights:
230             return False
231         return rights.can_perform(op_name)
232
233     def append_sub(self, doc, parent, element, text):
234         ele = doc.createElement(element)
235         ele.appendChild(doc.createTextNode(text))
236         parent.appendChild(ele)
237
238     ##
239     # Encode the attributes of the credential into an XML string    
240     # This should be done immediately before signing the credential.    
241
242     def encode(self):
243
244         # Get information from the parent credential
245         if self.parent:
246             p_doc = xml.dom.minidom.parseString(self.parent)
247             p_signed_cred = p_doc.getElementsByTagName("signed-credential")[0]
248             p_cred = p_signed_cred.getElementsByTagName("credential")[0]               
249             p_signatures = p_signed_cred.getElementsByTagName("signatures")[0]
250             p_sigs = p_signatures.getElementsByTagName("Signature")
251
252         # Create the XML document
253         doc = Document()
254         signed_cred = doc.createElement("signed-credential")
255         doc.appendChild(signed_cred)  
256         
257
258         # Fill in the <credential> bit
259         refid = "ref1"
260         cred = doc.createElement("credential")
261         cred.setAttribute("xml:id", refid)
262         signed_cred.appendChild(cred)
263         self.append_sub(doc, cred, "type", "privilege")
264         self.append_sub(doc, cred, "serial", "8")
265         self.append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
266         self.append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
267         self.append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
268         self.append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
269         self.append_sub(doc, cred, "uuid", "")
270         self.append_sub(doc, cred, "expires", str(self.lifeTime))
271         priveleges = doc.createElement("privileges")
272         cred.appendChild(priveleges)
273
274         if self.privileges:
275             rights = self.privileges.save_to_string().split(",")
276             for right in rights:
277                 priv = doc.createElement("privelege")
278                 priv.append_sub(doc, priv, "name", right.strip())
279                 priv.append_sub(doc, priv, "can_delegate", str(self.delegate))
280                 priveleges.appendChild(priv)
281
282         # Add the parent credential if it exists
283         if self.parent:
284             cred.appendChild(doc.createElement("parent").appendChild(p_cred))         
285         
286
287         signed_cred.appendChild(cred)
288
289
290         # Fill in the <signature> bit
291         signatures = doc.createElement("signatures")
292
293         sz_sig = signature_template % (refid,refid)
294
295         sdoc = xml.dom.minidom.parseString(sz_sig)
296         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
297         signatures.appendChild(sig_ele)
298
299
300         # Add any parent signatures
301         if self.parent:
302             for sig in p_sigs:
303                 signatures.appendChild(sig)
304
305         signed_cred.appendChild(signatures)
306         # Get the finished product
307         self.xml = doc.toxml()
308         #print doc.toprettyxml()
309
310
311
312
313     def save_to_string(self):
314         if not self.xml:
315             self.encode()
316         return self.xml
317
318     def sign(self):
319         if not self.xml:
320             self.encode()
321         
322         # Call out to xmlsec1 to sign it
323         XMLSEC = '/usr/bin/xmlsec1'
324
325         filename = "/tmp/cred_%d" % random.randint(0,999999999)
326         f = open(filename, "w")
327         f.write(self.xml);
328         f.close()
329         signed = os.popen('/usr/bin/xmlsec1 --sign --node-id "%s" --privkey-pem %s,%s %s' \
330                  % ('ref1', self.issuer_privkey, self.issuer_gid, filename)).read()
331         os.remove(filename)
332
333         self.xml = signed
334
335     def getTextNode(self, element, subele):
336         sub = element.getElementsByTagName(subele)[0]
337         return sub.childNodes[0].nodeValue
338
339     ##
340     # Retrieve the attributes of the credential from the XML.
341     # This is automatically caleld by the various get_* methods of
342     # this class and should not need to be called explicitly.
343
344     def decode(self):
345         p_doc = xml.dom.minidom.parseString(self.xml)
346         p_signed_cred = p_doc.getElementsByTagName("signed-credential")[0]
347         p_cred = p_signed_cred.getElementsByTagName("credential")[0]
348         p_signatures = p_signed_cred.getElementsByTagName("signatures")[0]
349         p_sigs = p_signatures.getElementsByTagName("Signature")
350
351         self.lifeTime = self.getTextNode(p_cred, "expires")
352         self.gidCaller = GID(string=self.getTextNode(p_cred, "owner_gid"))
353         self.gidObject = GID(string=self.getTextNode(p_cred, "target_gid"))
354         privs = p_cred.getElementsByTagName("priveleges")[0]
355         sz_privs = ''
356         delegates = []
357         for priv in privs.getElementsByTagName("privelege"):
358             sz_privs += self.getTextNode(priv, "name")
359             sz_privs += ", "
360             delegates.append(self.getTextNode(priv, "can_delegate"))
361
362         # Can we delegate?
363         delegate = False
364         if "false" not in delegates:
365             self.delegate = True
366
367         # Make the rights list
368         sz_privs.rstrip(", ")
369         self.priveleges = RightList(string=sz_privs)
370         self.delegate
371             
372         
373 ##     ##
374 ##     # Retrieve the attributes of the credential from the alt-subject-name field
375 ##     # of the X509 certificate. This is automatically done by the various
376 ##     # get_* methods of this class and should not need to be called explicitly.
377
378 ##     def decode(self):
379 ##         data = self.get_data().lstrip('URI:http://')
380         
381 ##         if data:
382 ##             dict = xmlrpclib.loads(data)[0][0]
383 ##         else:
384 ##             dict = {}
385
386 ##         self.lifeTime = dict.get("lifeTime", None)
387 ##         self.delegate = dict.get("delegate", None)
388
389 ##         privStr = dict.get("privileges", None)
390 ##         if privStr:
391 ##             self.privileges = RightList(string = privStr)
392 ##         else:
393 ##             self.privileges = None
394
395 ##         gidCallerStr = dict.get("gidCaller", None)
396 ##         if gidCallerStr:
397 ##             self.gidCaller = GID(string=gidCallerStr)
398 ##         else:
399 ##             self.gidCaller = None
400
401 ##         gidObjectStr = dict.get("gidObject", None)
402 ##         if gidObjectStr:
403 ##             self.gidObject = GID(string=gidObjectStr)
404 ##         else:
405 ##             self.gidObject = None
406
407
408     ##
409     # Verify for the initial credential:
410     # 1. That the signature is valid
411     # 2. That the xml signer's certificate matches the object's certificate
412     # 3. That the urns match those in the gids
413     #
414     # Verify for the delegated credentials:
415     # 1. That the signature is valid
416     
417     # 4. 
418     # 3. That the object's certificate stays the s
419     # 2. That the GID of the 
420
421     #def verify(self, trusted_certs = None):
422         
423
424
425     ##
426     # Verify that a chain of credentials is valid (see cert.py:verify). In
427     # addition to the checks for ordinary certificates, verification also
428     # ensures that the delegate bit was set by each parent in the chain. If
429     # a delegate bit was not set, then an exception is thrown.
430     #
431     # Each credential must be a subset of the rights of the parent.
432
433     def verify_chain(self, trusted_certs = None):
434         # do the normal certificate verification stuff
435         Certificate.verify_chain(self, trusted_certs)
436
437         if self.parent:
438             # make sure the parent delegated rights to the child
439             if not self.parent.get_delegate():
440                 raise MissingDelegateBit(self.parent.get_subject())
441
442             # make sure the rights given to the child are a subset of the
443             # parents rights
444             if not self.parent.get_privileges().is_superset(self.get_privileges()):
445                 raise ChildRightsNotSubsetOfParent(self.get_subject() 
446                                                    + " " + self.parent.get_privileges().save_to_string()
447                                                    + " " + self.get_privileges().save_to_string())
448
449         return
450
451     ##
452     # Dump the contents of a credential to stdout in human-readable format
453     #
454     # @param dump_parents If true, also dump the parent certificates
455
456     def dump(self, dump_parents=False):
457         print "CREDENTIAL", self.get_subject()
458
459         print "      privs:", self.get_privileges().save_to_string()
460
461         print "  gidCaller:"
462         gidCaller = self.get_gid_caller()
463         if gidCaller:
464             gidCaller.dump(8, dump_parents)
465
466         print "  gidObject:"
467         gidObject = self.get_gid_object()
468         if gidObject:
469             gidObject.dump(8, dump_parents)
470
471         print "   delegate:", self.get_delegate()
472
473         if self.parent and dump_parents:
474            print "PARENT",
475            self.parent.dump(dump_parents)
476