From c767aac3b7e5cf1ef3a0ac92b8f3282ba7c4608d Mon Sep 17 00:00:00 2001 From: Tony Mack Date: Wed, 21 May 2014 22:32:25 -0400 Subject: [PATCH] fix speaks for auth --- sfa/trust/abac_credential.py | 278 +++++++++++++++++++ sfa/trust/credential_factory.py | 110 ++++++++ sfa/trust/speaksfor_util.py | 466 ++++++++++++++++++++++++++++++++ 3 files changed, 854 insertions(+) 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/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