Python3 compatibility for Credential & Certificate in save_to_string & save_to_file...
[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         if isinstance(self.xml, bytes):
583             self.xml = self.xml.decode()
584         f.write(self.xml)
585         f.close()
586
587     def save_to_string(self, save_parents=True):
588         if not self.xml:
589             self.encode()
590         if isinstance(self.xml, bytes):
591             self.xml = self.xml.decode()
592         return self.xml
593
594     def get_refid(self):
595         if not self.refid:
596             self.refid = 'ref0'
597         return self.refid
598
599     def set_refid(self, rid):
600         self.refid = rid
601
602     ##
603     # Figure out what refids exist, and update this credential's id
604     # so that it doesn't clobber the others.  Returns the refids of
605     # the parents.
606     
607     def updateRefID(self):
608         if not self.parent:
609             self.set_refid('ref0')
610             return []
611         
612         refs = []
613
614         next_cred = self.parent
615         while next_cred:
616             refs.append(next_cred.get_refid())
617             if next_cred.parent:
618                 next_cred = next_cred.parent
619             else:
620                 next_cred = None
621
622         
623         # Find a unique refid for this credential
624         rid = self.get_refid()
625         while rid in refs:
626             val = int(rid[3:])
627             rid = "ref{}".format(val + 1)
628
629         # Set the new refid
630         self.set_refid(rid)
631
632         # Return the set of parent credential ref ids
633         return refs
634
635     def get_xml(self):
636         if not self.xml:
637             self.encode()
638         return self.xml
639
640     ##
641     # Sign the XML file created by encode()
642     #
643     # WARNING:
644     # In general, a signed credential obtained externally should
645     # not be changed else the signature is no longer valid.  So, once
646     # you have loaded an existing signed credential, do not call encode() or sign() on it.
647
648     def sign(self):
649         if not self.issuer_privkey:
650             logger.warn("Cannot sign credential (no private key)")
651             return
652         if not self.issuer_gid:
653             logger.warn("Cannot sign credential (no issuer gid)")
654             return
655         doc = parseString(self.get_xml())
656         sigs = doc.getElementsByTagName("signatures")[0]
657
658         # Create the signature template to be signed
659         signature = Signature()
660         signature.set_refid(self.get_refid())
661         sdoc = parseString(signature.get_xml())        
662         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
663         sigs.appendChild(sig_ele)
664
665         self.xml = doc.toxml("utf-8")
666
667
668         # Split the issuer GID into multiple certificates if it's a chain
669         chain = GID(filename=self.issuer_gid)
670         gid_files = []
671         while chain:
672             gid_files.append(chain.save_to_random_tmp_file(False))
673             if chain.get_parent():
674                 chain = chain.get_parent()
675             else:
676                 chain = None
677
678
679         # Call out to xmlsec1 to sign it
680         ref = 'Sig_{}'.format(self.get_refid())
681         filename = self.save_to_random_tmp_file()
682         xmlsec1 = self.get_xmlsec1_path()
683         if not xmlsec1:
684             raise Exception("Could not locate required 'xmlsec1' program")
685         command = '{} --sign --node-id "{}" --privkey-pem {},{} {}' \
686                   .format(xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
687         signed = os.popen(command).read()
688         os.remove(filename)
689
690         for gid_file in gid_files:
691             os.remove(gid_file)
692
693         self.xml = signed
694
695         # Update signatures
696         self.decode()       
697
698
699     ##
700     # Retrieve the attributes of the credential from the XML.
701     # This is automatically called by the various get_* methods of
702     # this class and should not need to be called explicitly.
703
704     def decode(self):
705         if not self.xml:
706             return
707
708         doc = None
709         try:
710             doc = parseString(self.xml)
711         except ExpatError as e:
712             raise CredentialNotVerifiable("Malformed credential")
713         doc = parseString(self.xml)
714         sigs = []
715         signed_cred = doc.getElementsByTagName("signed-credential")
716
717         # Is this a signed-cred or just a cred?
718         if len(signed_cred) > 0:
719             creds = signed_cred[0].getElementsByTagName("credential")
720             signatures = signed_cred[0].getElementsByTagName("signatures")
721             if len(signatures) > 0:
722                 sigs = signatures[0].getElementsByTagName("Signature")
723         else:
724             creds = doc.getElementsByTagName("credential")
725         
726         if creds is None or len(creds) == 0:
727             # malformed cred file
728             raise CredentialNotVerifiable("Malformed XML: No credential tag found")
729
730         # Just take the first cred if there are more than one
731         cred = creds[0]
732
733         self.set_refid(cred.getAttribute("xml:id"))
734         self.set_expiration(utcparse(getTextNode(cred, "expires")))
735         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
736         self.gidObject = GID(string=getTextNode(cred, "target_gid"))
737
738
739         ## This code until the end of function rewritten by Aaron Helsinger
740         # Process privileges
741         rlist = Rights()
742         priv_nodes = cred.getElementsByTagName("privileges")
743         if len(priv_nodes) > 0:
744             privs = priv_nodes[0]
745             for priv in privs.getElementsByTagName("privilege"):
746                 kind = getTextNode(priv, "name")
747                 deleg = str2bool(getTextNode(priv, "can_delegate"))
748                 if kind == '*':
749                     # Convert * into the default privileges for the credential's type
750                     # Each inherits the delegatability from the * above
751                     _ , type = urn_to_hrn(self.gidObject.get_urn())
752                     rl = determine_rights(type, self.gidObject.get_urn())
753                     for r in rl.rights:
754                         r.delegate = deleg
755                         rlist.add(r)
756                 else:
757                     rlist.add(Right(kind.strip(), deleg))
758         self.set_privileges(rlist)
759
760
761         # Is there a parent?
762         parent = cred.getElementsByTagName("parent")
763         if len(parent) > 0:
764             parent_doc = parent[0].getElementsByTagName("credential")[0]
765             parent_xml = parent_doc.toxml("utf-8")
766             if parent_xml is None or parent_xml.strip() == "":
767                 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
768             self.parent = Credential(string=parent_xml)
769             self.updateRefID()
770
771         # Assign the signatures to the credentials
772         for sig in sigs:
773             Sig = Signature(string=sig.toxml("utf-8"))
774
775             for cur_cred in self.get_credential_list():
776                 if cur_cred.get_refid() == Sig.get_refid():
777                     cur_cred.set_signature(Sig)
778                                     
779             
780     ##
781     # Verify
782     #   trusted_certs: A list of trusted GID filenames (not GID objects!) 
783     #                  Chaining is not supported within the GIDs by xmlsec1.
784     #
785     #   trusted_certs_required: Should usually be true. Set False means an
786     #                 empty list of trusted_certs would still let this method pass.
787     #                 It just skips xmlsec1 verification et al. Only used by some utils
788     #    
789     # Verify that:
790     # . All of the signatures are valid and that the issuers trace back
791     #   to trusted roots (performed by xmlsec1)
792     # . The XML matches the credential schema
793     # . That the issuer of the credential is the authority in the target's urn
794     #    . In the case of a delegated credential, this must be true of the root
795     # . That all of the gids presented in the credential are valid
796     #    . Including verifying GID chains, and includ the issuer
797     # . The credential is not expired
798     #
799     # -- For Delegates (credentials with parents)
800     # . The privileges must be a subset of the parent credentials
801     # . The privileges must have "can_delegate" set for each delegated privilege
802     # . The target gid must be the same between child and parents
803     # . The expiry time on the child must be no later than the parent
804     # . The signer of the child must be the owner of the parent
805     #
806     # -- Verify does *NOT*
807     # . ensure that an xmlrpc client's gid matches a credential gid, that
808     #   must be done elsewhere
809     #
810     # @param trusted_certs: The certificates of trusted CA certificates
811     def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
812         if not self.xml:
813             self.decode()
814
815         # validate against RelaxNG schema
816         if HAVELXML:
817             if schema and os.path.exists(schema):
818                 tree = etree.parse(StringIO(self.xml))
819                 schema_doc = etree.parse(schema)
820                 xmlschema = etree.XMLSchema(schema_doc)
821                 if not xmlschema.validate(tree):
822                     error = xmlschema.error_log.last_error
823                     message = "{}: {} (line {})".format(self.pretty_cred(),
824                                                         error.message, error.line)
825                     raise CredentialNotVerifiable(message)
826
827         if trusted_certs_required and trusted_certs is None:
828             trusted_certs = []
829
830 #        trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
831         trusted_cert_objects = []
832         ok_trusted_certs = []
833         # If caller explicitly passed in None that means skip cert chain validation.
834         # Strange and not typical
835         if trusted_certs is not None:
836             for f in trusted_certs:
837                 try:
838                     # Failures here include unreadable files
839                     # or non PEM files
840                     trusted_cert_objects.append(GID(filename=f))
841                     ok_trusted_certs.append(f)
842                 except Exception as exc:
843                     logger.error("Failed to load trusted cert from {}: {}".format(f, exc))
844             trusted_certs = ok_trusted_certs
845
846         # make sure it is not expired
847         if self.get_expiration() < datetime.datetime.utcnow():
848             raise CredentialNotVerifiable("Credential {} expired at {}" \
849                                           .format(self.pretty_cred(),
850                                                   self.expiration.strftime(SFATIME_FORMAT)))
851
852         # Verify the signatures
853         filename = self.save_to_random_tmp_file()
854
855         # If caller explicitly passed in None that means skip cert chain validation.
856         # - Strange and not typical
857         if trusted_certs is not None:
858             # Verify the gids of this cred and of its parents
859             for cur_cred in self.get_credential_list():
860                 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
861                 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
862
863         refs = []
864         refs.append("Sig_{}".format(self.get_refid()))
865
866         parentRefs = self.updateRefID()
867         for ref in parentRefs:
868             refs.append("Sig_{}".format(ref))
869
870         for ref in refs:
871             # If caller explicitly passed in None that means skip xmlsec1 validation.
872             # Strange and not typical
873             if trusted_certs is None:
874                 break
875
876             # Thierry - jan 2015
877             # up to fedora20 we used os.popen and checked that the output begins with OK
878             # turns out, with fedora21, there is extra input before this 'OK' thing
879             # looks like we're better off just using the exit code - that's what it is made for
880             #cert_args = " ".join(['--trusted-pem {}'.format(x) for x in trusted_certs])
881             #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
882             #          format(self.xmlsec_path, ref, cert_args, filename)
883             xmlsec1 = self.get_xmlsec1_path()
884             if not xmlsec1:
885                 raise Exception("Could not locate required 'xmlsec1' program")
886             command = [ xmlsec1, '--verify', '--node-id', ref ]
887             for trusted in trusted_certs:
888                 command += ["--trusted-pem", trusted ]
889             command += [ filename ]
890             logger.debug("Running " + " ".join(command))
891             try:
892                 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
893                 logger.debug("xmlsec command returned {}".format(verified))
894                 if "OK\n" not in verified:
895                     logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
896             except subprocess.CalledProcessError as e:
897                 verified = e.output
898                 # xmlsec errors have a msg= which is the interesting bit.
899                 mstart = verified.find("msg=")
900                 msg = ""
901                 if mstart > -1 and len(verified) > 4:
902                     mstart = mstart + 4
903                     mend = verified.find('\\', mstart)
904                     msg = verified[mstart:mend]
905                 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
906                 raise CredentialNotVerifiable("xmlsec1 error verifying cred {} using Signature ID {}: {}"\
907                                               .format(self.pretty_cred(), ref, msg))
908         os.remove(filename)
909
910         # Verify the parents (delegation)
911         if self.parent:
912             self.verify_parent(self.parent)
913
914         # Make sure the issuer is the target's authority, and is
915         # itself a valid GID
916         self.verify_issuer(trusted_cert_objects)
917         return True
918
919     ##
920     # Creates a list of the credential and its parents, with the root 
921     # (original delegated credential) as the last item in the list
922     def get_credential_list(self):    
923         cur_cred = self
924         list = []
925         while cur_cred:
926             list.append(cur_cred)
927             if cur_cred.parent:
928                 cur_cred = cur_cred.parent
929             else:
930                 cur_cred = None
931         return list
932     
933     ##
934     # Make sure the credential's target gid (a) was signed by or (b)
935     # is the same as the entity that signed the original credential,
936     # or (c) is an authority over the target's namespace.
937     # Also ensure that the credential issuer / signer itself has a valid
938     # GID signature chain (signed by an authority with namespace rights).
939     def verify_issuer(self, trusted_gids):
940         root_cred = self.get_credential_list()[-1]
941         root_target_gid = root_cred.get_gid_object()
942         if root_cred.get_signature() is None:
943             # malformed
944             raise CredentialNotVerifiable("Could not verify credential owned by {} for object {}. "
945                                           "Cred has no signature" \
946                                           .format(self.gidCaller.get_urn(), self.gidObject.get_urn()))
947
948         root_cred_signer = root_cred.get_signature().get_issuer_gid()
949
950         # Case 1:
951         # Allow non authority to sign target and cred about target.
952         #
953         # Why do we need to allow non authorities to sign?
954         # If in the target gid validation step we correctly
955         # checked that the target is only signed by an authority,
956         # then this is just a special case of case 3.
957         # This short-circuit is the common case currently -
958         # and cause GID validation doesn't check 'authority',
959         # this allows users to generate valid slice credentials.
960         if root_target_gid.is_signed_by_cert(root_cred_signer):
961             # cred signer matches target signer, return success
962             return
963
964         # Case 2:
965         # Allow someone to sign credential about themeselves. Used?
966         # If not, remove this.
967         #root_target_gid_str = root_target_gid.save_to_string()
968         #root_cred_signer_str = root_cred_signer.save_to_string()
969         #if root_target_gid_str == root_cred_signer_str:
970         #    # cred signer is target, return success
971         #    return
972
973         # Case 3:
974
975         # root_cred_signer is not the target_gid
976         # So this is a different gid that we have not verified.
977         # xmlsec1 verified the cert chain on this already, but
978         # it hasn't verified that the gid meets the HRN namespace
979         # requirements.
980         # Below we'll ensure that it is an authority.
981         # But we haven't verified that it is _signed by_ an authority
982         # We also don't know if xmlsec1 requires that cert signers
983         # are marked as CAs.
984
985         # Note that if verify() gave us no trusted_gids then this
986         # call will fail. So skip it if we have no trusted_gids
987         if trusted_gids and len(trusted_gids) > 0:
988             root_cred_signer.verify_chain(trusted_gids)
989         else:
990             logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
991                          "No trusted gids. Skipping that check.")
992
993         # See if the signer is an authority over the domain of the target.
994         # There are multiple types of authority - accept them all here
995         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
996         root_cred_signer_type = root_cred_signer.get_type()
997         if root_cred_signer_type.find('authority') == 0:
998             #logger.debug('Cred signer is an authority')
999             # signer is an authority, see if target is in authority's domain
1000             signerhrn = root_cred_signer.get_hrn()
1001             if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1002                 return
1003
1004         # We've required that the credential be signed by an authority
1005         # for that domain. Reasonable and probably correct.
1006         # A looser model would also allow the signer to be an authority
1007         # in my control framework - eg My CA or CH. Even if it is not
1008         # the CH that issued these, eg, user credentials.
1009
1010         # Give up, credential does not pass issuer verification
1011
1012         raise CredentialNotVerifiable(
1013             "Could not verify credential owned by {} for object {}. "
1014             "Cred signer {} not the trusted authority for Cred target {}"
1015             .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1016                     root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1017
1018     ##
1019     # -- For Delegates (credentials with parents) verify that:
1020     # . The privileges must be a subset of the parent credentials
1021     # . The privileges must have "can_delegate" set for each delegated privilege
1022     # . The target gid must be the same between child and parents
1023     # . The expiry time on the child must be no later than the parent
1024     # . The signer of the child must be the owner of the parent        
1025     def verify_parent(self, parent_cred):
1026         # make sure the rights given to the child are a subset of the
1027         # parents rights (and check delegate bits)
1028         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1029             message = (
1030                 "Parent cred {} (ref {}) rights {} "
1031                 " not superset of delegated cred {} (ref {}) rights {}"
1032                 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1033                         parent_cred.get_privileges().pretty_rights(),
1034                         self.pretty_cred(), self.get_refid(),
1035                         self.get_privileges().pretty_rights()))
1036             logger.error(message)
1037             logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1038             logger.error("self details {}".format(self.get_privileges().save_to_string()))
1039             raise ChildRightsNotSubsetOfParent(message)
1040
1041         # make sure my target gid is the same as the parent's
1042         if not parent_cred.get_gid_object().save_to_string() == \
1043            self.get_gid_object().save_to_string():
1044             message = (
1045                 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1046                 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1047             logger.error(message)
1048             logger.error("parent details {}".format(parent_cred.save_to_string()))
1049             logger.error("self details {}".format(self.save_to_string()))
1050             raise CredentialNotVerifiable(message)
1051
1052         # make sure my expiry time is <= my parent's
1053         if not parent_cred.get_expiration() >= self.get_expiration():
1054             raise CredentialNotVerifiable(
1055                 "Delegated credential {} expires after parent {}"
1056                 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1057
1058         # make sure my signer is the parent's caller
1059         if not parent_cred.get_gid_caller().save_to_string(False) == \
1060            self.get_signature().get_issuer_gid().save_to_string(False):
1061             message = "Delegated credential {} not signed by parent {}'s caller"\
1062                 .format(self.pretty_cred(), parent_cred.pretty_cred())
1063             logger.error(message)
1064             logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1065             logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1066             logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1067             logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1068             raise CredentialNotVerifiable(message)
1069                 
1070         # Recurse
1071         if parent_cred.parent:
1072             parent_cred.verify_parent(parent_cred.parent)
1073
1074
1075     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1076         """
1077         Return a delegated copy of this credential, delegated to the 
1078         specified gid's user.    
1079         """
1080         # get the gid of the object we are delegating
1081         object_gid = self.get_gid_object()
1082         object_hrn = object_gid.get_hrn()        
1083  
1084         # the hrn of the user who will be delegated to
1085         delegee_gid = GID(filename=delegee_gidfile)
1086         delegee_hrn = delegee_gid.get_hrn()
1087   
1088         #user_key = Keypair(filename=keyfile)
1089         #user_hrn = self.get_gid_caller().get_hrn()
1090         subject_string = "{} delegated to {}".format(object_hrn, delegee_hrn)
1091         dcred = Credential(subject=subject_string)
1092         dcred.set_gid_caller(delegee_gid)
1093         dcred.set_gid_object(object_gid)
1094         dcred.set_parent(self)
1095         dcred.set_expiration(self.get_expiration())
1096         dcred.set_privileges(self.get_privileges())
1097         dcred.get_privileges().delegate_all_privileges(True)
1098         #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1099         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1100         dcred.encode()
1101         dcred.sign()
1102
1103         return dcred
1104
1105     # only informative
1106     def get_filename(self):
1107         return getattr(self,'filename',None)
1108
1109     def actual_caller_hrn(self):
1110         """a helper method used by some API calls like e.g. Allocate
1111         to try and find out who really is the original caller
1112
1113         This admittedly is a bit of a hack, please USE IN LAST RESORT
1114
1115         This code uses a heuristic to identify a delegated credential
1116
1117         A first known restriction if for traffic that gets through a slice manager
1118         in this case the hrn reported is the one from the last SM in the call graph
1119         which is not at all what is meant here"""
1120
1121         caller_hrn = self.get_gid_caller().get_hrn()
1122         issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1123         subject_hrn = self.get_gid_object().get_hrn()
1124         # if we find that the caller_hrn is an immediate descendant of the issuer, then
1125         # this seems to be a 'regular' credential
1126         if caller_hrn.startswith(issuer_hrn):
1127             actual_caller_hrn=caller_hrn
1128         # else this looks like a delegated credential, and the real caller is the issuer
1129         else:
1130             actual_caller_hrn=issuer_hrn
1131         logger.info("actual_caller_hrn: caller_hrn={}, issuer_hrn={}, returning {}"
1132                     .format(caller_hrn,issuer_hrn,actual_caller_hrn))
1133         return actual_caller_hrn
1134
1135     ##
1136     # Dump the contents of a credential to stdout in human-readable format
1137     #
1138     # @param dump_parents If true, also dump the parent certificates
1139     def dump(self, *args, **kwargs):
1140         print(self.dump_string(*args, **kwargs))
1141
1142     # SFA code ignores show_xml and disables printing the cred xml
1143     def dump_string(self, dump_parents=False, show_xml=False):
1144         result=""
1145         result += "CREDENTIAL {}\n".format(self.pretty_subject())
1146         filename=self.get_filename()
1147         if filename: result += "Filename {}\n".format(filename)
1148         privileges = self.get_privileges()
1149         if privileges:
1150             result += "      privs: {}\n".format(privileges.save_to_string())
1151         else:
1152             result += "      privs: \n"
1153         gidCaller = self.get_gid_caller()
1154         if gidCaller:
1155             result += "  gidCaller:\n"
1156             result += gidCaller.dump_string(8, dump_parents)
1157
1158         if self.get_signature():
1159             result += "  gidIssuer:\n"
1160             result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1161
1162         if self.expiration:
1163             result += "  expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1164
1165         gidObject = self.get_gid_object()
1166         if gidObject:
1167             result += "  gidObject:\n"
1168             result += gidObject.dump_string(8, dump_parents)
1169
1170         if self.parent and dump_parents:
1171             result += "\nPARENT"
1172             result += self.parent.dump_string(True)
1173
1174         if show_xml and HAVELXML:
1175             try:
1176                 tree = etree.parse(StringIO(self.xml))
1177                 aside = etree.tostring(tree, pretty_print=True)
1178                 result += "\nXML:\n\n"
1179                 result += aside
1180                 result += "\nEnd XML\n"
1181             except:
1182                 import traceback
1183                 print("exc. Credential.dump_string / XML")
1184                 traceback.print_exc()
1185
1186         return result