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