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