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
163 #print>>sys.stderr," \r\n \r\n credential.py Signature get_refid\ self.refid %s " %(self.refid)
166 #print>>sys.stderr," \r\n \r\n credential.py Signature get_refid self.refid %s " %(self.refid)
174 def set_refid(self, id):
177 def get_issuer_gid(self):
182 def set_issuer_gid(self, gid):
187 doc = parseString(self.xml)
189 logger.log_exc ("Failed to parse credential, %s"%self.xml)
191 sig = doc.getElementsByTagName("Signature")[0]
192 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
193 keyinfo = sig.getElementsByTagName("X509Data")[0]
194 szgid = getTextNode(keyinfo, "X509Certificate")
195 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
196 self.set_issuer_gid(GID(string=szgid))
199 self.xml = signature_template % (self.get_refid(), self.get_refid())
203 # A credential provides a caller gid with privileges to an object gid.
204 # A signed credential is signed by the object's authority.
206 # Credentials are encoded in one of two ways. The legacy style places
207 # it in the subjectAltName of an X509 certificate. The new credentials
208 # are placed in signed XML.
211 # In general, a signed credential obtained externally should
212 # not be changed else the signature is no longer valid. So, once
213 # you have loaded an existing signed credential, do not call encode() or sign() on it.
215 def filter_creds_by_caller(creds, caller_hrn_list):
217 Returns a list of creds who's gid caller matches the
220 if not isinstance(creds, list): creds = [creds]
221 if not isinstance(caller_hrn_list, list):
222 caller_hrn_list = [caller_hrn_list]
226 tmp_cred = Credential(string=cred)
227 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
228 caller_creds.append(cred)
232 class Credential(object):
235 # Create a Credential object
237 # @param create If true, create a blank x509 certificate
238 # @param subject If subject!=None, create an x509 cert with the subject name
239 # @param string If string!=None, load the credential from the string
240 # @param filename If filename!=None, load the credential from the file
241 # FIXME: create and subject are ignored!
242 def __init__(self, create=False, subject=None, string=None, filename=None):
243 self.gidCaller = None
244 self.gidObject = None
245 self.expiration = None
246 self.privileges = None
247 self.issuer_privkey = None
248 self.issuer_gid = None
249 self.issuer_pubkey = None
251 self.signature = None
256 # Check if this is a legacy credential, translate it if so
257 if string or filename:
261 str = file(filename).read()
263 if str.strip().startswith("-----"):
264 self.legacy = CredentialLegacy(False,string=str)
265 self.translate_legacy(str)
270 # Find an xmlsec1 path
271 self.xmlsec_path = ''
272 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
274 if os.path.isfile(path + '/' + 'xmlsec1'):
275 self.xmlsec_path = path + '/' + 'xmlsec1'
278 def get_subject(self):
279 if not self.gidObject:
281 return self.gidObject.get_printable_subject()
283 def get_summary_tostring(self):
284 if not self.gidObject:
286 obj = self.gidObject.get_printable_subject()
287 caller = self.gidCaller.get_printable_subject()
288 exp = self.get_expiration()
289 # Summarize the rights too? The issuer?
290 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
292 def get_signature(self):
293 if not self.signature:
295 return self.signature
297 def set_signature(self, sig):
302 # Translate a legacy credential into a new one
304 # @param String of the legacy credential
306 def translate_legacy(self, str):
307 legacy = CredentialLegacy(False,string=str)
308 self.gidCaller = legacy.get_gid_caller()
309 self.gidObject = legacy.get_gid_object()
310 lifetime = legacy.get_lifetime()
312 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
314 self.set_expiration(int(lifetime))
315 self.lifeTime = legacy.get_lifetime()
316 self.set_privileges(legacy.get_privileges())
317 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
320 # Need the issuer's private key and name
321 # @param key Keypair object containing the private key of the issuer
322 # @param gid GID of the issuing authority
324 def set_issuer_keys(self, privkey, gid):
325 self.issuer_privkey = privkey
326 self.issuer_gid = gid
330 # Set this credential's parent
331 def set_parent(self, cred):
336 # set the GID of the caller
338 # @param gid GID object of the caller
340 def set_gid_caller(self, gid):
342 # gid origin caller is the caller's gid by default
343 self.gidOriginCaller = gid
346 # get the GID of the object
348 def get_gid_caller(self):
349 if not self.gidCaller:
351 return self.gidCaller
354 # set the GID of the object
356 # @param gid GID object of the object
358 def set_gid_object(self, gid):
362 # get the GID of the object
364 def get_gid_object(self):
365 if not self.gidObject:
367 return self.gidObject
372 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
374 def set_expiration(self, expiration):
375 if isinstance(expiration, (int, float)):
376 self.expiration = datetime.datetime.fromtimestamp(expiration)
377 elif isinstance (expiration, datetime.datetime):
378 self.expiration = expiration
379 elif isinstance (expiration, StringTypes):
380 self.expiration = utcparse (expiration)
382 logger.error ("unexpected input type in Credential.set_expiration")
386 # get the lifetime of the credential (always in datetime format)
388 def get_expiration(self):
389 if not self.expiration:
391 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
392 return self.expiration
396 def get_lifetime(self):
397 return self.get_expiration()
402 # @param privs either a comma-separated list of privileges of a Rights object
404 def set_privileges(self, privs):
405 if isinstance(privs, str):
406 self.privileges = Rights(string = privs)
408 self.privileges = privs
412 # return the privileges as a Rights object
414 def get_privileges(self):
415 if not self.privileges:
417 return self.privileges
420 # determine whether the credential allows a particular operation to be
423 # @param op_name string specifying name of operation ("lookup", "update", etc)
425 def can_perform(self, op_name):
426 rights = self.get_privileges()
431 return rights.can_perform(op_name)
435 # Encode the attributes of the credential into an XML string
436 # This should be done immediately before signing the credential.
438 # In general, a signed credential obtained externally should
439 # not be changed else the signature is no longer valid. So, once
440 # you have loaded an existing signed credential, do not call encode() or sign() on it.
443 # Create the XML document
445 signed_cred = doc.createElement("signed-credential")
448 # Note that credential/policy.xsd are really the PG schemas
450 # Note that delegation of credentials between the 2 only really works
451 # cause those schemas are identical.
452 # Also note these PG schemas talk about PG tickets and CM policies.
453 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
454 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
455 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")
457 # PG says for those last 2:
458 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
459 # 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")
461 doc.appendChild(signed_cred)
463 # Fill in the <credential> bit
464 cred = doc.createElement("credential")
465 cred.setAttribute("xml:id", self.get_refid())
466 signed_cred.appendChild(cred)
467 append_sub(doc, cred, "type", "privilege")
468 append_sub(doc, cred, "serial", "8")
469 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
470 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
471 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
472 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
473 append_sub(doc, cred, "uuid", "")
474 if not self.expiration:
475 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
476 self.expiration = self.expiration.replace(microsecond=0)
477 append_sub(doc, cred, "expires", self.expiration.isoformat())
478 privileges = doc.createElement("privileges")
479 cred.appendChild(privileges)
482 rights = self.get_privileges()
483 for right in rights.rights:
484 priv = doc.createElement("privilege")
485 append_sub(doc, priv, "name", right.kind)
486 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
487 privileges.appendChild(priv)
489 # Add the parent credential if it exists
491 sdoc = parseString(self.parent.get_xml())
492 # If the root node is a signed-credential (it should be), then
493 # get all its attributes and attach those to our signed_cred
495 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
496 # and we need to include those again here or else their signature
497 # no longer matches on the credential.
498 # We expect three of these, but here we copy them all:
499 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
500 # and from PG (PL is equivalent, as shown above):
501 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
502 # 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")
505 # PL now also declares these, with different URLs, so
506 # the code notices those attributes already existed with
507 # different values, and complains.
508 # This happens regularly on delegation now that PG and
509 # PL both declare the namespace with different URLs.
510 # If the content ever differs this is a problem,
511 # but for now it works - different URLs (values in the attributes)
512 # but the same actual schema, so using the PG schema
513 # on delegated-to-PL credentials works fine.
515 # Note: you could also not copy attributes
516 # which already exist. It appears that both PG and PL
517 # will actually validate a slicecred with a parent
518 # signed using PG namespaces and a child signed with PL
519 # namespaces over the whole thing. But I don't know
520 # if that is a bug in xmlsec1, an accident since
521 # the contents of the schemas are the same,
522 # or something else, but it seems odd. And this works.
523 parentRoot = sdoc.documentElement
524 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
525 for attrIx in range(0, parentRoot.attributes.length):
526 attr = parentRoot.attributes.item(attrIx)
527 # returns the old attribute of same name that was
529 # Below throws InUse exception if we forgot to clone the attribute first
530 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
531 if oldAttr and oldAttr.value != attr.value:
532 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)
534 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
536 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
537 p = doc.createElement("parent")
538 p.appendChild(p_cred)
540 # done handling parent credential
542 # Create the <signatures> tag
543 signatures = doc.createElement("signatures")
544 signed_cred.appendChild(signatures)
546 # Add any parent signatures
548 for cur_cred in self.get_credential_list()[1:]:
549 sdoc = parseString(cur_cred.get_signature().get_xml())
550 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
551 signatures.appendChild(ele)
553 # Get the finished product
554 self.xml = doc.toxml()
557 def save_to_random_tmp_file(self):
558 fp, filename = mkstemp(suffix='cred', text=True)
559 fp = os.fdopen(fp, "w")
560 self.save_to_file(filename, save_parents=True, filep=fp)
563 def save_to_file(self, filename, save_parents=True, filep=None):
569 f = open(filename, "w")
573 def save_to_string(self, save_parents=True):
583 def set_refid(self, rid):
587 # Figure out what refids exist, and update this credential's id
588 # so that it doesn't clobber the others. Returns the refids of
591 def updateRefID(self):
593 self.set_refid('ref0')
594 #print>>sys.stderr, " \r\n \r\n updateRefID next_cred ref0 "
599 next_cred = self.parent
603 refs.append(next_cred.get_refid())
605 next_cred = next_cred.parent
606 #print>>sys.stderr, " \r\n \r\n updateRefID next_cred "
609 #print>>sys.stderr, " \r\n \r\n updateRefID next_cred NONE"
612 # Find a unique refid for this credential
613 rid = self.get_refid()
616 rid = "ref%d" % (val + 1)
621 # Return the set of parent credential ref ids
630 # Sign the XML file created by encode()
633 # In general, a signed credential obtained externally should
634 # not be changed else the signature is no longer valid. So, once
635 # you have loaded an existing signed credential, do not call encode() or sign() on it.
638 if not self.issuer_privkey or not self.issuer_gid:
640 doc = parseString(self.get_xml())
641 sigs = doc.getElementsByTagName("signatures")[0]
643 # Create the signature template to be signed
644 signature = Signature()
645 signature.set_refid(self.get_refid())
646 sdoc = parseString(signature.get_xml())
647 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
648 sigs.appendChild(sig_ele)
650 self.xml = doc.toxml()
653 # Split the issuer GID into multiple certificates if it's a chain
654 chain = GID(filename=self.issuer_gid)
657 gid_files.append(chain.save_to_random_tmp_file(False))
658 if chain.get_parent():
659 chain = chain.get_parent()
664 # Call out to xmlsec1 to sign it
665 ref = 'Sig_%s' % self.get_refid()
666 filename = self.save_to_random_tmp_file()
667 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
668 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
671 for gid_file in gid_files:
676 # This is no longer a legacy credential
685 # Retrieve the attributes of the credential from the XML.
686 # This is automatically called by the various get_* methods of
687 # this class and should not need to be called explicitly.
692 doc = parseString(self.xml)
694 signed_cred = doc.getElementsByTagName("signed-credential")
696 # Is this a signed-cred or just a cred?
697 if len(signed_cred) > 0:
698 creds = signed_cred[0].getElementsByTagName("credential")
699 signatures = signed_cred[0].getElementsByTagName("signatures")
700 if len(signatures) > 0:
701 sigs = signatures[0].getElementsByTagName("Signature")
703 creds = doc.getElementsByTagName("credential")
705 if creds is None or len(creds) == 0:
706 # malformed cred file
707 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
709 # Just take the first cred if there are more than one
712 self.set_refid(cred.getAttribute("xml:id"))
713 self.set_expiration(utcparse(getTextNode(cred, "expires")))
714 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
715 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
719 privs = cred.getElementsByTagName("privileges")[0]
721 for priv in privs.getElementsByTagName("privilege"):
722 kind = getTextNode(priv, "name")
723 deleg = str2bool(getTextNode(priv, "can_delegate"))
725 # Convert * into the default privileges for the credential's type
726 # Each inherits the delegatability from the * above
727 _ , type = urn_to_hrn(self.gidObject.get_urn())
728 rl = determine_rights(type, self.gidObject.get_urn())
733 rlist.add(Right(kind.strip(), deleg))
734 self.set_privileges(rlist)
738 parent = cred.getElementsByTagName("parent")
740 parent_doc = parent[0].getElementsByTagName("credential")[0]
741 parent_xml = parent_doc.toxml()
742 self.parent = Credential(string=parent_xml)
745 # Assign the signatures to the credentials
747 Sig = Signature(string=sig.toxml())
749 for cur_cred in self.get_credential_list():
750 if cur_cred.get_refid() == Sig.get_refid():
751 cur_cred.set_signature(Sig)
756 # trusted_certs: A list of trusted GID filenames (not GID objects!)
757 # Chaining is not supported within the GIDs by xmlsec1.
759 # trusted_certs_required: Should usually be true. Set False means an
760 # empty list of trusted_certs would still let this method pass.
761 # It just skips xmlsec1 verification et al. Only used by some utils
764 # . All of the signatures are valid and that the issuers trace back
765 # to trusted roots (performed by xmlsec1)
766 # . The XML matches the credential schema
767 # . That the issuer of the credential is the authority in the target's urn
768 # . In the case of a delegated credential, this must be true of the root
769 # . That all of the gids presented in the credential are valid
770 # . Including verifying GID chains, and includ the issuer
771 # . The credential is not expired
773 # -- For Delegates (credentials with parents)
774 # . The privileges must be a subset of the parent credentials
775 # . The privileges must have "can_delegate" set for each delegated privilege
776 # . The target gid must be the same between child and parents
777 # . The expiry time on the child must be no later than the parent
778 # . The signer of the child must be the owner of the parent
780 # -- Verify does *NOT*
781 # . ensure that an xmlrpc client's gid matches a credential gid, that
782 # must be done elsewhere
784 # @param trusted_certs: The certificates of trusted CA certificates
785 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
789 # validate against RelaxNG schema
790 if HAVELXML and not self.legacy:
791 if schema and os.path.exists(schema):
792 tree = etree.parse(StringIO(self.xml))
793 schema_doc = etree.parse(schema)
794 xmlschema = etree.XMLSchema(schema_doc)
795 if not xmlschema.validate(tree):
796 error = xmlschema.error_log.last_error
797 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
798 raise CredentialNotVerifiable(message)
800 if trusted_certs_required and trusted_certs is None:
803 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
804 trusted_cert_objects = []
805 ok_trusted_certs = []
806 # If caller explicitly passed in None that means skip cert chain validation.
807 # Strange and not typical
808 if trusted_certs is not None:
809 for f in trusted_certs:
811 # Failures here include unreadable files
813 trusted_cert_objects.append(GID(filename=f))
814 #print>>sys.stderr, " \r\n \t\t\t credential.py verify trusted_certs %s" %(GID(filename=f).get_hrn())
815 ok_trusted_certs.append(f)
816 except Exception, exc:
817 logger.error("Failed to load trusted cert from %s: %r", f, exc)
818 trusted_certs = ok_trusted_certs
819 #print>>sys.stderr, " \r\n \t\t\t credential.py verify trusted_certs elemnebts %s" %(len(trusted_certs))
821 # Use legacy verification if this is a legacy credential
823 self.legacy.verify_chain(trusted_cert_objects)
824 if self.legacy.client_gid:
825 self.legacy.client_gid.verify_chain(trusted_cert_objects)
826 if self.legacy.object_gid:
827 self.legacy.object_gid.verify_chain(trusted_cert_objects)
830 # make sure it is not expired
831 if self.get_expiration() < datetime.datetime.utcnow():
832 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
834 # Verify the signatures
835 filename = self.save_to_random_tmp_file()
836 if trusted_certs is not None:
837 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
839 # If caller explicitly passed in None that means skip cert chain validation.
840 # - Strange and not typical
841 if trusted_certs is not None:
842 # Verify the gids of this cred and of its parents
843 for cur_cred in self.get_credential_list():
844 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
845 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
846 #print>>sys.stderr, " \r\n \t\t\t credential.py verify cur_cred get_gid_object hrn %s get_gid_caller %s" %(cur_cred.get_gid_object().get_hrn(),cur_cred.get_gid_caller().get_hrn())
849 refs.append("Sig_%s" % self.get_refid())
851 parentRefs = self.updateRefID()
852 for ref in parentRefs:
853 refs.append("Sig_%s" % ref)
854 #print>>sys.stderr, " \r\n \t\t\t credential.py verify trusted_certs refs", ref
856 # If caller explicitly passed in None that means skip xmlsec1 validation.
857 # Strange and not typical
858 if trusted_certs is None:
861 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
862 # (self.xmlsec_path, ref, cert_args, filename)
863 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
864 % (self.xmlsec_path, ref, cert_args, filename)).read()
865 #print>>sys.stderr, " \r\n \t\t\t credential.py verify filename %s verified %s " %(filename,verified)
866 if not verified.strip().startswith("OK"):
867 # xmlsec errors have a msg= which is the interesting bit.
868 mstart = verified.find("msg=")
870 if mstart > -1 and len(verified) > 4:
872 mend = verified.find('\\', mstart)
873 msg = verified[mstart:mend]
874 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
877 #print>>sys.stderr, " \r\n \t\t\t credential.py HUMMM parents %s", self.parent
878 # Verify the parents (delegation)
880 self.verify_parent(self.parent)
881 #print>>sys.stderr, " \r\n \t\t\t credential.py verify trusted_certs parents"
882 # Make sure the issuer is the target's authority, and is
884 self.verify_issuer(trusted_cert_objects)
888 # Creates a list of the credential and its parents, with the root
889 # (original delegated credential) as the last item in the list
890 def get_credential_list(self):
894 list.append(cur_cred)
896 cur_cred = cur_cred.parent
902 # Make sure the credential's target gid (a) was signed by or (b)
903 # is the same as the entity that signed the original credential,
904 # or (c) is an authority over the target's namespace.
905 # Also ensure that the credential issuer / signer itself has a valid
906 # GID signature chain (signed by an authority with namespace rights).
907 def verify_issuer(self, trusted_gids):
908 root_cred = self.get_credential_list()[-1]
909 root_target_gid = root_cred.get_gid_object()
910 root_cred_signer = root_cred.get_signature().get_issuer_gid()
913 # Allow non authority to sign target and cred about target.
915 # Why do we need to allow non authorities to sign?
916 # If in the target gid validation step we correctly
917 # checked that the target is only signed by an authority,
918 # then this is just a special case of case 3.
919 # This short-circuit is the common case currently -
920 # and cause GID validation doesn't check 'authority',
921 # this allows users to generate valid slice credentials.
922 if root_target_gid.is_signed_by_cert(root_cred_signer):
923 # cred signer matches target signer, return success
927 # Allow someone to sign credential about themeselves. Used?
928 # If not, remove this.
929 #root_target_gid_str = root_target_gid.save_to_string()
930 #root_cred_signer_str = root_cred_signer.save_to_string()
931 #if root_target_gid_str == root_cred_signer_str:
932 # # cred signer is target, return success
937 # root_cred_signer is not the target_gid
938 # So this is a different gid that we have not verified.
939 # xmlsec1 verified the cert chain on this already, but
940 # it hasn't verified that the gid meets the HRN namespace
942 # Below we'll ensure that it is an authority.
943 # But we haven't verified that it is _signed by_ an authority
944 # We also don't know if xmlsec1 requires that cert signers
947 # Note that if verify() gave us no trusted_gids then this
948 # call will fail. So skip it if we have no trusted_gids
949 if trusted_gids and len(trusted_gids) > 0:
950 root_cred_signer.verify_chain(trusted_gids)
952 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
954 # See if the signer is an authority over the domain of the target.
955 # There are multiple types of authority - accept them all here
956 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
957 root_cred_signer_type = root_cred_signer.get_type()
958 if (root_cred_signer_type.find('authority') == 0):
959 #logger.debug('Cred signer is an authority')
960 # signer is an authority, see if target is in authority's domain
961 signerhrn = root_cred_signer.get_hrn()
962 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
965 # We've required that the credential be signed by an authority
966 # for that domain. Reasonable and probably correct.
967 # A looser model would also allow the signer to be an authority
968 # in my control framework - eg My CA or CH. Even if it is not
969 # the CH that issued these, eg, user credentials.
971 # Give up, credential does not pass issuer verification
973 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()))
977 # -- For Delegates (credentials with parents) verify that:
978 # . The privileges must be a subset of the parent credentials
979 # . The privileges must have "can_delegate" set for each delegated privilege
980 # . The target gid must be the same between child and parents
981 # . The expiry time on the child must be no later than the parent
982 # . The signer of the child must be the owner of the parent
983 def verify_parent(self, parent_cred):
984 #print>>sys.stderr, " \r\n\r\n \t verify_parent parent_cred.get_gid_caller().save_to_string(False) %s self.get_signature().get_issuer_gid().save_to_string(False) %s" %(parent_cred.get_gid_caller().get_hrn(),self.get_signature().get_issuer_gid().get_hrn())
985 # make sure the rights given to the child are a subset of the
986 # parents rights (and check delegate bits)
987 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
988 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
989 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
990 self.get_privileges().save_to_string())
992 # make sure my target gid is the same as the parent's
993 if not parent_cred.get_gid_object().save_to_string() == \
994 self.get_gid_object().save_to_string():
995 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
997 # make sure my expiry time is <= my parent's
998 if not parent_cred.get_expiration() >= self.get_expiration():
999 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1001 # make sure my signer is the parent's caller
1002 if not parent_cred.get_gid_caller().save_to_string(False) == \
1003 self.get_signature().get_issuer_gid().save_to_string(False):
1004 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1007 if parent_cred.parent:
1008 parent_cred.verify_parent(parent_cred.parent)
1011 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1013 Return a delegated copy of this credential, delegated to the
1014 specified gid's user.
1016 # get the gid of the object we are delegating
1017 object_gid = self.get_gid_object()
1018 object_hrn = object_gid.get_hrn()
1020 # the hrn of the user who will be delegated to
1021 delegee_gid = GID(filename=delegee_gidfile)
1022 delegee_hrn = delegee_gid.get_hrn()
1024 #user_key = Keypair(filename=keyfile)
1025 #user_hrn = self.get_gid_caller().get_hrn()
1026 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1027 dcred = Credential(subject=subject_string)
1028 dcred.set_gid_caller(delegee_gid)
1029 dcred.set_gid_object(object_gid)
1030 dcred.set_parent(self)
1031 dcred.set_expiration(self.get_expiration())
1032 dcred.set_privileges(self.get_privileges())
1033 dcred.get_privileges().delegate_all_privileges(True)
1034 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1035 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1042 def get_filename(self):
1043 return getattr(self,'filename',None)
1046 # Dump the contents of a credential to stdout in human-readable format
1048 # @param dump_parents If true, also dump the parent certificates
1049 def dump (self, *args, **kwargs):
1050 print self.dump_string(*args, **kwargs)
1053 def dump_string(self, dump_parents=False):
1055 result += "CREDENTIAL %s\n" % self.get_subject()
1056 filename=self.get_filename()
1057 if filename: result += "Filename %s\n"%filename
1058 result += " privs: %s\n" % self.get_privileges().save_to_string()
1059 gidCaller = self.get_gid_caller()
1061 result += " gidCaller:\n"
1062 result += gidCaller.dump_string(8, dump_parents)
1064 if self.get_signature():
1066 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1068 gidObject = self.get_gid_object()
1070 result += " gidObject:\n"
1071 result += gidObject.dump_string(8, dump_parents)
1073 if self.parent and dump_parents:
1074 result += "\nPARENT"
1075 result += self.parent.dump_string(True)