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