instead of just dropping timezone info, use sfa.util.sfatime.utcparse to correctly...
[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 ### $Id$\r
30 ### $URL$\r
31 \r
32 import os\r
33 import datetime\r
34 from sfa.util.sfatime import utcparse\r
35 from tempfile import mkstemp\r
36 from xml.dom.minidom import Document, parseString\r
37 from lxml import etree\r
38 from dateutil.parser import parse\r
39 from StringIO import StringIO\r
40 from sfa.util.faults import *\r
41 from sfa.util.sfalogging import logger\r
42 from sfa.trust.certificate import Keypair\r
43 from sfa.trust.credential_legacy import CredentialLegacy\r
44 from sfa.trust.rights import Right, Rights\r
45 from sfa.trust.gid import GID\r
46 from sfa.util.xrn import urn_to_hrn\r
47 \r
48 # 2 weeks, in seconds \r
49 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14\r
50 \r
51 \r
52 # TODO:\r
53 # . make privs match between PG and PL\r
54 # . Need to add support for other types of credentials, e.g. tickets\r
55 # . add namespaces to signed-credential element?\r
56 \r
57 signature_template = \\r
58 '''\r
59 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">\r
60   <SignedInfo>\r
61     <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>\r
62     <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>\r
63     <Reference URI="#%s">\r
64       <Transforms>\r
65         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />\r
66       </Transforms>\r
67       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>\r
68       <DigestValue></DigestValue>\r
69     </Reference>\r
70   </SignedInfo>\r
71   <SignatureValue />\r
72   <KeyInfo>\r
73     <X509Data>\r
74       <X509SubjectName/>\r
75       <X509IssuerSerial/>\r
76       <X509Certificate/>\r
77     </X509Data>\r
78     <KeyValue />\r
79   </KeyInfo>\r
80 </Signature>\r
81 '''\r
82 \r
83 # PG formats the template (whitespace) slightly differently.\r
84 # Note that they don't include the xmlns in the template, but add it later.\r
85 # Otherwise the two are equivalent.\r
86 #signature_template_as_in_pg = \\r
87 #'''\r
88 #<Signature xml:id="Sig_%s" >\r
89 # <SignedInfo>\r
90 #  <CanonicalizationMethod      Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>\r
91 #  <SignatureMethod      Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>\r
92 #  <Reference URI="#%s">\r
93 #    <Transforms>\r
94 #      <Transform         Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />\r
95 #    </Transforms>\r
96 #    <DigestMethod        Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>\r
97 #    <DigestValue></DigestValue>\r
98 #    </Reference>\r
99 # </SignedInfo>\r
100 # <SignatureValue />\r
101 # <KeyInfo>\r
102 #  <X509Data >\r
103 #   <X509SubjectName/>\r
104 #   <X509IssuerSerial/>\r
105 #   <X509Certificate/>\r
106 #  </X509Data>\r
107 #  <KeyValue />\r
108 # </KeyInfo>\r
109 #</Signature>\r
110 #'''\r
111 \r
112 ##\r
113 # Convert a string into a bool\r
114 # used to convert an xsd:boolean to a Python boolean\r
115 def str2bool(str):\r
116     if str.lower() in ['true','1']:\r
117         return True\r
118     return False\r
119 \r
120 \r
121 ##\r
122 # Utility function to get the text of an XML element\r
123 \r
124 def getTextNode(element, subele):\r
125     sub = element.getElementsByTagName(subele)[0]\r
126     if len(sub.childNodes) > 0:            \r
127         return sub.childNodes[0].nodeValue\r
128     else:\r
129         return None\r
130         \r
131 ##\r
132 # Utility function to set the text of an XML element\r
133 # It creates the element, adds the text to it,\r
134 # and then appends it to the parent.\r
135 \r
136 def append_sub(doc, parent, element, text):\r
137     ele = doc.createElement(element)\r
138     ele.appendChild(doc.createTextNode(text))\r
139     parent.appendChild(ele)\r
140 \r
141 ##\r
142 # Signature contains information about an xmlsec1 signature\r
143 # for a signed-credential\r
144 #\r
145 \r
146 class Signature(object):\r
147    \r
148     def __init__(self, string=None):\r
149         self.refid = None\r
150         self.issuer_gid = None\r
151         self.xml = None\r
152         if string:\r
153             self.xml = string\r
154             self.decode()\r
155 \r
156 \r
157     def get_refid(self):\r
158         if not self.refid:\r
159             self.decode()\r
160         return self.refid\r
161 \r
162     def get_xml(self):\r
163         if not self.xml:\r
164             self.encode()\r
165         return self.xml\r
166 \r
167     def set_refid(self, id):\r
168         self.refid = id\r
169 \r
170     def get_issuer_gid(self):\r
171         if not self.gid:\r
172             self.decode()\r
173         return self.gid        \r
174 \r
175     def set_issuer_gid(self, gid):\r
176         self.gid = gid\r
177 \r
178     def decode(self):\r
179         doc = parseString(self.xml)\r
180         sig = doc.getElementsByTagName("Signature")[0]\r
181         self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))\r
182         keyinfo = sig.getElementsByTagName("X509Data")[0]\r
183         szgid = getTextNode(keyinfo, "X509Certificate")\r
184         szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid\r
185         self.set_issuer_gid(GID(string=szgid))        \r
186         \r
187     def encode(self):\r
188         self.xml = signature_template % (self.get_refid(), self.get_refid())\r
189 \r
190 \r
191 ##\r
192 # A credential provides a caller gid with privileges to an object gid.\r
193 # A signed credential is signed by the object's authority.\r
194 #\r
195 # Credentials are encoded in one of two ways.  The legacy style places\r
196 # it in the subjectAltName of an X509 certificate.  The new credentials\r
197 # are placed in signed XML.\r
198 #\r
199 # WARNING:\r
200 # In general, a signed credential obtained externally should\r
201 # not be changed else the signature is no longer valid.  So, once\r
202 # you have loaded an existing signed credential, do not call encode() or sign() on it.\r
203 \r
204 def filter_creds_by_caller(creds, caller_hrn):\r
205         """\r
206         Returns a list of creds who's gid caller matches the\r
207         specified caller hrn\r
208         """\r
209         if not isinstance(creds, list): creds = [creds]\r
210         caller_creds = []\r
211         for cred in creds:\r
212             try:\r
213                 tmp_cred = Credential(string=cred)\r
214                 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:\r
215                     caller_creds.append(cred)\r
216             except: pass\r
217         return caller_creds\r
218 \r
219 class Credential(object):\r
220 \r
221     ##\r
222     # Create a Credential object\r
223     #\r
224     # @param create If true, create a blank x509 certificate\r
225     # @param subject If subject!=None, create an x509 cert with the subject name\r
226     # @param string If string!=None, load the credential from the string\r
227     # @param filename If filename!=None, load the credential from the file\r
228     # FIXME: create and subject are ignored!\r
229     def __init__(self, create=False, subject=None, string=None, filename=None):\r
230         self.gidCaller = None\r
231         self.gidObject = None\r
232         self.expiration = None\r
233         self.privileges = None\r
234         self.issuer_privkey = None\r
235         self.issuer_gid = None\r
236         self.issuer_pubkey = None\r
237         self.parent = None\r
238         self.signature = None\r
239         self.xml = None\r
240         self.refid = None\r
241         self.legacy = None\r
242 \r
243         # Check if this is a legacy credential, translate it if so\r
244         if string or filename:\r
245             if string:                \r
246                 str = string\r
247             elif filename:\r
248                 str = file(filename).read()\r
249                 \r
250             if str.strip().startswith("-----"):\r
251                 self.legacy = CredentialLegacy(False,string=str)\r
252                 self.translate_legacy(str)\r
253             else:\r
254                 self.xml = str\r
255                 self.decode()\r
256 \r
257         # Find an xmlsec1 path\r
258         self.xmlsec_path = ''\r
259         paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']\r
260         for path in paths:\r
261             if os.path.isfile(path + '/' + 'xmlsec1'):\r
262                 self.xmlsec_path = path + '/' + 'xmlsec1'\r
263                 break\r
264 \r
265     def get_subject(self):\r
266         if not self.gidObject:\r
267             self.decode()\r
268         return self.gidObject.get_subject()   \r
269 \r
270     def get_signature(self):\r
271         if not self.signature:\r
272             self.decode()\r
273         return self.signature\r
274 \r
275     def set_signature(self, sig):\r
276         self.signature = sig\r
277 \r
278         \r
279     ##\r
280     # Translate a legacy credential into a new one\r
281     #\r
282     # @param String of the legacy credential\r
283 \r
284     def translate_legacy(self, str):\r
285         legacy = CredentialLegacy(False,string=str)\r
286         self.gidCaller = legacy.get_gid_caller()\r
287         self.gidObject = legacy.get_gid_object()\r
288         lifetime = legacy.get_lifetime()\r
289         if not lifetime:\r
290             self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))\r
291         else:\r
292             self.set_expiration(int(lifetime))\r
293         self.lifeTime = legacy.get_lifetime()\r
294         self.set_privileges(legacy.get_privileges())\r
295         self.get_privileges().delegate_all_privileges(legacy.get_delegate())\r
296 \r
297     ##\r
298     # Need the issuer's private key and name\r
299     # @param key Keypair object containing the private key of the issuer\r
300     # @param gid GID of the issuing authority\r
301 \r
302     def set_issuer_keys(self, privkey, gid):\r
303         self.issuer_privkey = privkey\r
304         self.issuer_gid = gid\r
305 \r
306 \r
307     ##\r
308     # Set this credential's parent\r
309     def set_parent(self, cred):\r
310         self.parent = cred\r
311         self.updateRefID()\r
312 \r
313     ##\r
314     # set the GID of the caller\r
315     #\r
316     # @param gid GID object of the caller\r
317 \r
318     def set_gid_caller(self, gid):\r
319         self.gidCaller = gid\r
320         # gid origin caller is the caller's gid by default\r
321         self.gidOriginCaller = gid\r
322 \r
323     ##\r
324     # get the GID of the object\r
325 \r
326     def get_gid_caller(self):\r
327         if not self.gidCaller:\r
328             self.decode()\r
329         return self.gidCaller\r
330 \r
331     ##\r
332     # set the GID of the object\r
333     #\r
334     # @param gid GID object of the object\r
335 \r
336     def set_gid_object(self, gid):\r
337         self.gidObject = gid\r
338 \r
339     ##\r
340     # get the GID of the object\r
341 \r
342     def get_gid_object(self):\r
343         if not self.gidObject:\r
344             self.decode()\r
345         return self.gidObject\r
346 \r
347 \r
348             \r
349     ##\r
350     # Expiration: an absolute UTC time of expiration (as either an int or datetime)\r
351     # \r
352     def set_expiration(self, expiration):\r
353         if isinstance(expiration, int):\r
354             self.expiration = datetime.datetime.fromtimestamp(expiration)\r
355         else:\r
356             self.expiration = expiration\r
357             \r
358 \r
359     ##\r
360     # get the lifetime of the credential (in datetime format)\r
361 \r
362     def get_expiration(self):\r
363         if not self.expiration:\r
364             self.decode()\r
365         return self.expiration\r
366 \r
367     ##\r
368     # For legacy sake\r
369     def get_lifetime(self):\r
370         return self.get_expiration()\r
371  \r
372     ##\r
373     # set the privileges\r
374     #\r
375     # @param privs either a comma-separated list of privileges of a Rights object\r
376 \r
377     def set_privileges(self, privs):\r
378         if isinstance(privs, str):\r
379             self.privileges = Rights(string = privs)\r
380         else:\r
381             self.privileges = privs\r
382         \r
383 \r
384     ##\r
385     # return the privileges as a Rights object\r
386 \r
387     def get_privileges(self):\r
388         if not self.privileges:\r
389             self.decode()\r
390         return self.privileges\r
391 \r
392     ##\r
393     # determine whether the credential allows a particular operation to be\r
394     # performed\r
395     #\r
396     # @param op_name string specifying name of operation ("lookup", "update", etc)\r
397 \r
398     def can_perform(self, op_name):\r
399         rights = self.get_privileges()\r
400         \r
401         if not rights:\r
402             return False\r
403 \r
404         return rights.can_perform(op_name)\r
405 \r
406 \r
407     ##\r
408     # Encode the attributes of the credential into an XML string    \r
409     # This should be done immediately before signing the credential.    \r
410     # WARNING:\r
411     # In general, a signed credential obtained externally should\r
412     # not be changed else the signature is no longer valid.  So, once\r
413     # you have loaded an existing signed credential, do not call encode() or sign() on it.\r
414 \r
415     def encode(self):\r
416         # Create the XML document\r
417         doc = Document()\r
418         signed_cred = doc.createElement("signed-credential")\r
419 \r
420 # PG adds these. It would be nice to be consistent.\r
421 # But it's kind of odd for PL to use PG schemas that talk\r
422 # about tickets, and the PG CM policies.\r
423 # Note the careful addition of attributes from the parent below...\r
424 #        signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")\r
425 #        signed_cred.setAttribute("xsinoNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")\r
426 #        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
427 \r
428         doc.appendChild(signed_cred)  \r
429         \r
430         # Fill in the <credential> bit        \r
431         cred = doc.createElement("credential")\r
432         cred.setAttribute("xml:id", self.get_refid())\r
433         signed_cred.appendChild(cred)\r
434         append_sub(doc, cred, "type", "privilege")\r
435         append_sub(doc, cred, "serial", "8")\r
436         append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())\r
437         append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())\r
438         append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())\r
439         append_sub(doc, cred, "target_urn", self.gidObject.get_urn())\r
440         append_sub(doc, cred, "uuid", "")\r
441         if not self.expiration:\r
442             self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))\r
443         self.expiration = self.expiration.replace(microsecond=0)\r
444         append_sub(doc, cred, "expires", self.expiration.isoformat())\r
445         privileges = doc.createElement("privileges")\r
446         cred.appendChild(privileges)\r
447 \r
448         if self.privileges:\r
449             rights = self.get_privileges()\r
450             for right in rights.rights:\r
451                 priv = doc.createElement("privilege")\r
452                 append_sub(doc, priv, "name", right.kind)\r
453                 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())\r
454                 privileges.appendChild(priv)\r
455 \r
456         # Add the parent credential if it exists\r
457         if self.parent:\r
458             sdoc = parseString(self.parent.get_xml())\r
459             # If the root node is a signed-credential (it should be), then\r
460             # get all its attributes and attach those to our signed_cred\r
461             # node.\r
462             # Specifically, PG adds attributes for namespaces (which is reasonable),\r
463             # and we need to include those again here or else their signature\r
464             # no longer matches on the credential.\r
465             # We expect three of these, but here we copy them all:\r
466 #        signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")\r
467 #        signed_cred.setAttribute("xsinoNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")\r
468 #        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
469             parentRoot = sdoc.documentElement\r
470             if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():\r
471                 for attrIx in range(0, parentRoot.attributes.length):\r
472                     attr = parentRoot.attributes.item(attrIx)\r
473                     # returns the old attribute of same name that was\r
474                     # on the credential\r
475                     # Below throws InUse exception if we forgot to clone the attribute first\r
476                     oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))\r
477                     if oldAttr and oldAttr.value != attr.value:\r
478                         msg = "Delegating cred from owner %s to %s over %s 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
479                         logger.error(msg)\r
480                         raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)\r
481 \r
482             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)\r
483             p = doc.createElement("parent")\r
484             p.appendChild(p_cred)\r
485             cred.appendChild(p)\r
486         # done handling parent credential\r
487 \r
488         # Create the <signatures> tag\r
489         signatures = doc.createElement("signatures")\r
490         signed_cred.appendChild(signatures)\r
491 \r
492         # Add any parent signatures\r
493         if self.parent:\r
494             for cur_cred in self.get_credential_list()[1:]:\r
495                 sdoc = parseString(cur_cred.get_signature().get_xml())\r
496                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)\r
497                 signatures.appendChild(ele)\r
498                 \r
499         # Get the finished product\r
500         self.xml = doc.toxml()\r
501 \r
502 \r
503     def save_to_random_tmp_file(self):       \r
504         fp, filename = mkstemp(suffix='cred', text=True)\r
505         fp = os.fdopen(fp, "w")\r
506         self.save_to_file(filename, save_parents=True, filep=fp)\r
507         return filename\r
508     \r
509     def save_to_file(self, filename, save_parents=True, filep=None):\r
510         if not self.xml:\r
511             self.encode()\r
512         if filep:\r
513             f = filep \r
514         else:\r
515             f = open(filename, "w")\r
516         f.write(self.xml)\r
517         f.close()\r
518 \r
519     def save_to_string(self, save_parents=True):\r
520         if not self.xml:\r
521             self.encode()\r
522         return self.xml\r
523 \r
524     def get_refid(self):\r
525         if not self.refid:\r
526             self.refid = 'ref0'\r
527         return self.refid\r
528 \r
529     def set_refid(self, rid):\r
530         self.refid = rid\r
531 \r
532     ##\r
533     # Figure out what refids exist, and update this credential's id\r
534     # so that it doesn't clobber the others.  Returns the refids of\r
535     # the parents.\r
536     \r
537     def updateRefID(self):\r
538         if not self.parent:\r
539             self.set_refid('ref0')\r
540             return []\r
541         \r
542         refs = []\r
543 \r
544         next_cred = self.parent\r
545         while next_cred:\r
546             refs.append(next_cred.get_refid())\r
547             if next_cred.parent:\r
548                 next_cred = next_cred.parent\r
549             else:\r
550                 next_cred = None\r
551 \r
552         \r
553         # Find a unique refid for this credential\r
554         rid = self.get_refid()\r
555         while rid in refs:\r
556             val = int(rid[3:])\r
557             rid = "ref%d" % (val + 1)\r
558 \r
559         # Set the new refid\r
560         self.set_refid(rid)\r
561 \r
562         # Return the set of parent credential ref ids\r
563         return refs\r
564 \r
565     def get_xml(self):\r
566         if not self.xml:\r
567             self.encode()\r
568         return self.xml\r
569 \r
570     ##\r
571     # Sign the XML file created by encode()\r
572     #\r
573     # WARNING:\r
574     # In general, a signed credential obtained externally should\r
575     # not be changed else the signature is no longer valid.  So, once\r
576     # you have loaded an existing signed credential, do not call encode() or sign() on it.\r
577 \r
578     def sign(self):\r
579         if not self.issuer_privkey or not self.issuer_gid:\r
580             return\r
581         doc = parseString(self.get_xml())\r
582         sigs = doc.getElementsByTagName("signatures")[0]\r
583 \r
584         # Create the signature template to be signed\r
585         signature = Signature()\r
586         signature.set_refid(self.get_refid())\r
587         sdoc = parseString(signature.get_xml())        \r
588         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)\r
589         sigs.appendChild(sig_ele)\r
590 \r
591         self.xml = doc.toxml()\r
592 \r
593 \r
594         # Split the issuer GID into multiple certificates if it's a chain\r
595         chain = GID(filename=self.issuer_gid)\r
596         gid_files = []\r
597         while chain:\r
598             gid_files.append(chain.save_to_random_tmp_file(False))\r
599             if chain.get_parent():\r
600                 chain = chain.get_parent()\r
601             else:\r
602                 chain = None\r
603 \r
604 \r
605         # Call out to xmlsec1 to sign it\r
606         ref = 'Sig_%s' % self.get_refid()\r
607         filename = self.save_to_random_tmp_file()\r
608         signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \\r
609                  % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()\r
610         os.remove(filename)\r
611 \r
612         for gid_file in gid_files:\r
613             os.remove(gid_file)\r
614 \r
615         self.xml = signed\r
616 \r
617         # This is no longer a legacy credential\r
618         if self.legacy:\r
619             self.legacy = None\r
620 \r
621         # Update signatures\r
622         self.decode()       \r
623 \r
624         \r
625     ##\r
626     # Retrieve the attributes of the credential from the XML.\r
627     # This is automatically called by the various get_* methods of\r
628     # this class and should not need to be called explicitly.\r
629 \r
630     def decode(self):\r
631         if not self.xml:\r
632             return\r
633         doc = parseString(self.xml)\r
634         sigs = []\r
635         signed_cred = doc.getElementsByTagName("signed-credential")\r
636 \r
637         # Is this a signed-cred or just a cred?\r
638         if len(signed_cred) > 0:\r
639             cred = signed_cred[0].getElementsByTagName("credential")[0]\r
640             signatures = signed_cred[0].getElementsByTagName("signatures")\r
641             if len(signatures) > 0:\r
642                 sigs = signatures[0].getElementsByTagName("Signature")\r
643         else:\r
644             cred = doc.getElementsByTagName("credential")[0]\r
645         \r
646 \r
647         self.set_refid(cred.getAttribute("xml:id"))\r
648         self.set_expiration(parse(getTextNode(cred, "expires")))\r
649         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))\r
650         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   \r
651 \r
652 \r
653         # Process privileges\r
654         privs = cred.getElementsByTagName("privileges")[0]\r
655         rlist = Rights()\r
656         for priv in privs.getElementsByTagName("privilege"):\r
657             kind = getTextNode(priv, "name")\r
658             deleg = str2bool(getTextNode(priv, "can_delegate"))\r
659             if kind == '*':\r
660                 # Convert * into the default privileges for the credential's type\r
661                 # Each inherits the delegatability from the * above\r
662                 _ , type = urn_to_hrn(self.gidObject.get_urn())\r
663                 rl = rlist.determine_rights(type, self.gidObject.get_urn())\r
664                 for r in rl.rights:\r
665                     r.delegate = deleg\r
666                     rlist.add(r)\r
667             else:\r
668                 rlist.add(Right(kind.strip(), deleg))\r
669         self.set_privileges(rlist)\r
670 \r
671 \r
672         # Is there a parent?\r
673         parent = cred.getElementsByTagName("parent")\r
674         if len(parent) > 0:\r
675             parent_doc = parent[0].getElementsByTagName("credential")[0]\r
676             parent_xml = parent_doc.toxml()\r
677             self.parent = Credential(string=parent_xml)\r
678             self.updateRefID()\r
679 \r
680         # Assign the signatures to the credentials\r
681         for sig in sigs:\r
682             Sig = Signature(string=sig.toxml())\r
683 \r
684             for cur_cred in self.get_credential_list():\r
685                 if cur_cred.get_refid() == Sig.get_refid():\r
686                     cur_cred.set_signature(Sig)\r
687                                     \r
688             \r
689     ##\r
690     # Verify\r
691     #   trusted_certs: A list of trusted GID filenames (not GID objects!) \r
692     #                  Chaining is not supported within the GIDs by xmlsec1.\r
693     #\r
694     #   trusted_certs_required: Should usually be true. Set False means an\r
695     #                 empty list of trusted_certs would still let this method pass.\r
696     #                 It just skips xmlsec1 verification et al. Only used by some utils\r
697     #    \r
698     # Verify that:\r
699     # . All of the signatures are valid and that the issuers trace back\r
700     #   to trusted roots (performed by xmlsec1)\r
701     # . The XML matches the credential schema\r
702     # . That the issuer of the credential is the authority in the target's urn\r
703     #    . In the case of a delegated credential, this must be true of the root\r
704     # . That all of the gids presented in the credential are valid\r
705     # . The credential is not expired\r
706     #\r
707     # -- For Delegates (credentials with parents)\r
708     # . The privileges must be a subset of the parent credentials\r
709     # . The privileges must have "can_delegate" set for each delegated privilege\r
710     # . The target gid must be the same between child and parents\r
711     # . The expiry time on the child must be no later than the parent\r
712     # . The signer of the child must be the owner of the parent\r
713     #\r
714     # -- Verify does *NOT*\r
715     # . ensure that an xmlrpc client's gid matches a credential gid, that\r
716     #   must be done elsewhere\r
717     #\r
718     # @param trusted_certs: The certificates of trusted CA certificates\r
719     def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):\r
720         if not self.xml:\r
721             self.decode()\r
722 \r
723         # validate against RelaxNG schema\r
724         if not self.legacy:\r
725             if schema and os.path.exists(schema):\r
726                 tree = etree.parse(StringIO(self.xml))\r
727                 schema_doc = etree.parse(schema)\r
728                 xmlschema = etree.XMLSchema(schema_doc)\r
729                 if not xmlschema.validate(tree):\r
730                     error = xmlschema.error_log.last_error\r
731                     message = "%s (line %s)" % (error.message, error.line)\r
732                     raise CredentialNotVerifiable(message)        \r
733 \r
734         if trusted_certs_required and trusted_certs is None:\r
735             trusted_certs = []\r
736 \r
737 #        trusted_cert_objects = [GID(filename=f) for f in trusted_certs]\r
738         trusted_cert_objects = []\r
739         ok_trusted_certs = []\r
740         # If caller explicitly passed in None that means skip cert chain validation.\r
741         # Strange and not typical\r
742         if trusted_certs is not None:\r
743             for f in trusted_certs:\r
744                 try:\r
745                     # Failures here include unreadable files\r
746                     # or non PEM files\r
747                     trusted_cert_objects.append(GID(filename=f))\r
748                     ok_trusted_certs.append(f)\r
749                 except Exception, exc:\r
750                     logger.error("Failed to load trusted cert from %s: %r", f, exc)\r
751             trusted_certs = ok_trusted_certs\r
752 \r
753         # Use legacy verification if this is a legacy credential\r
754         if self.legacy:\r
755             self.legacy.verify_chain(trusted_cert_objects)\r
756             if self.legacy.client_gid:\r
757                 self.legacy.client_gid.verify_chain(trusted_cert_objects)\r
758             if self.legacy.object_gid:\r
759                 self.legacy.object_gid.verify_chain(trusted_cert_objects)\r
760             return True\r
761         \r
762         # make sure it is not expired\r
763         if utcparse(self.get_expiration()) < datetime.datetime.utcnow():\r
764             raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())\r
765 \r
766         # Verify the signatures\r
767         filename = self.save_to_random_tmp_file()\r
768         if trusted_certs is not None:\r
769             cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])\r
770 \r
771         # If caller explicitly passed in None that means skip cert chain validation.\r
772         # Strange and not typical\r
773         if trusted_certs is not None:\r
774             # Verify the gids of this cred and of its parents\r
775             for cur_cred in self.get_credential_list():\r
776                 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)\r
777                 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)\r
778 \r
779         refs = []\r
780         refs.append("Sig_%s" % self.get_refid())\r
781 \r
782         parentRefs = self.updateRefID()\r
783         for ref in parentRefs:\r
784             refs.append("Sig_%s" % ref)\r
785 \r
786         for ref in refs:\r
787             # If caller explicitly passed in None that means skip xmlsec1 validation.\r
788             # Strange and not typical\r
789             if trusted_certs is None:\r
790                 break\r
791 \r
792 #            print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \\r
793 #                (self.xmlsec_path, ref, cert_args, filename)\r
794             verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \\r
795                             % (self.xmlsec_path, ref, cert_args, filename)).read()\r
796             if not verified.strip().startswith("OK"):\r
797                 # xmlsec errors have a msg= which is the interesting bit.\r
798                 mstart = verified.find("msg=")\r
799                 msg = ""\r
800                 if mstart > -1 and len(verified) > 4:\r
801                     mstart = mstart + 4\r
802                     mend = verified.find('\\', mstart)\r
803                     msg = verified[mstart:mend]\r
804                 raise CredentialNotVerifiable("xmlsec1 error verifying cred using Signature ID %s: %s %s" % (ref, msg, verified.strip()))\r
805         os.remove(filename)\r
806 \r
807         # Verify the parents (delegation)\r
808         if self.parent:\r
809             self.verify_parent(self.parent)\r
810 \r
811         # Make sure the issuer is the target's authority\r
812         self.verify_issuer()\r
813         return True\r
814 \r
815     ##\r
816     # Creates a list of the credential and its parents, with the root \r
817     # (original delegated credential) as the last item in the list\r
818     def get_credential_list(self):    \r
819         cur_cred = self\r
820         list = []\r
821         while cur_cred:\r
822             list.append(cur_cred)\r
823             if cur_cred.parent:\r
824                 cur_cred = cur_cred.parent\r
825             else:\r
826                 cur_cred = None\r
827         return list\r
828     \r
829     ##\r
830     # Make sure the credential's target gid was signed by (or is the same) the entity that signed\r
831     # the original credential or an authority over that namespace.\r
832     def verify_issuer(self):                \r
833         root_cred = self.get_credential_list()[-1]\r
834         root_target_gid = root_cred.get_gid_object()\r
835         root_cred_signer = root_cred.get_signature().get_issuer_gid()\r
836 \r
837         if root_target_gid.is_signed_by_cert(root_cred_signer):\r
838             # cred signer matches target signer, return success\r
839             return\r
840 \r
841         root_target_gid_str = root_target_gid.save_to_string()\r
842         root_cred_signer_str = root_cred_signer.save_to_string()\r
843         if root_target_gid_str == root_cred_signer_str:\r
844             # cred signer is target, return success\r
845             return\r
846 \r
847         # See if it the signer is an authority over the domain of the target\r
848         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())\r
849         root_cred_signer_type = root_cred_signer.get_type()\r
850         if (root_cred_signer_type == 'authority'):\r
851             #sfa_logger.debug('Cred signer is an authority')\r
852             # signer is an authority, see if target is in authority's domain\r
853             hrn = root_cred_signer.get_hrn()\r
854             if root_target_gid.get_hrn().startswith(hrn):\r
855                 return\r
856 \r
857         # We've required that the credential be signed by an authority\r
858         # for that domain. Reasonable and probably correct.\r
859         # A looser model would also allow the signer to be an authority\r
860         # in my control framework - eg My CA or CH. Even if it is not\r
861         # the CH that issued these, eg, user credentials.\r
862 \r
863         # Give up, credential does not pass issuer verification\r
864 \r
865         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
866 \r
867 \r
868     ##\r
869     # -- For Delegates (credentials with parents) verify that:\r
870     # . The privileges must be a subset of the parent credentials\r
871     # . The privileges must have "can_delegate" set for each delegated privilege\r
872     # . The target gid must be the same between child and parents\r
873     # . The expiry time on the child must be no later than the parent\r
874     # . The signer of the child must be the owner of the parent        \r
875     def verify_parent(self, parent_cred):\r
876         # make sure the rights given to the child are a subset of the\r
877         # parents rights (and check delegate bits)\r
878         if not parent_cred.get_privileges().is_superset(self.get_privileges()):\r
879             raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % self.parent.get_refid()) + \r
880                 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred ref %s rights " % self.get_refid()) +\r
881                 self.get_privileges().save_to_string())\r
882 \r
883         # make sure my target gid is the same as the parent's\r
884         if not parent_cred.get_gid_object().save_to_string() == \\r
885            self.get_gid_object().save_to_string():\r
886             raise CredentialNotVerifiable("Target gid not equal between parent and child")\r
887 \r
888         # make sure my expiry time is <= my parent's\r
889         if not parent_cred.get_expiration() >= self.get_expiration():\r
890             raise CredentialNotVerifiable("Delegated credential expires after parent")\r
891 \r
892         # make sure my signer is the parent's caller\r
893         if not parent_cred.get_gid_caller().save_to_string(False) == \\r
894            self.get_signature().get_issuer_gid().save_to_string(False):\r
895             raise CredentialNotVerifiable("Delegated credential not signed by parent caller")\r
896                 \r
897         # Recurse\r
898         if parent_cred.parent:\r
899             parent_cred.verify_parent(parent_cred.parent)\r
900 \r
901 \r
902     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):\r
903         """\r
904         Return a delegated copy of this credential, delegated to the \r
905         specified gid's user.    \r
906         """\r
907         # get the gid of the object we are delegating\r
908         object_gid = self.get_gid_object()\r
909         object_hrn = object_gid.get_hrn()        \r
910  \r
911         # the hrn of the user who will be delegated to\r
912         delegee_gid = GID(filename=delegee_gidfile)\r
913         delegee_hrn = delegee_gid.get_hrn()\r
914   \r
915         #user_key = Keypair(filename=keyfile)\r
916         #user_hrn = self.get_gid_caller().get_hrn()\r
917         subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)\r
918         dcred = Credential(subject=subject_string)\r
919         dcred.set_gid_caller(delegee_gid)\r
920         dcred.set_gid_object(object_gid)\r
921         dcred.set_parent(self)\r
922         dcred.set_expiration(self.get_expiration())\r
923         dcred.set_privileges(self.get_privileges())\r
924         dcred.get_privileges().delegate_all_privileges(True)\r
925         #dcred.set_issuer_keys(keyfile, delegee_gidfile)\r
926         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)\r
927         dcred.encode()\r
928         dcred.sign()\r
929 \r
930         return dcred\r
931 \r
932     # only informative\r
933     def get_filename(self):\r
934         return getattr(self,'filename',None)\r
935  \r
936     ##\r
937     # Dump the contents of a credential to stdout in human-readable format\r
938     #\r
939     # @param dump_parents If true, also dump the parent certificates\r
940     def dump (self, *args, **kwargs):\r
941         print self.dump_string(*args, **kwargs)\r
942 \r
943 \r
944     def dump_string(self, dump_parents=False):\r
945         result=""\r
946         result += "CREDENTIAL %s\n" % self.get_subject()\r
947         filename=self.get_filename()\r
948         if filename: result += "Filename %s\n"%filename\r
949         result += "      privs: %s\n" % self.get_privileges().save_to_string()\r
950         gidCaller = self.get_gid_caller()\r
951         if gidCaller:\r
952             result += "  gidCaller:\n"\r
953             result += gidCaller.dump_string(8, dump_parents)\r
954 \r
955         if self.get_signature():\r
956             print "  gidIssuer:"\r
957             self.get_signature().get_issuer_gid().dump(8, dump_parents)\r
958 \r
959         gidObject = self.get_gid_object()\r
960         if gidObject:\r
961             result += "  gidObject:\n"\r
962             result += gidObject.dump_string(8, dump_parents)\r
963 \r
964         if self.parent and dump_parents:\r
965             result += "\nPARENT"\r
966             result += self.parent.dump(True)\r
967 \r
968         return result\r