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