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 * 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 ## This code until the end of function rewritten by Aaron Helsinger
191 ref_id = sig.getAttribute("xml:id").strip().strip("Sig_")
192 # The xml:id tag is optional, and could be in a
193 # Reference xml:id or Reference UID sub element instead
194 if not ref_id or ref_id == '':
195 reference = sig.getElementsByTagName('Reference')[0]
196 ref_id = reference.getAttribute('xml:id').strip().strip('Sig_')
197 if not ref_id or ref_id == '':
198 ref_id = reference.getAttribute('URI').strip().strip('#')
199 self.set_refid(ref_id)
200 keyinfos = sig.getElementsByTagName("X509Data")
202 for keyinfo in keyinfos:
203 certs = keyinfo.getElementsByTagName("X509Certificate")
205 if len(cert.childNodes) > 0:
206 szgid = cert.childNodes[0].nodeValue
207 szgid = szgid.strip()
208 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
214 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
215 self.set_issuer_gid(GID(string=gids))
218 self.xml = signature_template % (self.get_refid(), self.get_refid())
221 # A credential provides a caller gid with privileges to an object gid.
222 # A signed credential is signed by the object's authority.
224 # Credentials are encoded in one of two ways.
225 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
226 # The new credentials are placed in signed XML.
229 # In general, a signed credential obtained externally should
230 # not be changed else the signature is no longer valid. So, once
231 # you have loaded an existing signed credential, do not call encode() or sign() on it.
233 def filter_creds_by_caller(creds, caller_hrn_list):
235 Returns a list of creds who's gid caller matches the
238 if not isinstance(creds, list): creds = [creds]
239 if not isinstance(caller_hrn_list, list):
240 caller_hrn_list = [caller_hrn_list]
244 tmp_cred = Credential(string=cred)
245 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
247 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
248 caller_creds.append(cred)
252 class Credential(object):
254 SFA_CREDENTIAL_TYPE = "geni_sfa"
257 # Create a Credential object
259 # @param create If true, create a blank x509 certificate
260 # @param subject If subject!=None, create an x509 cert with the subject name
261 # @param string If string!=None, load the credential from the string
262 # @param filename If filename!=None, load the credential from the file
263 # FIXME: create and subject are ignored!
264 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
265 self.gidCaller = None
266 self.gidObject = None
267 self.expiration = None
268 self.privileges = None
269 self.issuer_privkey = None
270 self.issuer_gid = None
271 self.issuer_pubkey = None
273 self.signature = None
276 self.type = Credential.SFA_CREDENTIAL_TYPE
280 if isinstance(cred, StringTypes):
282 self.type = Credential.SFA_CREDENTIAL_TYPE
284 elif isinstance(cred, dict):
285 string = cred['geni_value']
286 self.type = cred['geni_type']
287 self.version = cred['geni_version']
289 if string or filename:
293 str = file(filename).read()
295 # if this is a legacy credential, write error and bail out
296 if isinstance (str, StringTypes) and str.strip().startswith("-----"):
297 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
303 # Find an xmlsec1 path
304 self.xmlsec_path = ''
305 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
307 if os.path.isfile(path + '/' + 'xmlsec1'):
308 self.xmlsec_path = path + '/' + 'xmlsec1'
310 if not self.xmlsec_path:
311 logger.warn("Could not locate binary for xmlsec1 - SFA will be unable to sign stuff !!")
313 def get_subject(self):
314 if not self.gidObject:
316 return self.gidObject.get_subject()
318 def pretty_subject(self):
320 if not self.gidObject:
323 subject = self.gidObject.pretty_cert()
326 # sounds like this should be __repr__ instead ??
327 def pretty_cred(self):
328 if not self.gidObject:
330 obj = self.gidObject.pretty_cert()
331 caller = self.gidCaller.pretty_cert()
332 exp = self.get_expiration()
333 # Summarize the rights too? The issuer?
334 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
336 def get_signature(self):
337 if not self.signature:
339 return self.signature
341 def set_signature(self, sig):
346 # Need the issuer's private key and name
347 # @param key Keypair object containing the private key of the issuer
348 # @param gid GID of the issuing authority
350 def set_issuer_keys(self, privkey, gid):
351 self.issuer_privkey = privkey
352 self.issuer_gid = gid
356 # Set this credential's parent
357 def set_parent(self, cred):
362 # set the GID of the caller
364 # @param gid GID object of the caller
366 def set_gid_caller(self, gid):
368 # gid origin caller is the caller's gid by default
369 self.gidOriginCaller = gid
372 # get the GID of the object
374 def get_gid_caller(self):
375 if not self.gidCaller:
377 return self.gidCaller
380 # set the GID of the object
382 # @param gid GID object of the object
384 def set_gid_object(self, gid):
388 # get the GID of the object
390 def get_gid_object(self):
391 if not self.gidObject:
393 return self.gidObject
396 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
398 def set_expiration(self, expiration):
399 expiration_datetime = utcparse (expiration)
400 if expiration_datetime is not None:
401 self.expiration = expiration_datetime
403 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
406 # get the lifetime of the credential (always in datetime format)
408 def get_expiration(self):
409 if not self.expiration:
411 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
412 return self.expiration
417 # @param privs either a comma-separated list of privileges of a Rights object
419 def set_privileges(self, privs):
420 if isinstance(privs, str):
421 self.privileges = Rights(string = privs)
423 self.privileges = privs
426 # return the privileges as a Rights object
428 def get_privileges(self):
429 if not self.privileges:
431 return self.privileges
434 # determine whether the credential allows a particular operation to be
437 # @param op_name string specifying name of operation ("lookup", "update", etc)
439 def can_perform(self, op_name):
440 rights = self.get_privileges()
445 return rights.can_perform(op_name)
449 # Encode the attributes of the credential into an XML string
450 # This should be done immediately before signing the credential.
452 # In general, a signed credential obtained externally should
453 # not be changed else the signature is no longer valid. So, once
454 # you have loaded an existing signed credential, do not call encode() or sign() on it.
457 # Create the XML document
459 signed_cred = doc.createElement("signed-credential")
462 # Note that credential/policy.xsd are really the PG schemas
464 # Note that delegation of credentials between the 2 only really works
465 # cause those schemas are identical.
466 # Also note these PG schemas talk about PG tickets and CM policies.
467 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
468 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
469 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
470 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")
472 # PG says for those last 2:
473 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
474 # 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")
476 doc.appendChild(signed_cred)
478 # Fill in the <credential> bit
479 cred = doc.createElement("credential")
480 cred.setAttribute("xml:id", self.get_refid())
481 signed_cred.appendChild(cred)
482 append_sub(doc, cred, "type", "privilege")
483 append_sub(doc, cred, "serial", "8")
484 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
485 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
486 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
487 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
488 append_sub(doc, cred, "uuid", "")
489 if not self.expiration:
490 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
491 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
492 self.expiration = self.expiration.replace(microsecond=0)
493 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
494 # TZ aware. Make sure it is UTC - by Aaron Helsinger
495 self.expiration = self.expiration.astimezone(tz.tzutc())
496 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
497 privileges = doc.createElement("privileges")
498 cred.appendChild(privileges)
501 rights = self.get_privileges()
502 for right in rights.rights:
503 priv = doc.createElement("privilege")
504 append_sub(doc, priv, "name", right.kind)
505 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
506 privileges.appendChild(priv)
508 # Add the parent credential if it exists
510 sdoc = parseString(self.parent.get_xml())
511 # If the root node is a signed-credential (it should be), then
512 # get all its attributes and attach those to our signed_cred
514 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
515 # and we need to include those again here or else their signature
516 # no longer matches on the credential.
517 # We expect three of these, but here we copy them all:
518 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
519 # and from PG (PL is equivalent, as shown above):
520 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
521 # 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")
524 # PL now also declares these, with different URLs, so
525 # the code notices those attributes already existed with
526 # different values, and complains.
527 # This happens regularly on delegation now that PG and
528 # PL both declare the namespace with different URLs.
529 # If the content ever differs this is a problem,
530 # but for now it works - different URLs (values in the attributes)
531 # but the same actual schema, so using the PG schema
532 # on delegated-to-PL credentials works fine.
534 # Note: you could also not copy attributes
535 # which already exist. It appears that both PG and PL
536 # will actually validate a slicecred with a parent
537 # signed using PG namespaces and a child signed with PL
538 # namespaces over the whole thing. But I don't know
539 # if that is a bug in xmlsec1, an accident since
540 # the contents of the schemas are the same,
541 # or something else, but it seems odd. And this works.
542 parentRoot = sdoc.documentElement
543 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
544 for attrIx in range(0, parentRoot.attributes.length):
545 attr = parentRoot.attributes.item(attrIx)
546 # returns the old attribute of same name that was
548 # Below throws InUse exception if we forgot to clone the attribute first
549 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
550 if oldAttr and oldAttr.value != attr.value:
551 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
552 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
554 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
556 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
557 p = doc.createElement("parent")
558 p.appendChild(p_cred)
560 # done handling parent credential
562 # Create the <signatures> tag
563 signatures = doc.createElement("signatures")
564 signed_cred.appendChild(signatures)
566 # Add any parent signatures
568 for cur_cred in self.get_credential_list()[1:]:
569 sdoc = parseString(cur_cred.get_signature().get_xml())
570 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
571 signatures.appendChild(ele)
573 # Get the finished product
574 self.xml = doc.toxml("utf-8")
577 def save_to_random_tmp_file(self):
578 fp, filename = mkstemp(suffix='cred', text=True)
579 fp = os.fdopen(fp, "w")
580 self.save_to_file(filename, save_parents=True, filep=fp)
583 def save_to_file(self, filename, save_parents=True, filep=None):
589 f = open(filename, "w")
593 def save_to_string(self, save_parents=True):
603 def set_refid(self, rid):
607 # Figure out what refids exist, and update this credential's id
608 # so that it doesn't clobber the others. Returns the refids of
611 def updateRefID(self):
613 self.set_refid('ref0')
618 next_cred = self.parent
620 refs.append(next_cred.get_refid())
622 next_cred = next_cred.parent
627 # Find a unique refid for this credential
628 rid = self.get_refid()
631 rid = "ref%d" % (val + 1)
636 # Return the set of parent credential ref ids
645 # Sign the XML file created by encode()
648 # In general, a signed credential obtained externally should
649 # not be changed else the signature is no longer valid. So, once
650 # you have loaded an existing signed credential, do not call encode() or sign() on it.
653 if not self.issuer_privkey:
654 logger.warn("Cannot sign credential (no private key)")
656 if not self.issuer_gid:
657 logger.warn("Cannot sign credential (no issuer gid)")
659 doc = parseString(self.get_xml())
660 sigs = doc.getElementsByTagName("signatures")[0]
662 # Create the signature template to be signed
663 signature = Signature()
664 signature.set_refid(self.get_refid())
665 sdoc = parseString(signature.get_xml())
666 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
667 sigs.appendChild(sig_ele)
669 self.xml = doc.toxml("utf-8")
672 # Split the issuer GID into multiple certificates if it's a chain
673 chain = GID(filename=self.issuer_gid)
676 gid_files.append(chain.save_to_random_tmp_file(False))
677 if chain.get_parent():
678 chain = chain.get_parent()
683 # Call out to xmlsec1 to sign it
684 ref = 'Sig_%s' % self.get_refid()
685 filename = self.save_to_random_tmp_file()
686 command='%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
687 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)
688 # print 'command',command
689 signed = os.popen(command).read()
692 for gid_file in gid_files:
702 # Retrieve the attributes of the credential from the XML.
703 # This is automatically called by the various get_* methods of
704 # this class and should not need to be called explicitly.
712 doc = parseString(self.xml)
714 raise CredentialNotVerifiable("Malformed credential")
715 doc = parseString(self.xml)
717 signed_cred = doc.getElementsByTagName("signed-credential")
719 # Is this a signed-cred or just a cred?
720 if len(signed_cred) > 0:
721 creds = signed_cred[0].getElementsByTagName("credential")
722 signatures = signed_cred[0].getElementsByTagName("signatures")
723 if len(signatures) > 0:
724 sigs = signatures[0].getElementsByTagName("Signature")
726 creds = doc.getElementsByTagName("credential")
728 if creds is None or len(creds) == 0:
729 # malformed cred file
730 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
732 # Just take the first cred if there are more than one
735 self.set_refid(cred.getAttribute("xml:id"))
736 self.set_expiration(utcparse(getTextNode(cred, "expires")))
737 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
738 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
741 ## This code until the end of function rewritten by Aaron Helsinger
744 priv_nodes = cred.getElementsByTagName("privileges")
745 if len(priv_nodes) > 0:
746 privs = priv_nodes[0]
747 for priv in privs.getElementsByTagName("privilege"):
748 kind = getTextNode(priv, "name")
749 deleg = str2bool(getTextNode(priv, "can_delegate"))
751 # Convert * into the default privileges for the credential's type
752 # Each inherits the delegatability from the * above
753 _ , type = urn_to_hrn(self.gidObject.get_urn())
754 rl = determine_rights(type, self.gidObject.get_urn())
759 rlist.add(Right(kind.strip(), deleg))
760 self.set_privileges(rlist)
764 parent = cred.getElementsByTagName("parent")
766 parent_doc = parent[0].getElementsByTagName("credential")[0]
767 parent_xml = parent_doc.toxml("utf-8")
768 if parent_xml is None or parent_xml.strip() == "":
769 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
770 self.parent = Credential(string=parent_xml)
773 # Assign the signatures to the credentials
775 Sig = Signature(string=sig.toxml("utf-8"))
777 for cur_cred in self.get_credential_list():
778 if cur_cred.get_refid() == Sig.get_refid():
779 cur_cred.set_signature(Sig)
784 # trusted_certs: A list of trusted GID filenames (not GID objects!)
785 # Chaining is not supported within the GIDs by xmlsec1.
787 # trusted_certs_required: Should usually be true. Set False means an
788 # empty list of trusted_certs would still let this method pass.
789 # It just skips xmlsec1 verification et al. Only used by some utils
792 # . All of the signatures are valid and that the issuers trace back
793 # to trusted roots (performed by xmlsec1)
794 # . The XML matches the credential schema
795 # . That the issuer of the credential is the authority in the target's urn
796 # . In the case of a delegated credential, this must be true of the root
797 # . That all of the gids presented in the credential are valid
798 # . Including verifying GID chains, and includ the issuer
799 # . The credential is not expired
801 # -- For Delegates (credentials with parents)
802 # . The privileges must be a subset of the parent credentials
803 # . The privileges must have "can_delegate" set for each delegated privilege
804 # . The target gid must be the same between child and parents
805 # . The expiry time on the child must be no later than the parent
806 # . The signer of the child must be the owner of the parent
808 # -- Verify does *NOT*
809 # . ensure that an xmlrpc client's gid matches a credential gid, that
810 # must be done elsewhere
812 # @param trusted_certs: The certificates of trusted CA certificates
813 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
817 # validate against RelaxNG schema
819 if schema and os.path.exists(schema):
820 tree = etree.parse(StringIO(self.xml))
821 schema_doc = etree.parse(schema)
822 xmlschema = etree.XMLSchema(schema_doc)
823 if not xmlschema.validate(tree):
824 error = xmlschema.error_log.last_error
825 message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
826 raise CredentialNotVerifiable(message)
828 if trusted_certs_required and trusted_certs is None:
831 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
832 trusted_cert_objects = []
833 ok_trusted_certs = []
834 # If caller explicitly passed in None that means skip cert chain validation.
835 # Strange and not typical
836 if trusted_certs is not None:
837 for f in trusted_certs:
839 # Failures here include unreadable files
841 trusted_cert_objects.append(GID(filename=f))
842 ok_trusted_certs.append(f)
843 except Exception, exc:
844 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
845 trusted_certs = ok_trusted_certs
847 # make sure it is not expired
848 if self.get_expiration() < datetime.datetime.utcnow():
849 raise CredentialNotVerifiable("Credential %s expired at %s" % \
851 self.expiration.strftime(SFATIME_FORMAT)))
853 # Verify the signatures
854 filename = self.save_to_random_tmp_file()
856 # If caller explicitly passed in None that means skip cert chain validation.
857 # - Strange and not typical
858 if trusted_certs is not None:
859 # Verify the gids of this cred and of its parents
860 for cur_cred in self.get_credential_list():
861 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
862 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
865 refs.append("Sig_%s" % self.get_refid())
867 parentRefs = self.updateRefID()
868 for ref in parentRefs:
869 refs.append("Sig_%s" % ref)
872 # If caller explicitly passed in None that means skip xmlsec1 validation.
873 # Strange and not typical
874 if trusted_certs is None:
878 # up to fedora20 we used os.popen and checked that the output begins with OK
879 # turns out, with fedora21, there is extra input before this 'OK' thing
880 # looks like we're better off just using the exit code - that's what it is made for
881 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
882 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
883 # format(self.xmlsec_path, ref, cert_args, filename)
884 command = [ self.xmlsec_path, '--verify', '--node-id', ref ]
885 for trusted in trusted_certs:
886 command += ["--trusted-pem", trusted ]
887 command += [ filename ]
888 logger.debug("Running " + " ".join(command))
890 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
891 logger.debug("xmlsec command returned {}".format(verified))
892 if "OK\n" not in verified:
893 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
894 except subprocess.CalledProcessError as e:
896 # xmlsec errors have a msg= which is the interesting bit.
897 mstart = verified.find("msg=")
899 if mstart > -1 and len(verified) > 4:
901 mend = verified.find('\\', mstart)
902 msg = verified[mstart:mend]
903 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
904 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
905 (self.pretty_cred(), ref, msg))
908 # Verify the parents (delegation)
910 self.verify_parent(self.parent)
912 # Make sure the issuer is the target's authority, and is
914 self.verify_issuer(trusted_cert_objects)
918 # Creates a list of the credential and its parents, with the root
919 # (original delegated credential) as the last item in the list
920 def get_credential_list(self):
924 list.append(cur_cred)
926 cur_cred = cur_cred.parent
932 # Make sure the credential's target gid (a) was signed by or (b)
933 # is the same as the entity that signed the original credential,
934 # or (c) is an authority over the target's namespace.
935 # Also ensure that the credential issuer / signer itself has a valid
936 # GID signature chain (signed by an authority with namespace rights).
937 def verify_issuer(self, trusted_gids):
938 root_cred = self.get_credential_list()[-1]
939 root_target_gid = root_cred.get_gid_object()
940 if root_cred.get_signature() is None:
942 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
944 root_cred_signer = root_cred.get_signature().get_issuer_gid()
947 # Allow non authority to sign target and cred about target.
949 # Why do we need to allow non authorities to sign?
950 # If in the target gid validation step we correctly
951 # checked that the target is only signed by an authority,
952 # then this is just a special case of case 3.
953 # This short-circuit is the common case currently -
954 # and cause GID validation doesn't check 'authority',
955 # this allows users to generate valid slice credentials.
956 if root_target_gid.is_signed_by_cert(root_cred_signer):
957 # cred signer matches target signer, return success
961 # Allow someone to sign credential about themeselves. Used?
962 # If not, remove this.
963 #root_target_gid_str = root_target_gid.save_to_string()
964 #root_cred_signer_str = root_cred_signer.save_to_string()
965 #if root_target_gid_str == root_cred_signer_str:
966 # # cred signer is target, return success
971 # root_cred_signer is not the target_gid
972 # So this is a different gid that we have not verified.
973 # xmlsec1 verified the cert chain on this already, but
974 # it hasn't verified that the gid meets the HRN namespace
976 # Below we'll ensure that it is an authority.
977 # But we haven't verified that it is _signed by_ an authority
978 # We also don't know if xmlsec1 requires that cert signers
981 # Note that if verify() gave us no trusted_gids then this
982 # call will fail. So skip it if we have no trusted_gids
983 if trusted_gids and len(trusted_gids) > 0:
984 root_cred_signer.verify_chain(trusted_gids)
986 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
987 "No trusted gids. Skipping that check.")
989 # See if the signer is an authority over the domain of the target.
990 # There are multiple types of authority - accept them all here
991 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
992 root_cred_signer_type = root_cred_signer.get_type()
993 if root_cred_signer_type.find('authority') == 0:
994 #logger.debug('Cred signer is an authority')
995 # signer is an authority, see if target is in authority's domain
996 signerhrn = root_cred_signer.get_hrn()
997 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1000 # We've required that the credential be signed by an authority
1001 # for that domain. Reasonable and probably correct.
1002 # A looser model would also allow the signer to be an authority
1003 # in my control framework - eg My CA or CH. Even if it is not
1004 # the CH that issued these, eg, user credentials.
1006 # Give up, credential does not pass issuer verification
1008 raise CredentialNotVerifiable(
1009 "Could not verify credential owned by {} for object {}. "
1010 "Cred signer {} not the trusted authority for Cred target {}"
1011 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1012 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1015 # -- For Delegates (credentials with parents) verify that:
1016 # . The privileges must be a subset of the parent credentials
1017 # . The privileges must have "can_delegate" set for each delegated privilege
1018 # . The target gid must be the same between child and parents
1019 # . The expiry time on the child must be no later than the parent
1020 # . The signer of the child must be the owner of the parent
1021 def verify_parent(self, parent_cred):
1022 # make sure the rights given to the child are a subset of the
1023 # parents rights (and check delegate bits)
1024 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1026 "Parent cred {} (ref {}) rights {} "
1027 " not superset of delegated cred {} (ref {}) rights {}"
1028 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1029 parent_cred.get_privileges().pretty_rights(),
1030 self.pretty_cred(), self.get_refid(),
1031 self.get_privileges().pretty_rights()))
1032 logger.error(message)
1033 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1034 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1035 raise ChildRightsNotSubsetOfParent(message)
1037 # make sure my target gid is the same as the parent's
1038 if not parent_cred.get_gid_object().save_to_string() == \
1039 self.get_gid_object().save_to_string():
1041 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1042 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1043 logger.error(message)
1044 logger.error("parent details {}".format(parent_cred.save_to_string()))
1045 logger.error("self details {}".format(self.save_to_string()))
1046 raise CredentialNotVerifiable(message)
1048 # make sure my expiry time is <= my parent's
1049 if not parent_cred.get_expiration() >= self.get_expiration():
1050 raise CredentialNotVerifiable(
1051 "Delegated credential {} expires after parent {}"
1052 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1054 # make sure my signer is the parent's caller
1055 if not parent_cred.get_gid_caller().save_to_string(False) == \
1056 self.get_signature().get_issuer_gid().save_to_string(False):
1057 message = "Delegated credential {} not signed by parent {}'s caller"\
1058 .format(self.pretty_cred(), parent_cred.pretty_cred())
1059 logger.error(message)
1060 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1061 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1062 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1063 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1064 raise CredentialNotVerifiable(message)
1067 if parent_cred.parent:
1068 parent_cred.verify_parent(parent_cred.parent)
1071 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1073 Return a delegated copy of this credential, delegated to the
1074 specified gid's user.
1076 # get the gid of the object we are delegating
1077 object_gid = self.get_gid_object()
1078 object_hrn = object_gid.get_hrn()
1080 # the hrn of the user who will be delegated to
1081 delegee_gid = GID(filename=delegee_gidfile)
1082 delegee_hrn = delegee_gid.get_hrn()
1084 #user_key = Keypair(filename=keyfile)
1085 #user_hrn = self.get_gid_caller().get_hrn()
1086 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1087 dcred = Credential(subject=subject_string)
1088 dcred.set_gid_caller(delegee_gid)
1089 dcred.set_gid_object(object_gid)
1090 dcred.set_parent(self)
1091 dcred.set_expiration(self.get_expiration())
1092 dcred.set_privileges(self.get_privileges())
1093 dcred.get_privileges().delegate_all_privileges(True)
1094 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1095 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1102 def get_filename(self):
1103 return getattr(self,'filename',None)
1105 def actual_caller_hrn (self):
1106 """a helper method used by some API calls like e.g. Allocate
1107 to try and find out who really is the original caller
1109 This admittedly is a bit of a hack, please USE IN LAST RESORT
1111 This code uses a heuristic to identify a delegated credential
1113 A first known restriction if for traffic that gets through a slice manager
1114 in this case the hrn reported is the one from the last SM in the call graph
1115 which is not at all what is meant here"""
1117 caller_hrn = self.get_gid_caller().get_hrn()
1118 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1119 subject_hrn = self.get_gid_object().get_hrn()
1120 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1121 # this seems to be a 'regular' credential
1122 if caller_hrn.startswith(issuer_hrn):
1123 actual_caller_hrn=caller_hrn
1124 # else this looks like a delegated credential, and the real caller is the issuer
1126 actual_caller_hrn=issuer_hrn
1127 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1128 %(caller_hrn,issuer_hrn,actual_caller_hrn))
1129 return actual_caller_hrn
1132 # Dump the contents of a credential to stdout in human-readable format
1134 # @param dump_parents If true, also dump the parent certificates
1135 def dump (self, *args, **kwargs):
1136 print self.dump_string(*args, **kwargs)
1138 # SFA code ignores show_xml and disables printing the cred xml
1139 def dump_string(self, dump_parents=False, show_xml=False):
1141 result += "CREDENTIAL %s\n" % self.pretty_subject()
1142 filename=self.get_filename()
1143 if filename: result += "Filename %s\n"%filename
1144 privileges = self.get_privileges()
1146 result += " privs: %s\n" % privileges.save_to_string()
1148 result += " privs: \n"
1149 gidCaller = self.get_gid_caller()
1151 result += " gidCaller:\n"
1152 result += gidCaller.dump_string(8, dump_parents)
1154 if self.get_signature():
1155 result += " gidIssuer:\n"
1156 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1159 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1161 gidObject = self.get_gid_object()
1163 result += " gidObject:\n"
1164 result += gidObject.dump_string(8, dump_parents)
1166 if self.parent and dump_parents:
1167 result += "\nPARENT"
1168 result += self.parent.dump_string(True)
1170 if show_xml and HAVELXML:
1172 tree = etree.parse(StringIO(self.xml))
1173 aside = etree.tostring(tree, pretty_print=True)
1174 result += "\nXML:\n\n"
1176 result += "\nEnd XML\n"
1179 print "exc. Credential.dump_string / XML"
1180 traceback.print_exc()