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):
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
254 # Check if this is a legacy credential, translate it if so
255 if string or filename:
259 str = file(filename).read()
261 if str.strip().startswith("-----"):
262 self.legacy = CredentialLegacy(False,string=str)
263 self.translate_legacy(str)
268 # Find an xmlsec1 path
269 self.xmlsec_path = ''
270 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
272 if os.path.isfile(path + '/' + 'xmlsec1'):
273 self.xmlsec_path = path + '/' + 'xmlsec1'
275 if not self.xmlsec_path:
276 logger.warn("Could not locate binary for xmlsec1 - SFA will be unable to sign stuff !!")
278 def get_subject(self):
279 if not self.gidObject:
281 return self.gidObject.get_subject()
283 # sounds like this should be __repr__ instead ??
284 def get_summary_tostring(self):
285 if not self.gidObject:
287 obj = self.gidObject.get_printable_subject()
288 caller = self.gidCaller.get_printable_subject()
289 exp = self.get_expiration()
290 # Summarize the rights too? The issuer?
291 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
293 def get_signature(self):
294 if not self.signature:
296 return self.signature
298 def set_signature(self, sig):
303 # Translate a legacy credential into a new one
305 # @param String of the legacy credential
307 def translate_legacy(self, str):
308 legacy = CredentialLegacy(False,string=str)
309 self.gidCaller = legacy.get_gid_caller()
310 self.gidObject = legacy.get_gid_object()
311 lifetime = legacy.get_lifetime()
313 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
315 self.set_expiration(int(lifetime))
316 self.lifeTime = legacy.get_lifetime()
317 self.set_privileges(legacy.get_privileges())
318 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
321 # Need the issuer's private key and name
322 # @param key Keypair object containing the private key of the issuer
323 # @param gid GID of the issuing authority
325 def set_issuer_keys(self, privkey, gid):
326 self.issuer_privkey = privkey
327 self.issuer_gid = gid
331 # Set this credential's parent
332 def set_parent(self, cred):
337 # set the GID of the caller
339 # @param gid GID object of the caller
341 def set_gid_caller(self, gid):
343 # gid origin caller is the caller's gid by default
344 self.gidOriginCaller = gid
347 # get the GID of the object
349 def get_gid_caller(self):
350 if not self.gidCaller:
352 return self.gidCaller
355 # set the GID of the object
357 # @param gid GID object of the object
359 def set_gid_object(self, gid):
363 # get the GID of the object
365 def get_gid_object(self):
366 if not self.gidObject:
368 return self.gidObject
371 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
373 def set_expiration(self, expiration):
374 if isinstance(expiration, (int, float)):
375 self.expiration = datetime.datetime.fromtimestamp(expiration)
376 elif isinstance (expiration, datetime.datetime):
377 self.expiration = expiration
378 elif isinstance (expiration, StringTypes):
379 self.expiration = utcparse (expiration)
381 logger.error ("unexpected input type in Credential.set_expiration")
385 # get the lifetime of the credential (always in datetime format)
387 def get_expiration(self):
388 if not self.expiration:
390 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
391 return self.expiration
395 def get_lifetime(self):
396 return self.get_expiration()
401 # @param privs either a comma-separated list of privileges of a Rights object
403 def set_privileges(self, privs):
404 if isinstance(privs, str):
405 self.privileges = Rights(string = privs)
407 self.privileges = privs
410 # return the privileges as a Rights object
412 def get_privileges(self):
413 if not self.privileges:
415 return self.privileges
418 # determine whether the credential allows a particular operation to be
421 # @param op_name string specifying name of operation ("lookup", "update", etc)
423 def can_perform(self, op_name):
424 rights = self.get_privileges()
429 return rights.can_perform(op_name)
433 # Encode the attributes of the credential into an XML string
434 # This should be done immediately before signing the credential.
436 # In general, a signed credential obtained externally should
437 # not be changed else the signature is no longer valid. So, once
438 # you have loaded an existing signed credential, do not call encode() or sign() on it.
441 # Create the XML document
443 signed_cred = doc.createElement("signed-credential")
446 # Note that credential/policy.xsd are really the PG schemas
448 # Note that delegation of credentials between the 2 only really works
449 # cause those schemas are identical.
450 # Also note these PG schemas talk about PG tickets and CM policies.
451 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
452 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
453 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")
455 # PG says for those last 2:
456 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
457 # 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")
459 doc.appendChild(signed_cred)
461 # Fill in the <credential> bit
462 cred = doc.createElement("credential")
463 cred.setAttribute("xml:id", self.get_refid())
464 signed_cred.appendChild(cred)
465 append_sub(doc, cred, "type", "privilege")
466 append_sub(doc, cred, "serial", "8")
467 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
468 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
469 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
470 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
471 append_sub(doc, cred, "uuid", "")
472 if not self.expiration:
473 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
474 self.expiration = self.expiration.replace(microsecond=0)
475 append_sub(doc, cred, "expires", self.expiration.isoformat())
476 privileges = doc.createElement("privileges")
477 cred.appendChild(privileges)
480 rights = self.get_privileges()
481 for right in rights.rights:
482 priv = doc.createElement("privilege")
483 append_sub(doc, priv, "name", right.kind)
484 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
485 privileges.appendChild(priv)
487 # Add the parent credential if it exists
489 sdoc = parseString(self.parent.get_xml())
490 # If the root node is a signed-credential (it should be), then
491 # get all its attributes and attach those to our signed_cred
493 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
494 # and we need to include those again here or else their signature
495 # no longer matches on the credential.
496 # We expect three of these, but here we copy them all:
497 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
498 # and from PG (PL is equivalent, as shown above):
499 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
500 # 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")
503 # PL now also declares these, with different URLs, so
504 # the code notices those attributes already existed with
505 # different values, and complains.
506 # This happens regularly on delegation now that PG and
507 # PL both declare the namespace with different URLs.
508 # If the content ever differs this is a problem,
509 # but for now it works - different URLs (values in the attributes)
510 # but the same actual schema, so using the PG schema
511 # on delegated-to-PL credentials works fine.
513 # Note: you could also not copy attributes
514 # which already exist. It appears that both PG and PL
515 # will actually validate a slicecred with a parent
516 # signed using PG namespaces and a child signed with PL
517 # namespaces over the whole thing. But I don't know
518 # if that is a bug in xmlsec1, an accident since
519 # the contents of the schemas are the same,
520 # or something else, but it seems odd. And this works.
521 parentRoot = sdoc.documentElement
522 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
523 for attrIx in range(0, parentRoot.attributes.length):
524 attr = parentRoot.attributes.item(attrIx)
525 # returns the old attribute of same name that was
527 # Below throws InUse exception if we forgot to clone the attribute first
528 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
529 if oldAttr and oldAttr.value != attr.value:
530 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)
532 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
534 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
535 p = doc.createElement("parent")
536 p.appendChild(p_cred)
538 # done handling parent credential
540 # Create the <signatures> tag
541 signatures = doc.createElement("signatures")
542 signed_cred.appendChild(signatures)
544 # Add any parent signatures
546 for cur_cred in self.get_credential_list()[1:]:
547 sdoc = parseString(cur_cred.get_signature().get_xml())
548 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
549 signatures.appendChild(ele)
551 # Get the finished product
552 self.xml = doc.toxml()
555 def save_to_random_tmp_file(self):
556 fp, filename = mkstemp(suffix='cred', text=True)
557 fp = os.fdopen(fp, "w")
558 self.save_to_file(filename, save_parents=True, filep=fp)
561 def save_to_file(self, filename, save_parents=True, filep=None):
567 f = open(filename, "w")
571 def save_to_string(self, save_parents=True):
581 def set_refid(self, rid):
585 # Figure out what refids exist, and update this credential's id
586 # so that it doesn't clobber the others. Returns the refids of
589 def updateRefID(self):
591 self.set_refid('ref0')
596 next_cred = self.parent
598 refs.append(next_cred.get_refid())
600 next_cred = next_cred.parent
605 # Find a unique refid for this credential
606 rid = self.get_refid()
609 rid = "ref%d" % (val + 1)
614 # Return the set of parent credential ref ids
623 # Sign the XML file created by encode()
626 # In general, a signed credential obtained externally should
627 # not be changed else the signature is no longer valid. So, once
628 # you have loaded an existing signed credential, do not call encode() or sign() on it.
631 if not self.issuer_privkey:
632 logger.warn("Cannot sign credential (no private key)")
634 if not self.issuer_gid:
635 logger.warn("Cannot sign credential (no issuer gid)")
637 doc = parseString(self.get_xml())
638 sigs = doc.getElementsByTagName("signatures")[0]
640 # Create the signature template to be signed
641 signature = Signature()
642 signature.set_refid(self.get_refid())
643 sdoc = parseString(signature.get_xml())
644 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
645 sigs.appendChild(sig_ele)
647 self.xml = doc.toxml()
650 # Split the issuer GID into multiple certificates if it's a chain
651 chain = GID(filename=self.issuer_gid)
654 gid_files.append(chain.save_to_random_tmp_file(False))
655 if chain.get_parent():
656 chain = chain.get_parent()
661 # Call out to xmlsec1 to sign it
662 ref = 'Sig_%s' % self.get_refid()
663 filename = self.save_to_random_tmp_file()
664 command='%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
665 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)
666 # print 'command',command
667 signed = os.popen(command).read()
670 for gid_file in gid_files:
675 # This is no longer a legacy credential
684 # Retrieve the attributes of the credential from the XML.
685 # This is automatically called by the various get_* methods of
686 # this class and should not need to be called explicitly.
691 doc = parseString(self.xml)
693 signed_cred = doc.getElementsByTagName("signed-credential")
695 # Is this a signed-cred or just a cred?
696 if len(signed_cred) > 0:
697 creds = signed_cred[0].getElementsByTagName("credential")
698 signatures = signed_cred[0].getElementsByTagName("signatures")
699 if len(signatures) > 0:
700 sigs = signatures[0].getElementsByTagName("Signature")
702 creds = doc.getElementsByTagName("credential")
704 if creds is None or len(creds) == 0:
705 # malformed cred file
706 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
708 # Just take the first cred if there are more than one
711 self.set_refid(cred.getAttribute("xml:id"))
712 self.set_expiration(utcparse(getTextNode(cred, "expires")))
713 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
714 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
718 privs = cred.getElementsByTagName("privileges")[0]
720 for priv in privs.getElementsByTagName("privilege"):
721 kind = getTextNode(priv, "name")
722 deleg = str2bool(getTextNode(priv, "can_delegate"))
724 # Convert * into the default privileges for the credential's type
725 # Each inherits the delegatability from the * above
726 _ , type = urn_to_hrn(self.gidObject.get_urn())
727 rl = determine_rights(type, self.gidObject.get_urn())
732 rlist.add(Right(kind.strip(), deleg))
733 self.set_privileges(rlist)
737 parent = cred.getElementsByTagName("parent")
739 parent_doc = parent[0].getElementsByTagName("credential")[0]
740 parent_xml = parent_doc.toxml()
741 self.parent = Credential(string=parent_xml)
744 # Assign the signatures to the credentials
746 Sig = Signature(string=sig.toxml())
748 for cur_cred in self.get_credential_list():
749 if cur_cred.get_refid() == Sig.get_refid():
750 cur_cred.set_signature(Sig)
755 # trusted_certs: A list of trusted GID filenames (not GID objects!)
756 # Chaining is not supported within the GIDs by xmlsec1.
758 # trusted_certs_required: Should usually be true. Set False means an
759 # empty list of trusted_certs would still let this method pass.
760 # It just skips xmlsec1 verification et al. Only used by some utils
763 # . All of the signatures are valid and that the issuers trace back
764 # to trusted roots (performed by xmlsec1)
765 # . The XML matches the credential schema
766 # . That the issuer of the credential is the authority in the target's urn
767 # . In the case of a delegated credential, this must be true of the root
768 # . That all of the gids presented in the credential are valid
769 # . Including verifying GID chains, and includ the issuer
770 # . The credential is not expired
772 # -- For Delegates (credentials with parents)
773 # . The privileges must be a subset of the parent credentials
774 # . The privileges must have "can_delegate" set for each delegated privilege
775 # . The target gid must be the same between child and parents
776 # . The expiry time on the child must be no later than the parent
777 # . The signer of the child must be the owner of the parent
779 # -- Verify does *NOT*
780 # . ensure that an xmlrpc client's gid matches a credential gid, that
781 # must be done elsewhere
783 # @param trusted_certs: The certificates of trusted CA certificates
784 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
788 # validate against RelaxNG schema
789 if HAVELXML and not self.legacy:
790 if schema and os.path.exists(schema):
791 tree = etree.parse(StringIO(self.xml))
792 schema_doc = etree.parse(schema)
793 xmlschema = etree.XMLSchema(schema_doc)
794 if not xmlschema.validate(tree):
795 error = xmlschema.error_log.last_error
796 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
797 raise CredentialNotVerifiable(message)
799 if trusted_certs_required and trusted_certs is None:
802 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
803 trusted_cert_objects = []
804 ok_trusted_certs = []
805 # If caller explicitly passed in None that means skip cert chain validation.
806 # Strange and not typical
807 if trusted_certs is not None:
808 for f in trusted_certs:
810 # Failures here include unreadable files
812 trusted_cert_objects.append(GID(filename=f))
813 ok_trusted_certs.append(f)
814 except Exception, exc:
815 logger.error("Failed to load trusted cert from %s: %r", f, exc)
816 trusted_certs = ok_trusted_certs
818 # Use legacy verification if this is a legacy credential
820 self.legacy.verify_chain(trusted_cert_objects)
821 if self.legacy.client_gid:
822 self.legacy.client_gid.verify_chain(trusted_cert_objects)
823 if self.legacy.object_gid:
824 self.legacy.object_gid.verify_chain(trusted_cert_objects)
827 # make sure it is not expired
828 if self.get_expiration() < datetime.datetime.utcnow():
829 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
831 # Verify the signatures
832 filename = self.save_to_random_tmp_file()
833 if trusted_certs is not None:
834 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
836 # If caller explicitly passed in None that means skip cert chain validation.
837 # - Strange and not typical
838 if trusted_certs is not None:
839 # Verify the gids of this cred and of its parents
840 for cur_cred in self.get_credential_list():
841 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
842 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
845 refs.append("Sig_%s" % self.get_refid())
847 parentRefs = self.updateRefID()
848 for ref in parentRefs:
849 refs.append("Sig_%s" % ref)
852 # If caller explicitly passed in None that means skip xmlsec1 validation.
853 # Strange and not typical
854 if trusted_certs is None:
857 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
858 # (self.xmlsec_path, ref, cert_args, filename)
859 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
860 % (self.xmlsec_path, ref, cert_args, filename)).read()
861 if not verified.strip().startswith("OK"):
862 # xmlsec errors have a msg= which is the interesting bit.
863 mstart = verified.find("msg=")
865 if mstart > -1 and len(verified) > 4:
867 mend = verified.find('\\', mstart)
868 msg = verified[mstart:mend]
869 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
872 # Verify the parents (delegation)
874 self.verify_parent(self.parent)
876 # Make sure the issuer is the target's authority, and is
878 self.verify_issuer(trusted_cert_objects)
882 # Creates a list of the credential and its parents, with the root
883 # (original delegated credential) as the last item in the list
884 def get_credential_list(self):
888 list.append(cur_cred)
890 cur_cred = cur_cred.parent
896 # Make sure the credential's target gid (a) was signed by or (b)
897 # is the same as the entity that signed the original credential,
898 # or (c) is an authority over the target's namespace.
899 # Also ensure that the credential issuer / signer itself has a valid
900 # GID signature chain (signed by an authority with namespace rights).
901 def verify_issuer(self, trusted_gids):
902 root_cred = self.get_credential_list()[-1]
903 root_target_gid = root_cred.get_gid_object()
904 root_cred_signer = root_cred.get_signature().get_issuer_gid()
907 # Allow non authority to sign target and cred about target.
909 # Why do we need to allow non authorities to sign?
910 # If in the target gid validation step we correctly
911 # checked that the target is only signed by an authority,
912 # then this is just a special case of case 3.
913 # This short-circuit is the common case currently -
914 # and cause GID validation doesn't check 'authority',
915 # this allows users to generate valid slice credentials.
916 if root_target_gid.is_signed_by_cert(root_cred_signer):
917 # cred signer matches target signer, return success
921 # Allow someone to sign credential about themeselves. Used?
922 # If not, remove this.
923 #root_target_gid_str = root_target_gid.save_to_string()
924 #root_cred_signer_str = root_cred_signer.save_to_string()
925 #if root_target_gid_str == root_cred_signer_str:
926 # # cred signer is target, return success
931 # root_cred_signer is not the target_gid
932 # So this is a different gid that we have not verified.
933 # xmlsec1 verified the cert chain on this already, but
934 # it hasn't verified that the gid meets the HRN namespace
936 # Below we'll ensure that it is an authority.
937 # But we haven't verified that it is _signed by_ an authority
938 # We also don't know if xmlsec1 requires that cert signers
941 # Note that if verify() gave us no trusted_gids then this
942 # call will fail. So skip it if we have no trusted_gids
943 if trusted_gids and len(trusted_gids) > 0:
944 root_cred_signer.verify_chain(trusted_gids)
946 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
948 # See if the signer is an authority over the domain of the target.
949 # There are multiple types of authority - accept them all here
950 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
951 root_cred_signer_type = root_cred_signer.get_type()
952 if (root_cred_signer_type.find('authority') == 0):
953 #logger.debug('Cred signer is an authority')
954 # signer is an authority, see if target is in authority's domain
955 signerhrn = root_cred_signer.get_hrn()
956 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
959 # We've required that the credential be signed by an authority
960 # for that domain. Reasonable and probably correct.
961 # A looser model would also allow the signer to be an authority
962 # in my control framework - eg My CA or CH. Even if it is not
963 # the CH that issued these, eg, user credentials.
965 # Give up, credential does not pass issuer verification
967 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()))
971 # -- For Delegates (credentials with parents) verify that:
972 # . The privileges must be a subset of the parent credentials
973 # . The privileges must have "can_delegate" set for each delegated privilege
974 # . The target gid must be the same between child and parents
975 # . The expiry time on the child must be no later than the parent
976 # . The signer of the child must be the owner of the parent
977 def verify_parent(self, parent_cred):
978 # make sure the rights given to the child are a subset of the
979 # parents rights (and check delegate bits)
980 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
981 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
982 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
983 self.get_privileges().save_to_string())
985 # make sure my target gid is the same as the parent's
986 if not parent_cred.get_gid_object().save_to_string() == \
987 self.get_gid_object().save_to_string():
988 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
990 # make sure my expiry time is <= my parent's
991 if not parent_cred.get_expiration() >= self.get_expiration():
992 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
994 # make sure my signer is the parent's caller
995 if not parent_cred.get_gid_caller().save_to_string(False) == \
996 self.get_signature().get_issuer_gid().save_to_string(False):
997 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1000 if parent_cred.parent:
1001 parent_cred.verify_parent(parent_cred.parent)
1004 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1006 Return a delegated copy of this credential, delegated to the
1007 specified gid's user.
1009 # get the gid of the object we are delegating
1010 object_gid = self.get_gid_object()
1011 object_hrn = object_gid.get_hrn()
1013 # the hrn of the user who will be delegated to
1014 delegee_gid = GID(filename=delegee_gidfile)
1015 delegee_hrn = delegee_gid.get_hrn()
1017 #user_key = Keypair(filename=keyfile)
1018 #user_hrn = self.get_gid_caller().get_hrn()
1019 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1020 dcred = Credential(subject=subject_string)
1021 dcred.set_gid_caller(delegee_gid)
1022 dcred.set_gid_object(object_gid)
1023 dcred.set_parent(self)
1024 dcred.set_expiration(self.get_expiration())
1025 dcred.set_privileges(self.get_privileges())
1026 dcred.get_privileges().delegate_all_privileges(True)
1027 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1028 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1035 def get_filename(self):
1036 return getattr(self,'filename',None)
1039 # Dump the contents of a credential to stdout in human-readable format
1041 # @param dump_parents If true, also dump the parent certificates
1042 def dump (self, *args, **kwargs):
1043 print self.dump_string(*args, **kwargs)
1046 def dump_string(self, dump_parents=False, show_xml=False):
1048 result += "CREDENTIAL %s\n" % self.get_subject()
1049 filename=self.get_filename()
1050 if filename: result += "Filename %s\n"%filename
1051 result += " privs: %s\n" % self.get_privileges().save_to_string()
1052 gidCaller = self.get_gid_caller()
1054 result += " gidCaller:\n"
1055 result += gidCaller.dump_string(8, dump_parents)
1057 if self.get_signature():
1059 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1062 print " expiration:", self.expiration.isoformat()
1064 gidObject = self.get_gid_object()
1066 result += " gidObject:\n"
1067 result += gidObject.dump_string(8, dump_parents)
1069 if self.parent and dump_parents:
1070 result += "\nPARENT"
1071 result += self.parent.dump_string(True)
1075 tree = etree.parse(StringIO(self.xml))
1076 aside = etree.tostring(tree, pretty_print=True)
1079 result += "\nEnd XML\n"
1082 print "exc. Credential.dump_string / XML"
1083 traceback.print_exc()