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