71325b4fd3420f091a334e74629ae7b2b07291f9
[sfa.git] / sfa / trust / credential.py
1 ##
2 # Implements SFA Credentials
3 #
4 # Credentials are signed XML files that assign a subject gid privileges to an object gid
5 ##
6
7 ### $Id$
8 ### $URL$
9
10 import os
11 import datetime
12 from random import randint
13 from xml.dom.minidom import Document, parseString
14
15 from sfa.trust.credential_legacy import CredentialLegacy
16 from sfa.trust.rights import *
17 from sfa.trust.gid import *
18 from sfa.util.faults import *
19
20 from sfa.util.sfalogging import logger
21
22
23
24 # Two years, in minutes 
25 DEFAULT_CREDENTIAL_LIFETIME = 1051200
26
27
28 # TODO:
29 # . make privs match between PG and PL
30 # . Need to add support for other types of credentials, e.g. tickets
31
32
33
34 signature_template = \
35 '''
36 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
37     <SignedInfo>
38       <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
39       <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
40       <Reference URI="#%s">
41       <Transforms>
42         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
43       </Transforms>
44       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
45       <DigestValue></DigestValue>
46       </Reference>
47     </SignedInfo>
48     <SignatureValue />
49       <KeyInfo>
50         <X509Data>
51           <X509SubjectName/>
52           <X509IssuerSerial/>
53           <X509Certificate/>
54         </X509Data>
55       <KeyValue />
56       </KeyInfo>
57     </Signature>
58 '''
59
60 ##
61 # Convert a string into a bool
62
63 def str2bool(str):
64     if str.lower() in ['yes','true','1']:
65         return True
66     return False
67
68
69
70 ##
71 # Utility function to get the text of an XML element
72
73 def getTextNode(element, subele):
74     sub = element.getElementsByTagName(subele)[0]
75     if len(sub.childNodes) > 0:            
76         return sub.childNodes[0].nodeValue
77     else:
78         return None
79         
80 ##
81 # Utility function to set the text of an XML element
82 # It creates the element, adds the text to it,
83 # and then appends it to the parent.
84
85 def append_sub(doc, parent, element, text):
86     ele = doc.createElement(element)
87     ele.appendChild(doc.createTextNode(text))
88     parent.appendChild(ele)
89
90 ##
91 # Signature contains information about an xmlsec1 signature
92 # for a signed-credential
93 #
94
95 class Signature(object):
96
97     
98     def __init__(self, string=None):
99         self.refid = None
100         self.issuer_gid = None
101         self.xml = None
102         if string:
103             self.xml = string
104             self.decode()
105
106
107
108     def get_refid(self):
109         if not self.refid:
110             self.decode()
111         return self.refid
112
113     def get_xml(self):
114         if not self.xml:
115             self.encode()
116         return self.xml
117
118     def set_refid(self, id):
119         self.refid = id
120
121     def get_issuer_gid(self):
122         if not self.gid:
123             self.decode()
124         return self.gid        
125
126     def set_issuer_gid(self, gid):
127         self.gid = gid
128
129     def decode(self):
130         doc = parseString(self.xml)
131         sig = doc.getElementsByTagName("Signature")[0]
132         self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
133         keyinfo = sig.getElementsByTagName("X509Data")[0]
134         szgid = getTextNode(keyinfo, "X509Certificate")
135         szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
136         self.set_issuer_gid(GID(string=szgid))        
137         
138     def encode(self):
139         self.xml = signature_template % (self.get_refid(), self.get_refid())
140
141
142 ##
143 # A credential provides a caller gid with privileges to an object gid.
144 # A signed credential is signed by the object's authority.
145 #
146 # Credentials are encoded in one of two ways.  The legacy style places
147 # it in the subjectAltName of an X509 certificate.  The new credentials
148 # are placed in signed XML.
149 #
150 # WARNING:
151 # In general, a signed credential obtained externally should
152 # not be changed else the signature is no longer valid.  So, once
153 # you have loaded an existing signed credential, do not call encode() or sign() on it.
154
155
156 class Credential(object):
157
158
159     ##
160     # Create a Credential object
161     #
162     # @param create If true, create a blank x509 certificate
163     # @param subject If subject!=None, create an x509 cert with the subject name
164     # @param string If string!=None, load the credential from the string
165     # @param filename If filename!=None, load the credential from the file
166
167     def __init__(self, create=False, subject=None, string=None, filename=None):
168         self.gidCaller = None
169         self.gidObject = None
170         self.expiration = None
171         self.privileges = None
172         self.issuer_privkey = None
173         self.issuer_gid = None
174         self.issuer_pubkey = None
175         self.parent = None
176         self.signature = None
177         self.xml = None
178         self.refid = None
179         self.legacy = None
180
181
182         # Check if this is a legacy credential, translate it if so
183         if string or filename:
184             if string:                
185                 str = string
186             elif filename:
187                 str = file(filename).read()
188                 
189             if str.strip().startswith("-----"):
190                 self.legacy = CredentialLegacy(False,string=str)
191                 self.translate_legacy(str)
192             else:
193                 self.xml = str
194                 self.decode()
195
196
197     def get_signature(self):
198         if not self.signature:
199             self.decode()
200         return self.signature
201
202     def set_signature(self, sig):
203         self.signature = sig
204
205         
206     ##
207     # Translate a legacy credential into a new one
208     #
209     # @param String of the legacy credential
210
211     def translate_legacy(self, str):
212         legacy = CredentialLegacy(False,string=str)
213         self.gidCaller = legacy.get_gid_caller()
214         self.gidObject = legacy.get_gid_object()
215         lifetime = legacy.get_lifetime()
216         if not lifetime:
217             # Default to two years
218             self.set_lifetime(DEFAULT_CREDENTIAL_LIFETIME)
219         else:
220             self.set_lifetime(int(lifetime))
221         self.lifeTime = legacy.get_lifetime()
222         self.set_privileges(legacy.get_privileges())
223         self.get_privileges().delegate_all_privileges(legacy.get_delegate())
224
225     ##
226     # Need the issuer's private key and name
227     # @param key Keypair object containing the private key of the issuer
228     # @param gid GID of the issuing authority
229
230     def set_issuer_keys(self, privkey, gid):
231         self.issuer_privkey = privkey
232         self.issuer_gid = gid
233
234
235     ##
236     # Set this credential's parent
237     def set_parent(self, cred):
238         self.parent = cred
239         self.updateRefID()
240
241     ##
242     # set the GID of the caller
243     #
244     # @param gid GID object of the caller
245
246     def set_gid_caller(self, gid):
247         self.gidCaller = gid
248         # gid origin caller is the caller's gid by default
249         self.gidOriginCaller = gid
250
251     ##
252     # get the GID of the object
253
254     def get_gid_caller(self):
255         if not self.gidCaller:
256             self.decode()
257         return self.gidCaller
258
259     ##
260     # set the GID of the object
261     #
262     # @param gid GID object of the object
263
264     def set_gid_object(self, gid):
265         self.gidObject = gid
266
267     ##
268     # get the GID of the object
269
270     def get_gid_object(self):
271         if not self.gidObject:
272             self.decode()
273         return self.gidObject
274
275     ##
276     # set the lifetime of this credential
277     #
278     # @param lifetime lifetime of credential
279     # . if lifeTime is a datetime object, it is used for the expiration time
280     # . if lifeTime is an integer value, it is considered the number of minutes
281     #   remaining before expiration
282
283     def set_lifetime(self, lifeTime):
284         if isinstance(lifeTime, int):
285             self.expiration = datetime.timedelta(seconds=lifeTime*60) + datetime.datetime.utcnow()
286         else:
287             self.expiration = lifeTime
288
289     ##
290     # get the lifetime of the credential (in minutes)
291
292     def get_lifetime(self):
293         if not self.expiration:
294             self.decode()
295         return self.expiration
296
297  
298     ##
299     # set the privileges
300     #
301     # @param privs either a comma-separated list of privileges of a RightList object
302
303     def set_privileges(self, privs):
304         if isinstance(privs, str):
305             self.privileges = RightList(string = privs)
306         else:
307             self.privileges = privs
308         
309
310     ##
311     # return the privileges as a RightList object
312
313     def get_privileges(self):
314         if not self.privileges:
315             self.decode()
316         return self.privileges
317
318     ##
319     # determine whether the credential allows a particular operation to be
320     # performed
321     #
322     # @param op_name string specifying name of operation ("lookup", "update", etc)
323
324     def can_perform(self, op_name):
325         rights = self.get_privileges()
326         
327         if not rights:
328             return False
329
330         return rights.can_perform(op_name)
331
332
333     ##
334     # Encode the attributes of the credential into an XML string    
335     # This should be done immediately before signing the credential.    
336     # WARNING:
337     # In general, a signed credential obtained externally should
338     # not be changed else the signature is no longer valid.  So, once
339     # you have loaded an existing signed credential, do not call encode() or sign() on it.
340
341     def encode(self):
342         # Create the XML document
343         doc = Document()
344         signed_cred = doc.createElement("signed-credential")
345         doc.appendChild(signed_cred)  
346         
347         # Fill in the <credential> bit        
348         cred = doc.createElement("credential")
349         cred.setAttribute("xml:id", self.get_refid())
350         signed_cred.appendChild(cred)
351         append_sub(doc, cred, "type", "privilege")
352         append_sub(doc, cred, "serial", "8")
353         append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
354         append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
355         append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
356         append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
357         append_sub(doc, cred, "uuid", "")
358         if  not self.expiration:
359             self.set_lifetime(3600)
360         self.expiration = self.expiration.replace(microsecond=0)
361         append_sub(doc, cred, "expires", self.expiration.isoformat())
362         privileges = doc.createElement("privileges")
363         cred.appendChild(privileges)
364
365         if self.privileges:
366             rights = self.get_privileges()
367             for right in rights.rights:
368                 priv = doc.createElement("privilege")
369                 append_sub(doc, priv, "name", right.kind)
370                 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
371                 privileges.appendChild(priv)
372
373         # Add the parent credential if it exists
374         if self.parent:
375             sdoc = parseString(self.parent.get_xml())
376             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
377             p = doc.createElement("parent")
378             p.appendChild(p_cred)
379             cred.appendChild(p)
380
381
382         # Create the <signatures> tag
383         signatures = doc.createElement("signatures")
384         signed_cred.appendChild(signatures)
385
386         # Add any parent signatures
387         if self.parent:
388             cur_cred = self.parent
389             while cur_cred:
390                 sdoc = parseString(cur_cred.get_signature().get_xml())
391                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
392                 signatures.appendChild(ele)
393
394                 if cur_cred.parent:
395                     cur_cred = cur_cred.parent
396                 else:
397                     cur_cred = None
398                 
399         # Get the finished product
400         self.xml = doc.toxml()
401
402
403     def save_to_random_tmp_file(self):
404         filename = "/tmp/cred_%d" % randint(0,999999999)
405         self.save_to_file(filename)
406         return filename
407     
408     def save_to_file(self, filename, save_parents=True):
409         if not self.xml:
410             self.encode()
411         f = open(filename, "w")
412         f.write(self.xml)
413         f.close()
414
415     def save_to_string(self, save_parents=True):
416         if not self.xml:
417             self.encode()
418         return self.xml
419
420     def get_refid(self):
421         if not self.refid:
422             self.refid = 'ref0'
423         return self.refid
424
425     def set_refid(self, rid):
426         self.refid = rid
427
428     ##
429     # Figure out what refids exist, and update this credential's id
430     # so that it doesn't clobber the others.  Returns the refids of
431     # the parents.
432     
433     def updateRefID(self):
434         if not self.parent:
435             self.set_refid('ref0')
436             return []
437         
438         refs = []
439
440         next_cred = self.parent
441         while next_cred:
442             refs.append(next_cred.get_refid())
443             if next_cred.parent:
444                 next_cred = next_cred.parent
445             else:
446                 next_cred = None
447
448         
449         # Find a unique refid for this credential
450         rid = self.get_refid()
451         while rid in refs:
452             val = int(rid[3:])
453             rid = "ref%d" % (val + 1)
454
455         # Set the new refid
456         self.set_refid(rid)
457
458         # Return the set of parent credential ref ids
459         return refs
460
461     def get_xml(self):
462         if not self.xml:
463             self.encode()
464         return self.xml
465
466     ##
467     # Sign the XML file created by encode()
468     #
469     # WARNING:
470     # In general, a signed credential obtained externally should
471     # not be changed else the signature is no longer valid.  So, once
472     # you have loaded an existing signed credential, do not call encode() or sign() on it.
473
474     def sign(self):
475         if not self.issuer_privkey or not self.issuer_gid:
476             return
477         doc = parseString(self.get_xml())
478         sigs = doc.getElementsByTagName("signatures")[0]
479
480         # Create the signature template to be signed
481         signature = Signature()
482         signature.set_refid(self.get_refid())
483         sdoc = parseString(signature.get_xml())        
484         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
485         sigs.appendChild(sig_ele)
486
487         self.xml = doc.toxml()
488
489
490         # Split the issuer GID into multiple certificates if it's a chain
491         chain = GID(filename=self.issuer_gid)
492         gid_files = []
493         while chain:
494             gid_files.append(chain.save_to_random_tmp_file(False))
495             if chain.get_parent():
496                 chain = chain.get_parent()
497             else:
498                 chain = None
499
500
501         # Call out to xmlsec1 to sign it
502         ref = 'Sig_%s' % self.get_refid()
503         filename = self.save_to_random_tmp_file()
504         signed = os.popen('/usr/bin/xmlsec1 --sign --node-id "%s" --privkey-pem %s,%s %s' \
505                  % (ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
506         os.remove(filename)
507
508         for gid_file in gid_files:
509             os.remove(gid_file)
510
511         self.xml = signed
512
513         # This is no longer a legacy credential
514         if self.legacy:
515             self.legacy = None
516
517         # Update signatures
518         self.decode()
519
520         
521
522         
523     ##
524     # Retrieve the attributes of the credential from the XML.
525     # This is automatically called by the various get_* methods of
526     # this class and should not need to be called explicitly.
527
528     def decode(self):
529         if not self.xml:
530             return
531         doc = parseString(self.xml)
532         sigs = []
533         signed_cred = doc.getElementsByTagName("signed-credential")
534
535         # Is this a signed-cred or just a cred?
536         if len(signed_cred) > 0:
537             cred = signed_cred[0].getElementsByTagName("credential")[0]
538             signatures = signed_cred[0].getElementsByTagName("signatures")
539             if len(signatures) > 0:
540                 sigs = signatures[0].getElementsByTagName("Signature")
541         else:
542             cred = doc.getElementsByTagName("credential")[0]
543         
544
545
546         self.set_refid(cred.getAttribute("xml:id"))
547         sz_expires = getTextNode(cred, "expires")
548         if sz_expires != '':
549             self.expiration = datetime.datetime.strptime(sz_expires, '%Y-%m-%dT%H:%M:%S')        
550         self.lifeTime = getTextNode(cred, "expires")
551         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
552         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   
553
554
555         # Process privileges
556         privs = cred.getElementsByTagName("privileges")[0]
557         rlist = RightList()
558         for priv in privs.getElementsByTagName("privilege"):
559             kind = getTextNode(priv, "name")
560             deleg = str2bool(getTextNode(priv, "can_delegate"))
561             if kind == '*':
562                 # Convert * into the default privileges for the credential's type                
563                 _ , type = urn_to_hrn(self.gidObject.get_urn())
564                 rl = rlist.determine_rights(type, self.gidObject.get_urn())
565                 for r in rl.rights:
566                     rlist.add(r)
567             else:
568                 rlist.add(Right(kind.strip(), deleg))
569         self.set_privileges(rlist)
570
571
572         # Is there a parent?
573         parent = cred.getElementsByTagName("parent")
574         if len(parent) > 0:
575             parent_doc = parent[0].getElementsByTagName("credential")[0]
576             parent_xml = parent_doc.toxml()
577             self.parent = Credential(string=parent_xml)
578             self.updateRefID()
579
580         # Assign the signatures to the credentials
581         for sig in sigs:
582             Sig = Signature(string=sig.toxml())
583
584             cur_cred = self
585             while cur_cred:
586                 if cur_cred.get_refid() == Sig.get_refid():
587                     cur_cred.set_signature(Sig)
588                     
589                 if cur_cred.parent:
590                     cur_cred = cur_cred.parent
591                 else:
592                     cur_cred = None
593                 
594             
595     ##
596     # Verify that:
597     # . All of the signatures are valid and that the issuers trace back
598     #   to trusted roots (performed by xmlsec1)
599     # . The XML matches the credential schema
600     # . That the issuer of the credential is the authority in the target's urn
601     #    . In the case of a delegated credential, this must be true of the root
602     # . That all of the gids presented in the credential are valid
603     #
604     # -- For Delegates (credentials with parents)
605     # . The privileges must be a subset of the parent credentials
606     # . The privileges must have "can_delegate" set for each delegated privilege
607     # . The target gid must be the same between child and parents
608     # . The expiry time on the child must be no later than the parent
609     # . The signer of the child must be the owner of the parent
610     #
611     # -- Verify does *NOT*
612     # . ensure that an xmlrpc client's gid matches a credential gid, that
613     #   must be done elsewhere
614     #
615     # @param trusted_certs: The certificates of trusted CA certificates
616     
617     def verify(self, trusted_certs):
618         if not self.xml:
619             self.decode()        
620
621         trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
622
623         # Use legacy verification if this is a legacy credential
624         if self.legacy:
625             self.legacy.verify_chain(trusted_cert_objects)
626             if self.legacy.client_gid:
627                 self.legacy.client_gid.verify_chain(trusted_cert_objects)
628             if self.legacy.object_gid:
629                 self.legacy.object_gid.verify_chain(trusted_cert_objects)
630             return True
631
632         # Verify the signatures
633         filename = self.save_to_random_tmp_file()
634         cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
635
636         # Verify the gids of this cred and of its parents
637
638         cur_cred = self
639         while cur_cred:
640             cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
641             cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
642             if cur_cred.parent:
643                 cur_cred = cur_cred.parent
644             else:
645                 cur_cred = None
646         
647         refs = []
648         refs.append("Sig_%s" % self.get_refid())
649
650         parentRefs = self.updateRefID()
651         for ref in parentRefs:
652             refs.append("Sig_%s" % ref)
653
654         for ref in refs:
655             verified = os.popen('/usr/bin/xmlsec1 --verify --node-id "%s" %s %s 2>&1' \
656                             % (ref, cert_args, filename)).read()
657             if not verified.strip().startswith("OK"):
658                 raise CredentialNotVerifiable("xmlsec1 error: " + verified)
659         os.remove(filename)
660
661         # Verify the parents (delegation)
662         if self.parent:
663             self.verify_parent(self.parent)
664         # Make sure the issuer is the target's authority
665         self.verify_issuer()
666         return True
667
668         
669     ##
670     # Make sure the issuer of this credential is the target's authority
671     def verify_issuer(self):        
672         target_authority = get_authority(self.get_gid_object().get_urn())
673
674         
675         # Find the root credential's signature
676         cur_cred = self
677         while cur_cred:            
678             if cur_cred.parent:
679                 cur_cred = cur_cred.parent
680             else:
681                 root_issuer = cur_cred.get_signature().get_issuer_gid().get_urn()
682                 cur_cred = None
683
684         # Ensure that the signer of the root credential is the target_authority
685         target_authority = hrn_to_urn(target_authority, 'authority')
686
687         logger.info( "%s %s" % (root_issuer, target_authority))
688
689         if root_issuer != target_authority:
690             raise CredentialNotVerifiable("issuer (%s) != authority of target (%s)" \
691                                           % (root_issuer, target_authority))
692
693     ##
694     # -- For Delegates (credentials with parents) verify that:
695     # . The privileges must be a subset of the parent credentials
696     # . The privileges must have "can_delegate" set for each delegated privilege
697     # . The target gid must be the same between child and parents
698     # . The expiry time on the child must be no later than the parent
699     # . The signer of the child must be the owner of the parent
700         
701     def verify_parent(self, parent_cred):
702         # make sure the rights given to the child are a subset of the
703         # parents rights (and check delegate bits)
704         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
705             raise ChildRightsNotSubsetOfParent(
706                 self.parent.get_privileges().save_to_string() + " " +
707                 self.get_privileges().save_to_string())
708
709         # make sure my target gid is the same as the parent's
710         if not parent_cred.get_gid_object().save_to_string() == \
711            self.get_gid_object().save_to_string():
712             raise CredentialNotVerifiable("target gid not equal between parent and child")
713
714         # make sure my expiry time is <= my parent's
715         if not parent_cred.get_lifetime() >= self.get_lifetime():
716             raise CredentialNotVerifiable("delegated credential expires after parent")
717
718         # make sure my signer is the parent's caller
719         if not parent_cred.get_gid_caller().save_to_string(False) == \
720            self.get_signature().get_issuer_gid().save_to_string(False):
721             raise CredentialNotVerifiable("delegated credential not signed by parent caller")
722                 
723         if parent_cred.parent:
724             parent_cred.verify_parent(parent_cred.parent)
725
726     ##
727     # Dump the contents of a credential to stdout in human-readable format
728     #
729     # @param dump_parents If true, also dump the parent certificates
730
731     def dump(self, dump_parents=False):
732         print "CREDENTIAL", self.get_subject()
733
734         print "      privs:", self.get_privileges().save_to_string()
735
736         print "  gidCaller:"
737         gidCaller = self.get_gid_caller()
738         if gidCaller:
739             gidCaller.dump(8, dump_parents)
740
741         print "  gidObject:"
742         gidObject = self.get_gid_object()
743         if gidObject:
744             gidObject.dump(8, dump_parents)
745
746
747         if self.parent and dump_parents:
748             print "PARENT",
749             self.parent.dump_parents()
750