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