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.credential_legacy import CredentialLegacy
49 from sfa.trust.rights import Right, Rights, determine_rights
50 from sfa.trust.gid import GID
51 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
54 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
58 # . make privs match between PG and PL
59 # . Need to add support for other types of credentials, e.g. tickets
60 # . add namespaces to signed-credential element?
62 signature_template = \
64 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
66 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
67 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
70 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
72 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
73 <DigestValue></DigestValue>
88 # PG formats the template (whitespace) slightly differently.
89 # Note that they don't include the xmlns in the template, but add it later.
90 # Otherwise the two are equivalent.
91 #signature_template_as_in_pg = \
93 #<Signature xml:id="Sig_%s" >
95 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
96 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
97 # <Reference URI="#%s">
99 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
101 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
102 # <DigestValue></DigestValue>
109 # <X509IssuerSerial/>
118 # Convert a string into a bool
119 # used to convert an xsd:boolean to a Python boolean
121 if str.lower() in ['true','1']:
127 # Utility function to get the text of an XML element
129 def getTextNode(element, subele):
130 sub = element.getElementsByTagName(subele)[0]
131 if len(sub.childNodes) > 0:
132 return sub.childNodes[0].nodeValue
137 # Utility function to set the text of an XML element
138 # It creates the element, adds the text to it,
139 # and then appends it to the parent.
141 def append_sub(doc, parent, element, text):
142 ele = doc.createElement(element)
143 ele.appendChild(doc.createTextNode(text))
144 parent.appendChild(ele)
147 # Signature contains information about an xmlsec1 signature
148 # for a signed-credential
151 class Signature(object):
153 def __init__(self, string=None):
155 self.issuer_gid = None
172 def set_refid(self, id):
175 def get_issuer_gid(self):
180 def set_issuer_gid(self, gid):
185 doc = parseString(self.xml)
187 logger.log_exc ("Failed to parse credential, %s"%self.xml)
189 sig = doc.getElementsByTagName("Signature")[0]
190 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
191 keyinfo = sig.getElementsByTagName("X509Data")[0]
192 szgid = getTextNode(keyinfo, "X509Certificate")
193 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
194 self.set_issuer_gid(GID(string=szgid))
197 self.xml = signature_template % (self.get_refid(), self.get_refid())
201 # A credential provides a caller gid with privileges to an object gid.
202 # A signed credential is signed by the object's authority.
204 # Credentials are encoded in one of two ways. The legacy style places
205 # it in the subjectAltName of an X509 certificate. The new credentials
206 # are placed in signed XML.
209 # In general, a signed credential obtained externally should
210 # not be changed else the signature is no longer valid. So, once
211 # you have loaded an existing signed credential, do not call encode() or sign() on it.
213 def filter_creds_by_caller(creds, caller_hrn_list):
215 Returns a list of creds who's gid caller matches the
218 if not isinstance(creds, list): creds = [creds]
219 if not isinstance(caller_hrn_list, list):
220 caller_hrn_list = [caller_hrn_list]
224 tmp_cred = Credential(string=cred)
225 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
226 caller_creds.append(cred)
230 class Credential(object):
233 # Create a Credential object
235 # @param create If true, create a blank x509 certificate
236 # @param subject If subject!=None, create an x509 cert with the subject name
237 # @param string If string!=None, load the credential from the string
238 # @param filename If filename!=None, load the credential from the file
239 # FIXME: create and subject are ignored!
240 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
241 self.gidCaller = None
242 self.gidObject = None
243 self.expiration = None
244 self.privileges = None
245 self.issuer_privkey = None
246 self.issuer_gid = None
247 self.issuer_pubkey = None
249 self.signature = None
257 if isinstance(cred, StringTypes):
259 self.type = 'geni_sfa'
261 elif isinstance(cred, dict):
262 string = cred['geni_value']
263 self.type = cred['geni_type']
264 self.version = cred['geni_version']
267 # Check if this is a legacy credential, translate it if so
268 if string or filename:
272 str = file(filename).read()
274 if str.strip().startswith("-----"):
275 self.legacy = CredentialLegacy(False,string=str)
276 self.translate_legacy(str)
281 # Find an xmlsec1 path
282 self.xmlsec_path = ''
283 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
285 if os.path.isfile(path + '/' + 'xmlsec1'):
286 self.xmlsec_path = path + '/' + 'xmlsec1'
289 def get_subject(self):
291 if not self.gidObject:
294 subject = self.gidObject.get_printable_subject()
297 # sounds like this should be __repr__ instead ??
298 def get_summary_tostring(self):
299 if not self.gidObject:
301 obj = self.gidObject.get_printable_subject()
302 caller = self.gidCaller.get_printable_subject()
303 exp = self.get_expiration()
304 # Summarize the rights too? The issuer?
305 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
307 def get_signature(self):
308 if not self.signature:
310 return self.signature
312 def set_signature(self, sig):
317 # Translate a legacy credential into a new one
319 # @param String of the legacy credential
321 def translate_legacy(self, str):
322 legacy = CredentialLegacy(False,string=str)
323 self.gidCaller = legacy.get_gid_caller()
324 self.gidObject = legacy.get_gid_object()
325 lifetime = legacy.get_lifetime()
327 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
329 self.set_expiration(int(lifetime))
330 self.lifeTime = legacy.get_lifetime()
331 self.set_privileges(legacy.get_privileges())
332 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
335 # Need the issuer's private key and name
336 # @param key Keypair object containing the private key of the issuer
337 # @param gid GID of the issuing authority
339 def set_issuer_keys(self, privkey, gid):
340 self.issuer_privkey = privkey
341 self.issuer_gid = gid
345 # Set this credential's parent
346 def set_parent(self, cred):
351 # set the GID of the caller
353 # @param gid GID object of the caller
355 def set_gid_caller(self, gid):
357 # gid origin caller is the caller's gid by default
358 self.gidOriginCaller = gid
361 # get the GID of the object
363 def get_gid_caller(self):
364 if not self.gidCaller:
366 return self.gidCaller
369 # set the GID of the object
371 # @param gid GID object of the object
373 def set_gid_object(self, gid):
377 # get the GID of the object
379 def get_gid_object(self):
380 if not self.gidObject:
382 return self.gidObject
385 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
387 def set_expiration(self, expiration):
388 if isinstance(expiration, (int, float)):
389 self.expiration = datetime.datetime.fromtimestamp(expiration)
390 elif isinstance (expiration, datetime.datetime):
391 self.expiration = expiration
392 elif isinstance (expiration, StringTypes):
393 self.expiration = utcparse (expiration)
395 logger.error ("unexpected input type in Credential.set_expiration")
399 # get the lifetime of the credential (always in datetime format)
401 def get_expiration(self):
402 if not self.expiration:
404 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
405 return self.expiration
409 def get_lifetime(self):
410 return self.get_expiration()
415 # @param privs either a comma-separated list of privileges of a Rights object
417 def set_privileges(self, privs):
418 if isinstance(privs, str):
419 self.privileges = Rights(string = privs)
421 self.privileges = privs
424 # return the privileges as a Rights object
426 def get_privileges(self):
427 if not self.privileges:
429 return self.privileges
432 # determine whether the credential allows a particular operation to be
435 # @param op_name string specifying name of operation ("lookup", "update", etc)
437 def can_perform(self, op_name):
438 rights = self.get_privileges()
443 return rights.can_perform(op_name)
447 # Encode the attributes of the credential into an XML string
448 # This should be done immediately before signing the credential.
450 # In general, a signed credential obtained externally should
451 # not be changed else the signature is no longer valid. So, once
452 # you have loaded an existing signed credential, do not call encode() or sign() on it.
455 # Create the XML document
457 signed_cred = doc.createElement("signed-credential")
460 # Note that credential/policy.xsd are really the PG schemas
462 # Note that delegation of credentials between the 2 only really works
463 # cause those schemas are identical.
464 # Also note these PG schemas talk about PG tickets and CM policies.
465 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
466 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
467 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")
469 # PG says for those last 2:
470 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
471 # 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")
473 doc.appendChild(signed_cred)
475 # Fill in the <credential> bit
476 cred = doc.createElement("credential")
477 cred.setAttribute("xml:id", self.get_refid())
478 signed_cred.appendChild(cred)
479 append_sub(doc, cred, "type", "privilege")
480 append_sub(doc, cred, "serial", "8")
481 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
482 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
483 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
484 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
485 append_sub(doc, cred, "uuid", "")
486 if not self.expiration:
487 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
488 self.expiration = self.expiration.replace(microsecond=0)
489 append_sub(doc, cred, "expires", self.expiration.isoformat())
490 privileges = doc.createElement("privileges")
491 cred.appendChild(privileges)
494 rights = self.get_privileges()
495 for right in rights.rights:
496 priv = doc.createElement("privilege")
497 append_sub(doc, priv, "name", right.kind)
498 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
499 privileges.appendChild(priv)
501 # Add the parent credential if it exists
503 sdoc = parseString(self.parent.get_xml())
504 # If the root node is a signed-credential (it should be), then
505 # get all its attributes and attach those to our signed_cred
507 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
508 # and we need to include those again here or else their signature
509 # no longer matches on the credential.
510 # We expect three of these, but here we copy them all:
511 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
512 # and from PG (PL is equivalent, as shown above):
513 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
514 # 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")
517 # PL now also declares these, with different URLs, so
518 # the code notices those attributes already existed with
519 # different values, and complains.
520 # This happens regularly on delegation now that PG and
521 # PL both declare the namespace with different URLs.
522 # If the content ever differs this is a problem,
523 # but for now it works - different URLs (values in the attributes)
524 # but the same actual schema, so using the PG schema
525 # on delegated-to-PL credentials works fine.
527 # Note: you could also not copy attributes
528 # which already exist. It appears that both PG and PL
529 # will actually validate a slicecred with a parent
530 # signed using PG namespaces and a child signed with PL
531 # namespaces over the whole thing. But I don't know
532 # if that is a bug in xmlsec1, an accident since
533 # the contents of the schemas are the same,
534 # or something else, but it seems odd. And this works.
535 parentRoot = sdoc.documentElement
536 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
537 for attrIx in range(0, parentRoot.attributes.length):
538 attr = parentRoot.attributes.item(attrIx)
539 # returns the old attribute of same name that was
541 # Below throws InUse exception if we forgot to clone the attribute first
542 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
543 if oldAttr and oldAttr.value != attr.value:
544 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)
546 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
548 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
549 p = doc.createElement("parent")
550 p.appendChild(p_cred)
552 # done handling parent credential
554 # Create the <signatures> tag
555 signatures = doc.createElement("signatures")
556 signed_cred.appendChild(signatures)
558 # Add any parent signatures
560 for cur_cred in self.get_credential_list()[1:]:
561 sdoc = parseString(cur_cred.get_signature().get_xml())
562 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
563 signatures.appendChild(ele)
565 # Get the finished product
566 self.xml = doc.toxml()
569 def save_to_random_tmp_file(self):
570 fp, filename = mkstemp(suffix='cred', text=True)
571 fp = os.fdopen(fp, "w")
572 self.save_to_file(filename, save_parents=True, filep=fp)
575 def save_to_file(self, filename, save_parents=True, filep=None):
581 f = open(filename, "w")
585 def save_to_string(self, save_parents=True):
595 def set_refid(self, rid):
599 # Figure out what refids exist, and update this credential's id
600 # so that it doesn't clobber the others. Returns the refids of
603 def updateRefID(self):
605 self.set_refid('ref0')
610 next_cred = self.parent
612 refs.append(next_cred.get_refid())
614 next_cred = next_cred.parent
619 # Find a unique refid for this credential
620 rid = self.get_refid()
623 rid = "ref%d" % (val + 1)
628 # Return the set of parent credential ref ids
637 # Sign the XML file created by encode()
640 # In general, a signed credential obtained externally should
641 # not be changed else the signature is no longer valid. So, once
642 # you have loaded an existing signed credential, do not call encode() or sign() on it.
645 if not self.issuer_privkey or not self.issuer_gid:
647 doc = parseString(self.get_xml())
648 sigs = doc.getElementsByTagName("signatures")[0]
650 # Create the signature template to be signed
651 signature = Signature()
652 signature.set_refid(self.get_refid())
653 sdoc = parseString(signature.get_xml())
654 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
655 sigs.appendChild(sig_ele)
657 self.xml = doc.toxml()
660 # Split the issuer GID into multiple certificates if it's a chain
661 chain = GID(filename=self.issuer_gid)
664 gid_files.append(chain.save_to_random_tmp_file(False))
665 if chain.get_parent():
666 chain = chain.get_parent()
671 # Call out to xmlsec1 to sign it
672 ref = 'Sig_%s' % self.get_refid()
673 filename = self.save_to_random_tmp_file()
674 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
675 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
678 for gid_file in gid_files:
683 # This is no longer a legacy credential
692 # Retrieve the attributes of the credential from the XML.
693 # This is automatically called by the various get_* methods of
694 # this class and should not need to be called explicitly.
702 doc = parseString(self.xml)
704 raise CredentialNotVerifiable("Malformed credential")
705 doc = parseString(self.xml)
707 signed_cred = doc.getElementsByTagName("signed-credential")
709 # Is this a signed-cred or just a cred?
710 if len(signed_cred) > 0:
711 creds = signed_cred[0].getElementsByTagName("credential")
712 signatures = signed_cred[0].getElementsByTagName("signatures")
713 if len(signatures) > 0:
714 sigs = signatures[0].getElementsByTagName("Signature")
716 creds = doc.getElementsByTagName("credential")
718 if creds is None or len(creds) == 0:
719 # malformed cred file
720 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
722 # Just take the first cred if there are more than one
725 self.set_refid(cred.getAttribute("xml:id"))
726 self.set_expiration(utcparse(getTextNode(cred, "expires")))
727 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
728 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
732 privs = cred.getElementsByTagName("privileges")[0]
734 for priv in privs.getElementsByTagName("privilege"):
735 kind = getTextNode(priv, "name")
736 deleg = str2bool(getTextNode(priv, "can_delegate"))
738 # Convert * into the default privileges for the credential's type
739 # Each inherits the delegatability from the * above
740 _ , type = urn_to_hrn(self.gidObject.get_urn())
741 rl = determine_rights(type, self.gidObject.get_urn())
746 rlist.add(Right(kind.strip(), deleg))
747 self.set_privileges(rlist)
751 parent = cred.getElementsByTagName("parent")
753 parent_doc = parent[0].getElementsByTagName("credential")[0]
754 parent_xml = parent_doc.toxml()
755 self.parent = Credential(string=parent_xml)
758 # Assign the signatures to the credentials
760 Sig = Signature(string=sig.toxml())
762 for cur_cred in self.get_credential_list():
763 if cur_cred.get_refid() == Sig.get_refid():
764 cur_cred.set_signature(Sig)
769 # trusted_certs: A list of trusted GID filenames (not GID objects!)
770 # Chaining is not supported within the GIDs by xmlsec1.
772 # trusted_certs_required: Should usually be true. Set False means an
773 # empty list of trusted_certs would still let this method pass.
774 # It just skips xmlsec1 verification et al. Only used by some utils
777 # . All of the signatures are valid and that the issuers trace back
778 # to trusted roots (performed by xmlsec1)
779 # . The XML matches the credential schema
780 # . That the issuer of the credential is the authority in the target's urn
781 # . In the case of a delegated credential, this must be true of the root
782 # . That all of the gids presented in the credential are valid
783 # . Including verifying GID chains, and includ the issuer
784 # . The credential is not expired
786 # -- For Delegates (credentials with parents)
787 # . The privileges must be a subset of the parent credentials
788 # . The privileges must have "can_delegate" set for each delegated privilege
789 # . The target gid must be the same between child and parents
790 # . The expiry time on the child must be no later than the parent
791 # . The signer of the child must be the owner of the parent
793 # -- Verify does *NOT*
794 # . ensure that an xmlrpc client's gid matches a credential gid, that
795 # must be done elsewhere
797 # @param trusted_certs: The certificates of trusted CA certificates
798 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
802 # validate against RelaxNG schema
803 if HAVELXML and not self.legacy:
804 if schema and os.path.exists(schema):
805 tree = etree.parse(StringIO(self.xml))
806 schema_doc = etree.parse(schema)
807 xmlschema = etree.XMLSchema(schema_doc)
808 if not xmlschema.validate(tree):
809 error = xmlschema.error_log.last_error
810 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
811 raise CredentialNotVerifiable(message)
813 if trusted_certs_required and trusted_certs is None:
816 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
817 trusted_cert_objects = []
818 ok_trusted_certs = []
819 # If caller explicitly passed in None that means skip cert chain validation.
820 # Strange and not typical
821 if trusted_certs is not None:
822 for f in trusted_certs:
824 # Failures here include unreadable files
826 trusted_cert_objects.append(GID(filename=f))
827 ok_trusted_certs.append(f)
828 except Exception, exc:
829 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
830 trusted_certs = ok_trusted_certs
832 # Use legacy verification if this is a legacy credential
834 self.legacy.verify_chain(trusted_cert_objects)
835 if self.legacy.client_gid:
836 self.legacy.client_gid.verify_chain(trusted_cert_objects)
837 if self.legacy.object_gid:
838 self.legacy.object_gid.verify_chain(trusted_cert_objects)
841 # make sure it is not expired
842 if self.get_expiration() < datetime.datetime.utcnow():
843 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
845 # Verify the signatures
846 filename = self.save_to_random_tmp_file()
847 if trusted_certs is not None:
848 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
850 # If caller explicitly passed in None that means skip cert chain validation.
851 # - Strange and not typical
852 if trusted_certs is not None:
853 # Verify the gids of this cred and of its parents
854 for cur_cred in self.get_credential_list():
855 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
856 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
859 refs.append("Sig_%s" % self.get_refid())
861 parentRefs = self.updateRefID()
862 for ref in parentRefs:
863 refs.append("Sig_%s" % ref)
866 # If caller explicitly passed in None that means skip xmlsec1 validation.
867 # Strange and not typical
868 if trusted_certs is None:
871 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
872 # (self.xmlsec_path, ref, cert_args, filename)
873 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
874 % (self.xmlsec_path, ref, cert_args, filename)).read()
875 if not verified.strip().startswith("OK"):
876 # xmlsec errors have a msg= which is the interesting bit.
877 mstart = verified.find("msg=")
879 if mstart > -1 and len(verified) > 4:
881 mend = verified.find('\\', mstart)
882 msg = verified[mstart:mend]
883 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
886 # Verify the parents (delegation)
888 self.verify_parent(self.parent)
890 # Make sure the issuer is the target's authority, and is
892 self.verify_issuer(trusted_cert_objects)
896 # Creates a list of the credential and its parents, with the root
897 # (original delegated credential) as the last item in the list
898 def get_credential_list(self):
902 list.append(cur_cred)
904 cur_cred = cur_cred.parent
910 # Make sure the credential's target gid (a) was signed by or (b)
911 # is the same as the entity that signed the original credential,
912 # or (c) is an authority over the target's namespace.
913 # Also ensure that the credential issuer / signer itself has a valid
914 # GID signature chain (signed by an authority with namespace rights).
915 def verify_issuer(self, trusted_gids):
916 root_cred = self.get_credential_list()[-1]
917 root_target_gid = root_cred.get_gid_object()
918 root_cred_signer = root_cred.get_signature().get_issuer_gid()
921 # Allow non authority to sign target and cred about target.
923 # Why do we need to allow non authorities to sign?
924 # If in the target gid validation step we correctly
925 # checked that the target is only signed by an authority,
926 # then this is just a special case of case 3.
927 # This short-circuit is the common case currently -
928 # and cause GID validation doesn't check 'authority',
929 # this allows users to generate valid slice credentials.
930 if root_target_gid.is_signed_by_cert(root_cred_signer):
931 # cred signer matches target signer, return success
935 # Allow someone to sign credential about themeselves. Used?
936 # If not, remove this.
937 #root_target_gid_str = root_target_gid.save_to_string()
938 #root_cred_signer_str = root_cred_signer.save_to_string()
939 #if root_target_gid_str == root_cred_signer_str:
940 # # cred signer is target, return success
945 # root_cred_signer is not the target_gid
946 # So this is a different gid that we have not verified.
947 # xmlsec1 verified the cert chain on this already, but
948 # it hasn't verified that the gid meets the HRN namespace
950 # Below we'll ensure that it is an authority.
951 # But we haven't verified that it is _signed by_ an authority
952 # We also don't know if xmlsec1 requires that cert signers
955 # Note that if verify() gave us no trusted_gids then this
956 # call will fail. So skip it if we have no trusted_gids
957 if trusted_gids and len(trusted_gids) > 0:
958 root_cred_signer.verify_chain(trusted_gids)
960 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
962 # See if the signer is an authority over the domain of the target.
963 # There are multiple types of authority - accept them all here
964 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
965 root_cred_signer_type = root_cred_signer.get_type()
966 if (root_cred_signer_type.find('authority') == 0):
967 #logger.debug('Cred signer is an authority')
968 # signer is an authority, see if target is in authority's domain
969 signerhrn = root_cred_signer.get_hrn()
970 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
973 # We've required that the credential be signed by an authority
974 # for that domain. Reasonable and probably correct.
975 # A looser model would also allow the signer to be an authority
976 # in my control framework - eg My CA or CH. Even if it is not
977 # the CH that issued these, eg, user credentials.
979 # Give up, credential does not pass issuer verification
981 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()))
985 # -- For Delegates (credentials with parents) verify that:
986 # . The privileges must be a subset of the parent credentials
987 # . The privileges must have "can_delegate" set for each delegated privilege
988 # . The target gid must be the same between child and parents
989 # . The expiry time on the child must be no later than the parent
990 # . The signer of the child must be the owner of the parent
991 def verify_parent(self, parent_cred):
992 # make sure the rights given to the child are a subset of the
993 # parents rights (and check delegate bits)
994 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
995 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
996 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
997 self.get_privileges().save_to_string())
999 # make sure my target gid is the same as the parent's
1000 if not parent_cred.get_gid_object().save_to_string() == \
1001 self.get_gid_object().save_to_string():
1002 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1004 # make sure my expiry time is <= my parent's
1005 if not parent_cred.get_expiration() >= self.get_expiration():
1006 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1008 # make sure my signer is the parent's caller
1009 if not parent_cred.get_gid_caller().save_to_string(False) == \
1010 self.get_signature().get_issuer_gid().save_to_string(False):
1011 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1014 if parent_cred.parent:
1015 parent_cred.verify_parent(parent_cred.parent)
1018 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1020 Return a delegated copy of this credential, delegated to the
1021 specified gid's user.
1023 # get the gid of the object we are delegating
1024 object_gid = self.get_gid_object()
1025 object_hrn = object_gid.get_hrn()
1027 # the hrn of the user who will be delegated to
1028 delegee_gid = GID(filename=delegee_gidfile)
1029 delegee_hrn = delegee_gid.get_hrn()
1031 #user_key = Keypair(filename=keyfile)
1032 #user_hrn = self.get_gid_caller().get_hrn()
1033 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1034 dcred = Credential(subject=subject_string)
1035 dcred.set_gid_caller(delegee_gid)
1036 dcred.set_gid_object(object_gid)
1037 dcred.set_parent(self)
1038 dcred.set_expiration(self.get_expiration())
1039 dcred.set_privileges(self.get_privileges())
1040 dcred.get_privileges().delegate_all_privileges(True)
1041 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1042 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1049 def get_filename(self):
1050 return getattr(self,'filename',None)
1053 # Dump the contents of a credential to stdout in human-readable format
1055 # @param dump_parents If true, also dump the parent certificates
1056 def dump (self, *args, **kwargs):
1057 print self.dump_string(*args, **kwargs)
1060 def dump_string(self, dump_parents=False):
1062 result += "CREDENTIAL %s\n" % self.get_subject()
1063 filename=self.get_filename()
1064 if filename: result += "Filename %s\n"%filename
1065 privileges = self.get_privileges()
1067 result += " privs: %s\n" % privileges.save_to_string()
1069 result += " privs: \n"
1070 gidCaller = self.get_gid_caller()
1072 result += " gidCaller:\n"
1073 result += gidCaller.dump_string(8, dump_parents)
1075 if self.get_signature():
1077 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1080 print " expiration:", self.expiration.isoformat()
1082 gidObject = self.get_gid_object()
1084 result += " gidObject:\n"
1085 result += gidObject.dump_string(8, dump_parents)
1087 if self.parent and dump_parents:
1088 result += "\nPARENT"
1089 result += self.parent.dump_string(True)