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