fix typo
[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, determine_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         signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
430         signed_cred.setAttribute("xsi:schemaLocation", "http://www.planet-lab.org/resources/sfa/ext/policy/1 http://www.planet-lab.org/resources/sfa/ext/policy.xsd")
431
432         doc.appendChild(signed_cred)  
433         
434         # Fill in the <credential> bit        
435         cred = doc.createElement("credential")
436         cred.setAttribute("xml:id", self.get_refid())
437         signed_cred.appendChild(cred)
438         append_sub(doc, cred, "type", "privilege")
439         append_sub(doc, cred, "serial", "8")
440         append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
441         append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
442         append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
443         append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
444         append_sub(doc, cred, "uuid", "")
445         if not self.expiration:
446             self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
447         self.expiration = self.expiration.replace(microsecond=0)
448         append_sub(doc, cred, "expires", self.expiration.isoformat())
449         privileges = doc.createElement("privileges")
450         cred.appendChild(privileges)
451
452         if self.privileges:
453             rights = self.get_privileges()
454             for right in rights.rights:
455                 priv = doc.createElement("privilege")
456                 append_sub(doc, priv, "name", right.kind)
457                 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
458                 privileges.appendChild(priv)
459
460         # Add the parent credential if it exists
461         if self.parent:
462             sdoc = parseString(self.parent.get_xml())
463             # If the root node is a signed-credential (it should be), then
464             # get all its attributes and attach those to our signed_cred
465             # node.
466             # Specifically, PG adds attributes for namespaces (which is reasonable),
467             # and we need to include those again here or else their signature
468             # no longer matches on the credential.
469             # We expect three of these, but here we copy them all:
470 #        signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
471 #        signed_cred.setAttribute("xsinoNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
472 #        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")
473             parentRoot = sdoc.documentElement
474             if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
475                 for attrIx in range(0, parentRoot.attributes.length):
476                     attr = parentRoot.attributes.item(attrIx)
477                     # returns the old attribute of same name that was
478                     # on the credential
479                     # Below throws InUse exception if we forgot to clone the attribute first
480                     oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
481                     if oldAttr and oldAttr.value != attr.value:
482                         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)
483                         logger.error(msg)
484                         raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
485
486             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
487             p = doc.createElement("parent")
488             p.appendChild(p_cred)
489             cred.appendChild(p)
490         # done handling parent credential
491
492         # Create the <signatures> tag
493         signatures = doc.createElement("signatures")
494         signed_cred.appendChild(signatures)
495
496         # Add any parent signatures
497         if self.parent:
498             for cur_cred in self.get_credential_list()[1:]:
499                 sdoc = parseString(cur_cred.get_signature().get_xml())
500                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
501                 signatures.appendChild(ele)
502                 
503         # Get the finished product
504         self.xml = doc.toxml()
505
506
507     def save_to_random_tmp_file(self):       
508         fp, filename = mkstemp(suffix='cred', text=True)
509         fp = os.fdopen(fp, "w")
510         self.save_to_file(filename, save_parents=True, filep=fp)
511         return filename
512     
513     def save_to_file(self, filename, save_parents=True, filep=None):
514         if not self.xml:
515             self.encode()
516         if filep:
517             f = filep 
518         else:
519             f = open(filename, "w")
520         f.write(self.xml)
521         f.close()
522
523     def save_to_string(self, save_parents=True):
524         if not self.xml:
525             self.encode()
526         return self.xml
527
528     def get_refid(self):
529         if not self.refid:
530             self.refid = 'ref0'
531         return self.refid
532
533     def set_refid(self, rid):
534         self.refid = rid
535
536     ##
537     # Figure out what refids exist, and update this credential's id
538     # so that it doesn't clobber the others.  Returns the refids of
539     # the parents.
540     
541     def updateRefID(self):
542         if not self.parent:
543             self.set_refid('ref0')
544             return []
545         
546         refs = []
547
548         next_cred = self.parent
549         while next_cred:
550             refs.append(next_cred.get_refid())
551             if next_cred.parent:
552                 next_cred = next_cred.parent
553             else:
554                 next_cred = None
555
556         
557         # Find a unique refid for this credential
558         rid = self.get_refid()
559         while rid in refs:
560             val = int(rid[3:])
561             rid = "ref%d" % (val + 1)
562
563         # Set the new refid
564         self.set_refid(rid)
565
566         # Return the set of parent credential ref ids
567         return refs
568
569     def get_xml(self):
570         if not self.xml:
571             self.encode()
572         return self.xml
573
574     ##
575     # Sign the XML file created by encode()
576     #
577     # WARNING:
578     # In general, a signed credential obtained externally should
579     # not be changed else the signature is no longer valid.  So, once
580     # you have loaded an existing signed credential, do not call encode() or sign() on it.
581
582     def sign(self):
583         if not self.issuer_privkey or not self.issuer_gid:
584             return
585         doc = parseString(self.get_xml())
586         sigs = doc.getElementsByTagName("signatures")[0]
587
588         # Create the signature template to be signed
589         signature = Signature()
590         signature.set_refid(self.get_refid())
591         sdoc = parseString(signature.get_xml())        
592         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
593         sigs.appendChild(sig_ele)
594
595         self.xml = doc.toxml()
596
597
598         # Split the issuer GID into multiple certificates if it's a chain
599         chain = GID(filename=self.issuer_gid)
600         gid_files = []
601         while chain:
602             gid_files.append(chain.save_to_random_tmp_file(False))
603             if chain.get_parent():
604                 chain = chain.get_parent()
605             else:
606                 chain = None
607
608
609         # Call out to xmlsec1 to sign it
610         ref = 'Sig_%s' % self.get_refid()
611         filename = self.save_to_random_tmp_file()
612         signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
613                  % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
614         os.remove(filename)
615
616         for gid_file in gid_files:
617             os.remove(gid_file)
618
619         self.xml = signed
620
621         # This is no longer a legacy credential
622         if self.legacy:
623             self.legacy = None
624
625         # Update signatures
626         self.decode()       
627
628         
629     ##
630     # Retrieve the attributes of the credential from the XML.
631     # This is automatically called by the various get_* methods of
632     # this class and should not need to be called explicitly.
633
634     def decode(self):
635         if not self.xml:
636             return
637         doc = parseString(self.xml)
638         sigs = []
639         signed_cred = doc.getElementsByTagName("signed-credential")
640
641         # Is this a signed-cred or just a cred?
642         if len(signed_cred) > 0:
643             cred = signed_cred[0].getElementsByTagName("credential")[0]
644             signatures = signed_cred[0].getElementsByTagName("signatures")
645             if len(signatures) > 0:
646                 sigs = signatures[0].getElementsByTagName("Signature")
647         else:
648             cred = doc.getElementsByTagName("credential")[0]
649         
650
651         self.set_refid(cred.getAttribute("xml:id"))
652         self.set_expiration(utcparse(getTextNode(cred, "expires")))
653         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
654         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   
655
656
657         # Process privileges
658         privs = cred.getElementsByTagName("privileges")[0]
659         rlist = Rights()
660         for priv in privs.getElementsByTagName("privilege"):
661             kind = getTextNode(priv, "name")
662             deleg = str2bool(getTextNode(priv, "can_delegate"))
663             if kind == '*':
664                 # Convert * into the default privileges for the credential's type
665                 # Each inherits the delegatability from the * above
666                 _ , type = urn_to_hrn(self.gidObject.get_urn())
667                 rl = determine_rights(type, self.gidObject.get_urn())
668                 for r in rl.rights:
669                     r.delegate = deleg
670                     rlist.add(r)
671             else:
672                 rlist.add(Right(kind.strip(), deleg))
673         self.set_privileges(rlist)
674
675
676         # Is there a parent?
677         parent = cred.getElementsByTagName("parent")
678         if len(parent) > 0:
679             parent_doc = parent[0].getElementsByTagName("credential")[0]
680             parent_xml = parent_doc.toxml()
681             self.parent = Credential(string=parent_xml)
682             self.updateRefID()
683
684         # Assign the signatures to the credentials
685         for sig in sigs:
686             Sig = Signature(string=sig.toxml())
687
688             for cur_cred in self.get_credential_list():
689                 if cur_cred.get_refid() == Sig.get_refid():
690                     cur_cred.set_signature(Sig)
691                                     
692             
693     ##
694     # Verify
695     #   trusted_certs: A list of trusted GID filenames (not GID objects!) 
696     #                  Chaining is not supported within the GIDs by xmlsec1.
697     #
698     #   trusted_certs_required: Should usually be true. Set False means an
699     #                 empty list of trusted_certs would still let this method pass.
700     #                 It just skips xmlsec1 verification et al. Only used by some utils
701     #    
702     # Verify that:
703     # . All of the signatures are valid and that the issuers trace back
704     #   to trusted roots (performed by xmlsec1)
705     # . The XML matches the credential schema
706     # . That the issuer of the credential is the authority in the target's urn
707     #    . In the case of a delegated credential, this must be true of the root
708     # . That all of the gids presented in the credential are valid
709     # . The credential is not expired
710     #
711     # -- For Delegates (credentials with parents)
712     # . The privileges must be a subset of the parent credentials
713     # . The privileges must have "can_delegate" set for each delegated privilege
714     # . The target gid must be the same between child and parents
715     # . The expiry time on the child must be no later than the parent
716     # . The signer of the child must be the owner of the parent
717     #
718     # -- Verify does *NOT*
719     # . ensure that an xmlrpc client's gid matches a credential gid, that
720     #   must be done elsewhere
721     #
722     # @param trusted_certs: The certificates of trusted CA certificates
723     def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
724         if not self.xml:
725             self.decode()
726
727         # validate against RelaxNG schema
728         if not self.legacy:
729             if schema and os.path.exists(schema):
730                 tree = etree.parse(StringIO(self.xml))
731                 schema_doc = etree.parse(schema)
732                 xmlschema = etree.XMLSchema(schema_doc)
733                 if not xmlschema.validate(tree):
734                     error = xmlschema.error_log.last_error
735                     message = "%s (line %s)" % (error.message, error.line)
736                     raise CredentialNotVerifiable(message)        
737
738         if trusted_certs_required and trusted_certs is None:
739             trusted_certs = []
740
741 #        trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
742         trusted_cert_objects = []
743         ok_trusted_certs = []
744         # If caller explicitly passed in None that means skip cert chain validation.
745         # Strange and not typical
746         if trusted_certs is not None:
747             for f in trusted_certs:
748                 try:
749                     # Failures here include unreadable files
750                     # or non PEM files
751                     trusted_cert_objects.append(GID(filename=f))
752                     ok_trusted_certs.append(f)
753                 except Exception, exc:
754                     logger.error("Failed to load trusted cert from %s: %r", f, exc)
755             trusted_certs = ok_trusted_certs
756
757         # Use legacy verification if this is a legacy credential
758         if self.legacy:
759             self.legacy.verify_chain(trusted_cert_objects)
760             if self.legacy.client_gid:
761                 self.legacy.client_gid.verify_chain(trusted_cert_objects)
762             if self.legacy.object_gid:
763                 self.legacy.object_gid.verify_chain(trusted_cert_objects)
764             return True
765         
766         # make sure it is not expired
767         if self.get_expiration() < datetime.datetime.utcnow():
768             raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
769
770         # Verify the signatures
771         filename = self.save_to_random_tmp_file()
772         if trusted_certs is not None:
773             cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
774
775         # If caller explicitly passed in None that means skip cert chain validation.
776         # Strange and not typical
777         if trusted_certs is not None:
778             # Verify the gids of this cred and of its parents
779             for cur_cred in self.get_credential_list():
780                 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
781                 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
782
783         refs = []
784         refs.append("Sig_%s" % self.get_refid())
785
786         parentRefs = self.updateRefID()
787         for ref in parentRefs:
788             refs.append("Sig_%s" % ref)
789
790         for ref in refs:
791             # If caller explicitly passed in None that means skip xmlsec1 validation.
792             # Strange and not typical
793             if trusted_certs is None:
794                 break
795
796 #            print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
797 #                (self.xmlsec_path, ref, cert_args, filename)
798             verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
799                             % (self.xmlsec_path, ref, cert_args, filename)).read()
800             if not verified.strip().startswith("OK"):
801                 # xmlsec errors have a msg= which is the interesting bit.
802                 mstart = verified.find("msg=")
803                 msg = ""
804                 if mstart > -1 and len(verified) > 4:
805                     mstart = mstart + 4
806                     mend = verified.find('\\', mstart)
807                     msg = verified[mstart:mend]
808                 raise CredentialNotVerifiable("xmlsec1 error verifying cred using Signature ID %s: %s %s" % (ref, msg, verified.strip()))
809         os.remove(filename)
810
811         # Verify the parents (delegation)
812         if self.parent:
813             self.verify_parent(self.parent)
814
815         # Make sure the issuer is the target's authority
816         self.verify_issuer()
817         return True
818
819     ##
820     # Creates a list of the credential and its parents, with the root 
821     # (original delegated credential) as the last item in the list
822     def get_credential_list(self):    
823         cur_cred = self
824         list = []
825         while cur_cred:
826             list.append(cur_cred)
827             if cur_cred.parent:
828                 cur_cred = cur_cred.parent
829             else:
830                 cur_cred = None
831         return list
832     
833     ##
834     # Make sure the credential's target gid was signed by (or is the same) the entity that signed
835     # the original credential or an authority over that namespace.
836     def verify_issuer(self):                
837         root_cred = self.get_credential_list()[-1]
838         root_target_gid = root_cred.get_gid_object()
839         root_cred_signer = root_cred.get_signature().get_issuer_gid()
840
841         if root_target_gid.is_signed_by_cert(root_cred_signer):
842             # cred signer matches target signer, return success
843             return
844
845         root_target_gid_str = root_target_gid.save_to_string()
846         root_cred_signer_str = root_cred_signer.save_to_string()
847         if root_target_gid_str == root_cred_signer_str:
848             # cred signer is target, return success
849             return
850
851         # See if it the signer is an authority over the domain of the target
852         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
853         root_cred_signer_type = root_cred_signer.get_type()
854         if (root_cred_signer_type == 'authority'):
855             #sfa_logger.debug('Cred signer is an authority')
856             # signer is an authority, see if target is in authority's domain
857             hrn = root_cred_signer.get_hrn()
858             if root_target_gid.get_hrn().startswith(hrn):
859                 return
860
861         # We've required that the credential be signed by an authority
862         # for that domain. Reasonable and probably correct.
863         # A looser model would also allow the signer to be an authority
864         # in my control framework - eg My CA or CH. Even if it is not
865         # the CH that issued these, eg, user credentials.
866
867         # Give up, credential does not pass issuer verification
868
869         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()))
870
871
872     ##
873     # -- For Delegates (credentials with parents) verify that:
874     # . The privileges must be a subset of the parent credentials
875     # . The privileges must have "can_delegate" set for each delegated privilege
876     # . The target gid must be the same between child and parents
877     # . The expiry time on the child must be no later than the parent
878     # . The signer of the child must be the owner of the parent        
879     def verify_parent(self, parent_cred):
880         # make sure the rights given to the child are a subset of the
881         # parents rights (and check delegate bits)
882         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
883             raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % self.parent.get_refid()) + 
884                 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred ref %s rights " % self.get_refid()) +
885                 self.get_privileges().save_to_string())
886
887         # make sure my target gid is the same as the parent's
888         if not parent_cred.get_gid_object().save_to_string() == \
889            self.get_gid_object().save_to_string():
890             raise CredentialNotVerifiable("Target gid not equal between parent and child")
891
892         # make sure my expiry time is <= my parent's
893         if not parent_cred.get_expiration() >= self.get_expiration():
894             raise CredentialNotVerifiable("Delegated credential expires after parent")
895
896         # make sure my signer is the parent's caller
897         if not parent_cred.get_gid_caller().save_to_string(False) == \
898            self.get_signature().get_issuer_gid().save_to_string(False):
899             raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
900                 
901         # Recurse
902         if parent_cred.parent:
903             parent_cred.verify_parent(parent_cred.parent)
904
905
906     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
907         """
908         Return a delegated copy of this credential, delegated to the 
909         specified gid's user.    
910         """
911         # get the gid of the object we are delegating
912         object_gid = self.get_gid_object()
913         object_hrn = object_gid.get_hrn()        
914  
915         # the hrn of the user who will be delegated to
916         delegee_gid = GID(filename=delegee_gidfile)
917         delegee_hrn = delegee_gid.get_hrn()
918   
919         #user_key = Keypair(filename=keyfile)
920         #user_hrn = self.get_gid_caller().get_hrn()
921         subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
922         dcred = Credential(subject=subject_string)
923         dcred.set_gid_caller(delegee_gid)
924         dcred.set_gid_object(object_gid)
925         dcred.set_parent(self)
926         dcred.set_expiration(self.get_expiration())
927         dcred.set_privileges(self.get_privileges())
928         dcred.get_privileges().delegate_all_privileges(True)
929         #dcred.set_issuer_keys(keyfile, delegee_gidfile)
930         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
931         dcred.encode()
932         dcred.sign()
933
934         return dcred
935
936     # only informative
937     def get_filename(self):
938         return getattr(self,'filename',None)
939  
940     ##
941     # Dump the contents of a credential to stdout in human-readable format
942     #
943     # @param dump_parents If true, also dump the parent certificates
944     def dump (self, *args, **kwargs):
945         print self.dump_string(*args, **kwargs)
946
947
948     def dump_string(self, dump_parents=False):
949         result=""
950         result += "CREDENTIAL %s\n" % self.get_subject()
951         filename=self.get_filename()
952         if filename: result += "Filename %s\n"%filename
953         result += "      privs: %s\n" % self.get_privileges().save_to_string()
954         gidCaller = self.get_gid_caller()
955         if gidCaller:
956             result += "  gidCaller:\n"
957             result += gidCaller.dump_string(8, dump_parents)
958
959         if self.get_signature():
960             print "  gidIssuer:"
961             self.get_signature().get_issuer_gid().dump(8, dump_parents)
962
963         gidObject = self.get_gid_object()
964         if gidObject:
965             result += "  gidObject:\n"
966             result += gidObject.dump_string(8, dump_parents)
967
968         if self.parent and dump_parents:
969             result += "\nPARENT"
970             result += self.parent.dump_string(True)
971
972         return result