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