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