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):
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())
200 # A credential provides a caller gid with privileges to an object gid.
201 # A signed credential is signed by the object's authority.
203 # Credentials are encoded in one of two ways. The legacy style places
204 # it in the subjectAltName of an X509 certificate. The new credentials
205 # are placed in signed XML.
208 # In general, a signed credential obtained externally should
209 # not be changed else the signature is no longer valid. So, once
210 # you have loaded an existing signed credential, do not call encode() or sign() on it.
212 def filter_creds_by_caller(creds, caller_hrn):
214 Returns a list of creds who's gid caller matches the
217 if not isinstance(creds, list): creds = [creds]
221 tmp_cred = Credential(string=cred)
222 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
223 caller_creds.append(cred)
227 class Credential(object):
230 # Create a Credential object
232 # @param create If true, create a blank x509 certificate
233 # @param subject If subject!=None, create an x509 cert with the subject name
234 # @param string If string!=None, load the credential from the string
235 # @param filename If filename!=None, load the credential from the file
236 # FIXME: create and subject are ignored!
237 def __init__(self, create=False, subject=None, string=None, filename=None):
238 self.gidCaller = None
239 self.gidObject = None
240 self.expiration = None
241 self.privileges = None
242 self.issuer_privkey = None
243 self.issuer_gid = None
244 self.issuer_pubkey = None
246 self.signature = None
251 # Check if this is a legacy credential, translate it if so
252 if string or filename:
256 str = file(filename).read()
258 if str.strip().startswith("-----"):
259 self.legacy = CredentialLegacy(False,string=str)
260 self.translate_legacy(str)
265 # Find an xmlsec1 path
266 self.xmlsec_path = ''
267 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
269 if os.path.isfile(path + '/' + 'xmlsec1'):
270 self.xmlsec_path = path + '/' + 'xmlsec1'
273 def get_subject(self):
274 if not self.gidObject:
276 return self.gidObject.get_printable_subject()
278 def get_summary_tostring(self):
279 if not self.gidObject:
281 obj = self.gidObject.get_printable_subject()
282 caller = self.gidCaller.get_printable_subject()
283 exp = self.get_expiration()
284 # Summarize the rights too? The issuer?
285 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
287 def get_signature(self):
288 if not self.signature:
290 return self.signature
292 def set_signature(self, sig):
297 # Translate a legacy credential into a new one
299 # @param String of the legacy credential
301 def translate_legacy(self, str):
302 legacy = CredentialLegacy(False,string=str)
303 self.gidCaller = legacy.get_gid_caller()
304 self.gidObject = legacy.get_gid_object()
305 lifetime = legacy.get_lifetime()
307 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
309 self.set_expiration(int(lifetime))
310 self.lifeTime = legacy.get_lifetime()
311 self.set_privileges(legacy.get_privileges())
312 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
315 # Need the issuer's private key and name
316 # @param key Keypair object containing the private key of the issuer
317 # @param gid GID of the issuing authority
319 def set_issuer_keys(self, privkey, gid):
320 self.issuer_privkey = privkey
321 self.issuer_gid = gid
325 # Set this credential's parent
326 def set_parent(self, cred):
331 # set the GID of the caller
333 # @param gid GID object of the caller
335 def set_gid_caller(self, gid):
337 # gid origin caller is the caller's gid by default
338 self.gidOriginCaller = gid
341 # get the GID of the object
343 def get_gid_caller(self):
344 if not self.gidCaller:
346 return self.gidCaller
349 # set the GID of the object
351 # @param gid GID object of the object
353 def set_gid_object(self, gid):
357 # get the GID of the object
359 def get_gid_object(self):
360 if not self.gidObject:
362 return self.gidObject
367 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
369 def set_expiration(self, expiration):
370 if isinstance(expiration, (int, float)):
371 self.expiration = datetime.datetime.fromtimestamp(expiration)
372 elif isinstance (expiration, datetime.datetime):
373 self.expiration = expiration
374 elif isinstance (expiration, StringTypes):
375 self.expiration = utcparse (expiration)
377 logger.error ("unexpected input type in Credential.set_expiration")
381 # get the lifetime of the credential (always in datetime format)
383 def get_expiration(self):
384 if not self.expiration:
386 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
387 return self.expiration
391 def get_lifetime(self):
392 return self.get_expiration()
397 # @param privs either a comma-separated list of privileges of a Rights object
399 def set_privileges(self, privs):
400 if isinstance(privs, str):
401 self.privileges = Rights(string = privs)
403 self.privileges = privs
407 # return the privileges as a Rights object
409 def get_privileges(self):
410 if not self.privileges:
412 return self.privileges
415 # determine whether the credential allows a particular operation to be
418 # @param op_name string specifying name of operation ("lookup", "update", etc)
420 def can_perform(self, op_name):
421 rights = self.get_privileges()
426 return rights.can_perform(op_name)
430 # Encode the attributes of the credential into an XML string
431 # This should be done immediately before signing the credential.
433 # In general, a signed credential obtained externally should
434 # not be changed else the signature is no longer valid. So, once
435 # you have loaded an existing signed credential, do not call encode() or sign() on it.
438 # Create the XML document
440 signed_cred = doc.createElement("signed-credential")
443 # Note that credential/policy.xsd are really the PG schemas
445 # Note that delegation of credentials between the 2 only really works
446 # cause those schemas are identical.
447 # Also note these PG schemas talk about PG tickets and CM policies.
448 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
449 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
450 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")
452 # PG says for those last 2:
453 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
454 # 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")
456 doc.appendChild(signed_cred)
458 # Fill in the <credential> bit
459 cred = doc.createElement("credential")
460 cred.setAttribute("xml:id", self.get_refid())
461 signed_cred.appendChild(cred)
462 append_sub(doc, cred, "type", "privilege")
463 append_sub(doc, cred, "serial", "8")
464 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
465 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
466 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
467 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
468 append_sub(doc, cred, "uuid", "")
469 if not self.expiration:
470 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
471 self.expiration = self.expiration.replace(microsecond=0)
472 append_sub(doc, cred, "expires", self.expiration.isoformat())
473 privileges = doc.createElement("privileges")
474 cred.appendChild(privileges)
477 rights = self.get_privileges()
478 for right in rights.rights:
479 priv = doc.createElement("privilege")
480 append_sub(doc, priv, "name", right.kind)
481 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
482 privileges.appendChild(priv)
484 # Add the parent credential if it exists
486 sdoc = parseString(self.parent.get_xml())
487 # If the root node is a signed-credential (it should be), then
488 # get all its attributes and attach those to our signed_cred
490 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
491 # and we need to include those again here or else their signature
492 # no longer matches on the credential.
493 # We expect three of these, but here we copy them all:
494 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
495 # and from PG (PL is equivalent, as shown above):
496 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
497 # 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")
500 # PL now also declares these, with different URLs, so
501 # the code notices those attributes already existed with
502 # different values, and complains.
503 # This happens regularly on delegation now that PG and
504 # PL both declare the namespace with different URLs.
505 # If the content ever differs this is a problem,
506 # but for now it works - different URLs (values in the attributes)
507 # but the same actual schema, so using the PG schema
508 # on delegated-to-PL credentials works fine.
510 # Note: you could also not copy attributes
511 # which already exist. It appears that both PG and PL
512 # will actually validate a slicecred with a parent
513 # signed using PG namespaces and a child signed with PL
514 # namespaces over the whole thing. But I don't know
515 # if that is a bug in xmlsec1, an accident since
516 # the contents of the schemas are the same,
517 # or something else, but it seems odd. And this works.
518 parentRoot = sdoc.documentElement
519 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
520 for attrIx in range(0, parentRoot.attributes.length):
521 attr = parentRoot.attributes.item(attrIx)
522 # returns the old attribute of same name that was
524 # Below throws InUse exception if we forgot to clone the attribute first
525 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
526 if oldAttr and oldAttr.value != attr.value:
527 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)
529 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
531 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
532 p = doc.createElement("parent")
533 p.appendChild(p_cred)
535 # done handling parent credential
537 # Create the <signatures> tag
538 signatures = doc.createElement("signatures")
539 signed_cred.appendChild(signatures)
541 # Add any parent signatures
543 for cur_cred in self.get_credential_list()[1:]:
544 sdoc = parseString(cur_cred.get_signature().get_xml())
545 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
546 signatures.appendChild(ele)
548 # Get the finished product
549 self.xml = doc.toxml()
552 def save_to_random_tmp_file(self):
553 fp, filename = mkstemp(suffix='cred', text=True)
554 fp = os.fdopen(fp, "w")
555 self.save_to_file(filename, save_parents=True, filep=fp)
558 def save_to_file(self, filename, save_parents=True, filep=None):
564 f = open(filename, "w")
568 def save_to_string(self, save_parents=True):
578 def set_refid(self, rid):
582 # Figure out what refids exist, and update this credential's id
583 # so that it doesn't clobber the others. Returns the refids of
586 def updateRefID(self):
588 self.set_refid('ref0')
593 next_cred = self.parent
595 refs.append(next_cred.get_refid())
597 next_cred = next_cred.parent
602 # Find a unique refid for this credential
603 rid = self.get_refid()
606 rid = "ref%d" % (val + 1)
611 # Return the set of parent credential ref ids
620 # Sign the XML file created by encode()
623 # In general, a signed credential obtained externally should
624 # not be changed else the signature is no longer valid. So, once
625 # you have loaded an existing signed credential, do not call encode() or sign() on it.
628 if not self.issuer_privkey or not self.issuer_gid:
630 doc = parseString(self.get_xml())
631 sigs = doc.getElementsByTagName("signatures")[0]
633 # Create the signature template to be signed
634 signature = Signature()
635 signature.set_refid(self.get_refid())
636 sdoc = parseString(signature.get_xml())
637 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
638 sigs.appendChild(sig_ele)
640 self.xml = doc.toxml()
643 # Split the issuer GID into multiple certificates if it's a chain
644 chain = GID(filename=self.issuer_gid)
647 gid_files.append(chain.save_to_random_tmp_file(False))
648 if chain.get_parent():
649 chain = chain.get_parent()
654 # Call out to xmlsec1 to sign it
655 ref = 'Sig_%s' % self.get_refid()
656 filename = self.save_to_random_tmp_file()
657 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
658 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
661 for gid_file in gid_files:
666 # This is no longer a legacy credential
675 # Retrieve the attributes of the credential from the XML.
676 # This is automatically called by the various get_* methods of
677 # this class and should not need to be called explicitly.
682 doc = parseString(self.xml)
684 signed_cred = doc.getElementsByTagName("signed-credential")
686 # Is this a signed-cred or just a cred?
687 if len(signed_cred) > 0:
688 creds = signed_cred[0].getElementsByTagName("credential")
689 signatures = signed_cred[0].getElementsByTagName("signatures")
690 if len(signatures) > 0:
691 sigs = signatures[0].getElementsByTagName("Signature")
693 creds = doc.getElementsByTagName("credential")
695 if creds is None or len(creds) == 0:
696 # malformed cred file
697 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
699 # Just take the first cred if there are more than one
702 self.set_refid(cred.getAttribute("xml:id"))
703 self.set_expiration(utcparse(getTextNode(cred, "expires")))
704 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
705 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
709 privs = cred.getElementsByTagName("privileges")[0]
711 for priv in privs.getElementsByTagName("privilege"):
712 kind = getTextNode(priv, "name")
713 deleg = str2bool(getTextNode(priv, "can_delegate"))
715 # Convert * into the default privileges for the credential's type
716 # Each inherits the delegatability from the * above
717 _ , type = urn_to_hrn(self.gidObject.get_urn())
718 rl = determine_rights(type, self.gidObject.get_urn())
723 rlist.add(Right(kind.strip(), deleg))
724 self.set_privileges(rlist)
728 parent = cred.getElementsByTagName("parent")
730 parent_doc = parent[0].getElementsByTagName("credential")[0]
731 parent_xml = parent_doc.toxml()
732 self.parent = Credential(string=parent_xml)
735 # Assign the signatures to the credentials
737 Sig = Signature(string=sig.toxml())
739 for cur_cred in self.get_credential_list():
740 if cur_cred.get_refid() == Sig.get_refid():
741 cur_cred.set_signature(Sig)
746 # trusted_certs: A list of trusted GID filenames (not GID objects!)
747 # Chaining is not supported within the GIDs by xmlsec1.
749 # trusted_certs_required: Should usually be true. Set False means an
750 # empty list of trusted_certs would still let this method pass.
751 # It just skips xmlsec1 verification et al. Only used by some utils
754 # . All of the signatures are valid and that the issuers trace back
755 # to trusted roots (performed by xmlsec1)
756 # . The XML matches the credential schema
757 # . That the issuer of the credential is the authority in the target's urn
758 # . In the case of a delegated credential, this must be true of the root
759 # . That all of the gids presented in the credential are valid
760 # . Including verifying GID chains, and includ the issuer
761 # . The credential is not expired
763 # -- For Delegates (credentials with parents)
764 # . The privileges must be a subset of the parent credentials
765 # . The privileges must have "can_delegate" set for each delegated privilege
766 # . The target gid must be the same between child and parents
767 # . The expiry time on the child must be no later than the parent
768 # . The signer of the child must be the owner of the parent
770 # -- Verify does *NOT*
771 # . ensure that an xmlrpc client's gid matches a credential gid, that
772 # must be done elsewhere
774 # @param trusted_certs: The certificates of trusted CA certificates
775 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
779 # validate against RelaxNG schema
780 if HAVELXML and not self.legacy:
781 if schema and os.path.exists(schema):
782 tree = etree.parse(StringIO(self.xml))
783 schema_doc = etree.parse(schema)
784 xmlschema = etree.XMLSchema(schema_doc)
785 if not xmlschema.validate(tree):
786 error = xmlschema.error_log.last_error
787 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
788 raise CredentialNotVerifiable(message)
790 if trusted_certs_required and trusted_certs is None:
793 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
794 trusted_cert_objects = []
795 ok_trusted_certs = []
796 # If caller explicitly passed in None that means skip cert chain validation.
797 # Strange and not typical
798 if trusted_certs is not None:
799 for f in trusted_certs:
801 # Failures here include unreadable files
803 trusted_cert_objects.append(GID(filename=f))
804 ok_trusted_certs.append(f)
805 except Exception, exc:
806 logger.error("Failed to load trusted cert from %s: %r", f, exc)
807 trusted_certs = ok_trusted_certs
809 # Use legacy verification if this is a legacy credential
811 self.legacy.verify_chain(trusted_cert_objects)
812 if self.legacy.client_gid:
813 self.legacy.client_gid.verify_chain(trusted_cert_objects)
814 if self.legacy.object_gid:
815 self.legacy.object_gid.verify_chain(trusted_cert_objects)
818 # make sure it is not expired
819 if self.get_expiration() < datetime.datetime.utcnow():
820 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
822 # Verify the signatures
823 filename = self.save_to_random_tmp_file()
824 if trusted_certs is not None:
825 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
827 # If caller explicitly passed in None that means skip cert chain validation.
828 # - Strange and not typical
829 if trusted_certs is not None:
830 # Verify the gids of this cred and of its parents
831 for cur_cred in self.get_credential_list():
832 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
833 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
836 refs.append("Sig_%s" % self.get_refid())
838 parentRefs = self.updateRefID()
839 for ref in parentRefs:
840 refs.append("Sig_%s" % ref)
843 # If caller explicitly passed in None that means skip xmlsec1 validation.
844 # Strange and not typical
845 if trusted_certs is None:
848 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
849 # (self.xmlsec_path, ref, cert_args, filename)
850 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
851 % (self.xmlsec_path, ref, cert_args, filename)).read()
852 if not verified.strip().startswith("OK"):
853 # xmlsec errors have a msg= which is the interesting bit.
854 mstart = verified.find("msg=")
856 if mstart > -1 and len(verified) > 4:
858 mend = verified.find('\\', mstart)
859 msg = verified[mstart:mend]
860 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
863 # Verify the parents (delegation)
865 self.verify_parent(self.parent)
867 # Make sure the issuer is the target's authority, and is
869 self.verify_issuer(trusted_cert_objects)
873 # Creates a list of the credential and its parents, with the root
874 # (original delegated credential) as the last item in the list
875 def get_credential_list(self):
879 list.append(cur_cred)
881 cur_cred = cur_cred.parent
887 # Make sure the credential's target gid (a) was signed by or (b)
888 # is the same as the entity that signed the original credential,
889 # or (c) is an authority over the target's namespace.
890 # Also ensure that the credential issuer / signer itself has a valid
891 # GID signature chain (signed by an authority with namespace rights).
892 def verify_issuer(self, trusted_gids):
893 root_cred = self.get_credential_list()[-1]
894 root_target_gid = root_cred.get_gid_object()
895 root_cred_signer = root_cred.get_signature().get_issuer_gid()
898 # Allow non authority to sign target and cred about target.
900 # Why do we need to allow non authorities to sign?
901 # If in the target gid validation step we correctly
902 # checked that the target is only signed by an authority,
903 # then this is just a special case of case 3.
904 # This short-circuit is the common case currently -
905 # and cause GID validation doesn't check 'authority',
906 # this allows users to generate valid slice credentials.
907 if root_target_gid.is_signed_by_cert(root_cred_signer):
908 # cred signer matches target signer, return success
912 # Allow someone to sign credential about themeselves. Used?
913 # If not, remove this.
914 #root_target_gid_str = root_target_gid.save_to_string()
915 #root_cred_signer_str = root_cred_signer.save_to_string()
916 #if root_target_gid_str == root_cred_signer_str:
917 # # cred signer is target, return success
922 # root_cred_signer is not the target_gid
923 # So this is a different gid that we have not verified.
924 # xmlsec1 verified the cert chain on this already, but
925 # it hasn't verified that the gid meets the HRN namespace
927 # Below we'll ensure that it is an authority.
928 # But we haven't verified that it is _signed by_ an authority
929 # We also don't know if xmlsec1 requires that cert signers
931 root_cred_signer.verify_chain(trusted_gids)
933 # See if the signer is an authority over the domain of the target.
934 # There are multiple types of authority - accept them all here
935 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
936 root_cred_signer_type = root_cred_signer.get_type()
937 if (root_cred_signer_type.find('authority') == 0):
938 #logger.debug('Cred signer is an authority')
939 # signer is an authority, see if target is in authority's domain
940 signerhrn = root_cred_signer.get_hrn()
941 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
944 # We've required that the credential be signed by an authority
945 # for that domain. Reasonable and probably correct.
946 # A looser model would also allow the signer to be an authority
947 # in my control framework - eg My CA or CH. Even if it is not
948 # the CH that issued these, eg, user credentials.
950 # Give up, credential does not pass issuer verification
952 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()))
956 # -- For Delegates (credentials with parents) verify that:
957 # . The privileges must be a subset of the parent credentials
958 # . The privileges must have "can_delegate" set for each delegated privilege
959 # . The target gid must be the same between child and parents
960 # . The expiry time on the child must be no later than the parent
961 # . The signer of the child must be the owner of the parent
962 def verify_parent(self, parent_cred):
963 # make sure the rights given to the child are a subset of the
964 # parents rights (and check delegate bits)
965 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
966 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
967 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
968 self.get_privileges().save_to_string())
970 # make sure my target gid is the same as the parent's
971 if not parent_cred.get_gid_object().save_to_string() == \
972 self.get_gid_object().save_to_string():
973 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
975 # make sure my expiry time is <= my parent's
976 if not parent_cred.get_expiration() >= self.get_expiration():
977 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
979 # make sure my signer is the parent's caller
980 if not parent_cred.get_gid_caller().save_to_string(False) == \
981 self.get_signature().get_issuer_gid().save_to_string(False):
982 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
985 if parent_cred.parent:
986 parent_cred.verify_parent(parent_cred.parent)
989 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
991 Return a delegated copy of this credential, delegated to the
992 specified gid's user.
994 # get the gid of the object we are delegating
995 object_gid = self.get_gid_object()
996 object_hrn = object_gid.get_hrn()
998 # the hrn of the user who will be delegated to
999 delegee_gid = GID(filename=delegee_gidfile)
1000 delegee_hrn = delegee_gid.get_hrn()
1002 #user_key = Keypair(filename=keyfile)
1003 #user_hrn = self.get_gid_caller().get_hrn()
1004 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1005 dcred = Credential(subject=subject_string)
1006 dcred.set_gid_caller(delegee_gid)
1007 dcred.set_gid_object(object_gid)
1008 dcred.set_parent(self)
1009 dcred.set_expiration(self.get_expiration())
1010 dcred.set_privileges(self.get_privileges())
1011 dcred.get_privileges().delegate_all_privileges(True)
1012 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1013 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1020 def get_filename(self):
1021 return getattr(self,'filename',None)
1024 # Dump the contents of a credential to stdout in human-readable format
1026 # @param dump_parents If true, also dump the parent certificates
1027 def dump (self, *args, **kwargs):
1028 print self.dump_string(*args, **kwargs)
1031 def dump_string(self, dump_parents=False):
1033 result += "CREDENTIAL %s\n" % self.get_subject()
1034 filename=self.get_filename()
1035 if filename: result += "Filename %s\n"%filename
1036 result += " privs: %s\n" % self.get_privileges().save_to_string()
1037 gidCaller = self.get_gid_caller()
1039 result += " gidCaller:\n"
1040 result += gidCaller.dump_string(8, dump_parents)
1042 if self.get_signature():
1044 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1046 gidObject = self.get_gid_object()
1048 result += " gidObject:\n"
1049 result += gidObject.dump_string(8, dump_parents)
1051 if self.parent and dump_parents:
1052 result += "\nPARENT"
1053 result += self.parent.dump_string(True)