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