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