#!/usr/local/bin/python import os, sys import subprocess import tempfile from M2Crypto import SSL, X509, EVP from pyasn1.codec.der import decoder from xmlrpclib import Binary # The version of M2Crypto on users is pretty old and doesn't have all the # features that are useful. The legacy code is somewhat more brittle than the # main line, but will work. if "as_der" not in dir(EVP.PKey): from asn1_raw import get_key_bits_from_file, get_key_bits_from_cert legacy = True else: legacy = False class fedd_ssl_context(SSL.Context): """ Simple wrapper around an M2Crypto.SSL.Context to initialize it for fedd. """ def __init__(self, my_cert, trusted_certs=None, password=None): """ construct a fedd_ssl_context @param my_cert: PEM file with my certificate in it @param trusted_certs: PEM file with trusted certs in it (optional) """ SSL.Context.__init__(self) # load_cert takes a callback to get a password, not a password, so if # the caller provided a password, this creates a nonce callback using a # lambda form. if password != None and not callable(password): # This is cute. password = lambda *args: password produces a # function object that returns itself rather than one that returns # the object itself. This is because password is an object # reference and after the assignment it's a lambda. So we assign # to a temp. pwd = password password =lambda *args: pwd if password != None: self.load_cert(my_cert, callback=password) else: self.load_cert(my_cert) if trusted_certs != None: self.load_verify_locations(trusted_certs) self.set_verify(SSL.verify_peer, 10) class fedid: """ Wrapper around the federated ID from an X509 certificate. """ HASHSIZE=20 def __init__(self, bits=None, hexstr=None, cert=None, file=None): if bits != None: self.set_bits(bits) elif hexstr != None: self.set_hexstr(hexstr) elif cert != None: self.set_cert(cert) elif file != None: self.set_file(file) else: self.buf = None def __hash__(self): return hash(self.buf) def __eq__(self, other): if isinstance(other, type(self)): return self.buf == other.buf elif isinstance(other, type(str())): return self.buf == other; else: return False def __ne__(self, other): return not self.__eq__(other) def __str__(self): if self.buf != None: return str().join([ "%02x" % ord(x) for x in self.buf]) else: return "" def __repr__(self): return "fedid(hexstr='%s')" % self.__str__() def pack_soap(self): return self.buf def pack_xmlrpc(self): return self.buf def digest_bits(self, bits): """Internal function. Compute the fedid from bits and store in buf""" d = EVP.MessageDigest('sha1') d.update(bits) self.buf = d.final() def set_hexstr(self, hexstr): h = hexstr.replace(':','') self.buf= str().join([chr(int(h[i:i+2],16)) \ for i in range(0,2*fedid.HASHSIZE,2)]) def get_hexstr(self): """Return the hexstring representation of the fedid""" return __str__(self) def set_bits(self, bits): """Set the fedid to bits(a 160 bit buffer)""" self.buf = bits def get_bits(self): """Get the 160 bit buffer from the fedid""" return self.buf def set_file(self, file): """Get the fedid from a certificate file Calculate the SHA1 hash over the bit string of the public key as defined in RFC3280. """ self.buf = None if legacy: self.digest_bits(get_key_bits_from_file(file)) else: self.set_cert(X509.load_cert(file)) def set_cert(self, cert): """Get the fedid from a certificate. Calculate the SHA1 hash over the bit string of the public key as defined in RFC3280. """ self.buf = None if (cert != None): if legacy: self.digest_bits(get_key_bits_from_cert(cert)) else: b = [] k = cert.get_pubkey() # Getting the key was easy, but getting the bit string of the # key requires a side trip through ASN.1 dec = decoder.decode(k.as_der()) # kv is a tuple of the bits in the key. The loop below # recombines these into bytes and then into a buffer for the # SSL digest function. kv = dec[0].getComponentByPosition(1) for i in range(0, len(kv), 8): v = 0 for j in range(0, 8): v = (v << 1) + kv[i+j] b.append(v) # The comprehension turns b from a list of bytes into a buffer # (string) of bytes self.digest_bits(str().join([chr(x) for x in b])) def pack_id(id): """ Return a dictionary with the field name set by the id type. Handy for creating dictionaries to be converted to messages. """ if isinstance(id, type(fedid())): return { 'fedid': id } elif id.startswith("http:") or id.startswith("https:"): return { 'uri': id } else: return { 'localname': id} def unpack_id(id): """return id as a type determined by the key""" if id.has_key("fedid"): return fedid(id["fedid"]) else: for k in ("localname", "uri", "kerberosUsername"): if id.has_key(k): return id[k] return None def pack_soap(container, name, contents): """ Convert the dictionary in contents into a tree of ZSI classes. The holder classes are constructed from factories in container and assigned to either the element or attribute name. This is used to recursively create the SOAP message. """ if getattr(contents, "__iter__", None) != None: attr =getattr(container, "new_%s" % name, None) if attr: obj = attr() else: print dir(container) raise TypeError("%s does not have a new_%s attribute" % \ (container, name)) for e, v in contents.iteritems(): assign = getattr(obj, "set_element_%s" % e, None) or \ getattr(obj, "set_attribute_%s" % e, None) if isinstance(v, type(dict())): assign(pack_soap(obj, e, v)) elif getattr(v, "__iter__", None) != None: assign([ pack_soap(obj, e, val ) for val in v]) elif getattr(v, "pack_soap", None) != None: assign(v.pack_soap()) else: assign(v) return obj else: return contents def unpack_soap(element): """ Convert a tree of ZSI SOAP classes intro a hash. The inverse of pack_soap Elements or elements that are empty are ignored. """ methods = [ m for m in dir(element) \ if m.startswith("get_element") or m.startswith("get_attribute")] if len(methods) > 0: rv = { } for m in methods: if m.startswith("get_element_"): n = m.replace("get_element_","",1) else: n = m.replace("get_attribute_", "", 1) sub = getattr(element, m)() if sub != None: if isinstance(sub, basestring): rv[n] = sub elif getattr(sub, "__iter__", None) != None: if len(sub) > 0: rv[n] = [unpack_soap(e) for e in sub] else: rv[n] = unpack_soap(sub) return rv else: return element def encapsulate_binaries(e, tags): """Walk through a message and encapsulate any dictionary entries in tags into a binary object.""" dict_type = type(dict()) list_type = type(list()) str_type = type(str()) if isinstance(e, dict_type): for k in e.keys(): if k in tags: if isinstance(e[k], list_type): bin_list = [] for ee in e[k]: if getattr(ee, 'pack_xmlrpc', None): bin_list.append(Binary(ee.pack_xmlrpc())) else: bin_list.append(Binary(ee)) e[k] = bin_list elif getattr(e[k],'pack_xmlrpc', None): e[k] = Binary(e[k].pack_xmlrpc()) else: e[k] = Binary(e[k]) elif isinstance(e[k], dict_type): encapsulate_binaries(e[k], tags) elif isinstance(e[k], list_type): for ee in e[k]: encapsulate_binaries(ee, tags) # Other types end the recursion - they should be leaves return e def decapsulate_binaries(e, tags): """Walk through a message and encapsulate any dictionary entries in tags into a binary object.""" dict_type = type(dict()) list_type = type(list()) str_type = type(str()) if isinstance(e, dict_type): for k in e.keys(): if k in tags: if isinstance(e[k], list_type): e[k] = [ b.data for b in e[k]] else: e[k] = e[k].data elif isinstance(e[k], dict_type): decapsulate_binaries(e[k], tags) elif isinstance(e[k], list_type): for ee in e[k]: decapsulate_binaries(ee, tags) # Other types end the recursion - they should be leaves return e def generate_fedid(subj, bits=2048, log=None, dir=None, trace=None): """ Create a new certificate and derive a fedid from it. The fedid and the certificte are returned as a tuple. """ keypath = None certpath = None try: try: kd, keypath = tempfile.mkstemp(dir=dir, prefix="key", suffix=".pem") cd, certpath = tempfile.mkstemp(dir=dir, prefix="cert", suffix=".pem") cmd = ["openssl", "req", "-text", "-newkey", "rsa:%d" % bits, "-keyout", keypath, "-nodes", "-subj", "/CN=%s" % subj, "-x509", "-days", "30", "-out", certpath] if log: log.debug("[generate_fedid] %s" % " ".join(cmd)) if trace: call_out = trace else: call_out = open("/dev/null", "w") rv = subprocess.call(cmd, stdout=call_out, stderr=call_out) if rv == 0: cert = "" for p in (certpath, keypath): f = open(p) for line in f: cert += line fid = fedid(file=certpath) return (fid, cert) else: return None except IOError, e: raise e finally: if keypath: os.remove(keypath) if certpath: os.remove(certpath) def make_soap_handler(typecode, method, constructor, body_name): """ Generate the handler code to unpack and pack SOAP requests and responses and call the given method. The code to decapsulate and encapsulate parameters encoded in SOAP is the same modulo a few parameters. This is basically a stub compiler for calling a fedd service trhough a soap interface. The parameters are the typecode of the request parameters, the method to call (usually a bound instance of a method on a fedd service providing class), the constructor of a response packet and the name of the body element of that packet. The handler takes a ParsedSoap object (the request) and returns an instance of the class created by constructor containing the response. Failures of the constructor or badly created constructors will result in None being returned. """ def handler(ps, fid): req = ps.Parse(typecode) msg = method(unpack_soap(req), fedid) resp = constructor() set_element = getattr(resp, "set_element_%s" % body_name, None) if set_element and callable(set_element): try: set_element(pack_soap(resp, body_name, msg)) return resp except (NameError, TypeError): return None else: return None return handler def make_xmlrpc_handler(method, body_name, input_binaries=('fedid',), output_binaries=('fedid',)): """ Generate the handler code to unpack and pack SOAP requests and responses and call the given method. The code to marshall and unmarshall XMLRPC parameters to and from a fedd service is largely the same. This helper creates such a handler. The parameters are the method name, the name of the body struct that contains the response asn the list of fields that are encoded as Binary objects in the input and in the output. A handler is created that takes the params response from an xm,lrpclib.loads on the incoming rpc and a fedid and responds with a hash representing the struct ro be returned to the other side. On error None is returned. """ def handler(params, fid): p = decapsulate_binaries(params[0], input_binaries) msg = method(p, fedid) if msg != None: return encapsulate_binaries({ body_name: msg }, output_binaries) else: return None return handler