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())
200 # A credential provides a caller gid with privileges to an object gid.
201 # A signed credential is signed by the object's authority.
203 # Credentials are encoded in one of two ways. The legacy style places
204 # it in the subjectAltName of an X509 certificate. The new credentials
205 # are placed in signed XML.
208 # In general, a signed credential obtained externally should
209 # not be changed else the signature is no longer valid. So, once
210 # you have loaded an existing signed credential, do not call encode() or sign() on it.
212 def filter_creds_by_caller(creds, caller_hrn_list):
214 Returns a list of creds who's gid caller matches the
217 if not isinstance(creds, list): creds = [creds]
218 if not isinstance(caller_hrn_list, list):
219 caller_hrn_list = [caller_hrn_list]
223 tmp_cred = Credential(string=cred)
224 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
225 caller_creds.append(cred)
229 class Credential(object):
232 # Create a Credential object
234 # @param create If true, create a blank x509 certificate
235 # @param subject If subject!=None, create an x509 cert with the subject name
236 # @param string If string!=None, load the credential from the string
237 # @param filename If filename!=None, load the credential from the file
238 # FIXME: create and subject are ignored!
239 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
240 self.gidCaller = None
241 self.gidObject = None
242 self.expiration = None
243 self.privileges = None
244 self.issuer_privkey = None
245 self.issuer_gid = None
246 self.issuer_pubkey = None
248 self.signature = None
256 if isinstance(cred, StringTypes):
258 self.type = 'geni_sfa'
260 elif isinstance(cred, dict):
261 string = cred['geni_value']
262 self.type = cred['geni_type']
263 self.version = cred['geni_version']
266 # Check if this is a legacy credential, translate it if so
267 if string or filename:
271 str = file(filename).read()
273 if str.strip().startswith("-----"):
274 self.legacy = CredentialLegacy(False,string=str)
275 self.translate_legacy(str)
280 # Find an xmlsec1 path
281 self.xmlsec_path = ''
282 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
284 if os.path.isfile(path + '/' + 'xmlsec1'):
285 self.xmlsec_path = path + '/' + 'xmlsec1'
288 def get_subject(self):
290 if not self.gidObject:
293 subject = self.gidObject.get_printable_subject()
296 # sounds like this should be __repr__ instead ??
297 def get_summary_tostring(self):
298 if not self.gidObject:
300 obj = self.gidObject.get_printable_subject()
301 caller = self.gidCaller.get_printable_subject()
302 exp = self.get_expiration()
303 # Summarize the rights too? The issuer?
304 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
306 def get_signature(self):
307 if not self.signature:
309 return self.signature
311 def set_signature(self, sig):
316 # Translate a legacy credential into a new one
318 # @param String of the legacy credential
320 def translate_legacy(self, str):
321 legacy = CredentialLegacy(False,string=str)
322 self.gidCaller = legacy.get_gid_caller()
323 self.gidObject = legacy.get_gid_object()
324 lifetime = legacy.get_lifetime()
326 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
328 self.set_expiration(int(lifetime))
329 self.lifeTime = legacy.get_lifetime()
330 self.set_privileges(legacy.get_privileges())
331 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
334 # Need the issuer's private key and name
335 # @param key Keypair object containing the private key of the issuer
336 # @param gid GID of the issuing authority
338 def set_issuer_keys(self, privkey, gid):
339 self.issuer_privkey = privkey
340 self.issuer_gid = gid
344 # Set this credential's parent
345 def set_parent(self, cred):
350 # set the GID of the caller
352 # @param gid GID object of the caller
354 def set_gid_caller(self, gid):
356 # gid origin caller is the caller's gid by default
357 self.gidOriginCaller = gid
360 # get the GID of the object
362 def get_gid_caller(self):
363 if not self.gidCaller:
365 return self.gidCaller
368 # set the GID of the object
370 # @param gid GID object of the object
372 def set_gid_object(self, gid):
376 # get the GID of the object
378 def get_gid_object(self):
379 if not self.gidObject:
381 return self.gidObject
384 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
386 def set_expiration(self, expiration):
387 if isinstance(expiration, (int, float)):
388 self.expiration = datetime.datetime.fromtimestamp(expiration)
389 elif isinstance (expiration, datetime.datetime):
390 self.expiration = expiration
391 elif isinstance (expiration, StringTypes):
392 self.expiration = utcparse (expiration)
394 logger.error ("unexpected input type in Credential.set_expiration")
398 # get the lifetime of the credential (always in datetime format)
400 def get_expiration(self):
401 if not self.expiration:
403 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
404 return self.expiration
408 def get_lifetime(self):
409 return self.get_expiration()
414 # @param privs either a comma-separated list of privileges of a Rights object
416 def set_privileges(self, privs):
417 if isinstance(privs, str):
418 self.privileges = Rights(string = privs)
420 self.privileges = privs
423 # return the privileges as a Rights object
425 def get_privileges(self):
426 if not self.privileges:
428 return self.privileges
431 # determine whether the credential allows a particular operation to be
434 # @param op_name string specifying name of operation ("lookup", "update", etc)
436 def can_perform(self, op_name):
437 rights = self.get_privileges()
442 return rights.can_perform(op_name)
446 # Encode the attributes of the credential into an XML string
447 # This should be done immediately before signing the credential.
449 # In general, a signed credential obtained externally should
450 # not be changed else the signature is no longer valid. So, once
451 # you have loaded an existing signed credential, do not call encode() or sign() on it.
454 # Create the XML document
456 signed_cred = doc.createElement("signed-credential")
459 # Note that credential/policy.xsd are really the PG schemas
461 # Note that delegation of credentials between the 2 only really works
462 # cause those schemas are identical.
463 # Also note these PG schemas talk about PG tickets and CM policies.
464 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
465 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
466 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")
468 # PG says for those last 2:
469 #signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
470 # 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")
472 doc.appendChild(signed_cred)
474 # Fill in the <credential> bit
475 cred = doc.createElement("credential")
476 cred.setAttribute("xml:id", self.get_refid())
477 signed_cred.appendChild(cred)
478 append_sub(doc, cred, "type", "privilege")
479 append_sub(doc, cred, "serial", "8")
480 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
481 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
482 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
483 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
484 append_sub(doc, cred, "uuid", "")
485 if not self.expiration:
486 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
487 self.expiration = self.expiration.replace(microsecond=0)
488 append_sub(doc, cred, "expires", self.expiration.isoformat())
489 privileges = doc.createElement("privileges")
490 cred.appendChild(privileges)
493 rights = self.get_privileges()
494 for right in rights.rights:
495 priv = doc.createElement("privilege")
496 append_sub(doc, priv, "name", right.kind)
497 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
498 privileges.appendChild(priv)
500 # Add the parent credential if it exists
502 sdoc = parseString(self.parent.get_xml())
503 # If the root node is a signed-credential (it should be), then
504 # get all its attributes and attach those to our signed_cred
506 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
507 # and we need to include those again here or else their signature
508 # no longer matches on the credential.
509 # We expect three of these, but here we copy them all:
510 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
511 # and from PG (PL is equivalent, as shown above):
512 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
513 # 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")
516 # PL now also declares these, with different URLs, so
517 # the code notices those attributes already existed with
518 # different values, and complains.
519 # This happens regularly on delegation now that PG and
520 # PL both declare the namespace with different URLs.
521 # If the content ever differs this is a problem,
522 # but for now it works - different URLs (values in the attributes)
523 # but the same actual schema, so using the PG schema
524 # on delegated-to-PL credentials works fine.
526 # Note: you could also not copy attributes
527 # which already exist. It appears that both PG and PL
528 # will actually validate a slicecred with a parent
529 # signed using PG namespaces and a child signed with PL
530 # namespaces over the whole thing. But I don't know
531 # if that is a bug in xmlsec1, an accident since
532 # the contents of the schemas are the same,
533 # or something else, but it seems odd. And this works.
534 parentRoot = sdoc.documentElement
535 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
536 for attrIx in range(0, parentRoot.attributes.length):
537 attr = parentRoot.attributes.item(attrIx)
538 # returns the old attribute of same name that was
540 # Below throws InUse exception if we forgot to clone the attribute first
541 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
542 if oldAttr and oldAttr.value != attr.value:
543 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)
545 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
547 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
548 p = doc.createElement("parent")
549 p.appendChild(p_cred)
551 # done handling parent credential
553 # Create the <signatures> tag
554 signatures = doc.createElement("signatures")
555 signed_cred.appendChild(signatures)
557 # Add any parent signatures
559 for cur_cred in self.get_credential_list()[1:]:
560 sdoc = parseString(cur_cred.get_signature().get_xml())
561 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
562 signatures.appendChild(ele)
564 # Get the finished product
565 self.xml = doc.toxml()
568 def save_to_random_tmp_file(self):
569 fp, filename = mkstemp(suffix='cred', text=True)
570 fp = os.fdopen(fp, "w")
571 self.save_to_file(filename, save_parents=True, filep=fp)
574 def save_to_file(self, filename, save_parents=True, filep=None):
580 f = open(filename, "w")
584 def save_to_string(self, save_parents=True):
594 def set_refid(self, rid):
598 # Figure out what refids exist, and update this credential's id
599 # so that it doesn't clobber the others. Returns the refids of
602 def updateRefID(self):
604 self.set_refid('ref0')
609 next_cred = self.parent
611 refs.append(next_cred.get_refid())
613 next_cred = next_cred.parent
618 # Find a unique refid for this credential
619 rid = self.get_refid()
622 rid = "ref%d" % (val + 1)
627 # Return the set of parent credential ref ids
636 # Sign the XML file created by encode()
639 # In general, a signed credential obtained externally should
640 # not be changed else the signature is no longer valid. So, once
641 # you have loaded an existing signed credential, do not call encode() or sign() on it.
644 if not self.issuer_privkey or not self.issuer_gid:
646 doc = parseString(self.get_xml())
647 sigs = doc.getElementsByTagName("signatures")[0]
649 # Create the signature template to be signed
650 signature = Signature()
651 signature.set_refid(self.get_refid())
652 sdoc = parseString(signature.get_xml())
653 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
654 sigs.appendChild(sig_ele)
656 self.xml = doc.toxml()
659 # Split the issuer GID into multiple certificates if it's a chain
660 chain = GID(filename=self.issuer_gid)
663 gid_files.append(chain.save_to_random_tmp_file(False))
664 if chain.get_parent():
665 chain = chain.get_parent()
670 # Call out to xmlsec1 to sign it
671 ref = 'Sig_%s' % self.get_refid()
672 filename = self.save_to_random_tmp_file()
673 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
674 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
677 for gid_file in gid_files:
682 # This is no longer a legacy credential
691 # Retrieve the attributes of the credential from the XML.
692 # This is automatically called by the various get_* methods of
693 # this class and should not need to be called explicitly.
701 doc = parseString(self.xml)
703 raise CredentialNotVerifiable("Malformed credential")
704 doc = parseString(self.xml)
706 signed_cred = doc.getElementsByTagName("signed-credential")
708 # Is this a signed-cred or just a cred?
709 if len(signed_cred) > 0:
710 creds = signed_cred[0].getElementsByTagName("credential")
711 signatures = signed_cred[0].getElementsByTagName("signatures")
712 if len(signatures) > 0:
713 sigs = signatures[0].getElementsByTagName("Signature")
715 creds = doc.getElementsByTagName("credential")
717 if creds is None or len(creds) == 0:
718 # malformed cred file
719 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
721 # Just take the first cred if there are more than one
724 self.set_refid(cred.getAttribute("xml:id"))
725 self.set_expiration(utcparse(getTextNode(cred, "expires")))
726 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
727 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
731 privs = cred.getElementsByTagName("privileges")[0]
733 for priv in privs.getElementsByTagName("privilege"):
734 kind = getTextNode(priv, "name")
735 deleg = str2bool(getTextNode(priv, "can_delegate"))
737 # Convert * into the default privileges for the credential's type
738 # Each inherits the delegatability from the * above
739 _ , type = urn_to_hrn(self.gidObject.get_urn())
740 rl = determine_rights(type, self.gidObject.get_urn())
745 rlist.add(Right(kind.strip(), deleg))
746 self.set_privileges(rlist)
750 parent = cred.getElementsByTagName("parent")
752 parent_doc = parent[0].getElementsByTagName("credential")[0]
753 parent_xml = parent_doc.toxml()
754 self.parent = Credential(string=parent_xml)
757 # Assign the signatures to the credentials
759 Sig = Signature(string=sig.toxml())
761 for cur_cred in self.get_credential_list():
762 if cur_cred.get_refid() == Sig.get_refid():
763 cur_cred.set_signature(Sig)
768 # trusted_certs: A list of trusted GID filenames (not GID objects!)
769 # Chaining is not supported within the GIDs by xmlsec1.
771 # trusted_certs_required: Should usually be true. Set False means an
772 # empty list of trusted_certs would still let this method pass.
773 # It just skips xmlsec1 verification et al. Only used by some utils
776 # . All of the signatures are valid and that the issuers trace back
777 # to trusted roots (performed by xmlsec1)
778 # . The XML matches the credential schema
779 # . That the issuer of the credential is the authority in the target's urn
780 # . In the case of a delegated credential, this must be true of the root
781 # . That all of the gids presented in the credential are valid
782 # . Including verifying GID chains, and includ the issuer
783 # . The credential is not expired
785 # -- For Delegates (credentials with parents)
786 # . The privileges must be a subset of the parent credentials
787 # . The privileges must have "can_delegate" set for each delegated privilege
788 # . The target gid must be the same between child and parents
789 # . The expiry time on the child must be no later than the parent
790 # . The signer of the child must be the owner of the parent
792 # -- Verify does *NOT*
793 # . ensure that an xmlrpc client's gid matches a credential gid, that
794 # must be done elsewhere
796 # @param trusted_certs: The certificates of trusted CA certificates
797 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
801 # validate against RelaxNG schema
802 if HAVELXML and not self.legacy:
803 if schema and os.path.exists(schema):
804 tree = etree.parse(StringIO(self.xml))
805 schema_doc = etree.parse(schema)
806 xmlschema = etree.XMLSchema(schema_doc)
807 if not xmlschema.validate(tree):
808 error = xmlschema.error_log.last_error
809 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
810 raise CredentialNotVerifiable(message)
812 if trusted_certs_required and trusted_certs is None:
815 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
816 trusted_cert_objects = []
817 ok_trusted_certs = []
818 # If caller explicitly passed in None that means skip cert chain validation.
819 # Strange and not typical
820 if trusted_certs is not None:
821 for f in trusted_certs:
823 # Failures here include unreadable files
825 trusted_cert_objects.append(GID(filename=f))
826 ok_trusted_certs.append(f)
827 except Exception, exc:
828 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
829 trusted_certs = ok_trusted_certs
831 # Use legacy verification if this is a legacy credential
833 self.legacy.verify_chain(trusted_cert_objects)
834 if self.legacy.client_gid:
835 self.legacy.client_gid.verify_chain(trusted_cert_objects)
836 if self.legacy.object_gid:
837 self.legacy.object_gid.verify_chain(trusted_cert_objects)
840 # make sure it is not expired
841 if self.get_expiration() < datetime.datetime.utcnow():
842 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
844 # Verify the signatures
845 filename = self.save_to_random_tmp_file()
846 if trusted_certs is not None:
847 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
849 # If caller explicitly passed in None that means skip cert chain validation.
850 # - Strange and not typical
851 if trusted_certs is not None:
852 # Verify the gids of this cred and of its parents
853 for cur_cred in self.get_credential_list():
854 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
855 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
858 refs.append("Sig_%s" % self.get_refid())
860 parentRefs = self.updateRefID()
861 for ref in parentRefs:
862 refs.append("Sig_%s" % ref)
865 # If caller explicitly passed in None that means skip xmlsec1 validation.
866 # Strange and not typical
867 if trusted_certs is None:
870 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
871 # (self.xmlsec_path, ref, cert_args, filename)
872 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
873 % (self.xmlsec_path, ref, cert_args, filename)).read()
874 if not verified.strip().startswith("OK"):
875 # xmlsec errors have a msg= which is the interesting bit.
876 mstart = verified.find("msg=")
878 if mstart > -1 and len(verified) > 4:
880 mend = verified.find('\\', mstart)
881 msg = verified[mstart:mend]
882 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
885 # Verify the parents (delegation)
887 self.verify_parent(self.parent)
889 # Make sure the issuer is the target's authority, and is
891 self.verify_issuer(trusted_cert_objects)
895 # Creates a list of the credential and its parents, with the root
896 # (original delegated credential) as the last item in the list
897 def get_credential_list(self):
901 list.append(cur_cred)
903 cur_cred = cur_cred.parent
909 # Make sure the credential's target gid (a) was signed by or (b)
910 # is the same as the entity that signed the original credential,
911 # or (c) is an authority over the target's namespace.
912 # Also ensure that the credential issuer / signer itself has a valid
913 # GID signature chain (signed by an authority with namespace rights).
914 def verify_issuer(self, trusted_gids):
915 root_cred = self.get_credential_list()[-1]
916 root_target_gid = root_cred.get_gid_object()
917 root_cred_signer = root_cred.get_signature().get_issuer_gid()
920 # Allow non authority to sign target and cred about target.
922 # Why do we need to allow non authorities to sign?
923 # If in the target gid validation step we correctly
924 # checked that the target is only signed by an authority,
925 # then this is just a special case of case 3.
926 # This short-circuit is the common case currently -
927 # and cause GID validation doesn't check 'authority',
928 # this allows users to generate valid slice credentials.
929 if root_target_gid.is_signed_by_cert(root_cred_signer):
930 # cred signer matches target signer, return success
934 # Allow someone to sign credential about themeselves. Used?
935 # If not, remove this.
936 #root_target_gid_str = root_target_gid.save_to_string()
937 #root_cred_signer_str = root_cred_signer.save_to_string()
938 #if root_target_gid_str == root_cred_signer_str:
939 # # cred signer is target, return success
944 # root_cred_signer is not the target_gid
945 # So this is a different gid that we have not verified.
946 # xmlsec1 verified the cert chain on this already, but
947 # it hasn't verified that the gid meets the HRN namespace
949 # Below we'll ensure that it is an authority.
950 # But we haven't verified that it is _signed by_ an authority
951 # We also don't know if xmlsec1 requires that cert signers
954 # Note that if verify() gave us no trusted_gids then this
955 # call will fail. So skip it if we have no trusted_gids
956 if trusted_gids and len(trusted_gids) > 0:
957 root_cred_signer.verify_chain(trusted_gids)
959 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
961 # See if the signer is an authority over the domain of the target.
962 # There are multiple types of authority - accept them all here
963 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
964 root_cred_signer_type = root_cred_signer.get_type()
965 if (root_cred_signer_type.find('authority') == 0):
966 #logger.debug('Cred signer is an authority')
967 # signer is an authority, see if target is in authority's domain
968 signerhrn = root_cred_signer.get_hrn()
969 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
972 # We've required that the credential be signed by an authority
973 # for that domain. Reasonable and probably correct.
974 # A looser model would also allow the signer to be an authority
975 # in my control framework - eg My CA or CH. Even if it is not
976 # the CH that issued these, eg, user credentials.
978 # Give up, credential does not pass issuer verification
980 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()))
984 # -- For Delegates (credentials with parents) verify that:
985 # . The privileges must be a subset of the parent credentials
986 # . The privileges must have "can_delegate" set for each delegated privilege
987 # . The target gid must be the same between child and parents
988 # . The expiry time on the child must be no later than the parent
989 # . The signer of the child must be the owner of the parent
990 def verify_parent(self, parent_cred):
991 # make sure the rights given to the child are a subset of the
992 # parents rights (and check delegate bits)
993 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
994 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
995 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
996 self.get_privileges().save_to_string())
998 # make sure my target gid is the same as the parent's
999 if not parent_cred.get_gid_object().save_to_string() == \
1000 self.get_gid_object().save_to_string():
1001 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1003 # make sure my expiry time is <= my parent's
1004 if not parent_cred.get_expiration() >= self.get_expiration():
1005 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1007 # make sure my signer is the parent's caller
1008 if not parent_cred.get_gid_caller().save_to_string(False) == \
1009 self.get_signature().get_issuer_gid().save_to_string(False):
1010 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
1013 if parent_cred.parent:
1014 parent_cred.verify_parent(parent_cred.parent)
1017 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1019 Return a delegated copy of this credential, delegated to the
1020 specified gid's user.
1022 # get the gid of the object we are delegating
1023 object_gid = self.get_gid_object()
1024 object_hrn = object_gid.get_hrn()
1026 # the hrn of the user who will be delegated to
1027 delegee_gid = GID(filename=delegee_gidfile)
1028 delegee_hrn = delegee_gid.get_hrn()
1030 #user_key = Keypair(filename=keyfile)
1031 #user_hrn = self.get_gid_caller().get_hrn()
1032 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1033 dcred = Credential(subject=subject_string)
1034 dcred.set_gid_caller(delegee_gid)
1035 dcred.set_gid_object(object_gid)
1036 dcred.set_parent(self)
1037 dcred.set_expiration(self.get_expiration())
1038 dcred.set_privileges(self.get_privileges())
1039 dcred.get_privileges().delegate_all_privileges(True)
1040 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1041 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1048 def get_filename(self):
1049 return getattr(self,'filename',None)
1051 def actual_caller_hrn (self):
1052 """a helper method used by some API calls like e.g. Allocate
1053 to try and find out who really is the original caller
1055 This admittedly is a bit of a hack, please USE IN LAST RESORT
1057 This code uses a heuristic to identify a delegated credential
1059 A first known restriction if for traffic that gets through a slice manager
1060 in this case the hrn reported is the one from the last SM in the call graph
1061 which is not at all what is meant here"""
1063 caller_hrn = self.get_gid_caller().get_hrn()
1064 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1065 subject_hrn = self.get_gid_object().get_hrn()
1066 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1067 # this seems to be a 'regular' credential
1068 if caller_hrn.startswith(issuer_hrn):
1069 actual_caller_hrn=caller_hrn
1070 # else this looks like a delegated credential, and the real caller is the issuer
1072 actual_caller_hrn=issuer_hrn
1073 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"%(caller_hrn,issuer_hrn,actual_caller_hrn))
1074 return actual_caller_hrn
1077 # Dump the contents of a credential to stdout in human-readable format
1079 # @param dump_parents If true, also dump the parent certificates
1080 def dump (self, *args, **kwargs):
1081 print self.dump_string(*args, **kwargs)
1083 # show_xml is ignored
1084 def dump_string(self, dump_parents=False, show_xml=None):
1086 result += "CREDENTIAL %s\n" % self.get_subject()
1087 filename=self.get_filename()
1088 if filename: result += "Filename %s\n"%filename
1089 privileges = self.get_privileges()
1091 result += " privs: %s\n" % privileges.save_to_string()
1093 result += " privs: \n"
1094 gidCaller = self.get_gid_caller()
1096 result += " gidCaller:\n"
1097 result += gidCaller.dump_string(8, dump_parents)
1099 if self.get_signature():
1101 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1104 print " expiration:", self.expiration.isoformat()
1106 gidObject = self.get_gid_object()
1108 result += " gidObject:\n"
1109 result += gidObject.dump_string(8, dump_parents)
1111 if self.parent and dump_parents:
1112 result += "\nPARENT"
1113 result += self.parent.dump_string(True)