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