restore default credential validity period to 28 days
[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 import os
30 from types import StringTypes
31 import datetime
32 from StringIO import StringIO
33 from tempfile import mkstemp
34 from xml.dom.minidom import Document, parseString
35
36 HAVELXML = False
37 try:
38     from lxml import etree
39     HAVELXML = True
40 except:
41     pass
42
43 from xml.parsers.expat import ExpatError
44
45 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
46 from sfa.util.sfalogging import logger
47 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
48 from sfa.trust.rights import Right, Rights, determine_rights
49 from sfa.trust.gid import GID
50 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
51
52 # 31 days, in seconds 
53 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 28
54
55
56 # TODO:
57 # . make privs match between PG and PL
58 # . Need to add support for other types of credentials, e.g. tickets
59 # . add namespaces to signed-credential element?
60
61 signature_template = \
62 '''
63 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
64   <SignedInfo>
65     <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
66     <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
67     <Reference URI="#%s">
68       <Transforms>
69         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
70       </Transforms>
71       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
72       <DigestValue></DigestValue>
73     </Reference>
74   </SignedInfo>
75   <SignatureValue />
76   <KeyInfo>
77     <X509Data>
78       <X509SubjectName/>
79       <X509IssuerSerial/>
80       <X509Certificate/>
81     </X509Data>
82     <KeyValue />
83   </KeyInfo>
84 </Signature>
85 '''
86
87 # PG formats the template (whitespace) slightly differently.
88 # Note that they don't include the xmlns in the template, but add it later.
89 # Otherwise the two are equivalent.
90 #signature_template_as_in_pg = \
91 #'''
92 #<Signature xml:id="Sig_%s" >
93 # <SignedInfo>
94 #  <CanonicalizationMethod      Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
95 #  <SignatureMethod      Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
96 #  <Reference URI="#%s">
97 #    <Transforms>
98 #      <Transform         Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
99 #    </Transforms>
100 #    <DigestMethod        Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
101 #    <DigestValue></DigestValue>
102 #    </Reference>
103 # </SignedInfo>
104 # <SignatureValue />
105 # <KeyInfo>
106 #  <X509Data >
107 #   <X509SubjectName/>
108 #   <X509IssuerSerial/>
109 #   <X509Certificate/>
110 #  </X509Data>
111 #  <KeyValue />
112 # </KeyInfo>
113 #</Signature>
114 #'''
115
116 ##
117 # Convert a string into a bool
118 # used to convert an xsd:boolean to a Python boolean
119 def str2bool(str):
120     if str.lower() in ['true','1']:
121         return True
122     return False
123
124
125 ##
126 # Utility function to get the text of an XML element
127
128 def getTextNode(element, subele):
129     sub = element.getElementsByTagName(subele)[0]
130     if len(sub.childNodes) > 0:            
131         return sub.childNodes[0].nodeValue
132     else:
133         return None
134         
135 ##
136 # Utility function to set the text of an XML element
137 # It creates the element, adds the text to it,
138 # and then appends it to the parent.
139
140 def append_sub(doc, parent, element, text):
141     ele = doc.createElement(element)
142     ele.appendChild(doc.createTextNode(text))
143     parent.appendChild(ele)
144
145 ##
146 # Signature contains information about an xmlsec1 signature
147 # for a signed-credential
148 #
149
150 class Signature(object):
151    
152     def __init__(self, string=None):
153         self.refid = None
154         self.issuer_gid = None
155         self.xml = None
156         if string:
157             self.xml = string
158             self.decode()
159
160
161     def get_refid(self):
162         if not self.refid:
163             self.decode()
164         return self.refid
165
166     def get_xml(self):
167         if not self.xml:
168             self.encode()
169         return self.xml
170
171     def set_refid(self, id):
172         self.refid = id
173
174     def get_issuer_gid(self):
175         if not self.gid:
176             self.decode()
177         return self.gid        
178
179     def set_issuer_gid(self, gid):
180         self.gid = gid
181
182     def decode(self):
183         try:
184             doc = parseString(self.xml)
185         except ExpatError,e:
186             logger.log_exc ("Failed to parse credential, %s"%self.xml)
187             raise
188         sig = doc.getElementsByTagName("Signature")[0]
189         self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
190         keyinfo = sig.getElementsByTagName("X509Data")[0]
191         szgid = getTextNode(keyinfo, "X509Certificate")
192         szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
193         self.set_issuer_gid(GID(string=szgid))        
194         
195     def encode(self):
196         self.xml = signature_template % (self.get_refid(), self.get_refid())
197
198 ##
199 # A credential provides a caller gid with privileges to an object gid.
200 # A signed credential is signed by the object's authority.
201 #
202 # Credentials are encoded in one of two ways. 
203 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
204 # The new credentials are placed in signed XML.
205 #
206 # WARNING:
207 # In general, a signed credential obtained externally should
208 # not be changed else the signature is no longer valid.  So, once
209 # you have loaded an existing signed credential, do not call encode() or sign() on it.
210
211 def filter_creds_by_caller(creds, caller_hrn_list):
212         """
213         Returns a list of creds who's gid caller matches the
214         specified caller hrn
215         """
216         if not isinstance(creds, list): creds = [creds]
217         if not isinstance(caller_hrn_list, list): 
218             caller_hrn_list = [caller_hrn_list]
219         caller_creds = []
220         for cred in creds:
221             try:
222                 tmp_cred = Credential(string=cred)
223                 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
224                     caller_creds.append(cred)
225             except: pass
226         return caller_creds
227
228 class Credential(object):
229
230     ##
231     # Create a Credential object
232     #
233     # @param create If true, create a blank x509 certificate
234     # @param subject If subject!=None, create an x509 cert with the subject name
235     # @param string If string!=None, load the credential from the string
236     # @param filename If filename!=None, load the credential from the file
237     # FIXME: create and subject are ignored!
238     def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
239         self.gidCaller = None
240         self.gidObject = None
241         self.expiration = None
242         self.privileges = None
243         self.issuer_privkey = None
244         self.issuer_gid = None
245         self.issuer_pubkey = None
246         self.parent = None
247         self.signature = None
248         self.xml = None
249         self.refid = None
250         self.type = None
251         self.version = None
252
253         if cred:
254             if isinstance(cred, StringTypes):
255                 string = cred
256                 self.type = 'geni_sfa'
257                 self.version = '1.0'
258             elif isinstance(cred, dict):
259                 string = cred['geni_value']
260                 self.type = cred['geni_type']
261                 self.version = cred['geni_version']
262                 
263
264         if string or filename:
265             if string:                
266                 str = string
267             elif filename:
268                 str = file(filename).read()
269                 
270             # if this is a legacy credential, write error and bail out
271             if isinstance (str, StringTypes) and str.strip().startswith("-----"):
272                 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
273                 return
274             else:
275                 self.xml = str
276                 self.decode()
277
278         # Find an xmlsec1 path
279         self.xmlsec_path = ''
280         paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
281         for path in paths:
282             if os.path.isfile(path + '/' + 'xmlsec1'):
283                 self.xmlsec_path = path + '/' + 'xmlsec1'
284                 break
285
286     def get_subject(self):
287         subject = ""
288         if not self.gidObject:
289             self.decode()
290         if self.gidObject:
291             subject = self.gidObject.get_printable_subject()
292         return subject
293
294     # sounds like this should be __repr__ instead ??
295     def get_summary_tostring(self):
296         if not self.gidObject:
297             self.decode()
298         obj = self.gidObject.get_printable_subject()
299         caller = self.gidCaller.get_printable_subject()
300         exp = self.get_expiration()
301         # Summarize the rights too? The issuer?
302         return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
303
304     def get_signature(self):
305         if not self.signature:
306             self.decode()
307         return self.signature
308
309     def set_signature(self, sig):
310         self.signature = sig
311
312         
313     ##
314     # Need the issuer's private key and name
315     # @param key Keypair object containing the private key of the issuer
316     # @param gid GID of the issuing authority
317
318     def set_issuer_keys(self, privkey, gid):
319         self.issuer_privkey = privkey
320         self.issuer_gid = gid
321
322
323     ##
324     # Set this credential's parent
325     def set_parent(self, cred):
326         self.parent = cred
327         self.updateRefID()
328
329     ##
330     # set the GID of the caller
331     #
332     # @param gid GID object of the caller
333
334     def set_gid_caller(self, gid):
335         self.gidCaller = gid
336         # gid origin caller is the caller's gid by default
337         self.gidOriginCaller = gid
338
339     ##
340     # get the GID of the object
341
342     def get_gid_caller(self):
343         if not self.gidCaller:
344             self.decode()
345         return self.gidCaller
346
347     ##
348     # set the GID of the object
349     #
350     # @param gid GID object of the object
351
352     def set_gid_object(self, gid):
353         self.gidObject = gid
354
355     ##
356     # get the GID of the object
357
358     def get_gid_object(self):
359         if not self.gidObject:
360             self.decode()
361         return self.gidObject
362             
363     ##
364     # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
365     # 
366     def set_expiration(self, expiration):
367         expiration_datetime = utcparse (expiration)
368         if expiration_datetime is not None:
369             self.expiration = expiration_datetime
370         else:
371             logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
372
373     ##
374     # get the lifetime of the credential (always in datetime format)
375
376     def get_expiration(self):
377         if not self.expiration:
378             self.decode()
379         # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
380         return self.expiration
381
382     ##
383     # set the privileges
384     #
385     # @param privs either a comma-separated list of privileges of a Rights object
386
387     def set_privileges(self, privs):
388         if isinstance(privs, str):
389             self.privileges = Rights(string = privs)
390         else:
391             self.privileges = privs        
392
393     ##
394     # return the privileges as a Rights object
395
396     def get_privileges(self):
397         if not self.privileges:
398             self.decode()
399         return self.privileges
400
401     ##
402     # determine whether the credential allows a particular operation to be
403     # performed
404     #
405     # @param op_name string specifying name of operation ("lookup", "update", etc)
406
407     def can_perform(self, op_name):
408         rights = self.get_privileges()
409         
410         if not rights:
411             return False
412
413         return rights.can_perform(op_name)
414
415
416     ##
417     # Encode the attributes of the credential into an XML string    
418     # This should be done immediately before signing the credential.    
419     # WARNING:
420     # In general, a signed credential obtained externally should
421     # not be changed else the signature is no longer valid.  So, once
422     # you have loaded an existing signed credential, do not call encode() or sign() on it.
423
424     def encode(self):
425         # Create the XML document
426         doc = Document()
427         signed_cred = doc.createElement("signed-credential")
428
429         # Declare namespaces
430         # Note that credential/policy.xsd are really the PG schemas
431         # in a PL namespace.
432         # Note that delegation of credentials between the 2 only really works
433         # cause those schemas are identical.
434         # Also note these PG schemas talk about PG tickets and CM policies.
435         signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
436         signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
437         signed_cred.setAttribute("xsi:schemaLocation", "http://www.planet-lab.org/resources/sfa/ext/policy/1 http://www.planet-lab.org/resources/sfa/ext/policy/1/policy.xsd")
438
439         # PG says for those last 2:
440        #signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
441        # signed_cred.setAttribute("xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
442
443         doc.appendChild(signed_cred)  
444         
445         # Fill in the <credential> bit        
446         cred = doc.createElement("credential")
447         cred.setAttribute("xml:id", self.get_refid())
448         signed_cred.appendChild(cred)
449         append_sub(doc, cred, "type", "privilege")
450         append_sub(doc, cred, "serial", "8")
451         append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
452         append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
453         append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
454         append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
455         append_sub(doc, cred, "uuid", "")
456         if not self.expiration:
457             logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
458             self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
459         self.expiration = self.expiration.replace(microsecond=0)
460         append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
461         privileges = doc.createElement("privileges")
462         cred.appendChild(privileges)
463
464         if self.privileges:
465             rights = self.get_privileges()
466             for right in rights.rights:
467                 priv = doc.createElement("privilege")
468                 append_sub(doc, priv, "name", right.kind)
469                 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
470                 privileges.appendChild(priv)
471
472         # Add the parent credential if it exists
473         if self.parent:
474             sdoc = parseString(self.parent.get_xml())
475             # If the root node is a signed-credential (it should be), then
476             # get all its attributes and attach those to our signed_cred
477             # node.
478             # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
479             # and we need to include those again here or else their signature
480             # no longer matches on the credential.
481             # We expect three of these, but here we copy them all:
482             #  signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
483             # and from PG (PL is equivalent, as shown above):
484             #  signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
485             #  signed_cred.setAttribute("xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
486
487             # HOWEVER!
488             # PL now also declares these, with different URLs, so
489             # the code notices those attributes already existed with
490             # different values, and complains.
491             # This happens regularly on delegation now that PG and
492             # PL both declare the namespace with different URLs.
493             # If the content ever differs this is a problem,
494             # but for now it works - different URLs (values in the attributes)
495             # but the same actual schema, so using the PG schema
496             # on delegated-to-PL credentials works fine.
497
498             # Note: you could also not copy attributes
499             # which already exist. It appears that both PG and PL
500             # will actually validate a slicecred with a parent
501             # signed using PG namespaces and a child signed with PL
502             # namespaces over the whole thing. But I don't know
503             # if that is a bug in xmlsec1, an accident since
504             # the contents of the schemas are the same,
505             # or something else, but it seems odd. And this works.
506             parentRoot = sdoc.documentElement
507             if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
508                 for attrIx in range(0, parentRoot.attributes.length):
509                     attr = parentRoot.attributes.item(attrIx)
510                     # returns the old attribute of same name that was
511                     # on the credential
512                     # Below throws InUse exception if we forgot to clone the attribute first
513                     oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
514                     if oldAttr and oldAttr.value != attr.value:
515                         msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
516                         logger.warn(msg)
517                         #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
518
519             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
520             p = doc.createElement("parent")
521             p.appendChild(p_cred)
522             cred.appendChild(p)
523         # done handling parent credential
524
525         # Create the <signatures> tag
526         signatures = doc.createElement("signatures")
527         signed_cred.appendChild(signatures)
528
529         # Add any parent signatures
530         if self.parent:
531             for cur_cred in self.get_credential_list()[1:]:
532                 sdoc = parseString(cur_cred.get_signature().get_xml())
533                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
534                 signatures.appendChild(ele)
535                 
536         # Get the finished product
537         self.xml = doc.toxml()
538
539
540     def save_to_random_tmp_file(self):       
541         fp, filename = mkstemp(suffix='cred', text=True)
542         fp = os.fdopen(fp, "w")
543         self.save_to_file(filename, save_parents=True, filep=fp)
544         return filename
545     
546     def save_to_file(self, filename, save_parents=True, filep=None):
547         if not self.xml:
548             self.encode()
549         if filep:
550             f = filep 
551         else:
552             f = open(filename, "w")
553         f.write(self.xml)
554         f.close()
555
556     def save_to_string(self, save_parents=True):
557         if not self.xml:
558             self.encode()
559         return self.xml
560
561     def get_refid(self):
562         if not self.refid:
563             self.refid = 'ref0'
564         return self.refid
565
566     def set_refid(self, rid):
567         self.refid = rid
568
569     ##
570     # Figure out what refids exist, and update this credential's id
571     # so that it doesn't clobber the others.  Returns the refids of
572     # the parents.
573     
574     def updateRefID(self):
575         if not self.parent:
576             self.set_refid('ref0')
577             return []
578         
579         refs = []
580
581         next_cred = self.parent
582         while next_cred:
583             refs.append(next_cred.get_refid())
584             if next_cred.parent:
585                 next_cred = next_cred.parent
586             else:
587                 next_cred = None
588
589         
590         # Find a unique refid for this credential
591         rid = self.get_refid()
592         while rid in refs:
593             val = int(rid[3:])
594             rid = "ref%d" % (val + 1)
595
596         # Set the new refid
597         self.set_refid(rid)
598
599         # Return the set of parent credential ref ids
600         return refs
601
602     def get_xml(self):
603         if not self.xml:
604             self.encode()
605         return self.xml
606
607     ##
608     # Sign the XML file created by encode()
609     #
610     # WARNING:
611     # In general, a signed credential obtained externally should
612     # not be changed else the signature is no longer valid.  So, once
613     # you have loaded an existing signed credential, do not call encode() or sign() on it.
614
615     def sign(self):
616         if not self.issuer_privkey or not self.issuer_gid:
617             return
618         doc = parseString(self.get_xml())
619         sigs = doc.getElementsByTagName("signatures")[0]
620
621         # Create the signature template to be signed
622         signature = Signature()
623         signature.set_refid(self.get_refid())
624         sdoc = parseString(signature.get_xml())        
625         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
626         sigs.appendChild(sig_ele)
627
628         self.xml = doc.toxml()
629
630
631         # Split the issuer GID into multiple certificates if it's a chain
632         chain = GID(filename=self.issuer_gid)
633         gid_files = []
634         while chain:
635             gid_files.append(chain.save_to_random_tmp_file(False))
636             if chain.get_parent():
637                 chain = chain.get_parent()
638             else:
639                 chain = None
640
641
642         # Call out to xmlsec1 to sign it
643         ref = 'Sig_%s' % self.get_refid()
644         filename = self.save_to_random_tmp_file()
645         signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
646                  % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
647         os.remove(filename)
648
649         for gid_file in gid_files:
650             os.remove(gid_file)
651
652         self.xml = signed
653
654         # Update signatures
655         self.decode()       
656
657         
658     ##
659     # Retrieve the attributes of the credential from the XML.
660     # This is automatically called by the various get_* methods of
661     # this class and should not need to be called explicitly.
662
663     def decode(self):
664         if not self.xml:
665             return
666
667         doc = None
668         try:
669             doc = parseString(self.xml)
670         except ExpatError,e:
671             raise CredentialNotVerifiable("Malformed credential")
672         doc = parseString(self.xml)
673         sigs = []
674         signed_cred = doc.getElementsByTagName("signed-credential")
675
676         # Is this a signed-cred or just a cred?
677         if len(signed_cred) > 0:
678             creds = signed_cred[0].getElementsByTagName("credential")
679             signatures = signed_cred[0].getElementsByTagName("signatures")
680             if len(signatures) > 0:
681                 sigs = signatures[0].getElementsByTagName("Signature")
682         else:
683             creds = doc.getElementsByTagName("credential")
684         
685         if creds is None or len(creds) == 0:
686             # malformed cred file
687             raise CredentialNotVerifiable("Malformed XML: No credential tag found")
688
689         # Just take the first cred if there are more than one
690         cred = creds[0]
691
692         self.set_refid(cred.getAttribute("xml:id"))
693         self.set_expiration(utcparse(getTextNode(cred, "expires")))
694         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
695         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   
696
697
698         # Process privileges
699         privs = cred.getElementsByTagName("privileges")[0]
700         rlist = Rights()
701         for priv in privs.getElementsByTagName("privilege"):
702             kind = getTextNode(priv, "name")
703             deleg = str2bool(getTextNode(priv, "can_delegate"))
704             if kind == '*':
705                 # Convert * into the default privileges for the credential's type
706                 # Each inherits the delegatability from the * above
707                 _ , type = urn_to_hrn(self.gidObject.get_urn())
708                 rl = determine_rights(type, self.gidObject.get_urn())
709                 for r in rl.rights:
710                     r.delegate = deleg
711                     rlist.add(r)
712             else:
713                 rlist.add(Right(kind.strip(), deleg))
714         self.set_privileges(rlist)
715
716
717         # Is there a parent?
718         parent = cred.getElementsByTagName("parent")
719         if len(parent) > 0:
720             parent_doc = parent[0].getElementsByTagName("credential")[0]
721             parent_xml = parent_doc.toxml()
722             self.parent = Credential(string=parent_xml)
723             self.updateRefID()
724
725         # Assign the signatures to the credentials
726         for sig in sigs:
727             Sig = Signature(string=sig.toxml())
728
729             for cur_cred in self.get_credential_list():
730                 if cur_cred.get_refid() == Sig.get_refid():
731                     cur_cred.set_signature(Sig)
732                                     
733             
734     ##
735     # Verify
736     #   trusted_certs: A list of trusted GID filenames (not GID objects!) 
737     #                  Chaining is not supported within the GIDs by xmlsec1.
738     #
739     #   trusted_certs_required: Should usually be true. Set False means an
740     #                 empty list of trusted_certs would still let this method pass.
741     #                 It just skips xmlsec1 verification et al. Only used by some utils
742     #    
743     # Verify that:
744     # . All of the signatures are valid and that the issuers trace back
745     #   to trusted roots (performed by xmlsec1)
746     # . The XML matches the credential schema
747     # . That the issuer of the credential is the authority in the target's urn
748     #    . In the case of a delegated credential, this must be true of the root
749     # . That all of the gids presented in the credential are valid
750     #    . Including verifying GID chains, and includ the issuer
751     # . The credential is not expired
752     #
753     # -- For Delegates (credentials with parents)
754     # . The privileges must be a subset of the parent credentials
755     # . The privileges must have "can_delegate" set for each delegated privilege
756     # . The target gid must be the same between child and parents
757     # . The expiry time on the child must be no later than the parent
758     # . The signer of the child must be the owner of the parent
759     #
760     # -- Verify does *NOT*
761     # . ensure that an xmlrpc client's gid matches a credential gid, that
762     #   must be done elsewhere
763     #
764     # @param trusted_certs: The certificates of trusted CA certificates
765     def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
766         if not self.xml:
767             self.decode()
768
769         # validate against RelaxNG schema
770         if HAVELXML:
771             if schema and os.path.exists(schema):
772                 tree = etree.parse(StringIO(self.xml))
773                 schema_doc = etree.parse(schema)
774                 xmlschema = etree.XMLSchema(schema_doc)
775                 if not xmlschema.validate(tree):
776                     error = xmlschema.error_log.last_error
777                     message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
778                     raise CredentialNotVerifiable(message)
779
780         if trusted_certs_required and trusted_certs is None:
781             trusted_certs = []
782
783 #        trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
784         trusted_cert_objects = []
785         ok_trusted_certs = []
786         # If caller explicitly passed in None that means skip cert chain validation.
787         # Strange and not typical
788         if trusted_certs is not None:
789             for f in trusted_certs:
790                 try:
791                     # Failures here include unreadable files
792                     # or non PEM files
793                     trusted_cert_objects.append(GID(filename=f))
794                     ok_trusted_certs.append(f)
795                 except Exception, exc:
796                     logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
797             trusted_certs = ok_trusted_certs
798
799         # make sure it is not expired
800         if self.get_expiration() < datetime.datetime.utcnow():
801             raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.strftime(SFATIME_FORMAT)))
802
803         # Verify the signatures
804         filename = self.save_to_random_tmp_file()
805         if trusted_certs is not None:
806             cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
807
808         # If caller explicitly passed in None that means skip cert chain validation.
809         # - Strange and not typical
810         if trusted_certs is not None:
811             # Verify the gids of this cred and of its parents
812             for cur_cred in self.get_credential_list():
813                 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
814                 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
815
816         refs = []
817         refs.append("Sig_%s" % self.get_refid())
818
819         parentRefs = self.updateRefID()
820         for ref in parentRefs:
821             refs.append("Sig_%s" % ref)
822
823         for ref in refs:
824             # If caller explicitly passed in None that means skip xmlsec1 validation.
825             # Strange and not typical
826             if trusted_certs is None:
827                 break
828
829 #            print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
830 #                (self.xmlsec_path, ref, cert_args, filename)
831             verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
832                             % (self.xmlsec_path, ref, cert_args, filename)).read()
833             if not verified.strip().startswith("OK"):
834                 # xmlsec errors have a msg= which is the interesting bit.
835                 mstart = verified.find("msg=")
836                 msg = ""
837                 if mstart > -1 and len(verified) > 4:
838                     mstart = mstart + 4
839                     mend = verified.find('\\', mstart)
840                     msg = verified[mstart:mend]
841                 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
842         os.remove(filename)
843
844         # Verify the parents (delegation)
845         if self.parent:
846             self.verify_parent(self.parent)
847
848         # Make sure the issuer is the target's authority, and is
849         # itself a valid GID
850         self.verify_issuer(trusted_cert_objects)
851         return True
852
853     ##
854     # Creates a list of the credential and its parents, with the root 
855     # (original delegated credential) as the last item in the list
856     def get_credential_list(self):    
857         cur_cred = self
858         list = []
859         while cur_cred:
860             list.append(cur_cred)
861             if cur_cred.parent:
862                 cur_cred = cur_cred.parent
863             else:
864                 cur_cred = None
865         return list
866     
867     ##
868     # Make sure the credential's target gid (a) was signed by or (b)
869     # is the same as the entity that signed the original credential,
870     # or (c) is an authority over the target's namespace.
871     # Also ensure that the credential issuer / signer itself has a valid
872     # GID signature chain (signed by an authority with namespace rights).
873     def verify_issuer(self, trusted_gids):
874         root_cred = self.get_credential_list()[-1]
875         root_target_gid = root_cred.get_gid_object()
876         root_cred_signer = root_cred.get_signature().get_issuer_gid()
877
878         # Case 1:
879         # Allow non authority to sign target and cred about target.
880         #
881         # Why do we need to allow non authorities to sign?
882         # If in the target gid validation step we correctly
883         # checked that the target is only signed by an authority,
884         # then this is just a special case of case 3.
885         # This short-circuit is the common case currently -
886         # and cause GID validation doesn't check 'authority',
887         # this allows users to generate valid slice credentials.
888         if root_target_gid.is_signed_by_cert(root_cred_signer):
889             # cred signer matches target signer, return success
890             return
891
892         # Case 2:
893         # Allow someone to sign credential about themeselves. Used?
894         # If not, remove this.
895         #root_target_gid_str = root_target_gid.save_to_string()
896         #root_cred_signer_str = root_cred_signer.save_to_string()
897         #if root_target_gid_str == root_cred_signer_str:
898         #    # cred signer is target, return success
899         #    return
900
901         # Case 3:
902
903         # root_cred_signer is not the target_gid
904         # So this is a different gid that we have not verified.
905         # xmlsec1 verified the cert chain on this already, but
906         # it hasn't verified that the gid meets the HRN namespace
907         # requirements.
908         # Below we'll ensure that it is an authority.
909         # But we haven't verified that it is _signed by_ an authority
910         # We also don't know if xmlsec1 requires that cert signers
911         # are marked as CAs.
912
913         # Note that if verify() gave us no trusted_gids then this
914         # call will fail. So skip it if we have no trusted_gids
915         if trusted_gids and len(trusted_gids) > 0:
916             root_cred_signer.verify_chain(trusted_gids)
917         else:
918             logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
919
920         # See if the signer is an authority over the domain of the target.
921         # There are multiple types of authority - accept them all here
922         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
923         root_cred_signer_type = root_cred_signer.get_type()
924         if (root_cred_signer_type.find('authority') == 0):
925             #logger.debug('Cred signer is an authority')
926             # signer is an authority, see if target is in authority's domain
927             signerhrn = root_cred_signer.get_hrn()
928             if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
929                 return
930
931         # We've required that the credential be signed by an authority
932         # for that domain. Reasonable and probably correct.
933         # A looser model would also allow the signer to be an authority
934         # in my control framework - eg My CA or CH. Even if it is not
935         # the CH that issued these, eg, user credentials.
936
937         # Give up, credential does not pass issuer verification
938
939         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()))
940
941
942     ##
943     # -- For Delegates (credentials with parents) verify that:
944     # . The privileges must be a subset of the parent credentials
945     # . The privileges must have "can_delegate" set for each delegated privilege
946     # . The target gid must be the same between child and parents
947     # . The expiry time on the child must be no later than the parent
948     # . The signer of the child must be the owner of the parent        
949     def verify_parent(self, parent_cred):
950         # make sure the rights given to the child are a subset of the
951         # parents rights (and check delegate bits)
952         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
953             raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
954                 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
955                 self.get_privileges().save_to_string())
956
957         # make sure my target gid is the same as the parent's
958         if not parent_cred.get_gid_object().save_to_string() == \
959            self.get_gid_object().save_to_string():
960             raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
961
962         # make sure my expiry time is <= my parent's
963         if not parent_cred.get_expiration() >= self.get_expiration():
964             raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
965
966         # make sure my signer is the parent's caller
967         if not parent_cred.get_gid_caller().save_to_string(False) == \
968            self.get_signature().get_issuer_gid().save_to_string(False):
969             raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
970                 
971         # Recurse
972         if parent_cred.parent:
973             parent_cred.verify_parent(parent_cred.parent)
974
975
976     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
977         """
978         Return a delegated copy of this credential, delegated to the 
979         specified gid's user.    
980         """
981         # get the gid of the object we are delegating
982         object_gid = self.get_gid_object()
983         object_hrn = object_gid.get_hrn()        
984  
985         # the hrn of the user who will be delegated to
986         delegee_gid = GID(filename=delegee_gidfile)
987         delegee_hrn = delegee_gid.get_hrn()
988   
989         #user_key = Keypair(filename=keyfile)
990         #user_hrn = self.get_gid_caller().get_hrn()
991         subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
992         dcred = Credential(subject=subject_string)
993         dcred.set_gid_caller(delegee_gid)
994         dcred.set_gid_object(object_gid)
995         dcred.set_parent(self)
996         dcred.set_expiration(self.get_expiration())
997         dcred.set_privileges(self.get_privileges())
998         dcred.get_privileges().delegate_all_privileges(True)
999         #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1000         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1001         dcred.encode()
1002         dcred.sign()
1003
1004         return dcred
1005
1006     # only informative
1007     def get_filename(self):
1008         return getattr(self,'filename',None)
1009     
1010     def actual_caller_hrn (self):
1011         """a helper method used by some API calls like e.g. Allocate
1012         to try and find out who really is the original caller
1013         
1014         This admittedly is a bit of a hack, please USE IN LAST RESORT
1015         
1016         This code uses a heuristic to identify a delegated credential
1017
1018         A first known restriction if for traffic that gets through a slice manager
1019         in this case the hrn reported is the one from the last SM in the call graph
1020         which is not at all what is meant here"""
1021
1022         caller_hrn = self.get_gid_caller().get_hrn()
1023         issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1024         subject_hrn = self.get_gid_object().get_hrn()
1025         # if we find that the caller_hrn is an immediate descendant of the issuer, then
1026         # this seems to be a 'regular' credential
1027         if caller_hrn.startswith(issuer_hrn): 
1028             actual_caller_hrn=caller_hrn
1029         # else this looks like a delegated credential, and the real caller is the issuer
1030         else:
1031             actual_caller_hrn=issuer_hrn
1032         logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"%(caller_hrn,issuer_hrn,actual_caller_hrn))
1033         return actual_caller_hrn
1034             
1035     ##
1036     # Dump the contents of a credential to stdout in human-readable format
1037     #
1038     # @param dump_parents If true, also dump the parent certificates
1039     def dump (self, *args, **kwargs):
1040         print self.dump_string(*args, **kwargs)
1041
1042     # show_xml is ignored
1043     def dump_string(self, dump_parents=False, show_xml=None):
1044         result=""
1045         result += "CREDENTIAL %s\n" % self.get_subject()
1046         filename=self.get_filename()
1047         if filename: result += "Filename %s\n"%filename
1048         privileges = self.get_privileges()
1049         if privileges:
1050             result += "      privs: %s\n" % privileges.save_to_string()
1051         else:
1052             result += "      privs: \n" 
1053         gidCaller = self.get_gid_caller()
1054         if gidCaller:
1055             result += "  gidCaller:\n"
1056             result += gidCaller.dump_string(8, dump_parents)
1057
1058         if self.get_signature():
1059             print "  gidIssuer:"
1060             self.get_signature().get_issuer_gid().dump(8, dump_parents)
1061
1062         if self.expiration:
1063             print "  expiration:", self.expiration.strftime(SFATIME_FORMAT)
1064
1065         gidObject = self.get_gid_object()
1066         if gidObject:
1067             result += "  gidObject:\n"
1068             result += gidObject.dump_string(8, dump_parents)
1069
1070         if self.parent and dump_parents:
1071             result += "\nPARENT"
1072             result += self.parent.dump_string(True)
1073
1074         return result