From 93c4c358fd8b0cd49cb2ece9c58737b883790482 Mon Sep 17 00:00:00 2001 From: Tony Mack Date: Wed, 21 May 2014 22:00:57 -0400 Subject: [PATCH] fix speaks for auth --- sfa/trust/abac_credential.py | 278 ++++ sfa/trust/auth.py | 46 +- sfa/trust/credential.py | 2232 ++++++++++++++++--------------- sfa/trust/credential.xsd | 504 ++++--- sfa/trust/credential_factory.py | 110 ++ sfa/trust/speaksfor_util.py | 466 +++++++ 6 files changed, 2309 insertions(+), 1327 deletions(-) create mode 100644 sfa/trust/abac_credential.py create mode 100644 sfa/trust/credential_factory.py create mode 100644 sfa/trust/speaksfor_util.py diff --git a/sfa/trust/abac_credential.py b/sfa/trust/abac_credential.py new file mode 100644 index 00000000..8f957ecb --- /dev/null +++ b/sfa/trust/abac_credential.py @@ -0,0 +1,278 @@ +#---------------------------------------------------------------------- +# Copyright (c) 2014 Raytheon BBN Technologies +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and/or hardware specification (the "Work") to +# deal in the Work without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Work, and to permit persons to whom the Work +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Work. +# +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# IN THE WORK. +#---------------------------------------------------------------------- + +from sfa.trust.credential import Credential, append_sub +from sfa.util.sfalogging import logger + +from StringIO import StringIO +from xml.dom.minidom import Document, parseString + +HAVELXML = False +try: + from lxml import etree + HAVELXML = True +except: + pass + +# This module defines a subtype of sfa.trust,credential.Credential +# called an ABACCredential. An ABAC credential is a signed statement +# asserting a role representing the relationship between a subject and target +# or between a subject and a class of targets (all those satisfying a role). +# +# An ABAC credential is like a normal SFA credential in that it has +# a validated signature block and is checked for expiration. +# It does not, however, have 'privileges'. Rather it contains a 'head' and +# list of 'tails' of elements, each of which represents a principal and +# role. + +# A special case of an ABAC credential is a speaks_for credential. Such +# a credential is simply an ABAC credential in form, but has a single +# tail and fixed role 'speaks_for'. In ABAC notation, it asserts +# AGENT.speaks_for(AGENT)<-CLIENT, or "AGENT asserts that CLIENT may speak +# for AGENT". The AGENT in this case is the head and the CLIENT is the +# tail and 'speaks_for_AGENT' is the role on the head. These speaks-for +# Credentials are used to allow a tool to 'speak as' itself but be recognized +# as speaking for an individual and be authorized to the rights of that +# individual and not to the rights of the tool itself. + +# For more detail on the semantics and syntax and expected usage patterns +# of ABAC credentials, see http://groups.geni.net/geni/wiki/TIEDABACCredential. + + +# An ABAC element contains a principal (keyid and optional mnemonic) +# and optional role and linking_role element +class ABACElement: + def __init__(self, principal_keyid, principal_mnemonic=None, \ + role=None, linking_role=None): + self._principal_keyid = principal_keyid + self._principal_mnemonic = principal_mnemonic + self._role = role + self._linking_role = linking_role + + def get_principal_keyid(self): return self._principal_keyid + def get_principal_mnemonic(self): return self._principal_mnemonic + def get_role(self): return self._role + def get_linking_role(self): return self._linking_role + + def __str__(self): + ret = self._principal_keyid + if self._principal_mnemonic: + ret = "%s (%s)" % (self._principal_mnemonic, self._principal_keyid) + if self._linking_role: + ret += ".%s" % self._linking_role + if self._role: + ret += ".%s" % self._role + return ret + +# Subclass of Credential for handling ABAC credentials +# They have a different cred_type (geni_abac vs. geni_sfa) +# and they have a head and tail and role (as opposed to privileges) +class ABACCredential(Credential): + + ABAC_CREDENTIAL_TYPE = 'geni_abac' + + def __init__(self, create=False, subject=None, + string=None, filename=None): + self.head = None # An ABACElemenet + self.tails = [] # List of ABACElements + super(ABACCredential, self).__init__(create=create, + subject=subject, + string=string, + filename=filename) + self.cred_type = ABACCredential.ABAC_CREDENTIAL_TYPE + + def get_head(self) : + if not self.head: + self.decode() + return self.head + + def get_tails(self) : + if len(self.tails) == 0: + self.decode() + return self.tails + + def decode(self): + super(ABACCredential, self).decode() + # Pull out the ABAC-specific info + doc = parseString(self.xml) + rt0s = doc.getElementsByTagName('rt0') + if len(rt0s) != 1: + raise CredentialNotVerifiable("ABAC credential had no rt0 element") + rt0_root = rt0s[0] + heads = self._get_abac_elements(rt0_root, 'head') + if len(heads) != 1: + raise CredentialNotVerifiable("ABAC credential should have exactly 1 head element, had %d" % len(heads)) + + self.head = heads[0] + self.tails = self._get_abac_elements(rt0_root, 'tail') + + def _get_abac_elements(self, root, label): + abac_elements = [] + elements = root.getElementsByTagName(label) + for elt in elements: + keyids = elt.getElementsByTagName('keyid') + if len(keyids) != 1: + raise CredentialNotVerifiable("ABAC credential element '%s' should have exactly 1 keyid, had %d." % (label, len(keyids))) + keyid_elt = keyids[0] + keyid = keyid_elt.childNodes[0].nodeValue.strip() + + mnemonic = None + mnemonic_elts = elt.getElementsByTagName('mnemonic') + if len(mnemonic_elts) > 0: + mnemonic = mnemonic_elts[0].childNodes[0].nodeValue.strip() + + role = None + role_elts = elt.getElementsByTagName('role') + if len(role_elts) > 0: + role = role_elts[0].childNodes[0].nodeValue.strip() + + linking_role = None + linking_role_elts = elt.getElementsByTagName('linking_role') + if len(linking_role_elts) > 0: + linking_role = linking_role_elts[0].childNodes[0].nodeValue.strip() + + abac_element = ABACElement(keyid, mnemonic, role, linking_role) + abac_elements.append(abac_element) + + return abac_elements + + def dump_string(self, dump_parents=False, show_xml=False): + result = "ABAC Credential\n" + filename=self.get_filename() + if filename: result += "Filename %s\n"%filename + if self.expiration: + result += "\texpiration: %s \n" % self.expiration.isoformat() + + result += "\tHead: %s\n" % self.get_head() + for tail in self.get_tails(): + result += "\tTail: %s\n" % tail + if self.get_signature(): + result += " gidIssuer:\n" + result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents) + if show_xml and HAVELXML: + try: + tree = etree.parse(StringIO(self.xml)) + aside = etree.tostring(tree, pretty_print=True) + result += "\nXML:\n\n" + result += aside + result += "\nEnd XML\n" + except: + import traceback + print "exc. Credential.dump_string / XML" + traceback.print_exc() + return result + + # sounds like this should be __repr__ instead ?? + # Produce the ABAC assertion. Something like [ABAC cred: Me.role<-You] or similar + def get_summary_tostring(self): + result = "[ABAC cred: " + str(self.get_head()) + for tail in self.get_tails(): + result += "<-%s" % str(tail) + result += "]" + return result + + def createABACElement(self, doc, tagName, abacObj): + kid = abacObj.get_principal_keyid() + mnem = abacObj.get_principal_mnemonic() # may be None + role = abacObj.get_role() # may be None + link = abacObj.get_linking_role() # may be None + ele = doc.createElement(tagName) + prin = doc.createElement('ABACprincipal') + ele.appendChild(prin) + append_sub(doc, prin, "keyid", kid) + if mnem: + append_sub(doc, prin, "mnemonic", mnem) + if role: + append_sub(doc, ele, "role", role) + if link: + append_sub(doc, ele, "linking_role", link) + return ele + + ## + # Encode the attributes of the credential into an XML string + # This should be done immediately before signing the credential. + # WARNING: + # In general, a signed credential obtained externally should + # not be changed else the signature is no longer valid. So, once + # you have loaded an existing signed credential, do not call encode() or sign() on it. + + def encode(self): + # Create the XML document + doc = Document() + signed_cred = doc.createElement("signed-credential") + +# Declare namespaces +# Note that credential/policy.xsd are really the PG schemas +# in a PL namespace. +# Note that delegation of credentials between the 2 only really works +# cause those schemas are identical. +# Also note these PG schemas talk about PG tickets and CM policies. + signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.geni.net/resources/credential/2/credential.xsd") + 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") + +# PG says for those last 2: +# signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") +# 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") + + doc.appendChild(signed_cred) + + # Fill in the bit + cred = doc.createElement("credential") + cred.setAttribute("xml:id", self.get_refid()) + signed_cred.appendChild(cred) + append_sub(doc, cred, "type", "abac") + + # Stub fields + append_sub(doc, cred, "serial", "8") + append_sub(doc, cred, "owner_gid", '') + append_sub(doc, cred, "owner_urn", '') + append_sub(doc, cred, "target_gid", '') + append_sub(doc, cred, "target_urn", '') + append_sub(doc, cred, "uuid", "") + + if not self.expiration: + self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) + self.expiration = self.expiration.replace(microsecond=0) + if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None: + # TZ aware. Make sure it is UTC + self.expiration = self.expiration.astimezone(tz.tzutc()) + append_sub(doc, cred, "expires", self.expiration.strftime('%Y-%m-%dT%H:%M:%SZ')) # RFC3339 + + abac = doc.createElement("abac") + rt0 = doc.createElement("rt0") + abac.appendChild(rt0) + cred.appendChild(abac) + append_sub(doc, rt0, "version", "1.1") + head = self.createABACElement(doc, "head", self.get_head()) + rt0.appendChild(head) + for tail in self.get_tails(): + tailEle = self.createABACElement(doc, "tail", tail) + rt0.appendChild(tailEle) + + # Create the tag + signatures = doc.createElement("signatures") + signed_cred.appendChild(signatures) + + # Get the finished product + self.xml = doc.toxml("utf-8") diff --git a/sfa/trust/auth.py b/sfa/trust/auth.py index 0dffa08d..f60f408b 100644 --- a/sfa/trust/auth.py +++ b/sfa/trust/auth.py @@ -16,6 +16,7 @@ from sfa.trust.credential import Credential from sfa.trust.trustedroots import TrustedRoots from sfa.trust.hierarchy import Hierarchy from sfa.trust.sfaticket import SfaTicket +from sfa.trust.speaksfor_util import determine_speaks_for class Auth: @@ -44,38 +45,27 @@ class Auth: return error valid = [] - speaking_for = options.get('geni_speaking_for', None) - speaks_for_cred = None - if not isinstance(creds, list): creds = [creds] - logger.debug("Auth.checkCredentials with %d creds"%len(creds)) - for cred in creds: - try: - self.check(cred, operation, hrn) - valid.append(cred) - except: - # check if credential is a 'speaks for credential' - if speaking_for: - try: - speaking_for_xrn = Xrn(speaking_for) - speaking_for_hrn = speaking_for_xrn.get_hrn() - self.check(cred, operation, speaking_for_hrn) - speaks_for_cred = cred - valid.append(cred) - except: - error = log_invalid_cred(cred) - else: + + # if speaks for gid matches caller cert then we've found a valid + # speaks for credential + speaks_for_gid = determine_speaks_for(logger, creds, self.peer_cert, \ + options, self.trusted_cert_list) + if self.peer_cert and \ + self.peer_cert.is_pubkey(speaks_for_gid.get_pubkey()): + valid = creds + else: + for cred in creds: + try: + self.check(cred, operation, hrn) + valid.append(cred) + except: error = log_invalid_cred(cred) - continue - - if not len(valid): - raise InsufficientRights('Access denied: %s -- %s' % (error[0],error[1])) - - if speaking_for and not speaks_for_cred: - raise InsufficientRights('Access denied: "geni_speaking_for" option specified but no valid speaks for credential found: %s -- %s' % (error[0],error[1])) + + if not len(valid): + raise InsufficientRights('Access denied: %s -- %s' % (error[0],error[1])) - return valid diff --git a/sfa/trust/credential.py b/sfa/trust/credential.py index ac8e5b72..070b71b4 100644 --- a/sfa/trust/credential.py +++ b/sfa/trust/credential.py @@ -1,1085 +1,1147 @@ -#---------------------------------------------------------------------- -# Copyright (c) 2008 Board of Trustees, Princeton University -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and/or hardware specification (the "Work") to -# deal in the Work without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Work, and to permit persons to whom the Work -# is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Work. -# -# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS -# IN THE WORK. -#---------------------------------------------------------------------- -## -# Implements SFA Credentials -# -# Credentials are signed XML files that assign a subject gid privileges to an object gid -## - -import os -from types import StringTypes -import datetime -from StringIO import StringIO -from tempfile import mkstemp -from xml.dom.minidom import Document, parseString - -HAVELXML = False -try: - from lxml import etree - HAVELXML = True -except: - pass - -from xml.parsers.expat import ExpatError - -from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent -from sfa.util.sfalogging import logger -from sfa.util.sfatime import utcparse -from sfa.trust.credential_legacy import CredentialLegacy -from sfa.trust.rights import Right, Rights, determine_rights -from sfa.trust.gid import GID -from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn - -# 2 weeks, in seconds -DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31 - - -# TODO: -# . make privs match between PG and PL -# . Need to add support for other types of credentials, e.g. tickets -# . add namespaces to signed-credential element? - -signature_template = \ -''' - - - - - - - - - - - - - - - - - - - - - - -''' - -# PG formats the template (whitespace) slightly differently. -# Note that they don't include the xmlns in the template, but add it later. -# Otherwise the two are equivalent. -#signature_template_as_in_pg = \ -#''' -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -#''' - -## -# Convert a string into a bool -# used to convert an xsd:boolean to a Python boolean -def str2bool(str): - if str.lower() in ['true','1']: - return True - return False - - -## -# Utility function to get the text of an XML element - -def getTextNode(element, subele): - sub = element.getElementsByTagName(subele)[0] - if len(sub.childNodes) > 0: - return sub.childNodes[0].nodeValue - else: - return None - -## -# Utility function to set the text of an XML element -# It creates the element, adds the text to it, -# and then appends it to the parent. - -def append_sub(doc, parent, element, text): - ele = doc.createElement(element) - ele.appendChild(doc.createTextNode(text)) - parent.appendChild(ele) - -## -# Signature contains information about an xmlsec1 signature -# for a signed-credential -# - -class Signature(object): - - def __init__(self, string=None): - self.refid = None - self.issuer_gid = None - self.xml = None - if string: - self.xml = string - self.decode() - - - def get_refid(self): - if not self.refid: - self.decode() - return self.refid - - def get_xml(self): - if not self.xml: - self.encode() - return self.xml - - def set_refid(self, id): - self.refid = id - - def get_issuer_gid(self): - if not self.gid: - self.decode() - return self.gid - - def set_issuer_gid(self, gid): - self.gid = gid - - def decode(self): - try: - doc = parseString(self.xml) - except ExpatError,e: - logger.log_exc ("Failed to parse credential, %s"%self.xml) - raise - sig = doc.getElementsByTagName("Signature")[0] - self.set_refid(sig.getAttribute("xml:id").strip("Sig_")) - keyinfo = sig.getElementsByTagName("X509Data")[0] - szgid = getTextNode(keyinfo, "X509Certificate") - szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid - self.set_issuer_gid(GID(string=szgid)) - - def encode(self): - self.xml = signature_template % (self.get_refid(), self.get_refid()) - - -## -# A credential provides a caller gid with privileges to an object gid. -# A signed credential is signed by the object's authority. -# -# Credentials are encoded in one of two ways. The legacy style places -# it in the subjectAltName of an X509 certificate. The new credentials -# are placed in signed XML. -# -# WARNING: -# In general, a signed credential obtained externally should -# not be changed else the signature is no longer valid. So, once -# you have loaded an existing signed credential, do not call encode() or sign() on it. - -def filter_creds_by_caller(creds, caller_hrn_list): - """ - Returns a list of creds who's gid caller matches the - specified caller hrn - """ - if not isinstance(creds, list): creds = [creds] - if not isinstance(caller_hrn_list, list): - caller_hrn_list = [caller_hrn_list] - caller_creds = [] - for cred in creds: - try: - tmp_cred = Credential(string=cred) - if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list: - caller_creds.append(cred) - except: pass - return caller_creds - -class Credential(object): - - ## - # Create a Credential object - # - # @param create If true, create a blank x509 certificate - # @param subject If subject!=None, create an x509 cert with the subject name - # @param string If string!=None, load the credential from the string - # @param filename If filename!=None, load the credential from the file - # FIXME: create and subject are ignored! - def __init__(self, create=False, subject=None, string=None, filename=None): - self.gidCaller = None - self.gidObject = None - self.expiration = None - self.privileges = None - self.issuer_privkey = None - self.issuer_gid = None - self.issuer_pubkey = None - self.parent = None - self.signature = None - self.xml = None - self.refid = None - self.legacy = None - - # Check if this is a legacy credential, translate it if so - if string or filename: - if string: - str = string - elif filename: - str = file(filename).read() - - if str.strip().startswith("-----"): - self.legacy = CredentialLegacy(False,string=str) - self.translate_legacy(str) - else: - self.xml = str - self.decode() - - # Find an xmlsec1 path - self.xmlsec_path = '' - paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin'] - for path in paths: - if os.path.isfile(path + '/' + 'xmlsec1'): - self.xmlsec_path = path + '/' + 'xmlsec1' - break - if not self.xmlsec_path: - logger.warn("Could not locate binary for xmlsec1 - SFA will be unable to sign stuff !!") - - def get_subject(self): - if not self.gidObject: - self.decode() - return self.gidObject.get_subject() - - # sounds like this should be __repr__ instead ?? - def get_summary_tostring(self): - if not self.gidObject: - self.decode() - obj = self.gidObject.get_printable_subject() - caller = self.gidCaller.get_printable_subject() - exp = self.get_expiration() - # Summarize the rights too? The issuer? - return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp) - - def get_signature(self): - if not self.signature: - self.decode() - return self.signature - - def set_signature(self, sig): - self.signature = sig - - - ## - # Translate a legacy credential into a new one - # - # @param String of the legacy credential - - def translate_legacy(self, str): - legacy = CredentialLegacy(False,string=str) - self.gidCaller = legacy.get_gid_caller() - self.gidObject = legacy.get_gid_object() - lifetime = legacy.get_lifetime() - if not lifetime: - self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) - else: - self.set_expiration(int(lifetime)) - self.lifeTime = legacy.get_lifetime() - self.set_privileges(legacy.get_privileges()) - self.get_privileges().delegate_all_privileges(legacy.get_delegate()) - - ## - # Need the issuer's private key and name - # @param key Keypair object containing the private key of the issuer - # @param gid GID of the issuing authority - - def set_issuer_keys(self, privkey, gid): - self.issuer_privkey = privkey - self.issuer_gid = gid - - - ## - # Set this credential's parent - def set_parent(self, cred): - self.parent = cred - self.updateRefID() - - ## - # set the GID of the caller - # - # @param gid GID object of the caller - - def set_gid_caller(self, gid): - self.gidCaller = gid - # gid origin caller is the caller's gid by default - self.gidOriginCaller = gid - - ## - # get the GID of the object - - def get_gid_caller(self): - if not self.gidCaller: - self.decode() - return self.gidCaller - - ## - # set the GID of the object - # - # @param gid GID object of the object - - def set_gid_object(self, gid): - self.gidObject = gid - - ## - # get the GID of the object - - def get_gid_object(self): - if not self.gidObject: - self.decode() - return self.gidObject - - ## - # Expiration: an absolute UTC time of expiration (as either an int or string or datetime) - # - def set_expiration(self, expiration): - if isinstance(expiration, (int, float)): - self.expiration = datetime.datetime.fromtimestamp(expiration) - elif isinstance (expiration, datetime.datetime): - self.expiration = expiration - elif isinstance (expiration, StringTypes): - self.expiration = utcparse (expiration) - else: - logger.error ("unexpected input type in Credential.set_expiration") - - - ## - # get the lifetime of the credential (always in datetime format) - - def get_expiration(self): - if not self.expiration: - self.decode() - # at this point self.expiration is normalized as a datetime - DON'T call utcparse again - return self.expiration - - ## - # For legacy sake - def get_lifetime(self): - return self.get_expiration() - - ## - # set the privileges - # - # @param privs either a comma-separated list of privileges of a Rights object - - def set_privileges(self, privs): - if isinstance(privs, str): - self.privileges = Rights(string = privs) - else: - self.privileges = privs - - ## - # return the privileges as a Rights object - - def get_privileges(self): - if not self.privileges: - self.decode() - return self.privileges - - ## - # determine whether the credential allows a particular operation to be - # performed - # - # @param op_name string specifying name of operation ("lookup", "update", etc) - - def can_perform(self, op_name): - rights = self.get_privileges() - - if not rights: - return False - - return rights.can_perform(op_name) - - - ## - # Encode the attributes of the credential into an XML string - # This should be done immediately before signing the credential. - # WARNING: - # In general, a signed credential obtained externally should - # not be changed else the signature is no longer valid. So, once - # you have loaded an existing signed credential, do not call encode() or sign() on it. - - def encode(self): - # Create the XML document - doc = Document() - signed_cred = doc.createElement("signed-credential") - -# Declare namespaces -# Note that credential/policy.xsd are really the PG schemas -# in a PL namespace. -# Note that delegation of credentials between the 2 only really works -# cause those schemas are identical. -# Also note these PG schemas talk about PG tickets and CM policies. - signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") - signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd") - 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") - -# PG says for those last 2: -# signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") -# 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") - - doc.appendChild(signed_cred) - - # Fill in the bit - cred = doc.createElement("credential") - cred.setAttribute("xml:id", self.get_refid()) - signed_cred.appendChild(cred) - append_sub(doc, cred, "type", "privilege") - append_sub(doc, cred, "serial", "8") - append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string()) - append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn()) - append_sub(doc, cred, "target_gid", self.gidObject.save_to_string()) - append_sub(doc, cred, "target_urn", self.gidObject.get_urn()) - append_sub(doc, cred, "uuid", "") - if not self.expiration: - self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) - self.expiration = self.expiration.replace(microsecond=0) - append_sub(doc, cred, "expires", self.expiration.isoformat()) - privileges = doc.createElement("privileges") - cred.appendChild(privileges) - - if self.privileges: - rights = self.get_privileges() - for right in rights.rights: - priv = doc.createElement("privilege") - append_sub(doc, priv, "name", right.kind) - append_sub(doc, priv, "can_delegate", str(right.delegate).lower()) - privileges.appendChild(priv) - - # Add the parent credential if it exists - if self.parent: - sdoc = parseString(self.parent.get_xml()) - # If the root node is a signed-credential (it should be), then - # get all its attributes and attach those to our signed_cred - # node. - # Specifically, PG and PLadd attributes for namespaces (which is reasonable), - # and we need to include those again here or else their signature - # no longer matches on the credential. - # We expect three of these, but here we copy them all: -# signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") -# and from PG (PL is equivalent, as shown above): -# signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") -# 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") - - # HOWEVER! - # PL now also declares these, with different URLs, so - # the code notices those attributes already existed with - # different values, and complains. - # This happens regularly on delegation now that PG and - # PL both declare the namespace with different URLs. - # If the content ever differs this is a problem, - # but for now it works - different URLs (values in the attributes) - # but the same actual schema, so using the PG schema - # on delegated-to-PL credentials works fine. - - # Note: you could also not copy attributes - # which already exist. It appears that both PG and PL - # will actually validate a slicecred with a parent - # signed using PG namespaces and a child signed with PL - # namespaces over the whole thing. But I don't know - # if that is a bug in xmlsec1, an accident since - # the contents of the schemas are the same, - # or something else, but it seems odd. And this works. - parentRoot = sdoc.documentElement - if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes(): - for attrIx in range(0, parentRoot.attributes.length): - attr = parentRoot.attributes.item(attrIx) - # returns the old attribute of same name that was - # on the credential - # Below throws InUse exception if we forgot to clone the attribute first - oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True)) - if oldAttr and oldAttr.value != attr.value: - 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) - logger.warn(msg) - #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg) - - p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True) - p = doc.createElement("parent") - p.appendChild(p_cred) - cred.appendChild(p) - # done handling parent credential - - # Create the tag - signatures = doc.createElement("signatures") - signed_cred.appendChild(signatures) - - # Add any parent signatures - if self.parent: - for cur_cred in self.get_credential_list()[1:]: - sdoc = parseString(cur_cred.get_signature().get_xml()) - ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) - signatures.appendChild(ele) - - # Get the finished product - self.xml = doc.toxml() - - - def save_to_random_tmp_file(self): - fp, filename = mkstemp(suffix='cred', text=True) - fp = os.fdopen(fp, "w") - self.save_to_file(filename, save_parents=True, filep=fp) - return filename - - def save_to_file(self, filename, save_parents=True, filep=None): - if not self.xml: - self.encode() - if filep: - f = filep - else: - f = open(filename, "w") - f.write(self.xml) - f.close() - - def save_to_string(self, save_parents=True): - if not self.xml: - self.encode() - return self.xml - - def get_refid(self): - if not self.refid: - self.refid = 'ref0' - return self.refid - - def set_refid(self, rid): - self.refid = rid - - ## - # Figure out what refids exist, and update this credential's id - # so that it doesn't clobber the others. Returns the refids of - # the parents. - - def updateRefID(self): - if not self.parent: - self.set_refid('ref0') - return [] - - refs = [] - - next_cred = self.parent - while next_cred: - refs.append(next_cred.get_refid()) - if next_cred.parent: - next_cred = next_cred.parent - else: - next_cred = None - - - # Find a unique refid for this credential - rid = self.get_refid() - while rid in refs: - val = int(rid[3:]) - rid = "ref%d" % (val + 1) - - # Set the new refid - self.set_refid(rid) - - # Return the set of parent credential ref ids - return refs - - def get_xml(self): - if not self.xml: - self.encode() - return self.xml - - ## - # Sign the XML file created by encode() - # - # WARNING: - # In general, a signed credential obtained externally should - # not be changed else the signature is no longer valid. So, once - # you have loaded an existing signed credential, do not call encode() or sign() on it. - - def sign(self): - if not self.issuer_privkey: - logger.warn("Cannot sign credential (no private key)") - return - if not self.issuer_gid: - logger.warn("Cannot sign credential (no issuer gid)") - return - doc = parseString(self.get_xml()) - sigs = doc.getElementsByTagName("signatures")[0] - - # Create the signature template to be signed - signature = Signature() - signature.set_refid(self.get_refid()) - sdoc = parseString(signature.get_xml()) - sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) - sigs.appendChild(sig_ele) - - self.xml = doc.toxml() - - - # Split the issuer GID into multiple certificates if it's a chain - chain = GID(filename=self.issuer_gid) - gid_files = [] - while chain: - gid_files.append(chain.save_to_random_tmp_file(False)) - if chain.get_parent(): - chain = chain.get_parent() - else: - chain = None - - - # Call out to xmlsec1 to sign it - ref = 'Sig_%s' % self.get_refid() - filename = self.save_to_random_tmp_file() - command='%s --sign --node-id "%s" --privkey-pem %s,%s %s' \ - % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename) -# print 'command',command - signed = os.popen(command).read() - os.remove(filename) - - for gid_file in gid_files: - os.remove(gid_file) - - self.xml = signed - - # This is no longer a legacy credential - if self.legacy: - self.legacy = None - - # Update signatures - self.decode() - - - ## - # Retrieve the attributes of the credential from the XML. - # This is automatically called by the various get_* methods of - # this class and should not need to be called explicitly. - - def decode(self): - if not self.xml: - return - doc = parseString(self.xml) - sigs = [] - signed_cred = doc.getElementsByTagName("signed-credential") - - # Is this a signed-cred or just a cred? - if len(signed_cred) > 0: - creds = signed_cred[0].getElementsByTagName("credential") - signatures = signed_cred[0].getElementsByTagName("signatures") - if len(signatures) > 0: - sigs = signatures[0].getElementsByTagName("Signature") - else: - creds = doc.getElementsByTagName("credential") - - if creds is None or len(creds) == 0: - # malformed cred file - raise CredentialNotVerifiable("Malformed XML: No credential tag found") - - # Just take the first cred if there are more than one - cred = creds[0] - - self.set_refid(cred.getAttribute("xml:id")) - self.set_expiration(utcparse(getTextNode(cred, "expires"))) - self.gidCaller = GID(string=getTextNode(cred, "owner_gid")) - self.gidObject = GID(string=getTextNode(cred, "target_gid")) - - - # Process privileges - privs = cred.getElementsByTagName("privileges")[0] - rlist = Rights() - for priv in privs.getElementsByTagName("privilege"): - kind = getTextNode(priv, "name") - deleg = str2bool(getTextNode(priv, "can_delegate")) - if kind == '*': - # Convert * into the default privileges for the credential's type - # Each inherits the delegatability from the * above - _ , type = urn_to_hrn(self.gidObject.get_urn()) - rl = determine_rights(type, self.gidObject.get_urn()) - for r in rl.rights: - r.delegate = deleg - rlist.add(r) - else: - rlist.add(Right(kind.strip(), deleg)) - self.set_privileges(rlist) - - - # Is there a parent? - parent = cred.getElementsByTagName("parent") - if len(parent) > 0: - parent_doc = parent[0].getElementsByTagName("credential")[0] - parent_xml = parent_doc.toxml() - self.parent = Credential(string=parent_xml) - self.updateRefID() - - # Assign the signatures to the credentials - for sig in sigs: - Sig = Signature(string=sig.toxml()) - - for cur_cred in self.get_credential_list(): - if cur_cred.get_refid() == Sig.get_refid(): - cur_cred.set_signature(Sig) - - - ## - # Verify - # trusted_certs: A list of trusted GID filenames (not GID objects!) - # Chaining is not supported within the GIDs by xmlsec1. - # - # trusted_certs_required: Should usually be true. Set False means an - # empty list of trusted_certs would still let this method pass. - # It just skips xmlsec1 verification et al. Only used by some utils - # - # Verify that: - # . All of the signatures are valid and that the issuers trace back - # to trusted roots (performed by xmlsec1) - # . The XML matches the credential schema - # . That the issuer of the credential is the authority in the target's urn - # . In the case of a delegated credential, this must be true of the root - # . That all of the gids presented in the credential are valid - # . Including verifying GID chains, and includ the issuer - # . The credential is not expired - # - # -- For Delegates (credentials with parents) - # . The privileges must be a subset of the parent credentials - # . The privileges must have "can_delegate" set for each delegated privilege - # . The target gid must be the same between child and parents - # . The expiry time on the child must be no later than the parent - # . The signer of the child must be the owner of the parent - # - # -- Verify does *NOT* - # . ensure that an xmlrpc client's gid matches a credential gid, that - # must be done elsewhere - # - # @param trusted_certs: The certificates of trusted CA certificates - def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): - if not self.xml: - self.decode() - - # validate against RelaxNG schema - if HAVELXML and not self.legacy: - if schema and os.path.exists(schema): - tree = etree.parse(StringIO(self.xml)) - schema_doc = etree.parse(schema) - xmlschema = etree.XMLSchema(schema_doc) - if not xmlschema.validate(tree): - error = xmlschema.error_log.last_error - message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line) - raise CredentialNotVerifiable(message) - - if trusted_certs_required and trusted_certs is None: - trusted_certs = [] - -# trusted_cert_objects = [GID(filename=f) for f in trusted_certs] - trusted_cert_objects = [] - ok_trusted_certs = [] - # If caller explicitly passed in None that means skip cert chain validation. - # Strange and not typical - if trusted_certs is not None: - for f in trusted_certs: - try: - # Failures here include unreadable files - # or non PEM files - trusted_cert_objects.append(GID(filename=f)) - ok_trusted_certs.append(f) - except Exception, exc: - logger.error("Failed to load trusted cert from %s: %r"%( f, exc)) - trusted_certs = ok_trusted_certs - - # Use legacy verification if this is a legacy credential - if self.legacy: - self.legacy.verify_chain(trusted_cert_objects) - if self.legacy.client_gid: - self.legacy.client_gid.verify_chain(trusted_cert_objects) - if self.legacy.object_gid: - self.legacy.object_gid.verify_chain(trusted_cert_objects) - return True - - # make sure it is not expired - if self.get_expiration() < datetime.datetime.utcnow(): - raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat())) - - # Verify the signatures - filename = self.save_to_random_tmp_file() - if trusted_certs is not None: - cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs]) - - # If caller explicitly passed in None that means skip cert chain validation. - # - Strange and not typical - if trusted_certs is not None: - # Verify the gids of this cred and of its parents - for cur_cred in self.get_credential_list(): - cur_cred.get_gid_object().verify_chain(trusted_cert_objects) - cur_cred.get_gid_caller().verify_chain(trusted_cert_objects) - - refs = [] - refs.append("Sig_%s" % self.get_refid()) - - parentRefs = self.updateRefID() - for ref in parentRefs: - refs.append("Sig_%s" % ref) - - for ref in refs: - # If caller explicitly passed in None that means skip xmlsec1 validation. - # Strange and not typical - if trusted_certs is None: - break - -# print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \ -# (self.xmlsec_path, ref, cert_args, filename) - verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \ - % (self.xmlsec_path, ref, cert_args, filename)).read() - if not verified.strip().startswith("OK"): - # xmlsec errors have a msg= which is the interesting bit. - mstart = verified.find("msg=") - msg = "" - if mstart > -1 and len(verified) > 4: - mstart = mstart + 4 - mend = verified.find('\\', mstart) - msg = verified[mstart:mend] - raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip())) - os.remove(filename) - - # Verify the parents (delegation) - if self.parent: - self.verify_parent(self.parent) - - # Make sure the issuer is the target's authority, and is - # itself a valid GID - self.verify_issuer(trusted_cert_objects) - return True - - ## - # Creates a list of the credential and its parents, with the root - # (original delegated credential) as the last item in the list - def get_credential_list(self): - cur_cred = self - list = [] - while cur_cred: - list.append(cur_cred) - if cur_cred.parent: - cur_cred = cur_cred.parent - else: - cur_cred = None - return list - - ## - # Make sure the credential's target gid (a) was signed by or (b) - # is the same as the entity that signed the original credential, - # or (c) is an authority over the target's namespace. - # Also ensure that the credential issuer / signer itself has a valid - # GID signature chain (signed by an authority with namespace rights). - def verify_issuer(self, trusted_gids): - root_cred = self.get_credential_list()[-1] - root_target_gid = root_cred.get_gid_object() - root_cred_signer = root_cred.get_signature().get_issuer_gid() - - # Case 1: - # Allow non authority to sign target and cred about target. - # - # Why do we need to allow non authorities to sign? - # If in the target gid validation step we correctly - # checked that the target is only signed by an authority, - # then this is just a special case of case 3. - # This short-circuit is the common case currently - - # and cause GID validation doesn't check 'authority', - # this allows users to generate valid slice credentials. - if root_target_gid.is_signed_by_cert(root_cred_signer): - # cred signer matches target signer, return success - return - - # Case 2: - # Allow someone to sign credential about themeselves. Used? - # If not, remove this. - #root_target_gid_str = root_target_gid.save_to_string() - #root_cred_signer_str = root_cred_signer.save_to_string() - #if root_target_gid_str == root_cred_signer_str: - # # cred signer is target, return success - # return - - # Case 3: - - # root_cred_signer is not the target_gid - # So this is a different gid that we have not verified. - # xmlsec1 verified the cert chain on this already, but - # it hasn't verified that the gid meets the HRN namespace - # requirements. - # Below we'll ensure that it is an authority. - # But we haven't verified that it is _signed by_ an authority - # We also don't know if xmlsec1 requires that cert signers - # are marked as CAs. - - # Note that if verify() gave us no trusted_gids then this - # call will fail. So skip it if we have no trusted_gids - if trusted_gids and len(trusted_gids) > 0: - root_cred_signer.verify_chain(trusted_gids) - else: - logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.") - - # See if the signer is an authority over the domain of the target. - # There are multiple types of authority - accept them all here - # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn()) - root_cred_signer_type = root_cred_signer.get_type() - if (root_cred_signer_type.find('authority') == 0): - #logger.debug('Cred signer is an authority') - # signer is an authority, see if target is in authority's domain - signerhrn = root_cred_signer.get_hrn() - if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()): - return - - # We've required that the credential be signed by an authority - # for that domain. Reasonable and probably correct. - # A looser model would also allow the signer to be an authority - # in my control framework - eg My CA or CH. Even if it is not - # the CH that issued these, eg, user credentials. - - # Give up, credential does not pass issuer verification - - 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())) - - - ## - # -- For Delegates (credentials with parents) verify that: - # . The privileges must be a subset of the parent credentials - # . The privileges must have "can_delegate" set for each delegated privilege - # . The target gid must be the same between child and parents - # . The expiry time on the child must be no later than the parent - # . The signer of the child must be the owner of the parent - def verify_parent(self, parent_cred): - # make sure the rights given to the child are a subset of the - # parents rights (and check delegate bits) - if not parent_cred.get_privileges().is_superset(self.get_privileges()): - raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) + - self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) + - self.get_privileges().save_to_string()) - - # make sure my target gid is the same as the parent's - if not parent_cred.get_gid_object().save_to_string() == \ - self.get_gid_object().save_to_string(): - raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) - - # make sure my expiry time is <= my parent's - if not parent_cred.get_expiration() >= self.get_expiration(): - raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) - - # make sure my signer is the parent's caller - if not parent_cred.get_gid_caller().save_to_string(False) == \ - self.get_signature().get_issuer_gid().save_to_string(False): - raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) - - # Recurse - if parent_cred.parent: - parent_cred.verify_parent(parent_cred.parent) - - - def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile): - """ - Return a delegated copy of this credential, delegated to the - specified gid's user. - """ - # get the gid of the object we are delegating - object_gid = self.get_gid_object() - object_hrn = object_gid.get_hrn() - - # the hrn of the user who will be delegated to - delegee_gid = GID(filename=delegee_gidfile) - delegee_hrn = delegee_gid.get_hrn() - - #user_key = Keypair(filename=keyfile) - #user_hrn = self.get_gid_caller().get_hrn() - subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn) - dcred = Credential(subject=subject_string) - dcred.set_gid_caller(delegee_gid) - dcred.set_gid_object(object_gid) - dcred.set_parent(self) - dcred.set_expiration(self.get_expiration()) - dcred.set_privileges(self.get_privileges()) - dcred.get_privileges().delegate_all_privileges(True) - #dcred.set_issuer_keys(keyfile, delegee_gidfile) - dcred.set_issuer_keys(caller_keyfile, caller_gidfile) - dcred.encode() - dcred.sign() - - return dcred - - # only informative - def get_filename(self): - return getattr(self,'filename',None) - - ## - # Dump the contents of a credential to stdout in human-readable format - # - # @param dump_parents If true, also dump the parent certificates - def dump (self, *args, **kwargs): - print self.dump_string(*args, **kwargs) - - - def dump_string(self, dump_parents=False, show_xml=False): - result="" - result += "CREDENTIAL %s\n" % self.get_subject() - filename=self.get_filename() - if filename: result += "Filename %s\n"%filename - result += " privs: %s\n" % self.get_privileges().save_to_string() - gidCaller = self.get_gid_caller() - if gidCaller: - result += " gidCaller:\n" - result += gidCaller.dump_string(8, dump_parents) - - if self.get_signature(): - print " gidIssuer:" - self.get_signature().get_issuer_gid().dump(8, dump_parents) - - if self.expiration: - print " expiration:", self.expiration.isoformat() - - gidObject = self.get_gid_object() - if gidObject: - result += " gidObject:\n" - result += gidObject.dump_string(8, dump_parents) - - if self.parent and dump_parents: - result += "\nPARENT" - result += self.parent.dump_string(True) - - if show_xml: - try: - tree = etree.parse(StringIO(self.xml)) - aside = etree.tostring(tree, pretty_print=True) - result += "\nXML\n" - result += aside - result += "\nEnd XML\n" - except: - import traceback - print "exc. Credential.dump_string / XML" - traceback.print_exc() - - return result +#---------------------------------------------------------------------- +# Copyright (c) 2008 Board of Trustees, Princeton University +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and/or hardware specification (the "Work") to +# deal in the Work without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Work, and to permit persons to whom the Work +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Work. +# +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# IN THE WORK. +#---------------------------------------------------------------------- +## +# Implements SFA Credentials +# +# Credentials are signed XML files that assign a subject gid privileges to an object gid +## + +import os +from types import StringTypes +import datetime +from StringIO import StringIO +from tempfile import mkstemp +from xml.dom.minidom import Document, parseString + +HAVELXML = False +try: + from lxml import etree + HAVELXML = True +except: + pass + +from xml.parsers.expat import ExpatError + +from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent +from sfa.util.sfalogging import logger +from sfa.util.sfatime import utcparse +from sfa.trust.credential_legacy import CredentialLegacy +from sfa.trust.rights import Right, Rights, determine_rights +from sfa.trust.gid import GID +from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn + +# 2 weeks, in seconds +DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31 + + +# TODO: +# . make privs match between PG and PL +# . Need to add support for other types of credentials, e.g. tickets +# . add namespaces to signed-credential element? + +signature_template = \ +''' + + + + + + + + + + + + + + + + + + + + + + +''' + +# PG formats the template (whitespace) slightly differently. +# Note that they don't include the xmlns in the template, but add it later. +# Otherwise the two are equivalent. +#signature_template_as_in_pg = \ +#''' +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +#''' + +## +# Convert a string into a bool +# used to convert an xsd:boolean to a Python boolean +def str2bool(str): + if str.lower() in ['true','1']: + return True + return False + + +## +# Utility function to get the text of an XML element + +def getTextNode(element, subele): + sub = element.getElementsByTagName(subele)[0] + if len(sub.childNodes) > 0: + return sub.childNodes[0].nodeValue + else: + return None + +## +# Utility function to set the text of an XML element +# It creates the element, adds the text to it, +# and then appends it to the parent. + +def append_sub(doc, parent, element, text): + ele = doc.createElement(element) + ele.appendChild(doc.createTextNode(text)) + parent.appendChild(ele) + +## +# Signature contains information about an xmlsec1 signature +# for a signed-credential +# + +class Signature(object): + + def __init__(self, string=None): + self.refid = None + self.issuer_gid = None + self.xml = None + if string: + self.xml = string + self.decode() + + + def get_refid(self): + if not self.refid: + self.decode() + return self.refid + + def get_xml(self): + if not self.xml: + self.encode() + return self.xml + + def set_refid(self, id): + self.refid = id + + def get_issuer_gid(self): + if not self.gid: + self.decode() + return self.gid + + def set_issuer_gid(self, gid): + self.gid = gid + + def decode(self): + try: + doc = parseString(self.xml) + except ExpatError,e: + logger.log_exc ("Failed to parse credential, %s"%self.xml) + raise + sig = doc.getElementsByTagName("Signature")[0] + ref_id = sig.getAttribute("xml:id").strip().strip("Sig_") + # The xml:id tag is optional, and could be in a + # Reference xml:id or Reference UID sub element instead + if not ref_id or ref_id == '': + reference = sig.getElementsByTagName('Reference')[0] + ref_id = reference.getAttribute('xml:id').strip().strip('Sig_') + if not ref_id or ref_id == '': + ref_id = reference.getAttribute('URI').strip().strip('#') + self.set_refid(ref_id) + keyinfos = sig.getElementsByTagName("X509Data") + gids = None + for keyinfo in keyinfos: + certs = keyinfo.getElementsByTagName("X509Certificate") + for cert in certs: + if len(cert.childNodes) > 0: + szgid = cert.childNodes[0].nodeValue + szgid = szgid.strip() + szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid + if gids is None: + gids = szgid + else: + gids += "\n" + szgid + if gids is None: + raise CredentialNotVerifiable("Malformed XML: No certificate found in signature") + self.set_issuer_gid(GID(string=gids)) + + def encode(self): + self.xml = signature_template % (self.get_refid(), self.get_refid()) + + +## +# A credential provides a caller gid with privileges to an object gid. +# A signed credential is signed by the object's authority. +# +# Credentials are encoded in one of two ways. The legacy style places +# it in the subjectAltName of an X509 certificate. The new credentials +# are placed in signed XML. +# +# WARNING: +# In general, a signed credential obtained externally should +# not be changed else the signature is no longer valid. So, once +# you have loaded an existing signed credential, do not call encode() or sign() on it. + +def filter_creds_by_caller(creds, caller_hrn_list): + """ + Returns a list of creds who's gid caller matches the + specified caller hrn + """ + if not isinstance(creds, list): creds = [creds] + if not isinstance(caller_hrn_list, list): + caller_hrn_list = [caller_hrn_list] + caller_creds = [] + for cred in creds: + try: + tmp_cred = Credential(string=cred) + if tmp_cred.get_cred_type() != Credential.SFA_CREDENTIAL_TYPE: + continue + if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list: + caller_creds.append(cred) + except: pass + return caller_creds + +class Credential(object): + + SFA_CREDENTIAL_TYPE = "geni_sfa" + + ## + # Create a Credential object + # + # @param create If true, create a blank x509 certificate + # @param subject If subject!=None, create an x509 cert with the subject name + # @param string If string!=None, load the credential from the string + # @param filename If filename!=None, load the credential from the file + # FIXME: create and subject are ignored! + def __init__(self, create=False, subject=None, string=None, filename=None): + self.gidCaller = None + self.gidObject = None + self.expiration = None + self.privileges = None + self.issuer_privkey = None + self.issuer_gid = None + self.issuer_pubkey = None + self.parent = None + self.signature = None + self.xml = None + self.refid = None + self.legacy = None + self.cred_type = Credential.SFA_CREDENTIAL_TYPE + + # Check if this is a legacy credential, translate it if so + if string or filename: + if string: + str = string + elif filename: + str = file(filename).read() + + if str.strip().startswith("-----"): + self.legacy = CredentialLegacy(False,string=str) + self.translate_legacy(str) + else: + self.xml = str + self.decode() + + # Find an xmlsec1 path + self.xmlsec_path = '' + paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin'] + for path in paths: + if os.path.isfile(path + '/' + 'xmlsec1'): + self.xmlsec_path = path + '/' + 'xmlsec1' + break + if not self.xmlsec_path: + logger.warn("Could not locate binary for xmlsec1 - SFA will be unable to sign stuff !!") + + def get_cred_type(self): + return self.cred_type + + def get_subject(self): + if not self.gidObject: + self.decode() + return self.gidObject.get_subject() + + # sounds like this should be __repr__ instead ?? + def get_summary_tostring(self): + if not self.gidObject: + self.decode() + obj = self.gidObject.get_printable_subject() + caller = self.gidCaller.get_printable_subject() + exp = self.get_expiration() + # Summarize the rights too? The issuer? + return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp) + + def get_signature(self): + if not self.signature: + self.decode() + return self.signature + + def set_signature(self, sig): + self.signature = sig + + + ## + # Translate a legacy credential into a new one + # + # @param String of the legacy credential + + def translate_legacy(self, str): + legacy = CredentialLegacy(False,string=str) + self.gidCaller = legacy.get_gid_caller() + self.gidObject = legacy.get_gid_object() + lifetime = legacy.get_lifetime() + if not lifetime: + self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) + else: + self.set_expiration(int(lifetime)) + self.lifeTime = legacy.get_lifetime() + self.set_privileges(legacy.get_privileges()) + self.get_privileges().delegate_all_privileges(legacy.get_delegate()) + + ## + # Need the issuer's private key and name + # @param key Keypair object containing the private key of the issuer + # @param gid GID of the issuing authority + + def set_issuer_keys(self, privkey, gid): + self.issuer_privkey = privkey + self.issuer_gid = gid + + + ## + # Set this credential's parent + def set_parent(self, cred): + self.parent = cred + self.updateRefID() + + ## + # set the GID of the caller + # + # @param gid GID object of the caller + + def set_gid_caller(self, gid): + self.gidCaller = gid + # gid origin caller is the caller's gid by default + self.gidOriginCaller = gid + + ## + # get the GID of the object + + def get_gid_caller(self): + if not self.gidCaller: + self.decode() + return self.gidCaller + + ## + # set the GID of the object + # + # @param gid GID object of the object + + def set_gid_object(self, gid): + self.gidObject = gid + + ## + # get the GID of the object + + def get_gid_object(self): + if not self.gidObject: + self.decode() + return self.gidObject + + ## + # Expiration: an absolute UTC time of expiration (as either an int or string or datetime) + # + def set_expiration(self, expiration): + if isinstance(expiration, (int, float)): + self.expiration = datetime.datetime.fromtimestamp(expiration) + elif isinstance (expiration, datetime.datetime): + self.expiration = expiration + elif isinstance (expiration, StringTypes): + self.expiration = utcparse (expiration) + else: + logger.error ("unexpected input type in Credential.set_expiration") + + + ## + # get the lifetime of the credential (always in datetime format) + + def get_expiration(self): + if not self.expiration: + self.decode() + # at this point self.expiration is normalized as a datetime - DON'T call utcparse again + return self.expiration + + ## + # For legacy sake + def get_lifetime(self): + return self.get_expiration() + + ## + # set the privileges + # + # @param privs either a comma-separated list of privileges of a Rights object + + def set_privileges(self, privs): + if isinstance(privs, str): + self.privileges = Rights(string = privs) + else: + self.privileges = privs + + ## + # return the privileges as a Rights object + + def get_privileges(self): + if not self.privileges: + self.decode() + return self.privileges + + ## + # determine whether the credential allows a particular operation to be + # performed + # + # @param op_name string specifying name of operation ("lookup", "update", etc) + + def can_perform(self, op_name): + rights = self.get_privileges() + + if not rights: + return False + + return rights.can_perform(op_name) + + + ## + # Encode the attributes of the credential into an XML string + # This should be done immediately before signing the credential. + # WARNING: + # In general, a signed credential obtained externally should + # not be changed else the signature is no longer valid. So, once + # you have loaded an existing signed credential, do not call encode() or sign() on it. + + def encode(self): + # Create the XML document + doc = Document() + signed_cred = doc.createElement("signed-credential") + +# Declare namespaces +# Note that credential/policy.xsd are really the PG schemas +# in a PL namespace. +# Note that delegation of credentials between the 2 only really works +# cause those schemas are identical. +# Also note these PG schemas talk about PG tickets and CM policies. + signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd + signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd") + 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") + +# PG says for those last 2: +# signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") +# 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") + + doc.appendChild(signed_cred) + + # Fill in the bit + cred = doc.createElement("credential") + cred.setAttribute("xml:id", self.get_refid()) + signed_cred.appendChild(cred) + append_sub(doc, cred, "type", "privilege") + append_sub(doc, cred, "serial", "8") + append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string()) + append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn()) + append_sub(doc, cred, "target_gid", self.gidObject.save_to_string()) + append_sub(doc, cred, "target_urn", self.gidObject.get_urn()) + append_sub(doc, cred, "uuid", "") + if not self.expiration: + self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) + self.expiration = self.expiration.replace(microsecond=0) + if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None: + # TZ aware. Make sure it is UTC + self.expiration = self.expiration.astimezone(tz.tzutc()) + append_sub(doc, cred, "expires", self.expiration.strftime('%Y-%m-%dT%H:%M:%SZ')) # RFC3339 + privileges = doc.createElement("privileges") + cred.appendChild(privileges) + + if self.privileges: + rights = self.get_privileges() + for right in rights.rights: + priv = doc.createElement("privilege") + append_sub(doc, priv, "name", right.kind) + append_sub(doc, priv, "can_delegate", str(right.delegate).lower()) + privileges.appendChild(priv) + + # Add the parent credential if it exists + if self.parent: + sdoc = parseString(self.parent.get_xml()) + # If the root node is a signed-credential (it should be), then + # get all its attributes and attach those to our signed_cred + # node. + # Specifically, PG and PLadd attributes for namespaces (which is reasonable), + # and we need to include those again here or else their signature + # no longer matches on the credential. + # We expect three of these, but here we copy them all: +# signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") +# and from PG (PL is equivalent, as shown above): +# signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") +# 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") + + # HOWEVER! + # PL now also declares these, with different URLs, so + # the code notices those attributes already existed with + # different values, and complains. + # This happens regularly on delegation now that PG and + # PL both declare the namespace with different URLs. + # If the content ever differs this is a problem, + # but for now it works - different URLs (values in the attributes) + # but the same actual schema, so using the PG schema + # on delegated-to-PL credentials works fine. + + # Note: you could also not copy attributes + # which already exist. It appears that both PG and PL + # will actually validate a slicecred with a parent + # signed using PG namespaces and a child signed with PL + # namespaces over the whole thing. But I don't know + # if that is a bug in xmlsec1, an accident since + # the contents of the schemas are the same, + # or something else, but it seems odd. And this works. + parentRoot = sdoc.documentElement + if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes(): + for attrIx in range(0, parentRoot.attributes.length): + attr = parentRoot.attributes.item(attrIx) + # returns the old attribute of same name that was + # on the credential + # Below throws InUse exception if we forgot to clone the attribute first + oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True)) + if oldAttr and oldAttr.value != attr.value: + 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) + logger.warn(msg) + #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg) + + p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True) + p = doc.createElement("parent") + p.appendChild(p_cred) + cred.appendChild(p) + # done handling parent credential + + # Create the tag + signatures = doc.createElement("signatures") + signed_cred.appendChild(signatures) + + # Add any parent signatures + if self.parent: + for cur_cred in self.get_credential_list()[1:]: + sdoc = parseString(cur_cred.get_signature().get_xml()) + ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) + signatures.appendChild(ele) + + # Get the finished product + self.xml = doc.toxml("utf-8") + + + def save_to_random_tmp_file(self): + fp, filename = mkstemp(suffix='cred', text=True) + fp = os.fdopen(fp, "w") + self.save_to_file(filename, save_parents=True, filep=fp) + return filename + + def save_to_file(self, filename, save_parents=True, filep=None): + if not self.xml: + self.encode() + if filep: + f = filep + else: + f = open(filename, "w") + f.write(self.xml) + f.close() + + def save_to_string(self, save_parents=True): + if not self.xml: + self.encode() + return self.xml + + def get_refid(self): + if not self.refid: + self.refid = 'ref0' + return self.refid + + def set_refid(self, rid): + self.refid = rid + + ## + # Figure out what refids exist, and update this credential's id + # so that it doesn't clobber the others. Returns the refids of + # the parents. + + def updateRefID(self): + if not self.parent: + self.set_refid('ref0') + return [] + + refs = [] + + next_cred = self.parent + while next_cred: + refs.append(next_cred.get_refid()) + if next_cred.parent: + next_cred = next_cred.parent + else: + next_cred = None + + + # Find a unique refid for this credential + rid = self.get_refid() + while rid in refs: + val = int(rid[3:]) + rid = "ref%d" % (val + 1) + + # Set the new refid + self.set_refid(rid) + + # Return the set of parent credential ref ids + return refs + + def get_xml(self): + if not self.xml: + self.encode() + return self.xml + + ## + # Sign the XML file created by encode() + # + # WARNING: + # In general, a signed credential obtained externally should + # not be changed else the signature is no longer valid. So, once + # you have loaded an existing signed credential, do not call encode() or sign() on it. + + def sign(self): + if not self.issuer_privkey: + logger.warn("Cannot sign credential (no private key)") + return + if not self.issuer_gid: + logger.warn("Cannot sign credential (no issuer gid)") + return + doc = parseString(self.get_xml()) + sigs = doc.getElementsByTagName("signatures")[0] + + # Create the signature template to be signed + signature = Signature() + signature.set_refid(self.get_refid()) + sdoc = parseString(signature.get_xml()) + sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) + sigs.appendChild(sig_ele) + + self.xml = doc.toxml("utf-8") + + + # Split the issuer GID into multiple certificates if it's a chain + chain = GID(filename=self.issuer_gid) + gid_files = [] + while chain: + gid_files.append(chain.save_to_random_tmp_file(False)) + if chain.get_parent(): + chain = chain.get_parent() + else: + chain = None + + + # Call out to xmlsec1 to sign it + ref = 'Sig_%s' % self.get_refid() + filename = self.save_to_random_tmp_file() + command='%s --sign --node-id "%s" --privkey-pem %s,%s %s' \ + % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename) +# print 'command',command + signed = os.popen(command).read() + os.remove(filename) + + for gid_file in gid_files: + os.remove(gid_file) + + self.xml = signed + + # This is no longer a legacy credential + if self.legacy: + self.legacy = None + + # Update signatures + self.decode() + + + ## + # Retrieve the attributes of the credential from the XML. + # This is automatically called by the various get_* methods of + # this class and should not need to be called explicitly. + + def decode(self): + if not self.xml: + return + doc = parseString(self.xml) + sigs = [] + signed_cred = doc.getElementsByTagName("signed-credential") + + # Is this a signed-cred or just a cred? + if len(signed_cred) > 0: + creds = signed_cred[0].getElementsByTagName("credential") + signatures = signed_cred[0].getElementsByTagName("signatures") + if len(signatures) > 0: + sigs = signatures[0].getElementsByTagName("Signature") + else: + creds = doc.getElementsByTagName("credential") + + if creds is None or len(creds) == 0: + # malformed cred file + raise CredentialNotVerifiable("Malformed XML: No credential tag found") + + # Just take the first cred if there are more than one + cred = creds[0] + + self.set_refid(cred.getAttribute("xml:id")) + self.set_expiration(utcparse(getTextNode(cred, "expires"))) + +# import traceback +# stack = traceback.extract_stack() + + og = getTextNode(cred, "owner_gid") + # ABAC creds will have this be None and use this method +# if og is None: +# found = False +# for frame in stack: +# if 'super(ABACCredential, self).decode()' in frame: +# found = True +# break +# if not found: +# raise CredentialNotVerifiable("Malformed XML: No owner_gid found") + self.gidCaller = GID(string=og) + tg = getTextNode(cred, "target_gid") +# if tg is None: +# found = False +# for frame in stack: +# if 'super(ABACCredential, self).decode()' in frame: +# found = True +# break +# if not found: +# raise CredentialNotVerifiable("Malformed XML: No target_gid found") + self.gidObject = GID(string=tg) + + # Process privileges + rlist = Rights() + priv_nodes = cred.getElementsByTagName("privileges") + if len(priv_nodes) > 0: + privs = priv_nodes[0] + for priv in privs.getElementsByTagName("privilege"): + kind = getTextNode(priv, "name") + deleg = str2bool(getTextNode(priv, "can_delegate")) + if kind == '*': + # Convert * into the default privileges for the credential's type + # Each inherits the delegatability from the * above + _ , type = urn_to_hrn(self.gidObject.get_urn()) + rl = determine_rights(type, self.gidObject.get_urn()) + for r in rl.rights: + r.delegate = deleg + rlist.add(r) + else: + rlist.add(Right(kind.strip(), deleg)) + self.set_privileges(rlist) + + + # Is there a parent? + parent = cred.getElementsByTagName("parent") + if len(parent) > 0: + parent_doc = parent[0].getElementsByTagName("credential")[0] + parent_xml = parent_doc.toxml("utf-8") + if parent_xml is None or parent_xml.strip() == "": + raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty") + self.parent = Credential(string=parent_xml) + self.updateRefID() + + # Assign the signatures to the credentials + for sig in sigs: + Sig = Signature(string=sig.toxml("utf-8")) + + for cur_cred in self.get_credential_list(): + if cur_cred.get_refid() == Sig.get_refid(): + cur_cred.set_signature(Sig) + + + ## + # Verify + # trusted_certs: A list of trusted GID filenames (not GID objects!) + # Chaining is not supported within the GIDs by xmlsec1. + # + # trusted_certs_required: Should usually be true. Set False means an + # empty list of trusted_certs would still let this method pass. + # It just skips xmlsec1 verification et al. Only used by some utils + # + # Verify that: + # . All of the signatures are valid and that the issuers trace back + # to trusted roots (performed by xmlsec1) + # . The XML matches the credential schema + # . That the issuer of the credential is the authority in the target's urn + # . In the case of a delegated credential, this must be true of the root + # . That all of the gids presented in the credential are valid + # . Including verifying GID chains, and includ the issuer + # . The credential is not expired + # + # -- For Delegates (credentials with parents) + # . The privileges must be a subset of the parent credentials + # . The privileges must have "can_delegate" set for each delegated privilege + # . The target gid must be the same between child and parents + # . The expiry time on the child must be no later than the parent + # . The signer of the child must be the owner of the parent + # + # -- Verify does *NOT* + # . ensure that an xmlrpc client's gid matches a credential gid, that + # must be done elsewhere + # + # @param trusted_certs: The certificates of trusted CA certificates + def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): + if not self.xml: + self.decode() + + # validate against RelaxNG schema + if HAVELXML and not self.legacy: + if schema and os.path.exists(schema): + tree = etree.parse(StringIO(self.xml)) + schema_doc = etree.parse(schema) + xmlschema = etree.XMLSchema(schema_doc) + if not xmlschema.validate(tree): + error = xmlschema.error_log.last_error + message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line) + raise CredentialNotVerifiable(message) + + if trusted_certs_required and trusted_certs is None: + trusted_certs = [] + +# trusted_cert_objects = [GID(filename=f) for f in trusted_certs] + trusted_cert_objects = [] + ok_trusted_certs = [] + # If caller explicitly passed in None that means skip cert chain validation. + # Strange and not typical + if trusted_certs is not None: + for f in trusted_certs: + try: + # Failures here include unreadable files + # or non PEM files + trusted_cert_objects.append(GID(filename=f)) + ok_trusted_certs.append(f) + except Exception, exc: + logger.error("Failed to load trusted cert from %s: %r", f, exc) + trusted_certs = ok_trusted_certs + + # Use legacy verification if this is a legacy credential + if self.legacy: + self.legacy.verify_chain(trusted_cert_objects) + if self.legacy.client_gid: + self.legacy.client_gid.verify_chain(trusted_cert_objects) + if self.legacy.object_gid: + self.legacy.object_gid.verify_chain(trusted_cert_objects) + return True + + # make sure it is not expired + if self.get_expiration() < datetime.datetime.utcnow(): + raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat())) + + # Verify the signatures + filename = self.save_to_random_tmp_file() + if trusted_certs is not None: + cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs]) + + # If caller explicitly passed in None that means skip cert chain validation. + # - Strange and not typical + if trusted_certs is not None: + # Verify the gids of this cred and of its parents + for cur_cred in self.get_credential_list(): + cur_cred.get_gid_object().verify_chain(trusted_cert_objects) + cur_cred.get_gid_caller().verify_chain(trusted_cert_objects) + + refs = [] + refs.append("Sig_%s" % self.get_refid()) + + parentRefs = self.updateRefID() + for ref in parentRefs: + refs.append("Sig_%s" % ref) + + for ref in refs: + # If caller explicitly passed in None that means skip xmlsec1 validation. + # Strange and not typical + if trusted_certs is None: + break + +# print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \ +# (self.xmlsec_path, ref, cert_args, filename) + verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \ + % (self.xmlsec_path, ref, cert_args, filename)).read() + if not verified.strip().startswith("OK"): + # xmlsec errors have a msg= which is the interesting bit. + mstart = verified.find("msg=") + msg = "" + if mstart > -1 and len(verified) > 4: + mstart = mstart + 4 + mend = verified.find('\\', mstart) + msg = verified[mstart:mend] + raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip())) + os.remove(filename) + + # Verify the parents (delegation) + if self.parent: + self.verify_parent(self.parent) + + # Make sure the issuer is the target's authority, and is + # itself a valid GID + self.verify_issuer(trusted_cert_objects) + return True + + ## + # Creates a list of the credential and its parents, with the root + # (original delegated credential) as the last item in the list + def get_credential_list(self): + cur_cred = self + list = [] + while cur_cred: + list.append(cur_cred) + if cur_cred.parent: + cur_cred = cur_cred.parent + else: + cur_cred = None + return list + + ## + # Make sure the credential's target gid (a) was signed by or (b) + # is the same as the entity that signed the original credential, + # or (c) is an authority over the target's namespace. + # Also ensure that the credential issuer / signer itself has a valid + # GID signature chain (signed by an authority with namespace rights). + def verify_issuer(self, trusted_gids): + root_cred = self.get_credential_list()[-1] + root_target_gid = root_cred.get_gid_object() + if root_cred.get_signature() is None: + # malformed + raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn())) + + root_cred_signer = root_cred.get_signature().get_issuer_gid() + + # Case 1: + # Allow non authority to sign target and cred about target. + # + # Why do we need to allow non authorities to sign? + # If in the target gid validation step we correctly + # checked that the target is only signed by an authority, + # then this is just a special case of case 3. + # This short-circuit is the common case currently - + # and cause GID validation doesn't check 'authority', + # this allows users to generate valid slice credentials. + if root_target_gid.is_signed_by_cert(root_cred_signer): + # cred signer matches target signer, return success + return + + # Case 2: + # Allow someone to sign credential about themeselves. Used? + # If not, remove this. + #root_target_gid_str = root_target_gid.save_to_string() + #root_cred_signer_str = root_cred_signer.save_to_string() + #if root_target_gid_str == root_cred_signer_str: + # # cred signer is target, return success + # return + + # Case 3: + + # root_cred_signer is not the target_gid + # So this is a different gid that we have not verified. + # xmlsec1 verified the cert chain on this already, but + # it hasn't verified that the gid meets the HRN namespace + # requirements. + # Below we'll ensure that it is an authority. + # But we haven't verified that it is _signed by_ an authority + # We also don't know if xmlsec1 requires that cert signers + # are marked as CAs. + + # Note that if verify() gave us no trusted_gids then this + # call will fail. So skip it if we have no trusted_gids + if trusted_gids and len(trusted_gids) > 0: + root_cred_signer.verify_chain(trusted_gids) + else: + logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.") + + # See if the signer is an authority over the domain of the target. + # There are multiple types of authority - accept them all here + # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn()) + root_cred_signer_type = root_cred_signer.get_type() + if (root_cred_signer_type.find('authority') == 0): + #logger.debug('Cred signer is an authority') + # signer is an authority, see if target is in authority's domain + signerhrn = root_cred_signer.get_hrn() + if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()): + return + + # We've required that the credential be signed by an authority + # for that domain. Reasonable and probably correct. + # A looser model would also allow the signer to be an authority + # in my control framework - eg My CA or CH. Even if it is not + # the CH that issued these, eg, user credentials. + + # Give up, credential does not pass issuer verification + + 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())) + + + ## + # -- For Delegates (credentials with parents) verify that: + # . The privileges must be a subset of the parent credentials + # . The privileges must have "can_delegate" set for each delegated privilege + # . The target gid must be the same between child and parents + # . The expiry time on the child must be no later than the parent + # . The signer of the child must be the owner of the parent + def verify_parent(self, parent_cred): + # make sure the rights given to the child are a subset of the + # parents rights (and check delegate bits) + if not parent_cred.get_privileges().is_superset(self.get_privileges()): + raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) + + self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) + + self.get_privileges().save_to_string()) + + # make sure my target gid is the same as the parent's + if not parent_cred.get_gid_object().save_to_string() == \ + self.get_gid_object().save_to_string(): + raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) + + # make sure my expiry time is <= my parent's + if not parent_cred.get_expiration() >= self.get_expiration(): + raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) + + # make sure my signer is the parent's caller + if not parent_cred.get_gid_caller().save_to_string(False) == \ + self.get_signature().get_issuer_gid().save_to_string(False): + raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) + + # Recurse + if parent_cred.parent: + parent_cred.verify_parent(parent_cred.parent) + + + def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile): + """ + Return a delegated copy of this credential, delegated to the + specified gid's user. + """ + # get the gid of the object we are delegating + object_gid = self.get_gid_object() + object_hrn = object_gid.get_hrn() + + # the hrn of the user who will be delegated to + delegee_gid = GID(filename=delegee_gidfile) + delegee_hrn = delegee_gid.get_hrn() + + #user_key = Keypair(filename=keyfile) + #user_hrn = self.get_gid_caller().get_hrn() + subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn) + dcred = Credential(subject=subject_string) + dcred.set_gid_caller(delegee_gid) + dcred.set_gid_object(object_gid) + dcred.set_parent(self) + dcred.set_expiration(self.get_expiration()) + dcred.set_privileges(self.get_privileges()) + dcred.get_privileges().delegate_all_privileges(True) + #dcred.set_issuer_keys(keyfile, delegee_gidfile) + dcred.set_issuer_keys(caller_keyfile, caller_gidfile) + dcred.encode() + dcred.sign() + + return dcred + + # only informative + def get_filename(self): + return getattr(self,'filename',None) + + ## + # Dump the contents of a credential to stdout in human-readable format + # + # @param dump_parents If true, also dump the parent certificates + def dump (self, *args, **kwargs): + print self.dump_string(*args, **kwargs) + + + def dump_string(self, dump_parents=False, show_xml=False): + result="" + result += "CREDENTIAL %s\n" % self.get_subject() + filename=self.get_filename() + if filename: result += "Filename %s\n"%filename + result += " privs: %s\n" % self.get_privileges().save_to_string() + gidCaller = self.get_gid_caller() + if gidCaller: + result += " gidCaller:\n" + result += gidCaller.dump_string(8, dump_parents) + + if self.get_signature(): + result += " gidIssuer:\n" + result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents) + + if self.expiration: + result += " expiration: " + self.expiration.isoformat() + "\n" + + gidObject = self.get_gid_object() + if gidObject: + result += " gidObject:\n" + result += gidObject.dump_string(8, dump_parents) + + if self.parent and dump_parents: + result += "\nPARENT" + result += self.parent.dump_string(True) + + if show_xml and HAVELXML: + try: + tree = etree.parse(StringIO(self.xml)) + aside = etree.tostring(tree, pretty_print=True) + result += "\nXML:\n\n" + result += aside + result += "\nEnd XML\n" + except: + import traceback + print "exc. Credential.dump_string / XML" + traceback.print_exc() + + return result diff --git a/sfa/trust/credential.xsd b/sfa/trust/credential.xsd index a57b94c0..c5f22f41 100644 --- a/sfa/trust/credential.xsd +++ b/sfa/trust/credential.xsd @@ -1,214 +1,290 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Can the ticket be delegated? - - - - - - A desciption of the resources that are being promised - - - - - - - - - The ticket must be "cashed in" by this date - - - - - - - - - - - - A credential granting privileges or a ticket. - - - - - - - - - - - - - - - - - - - Privileges or a ticket - - - - - - - - - - - - - - The type of this credential. Currently a Privilege set or a Ticket. - - - - - - - - - - - - A serial number. - - - - - GID of the owner of this credential. - - - - - URN of the owner. Not everyone can parse DER - - - - - GID of the target of this credential. - - - - - URN of the target. - - - - - UUID of this credential - - - - - Expires on - - - - - Optional Extensions - - - - - - - - - Parent that delegated to us - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Can the ticket be delegated? + + + + + + A desciption of the resources that are being promised + + + + + + + + + The ticket must be "cashed in" by this date + + + + + + + + + + + + + + + + An ABAC RT0 statement, used only for type 'abac'. + + + + + + + + + + + + + + + + + + + + + + + + + + + An ABAC assertion containing a single RT0 statement, used only for type 'abac'. + + + + + + + + + + + + + + + + + + A credential granting privileges or a ticket or making an ABAC assertion. + + + + + + + + + + + + + + + + + + + Privileges or a ticket or an ABAC assertion + + + + + + + + + + + + + + + The type of this credential. Currently a Privilege set or a Ticket or ABAC. + + + + + + + + + + + + + A serial number. + + + + + GID of the owner of this credential. + + + + + URN of the owner. Not everyone can parse DER + + + + + GID of the target of this credential. + + + + + URN of the target. + + + + + UUID of this credential + + + + + Expires on in ISO8601 format but preferably RFC3339 + + + + + Optional Extensions + + + + + + + + + Parent that delegated to us + + + + + + + + + + + + + + diff --git a/sfa/trust/credential_factory.py b/sfa/trust/credential_factory.py new file mode 100644 index 00000000..2fe37a70 --- /dev/null +++ b/sfa/trust/credential_factory.py @@ -0,0 +1,110 @@ +#---------------------------------------------------------------------- +# Copyright (c) 2014 Raytheon BBN Technologies +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and/or hardware specification (the "Work") to +# deal in the Work without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Work, and to permit persons to whom the Work +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Work. +# +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# IN THE WORK. +#---------------------------------------------------------------------- + +from sfa.util.sfalogging import logger +from sfa.trust.credential import Credential +from sfa.trust.abac_credential import ABACCredential + +import json +import re + +# Factory for creating credentials of different sorts by type. +# Specifically, this factory can create standard SFA credentials +# and ABAC credentials from XML strings based on their identifying content + +class CredentialFactory: + + UNKNOWN_CREDENTIAL_TYPE = 'geni_unknown' + + # Static Credential class method to determine the type of a credential + # string depending on its contents + @staticmethod + def getType(credString): + credString_nowhitespace = re.sub('\s', '', credString) + if credString_nowhitespace.find('abac') > -1: + return ABACCredential.ABAC_CREDENTIAL_TYPE + elif credString_nowhitespace.find('privilege') > -1: + return Credential.SFA_CREDENTIAL_TYPE + else: + st = credString_nowhitespace.find('') + end = credString_nowhitespace.find('', st) + return credString_nowhitespace[st + len(''):end] +# return CredentialFactory.UNKNOWN_CREDENTIAL_TYPE + + # Static Credential class method to create the appropriate credential + # (SFA or ABAC) depending on its type + @staticmethod + def createCred(credString=None, credFile=None): + if not credString and not credFile: + raise Exception("CredentialFactory.createCred called with no argument") + if credFile: + try: + credString = open(credFile).read() + except Exception, e: + logger.info("Error opening credential file %s: %s" % credFile, e) + return None + + # Try to treat the file as JSON, getting the cred_type from the struct + try: + credO = json.loads(credString, encoding='ascii') + if credO.has_key('geni_value') and credO.has_key('geni_type'): + cred_type = credO['geni_type'] + credString = credO['geni_value'] + except Exception, e: + # It wasn't a struct. So the credString is XML. Pull the type directly from the string + logger.debug("Credential string not JSON: %s" % e) + cred_type = CredentialFactory.getType(credString) + + if cred_type == Credential.SFA_CREDENTIAL_TYPE: + try: + cred = Credential(string=credString) + return cred + except Exception, e: + if credFile: + msg = "credString started: %s" % credString[:50] + raise Exception("%s not a parsable SFA credential: %s. " % (credFile, e) + msg) + else: + raise Exception("SFA Credential not parsable: %s. Cred start: %s..." % (e, credString[:50])) + + elif cred_type == ABACCredential.ABAC_CREDENTIAL_TYPE: + try: + cred = ABACCredential(string=credString) + return cred + except Exception, e: + if credFile: + raise Exception("%s not a parsable ABAC credential: %s" % (credFile, e)) + else: + raise Exception("ABAC Credential not parsable: %s. Cred start: %s..." % (e, credString[:50])) + else: + raise Exception("Unknown credential type '%s'" % cred_type) + +if __name__ == "__main__": + c2 = open('/tmp/sfa.xml').read() + cred1 = CredentialFactory.createCred(credFile='/tmp/cred.xml') + cred2 = CredentialFactory.createCred(credString=c2) + + print "C1 = %s" % cred1 + print "C2 = %s" % cred2 + c1s = cred1.dump_string() + print "C1 = %s" % c1s +# print "C2 = %s" % cred2.dump_string() diff --git a/sfa/trust/speaksfor_util.py b/sfa/trust/speaksfor_util.py new file mode 100644 index 00000000..bd121951 --- /dev/null +++ b/sfa/trust/speaksfor_util.py @@ -0,0 +1,466 @@ +#---------------------------------------------------------------------- +# Copyright (c) 2014 Raytheon BBN Technologies +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and/or hardware specification (the "Work") to +# deal in the Work without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Work, and to permit persons to whom the Work +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Work. +# +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# IN THE WORK. +#---------------------------------------------------------------------- + +import datetime +from dateutil import parser as du_parser, tz as du_tz +import optparse +import os +import subprocess +import sys +import tempfile +from xml.dom.minidom import * +from StringIO import StringIO + +from sfa.trust.certificate import Certificate +from sfa.trust.credential import Credential, signature_template, HAVELXML +from sfa.trust.abac_credential import ABACCredential, ABACElement +from sfa.trust.credential_factory import CredentialFactory +from sfa.trust.gid import GID + +# Routine to validate that a speaks-for credential +# says what it claims to say: +# It is a signed credential wherein the signer S is attesting to the +# ABAC statement: +# S.speaks_for(S)<-T Or "S says that T speaks for S" + +# Requires that openssl be installed and in the path +# create_speaks_for requires that xmlsec1 be on the path + +# Simple XML helper functions + +# Find the text associated with first child text node +def findTextChildValue(root): + child = findChildNamed(root, '#text') + if child: return str(child.nodeValue) + return None + +# Find first child with given name +def findChildNamed(root, name): + for child in root.childNodes: + if child.nodeName == name: + return child + return None + +# Write a string to a tempfile, returning name of tempfile +def write_to_tempfile(str): + str_fd, str_file = tempfile.mkstemp() + if str: + os.write(str_fd, str) + os.close(str_fd) + return str_file + +# Run a subprocess and return output +def run_subprocess(cmd, stdout, stderr): + try: + proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr) + proc.wait() + if stdout: + output = proc.stdout.read() + else: + output = proc.returncode + return output + except Exception as e: + raise Exception("Failed call to subprocess '%s': %s" % (" ".join(cmd), e)) + +def get_cert_keyid(gid): + """Extract the subject key identifier from the given certificate. + Return they key id as lowercase string with no colon separators + between pairs. The key id as shown in the text output of a + certificate are in uppercase with colon separators. + + """ + raw_key_id = gid.get_extension('subjectKeyIdentifier') + # Raw has colons separating pairs, and all characters are upper case. + # Remove the colons and convert to lower case. + keyid = raw_key_id.replace(':', '').lower() + return keyid + +# Pull the cert out of a list of certs in a PEM formatted cert string +def grab_toplevel_cert(cert): + start_label = '-----BEGIN CERTIFICATE-----' + if cert.find(start_label) > -1: + start_index = cert.find(start_label) + len(start_label) + else: + start_index = 0 + end_label = '-----END CERTIFICATE-----' + end_index = cert.find(end_label) + first_cert = cert[start_index:end_index] + pieces = first_cert.split('\n') + first_cert = "".join(pieces) + return first_cert + +# Validate that the given speaks-for credential represents the +# statement User.speaks_for(User)<-Tool for the given user and tool certs +# and was signed by the user +# Return: +# Boolean indicating whether the given credential +# is not expired +# is an ABAC credential +# was signed by the user associated with the speaking_for_urn +# is verified by xmlsec1 +# asserts U.speaks_for(U)<-T ("user says that T may speak for user") +# If schema provided, validate against schema +# is trusted by given set of trusted roots (both user cert and tool cert) +# String user certificate of speaking_for user if the above tests succeed +# (None otherwise) +# Error message indicating why the speaks_for call failed ("" otherwise) +def verify_speaks_for(cred, tool_gid, speaking_for_urn, \ + trusted_roots, schema=None, logger=None): + + # Credential has not expired + if cred.expiration and cred.expiration < datetime.datetime.utcnow(): + return False, None, "ABAC Credential expired at %s (%s)" % (cred.expiration.isoformat(), cred.get_summary_tostring()) + + # Must be ABAC + if cred.get_cred_type() != ABACCredential.ABAC_CREDENTIAL_TYPE: + return False, None, "Credential not of type ABAC but %s" % cred.get_cred_type + + if cred.signature is None or cred.signature.gid is None: + return False, None, "Credential malformed: missing signature or signer cert. Cred: %s" % cred.get_summary_tostring() + user_gid = cred.signature.gid + user_urn = user_gid.get_urn() + + # URN of signer from cert must match URN of 'speaking-for' argument + if user_urn != speaking_for_urn: + return False, None, "User URN from cred doesn't match speaking_for URN: %s != %s (cred %s)" % \ + (user_urn, speaking_for_urn, cred.get_summary_tostring()) + + tails = cred.get_tails() + if len(tails) != 1: + return False, None, "Invalid ABAC-SF credential: Need exactly 1 tail element, got %d (%s)" % \ + (len(tails), cred.get_summary_tostring()) + + user_keyid = get_cert_keyid(user_gid) + tool_keyid = get_cert_keyid(tool_gid) + subject_keyid = tails[0].get_principal_keyid() + + head = cred.get_head() + principal_keyid = head.get_principal_keyid() + role = head.get_role() + + logger.info('user keyid: %s' % user_keyid) + logger.info('principal keyid: %s' % principal_keyid) + logger.info('tool keyid: %s' % tool_keyid) + logger.info('subject keyid: %s' % subject_keyid) + logger.info('role: %s' % role) + logger.info('user gid: %s' % user_gid.dump_string()) + f = open('/tmp/speaksfor/tool.gid', 'w') + f.write(tool_gid.dump_string()) + f.close() + + # Credential must pass xmlsec1 verify + cred_file = write_to_tempfile(cred.save_to_string()) + cert_args = [] + if trusted_roots: + for x in trusted_roots: + cert_args += ['--trusted-pem', x.filename] + # FIXME: Why do we not need to specify the --node-id option as credential.py does? + xmlsec1_args = [cred.xmlsec_path, '--verify'] + cert_args + [ cred_file] + output = run_subprocess(xmlsec1_args, stdout=None, stderr=subprocess.PIPE) + os.unlink(cred_file) + if output != 0: + # FIXME + # xmlsec errors have a msg= which is the interesting bit. + # But does this go to stderr or stdout? Do we have it here? + mstart = verified.find("msg=") + msg = "" + if mstart > -1 and len(verified) > 4: + mstart = mstart + 4 + mend = verified.find('\\', mstart) + msg = verified[mstart:mend] + if msg == "": + msg = output + return False, None, "ABAC credential failed to xmlsec1 verify: %s" % msg + + # Must say U.speaks_for(U)<-T + if user_keyid != principal_keyid or \ + tool_keyid != subject_keyid or \ + role != ('speaks_for_%s' % user_keyid): + return False, None, "ABAC statement doesn't assert U.speaks_for(U)<-T (%s)" % cred.get_summary_tostring() + + # If schema provided, validate against schema + if HAVELXML and schema and os.path.exists(schema): + from lxml import etree + tree = etree.parse(StringIO(cred.xml)) + schema_doc = etree.parse(schema) + xmlschema = etree.XMLSchema(schema_doc) + if not xmlschema.validate(tree): + error = xmlschema.error_log.last_error + message = "%s: %s (line %s)" % (cred.get_summary_tostring(), error.message, error.line) + return False, None, ("XML Credential schema invalid: %s" % message) + + if trusted_roots: + # User certificate must validate against trusted roots + try: + user_gid.verify_chain(trusted_roots) + except Exception, e: + return False, None, \ + "Cred signer (user) cert not trusted: %s" % e + + # Tool certificate must validate against trusted roots + try: + tool_gid.verify_chain(trusted_roots) + except Exception, e: + return False, None, \ + "Tool cert not trusted: %s" % e + + return True, user_gid, "" + +# Determine if this is a speaks-for context. If so, validate +# And return either the tool_cert (not speaks-for or not validated) +# or the user cert (validated speaks-for) +# +# credentials is a list of GENI-style credentials: +# Either a cred string xml string, or Credential object of a tuple +# [{'geni_type' : geni_type, 'geni_value : cred_value, +# 'geni_version' : version}] +# caller_gid is the raw X509 cert gid +# options is the dictionary of API-provided options +# trusted_roots is a list of Certificate objects from the system +# trusted_root directory +# Optionally, provide an XML schema against which to validate the credential +def determine_speaks_for(logger, credentials, caller_gid, options, \ + trusted_roots, schema=None): + logger.info(options) + logger.info("geni speaking for:%s " % 'geni_speaking_for' in options) + if options and 'geni_speaking_for' in options: + speaking_for_urn = options['geni_speaking_for'].strip() + for cred in credentials: + # Skip things that aren't ABAC credentials + if type(cred) == dict: + if cred['geni_type'] != ABACCredential.ABAC_CREDENTIAL_TYPE: continue + cred_value = cred['geni_value'] + elif isinstance(cred, Credential): + if not isinstance(cred, ABACCredential): + continue + else: + cred_value = cred + else: + if CredentialFactory.getType(cred) != ABACCredential.ABAC_CREDENTIAL_TYPE: continue + cred_value = cred + + # If the cred_value is xml, create the object + if not isinstance(cred_value, ABACCredential): + cred = CredentialFactory.createCred(cred_value) + +# print "Got a cred to check speaksfor for: %s" % cred.get_summary_tostring() +# #cred.dump(True, True) +# print "Caller: %s" % caller_gid.dump_string(2, True) + logger.info(cred.dump_string()) + f = open('/tmp/speaksfor/%s.cred' % cred, 'w') + f.write(cred.xml) + f.close() + # See if this is a valid speaks_for + is_valid_speaks_for, user_gid, msg = \ + verify_speaks_for(cred, + caller_gid, speaking_for_urn, \ + trusted_roots, schema, logger=logger) + logger.info(msg) + if is_valid_speaks_for: + return user_gid # speaks-for + else: + if logger: + logger.info("Got speaks-for option but not a valid speaks_for with this credential: %s" % msg) + else: + print "Got a speaks-for option but not a valid speaks_for with this credential: " + msg + return caller_gid # Not speaks-for + +# Create an ABAC Speaks For credential using the ABACCredential object and it's encode&sign methods +def create_sign_abaccred(tool_gid, user_gid, ma_gid, user_key_file, cred_filename, dur_days=365): + print "Creating ABAC SpeaksFor using ABACCredential...\n" + # Write out the user cert + from tempfile import mkstemp + ma_str = ma_gid.save_to_string() + user_cert_str = user_gid.save_to_string() + if not user_cert_str.endswith(ma_str): + user_cert_str += ma_str + fp, user_cert_filename = mkstemp(suffix='cred', text=True) + fp = os.fdopen(fp, "w") + fp.write(user_cert_str) + fp.close() + + # Create the cred + cred = ABACCredential() + cred.set_issuer_keys(user_key_file, user_cert_filename) + tool_urn = tool_gid.get_urn() + user_urn = user_gid.get_urn() + user_keyid = get_cert_keyid(user_gid) + tool_keyid = get_cert_keyid(tool_gid) + cred.head = ABACElement(user_keyid, user_urn, "speaks_for_%s" % user_keyid) + cred.tails.append(ABACElement(tool_keyid, tool_urn)) + cred.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(days=dur_days)) + cred.expiration = cred.expiration.replace(microsecond=0) + + # Produce the cred XML + cred.encode() + + # Sign it + cred.sign() + # Save it + cred.save_to_file(cred_filename) + print "Created ABAC credential: '%s' in file %s" % \ + (cred.get_summary_tostring(), cred_filename) + +# FIXME: Assumes xmlsec1 is on path +# FIXME: Assumes signer is itself signed by an 'ma_gid' that can be trusted +def create_speaks_for(tool_gid, user_gid, ma_gid, \ + user_key_file, cred_filename, dur_days=365): + tool_urn = tool_gid.get_urn() + user_urn = user_gid.get_urn() + + header = '' + reference = "ref0" + signature_block = \ + '\n' + \ + signature_template + \ + '' + template = header + '\n' + \ + '\n' + \ + 'abac\n' + \ + '\n' +\ + '\n' + \ + '\n' + \ + '\n' + \ + '\n' + \ + '\n' + \ + '%s' +\ + '\n' + \ + '\n' + \ + '%s\n' + \ + '\n' + \ + '%s%s\n' +\ + 'speaks_for_%s\n' + \ + '\n' + \ + '\n' +\ + '%s%s\n' +\ + '\n' +\ + '\n' + \ + '\n' + \ + '\n' + \ + signature_block + \ + '\n' + + + credential_duration = datetime.timedelta(days=dur_days) + expiration = datetime.datetime.now(du_tz.tzutc()) + credential_duration + expiration_str = expiration.strftime('%Y-%m-%dT%H:%M:%SZ') # FIXME: libabac can't handle .isoformat() + version = "1.1" + + user_keyid = get_cert_keyid(user_gid) + tool_keyid = get_cert_keyid(tool_gid) + unsigned_cred = template % (reference, expiration_str, version, \ + user_keyid, user_urn, user_keyid, tool_keyid, tool_urn, \ + reference, reference) + unsigned_cred_filename = write_to_tempfile(unsigned_cred) + + # Now sign the file with xmlsec1 + # xmlsec1 --sign --privkey-pem privkey.pem,cert.pem + # --output signed.xml tosign.xml + pems = "%s,%s,%s" % (user_key_file, user_gid.get_filename(), + ma_gid.get_filename()) + # FIXME: assumes xmlsec1 is on path + cmd = ['xmlsec1', '--sign', '--privkey-pem', pems, + '--output', cred_filename, unsigned_cred_filename] + +# print " ".join(cmd) + sign_proc_output = run_subprocess(cmd, stdout=subprocess.PIPE, stderr=None) + if sign_proc_output == None: + print "OUTPUT = %s" % sign_proc_output + else: + print "Created ABAC credential: '%s speaks_for %s' in file %s" % \ + (tool_urn, user_urn, cred_filename) + os.unlink(unsigned_cred_filename) + + +# Test procedure +if __name__ == "__main__": + + parser = optparse.OptionParser() + parser.add_option('--cred_file', + help='Name of credential file') + parser.add_option('--tool_cert_file', + help='Name of file containing tool certificate') + parser.add_option('--user_urn', + help='URN of speaks-for user') + parser.add_option('--user_cert_file', + help="filename of x509 certificate of signing user") + parser.add_option('--ma_cert_file', + help="filename of x509 cert of MA that signed user cert") + parser.add_option('--user_key_file', + help="filename of private key of signing user") + parser.add_option('--trusted_roots_directory', + help='Directory of trusted root certs') + parser.add_option('--create', + help="name of file of ABAC speaksfor cred to create") + parser.add_option('--useObject', action='store_true', default=False, + help='Use the ABACCredential object to create the credential (default False)') + + options, args = parser.parse_args(sys.argv) + + tool_gid = GID(filename=options.tool_cert_file) + + if options.create: + if options.user_cert_file and options.user_key_file \ + and options.ma_cert_file: + user_gid = GID(filename=options.user_cert_file) + ma_gid = GID(filename=options.ma_cert_file) + if options.useObject: + create_sign_abaccred(tool_gid, user_gid, ma_gid, \ + options.user_key_file, \ + options.create) + else: + create_speaks_for(tool_gid, user_gid, ma_gid, \ + options.user_key_file, \ + options.create) + else: + print "Usage: --create cred_file " + \ + "--user_cert_file user_cert_file" + \ + " --user_key_file user_key_file --ma_cert_file ma_cert_file" + sys.exit() + + user_urn = options.user_urn + + # Get list of trusted rootcerts + if options.cred_file and not options.trusted_roots_directory: + sys.exit("Must supply --trusted_roots_directory to validate a credential") + + trusted_roots_directory = options.trusted_roots_directory + trusted_roots = \ + [Certificate(filename=os.path.join(trusted_roots_directory, file)) \ + for file in os.listdir(trusted_roots_directory) \ + if file.endswith('.pem') and file != 'CATedCACerts.pem'] + + cred = open(options.cred_file).read() + + creds = [{'geni_type' : ABACCredential.ABAC_CREDENTIAL_TYPE, 'geni_value' : cred, + 'geni_version' : '1'}] + gid = determine_speaks_for(None, creds, tool_gid, \ + {'geni_speaking_for' : user_urn}, \ + trusted_roots) + + + print 'SPEAKS_FOR = %s' % (gid != tool_gid) + print "CERT URN = %s" % gid.get_urn() -- 2.43.0