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
31 from types import StringTypes
33 from StringIO import StringIO
34 from tempfile import mkstemp
35 from xml.dom.minidom import Document, parseString
39 from lxml import etree
44 from xml.parsers.expat import ExpatError
46 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
47 from sfa.util.sfalogging import logger
48 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
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 * 28
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.
204 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
205 # The new credentials 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
255 if isinstance(cred, StringTypes):
257 self.type = 'geni_sfa'
259 elif isinstance(cred, dict):
260 string = cred['geni_value']
261 self.type = cred['geni_type']
262 self.version = cred['geni_version']
265 if string or filename:
269 str = file(filename).read()
271 # if this is a legacy credential, write error and bail out
272 if isinstance (str, StringTypes) and str.strip().startswith("-----"):
273 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
279 # Find an xmlsec1 path
280 self.xmlsec_path = ''
281 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
283 if os.path.isfile(path + '/' + 'xmlsec1'):
284 self.xmlsec_path = path + '/' + 'xmlsec1'
287 def get_subject(self):
289 if not self.gidObject:
292 subject = self.gidObject.get_printable_subject()
295 # sounds like this should be __repr__ instead ??
296 def get_summary_tostring(self):
297 if not self.gidObject:
299 obj = self.gidObject.get_printable_subject()
300 caller = self.gidCaller.get_printable_subject()
301 exp = self.get_expiration()
302 # Summarize the rights too? The issuer?
303 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
305 def get_signature(self):
306 if not self.signature:
308 return self.signature
310 def set_signature(self, sig):
315 # Need the issuer's private key and name
316 # @param key Keypair object containing the private key of the issuer
317 # @param gid GID of the issuing authority
319 def set_issuer_keys(self, privkey, gid):
320 self.issuer_privkey = privkey
321 self.issuer_gid = gid
325 # Set this credential's parent
326 def set_parent(self, cred):
331 # set the GID of the caller
333 # @param gid GID object of the caller
335 def set_gid_caller(self, gid):
337 # gid origin caller is the caller's gid by default
338 self.gidOriginCaller = gid
341 # get the GID of the object
343 def get_gid_caller(self):
344 if not self.gidCaller:
346 return self.gidCaller
349 # set the GID of the object
351 # @param gid GID object of the object
353 def set_gid_object(self, gid):
357 # get the GID of the object
359 def get_gid_object(self):
360 if not self.gidObject:
362 return self.gidObject
365 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
367 def set_expiration(self, expiration):
368 expiration_datetime = utcparse (expiration)
369 if expiration_datetime is not None:
370 self.expiration = expiration_datetime
372 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
375 # get the lifetime of the credential (always in datetime format)
377 def get_expiration(self):
378 if not self.expiration:
380 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
381 return self.expiration
386 # @param privs either a comma-separated list of privileges of a Rights object
388 def set_privileges(self, privs):
389 if isinstance(privs, str):
390 self.privileges = Rights(string = privs)
392 self.privileges = privs
395 # return the privileges as a Rights object
397 def get_privileges(self):
398 if not self.privileges:
400 return self.privileges
403 # determine whether the credential allows a particular operation to be
406 # @param op_name string specifying name of operation ("lookup", "update", etc)
408 def can_perform(self, op_name):
409 rights = self.get_privileges()
414 return rights.can_perform(op_name)
418 # Encode the attributes of the credential into an XML string
419 # This should be done immediately before signing the credential.
421 # In general, a signed credential obtained externally should
422 # not be changed else the signature is no longer valid. So, once
423 # you have loaded an existing signed credential, do not call encode() or sign() on it.
426 # Create the XML document
428 signed_cred = doc.createElement("signed-credential")
431 # Note that credential/policy.xsd are really the PG schemas
433 # Note that delegation of credentials between the 2 only really works
434 # cause those schemas are identical.
435 # Also note these PG schemas talk about PG tickets and CM policies.
436 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
437 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
438 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")
440 # PG says for those last 2:
441 #signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
442 # 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")
444 doc.appendChild(signed_cred)
446 # Fill in the <credential> bit
447 cred = doc.createElement("credential")
448 cred.setAttribute("xml:id", self.get_refid())
449 signed_cred.appendChild(cred)
450 append_sub(doc, cred, "type", "privilege")
451 append_sub(doc, cred, "serial", "8")
452 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
453 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
454 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
455 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
456 append_sub(doc, cred, "uuid", "")
457 if not self.expiration:
458 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
459 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
460 self.expiration = self.expiration.replace(microsecond=0)
461 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
462 privileges = doc.createElement("privileges")
463 cred.appendChild(privileges)
466 rights = self.get_privileges()
467 for right in rights.rights:
468 priv = doc.createElement("privilege")
469 append_sub(doc, priv, "name", right.kind)
470 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
471 privileges.appendChild(priv)
473 # Add the parent credential if it exists
475 sdoc = parseString(self.parent.get_xml())
476 # If the root node is a signed-credential (it should be), then
477 # get all its attributes and attach those to our signed_cred
479 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
480 # and we need to include those again here or else their signature
481 # no longer matches on the credential.
482 # We expect three of these, but here we copy them all:
483 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
484 # and from PG (PL is equivalent, as shown above):
485 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
486 # 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")
489 # PL now also declares these, with different URLs, so
490 # the code notices those attributes already existed with
491 # different values, and complains.
492 # This happens regularly on delegation now that PG and
493 # PL both declare the namespace with different URLs.
494 # If the content ever differs this is a problem,
495 # but for now it works - different URLs (values in the attributes)
496 # but the same actual schema, so using the PG schema
497 # on delegated-to-PL credentials works fine.
499 # Note: you could also not copy attributes
500 # which already exist. It appears that both PG and PL
501 # will actually validate a slicecred with a parent
502 # signed using PG namespaces and a child signed with PL
503 # namespaces over the whole thing. But I don't know
504 # if that is a bug in xmlsec1, an accident since
505 # the contents of the schemas are the same,
506 # or something else, but it seems odd. And this works.
507 parentRoot = sdoc.documentElement
508 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
509 for attrIx in range(0, parentRoot.attributes.length):
510 attr = parentRoot.attributes.item(attrIx)
511 # returns the old attribute of same name that was
513 # Below throws InUse exception if we forgot to clone the attribute first
514 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
515 if oldAttr and oldAttr.value != attr.value:
516 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
517 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
519 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
521 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
522 p = doc.createElement("parent")
523 p.appendChild(p_cred)
525 # done handling parent credential
527 # Create the <signatures> tag
528 signatures = doc.createElement("signatures")
529 signed_cred.appendChild(signatures)
531 # Add any parent signatures
533 for cur_cred in self.get_credential_list()[1:]:
534 sdoc = parseString(cur_cred.get_signature().get_xml())
535 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
536 signatures.appendChild(ele)
538 # Get the finished product
539 self.xml = doc.toxml()
542 def save_to_random_tmp_file(self):
543 fp, filename = mkstemp(suffix='cred', text=True)
544 fp = os.fdopen(fp, "w")
545 self.save_to_file(filename, save_parents=True, filep=fp)
548 def save_to_file(self, filename, save_parents=True, filep=None):
554 f = open(filename, "w")
558 def save_to_string(self, save_parents=True):
568 def set_refid(self, rid):
572 # Figure out what refids exist, and update this credential's id
573 # so that it doesn't clobber the others. Returns the refids of
576 def updateRefID(self):
578 self.set_refid('ref0')
583 next_cred = self.parent
585 refs.append(next_cred.get_refid())
587 next_cred = next_cred.parent
592 # Find a unique refid for this credential
593 rid = self.get_refid()
596 rid = "ref%d" % (val + 1)
601 # Return the set of parent credential ref ids
610 # Sign the XML file created by encode()
613 # In general, a signed credential obtained externally should
614 # not be changed else the signature is no longer valid. So, once
615 # you have loaded an existing signed credential, do not call encode() or sign() on it.
618 if not self.issuer_privkey or not self.issuer_gid:
620 doc = parseString(self.get_xml())
621 sigs = doc.getElementsByTagName("signatures")[0]
623 # Create the signature template to be signed
624 signature = Signature()
625 signature.set_refid(self.get_refid())
626 sdoc = parseString(signature.get_xml())
627 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
628 sigs.appendChild(sig_ele)
630 self.xml = doc.toxml()
633 # Split the issuer GID into multiple certificates if it's a chain
634 chain = GID(filename=self.issuer_gid)
637 gid_files.append(chain.save_to_random_tmp_file(False))
638 if chain.get_parent():
639 chain = chain.get_parent()
644 # Call out to xmlsec1 to sign it
645 ref = 'Sig_%s' % self.get_refid()
646 filename = self.save_to_random_tmp_file()
647 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
648 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
651 for gid_file in gid_files:
661 # Retrieve the attributes of the credential from the XML.
662 # This is automatically called by the various get_* methods of
663 # this class and should not need to be called explicitly.
671 doc = parseString(self.xml)
673 raise CredentialNotVerifiable("Malformed credential")
674 doc = parseString(self.xml)
676 signed_cred = doc.getElementsByTagName("signed-credential")
678 # Is this a signed-cred or just a cred?
679 if len(signed_cred) > 0:
680 creds = signed_cred[0].getElementsByTagName("credential")
681 signatures = signed_cred[0].getElementsByTagName("signatures")
682 if len(signatures) > 0:
683 sigs = signatures[0].getElementsByTagName("Signature")
685 creds = doc.getElementsByTagName("credential")
687 if creds is None or len(creds) == 0:
688 # malformed cred file
689 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
691 # Just take the first cred if there are more than one
694 self.set_refid(cred.getAttribute("xml:id"))
695 self.set_expiration(utcparse(getTextNode(cred, "expires")))
696 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
697 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
701 privs = cred.getElementsByTagName("privileges")[0]
703 for priv in privs.getElementsByTagName("privilege"):
704 kind = getTextNode(priv, "name")
705 deleg = str2bool(getTextNode(priv, "can_delegate"))
707 # Convert * into the default privileges for the credential's type
708 # Each inherits the delegatability from the * above
709 _ , type = urn_to_hrn(self.gidObject.get_urn())
710 rl = determine_rights(type, self.gidObject.get_urn())
715 rlist.add(Right(kind.strip(), deleg))
716 self.set_privileges(rlist)
720 parent = cred.getElementsByTagName("parent")
722 parent_doc = parent[0].getElementsByTagName("credential")[0]
723 parent_xml = parent_doc.toxml()
724 self.parent = Credential(string=parent_xml)
727 # Assign the signatures to the credentials
729 Sig = Signature(string=sig.toxml())
731 for cur_cred in self.get_credential_list():
732 if cur_cred.get_refid() == Sig.get_refid():
733 cur_cred.set_signature(Sig)
738 # trusted_certs: A list of trusted GID filenames (not GID objects!)
739 # Chaining is not supported within the GIDs by xmlsec1.
741 # trusted_certs_required: Should usually be true. Set False means an
742 # empty list of trusted_certs would still let this method pass.
743 # It just skips xmlsec1 verification et al. Only used by some utils
746 # . All of the signatures are valid and that the issuers trace back
747 # to trusted roots (performed by xmlsec1)
748 # . The XML matches the credential schema
749 # . That the issuer of the credential is the authority in the target's urn
750 # . In the case of a delegated credential, this must be true of the root
751 # . That all of the gids presented in the credential are valid
752 # . Including verifying GID chains, and includ the issuer
753 # . The credential is not expired
755 # -- For Delegates (credentials with parents)
756 # . The privileges must be a subset of the parent credentials
757 # . The privileges must have "can_delegate" set for each delegated privilege
758 # . The target gid must be the same between child and parents
759 # . The expiry time on the child must be no later than the parent
760 # . The signer of the child must be the owner of the parent
762 # -- Verify does *NOT*
763 # . ensure that an xmlrpc client's gid matches a credential gid, that
764 # must be done elsewhere
766 # @param trusted_certs: The certificates of trusted CA certificates
767 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
771 # validate against RelaxNG schema
773 if schema and os.path.exists(schema):
774 tree = etree.parse(StringIO(self.xml))
775 schema_doc = etree.parse(schema)
776 xmlschema = etree.XMLSchema(schema_doc)
777 if not xmlschema.validate(tree):
778 error = xmlschema.error_log.last_error
779 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
780 raise CredentialNotVerifiable(message)
782 if trusted_certs_required and trusted_certs is None:
785 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
786 trusted_cert_objects = []
787 ok_trusted_certs = []
788 # If caller explicitly passed in None that means skip cert chain validation.
789 # Strange and not typical
790 if trusted_certs is not None:
791 for f in trusted_certs:
793 # Failures here include unreadable files
795 trusted_cert_objects.append(GID(filename=f))
796 ok_trusted_certs.append(f)
797 except Exception, exc:
798 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
799 trusted_certs = ok_trusted_certs
801 # make sure it is not expired
802 if self.get_expiration() < datetime.datetime.utcnow():
803 raise CredentialNotVerifiable("Credential %s expired at %s" % \
804 (self.get_summary_tostring(),
805 self.expiration.strftime(SFATIME_FORMAT)))
807 # Verify the signatures
808 filename = self.save_to_random_tmp_file()
810 # If caller explicitly passed in None that means skip cert chain validation.
811 # - Strange and not typical
812 if trusted_certs is not None:
813 # Verify the gids of this cred and of its parents
814 for cur_cred in self.get_credential_list():
815 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
816 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
819 refs.append("Sig_%s" % self.get_refid())
821 parentRefs = self.updateRefID()
822 for ref in parentRefs:
823 refs.append("Sig_%s" % ref)
826 # If caller explicitly passed in None that means skip xmlsec1 validation.
827 # Strange and not typical
828 if trusted_certs is None:
832 # up to fedora20 we used os.popen and checked that the output begins with OK
833 # turns out, with fedora21, there is extra input before this 'OK' thing
834 # looks like we're better off just using the exit code - that's what it is made for
835 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
836 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
837 # format(self.xmlsec_path, ref, cert_args, filename)
838 command = [ self.xmlsec_path, '--verify', '--node-id', ref ]
839 for trusted in trusted_certs:
840 command += ["--trusted-pem", trusted ]
841 command += [ filename ]
842 logger.debug("Running " + " ".join(command))
844 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
845 logger.debug("xmlsec command returned {}".format(verified))
846 if "OK\n" not in verified:
847 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
848 except subprocess.CalledProcessError as e:
850 # xmlsec errors have a msg= which is the interesting bit.
851 mstart = verified.find("msg=")
853 if mstart > -1 and len(verified) > 4:
855 mend = verified.find('\\', mstart)
856 msg = verified[mstart:mend]
857 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
858 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
859 (self.get_summary_tostring(), ref, msg))
862 # Verify the parents (delegation)
864 self.verify_parent(self.parent)
866 # Make sure the issuer is the target's authority, and is
868 self.verify_issuer(trusted_cert_objects)
872 # Creates a list of the credential and its parents, with the root
873 # (original delegated credential) as the last item in the list
874 def get_credential_list(self):
878 list.append(cur_cred)
880 cur_cred = cur_cred.parent
886 # Make sure the credential's target gid (a) was signed by or (b)
887 # is the same as the entity that signed the original credential,
888 # or (c) is an authority over the target's namespace.
889 # Also ensure that the credential issuer / signer itself has a valid
890 # GID signature chain (signed by an authority with namespace rights).
891 def verify_issuer(self, trusted_gids):
892 root_cred = self.get_credential_list()[-1]
893 root_target_gid = root_cred.get_gid_object()
894 root_cred_signer = root_cred.get_signature().get_issuer_gid()
897 # Allow non authority to sign target and cred about target.
899 # Why do we need to allow non authorities to sign?
900 # If in the target gid validation step we correctly
901 # checked that the target is only signed by an authority,
902 # then this is just a special case of case 3.
903 # This short-circuit is the common case currently -
904 # and cause GID validation doesn't check 'authority',
905 # this allows users to generate valid slice credentials.
906 if root_target_gid.is_signed_by_cert(root_cred_signer):
907 # cred signer matches target signer, return success
911 # Allow someone to sign credential about themeselves. Used?
912 # If not, remove this.
913 #root_target_gid_str = root_target_gid.save_to_string()
914 #root_cred_signer_str = root_cred_signer.save_to_string()
915 #if root_target_gid_str == root_cred_signer_str:
916 # # cred signer is target, return success
921 # root_cred_signer is not the target_gid
922 # So this is a different gid that we have not verified.
923 # xmlsec1 verified the cert chain on this already, but
924 # it hasn't verified that the gid meets the HRN namespace
926 # Below we'll ensure that it is an authority.
927 # But we haven't verified that it is _signed by_ an authority
928 # We also don't know if xmlsec1 requires that cert signers
931 # Note that if verify() gave us no trusted_gids then this
932 # call will fail. So skip it if we have no trusted_gids
933 if trusted_gids and len(trusted_gids) > 0:
934 root_cred_signer.verify_chain(trusted_gids)
936 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
938 # See if the signer is an authority over the domain of the target.
939 # There are multiple types of authority - accept them all here
940 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
941 root_cred_signer_type = root_cred_signer.get_type()
942 if (root_cred_signer_type.find('authority') == 0):
943 #logger.debug('Cred signer is an authority')
944 # signer is an authority, see if target is in authority's domain
945 signerhrn = root_cred_signer.get_hrn()
946 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
949 # We've required that the credential be signed by an authority
950 # for that domain. Reasonable and probably correct.
951 # A looser model would also allow the signer to be an authority
952 # in my control framework - eg My CA or CH. Even if it is not
953 # the CH that issued these, eg, user credentials.
955 # Give up, credential does not pass issuer verification
957 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred signer %s not the trusted authority for Cred target %s" % \
958 (self.gidCaller.get_urn(), self.gidObject.get_urn(), root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
962 # -- For Delegates (credentials with parents) verify that:
963 # . The privileges must be a subset of the parent credentials
964 # . The privileges must have "can_delegate" set for each delegated privilege
965 # . The target gid must be the same between child and parents
966 # . The expiry time on the child must be no later than the parent
967 # . The signer of the child must be the owner of the parent
968 def verify_parent(self, parent_cred):
969 # make sure the rights given to the child are a subset of the
970 # parents rights (and check delegate bits)
971 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
972 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
973 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % \
974 (self.get_summary_tostring(), self.get_refid())) +
975 self.get_privileges().save_to_string())
977 # make sure my target gid is the same as the parent's
978 if not parent_cred.get_gid_object().save_to_string() == \
979 self.get_gid_object().save_to_string():
980 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % \
981 (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
983 # make sure my expiry time is <= my parent's
984 if not parent_cred.get_expiration() >= self.get_expiration():
985 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % \
986 (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
988 # make sure my signer is the parent's caller
989 if not parent_cred.get_gid_caller().save_to_string(False) == \
990 self.get_signature().get_issuer_gid().save_to_string(False):
991 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % \
992 (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
995 if parent_cred.parent:
996 parent_cred.verify_parent(parent_cred.parent)
999 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1001 Return a delegated copy of this credential, delegated to the
1002 specified gid's user.
1004 # get the gid of the object we are delegating
1005 object_gid = self.get_gid_object()
1006 object_hrn = object_gid.get_hrn()
1008 # the hrn of the user who will be delegated to
1009 delegee_gid = GID(filename=delegee_gidfile)
1010 delegee_hrn = delegee_gid.get_hrn()
1012 #user_key = Keypair(filename=keyfile)
1013 #user_hrn = self.get_gid_caller().get_hrn()
1014 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1015 dcred = Credential(subject=subject_string)
1016 dcred.set_gid_caller(delegee_gid)
1017 dcred.set_gid_object(object_gid)
1018 dcred.set_parent(self)
1019 dcred.set_expiration(self.get_expiration())
1020 dcred.set_privileges(self.get_privileges())
1021 dcred.get_privileges().delegate_all_privileges(True)
1022 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1023 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1030 def get_filename(self):
1031 return getattr(self,'filename',None)
1033 def actual_caller_hrn (self):
1034 """a helper method used by some API calls like e.g. Allocate
1035 to try and find out who really is the original caller
1037 This admittedly is a bit of a hack, please USE IN LAST RESORT
1039 This code uses a heuristic to identify a delegated credential
1041 A first known restriction if for traffic that gets through a slice manager
1042 in this case the hrn reported is the one from the last SM in the call graph
1043 which is not at all what is meant here"""
1045 caller_hrn = self.get_gid_caller().get_hrn()
1046 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1047 subject_hrn = self.get_gid_object().get_hrn()
1048 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1049 # this seems to be a 'regular' credential
1050 if caller_hrn.startswith(issuer_hrn):
1051 actual_caller_hrn=caller_hrn
1052 # else this looks like a delegated credential, and the real caller is the issuer
1054 actual_caller_hrn=issuer_hrn
1055 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"%(caller_hrn,issuer_hrn,actual_caller_hrn))
1056 return actual_caller_hrn
1059 # Dump the contents of a credential to stdout in human-readable format
1061 # @param dump_parents If true, also dump the parent certificates
1062 def dump (self, *args, **kwargs):
1063 print self.dump_string(*args, **kwargs)
1065 # show_xml is ignored
1066 def dump_string(self, dump_parents=False, show_xml=None):
1068 result += "CREDENTIAL %s\n" % self.get_subject()
1069 filename=self.get_filename()
1070 if filename: result += "Filename %s\n"%filename
1071 privileges = self.get_privileges()
1073 result += " privs: %s\n" % privileges.save_to_string()
1075 result += " privs: \n"
1076 gidCaller = self.get_gid_caller()
1078 result += " gidCaller:\n"
1079 result += gidCaller.dump_string(8, dump_parents)
1081 if self.get_signature():
1083 self.get_signature().get_issuer_gid().dump(8, dump_parents)
1086 print " expiration:", self.expiration.strftime(SFATIME_FORMAT)
1088 gidObject = self.get_gid_object()
1090 result += " gidObject:\n"
1091 result += gidObject.dump_string(8, dump_parents)
1093 if self.parent and dump_parents:
1094 result += "\nPARENT"
1095 result += self.parent.dump_string(True)