narrower text again
[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'" % \
516                               (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" % \
803                                           (self.get_summary_tostring(),
804                                            self.expiration.strftime(SFATIME_FORMAT)))
805
806         # Verify the signatures
807         filename = self.save_to_random_tmp_file()
808         if trusted_certs is not None:
809             cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
810
811         # If caller explicitly passed in None that means skip cert chain validation.
812         # - Strange and not typical
813         if trusted_certs is not None:
814             # Verify the gids of this cred and of its parents
815             for cur_cred in self.get_credential_list():
816                 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
817                 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
818
819         refs = []
820         refs.append("Sig_%s" % self.get_refid())
821
822         parentRefs = self.updateRefID()
823         for ref in parentRefs:
824             refs.append("Sig_%s" % ref)
825
826         for ref in refs:
827             # If caller explicitly passed in None that means skip xmlsec1 validation.
828             # Strange and not typical
829             if trusted_certs is None:
830                 break
831
832             command = '{} --verify --node-id "{}" {} {} 2>&1'.\
833                       format(self.xmlsec_path, ref, cert_args, filename)
834             logger.debug("Running '{}'".format(command))
835             verified = os.popen(command).read()
836             logger.debug("xmlsec command returned {}".format(verified))
837             if not verified.strip().startswith("OK"):
838                 # xmlsec errors have a msg= which is the interesting bit.
839                 mstart = verified.find("msg=")
840                 msg = ""
841                 if mstart > -1 and len(verified) > 4:
842                     mstart = mstart + 4
843                     mend = verified.find('\\', mstart)
844                     msg = verified[mstart:mend]
845                 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s"
846                                               "using Signature ID %s: %s %s" % \
847                                               (self.get_summary_tostring(),
848                                                ref, msg, verified.strip()))
849         os.remove(filename)
850
851         # Verify the parents (delegation)
852         if self.parent:
853             self.verify_parent(self.parent)
854
855         # Make sure the issuer is the target's authority, and is
856         # itself a valid GID
857         self.verify_issuer(trusted_cert_objects)
858         return True
859
860     ##
861     # Creates a list of the credential and its parents, with the root 
862     # (original delegated credential) as the last item in the list
863     def get_credential_list(self):    
864         cur_cred = self
865         list = []
866         while cur_cred:
867             list.append(cur_cred)
868             if cur_cred.parent:
869                 cur_cred = cur_cred.parent
870             else:
871                 cur_cred = None
872         return list
873     
874     ##
875     # Make sure the credential's target gid (a) was signed by or (b)
876     # is the same as the entity that signed the original credential,
877     # or (c) is an authority over the target's namespace.
878     # Also ensure that the credential issuer / signer itself has a valid
879     # GID signature chain (signed by an authority with namespace rights).
880     def verify_issuer(self, trusted_gids):
881         root_cred = self.get_credential_list()[-1]
882         root_target_gid = root_cred.get_gid_object()
883         root_cred_signer = root_cred.get_signature().get_issuer_gid()
884
885         # Case 1:
886         # Allow non authority to sign target and cred about target.
887         #
888         # Why do we need to allow non authorities to sign?
889         # If in the target gid validation step we correctly
890         # checked that the target is only signed by an authority,
891         # then this is just a special case of case 3.
892         # This short-circuit is the common case currently -
893         # and cause GID validation doesn't check 'authority',
894         # this allows users to generate valid slice credentials.
895         if root_target_gid.is_signed_by_cert(root_cred_signer):
896             # cred signer matches target signer, return success
897             return
898
899         # Case 2:
900         # Allow someone to sign credential about themeselves. Used?
901         # If not, remove this.
902         #root_target_gid_str = root_target_gid.save_to_string()
903         #root_cred_signer_str = root_cred_signer.save_to_string()
904         #if root_target_gid_str == root_cred_signer_str:
905         #    # cred signer is target, return success
906         #    return
907
908         # Case 3:
909
910         # root_cred_signer is not the target_gid
911         # So this is a different gid that we have not verified.
912         # xmlsec1 verified the cert chain on this already, but
913         # it hasn't verified that the gid meets the HRN namespace
914         # requirements.
915         # Below we'll ensure that it is an authority.
916         # But we haven't verified that it is _signed by_ an authority
917         # We also don't know if xmlsec1 requires that cert signers
918         # are marked as CAs.
919
920         # Note that if verify() gave us no trusted_gids then this
921         # call will fail. So skip it if we have no trusted_gids
922         if trusted_gids and len(trusted_gids) > 0:
923             root_cred_signer.verify_chain(trusted_gids)
924         else:
925             logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
926
927         # See if the signer is an authority over the domain of the target.
928         # There are multiple types of authority - accept them all here
929         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
930         root_cred_signer_type = root_cred_signer.get_type()
931         if (root_cred_signer_type.find('authority') == 0):
932             #logger.debug('Cred signer is an authority')
933             # signer is an authority, see if target is in authority's domain
934             signerhrn = root_cred_signer.get_hrn()
935             if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
936                 return
937
938         # We've required that the credential be signed by an authority
939         # for that domain. Reasonable and probably correct.
940         # A looser model would also allow the signer to be an authority
941         # in my control framework - eg My CA or CH. Even if it is not
942         # the CH that issued these, eg, user credentials.
943
944         # Give up, credential does not pass issuer verification
945
946         raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred signer %s not the trusted authority for Cred target %s" % \
947                                       (self.gidCaller.get_urn(), self.gidObject.get_urn(), root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
948
949
950     ##
951     # -- For Delegates (credentials with parents) verify that:
952     # . The privileges must be a subset of the parent credentials
953     # . The privileges must have "can_delegate" set for each delegated privilege
954     # . The target gid must be the same between child and parents
955     # . The expiry time on the child must be no later than the parent
956     # . The signer of the child must be the owner of the parent        
957     def verify_parent(self, parent_cred):
958         # make sure the rights given to the child are a subset of the
959         # parents rights (and check delegate bits)
960         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
961             raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
962                 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % \
963                                                                  (self.get_summary_tostring(), self.get_refid())) +
964                 self.get_privileges().save_to_string())
965
966         # make sure my target gid is the same as the parent's
967         if not parent_cred.get_gid_object().save_to_string() == \
968            self.get_gid_object().save_to_string():
969             raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % \
970                                           (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
971
972         # make sure my expiry time is <= my parent's
973         if not parent_cred.get_expiration() >= self.get_expiration():
974             raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % \
975                                           (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
976
977         # make sure my signer is the parent's caller
978         if not parent_cred.get_gid_caller().save_to_string(False) == \
979            self.get_signature().get_issuer_gid().save_to_string(False):
980             raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % \
981                                           (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
982                 
983         # Recurse
984         if parent_cred.parent:
985             parent_cred.verify_parent(parent_cred.parent)
986
987
988     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
989         """
990         Return a delegated copy of this credential, delegated to the 
991         specified gid's user.    
992         """
993         # get the gid of the object we are delegating
994         object_gid = self.get_gid_object()
995         object_hrn = object_gid.get_hrn()        
996  
997         # the hrn of the user who will be delegated to
998         delegee_gid = GID(filename=delegee_gidfile)
999         delegee_hrn = delegee_gid.get_hrn()
1000   
1001         #user_key = Keypair(filename=keyfile)
1002         #user_hrn = self.get_gid_caller().get_hrn()
1003         subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1004         dcred = Credential(subject=subject_string)
1005         dcred.set_gid_caller(delegee_gid)
1006         dcred.set_gid_object(object_gid)
1007         dcred.set_parent(self)
1008         dcred.set_expiration(self.get_expiration())
1009         dcred.set_privileges(self.get_privileges())
1010         dcred.get_privileges().delegate_all_privileges(True)
1011         #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1012         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1013         dcred.encode()
1014         dcred.sign()
1015
1016         return dcred
1017
1018     # only informative
1019     def get_filename(self):
1020         return getattr(self,'filename',None)
1021     
1022     def actual_caller_hrn (self):
1023         """a helper method used by some API calls like e.g. Allocate
1024         to try and find out who really is the original caller
1025         
1026         This admittedly is a bit of a hack, please USE IN LAST RESORT
1027         
1028         This code uses a heuristic to identify a delegated credential
1029
1030         A first known restriction if for traffic that gets through a slice manager
1031         in this case the hrn reported is the one from the last SM in the call graph
1032         which is not at all what is meant here"""
1033
1034         caller_hrn = self.get_gid_caller().get_hrn()
1035         issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1036         subject_hrn = self.get_gid_object().get_hrn()
1037         # if we find that the caller_hrn is an immediate descendant of the issuer, then
1038         # this seems to be a 'regular' credential
1039         if caller_hrn.startswith(issuer_hrn): 
1040             actual_caller_hrn=caller_hrn
1041         # else this looks like a delegated credential, and the real caller is the issuer
1042         else:
1043             actual_caller_hrn=issuer_hrn
1044         logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"%(caller_hrn,issuer_hrn,actual_caller_hrn))
1045         return actual_caller_hrn
1046             
1047     ##
1048     # Dump the contents of a credential to stdout in human-readable format
1049     #
1050     # @param dump_parents If true, also dump the parent certificates
1051     def dump (self, *args, **kwargs):
1052         print self.dump_string(*args, **kwargs)
1053
1054     # show_xml is ignored
1055     def dump_string(self, dump_parents=False, show_xml=None):
1056         result=""
1057         result += "CREDENTIAL %s\n" % self.get_subject()
1058         filename=self.get_filename()
1059         if filename: result += "Filename %s\n"%filename
1060         privileges = self.get_privileges()
1061         if privileges:
1062             result += "      privs: %s\n" % privileges.save_to_string()
1063         else:
1064             result += "      privs: \n" 
1065         gidCaller = self.get_gid_caller()
1066         if gidCaller:
1067             result += "  gidCaller:\n"
1068             result += gidCaller.dump_string(8, dump_parents)
1069
1070         if self.get_signature():
1071             print "  gidIssuer:"
1072             self.get_signature().get_issuer_gid().dump(8, dump_parents)
1073
1074         if self.expiration:
1075             print "  expiration:", self.expiration.strftime(SFATIME_FORMAT)
1076
1077         gidObject = self.get_gid_object()
1078         if gidObject:
1079             result += "  gidObject:\n"
1080             result += gidObject.dump_string(8, dump_parents)
1081
1082         if self.parent and dump_parents:
1083             result += "\nPARENT"
1084             result += self.parent.dump_string(True)
1085
1086         return result