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