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