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