reviewed error msg formatting - fixed a couple glitches
[sfa.git] / sfa / trust / credential.py
1 #----------------------------------------------------------------------
2 # Copyright (c) 2008 Board of Trustees, Princeton University
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and/or hardware specification (the "Work") to
6 # deal in the Work without restriction, including without limitation the
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Work, and to permit persons to whom the Work
9 # is furnished to do so, subject to the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Work.
13 #
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS 
21 # IN THE WORK.
22 #----------------------------------------------------------------------
23 ##
24 # Implements SFA Credentials
25 #
26 # Credentials are signed XML files that assign a subject gid privileges to an object gid
27 ##
28
29 ### $Id$
30 ### $URL$
31
32 import os
33 import datetime
34 from tempfile import mkstemp
35 from xml.dom.minidom import Document, parseString
36 from dateutil.parser import parse
37 from lxml import etree
38 from StringIO import StringIO 
39 from sfa.util.faults import *
40 from sfa.util.sfalogging import sfa_logger
41 from sfa.trust.certificate import Keypair
42 from sfa.trust.credential_legacy import CredentialLegacy
43 from sfa.trust.rights import Right, Rights
44 from sfa.trust.gid import GID
45 from sfa.util.xrn import urn_to_hrn
46
47 # 2 weeks, in seconds 
48 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14
49
50
51 # TODO:
52 # . make privs match between PG and PL
53 # . Need to add support for other types of credentials, e.g. tickets
54
55
56 signature_template = \
57 '''
58 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
59     <SignedInfo>
60       <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
61       <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
62       <Reference URI="#%s">
63       <Transforms>
64         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
65       </Transforms>
66       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
67       <DigestValue></DigestValue>
68       </Reference>
69     </SignedInfo>
70     <SignatureValue />
71       <KeyInfo>
72         <X509Data>
73           <X509SubjectName/>
74           <X509IssuerSerial/>
75           <X509Certificate/>
76         </X509Data>
77       <KeyValue />
78       </KeyInfo>
79     </Signature>
80 '''
81
82 ##
83 # Convert a string into a bool
84
85 def str2bool(str):
86     if str.lower() in ['yes','true','1']:
87         return True
88     return False
89
90
91 ##
92 # Utility function to get the text of an XML element
93
94 def getTextNode(element, subele):
95     sub = element.getElementsByTagName(subele)[0]
96     if len(sub.childNodes) > 0:            
97         return sub.childNodes[0].nodeValue
98     else:
99         return None
100         
101 ##
102 # Utility function to set the text of an XML element
103 # It creates the element, adds the text to it,
104 # and then appends it to the parent.
105
106 def append_sub(doc, parent, element, text):
107     ele = doc.createElement(element)
108     ele.appendChild(doc.createTextNode(text))
109     parent.appendChild(ele)
110
111 ##
112 # Signature contains information about an xmlsec1 signature
113 # for a signed-credential
114 #
115
116 class Signature(object):
117    
118     def __init__(self, string=None):
119         self.refid = None
120         self.issuer_gid = None
121         self.xml = None
122         if string:
123             self.xml = string
124             self.decode()
125
126
127     def get_refid(self):
128         if not self.refid:
129             self.decode()
130         return self.refid
131
132     def get_xml(self):
133         if not self.xml:
134             self.encode()
135         return self.xml
136
137     def set_refid(self, id):
138         self.refid = id
139
140     def get_issuer_gid(self):
141         if not self.gid:
142             self.decode()
143         return self.gid        
144
145     def set_issuer_gid(self, gid):
146         self.gid = gid
147
148     def decode(self):
149         doc = parseString(self.xml)
150         sig = doc.getElementsByTagName("Signature")[0]
151         self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
152         keyinfo = sig.getElementsByTagName("X509Data")[0]
153         szgid = getTextNode(keyinfo, "X509Certificate")
154         szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
155         self.set_issuer_gid(GID(string=szgid))        
156         
157     def encode(self):
158         self.xml = signature_template % (self.get_refid(), self.get_refid())
159
160
161 ##
162 # A credential provides a caller gid with privileges to an object gid.
163 # A signed credential is signed by the object's authority.
164 #
165 # Credentials are encoded in one of two ways.  The legacy style places
166 # it in the subjectAltName of an X509 certificate.  The new credentials
167 # are placed in signed XML.
168 #
169 # WARNING:
170 # In general, a signed credential obtained externally should
171 # not be changed else the signature is no longer valid.  So, once
172 # you have loaded an existing signed credential, do not call encode() or sign() on it.
173
174 def filter_creds_by_caller(creds, caller_hrn):
175         """
176         Returns a list of creds who's gid caller matches the
177         specified caller hrn
178         """
179         if not isinstance(creds, list): creds = [creds]
180         caller_creds = []
181         for cred in creds:
182             try:
183                 tmp_cred = Credential(string=cred)
184                 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
185                     caller_creds.append(cred)
186             except: pass
187         return caller_creds
188
189 class Credential(object):
190
191     ##
192     # Create a Credential object
193     #
194     # @param create If true, create a blank x509 certificate
195     # @param subject If subject!=None, create an x509 cert with the subject name
196     # @param string If string!=None, load the credential from the string
197     # @param filename If filename!=None, load the credential from the file
198     # FIXME: create and subject are ignored!
199     def __init__(self, create=False, subject=None, string=None, filename=None):
200         self.gidCaller = None
201         self.gidObject = None
202         self.expiration = None
203         self.privileges = None
204         self.issuer_privkey = None
205         self.issuer_gid = None
206         self.issuer_pubkey = None
207         self.parent = None
208         self.signature = None
209         self.xml = None
210         self.refid = None
211         self.legacy = None
212
213         # Check if this is a legacy credential, translate it if so
214         if string or filename:
215             if string:                
216                 str = string
217             elif filename:
218                 str = file(filename).read()
219                 self.filename=filename
220                 
221             if str.strip().startswith("-----"):
222                 self.legacy = CredentialLegacy(False,string=str)
223                 self.translate_legacy(str)
224             else:
225                 self.xml = str
226                 self.decode()
227
228         # Find an xmlsec1 path
229         self.xmlsec_path = ''
230         paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
231         for path in paths:
232             if os.path.isfile(path + '/' + 'xmlsec1'):
233                 self.xmlsec_path = path + '/' + 'xmlsec1'
234                 break
235
236     def get_subject(self):
237         if not self.gidObject:
238             self.decode()
239         return self.gidObject.get_subject()   
240
241     def get_signature(self):
242         if not self.signature:
243             self.decode()
244         return self.signature
245
246     def set_signature(self, sig):
247         self.signature = sig
248
249         
250     ##
251     # Translate a legacy credential into a new one
252     #
253     # @param String of the legacy credential
254
255     def translate_legacy(self, str):
256         legacy = CredentialLegacy(False,string=str)
257         self.gidCaller = legacy.get_gid_caller()
258         self.gidObject = legacy.get_gid_object()
259         lifetime = legacy.get_lifetime()
260         if not lifetime:
261             self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
262         else:
263             self.set_expiration(int(lifetime))
264         self.lifeTime = legacy.get_lifetime()
265         self.set_privileges(legacy.get_privileges())
266         self.get_privileges().delegate_all_privileges(legacy.get_delegate())
267
268     ##
269     # Need the issuer's private key and name
270     # @param key Keypair object containing the private key of the issuer
271     # @param gid GID of the issuing authority
272
273     def set_issuer_keys(self, privkey, gid):
274         self.issuer_privkey = privkey
275         self.issuer_gid = gid
276
277
278     ##
279     # Set this credential's parent
280     def set_parent(self, cred):
281         self.parent = cred
282         self.updateRefID()
283
284     ##
285     # set the GID of the caller
286     #
287     # @param gid GID object of the caller
288
289     def set_gid_caller(self, gid):
290         self.gidCaller = gid
291         # gid origin caller is the caller's gid by default
292         self.gidOriginCaller = gid
293
294     ##
295     # get the GID of the object
296
297     def get_gid_caller(self):
298         if not self.gidCaller:
299             self.decode()
300         return self.gidCaller
301
302     ##
303     # set the GID of the object
304     #
305     # @param gid GID object of the object
306
307     def set_gid_object(self, gid):
308         self.gidObject = gid
309
310     ##
311     # get the GID of the object
312
313     def get_gid_object(self):
314         if not self.gidObject:
315             self.decode()
316         return self.gidObject
317
318
319             
320     ##
321     # Expiration: an absolute UTC time of expiration (as either an int or datetime)
322     # 
323     def set_expiration(self, expiration):
324         if isinstance(expiration, int):
325             self.expiration = datetime.datetime.fromtimestamp(expiration)
326         else:
327             self.expiration = expiration
328             
329
330     ##
331     # get the lifetime of the credential (in datetime format)
332
333     def get_expiration(self):
334         if not self.expiration:
335             self.decode()
336         return self.expiration
337
338     ##
339     # For legacy sake
340     def get_lifetime(self):
341         return self.get_expiration()
342  
343     ##
344     # set the privileges
345     #
346     # @param privs either a comma-separated list of privileges of a Rights object
347
348     def set_privileges(self, privs):
349         if isinstance(privs, str):
350             self.privileges = Rights(string = privs)
351         else:
352             self.privileges = privs
353         
354
355     ##
356     # return the privileges as a Rights object
357
358     def get_privileges(self):
359         if not self.privileges:
360             self.decode()
361         return self.privileges
362
363     ##
364     # determine whether the credential allows a particular operation to be
365     # performed
366     #
367     # @param op_name string specifying name of operation ("lookup", "update", etc)
368
369     def can_perform(self, op_name):
370         rights = self.get_privileges()
371         
372         if not rights:
373             return False
374
375         return rights.can_perform(op_name)
376
377
378     ##
379     # Encode the attributes of the credential into an XML string    
380     # This should be done immediately before signing the credential.    
381     # WARNING:
382     # In general, a signed credential obtained externally should
383     # not be changed else the signature is no longer valid.  So, once
384     # you have loaded an existing signed credential, do not call encode() or sign() on it.
385
386     def encode(self):
387         # Create the XML document
388         doc = Document()
389         signed_cred = doc.createElement("signed-credential")
390         doc.appendChild(signed_cred)  
391         
392         # Fill in the <credential> bit        
393         cred = doc.createElement("credential")
394         cred.setAttribute("xml:id", self.get_refid())
395         signed_cred.appendChild(cred)
396         append_sub(doc, cred, "type", "privilege")
397         append_sub(doc, cred, "serial", "8")
398         append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
399         append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
400         append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
401         append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
402         append_sub(doc, cred, "uuid", "")
403         if not self.expiration:
404             self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
405         self.expiration = self.expiration.replace(microsecond=0)
406         append_sub(doc, cred, "expires", self.expiration.isoformat())
407         privileges = doc.createElement("privileges")
408         cred.appendChild(privileges)
409
410         if self.privileges:
411             rights = self.get_privileges()
412             for right in rights.rights:
413                 priv = doc.createElement("privilege")
414                 append_sub(doc, priv, "name", right.kind)
415                 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
416                 privileges.appendChild(priv)
417
418         # Add the parent credential if it exists
419         if self.parent:
420             sdoc = parseString(self.parent.get_xml())
421             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
422             p = doc.createElement("parent")
423             p.appendChild(p_cred)
424             cred.appendChild(p)
425
426
427         # Create the <signatures> tag
428         signatures = doc.createElement("signatures")
429         signed_cred.appendChild(signatures)
430
431         # Add any parent signatures
432         if self.parent:
433             for cur_cred in self.get_credential_list()[1:]:
434                 sdoc = parseString(cur_cred.get_signature().get_xml())
435                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
436                 signatures.appendChild(ele)
437                 
438         # Get the finished product
439         self.xml = doc.toxml()
440
441
442     def save_to_random_tmp_file(self):       
443         fp, filename = mkstemp(suffix='cred', text=True)
444         fp = os.fdopen(fp, "w")
445         self.save_to_file(filename, save_parents=True, filep=fp)
446         return filename
447     
448     def save_to_file(self, filename, save_parents=True, filep=None):
449         if not self.xml:
450             self.encode()
451         if filep:
452             f = filep 
453         else:
454             f = open(filename, "w")
455         f.write(self.xml)
456         f.close()
457         self.filename=filename
458
459     def save_to_string(self, save_parents=True):
460         if not self.xml:
461             self.encode()
462         return self.xml
463
464     def get_refid(self):
465         if not self.refid:
466             self.refid = 'ref0'
467         return self.refid
468
469     def set_refid(self, rid):
470         self.refid = rid
471
472     ##
473     # Figure out what refids exist, and update this credential's id
474     # so that it doesn't clobber the others.  Returns the refids of
475     # the parents.
476     
477     def updateRefID(self):
478         if not self.parent:
479             self.set_refid('ref0')
480             return []
481         
482         refs = []
483
484         next_cred = self.parent
485         while next_cred:
486             refs.append(next_cred.get_refid())
487             if next_cred.parent:
488                 next_cred = next_cred.parent
489             else:
490                 next_cred = None
491
492         
493         # Find a unique refid for this credential
494         rid = self.get_refid()
495         while rid in refs:
496             val = int(rid[3:])
497             rid = "ref%d" % (val + 1)
498
499         # Set the new refid
500         self.set_refid(rid)
501
502         # Return the set of parent credential ref ids
503         return refs
504
505     def get_xml(self):
506         if not self.xml:
507             self.encode()
508         return self.xml
509
510     ##
511     # Sign the XML file created by encode()
512     #
513     # WARNING:
514     # In general, a signed credential obtained externally should
515     # not be changed else the signature is no longer valid.  So, once
516     # you have loaded an existing signed credential, do not call encode() or sign() on it.
517
518     def sign(self):
519         if not self.issuer_privkey or not self.issuer_gid:
520             return
521         doc = parseString(self.get_xml())
522         sigs = doc.getElementsByTagName("signatures")[0]
523
524         # Create the signature template to be signed
525         signature = Signature()
526         signature.set_refid(self.get_refid())
527         sdoc = parseString(signature.get_xml())        
528         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
529         sigs.appendChild(sig_ele)
530
531         self.xml = doc.toxml()
532
533
534         # Split the issuer GID into multiple certificates if it's a chain
535         chain = GID(filename=self.issuer_gid)
536         gid_files = []
537         while chain:
538             gid_files.append(chain.save_to_random_tmp_file(False))
539             if chain.get_parent():
540                 chain = chain.get_parent()
541             else:
542                 chain = None
543
544
545         # Call out to xmlsec1 to sign it
546         ref = 'Sig_%s' % self.get_refid()
547         filename = self.save_to_random_tmp_file()
548         signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
549                  % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
550         os.remove(filename)
551
552         for gid_file in gid_files:
553             os.remove(gid_file)
554
555         self.xml = signed
556
557         # This is no longer a legacy credential
558         if self.legacy:
559             self.legacy = None
560
561         # Update signatures
562         self.decode()       
563
564         
565     ##
566     # Retrieve the attributes of the credential from the XML.
567     # This is automatically called by the various get_* methods of
568     # this class and should not need to be called explicitly.
569
570     def decode(self):
571         if not self.xml:
572             return
573         doc = parseString(self.xml)
574         sigs = []
575         signed_cred = doc.getElementsByTagName("signed-credential")
576
577         # Is this a signed-cred or just a cred?
578         if len(signed_cred) > 0:
579             cred = signed_cred[0].getElementsByTagName("credential")[0]
580             signatures = signed_cred[0].getElementsByTagName("signatures")
581             if len(signatures) > 0:
582                 sigs = signatures[0].getElementsByTagName("Signature")
583         else:
584             cred = doc.getElementsByTagName("credential")[0]
585         
586
587         self.set_refid(cred.getAttribute("xml:id"))
588         self.set_expiration(parse(getTextNode(cred, "expires")))
589         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
590         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   
591
592
593         # Process privileges
594         privs = cred.getElementsByTagName("privileges")[0]
595         rlist = Rights()
596         for priv in privs.getElementsByTagName("privilege"):
597             kind = getTextNode(priv, "name")
598             deleg = str2bool(getTextNode(priv, "can_delegate"))
599             if kind == '*':
600                 # Convert * into the default privileges for the credential's type                
601                 _ , type = urn_to_hrn(self.gidObject.get_urn())
602                 rl = rlist.determine_rights(type, self.gidObject.get_urn())
603                 for r in rl.rights:
604                     rlist.add(r)
605             else:
606                 rlist.add(Right(kind.strip(), deleg))
607         self.set_privileges(rlist)
608
609
610         # Is there a parent?
611         parent = cred.getElementsByTagName("parent")
612         if len(parent) > 0:
613             parent_doc = parent[0].getElementsByTagName("credential")[0]
614             parent_xml = parent_doc.toxml()
615             self.parent = Credential(string=parent_xml)
616             self.updateRefID()
617
618         # Assign the signatures to the credentials
619         for sig in sigs:
620             Sig = Signature(string=sig.toxml())
621
622             for cur_cred in self.get_credential_list():
623                 if cur_cred.get_refid() == Sig.get_refid():
624                     cur_cred.set_signature(Sig)
625                                     
626             
627     ##
628     # Verify
629     #   trusted_certs: A list of trusted GID filenames (not GID objects!) 
630     #                  Chaining is not supported within the GIDs by xmlsec1.
631     #    
632     # Verify that:
633     # . All of the signatures are valid and that the issuers trace back
634     #   to trusted roots (performed by xmlsec1)
635     # . The XML matches the credential schema
636     # . That the issuer of the credential is the authority in the target's urn
637     #    . In the case of a delegated credential, this must be true of the root
638     # . That all of the gids presented in the credential are valid
639     # . The credential is not expired
640     #
641     # -- For Delegates (credentials with parents)
642     # . The privileges must be a subset of the parent credentials
643     # . The privileges must have "can_delegate" set for each delegated privilege
644     # . The target gid must be the same between child and parents
645     # . The expiry time on the child must be no later than the parent
646     # . The signer of the child must be the owner of the parent
647     #
648     # -- Verify does *NOT*
649     # . ensure that an xmlrpc client's gid matches a credential gid, that
650     #   must be done elsewhere
651     #
652     # @param trusted_certs: The certificates of trusted CA certificates
653     # @param schema: The RelaxNG schema to validate the credential against 
654     def verify(self, trusted_certs, schema=None):
655         if not self.xml:
656             self.decode()        
657         
658         # validate against RelaxNG schema
659         if not self.legacy:
660             if schema and os.path.exists(schema):
661                 tree = etree.parse(StringIO(self.xml))
662                 schema_doc = etree.parse(schema)
663                 xmlschema = etree.XMLSchema(schema_doc)
664                 if not xmlschema.validate(tree):
665                     error = xmlschema.error_log.last_error
666                     message = "%s (line %s)" % (error.message, error.line)
667                     raise CredentialNotVerifiable(message) 
668             
669
670 #       trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
671         trusted_cert_objects = []
672         ok_trusted_certs = []
673         for f in trusted_certs:
674             try:
675                 # Failures here include unreadable files
676                 # or non PEM files
677                 trusted_cert_objects.append(GID(filename=f))
678                 ok_trusted_certs.append(f)
679             except Exception, exc:
680                 sfa_logger().error("Failed to load trusted cert from %s: %r"%( f, exc))
681         trusted_certs = ok_trusted_certs
682
683         # Use legacy verification if this is a legacy credential
684         if self.legacy:
685             self.legacy.verify_chain(trusted_cert_objects)
686             if self.legacy.client_gid:
687                 self.legacy.client_gid.verify_chain(trusted_cert_objects)
688             if self.legacy.object_gid:
689                 self.legacy.object_gid.verify_chain(trusted_cert_objects)
690             return True
691
692         
693         # make sure it is not expired
694         if self.get_expiration() < datetime.datetime.utcnow():
695             raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
696
697         # Verify the signatures
698         filename = self.save_to_random_tmp_file()
699         cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
700
701         # Verify the gids of this cred and of its parents
702         for cur_cred in self.get_credential_list():
703             cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
704             cur_cred.get_gid_caller().verify_chain(trusted_cert_objects) 
705
706         refs = []
707         refs.append("Sig_%s" % self.get_refid())
708
709         parentRefs = self.updateRefID()
710         for ref in parentRefs:
711             refs.append("Sig_%s" % ref)
712
713         for ref in refs:
714             verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
715                             % (self.xmlsec_path, ref, cert_args, filename)).read()
716             if not verified.strip().startswith("OK"):
717                 raise CredentialNotVerifiable("xmlsec1 error verifying cert: " + verified)
718         os.remove(filename)
719
720         # Verify the parents (delegation)
721         if self.parent:
722             self.verify_parent(self.parent)
723
724         # Make sure the issuer is the target's authority
725         self.verify_issuer()
726         return True
727
728     ##
729     # Creates a list of the credential and its parents, with the root 
730     # (original delegated credential) as the last item in the list
731     def get_credential_list(self):    
732         cur_cred = self
733         list = []
734         while cur_cred:
735             list.append(cur_cred)
736             if cur_cred.parent:
737                 cur_cred = cur_cred.parent
738             else:
739                 cur_cred = None
740         return list
741     
742     ##
743     # Make sure the credential's target gid was signed by (or is the same) the entity that signed
744     # the original credential or an authority over that namespace.
745     def verify_issuer(self):                
746         root_cred = self.get_credential_list()[-1]
747         root_target_gid = root_cred.get_gid_object()
748         root_cred_signer = root_cred.get_signature().get_issuer_gid()
749
750         if root_target_gid.is_signed_by_cert(root_cred_signer):
751             # cred signer matches target signer, return success
752             return
753
754         root_target_gid_str = root_target_gid.save_to_string()
755         root_cred_signer_str = root_cred_signer.save_to_string()
756         if root_target_gid_str == root_cred_signer_str:
757             # cred signer is target, return success
758             return
759
760         # See if it the signer is an authority over the domain of the target
761         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
762         root_cred_signer_type = root_cred_signer.get_type()
763         if (root_cred_signer_type == 'authority'):
764             #sfa_logger().debug('Cred signer is an authority')
765             # signer is an authority, see if target is in authority's domain
766             hrn = root_cred_signer.get_hrn()
767             if root_target_gid.get_hrn().startswith(hrn):
768                 return
769
770         # We've required that the credential be signed by an authority
771         # for that domain. Reasonable and probably correct.
772         # A looser model would also allow the signer to be an authority
773         # in my control framework - eg My CA or CH. Even if it is not
774         # the CH that issued these, eg, user credentials.
775
776         # Give up, credential does not pass issuer verification
777
778         raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred signer %s not the trusted authority for Cred target %s" % (self.gidCaller.get_urn(), self.gidObject.get_urn(), root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
779
780
781     ##
782     # -- For Delegates (credentials with parents) verify that:
783     # . The privileges must be a subset of the parent credentials
784     # . The privileges must have "can_delegate" set for each delegated privilege
785     # . The target gid must be the same between child and parents
786     # . The expiry time on the child must be no later than the parent
787     # . The signer of the child must be the owner of the parent        
788     def verify_parent(self, parent_cred):
789         # make sure the rights given to the child are a subset of the
790         # parents rights (and check delegate bits)
791         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
792             raise ChildRightsNotSubsetOfParent(
793                 self.parent.get_privileges().save_to_string() + " " +
794                 self.get_privileges().save_to_string())
795
796         # make sure my target gid is the same as the parent's
797         if not parent_cred.get_gid_object().save_to_string() == \
798            self.get_gid_object().save_to_string():
799             raise CredentialNotVerifiable("Target gid not equal between parent and child")
800
801         # make sure my expiry time is <= my parent's
802         if not parent_cred.get_expiration() >= self.get_expiration():
803             raise CredentialNotVerifiable("Delegated credential expires after parent")
804
805         # make sure my signer is the parent's caller
806         if not parent_cred.get_gid_caller().save_to_string(False) == \
807            self.get_signature().get_issuer_gid().save_to_string(False):
808             raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
809                 
810         # Recurse
811         if parent_cred.parent:
812             parent_cred.verify_parent(parent_cred.parent)
813
814
815     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
816         """
817         Return a delegated copy of this credential, delegated to the 
818         specified gid's user.    
819         """
820         # get the gid of the object we are delegating
821         object_gid = self.get_gid_object()
822         object_hrn = object_gid.get_hrn()        
823  
824         # the hrn of the user who will be delegated to
825         delegee_gid = GID(filename=delegee_gidfile)
826         delegee_hrn = delegee_gid.get_hrn()
827   
828         #user_key = Keypair(filename=keyfile)
829         #user_hrn = self.get_gid_caller().get_hrn()
830         subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
831         dcred = Credential(subject=subject_string)
832         dcred.set_gid_caller(delegee_gid)
833         dcred.set_gid_object(object_gid)
834         dcred.set_parent(self)
835         dcred.set_expiration(self.get_expiration())
836         dcred.set_privileges(self.get_privileges())
837         dcred.get_privileges().delegate_all_privileges(True)
838         #dcred.set_issuer_keys(keyfile, delegee_gidfile)
839         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
840         dcred.encode()
841         dcred.sign()
842
843         return dcred 
844
845     # only informative
846     def get_filename(self):
847         return getattr(self,'filename',None)
848
849     # @param dump_parents If true, also dump the parent certificates
850     def dump (self, *args, **kwargs):
851         print self.dump_string(*args, **kwargs)
852
853     def dump_string(self, dump_parents=False):
854         result=""
855         result += "CREDENTIAL %s\n" % self.get_subject() 
856         filename=self.get_filename()
857         if filename: result += "Filename %s\n"%filename
858         result += "      privs: %s\n" % self.get_privileges().save_to_string()
859         gidCaller = self.get_gid_caller()
860         if gidCaller:
861             result += "  gidCaller:\n"
862             result += gidCaller.dump_string(8, dump_parents)
863
864         gidObject = self.get_gid_object()
865         if gidObject:
866             result += "  gidObject:\n"
867             result += gidObject.dump_string(8, dump_parents)
868
869         if self.parent and dump_parents:
870             result += "PARENT"
871             result += self.parent.dump_string(dump_parents)
872         return result
873