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