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