## # Implements SFA Credentials # # Credentials are signed XML files that assign a subject gid privileges to an object gid ## ### $Id$ ### $URL$ import xmlrpclib import os import datetime from random import randint from xml.dom.minidom import Document, parseString from lxml import etree from sfa.trust.credential_legacy import CredentialLegacy from sfa.trust.certificate import Certificate from sfa.trust.rights import * from sfa.trust.gid import * from sfa.util.faults import * from sfa.util.sfalogging import logger # Two years, in minutes DEFAULT_CREDENTIAL_LIFETIME = 1051200 # TODO: # . make privs match between PG and PL # . Need to add support for other types of credentials, e.g. tickets signature_template = \ ''' ''' ## # Convert a string into a bool def str2bool(str): if str.lower() in ['yes','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): refid = None issuer_gid = None xml = None def __init__(self, string=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): doc = parseString(self.xml) 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. class Credential(object): gidCaller = None gidObject = None expiration = None privileges = None issuer_privkey = None issuer_gid = None issuer_pubkey = None parent = None signature = None xml = None refid = None legacy = None ## # 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 def __init__(self, create=False, subject=None, string=None, filename=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() 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: # Default to two years self.set_lifetime(DEFAULT_CREDENTIAL_LIFETIME) else: self.set_lifetime(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 ## # set the lifetime of this credential # # @param lifetime lifetime of credential # . if lifeTime is a datetime object, it is used for the expiration time # . if lifeTime is an integer value, it is considered the number of minutes # remaining before expiration def set_lifetime(self, lifeTime): if isinstance(lifeTime, int): self.expiration = datetime.timedelta(seconds=lifeTime*60) + datetime.datetime.utcnow() else: self.expiration = lifeTime ## # get the lifetime of the credential (in minutes) def get_lifetime(self): if not self.expiration: self.decode() return self.expiration ## # set the privileges # # @param privs either a comma-separated list of privileges of a RightList object def set_privileges(self, privs): if isinstance(privs, str): self.privileges = RightList(string = privs) else: self.privileges = privs ## # return the privileges as a RightList 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): p_sigs = None # Create the XML document doc = Document() signed_cred = doc.createElement("signed-credential") 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_lifetime(3600) 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()) p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True) p = doc.createElement("parent") p.appendChild(p_cred) cred.appendChild(p) # Create the tag signatures = doc.createElement("signatures") signed_cred.appendChild(signatures) # Add any parent signatures if self.parent: cur_cred = self.parent while cur_cred: sdoc = parseString(cur_cred.get_signature().get_xml()) ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) signatures.appendChild(ele) if cur_cred.parent: cur_cred = cur_cred.parent else: cur_cred = None # Get the finished product self.xml = doc.toxml() def save_to_random_tmp_file(self): filename = "/tmp/cred_%d" % randint(0,999999999) self.save_to_file(filename) return filename def save_to_file(self, filename, save_parents=True): if not self.xml: self.encode() 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 or not self.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() signed = os.popen('/usr/bin/xmlsec1 --sign --node-id "%s" --privkey-pem %s,%s %s' \ % (ref, self.issuer_privkey, ",".join(gid_files), filename)).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: cred = signed_cred[0].getElementsByTagName("credential")[0] signatures = signed_cred[0].getElementsByTagName("signatures") if len(signatures) > 0: sigs = signatures[0].getElementsByTagName("Signature") else: cred = doc.getElementsByTagName("credential")[0] self.set_refid(cred.getAttribute("xml:id")) sz_expires = getTextNode(cred, "expires") if sz_expires != '': self.expiration = datetime.datetime.strptime(sz_expires, '%Y-%m-%dT%H:%M:%S') self.lifeTime = 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 = RightList() 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 _ , type = urn_to_hrn(self.gidObject) rl = rlist.determine_rights(type, urn) for r in rlist.rights: 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()) cur_cred = self while cur_cred: if cur_cred.get_refid() == Sig.get_refid(): cur_cred.set_signature(Sig) if cur_cred.parent: cur_cred = cur_cred.parent else: cur_cred = None ## # 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 # # -- 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): if not self.xml: self.decode() # Check for schema conformance trusted_cert_objects = [GID(filename=f) for f in 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 # Verify the signatures filename = self.save_to_random_tmp_file() cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs]) # Verify the gids of this cred and of its parents cur_cred = self while cur_cred: cur_cred.get_gid_object().verify_chain(trusted_cert_objects) cur_cred.get_gid_caller().verify_chain(trusted_cert_objects) if cur_cred.parent: cur_cred = cur_cred.parent else: cur_cred = None refs = [] refs.append("Sig_%s" % self.get_refid()) parentRefs = self.updateRefID() for ref in parentRefs: refs.append("Sig_%s" % ref) for ref in refs: verified = os.popen('/usr/bin/xmlsec1 --verify --node-id "%s" %s %s 2>&1' \ % (ref, cert_args, filename)).read() if not verified.strip().startswith("OK"): raise CredentialNotVerifiable("xmlsec1 error: " + verified) os.remove(filename) # Verify the parents (delegation) if self.parent: self.verify_parent(self.parent) # Make sure the issuer is the target's authority self.verify_issuer() return True ## # Make sure the issuer of this credential is the target's authority def verify_issuer(self): target_authority = get_authority(self.get_gid_object().get_urn()) # Find the root credential's signature cur_cred = self root_refid = None while cur_cred: if cur_cred.parent: cur_cred = cur_cred.parent else: root_issuer = cur_cred.get_signature().get_issuer_gid().get_urn() cur_cred = None # Ensure that the signer of the root credential is the target_authority target_authority = hrn_to_urn(target_authority, 'authority') if root_issuer != target_authority: raise CredentialNotVerifiable("issuer (%s) != authority of target (%s)" \ % (root_issuer, target_authority)) ## # -- 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( self.parent.get_privileges().save_to_string() + " " + 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("target gid not equal between parent and child") # make sure my expiry time is <= my parent's if not parent_cred.get_lifetime() >= self.get_lifetime(): raise CredentialNotVerifiable("delegated credential expires after parent") # 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 not signed by parent caller") if parent_cred.parent: parent_cred.verify_parent(parent_cred.parent) ## # 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, dump_parents=False): print "CREDENTIAL", self.get_subject() print " privs:", self.get_privileges().save_to_string() print " gidCaller:" gidCaller = self.get_gid_caller() if gidCaller: gidCaller.dump(8, dump_parents) print " gidObject:" gidObject = self.get_gid_object() if gidObject: gidObject.dump(8, dump_parents) if self.parent and dump_parents: print "PARENT", self.parent.dump_parents()