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