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