Merge branch 'geni-v3' into pep8
[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 caller and object gids of this cred and of its parents
886             for cur_cred in self.get_credential_list():
887                 # check both the caller and the subject 
888                 for gid in cur_cred.get_gid_object(), cur_cred.get_gid_caller():
889                     logger.debug("Credential.verify: verifying chain {}"
890                                  .format(gid.pretty_cert()))
891                     logger.debug("Credential.verify: against trusted {}"
892                                  .format(" ".join(trusted_certs)))
893                     gid.verify_chain(trusted_cert_objects)
894                         
895         refs = []
896         refs.append("Sig_{}".format(self.get_refid()))
897
898         parentRefs = self.updateRefID()
899         for ref in parentRefs:
900             refs.append("Sig_{}".format(ref))
901
902         for ref in refs:
903             # If caller explicitly passed in None that means skip xmlsec1 validation.
904             # Strange and not typical
905             if trusted_certs is None:
906                 break
907
908             # Thierry - jan 2015
909             # up to fedora20 we used os.popen and checked that the output begins with OK
910             # turns out, with fedora21, there is extra input before this 'OK' thing
911             # looks like we're better off just using the exit code - that's what it is made for
912             #cert_args = " ".join(['--trusted-pem {}'.format(x) for x in trusted_certs])
913             # command = '{} --verify --node-id "{}" {} {} 2>&1'.\
914             #          format(self.xmlsec_path, ref, cert_args, filename)
915             xmlsec1 = self.get_xmlsec1_path()
916             if not xmlsec1:
917                 raise Exception("Could not locate required 'xmlsec1' program")
918             command = [xmlsec1, '--verify', '--node-id', ref]
919             for trusted in trusted_certs:
920                 command += ["--trusted-pem", trusted]
921             command += [filename]
922             logger.debug("Running " + " ".join(command))
923             try:
924                 verified = subprocess.check_output(
925                     command, stderr=subprocess.STDOUT)
926                 logger.debug("xmlsec command returned {}".format(verified))
927                 if "OK\n" not in verified:
928                     logger.warning(
929                         "WARNING: xmlsec1 seemed to return fine but without a OK in its output")
930             except subprocess.CalledProcessError as e:
931                 verified = e.output
932                 # xmlsec errors have a msg= which is the interesting bit.
933                 mstart = verified.find("msg=")
934                 msg = ""
935                 if mstart > -1 and len(verified) > 4:
936                     mstart = mstart + 4
937                     mend = verified.find('\\', mstart)
938                     msg = verified[mstart:mend]
939                 logger.warning(
940                     "Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
941                 raise CredentialNotVerifiable("xmlsec1 error verifying cred {} using Signature ID {}: {}"
942                                               .format(self.pretty_cred(), ref, msg))
943         os.remove(filename)
944
945         # Verify the parents (delegation)
946         if self.parent:
947             self.verify_parent(self.parent)
948
949         # Make sure the issuer is the target's authority, and is
950         # itself a valid GID
951         self.verify_issuer(trusted_cert_objects)
952         return True
953
954     ##
955     # Creates a list of the credential and its parents, with the root
956     # (original delegated credential) as the last item in the list
957     def get_credential_list(self):
958         cur_cred = self
959         list = []
960         while cur_cred:
961             list.append(cur_cred)
962             if cur_cred.parent:
963                 cur_cred = cur_cred.parent
964             else:
965                 cur_cred = None
966         return list
967
968     ##
969     # Make sure the credential's target gid (a) was signed by or (b)
970     # is the same as the entity that signed the original credential,
971     # or (c) is an authority over the target's namespace.
972     # Also ensure that the credential issuer / signer itself has a valid
973     # GID signature chain (signed by an authority with namespace rights).
974     def verify_issuer(self, trusted_gids):
975         root_cred = self.get_credential_list()[-1]
976         root_target_gid = root_cred.get_gid_object()
977         if root_cred.get_signature() is None:
978             # malformed
979             raise CredentialNotVerifiable("Could not verify credential owned by {} for object {}. "
980                                           "Cred has no signature"
981                                           .format(self.gidCaller.get_urn(), self.gidObject.get_urn()))
982
983         root_cred_signer = root_cred.get_signature().get_issuer_gid()
984
985         # Case 1:
986         # Allow non authority to sign target and cred about target.
987         #
988         # Why do we need to allow non authorities to sign?
989         # If in the target gid validation step we correctly
990         # checked that the target is only signed by an authority,
991         # then this is just a special case of case 3.
992         # This short-circuit is the common case currently -
993         # and cause GID validation doesn't check 'authority',
994         # this allows users to generate valid slice credentials.
995         if root_target_gid.is_signed_by_cert(root_cred_signer):
996             # cred signer matches target signer, return success
997             return
998
999         # Case 2:
1000         # Allow someone to sign credential about themeselves. Used?
1001         # If not, remove this.
1002         #root_target_gid_str = root_target_gid.save_to_string()
1003         #root_cred_signer_str = root_cred_signer.save_to_string()
1004         # if root_target_gid_str == root_cred_signer_str:
1005         #    # cred signer is target, return success
1006         #    return
1007
1008         # Case 3:
1009
1010         # root_cred_signer is not the target_gid
1011         # So this is a different gid that we have not verified.
1012         # xmlsec1 verified the cert chain on this already, but
1013         # it hasn't verified that the gid meets the HRN namespace
1014         # requirements.
1015         # Below we'll ensure that it is an authority.
1016         # But we haven't verified that it is _signed by_ an authority
1017         # We also don't know if xmlsec1 requires that cert signers
1018         # are marked as CAs.
1019
1020         # Note that if verify() gave us no trusted_gids then this
1021         # call will fail. So skip it if we have no trusted_gids
1022         if trusted_gids and len(trusted_gids) > 0:
1023             root_cred_signer.verify_chain(trusted_gids)
1024         else:
1025             logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
1026                          "No trusted gids. Skipping that check.")
1027
1028         # See if the signer is an authority over the domain of the target.
1029         # There are multiple types of authority - accept them all here
1030         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1031         root_cred_signer_type = root_cred_signer.get_type()
1032         if root_cred_signer_type.find('authority') == 0:
1033             #logger.debug('Cred signer is an authority')
1034             # signer is an authority, see if target is in authority's domain
1035             signerhrn = root_cred_signer.get_hrn()
1036             if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1037                 return
1038
1039         # We've required that the credential be signed by an authority
1040         # for that domain. Reasonable and probably correct.
1041         # A looser model would also allow the signer to be an authority
1042         # in my control framework - eg My CA or CH. Even if it is not
1043         # the CH that issued these, eg, user credentials.
1044
1045         # Give up, credential does not pass issuer verification
1046
1047         raise CredentialNotVerifiable(
1048             "Could not verify credential owned by {} for object {}. "
1049             "Cred signer {} not the trusted authority for Cred target {}"
1050             .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1051                     root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1052
1053     ##
1054     # -- For Delegates (credentials with parents) verify that:
1055     # . The privileges must be a subset of the parent credentials
1056     # . The privileges must have "can_delegate" set for each delegated privilege
1057     # . The target gid must be the same between child and parents
1058     # . The expiry time on the child must be no later than the parent
1059     # . The signer of the child must be the owner of the parent
1060     def verify_parent(self, parent_cred):
1061         # make sure the rights given to the child are a subset of the
1062         # parents rights (and check delegate bits)
1063         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1064             message = (
1065                 "Parent cred {} (ref {}) rights {} "
1066                 " not superset of delegated cred {} (ref {}) rights {}"
1067                 .format(parent_cred.pretty_cred(), parent_cred.get_refid(),
1068                         parent_cred.get_privileges().pretty_rights(),
1069                         self.pretty_cred(), self.get_refid(),
1070                         self.get_privileges().pretty_rights()))
1071             logger.error(message)
1072             logger.error("parent details {}".format(
1073                 parent_cred.get_privileges().save_to_string()))
1074             logger.error("self details {}".format(
1075                 self.get_privileges().save_to_string()))
1076             raise ChildRightsNotSubsetOfParent(message)
1077
1078         # make sure my target gid is the same as the parent's
1079         if not parent_cred.get_gid_object().save_to_string() == \
1080            self.get_gid_object().save_to_string():
1081             message = (
1082                 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1083                 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1084             logger.error(message)
1085             logger.error("parent details {}".format(
1086                 parent_cred.save_to_string()))
1087             logger.error("self details {}".format(self.save_to_string()))
1088             raise CredentialNotVerifiable(message)
1089
1090         # make sure my expiry time is <= my parent's
1091         if not parent_cred.get_expiration() >= self.get_expiration():
1092             raise CredentialNotVerifiable(
1093                 "Delegated credential {} expires after parent {}"
1094                 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1095
1096         # make sure my signer is the parent's caller
1097         if not parent_cred.get_gid_caller().save_to_string(False) == \
1098            self.get_signature().get_issuer_gid().save_to_string(False):
1099             message = "Delegated credential {} not signed by parent {}'s caller"\
1100                 .format(self.pretty_cred(), parent_cred.pretty_cred())
1101             logger.error(message)
1102             logger.error("compare1 parent {}".format(
1103                 parent_cred.get_gid_caller().pretty_cert()))
1104             logger.error("compare1 parent details {}".format(
1105                 parent_cred.get_gid_caller().save_to_string()))
1106             logger.error("compare2 self {}".format(
1107                 self.get_signature().get_issuer_gid().pretty_crert()))
1108             logger.error("compare2 self details {}".format(
1109                 self.get_signature().get_issuer_gid().save_to_string()))
1110             raise CredentialNotVerifiable(message)
1111
1112         # Recurse
1113         if parent_cred.parent:
1114             parent_cred.verify_parent(parent_cred.parent)
1115
1116     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1117         """
1118         Return a delegated copy of this credential, delegated to the 
1119         specified gid's user.    
1120         """
1121         # get the gid of the object we are delegating
1122         object_gid = self.get_gid_object()
1123         object_hrn = object_gid.get_hrn()
1124
1125         # the hrn of the user who will be delegated to
1126         delegee_gid = GID(filename=delegee_gidfile)
1127         delegee_hrn = delegee_gid.get_hrn()
1128
1129         #user_key = Keypair(filename=keyfile)
1130         #user_hrn = self.get_gid_caller().get_hrn()
1131         subject_string = "{} delegated to {}".format(object_hrn, delegee_hrn)
1132         dcred = Credential(subject=subject_string)
1133         dcred.set_gid_caller(delegee_gid)
1134         dcred.set_gid_object(object_gid)
1135         dcred.set_parent(self)
1136         dcred.set_expiration(self.get_expiration())
1137         dcred.set_privileges(self.get_privileges())
1138         dcred.get_privileges().delegate_all_privileges(True)
1139         #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1140         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1141         dcred.encode()
1142         dcred.sign()
1143
1144         return dcred
1145
1146     # only informative
1147     def get_filename(self):
1148         return getattr(self, 'filename', None)
1149
1150     def actual_caller_hrn(self):
1151         """
1152         a helper method used by some API calls like e.g. Allocate
1153         to try and find out who really is the original caller
1154
1155         This admittedly is a bit of a hack, please USE IN LAST RESORT
1156
1157         This code uses a heuristic to identify a delegated credential
1158
1159         A first known restriction if for traffic that gets through a
1160         slice manager in this case the hrn reported is the one from
1161         the last SM in the call graph which is not at all what is
1162         meant here
1163         """
1164
1165         caller_hrn, caller_type = urn_to_hrn(self.get_gid_caller().get_urn())
1166         issuer_hrn, issuer_type = urn_to_hrn(
1167             self.get_signature().get_issuer_gid().get_urn())
1168         subject_hrn = self.get_gid_object().get_hrn()
1169         # if the caller is a user and the issuer is not
1170         # it's probably the former
1171         if caller_type == "user" and issuer_type != "user":
1172             actual_caller_hrn = caller_hrn
1173         # if we find that the caller_hrn is an immediate descendant of the issuer, then
1174         # this seems to be a 'regular' credential
1175         elif caller_hrn.startswith(issuer_hrn):
1176             actual_caller_hrn = caller_hrn
1177         # else this looks like a delegated credential, and the real caller is
1178         # the issuer
1179         else:
1180             actual_caller_hrn = issuer_hrn
1181         logger.info("actual_caller_hrn: caller_hrn={}, issuer_hrn={}, returning {}"
1182                     .format(caller_hrn, issuer_hrn, actual_caller_hrn))
1183         return actual_caller_hrn
1184
1185     ##
1186     # Dump the contents of a credential to stdout in human-readable format
1187     #
1188     # @param dump_parents If true, also dump the parent certificates
1189     def dump(self, *args, **kwargs):
1190         print(self.dump_string(*args, **kwargs))
1191
1192     # SFA code ignores show_xml and disables printing the cred xml
1193     def dump_string(self, dump_parents=False, show_xml=False):
1194         result = ""
1195         result += "CREDENTIAL {}\n".format(self.pretty_subject())
1196         filename = self.get_filename()
1197         if filename:
1198             result += "Filename {}\n".format(filename)
1199         privileges = self.get_privileges()
1200         if privileges:
1201             result += "      privs: {}\n".format(privileges.save_to_string())
1202         else:
1203             result += "      privs: \n"
1204         gidCaller = self.get_gid_caller()
1205         if gidCaller:
1206             result += "  gidCaller:\n"
1207             result += gidCaller.dump_string(8, dump_parents)
1208
1209         if self.get_signature():
1210             result += "  gidIssuer:\n"
1211             result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1212
1213         if self.expiration:
1214             result += "  expiration: " + \
1215                 self.expiration.strftime(SFATIME_FORMAT) + "\n"
1216
1217         gidObject = self.get_gid_object()
1218         if gidObject:
1219             result += "  gidObject:\n"
1220             result += gidObject.dump_string(8, dump_parents)
1221
1222         if self.parent and dump_parents:
1223             result += "\nPARENT"
1224             result += self.parent.dump_string(True)
1225
1226         if show_xml and HAVELXML:
1227             try:
1228                 tree = etree.parse(StringIO(self.xml))
1229                 aside = etree.tostring(tree, pretty_print=True)
1230                 result += "\nXML:\n\n"
1231                 result += aside
1232                 result += "\nEnd XML\n"
1233             except:
1234                 import traceback
1235                 print("exc. Credential.dump_string / XML")
1236                 traceback.print_exc()
1237
1238         return result