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