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