Closer on credentials
[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 import datetime
15
16 #import xml.dom.minidom
17 from xml.dom.minidom import Document, parseString
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 logger
24
25
26 # TODO:
27 # . SFA is using set_parent to do chaining, but that's no longer necessary
28 #   with the new credentials. fix.
29 #    . This probably requires setting up some sort of CA hierarchy.
30 # . Make sure that the creds pass xml verification (probably need some reordering)
31 # . Need to implement full verification (parent signatures etc).
32 # . remove verify_chain
33 # . make delegation per privilege instead of global
34 # . make privs match between PG and PL
35 # . Need to test
36
37 signature_template = \
38 '''
39 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
40     <SignedInfo>
41       <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
42       <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
43       <Reference URI="#%s">
44       <Transforms>
45         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
46       </Transforms>
47       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
48       <DigestValue></DigestValue>
49       </Reference>
50     </SignedInfo>
51     <SignatureValue />
52       <KeyInfo>
53         <X509Data>
54           <X509SubjectName/>
55           <X509IssuerSerial/>
56           <X509Certificate/>
57         </X509Data>
58       <KeyValue />
59       </KeyInfo>
60     </Signature>
61 '''
62
63
64
65 ##
66 # Credential is a tuple:
67 #    (GIDCaller, GIDObject, Expiration (in UTC time), Privileges, DelegateBit)
68 #
69 # These fields are encoded in one of two ways.  The legacy style places
70 # it in the subjectAltName of an X509 certificate.  The new credentials
71 # are placed in signed XML.
72
73
74 class Credential(object):
75     gidCaller = None
76     gidObject = None
77     expiration = None
78     privileges = None
79     delegate = False
80     issuer_privkey = None
81     issuer_gid = None
82     issuer_pubkey = None
83     parent_xml = None
84     signatures = []
85     xml = None
86     refid = None
87     
88     ##
89     # Create a Credential object
90     #
91     # @param create If true, create a blank x509 certificate
92     # @param subject If subject!=None, create an x509 cert with the subject name
93     # @param string If string!=None, load the credential from the string
94     # @param filename If filename!=None, load the credential from the file
95
96     def __init__(self, create=False, subject=None, string=None, filename=None):
97
98         # Check if this is a legacy credential, translate it if so
99         if string or filename:
100             if string:                
101                 str = string
102             elif filename:
103                 str = file(filename).read()
104                 
105             if str.strip().startswith("-----"):
106                 self.translate_legacy(str)
107             else:
108                 self.xml = str
109                 self.decode()
110                 
111     ##
112     # Translate a legacy credential into a new one
113     #
114     # @param String of the legacy credential
115
116     def translate_legacy(self, str):
117         legacy = CredentialLegacy(False,string=str)
118         self.gidCaller = legacy.get_gid_caller()
119         self.gidObject = legacy.get_gid_object()
120         lifetime = legacy.get_lifetime()
121         if not lifetime:
122             self.set_lifetime(3600)
123         else:
124             self.set_lifetime(int(lifetime))
125         self.lifeTime = legacy.get_lifetime()
126         self.set_privileges(legacy.get_privileges())
127         self.delegate = legacy.get_delegate()
128
129     ##
130     # Need the issuer's private key and name
131     # @param key Keypair object containing the private key of the issuer
132     # @param gid GID of the issuing authority
133
134     def set_issuer_keys(self, privkey, gid):
135         self.issuer_privkey = privkey
136         self.issuer_gid = gid
137
138     #def set_issuer(self, issuer):
139     #    issuer = issuer
140
141     #def set_subject(self, subject):
142     #    subject = subject
143         
144     #def set_pubkey(self, pubkey):
145     #    self.issuer_pubkey = pubkey
146
147
148     ##
149     # Store the parent's credential in self.parent_xml
150     # Store the parent's signatures in self.signatures
151     # Update this credential's refid
152     def set_parent(self, cred):
153         if not cred.xml:
154             cred.encode()
155
156         logger.info("Setting parent for %s to %s" % (self.gidCaller.get_urn(), cred.gidCaller.get_urn()))
157         doc = parseString(cred.xml)
158         signed = doc.getElementsByTagName("signed-credential")[0]
159         cred = signed.getElementsByTagName("credential")[0]
160         signatures = signed.getElementsByTagName("signatures")[0]
161         sigs = signatures.getElementsByTagName("Signature")
162
163         self.parent_xml = cred.toxml()
164         self.signatures = []
165         for sig in sigs:
166             self.signatures.append(sig.toxml())
167
168         self.updateRefID()
169         
170
171     ##
172     # set the GID of the caller
173     #
174     # @param gid GID object of the caller
175
176     def set_gid_caller(self, gid):
177         self.gidCaller = gid
178         # gid origin caller is the caller's gid by default
179         self.gidOriginCaller = gid
180
181     ##
182     # get the GID of the object
183
184     def get_gid_caller(self):
185         if not self.gidCaller:
186             self.decode()
187         return self.gidCaller
188
189     ##
190     # set the GID of the object
191     #
192     # @param gid GID object of the object
193
194     def set_gid_object(self, gid):
195         self.gidObject = gid
196
197     ##
198     # get the GID of the object
199
200     def get_gid_object(self):
201         if not self.gidObject:
202             self.decode()
203         return self.gidObject
204
205     ##
206     # set the lifetime of this credential
207     #
208     # @param lifetime lifetime of credential
209     # . if lifeTime is a datetime object, it is used for the expiration time
210     # . if lifeTime is an integer value, it is considered the number of minutes
211     #   remaining before expiration
212
213     def set_lifetime(self, lifeTime):
214         if isinstance(lifeTime, int):
215             self.expiration = datetime.timedelta(seconds=lifeTime*60) + datetime.datetime.utcnow()
216         else:
217             self.expiration = lifeTime
218
219     ##
220     # get the lifetime of the credential (in minutes)
221
222     def get_lifetime(self):
223         if not self.lifeTime:
224             self.decode()
225         return self.expiration
226
227     ##
228     # set the delegate bit
229     #
230     # @param delegate boolean (True or False)
231
232     def set_delegate(self, delegate):
233         self.delegate = delegate
234
235     ##
236     # get the delegate bit
237
238     def get_delegate(self):
239         if not self.delegate:
240             self.decode()
241         return self.delegate
242
243     ##
244     # set the privileges
245     #
246     # @param privs either a comma-separated list of privileges of a RightList object
247
248     def set_privileges(self, privs):
249         if isinstance(privs, str):
250             self.privileges = RightList(string = privs)
251         else:
252             self.privileges = privs
253
254     ##
255     # return the privileges as a RightList object
256
257     def get_privileges(self):
258         if not self.privileges:
259             self.decode()
260         return self.privileges
261
262     ##
263     # determine whether the credential allows a particular operation to be
264     # performed
265     #
266     # @param op_name string specifying name of operation ("lookup", "update", etc)
267
268     def can_perform(self, op_name):
269         rights = self.get_privileges()
270         
271         if not rights:
272             return False
273
274         return rights.can_perform(op_name)
275
276     def append_sub(self, doc, parent, element, text):
277         ele = doc.createElement(element)
278         ele.appendChild(doc.createTextNode(text))
279         parent.appendChild(ele)
280
281     ##
282     # Encode the attributes of the credential into an XML string    
283     # This should be done immediately before signing the credential.    
284
285     def encode(self):
286         p_sigs = None
287
288         # Create the XML document
289         doc = Document()
290         signed_cred = doc.createElement("signed-credential")
291         doc.appendChild(signed_cred)  
292         
293         # Fill in the <credential> bit        
294         cred = doc.createElement("credential")
295         cred.setAttribute("xml:id", self.get_refid())
296         signed_cred.appendChild(cred)
297         self.append_sub(doc, cred, "type", "privilege")
298         self.append_sub(doc, cred, "serial", "8")
299         self.append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
300         self.append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
301         self.append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
302         self.append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
303         self.append_sub(doc, cred, "uuid", "")
304         if  not self.expiration:
305             self.set_lifetime(3600)
306         self.expiration = self.expiration.replace(microsecond=0)
307         self.append_sub(doc, cred, "expires", self.expiration.isoformat())
308         privileges = doc.createElement("privileges")
309         cred.appendChild(privileges)
310
311         if self.privileges:
312             rights = self.privileges.save_to_string().split(",")
313             for right in rights:
314                 priv = doc.createElement("privilege")
315                 self.append_sub(doc, priv, "name", right.strip())
316                 self.append_sub(doc, priv, "can_delegate", str(self.delegate))
317                 privileges.appendChild(priv)
318
319         # Add the parent credential if it exists
320         if self.parent_xml:
321             sdoc = parseString(self.parent_xml)
322             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
323             p = doc.createElement("parent")
324             p.appendChild(p_cred)
325             cred.appendChild(p)
326
327
328         # Create the <signatures> tag
329         signatures = doc.createElement("signatures")
330         signed_cred.appendChild(signatures)
331
332         # Add any parent signatures
333         if self.parent_xml:
334             for sig in self.signatures:
335                 sdoc = parseString(sig)
336                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
337                 signatures.appendChild(ele)
338
339         # Get the finished product
340         self.xml = doc.toxml()
341         #print doc.toprettyxml()
342         #self.sign()
343
344
345     def save_to_random_tmp_file(self):
346         filename = "/tmp/cred_%d" % random.randint(0,999999999)
347         self.save_to_file(filename)
348         return filename
349     
350     def save_to_file(self, filename, save_parents=True):
351         if not self.xml:
352             self.encode()
353         f = open(filename, "w")
354         f.write(self.xml)
355         f.close()
356
357     def save_to_string(self, save_parents=True):
358         if not self.xml:
359             self.encode()
360         return self.xml
361
362     def get_refid(self):
363         if not self.refid:
364             self.refid = 'ref0'
365         return self.refid
366
367     def set_refid(self, rid):
368         self.refid = rid
369
370     ##
371     # Figure out what refids exist, and update this credential's id
372     # so that it doesn't clobber the others.  Returns the refids of
373     # the parents.
374     
375     def updateRefID(self):
376         if not self.parent_xml:
377             self.set_refid('ref0')
378             return []
379         
380         refs = []
381         
382         next_xml = self.parent_xml
383
384         logger.info("instance--")
385
386         while next_xml:
387             doc = parseString(next_xml)
388             cred = doc.getElementsByTagName("credential")[0]
389             refid = cred.getAttribute("xml:id")
390             logger.info("Found refid %s" % refid)
391             assert(refid not in refs)
392             refs.append(refid)
393
394             parent = doc.getElementsByTagName("parent")
395             if len(parent) > 0:
396                 next_xml = parent[0].getElementsByTagName("credential")[0].toxml()
397             else:
398                 next_xml = None
399
400         # Find a unique refid for this credential
401         rid = self.get_refid()
402         while rid in refs:
403             val = int(rid[3:])
404             rid = "ref%d" % (val + 1)
405
406         # Set the new refid
407         self.set_refid(rid)
408
409         # Return the set of parent credential ref ids
410         return refs
411
412     
413
414     def sign(self):
415         if not self.issuer_privkey or not self.issuer_gid:
416             return
417         
418         if not self.xml:
419             self.encode()
420
421         doc = parseString(self.xml)
422         sigs = doc.getElementsByTagName("signatures")[0]
423         # Create the signature template
424         refid = self.get_refid()
425         sz_sig = signature_template % (refid,refid)
426         sdoc = parseString(sz_sig)
427         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
428         sigs.appendChild(sig_ele)
429
430         self.xml = doc.toxml()
431
432         
433         # Call out to xmlsec1 to sign it
434         ref = 'Sig_%s' % self.get_refid()
435         filename = self.save_to_random_tmp_file()
436         signed = os.popen('/usr/bin/xmlsec1 --sign --node-id "%s" --privkey-pem %s,%s %s' \
437                  % (ref, self.issuer_privkey, self.issuer_gid, filename)).read()
438         os.remove(filename)
439
440
441         self.xml = signed
442
443     def getTextNode(self, element, subele):
444         sub = element.getElementsByTagName(subele)[0]
445         if len(sub.childNodes) > 0:            
446             return sub.childNodes[0].nodeValue
447         else:
448             return None
449         
450     ##
451     # Retrieve the attributes of the credential from the XML.
452     # This is automatically caleld by the various get_* methods of
453     # this class and should not need to be called explicitly.
454
455     def decode(self):
456         doc = parseString(self.xml)
457         signed_cred = doc.getElementsByTagName("signed-credential")[0]
458         cred = signed_cred.getElementsByTagName("credential")[0]
459         signatures = signed_cred.getElementsByTagName("signatures")[0]
460         sigs = signatures.getElementsByTagName("Signature")
461
462
463         self.max_refid = int(cred.getAttribute("xml:id")[3:])
464         sz_expires = self.getTextNode(cred, "expires")
465         if sz_expires != '':
466             self.expiration = datetime.datetime.strptime(sz_expires, '%Y-%m-%dT%H:%M:%S')            
467         self.lifeTime = self.getTextNode(cred, "expires")
468         self.gidCaller = GID(string=self.getTextNode(cred, "owner_gid"))
469         self.gidObject = GID(string=self.getTextNode(cred, "target_gid"))
470         privs = cred.getElementsByTagName("privileges")[0]
471         sz_privs = ''
472         delegates = []
473         for priv in privs.getElementsByTagName("privilege"):
474             sz_privs += self.getTextNode(priv, "name")
475             sz_privs += ","
476             delegates.append(self.getTextNode(priv, "can_delegate"))
477
478         # Can we delegate?
479         delegate = False
480         if "false" not in delegates:
481             self.delegate = True
482
483         # Make the rights list
484         sz_privs.rstrip(", ")
485         self.privileges = RightList(string=sz_privs)
486         self.delegate
487
488         # Is there a parent?
489         parent = cred.getElementsByTagName("parent")
490         if len(parent) > 0:
491             self.parent_xml = self.getTextNode(cred, "parent")
492             self.updateRefID()
493
494         # Get the signatures
495         for sig in sigs:
496             self.signatures.append(sig.toxml())
497             
498     ##
499     # For a simple credential (no delegation) verify..
500     # . That the signature is valid for the credential's xml:id
501     # . That the signer's pub key matches the pub key of the target (object)
502     # . That the target/owner urns match those in the gids
503     #
504     # @param trusted_certs: The certificates of trusted CA certificates
505     
506     def verify(self, trusted_certs):
507         if not self.xml:
508             self.decode()        
509
510         # Verify the sigatures
511         filename = self.save_to_random_tmp_file()
512         cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
513
514         ref = "Sig_%s" % self.get_refid()
515         verified = os.popen('/usr/bin/xmlsec1 --verify --node-id "%s" %s %s 2>&1' \
516                             % (ref, cert_args, filename)).read()
517
518         if not verified.strip().startswith("OK"):
519             raise CredentialNotVerifiable("xmlsec1 error: " + verified)
520
521         os.remove(filename)
522
523     ##
524     # Verify that a chain of credentials is valid (see cert.py:verify). In
525     # addition to the checks for ordinary certificates, verification also
526     # ensures that the delegate bit was set by each parent in the chain. If
527     # a delegate bit was not set, then an exception is thrown.
528     #
529     # Each credential must be a subset of the rights of the parent.
530
531     def verify_chain(self, trusted_certs):
532         return
533
534  ##    def verify_chain(self, trusted_certs = None):
535 ##         # do the normal certificate verification stuff
536 ##         Certificate.verify_chain(self, trusted_certs)
537
538 ##         if self.parent:
539 ##             # make sure the parent delegated rights to the child
540 ##             if not self.parent.get_delegate():
541 ##                 raise MissingDelegateBit(self.parent.get_subject())
542
543 ##             # make sure the rights given to the child are a subset of the
544 ##             # parents rights
545 ##             if not self.parent.get_privileges().is_superset(self.get_privileges()):
546 ##                 raise ChildRightsNotSubsetOfParent(self.get_subject() 
547 ##                                                    + " " + self.parent.get_privileges().save_to_string()
548 ##                                                    + " " + self.get_privileges().save_to_string())
549
550 ##         return
551
552     ##
553     # Dump the contents of a credential to stdout in human-readable format
554     #
555     # @param dump_parents If true, also dump the parent certificates
556
557     def dump(self, dump_parents=False):
558         print "CREDENTIAL", self.get_subject()
559
560         print "      privs:", self.get_privileges().save_to_string()
561
562         print "  gidCaller:"
563         gidCaller = self.get_gid_caller()
564         if gidCaller:
565             gidCaller.dump(8, dump_parents)
566
567         print "  gidObject:"
568         gidObject = self.get_gid_object()
569         if gidObject:
570             gidObject.dump(8, dump_parents)
571
572         print "   delegate:", self.get_delegate()
573
574         if self.parent_xml and dump_parents:
575            print "PARENT",
576            #self.parent.dump(dump_parents)
577