1 #----------------------------------------------------------------------
2 # Copyright (c) 2008 Board of Trustees, Princeton University
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and/or hardware specification (the "Work") to
6 # deal in the Work without restriction, including without limitation the
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Work, and to permit persons to whom the Work
9 # is furnished to do so, subject to the following conditions:
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Work.
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
22 #----------------------------------------------------------------------
24 # Implements SFA Credentials
26 # Credentials are signed XML files that assign a subject gid privileges to an object gid
30 from types import StringTypes
32 from StringIO import StringIO
33 from tempfile import mkstemp
34 from xml.dom.minidom import Document, parseString
38 from lxml import etree
43 from xml.parsers.expat import ExpatError
45 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
46 from sfa.util.sfalogging import logger
47 from sfa.util.sfatime import utcparse
48 from sfa.trust.rights import Right, Rights, determine_rights
49 from sfa.trust.gid import GID
50 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
53 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
57 # . make privs match between PG and PL
58 # . Need to add support for other types of credentials, e.g. tickets
59 # . add namespaces to signed-credential element?
61 signature_template = \
63 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
65 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
66 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
69 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
71 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
72 <DigestValue></DigestValue>
87 # PG formats the template (whitespace) slightly differently.
88 # Note that they don't include the xmlns in the template, but add it later.
89 # Otherwise the two are equivalent.
90 #signature_template_as_in_pg = \
92 #<Signature xml:id="Sig_%s" >
94 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
95 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
96 # <Reference URI="#%s">
98 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
100 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
101 # <DigestValue></DigestValue>
108 # <X509IssuerSerial/>
117 # Convert a string into a bool
118 # used to convert an xsd:boolean to a Python boolean
120 if str.lower() in ['true','1']:
126 # Utility function to get the text of an XML element
128 def getTextNode(element, subele):
129 sub = element.getElementsByTagName(subele)[0]
130 if len(sub.childNodes) > 0:
131 return sub.childNodes[0].nodeValue
136 # Utility function to set the text of an XML element
137 # It creates the element, adds the text to it,
138 # and then appends it to the parent.
140 def append_sub(doc, parent, element, text):
141 ele = doc.createElement(element)
142 ele.appendChild(doc.createTextNode(text))
143 parent.appendChild(ele)
146 # Signature contains information about an xmlsec1 signature
147 # for a signed-credential
150 class Signature(object):
152 def __init__(self, string=None):
154 self.issuer_gid = None
171 def set_refid(self, id):
174 def get_issuer_gid(self):
179 def set_issuer_gid(self, gid):
184 doc = parseString(self.xml)
186 logger.log_exc ("Failed to parse credential, %s"%self.xml)
188 sig = doc.getElementsByTagName("Signature")[0]
189 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
190 keyinfo = sig.getElementsByTagName("X509Data")[0]
191 szgid = getTextNode(keyinfo, "X509Certificate")
192 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
193 self.set_issuer_gid(GID(string=szgid))
196 self.xml = signature_template % (self.get_refid(), self.get_refid())
199 # A credential provides a caller gid with privileges to an object gid.
200 # A signed credential is signed by the object's authority.
202 # Credentials are encoded in one of two ways.
203 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
204 # The new credentials are placed in signed XML.
207 # In general, a signed credential obtained externally should
208 # not be changed else the signature is no longer valid. So, once
209 # you have loaded an existing signed credential, do not call encode() or sign() on it.
211 def filter_creds_by_caller(creds, caller_hrn_list):
213 Returns a list of creds who's gid caller matches the
216 if not isinstance(creds, list): creds = [creds]
217 if not isinstance(caller_hrn_list, list):
218 caller_hrn_list = [caller_hrn_list]
222 tmp_cred = Credential(string=cred)
223 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
224 caller_creds.append(cred)
228 class Credential(object):
231 # Create a Credential object
233 # @param create If true, create a blank x509 certificate
234 # @param subject If subject!=None, create an x509 cert with the subject name
235 # @param string If string!=None, load the credential from the string
236 # @param filename If filename!=None, load the credential from the file
237 # FIXME: create and subject are ignored!
238 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
239 self.gidCaller = None
240 self.gidObject = None
241 self.expiration = None
242 self.privileges = None
243 self.issuer_privkey = None
244 self.issuer_gid = None
245 self.issuer_pubkey = None
247 self.signature = None
254 if isinstance(cred, StringTypes):
256 self.type = 'geni_sfa'
258 elif isinstance(cred, dict):
259 string = cred['geni_value']
260 self.type = cred['geni_type']
261 self.version = cred['geni_version']
264 if string or filename:
268 str = file(filename).read()
270 # if this is a legacy credential, write error and bail out
271 if isinstance (str, StringTypes) and str.strip().startswith("-----"):
272 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
278 # Find an xmlsec1 path
279 self.xmlsec_path = ''
280 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
282 if os.path.isfile(path + '/' + 'xmlsec1'):
283 self.xmlsec_path = path + '/' + 'xmlsec1'
286 def get_subject(self):
288 if not self.gidObject:
291 subject = self.gidObject.get_printable_subject()
294 # sounds like this should be __repr__ instead ??
295 def get_summary_tostring(self):
296 if not self.gidObject:
298 obj = self.gidObject.get_printable_subject()
299 caller = self.gidCaller.get_printable_subject()
300 exp = self.get_expiration()
301 # Summarize the rights too? The issuer?
302 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
304 def get_signature(self):
305 if not self.signature:
307 return self.signature
309 def set_signature(self, sig):
314 # Need the issuer's private key and name
315 # @param key Keypair object containing the private key of the issuer
316 # @param gid GID of the issuing authority
318 def set_issuer_keys(self, privkey, gid):
319 self.issuer_privkey = privkey
320 self.issuer_gid = gid
324 # Set this credential's parent
325 def set_parent(self, cred):
330 # set the GID of the caller
332 # @param gid GID object of the caller
334 def set_gid_caller(self, gid):
336 # gid origin caller is the caller's gid by default
337 self.gidOriginCaller = gid
340 # get the GID of the object
342 def get_gid_caller(self):
343 if not self.gidCaller:
345 return self.gidCaller
348 # set the GID of the object
350 # @param gid GID object of the object
352 def set_gid_object(self, gid):
356 # get the GID of the object
358 def get_gid_object(self):
359 if not self.gidObject:
361 return self.gidObject
364 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
366 def set_expiration(self, expiration):
367 if isinstance(expiration, (int, float)):
368 self.expiration = datetime.datetime.fromtimestamp(expiration)
369 elif isinstance (expiration, datetime.datetime):
370 self.expiration = expiration
371 elif isinstance (expiration, StringTypes):
372 self.expiration = utcparse (expiration)
374 logger.error ("unexpected input type in Credential.set_expiration")
378 # get the lifetime of the credential (always in datetime format)
380 def get_expiration(self):
381 if not self.expiration:
383 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
384 return self.expiration
389 # @param privs either a comma-separated list of privileges of a Rights object
391 def set_privileges(self, privs):
392 if isinstance(privs, str):
393 self.privileges = Rights(string = privs)
395 self.privileges = privs
398 # return the privileges as a Rights object
400 def get_privileges(self):
401 if not self.privileges:
403 return self.privileges
406 # determine whether the credential allows a particular operation to be
409 # @param op_name string specifying name of operation ("lookup", "update", etc)
411 def can_perform(self, op_name):
412 rights = self.get_privileges()
417 return rights.can_perform(op_name)
421 # Encode the attributes of the credential into an XML string
422 # This should be done immediately before signing the credential.
424 # In general, a signed credential obtained externally should
425 # not be changed else the signature is no longer valid. So, once
426 # you have loaded an existing signed credential, do not call encode() or sign() on it.
429 # Create the XML document
431 signed_cred = doc.createElement("signed-credential")
434 # Note that credential/policy.xsd are really the PG schemas
436 # Note that delegation of credentials between the 2 only really works
437 # cause those schemas are identical.
438 # Also note these PG schemas talk about PG tickets and CM policies.
439 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
440 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
441 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")
443 # PG says for those last 2:
444 #signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
445 # 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")
447 doc.appendChild(signed_cred)
449 # Fill in the <credential> bit
450 cred = doc.createElement("credential")
451 cred.setAttribute("xml:id", self.get_refid())
452 signed_cred.appendChild(cred)
453 append_sub(doc, cred, "type", "privilege")
454 append_sub(doc, cred, "serial", "8")
455 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
456 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
457 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
458 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
459 append_sub(doc, cred, "uuid", "")
460 if not self.expiration:
461 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
462 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
463 self.expiration = self.expiration.replace(microsecond=0)
464 append_sub(doc, cred, "expires", self.expiration.isoformat())
465 privileges = doc.createElement("privileges")
466 cred.appendChild(privileges)
469 rights = self.get_privileges()
470 for right in rights.rights:
471 priv = doc.createElement("privilege")
472 append_sub(doc, priv, "name", right.kind)
473 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
474 privileges.appendChild(priv)
476 # Add the parent credential if it exists
478 sdoc = parseString(self.parent.get_xml())
479 # If the root node is a signed-credential (it should be), then
480 # get all its attributes and attach those to our signed_cred
482 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
483 # and we need to include those again here or else their signature
484 # no longer matches on the credential.
485 # We expect three of these, but here we copy them all:
486 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
487 # and from PG (PL is equivalent, as shown above):
488 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
489 # 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")
492 # PL now also declares these, with different URLs, so
493 # the code notices those attributes already existed with
494 # different values, and complains.
495 # This happens regularly on delegation now that PG and
496 # PL both declare the namespace with different URLs.
497 # If the content ever differs this is a problem,
498 # but for now it works - different URLs (values in the attributes)
499 # but the same actual schema, so using the PG schema
500 # on delegated-to-PL credentials works fine.
502 # Note: you could also not copy attributes
503 # which already exist. It appears that both PG and PL
504 # will actually validate a slicecred with a parent
505 # signed using PG namespaces and a child signed with PL
506 # namespaces over the whole thing. But I don't know
507 # if that is a bug in xmlsec1, an accident since
508 # the contents of the schemas are the same,
509 # or something else, but it seems odd. And this works.
510 parentRoot = sdoc.documentElement
511 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
512 for attrIx in range(0, parentRoot.attributes.length):
513 attr = parentRoot.attributes.item(attrIx)
514 # returns the old attribute of same name that was
516 # Below throws InUse exception if we forgot to clone the attribute first
517 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
518 if oldAttr and oldAttr.value != attr.value:
519 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
521 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
523 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
524 p = doc.createElement("parent")
525 p.appendChild(p_cred)
527 # done handling parent credential
529 # Create the <signatures> tag
530 signatures = doc.createElement("signatures")
531 signed_cred.appendChild(signatures)
533 # Add any parent signatures
535 for cur_cred in self.get_credential_list()[1:]:
536 sdoc = parseString(cur_cred.get_signature().get_xml())
537 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
538 signatures.appendChild(ele)
540 # Get the finished product
541 self.xml = doc.toxml()
544 def save_to_random_tmp_file(self):
545 fp, filename = mkstemp(suffix='cred', text=True)
546 fp = os.fdopen(fp, "w")
547 self.save_to_file(filename, save_parents=True, filep=fp)
550 def save_to_file(self, filename, save_parents=True, filep=None):
556 f = open(filename, "w")
560 def save_to_string(self, save_parents=True):
570 def set_refid(self, rid):
574 # Figure out what refids exist, and update this credential's id
575 # so that it doesn't clobber the others. Returns the refids of
578 def updateRefID(self):
580 self.set_refid('ref0')
585 next_cred = self.parent
587 refs.append(next_cred.get_refid())
589 next_cred = next_cred.parent
594 # Find a unique refid for this credential
595 rid = self.get_refid()
598 rid = "ref%d" % (val + 1)
603 # Return the set of parent credential ref ids
612 # Sign the XML file created by encode()
615 # In general, a signed credential obtained externally should
616 # not be changed else the signature is no longer valid. So, once
617 # you have loaded an existing signed credential, do not call encode() or sign() on it.
620 if not self.issuer_privkey or not self.issuer_gid:
622 doc = parseString(self.get_xml())
623 sigs = doc.getElementsByTagName("signatures")[0]
625 # Create the signature template to be signed
626 signature = Signature()
627 signature.set_refid(self.get_refid())
628 sdoc = parseString(signature.get_xml())
629 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
630 sigs.appendChild(sig_ele)
632 self.xml = doc.toxml()
635 # Split the issuer GID into multiple certificates if it's a chain
636 chain = GID(filename=self.issuer_gid)
639 gid_files.append(chain.save_to_random_tmp_file(False))
640 if chain.get_parent():
641 chain = chain.get_parent()
646 # Call out to xmlsec1 to sign it
647 ref = 'Sig_%s' % self.get_refid()
648 filename = self.save_to_random_tmp_file()
649 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
650 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
653 for gid_file in gid_files:
663 # Retrieve the attributes of the credential from the XML.
664 # This is automatically called by the various get_* methods of
665 # this class and should not need to be called explicitly.
673 doc = parseString(self.xml)
675 raise CredentialNotVerifiable("Malformed credential")
676 doc = parseString(self.xml)
678 signed_cred = doc.getElementsByTagName("signed-credential")
680 # Is this a signed-cred or just a cred?
681 if len(signed_cred) > 0:
682 creds = signed_cred[0].getElementsByTagName("credential")
683 signatures = signed_cred[0].getElementsByTagName("signatures")
684 if len(signatures) > 0:
685 sigs = signatures[0].getElementsByTagName("Signature")
687 creds = doc.getElementsByTagName("credential")
689 if creds is None or len(creds) == 0:
690 # malformed cred file
691 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
693 # Just take the first cred if there are more than one
696 self.set_refid(cred.getAttribute("xml:id"))
697 self.set_expiration(utcparse(getTextNode(cred, "expires")))
698 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
699 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
703 privs = cred.getElementsByTagName("privileges")[0]
705 for priv in privs.getElementsByTagName("privilege"):
706 kind = getTextNode(priv, "name")
707 deleg = str2bool(getTextNode(priv, "can_delegate"))
709 # Convert * into the default privileges for the credential's type
710 # Each inherits the delegatability from the * above
711 _ , type = urn_to_hrn(self.gidObject.get_urn())
712 rl = determine_rights(type, self.gidObject.get_urn())
717 rlist.add(Right(kind.strip(), deleg))
718 self.set_privileges(rlist)
722 parent = cred.getElementsByTagName("parent")
724 parent_doc = parent[0].getElementsByTagName("credential")[0]
725 parent_xml = parent_doc.toxml()
726 self.parent = Credential(string=parent_xml)
729 # Assign the signatures to the credentials
731 Sig = Signature(string=sig.toxml())
733 for cur_cred in self.get_credential_list():
734 if cur_cred.get_refid() == Sig.get_refid():
735 cur_cred.set_signature(Sig)
740 # trusted_certs: A list of trusted GID filenames (not GID objects!)
741 # Chaining is not supported within the GIDs by xmlsec1.
743 # trusted_certs_required: Should usually be true. Set False means an
744 # empty list of trusted_certs would still let this method pass.
745 # It just skips xmlsec1 verification et al. Only used by some utils
748 # . All of the signatures are valid and that the issuers trace back
749 # to trusted roots (performed by xmlsec1)
750 # . The XML matches the credential schema
751 # . That the issuer of the credential is the authority in the target's urn
752 # . In the case of a delegated credential, this must be true of the root
753 # . That all of the gids presented in the credential are valid
754 # . Including verifying GID chains, and includ the issuer
755 # . The credential is not expired
757 # -- For Delegates (credentials with parents)
758 # . The privileges must be a subset of the parent credentials
759 # . The privileges must have "can_delegate" set for each delegated privilege
760 # . The target gid must be the same between child and parents
761 # . The expiry time on the child must be no later than the parent
762 # . The signer of the child must be the owner of the parent
764 # -- Verify does *NOT*
765 # . ensure that an xmlrpc client's gid matches a credential gid, that
766 # must be done elsewhere
768 # @param trusted_certs: The certificates of trusted CA certificates
769 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
773 # validate against RelaxNG schema
775 if schema and os.path.exists(schema):
776 tree = etree.parse(StringIO(self.xml))
777 schema_doc = etree.parse(schema)
778 xmlschema = etree.XMLSchema(schema_doc)
779 if not xmlschema.validate(tree):
780 error = xmlschema.error_log.last_error
781 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
782 raise CredentialNotVerifiable(message)
784 if trusted_certs_required and trusted_certs is None:
787 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
788 trusted_cert_objects = []
789 ok_trusted_certs = []
790 # If caller explicitly passed in None that means skip cert chain validation.
791 # Strange and not typical
792 if trusted_certs is not None:
793 for f in trusted_certs:
795 # Failures here include unreadable files
797 trusted_cert_objects.append(GID(filename=f))
798 ok_trusted_certs.append(f)
799 except Exception, exc:
800 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
801 trusted_certs = ok_trusted_certs
803 # make sure it is not expired
804 if self.get_expiration() < datetime.datetime.utcnow():
805 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
807 # Verify the signatures
808 filename = self.save_to_random_tmp_file()
809 if trusted_certs is not None:
810 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
812 # If caller explicitly passed in None that means skip cert chain validation.
813 # - Strange and not typical
814 if trusted_certs is not None:
815 # Verify the gids of this cred and of its parents
816 for cur_cred in self.get_credential_list():
817 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
818 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
821 refs.append("Sig_%s" % self.get_refid())
823 parentRefs = self.updateRefID()
824 for ref in parentRefs:
825 refs.append("Sig_%s" % ref)
828 # If caller explicitly passed in None that means skip xmlsec1 validation.
829 # Strange and not typical
830 if trusted_certs is None:
833 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
834 # (self.xmlsec_path, ref, cert_args, filename)
835 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
836 % (self.xmlsec_path, ref, cert_args, filename)).read()
837 if not verified.strip().startswith("OK"):
838 # xmlsec errors have a msg= which is the interesting bit.
839 mstart = verified.find("msg=")
841 if mstart > -1 and len(verified) > 4:
843 mend = verified.find('\\', mstart)
844 msg = verified[mstart:mend]
845 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
848 # Verify the parents (delegation)
850 self.verify_parent(self.parent)
852 # Make sure the issuer is the target's authority, and is
854 self.verify_issuer(trusted_cert_objects)
858 # Creates a list of the credential and its parents, with the root
859 # (original delegated credential) as the last item in the list
860 def get_credential_list(self):
864 list.append(cur_cred)
866 cur_cred = cur_cred.parent
872 # Make sure the credential's target gid (a) was signed by or (b)
873 # is the same as the entity that signed the original credential,
874 # or (c) is an authority over the target's namespace.
875 # Also ensure that the credential issuer / signer itself has a valid
876 # GID signature chain (signed by an authority with namespace rights).
877 def verify_issuer(self, trusted_gids):
878 root_cred = self.get_credential_list()[-1]
879 root_target_gid = root_cred.get_gid_object()
880 root_cred_signer = root_cred.get_signature().get_issuer_gid()
883 # Allow non authority to sign target and cred about target.
885 # Why do we need to allow non authorities to sign?
886 # If in the target gid validation step we correctly
887 # checked that the target is only signed by an authority,
888 # then this is just a special case of case 3.
889 # This short-circuit is the common case currently -
890 # and cause GID validation doesn't check 'authority',
891 # this allows users to generate valid slice credentials.
892 if root_target_gid.is_signed_by_cert(root_cred_signer):
893 # cred signer matches target signer, return success
897 # Allow someone to sign credential about themeselves. Used?
898 # If not, remove this.
899 #root_target_gid_str = root_target_gid.save_to_string()
900 #root_cred_signer_str = root_cred_signer.save_to_string()
901 #if root_target_gid_str == root_cred_signer_str:
902 # # cred signer is target, return success
907 # root_cred_signer is not the target_gid
908 # So this is a different gid that we have not verified.
909 # xmlsec1 verified the cert chain on this already, but
910 # it hasn't verified that the gid meets the HRN namespace
912 # Below we'll ensure that it is an authority.
913 # But we haven't verified that it is _signed by_ an authority
914 # We also don't know if xmlsec1 requires that cert signers
917 # Note that if verify() gave us no trusted_gids then this
918 # call will fail. So skip it if we have no trusted_gids
919 if trusted_gids and len(trusted_gids) > 0:
920 root_cred_signer.verify_chain(trusted_gids)
922 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
924 # See if the signer is an authority over the domain of the target.
925 # There are multiple types of authority - accept them all here
926 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
927 root_cred_signer_type = root_cred_signer.get_type()
928 if (root_cred_signer_type.find('authority') == 0):
929 #logger.debug('Cred signer is an authority')
930 # signer is an authority, see if target is in authority's domain
931 signerhrn = root_cred_signer.get_hrn()
932 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
935 # We've required that the credential be signed by an authority
936 # for that domain. Reasonable and probably correct.
937 # A looser model would also allow the signer to be an authority
938 # in my control framework - eg My CA or CH. Even if it is not
939 # the CH that issued these, eg, user credentials.
941 # Give up, credential does not pass issuer verification
943 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()))
947 # -- For Delegates (credentials with parents) verify that:
948 # . The privileges must be a subset of the parent credentials
949 # . The privileges must have "can_delegate" set for each delegated privilege
950 # . The target gid must be the same between child and parents
951 # . The expiry time on the child must be no later than the parent
952 # . The signer of the child must be the owner of the parent
953 def verify_parent(self, parent_cred):
954 # make sure the rights given to the child are a subset of the
955 # parents rights (and check delegate bits)
956 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
957 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
958 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
959 self.get_privileges().save_to_string())
961 # make sure my target gid is the same as the parent's
962 if not parent_cred.get_gid_object().save_to_string() == \
963 self.get_gid_object().save_to_string():
964 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
966 # make sure my expiry time is <= my parent's
967 if not parent_cred.get_expiration() >= self.get_expiration():
968 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
970 # make sure my signer is the parent's caller
971 if not parent_cred.get_gid_caller().save_to_string(False) == \
972 self.get_signature().get_issuer_gid().save_to_string(False):
973 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
976 if parent_cred.parent:
977 parent_cred.verify_parent(parent_cred.parent)
980 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
982 Return a delegated copy of this credential, delegated to the
983 specified gid's user.
985 # get the gid of the object we are delegating
986 object_gid = self.get_gid_object()
987 object_hrn = object_gid.get_hrn()
989 # the hrn of the user who will be delegated to
990 delegee_gid = GID(filename=delegee_gidfile)
991 delegee_hrn = delegee_gid.get_hrn()
993 #user_key = Keypair(filename=keyfile)
994 #user_hrn = self.get_gid_caller().get_hrn()
995 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
996 dcred = Credential(subject=subject_string)
997 dcred.set_gid_caller(delegee_gid)
998 dcred.set_gid_object(object_gid)
999 dcred.set_parent(self)
1000 dcred.set_expiration(self.get_expiration())
1001 dcred.set_privileges(self.get_privileges())
1002 dcred.get_privileges().delegate_all_privileges(True)
1003 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1004 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1011 def get_filename(self):
1012 return getattr(self,'filename',None)
1014 def actual_caller_hrn (self):
1015 """a helper method used by some API calls like e.g. Allocate
1016 to try and find out who really is the original caller
1018 This admittedly is a bit of a hack, please USE IN LAST RESORT
1020 This code uses a heuristic to identify a delegated credential
1022 A first known restriction if for traffic that gets through a slice manager
1023 in this case the hrn reported is the one from the last SM in the call graph
1024 which is not at all what is meant here"""
1026 caller_hrn = self.get_gid_caller().get_hrn()
1027 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1028 subject_hrn = self.get_gid_object().get_hrn()
1029 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1030 # this seems to be a 'regular' credential
1031 if caller_hrn.startswith(issuer_hrn):
1032 actual_caller_hrn=caller_hrn
1033 # else this looks like a delegated credential, and the real caller is the issuer
1035 actual_caller_hrn=issuer_hrn
1036 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"%(caller_hrn,issuer_hrn,actual_caller_hrn))
1037 return actual_caller_hrn
1040 # Dump the contents of a credential to stdout in human-readable format
1042 # @param dump_parents If true, also dump the parent certificates
1043 def dump (self, *args, **kwargs):
1044 print self.dump_string(*args, **kwargs)
1046 # show_xml is ignored
1047 def dump_string(self, dump_parents=False, show_xml=None):
1049 result += "CREDENTIAL %s\n" % self.get_subject()
1050 filename=self.get_filename()
1051 if filename: result += "Filename %s\n"%filename
1052 privileges = self.get_privileges()
1054 result += " privs: %s\n" % privileges.save_to_string()
1056 result += " privs: \n"
1057 gidCaller = self.get_gid_caller()
1059 result += " gidCaller:\n"
1060 result += gidCaller.dump_string(8, dump_parents)
1062 if self.get_signature():
1064 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1067 print " expiration:", self.expiration.isoformat()
1069 gidObject = self.get_gid_object()
1071 result += " gidObject:\n"
1072 result += gidObject.dump_string(8, dump_parents)
1074 if self.parent and dump_parents:
1075 result += "\nPARENT"
1076 result += self.parent.dump_string(True)