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