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 sfa.util.faults import *
44 from sfa.util.sfalogging import logger
45 from sfa.util.sfatime import utcparse
46 from sfa.trust.certificate import Keypair
47 from sfa.trust.credential_legacy import CredentialLegacy
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 * 14
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):
183 doc = parseString(self.xml)
184 sig = doc.getElementsByTagName("Signature")[0]
185 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
186 keyinfo = sig.getElementsByTagName("X509Data")[0]
187 szgid = getTextNode(keyinfo, "X509Certificate")
188 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
189 self.set_issuer_gid(GID(string=szgid))
192 self.xml = signature_template % (self.get_refid(), self.get_refid())
196 # A credential provides a caller gid with privileges to an object gid.
197 # A signed credential is signed by the object's authority.
199 # Credentials are encoded in one of two ways. The legacy style places
200 # it in the subjectAltName of an X509 certificate. The new credentials
201 # are placed in signed XML.
204 # In general, a signed credential obtained externally should
205 # not be changed else the signature is no longer valid. So, once
206 # you have loaded an existing signed credential, do not call encode() or sign() on it.
208 def filter_creds_by_caller(creds, caller_hrn):
210 Returns a list of creds who's gid caller matches the
213 if not isinstance(creds, list): creds = [creds]
217 tmp_cred = Credential(string=cred)
218 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
219 caller_creds.append(cred)
223 class Credential(object):
226 # Create a Credential object
228 # @param create If true, create a blank x509 certificate
229 # @param subject If subject!=None, create an x509 cert with the subject name
230 # @param string If string!=None, load the credential from the string
231 # @param filename If filename!=None, load the credential from the file
232 # FIXME: create and subject are ignored!
233 def __init__(self, create=False, subject=None, string=None, filename=None):
234 self.gidCaller = None
235 self.gidObject = None
236 self.expiration = None
237 self.privileges = None
238 self.issuer_privkey = None
239 self.issuer_gid = None
240 self.issuer_pubkey = None
242 self.signature = None
247 # Check if this is a legacy credential, translate it if so
248 if string or filename:
252 str = file(filename).read()
254 if str.strip().startswith("-----"):
255 self.legacy = CredentialLegacy(False,string=str)
256 self.translate_legacy(str)
261 # Find an xmlsec1 path
262 self.xmlsec_path = ''
263 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
265 if os.path.isfile(path + '/' + 'xmlsec1'):
266 self.xmlsec_path = path + '/' + 'xmlsec1'
269 def get_subject(self):
270 if not self.gidObject:
272 return self.gidObject.get_printable_subject()
274 def get_summary_tostring(self):
275 if not self.gidObject:
277 obj = self.gidObject.get_printable_subject()
278 caller = self.gidCaller.get_printable_subject()
279 exp = self.get_expiration()
280 # Summarize the rights too? The issuer?
281 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
283 def get_signature(self):
284 if not self.signature:
286 return self.signature
288 def set_signature(self, sig):
293 # Translate a legacy credential into a new one
295 # @param String of the legacy credential
297 def translate_legacy(self, str):
298 legacy = CredentialLegacy(False,string=str)
299 self.gidCaller = legacy.get_gid_caller()
300 self.gidObject = legacy.get_gid_object()
301 lifetime = legacy.get_lifetime()
303 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
305 self.set_expiration(int(lifetime))
306 self.lifeTime = legacy.get_lifetime()
307 self.set_privileges(legacy.get_privileges())
308 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
311 # Need the issuer's private key and name
312 # @param key Keypair object containing the private key of the issuer
313 # @param gid GID of the issuing authority
315 def set_issuer_keys(self, privkey, gid):
316 self.issuer_privkey = privkey
317 self.issuer_gid = gid
321 # Set this credential's parent
322 def set_parent(self, cred):
327 # set the GID of the caller
329 # @param gid GID object of the caller
331 def set_gid_caller(self, gid):
333 # gid origin caller is the caller's gid by default
334 self.gidOriginCaller = gid
337 # get the GID of the object
339 def get_gid_caller(self):
340 if not self.gidCaller:
342 return self.gidCaller
345 # set the GID of the object
347 # @param gid GID object of the object
349 def set_gid_object(self, gid):
353 # get the GID of the object
355 def get_gid_object(self):
356 if not self.gidObject:
358 return self.gidObject
363 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
365 def set_expiration(self, expiration):
366 if isinstance(expiration, (int, float)):
367 self.expiration = datetime.datetime.fromtimestamp(expiration)
368 elif isinstance (expiration, datetime.datetime):
369 self.expiration = expiration
370 elif isinstance (expiration, StringTypes):
371 self.expiration = utcparse (expiration)
373 logger.error ("unexpected input type in Credential.set_expiration")
377 # get the lifetime of the credential (always in datetime format)
379 def get_expiration(self):
380 if not self.expiration:
382 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
383 return self.expiration
387 def get_lifetime(self):
388 return self.get_expiration()
393 # @param privs either a comma-separated list of privileges of a Rights object
395 def set_privileges(self, privs):
396 if isinstance(privs, str):
397 self.privileges = Rights(string = privs)
399 self.privileges = privs
403 # return the privileges as a Rights object
405 def get_privileges(self):
406 if not self.privileges:
408 return self.privileges
411 # determine whether the credential allows a particular operation to be
414 # @param op_name string specifying name of operation ("lookup", "update", etc)
416 def can_perform(self, op_name):
417 rights = self.get_privileges()
422 return rights.can_perform(op_name)
426 # Encode the attributes of the credential into an XML string
427 # This should be done immediately before signing the credential.
429 # In general, a signed credential obtained externally should
430 # not be changed else the signature is no longer valid. So, once
431 # you have loaded an existing signed credential, do not call encode() or sign() on it.
434 # Create the XML document
436 signed_cred = doc.createElement("signed-credential")
439 # Note that credential/policy.xsd are really the PG schemas
441 # Note that delegation of credentials between the 2 only really works
442 # cause those schemas are identical.
443 # Also note these PG schemas talk about PG tickets and CM policies.
444 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
445 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
446 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")
448 # PG says for those last 2:
449 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
450 # 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")
452 doc.appendChild(signed_cred)
454 # Fill in the <credential> bit
455 cred = doc.createElement("credential")
456 cred.setAttribute("xml:id", self.get_refid())
457 signed_cred.appendChild(cred)
458 append_sub(doc, cred, "type", "privilege")
459 append_sub(doc, cred, "serial", "8")
460 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
461 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
462 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
463 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
464 append_sub(doc, cred, "uuid", "")
465 if not self.expiration:
466 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
467 self.expiration = self.expiration.replace(microsecond=0)
468 append_sub(doc, cred, "expires", self.expiration.isoformat())
469 privileges = doc.createElement("privileges")
470 cred.appendChild(privileges)
473 rights = self.get_privileges()
474 for right in rights.rights:
475 priv = doc.createElement("privilege")
476 append_sub(doc, priv, "name", right.kind)
477 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
478 privileges.appendChild(priv)
480 # Add the parent credential if it exists
482 sdoc = parseString(self.parent.get_xml())
483 # If the root node is a signed-credential (it should be), then
484 # get all its attributes and attach those to our signed_cred
486 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
487 # and we need to include those again here or else their signature
488 # no longer matches on the credential.
489 # We expect three of these, but here we copy them all:
490 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
491 # and from PG (PL is equivalent, as shown above):
492 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
493 # 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")
496 # PL now also declares these, with different URLs, so
497 # the code notices those attributes already existed with
498 # different values, and complains.
499 # This happens regularly on delegation now that PG and
500 # PL both declare the namespace with different URLs.
501 # If the content ever differs this is a problem,
502 # but for now it works - different URLs (values in the attributes)
503 # but the same actual schema, so using the PG schema
504 # on delegated-to-PL credentials works fine.
506 # Note: you could also not copy attributes
507 # which already exist. It appears that both PG and PL
508 # will actually validate a slicecred with a parent
509 # signed using PG namespaces and a child signed with PL
510 # namespaces over the whole thing. But I don't know
511 # if that is a bug in xmlsec1, an accident since
512 # the contents of the schemas are the same,
513 # or something else, but it seems odd. And this works.
514 parentRoot = sdoc.documentElement
515 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
516 for attrIx in range(0, parentRoot.attributes.length):
517 attr = parentRoot.attributes.item(attrIx)
518 # returns the old attribute of same name that was
520 # Below throws InUse exception if we forgot to clone the attribute first
521 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
522 if oldAttr and oldAttr.value != attr.value:
523 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)
525 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
527 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
528 p = doc.createElement("parent")
529 p.appendChild(p_cred)
531 # done handling parent credential
533 # Create the <signatures> tag
534 signatures = doc.createElement("signatures")
535 signed_cred.appendChild(signatures)
537 # Add any parent signatures
539 for cur_cred in self.get_credential_list()[1:]:
540 sdoc = parseString(cur_cred.get_signature().get_xml())
541 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
542 signatures.appendChild(ele)
544 # Get the finished product
545 self.xml = doc.toxml()
548 def save_to_random_tmp_file(self):
549 fp, filename = mkstemp(suffix='cred', text=True)
550 fp = os.fdopen(fp, "w")
551 self.save_to_file(filename, save_parents=True, filep=fp)
554 def save_to_file(self, filename, save_parents=True, filep=None):
560 f = open(filename, "w")
564 def save_to_string(self, save_parents=True):
574 def set_refid(self, rid):
578 # Figure out what refids exist, and update this credential's id
579 # so that it doesn't clobber the others. Returns the refids of
582 def updateRefID(self):
584 self.set_refid('ref0')
589 next_cred = self.parent
591 refs.append(next_cred.get_refid())
593 next_cred = next_cred.parent
598 # Find a unique refid for this credential
599 rid = self.get_refid()
602 rid = "ref%d" % (val + 1)
607 # Return the set of parent credential ref ids
616 # Sign the XML file created by encode()
619 # In general, a signed credential obtained externally should
620 # not be changed else the signature is no longer valid. So, once
621 # you have loaded an existing signed credential, do not call encode() or sign() on it.
624 if not self.issuer_privkey or not self.issuer_gid:
626 doc = parseString(self.get_xml())
627 sigs = doc.getElementsByTagName("signatures")[0]
629 # Create the signature template to be signed
630 signature = Signature()
631 signature.set_refid(self.get_refid())
632 sdoc = parseString(signature.get_xml())
633 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
634 sigs.appendChild(sig_ele)
636 self.xml = doc.toxml()
639 # Split the issuer GID into multiple certificates if it's a chain
640 chain = GID(filename=self.issuer_gid)
643 gid_files.append(chain.save_to_random_tmp_file(False))
644 if chain.get_parent():
645 chain = chain.get_parent()
650 # Call out to xmlsec1 to sign it
651 ref = 'Sig_%s' % self.get_refid()
652 filename = self.save_to_random_tmp_file()
653 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
654 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
657 for gid_file in gid_files:
662 # This is no longer a legacy credential
671 # Retrieve the attributes of the credential from the XML.
672 # This is automatically called by the various get_* methods of
673 # this class and should not need to be called explicitly.
678 doc = parseString(self.xml)
680 signed_cred = doc.getElementsByTagName("signed-credential")
682 # Is this a signed-cred or just a cred?
683 if len(signed_cred) > 0:
684 creds = signed_cred[0].getElementsByTagName("credential")
685 signatures = signed_cred[0].getElementsByTagName("signatures")
686 if len(signatures) > 0:
687 sigs = signatures[0].getElementsByTagName("Signature")
689 creds = doc.getElementsByTagName("credential")
691 if creds is None or len(creds) == 0:
692 # malformed cred file
693 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
695 # Just take the first cred if there are more than one
698 self.set_refid(cred.getAttribute("xml:id"))
699 self.set_expiration(utcparse(getTextNode(cred, "expires")))
700 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
701 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
705 privs = cred.getElementsByTagName("privileges")[0]
707 for priv in privs.getElementsByTagName("privilege"):
708 kind = getTextNode(priv, "name")
709 deleg = str2bool(getTextNode(priv, "can_delegate"))
711 # Convert * into the default privileges for the credential's type
712 # Each inherits the delegatability from the * above
713 _ , type = urn_to_hrn(self.gidObject.get_urn())
714 rl = determine_rights(type, self.gidObject.get_urn())
719 rlist.add(Right(kind.strip(), deleg))
720 self.set_privileges(rlist)
724 parent = cred.getElementsByTagName("parent")
726 parent_doc = parent[0].getElementsByTagName("credential")[0]
727 parent_xml = parent_doc.toxml()
728 self.parent = Credential(string=parent_xml)
731 # Assign the signatures to the credentials
733 Sig = Signature(string=sig.toxml())
735 for cur_cred in self.get_credential_list():
736 if cur_cred.get_refid() == Sig.get_refid():
737 cur_cred.set_signature(Sig)
742 # trusted_certs: A list of trusted GID filenames (not GID objects!)
743 # Chaining is not supported within the GIDs by xmlsec1.
745 # trusted_certs_required: Should usually be true. Set False means an
746 # empty list of trusted_certs would still let this method pass.
747 # It just skips xmlsec1 verification et al. Only used by some utils
750 # . All of the signatures are valid and that the issuers trace back
751 # to trusted roots (performed by xmlsec1)
752 # . The XML matches the credential schema
753 # . That the issuer of the credential is the authority in the target's urn
754 # . In the case of a delegated credential, this must be true of the root
755 # . That all of the gids presented in the credential are valid
756 # . Including verifying GID chains, and includ the issuer
757 # . The credential is not expired
759 # -- For Delegates (credentials with parents)
760 # . The privileges must be a subset of the parent credentials
761 # . The privileges must have "can_delegate" set for each delegated privilege
762 # . The target gid must be the same between child and parents
763 # . The expiry time on the child must be no later than the parent
764 # . The signer of the child must be the owner of the parent
766 # -- Verify does *NOT*
767 # . ensure that an xmlrpc client's gid matches a credential gid, that
768 # must be done elsewhere
770 # @param trusted_certs: The certificates of trusted CA certificates
771 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
775 # validate against RelaxNG schema
776 if HAVELXML and not self.legacy:
777 if schema and os.path.exists(schema):
778 tree = etree.parse(StringIO(self.xml))
779 schema_doc = etree.parse(schema)
780 xmlschema = etree.XMLSchema(schema_doc)
781 if not xmlschema.validate(tree):
782 error = xmlschema.error_log.last_error
783 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
784 raise CredentialNotVerifiable(message)
786 if trusted_certs_required and trusted_certs is None:
789 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
790 trusted_cert_objects = []
791 ok_trusted_certs = []
792 # If caller explicitly passed in None that means skip cert chain validation.
793 # Strange and not typical
794 if trusted_certs is not None:
795 for f in trusted_certs:
797 # Failures here include unreadable files
799 trusted_cert_objects.append(GID(filename=f))
800 ok_trusted_certs.append(f)
801 except Exception, exc:
802 logger.error("Failed to load trusted cert from %s: %r", f, exc)
803 trusted_certs = ok_trusted_certs
805 # Use legacy verification if this is a legacy credential
807 self.legacy.verify_chain(trusted_cert_objects)
808 if self.legacy.client_gid:
809 self.legacy.client_gid.verify_chain(trusted_cert_objects)
810 if self.legacy.object_gid:
811 self.legacy.object_gid.verify_chain(trusted_cert_objects)
814 # make sure it is not expired
815 if self.get_expiration() < datetime.datetime.utcnow():
816 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
818 # Verify the signatures
819 filename = self.save_to_random_tmp_file()
820 if trusted_certs is not None:
821 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
823 # If caller explicitly passed in None that means skip cert chain validation.
824 # - Strange and not typical
825 if trusted_certs is not None:
826 # Verify the gids of this cred and of its parents
827 for cur_cred in self.get_credential_list():
828 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
829 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
832 refs.append("Sig_%s" % self.get_refid())
834 parentRefs = self.updateRefID()
835 for ref in parentRefs:
836 refs.append("Sig_%s" % ref)
839 # If caller explicitly passed in None that means skip xmlsec1 validation.
840 # Strange and not typical
841 if trusted_certs is None:
844 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
845 # (self.xmlsec_path, ref, cert_args, filename)
846 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
847 % (self.xmlsec_path, ref, cert_args, filename)).read()
848 if not verified.strip().startswith("OK"):
849 # xmlsec errors have a msg= which is the interesting bit.
850 mstart = verified.find("msg=")
852 if mstart > -1 and len(verified) > 4:
854 mend = verified.find('\\', mstart)
855 msg = verified[mstart:mend]
856 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
859 # Verify the parents (delegation)
861 self.verify_parent(self.parent)
863 # Make sure the issuer is the target's authority, and is
865 self.verify_issuer(trusted_cert_objects)
869 # Creates a list of the credential and its parents, with the root
870 # (original delegated credential) as the last item in the list
871 def get_credential_list(self):
875 list.append(cur_cred)
877 cur_cred = cur_cred.parent
883 # Make sure the credential's target gid (a) was signed by or (b)
884 # is the same as the entity that signed the original credential,
885 # or (c) is an authority over the target's namespace.
886 # Also ensure that the credential issuer / signer itself has a valid
887 # GID signature chain (signed by an authority with namespace rights).
888 def verify_issuer(self, trusted_gids):
889 root_cred = self.get_credential_list()[-1]
890 root_target_gid = root_cred.get_gid_object()
891 root_cred_signer = root_cred.get_signature().get_issuer_gid()
894 # Allow non authority to sign target and cred about target.
896 # Why do we need to allow non authorities to sign?
897 # If in the target gid validation step we correctly
898 # checked that the target is only signed by an authority,
899 # then this is just a special case of case 3.
900 # This short-circuit is the common case currently -
901 # and cause GID validation doesn't check 'authority',
902 # this allows users to generate valid slice credentials.
903 if root_target_gid.is_signed_by_cert(root_cred_signer):
904 # cred signer matches target signer, return success
908 # Allow someone to sign credential about themeselves. Used?
909 # If not, remove this.
910 #root_target_gid_str = root_target_gid.save_to_string()
911 #root_cred_signer_str = root_cred_signer.save_to_string()
912 #if root_target_gid_str == root_cred_signer_str:
913 # # cred signer is target, return success
918 # root_cred_signer is not the target_gid
919 # So this is a different gid that we have not verified.
920 # xmlsec1 verified the cert chain on this already, but
921 # it hasn't verified that the gid meets the HRN namespace
923 # Below we'll ensure that it is an authority.
924 # But we haven't verified that it is _signed by_ an authority
925 # We also don't know if xmlsec1 requires that cert signers
927 root_cred_signer.verify_chain(trusted_gids)
929 # See if the signer is an authority over the domain of the target.
930 # There are multiple types of authority - accept them all here
931 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
932 root_cred_signer_type = root_cred_signer.get_type()
933 if (root_cred_signer_type.find('authority') == 0):
934 #logger.debug('Cred signer is an authority')
935 # signer is an authority, see if target is in authority's domain
936 signerhrn = root_cred_signer.get_hrn()
937 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
940 # We've required that the credential be signed by an authority
941 # for that domain. Reasonable and probably correct.
942 # A looser model would also allow the signer to be an authority
943 # in my control framework - eg My CA or CH. Even if it is not
944 # the CH that issued these, eg, user credentials.
946 # Give up, credential does not pass issuer verification
948 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()))
952 # -- For Delegates (credentials with parents) verify that:
953 # . The privileges must be a subset of the parent credentials
954 # . The privileges must have "can_delegate" set for each delegated privilege
955 # . The target gid must be the same between child and parents
956 # . The expiry time on the child must be no later than the parent
957 # . The signer of the child must be the owner of the parent
958 def verify_parent(self, parent_cred):
959 # make sure the rights given to the child are a subset of the
960 # parents rights (and check delegate bits)
961 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
962 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
963 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
964 self.get_privileges().save_to_string())
966 # make sure my target gid is the same as the parent's
967 if not parent_cred.get_gid_object().save_to_string() == \
968 self.get_gid_object().save_to_string():
969 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
971 # make sure my expiry time is <= my parent's
972 if not parent_cred.get_expiration() >= self.get_expiration():
973 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
975 # make sure my signer is the parent's caller
976 if not parent_cred.get_gid_caller().save_to_string(False) == \
977 self.get_signature().get_issuer_gid().save_to_string(False):
978 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
981 if parent_cred.parent:
982 parent_cred.verify_parent(parent_cred.parent)
985 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
987 Return a delegated copy of this credential, delegated to the
988 specified gid's user.
990 # get the gid of the object we are delegating
991 object_gid = self.get_gid_object()
992 object_hrn = object_gid.get_hrn()
994 # the hrn of the user who will be delegated to
995 delegee_gid = GID(filename=delegee_gidfile)
996 delegee_hrn = delegee_gid.get_hrn()
998 #user_key = Keypair(filename=keyfile)
999 #user_hrn = self.get_gid_caller().get_hrn()
1000 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1001 dcred = Credential(subject=subject_string)
1002 dcred.set_gid_caller(delegee_gid)
1003 dcred.set_gid_object(object_gid)
1004 dcred.set_parent(self)
1005 dcred.set_expiration(self.get_expiration())
1006 dcred.set_privileges(self.get_privileges())
1007 dcred.get_privileges().delegate_all_privileges(True)
1008 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1009 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1016 def get_filename(self):
1017 return getattr(self,'filename',None)
1020 # Dump the contents of a credential to stdout in human-readable format
1022 # @param dump_parents If true, also dump the parent certificates
1023 def dump (self, *args, **kwargs):
1024 print self.dump_string(*args, **kwargs)
1027 def dump_string(self, dump_parents=False):
1029 result += "CREDENTIAL %s\n" % self.get_subject()
1030 filename=self.get_filename()
1031 if filename: result += "Filename %s\n"%filename
1032 result += " privs: %s\n" % self.get_privileges().save_to_string()
1033 gidCaller = self.get_gid_caller()
1035 result += " gidCaller:\n"
1036 result += gidCaller.dump_string(8, dump_parents)
1038 if self.get_signature():
1040 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1042 gidObject = self.get_gid_object()
1044 result += " gidObject:\n"
1045 result += gidObject.dump_string(8, dump_parents)
1047 if self.parent and dump_parents:
1048 result += "\nPARENT"
1049 result += self.parent.dump_string(True)