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