source: fedd/fedd_util.py @ d199ced

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since d199ced was d199ced, checked in by Ted Faber <faber@…>, 16 years ago

upgrade

  • Property mode set to 100644
File size: 11.6 KB
Line 
1#!/usr/local/bin/python
2
3import os, sys
4import subprocess
5import tempfile
6
7from M2Crypto import SSL, X509, EVP
8from pyasn1.codec.der import decoder
9
10from xmlrpclib import Binary
11
12
13# The version of M2Crypto on users is pretty old and doesn't have all the
14# features that are useful.  The legacy code is somewhat more brittle than the
15# main line, but will work.
16if "as_der" not in dir(EVP.PKey):
17    from asn1_raw import get_key_bits_from_file, get_key_bits_from_cert
18    legacy = True
19else:
20    legacy = False
21
22class fedd_ssl_context(SSL.Context):
23    """
24    Simple wrapper around an M2Crypto.SSL.Context to initialize it for fedd.
25    """
26    def __init__(self, my_cert, trusted_certs=None, password=None):
27        """
28        construct a fedd_ssl_context
29
30        @param my_cert: PEM file with my certificate in it
31        @param trusted_certs: PEM file with trusted certs in it (optional)
32        """
33        SSL.Context.__init__(self)
34
35        # load_cert takes a callback to get a password, not a password, so if
36        # the caller provided a password, this creates a nonce callback using a
37        # lambda form.
38        if password != None and not callable(password):
39            # This is cute.  password = lambda *args: password produces a
40            # function object that returns itself rather than one that returns
41            # the object itself.  This is because password is an object
42            # reference and after the assignment it's a lambda.  So we assign
43            # to a temp.
44            pwd = password
45            password =lambda *args: pwd
46
47        if password != None:
48            self.load_cert(my_cert, callback=password)
49        else:
50            self.load_cert(my_cert)
51        if trusted_certs != None: self.load_verify_locations(trusted_certs)
52        self.set_verify(SSL.verify_peer, 10)
53
54class fedid:
55    """
56    Wrapper around the federated ID from an X509 certificate.
57    """
58    HASHSIZE=20
59    def __init__(self, bits=None, hexstr=None, cert=None, file=None):
60        if bits != None:
61            self.set_bits(bits)
62        elif hexstr != None:
63            self.set_hexstr(hexstr)
64        elif cert != None:
65            self.set_cert(cert)
66        elif file != None:
67            self.set_file(file)
68        else:
69            self.buf = None
70
71    def __hash__(self):
72        return hash(self.buf)
73
74    def __eq__(self, other):
75        if isinstance(other, type(self)):
76            return self.buf == other.buf
77        elif isinstance(other, type(str())):
78            return self.buf == other;
79        else:
80            return False
81
82    def __ne__(self, other): return not self.__eq__(other)
83
84    def __str__(self):
85        if self.buf != None:
86            return str().join([ "%02x" % ord(x) for x in self.buf])
87        else: return ""
88
89    def __repr__(self):
90        return "fedid(hexstr='%s')" % self.__str__()
91
92    def pack_soap(self):
93        return self.buf
94
95    def pack_xmlrpc(self):
96        return self.buf
97
98    def digest_bits(self, bits):
99        """Internal function.  Compute the fedid from bits and store in buf"""
100        d = EVP.MessageDigest('sha1')
101        d.update(bits)
102        self.buf = d.final()
103
104
105    def set_hexstr(self, hexstr):
106        h = hexstr.replace(':','')
107        self.buf= str().join([chr(int(h[i:i+2],16)) \
108                for i in range(0,2*fedid.HASHSIZE,2)])
109
110    def get_hexstr(self):
111        """Return the hexstring representation of the fedid"""
112        return __str__(self)
113
114    def set_bits(self, bits):
115        """Set the fedid to bits(a 160 bit buffer)"""
116        self.buf = bits
117
118    def get_bits(self):
119        """Get the 160 bit buffer from the fedid"""
120        return self.buf
121
122    def set_file(self, file):
123        """Get the fedid from a certificate file
124
125        Calculate the SHA1 hash over the bit string of the public key as
126        defined in RFC3280.
127        """
128        self.buf = None
129        if legacy: self.digest_bits(get_key_bits_from_file(file))
130        else: self.set_cert(X509.load_cert(file))
131
132    def set_cert(self, cert):
133        """Get the fedid from a certificate.
134
135        Calculate the SHA1 hash over the bit string of the public key as
136        defined in RFC3280.
137        """
138
139        self.buf = None
140        if (cert != None):
141            if legacy:
142                self.digest_bits(get_key_bits_from_cert(cert))
143            else:
144                b = []
145                k = cert.get_pubkey()
146
147                # Getting the key was easy, but getting the bit string of the
148                # key requires a side trip through ASN.1
149                dec = decoder.decode(k.as_der())
150
151                # kv is a tuple of the bits in the key.  The loop below
152                # recombines these into bytes and then into a buffer for the
153                # SSL digest function.
154                kv =  dec[0].getComponentByPosition(1)
155                for i in range(0, len(kv), 8):
156                    v = 0
157                    for j in range(0, 8):
158                        v = (v << 1) + kv[i+j]
159                    b.append(v)
160                # The comprehension turns b from a list of bytes into a buffer
161                # (string) of bytes
162                self.digest_bits(str().join([chr(x) for x in b]))
163
164def pack_id(id):
165    """
166    Return a dictionary with the field name set by the id type.  Handy for
167    creating dictionaries to be converted to messages.
168    """
169    if isinstance(id, type(fedid())): return { 'fedid': id }
170    elif id.startswith("http:") or id.startswith("https:"): return { 'uri': id }
171    else: return { 'localname': id}
172
173def unpack_id(id):
174    """return id as a type determined by the key"""
175    if id.has_key("fedid"): return fedid(id["fedid"])
176    else:
177        for k in ("localname", "uri", "kerberosUsername"):
178            if id.has_key(k): return id[k]
179    return None
180
181def pack_soap(container, name, contents):
182    """
183    Convert the dictionary in contents into a tree of ZSI classes.
184
185    The holder classes are constructed from factories in container and assigned
186    to either the element or attribute name.  This is used to recursively
187    create the SOAP message.
188    """
189    if getattr(contents, "__iter__", None) != None:
190        attr =getattr(container, "new_%s" % name, None)
191        if attr: obj = attr()
192        else:
193            print dir(container)
194            raise TypeError("%s does not have a new_%s attribute" % \
195                    (container, name))
196        for e, v in contents.iteritems():
197            assign = getattr(obj, "set_element_%s" % e, None) or \
198                    getattr(obj, "set_attribute_%s" % e, None)
199            if isinstance(v, type(dict())):
200                assign(pack_soap(obj, e, v))
201            elif getattr(v, "__iter__", None) != None:
202                assign([ pack_soap(obj, e, val ) for val in v])
203            elif getattr(v, "pack_soap", None) != None:
204                assign(v.pack_soap())
205            else:
206                assign(v)
207        return obj
208    else: return contents
209
210def unpack_soap(element):
211    """
212    Convert a tree of ZSI SOAP classes intro a hash.  The inverse of pack_soap
213
214    Elements or elements that are empty are ignored.
215    """
216    methods = [ m for m in dir(element) \
217            if m.startswith("get_element") or m.startswith("get_attribute")]
218    if len(methods) > 0:
219        rv = { }
220        for m in methods:
221            if m.startswith("get_element_"): n = m.replace("get_element_","",1)
222            else: n = m.replace("get_attribute_", "", 1)
223            sub = getattr(element, m)()
224            if sub != None:
225                if isinstance(sub, basestring):
226                    rv[n] = sub
227                elif getattr(sub, "__iter__", None) != None:
228                    if len(sub) > 0: rv[n] = [unpack_soap(e) for e in sub]
229                else:
230                    rv[n] = unpack_soap(sub)
231        return rv
232    else: 
233        return element
234
235def encapsulate_binaries(e, tags):
236    """Walk through a message and encapsulate any dictionary entries in
237    tags into a binary object."""
238    dict_type = type(dict())
239    list_type = type(list())
240    str_type = type(str())
241
242    if isinstance(e, dict_type):
243        for k in e.keys():
244            if k in tags:
245                if isinstance(e[k], list_type):
246                    bin_list = []
247                    for ee in e[k]:
248                        if getattr(ee, 'pack_xmlrpc', None):
249                            bin_list.append(Binary(ee.pack_xmlrpc()))
250                        else:
251                            bin_list.append(Binary(ee))
252                    e[k] = bin_list
253                elif getattr(e[k],'pack_xmlrpc', None):
254                    e[k] = Binary(e[k].pack_xmlrpc())
255                else:
256                    e[k] = Binary(e[k])
257            elif isinstance(e[k], dict_type):
258                encapsulate_binaries(e[k], tags)
259            elif isinstance(e[k], list_type):
260                for ee in e[k]:
261                    encapsulate_binaries(ee, tags)
262    # Other types end the recursion - they should be leaves
263    return e
264
265def decapsulate_binaries(e, tags):
266    """Walk through a message and encapsulate any dictionary entries in
267    tags into a binary object."""
268    dict_type = type(dict())
269    list_type = type(list())
270    str_type = type(str())
271
272    if isinstance(e, dict_type):
273        for k in e.keys():
274            if k in tags:
275                if isinstance(e[k], list_type):
276                    e[k] = [ b.data for b in e[k]]
277                else:
278                    e[k] = e[k].data
279            elif isinstance(e[k], dict_type):
280                decapsulate_binaries(e[k], tags)
281            elif isinstance(e[k], list_type):
282                for ee in e[k]:
283                    decapsulate_binaries(ee, tags)
284    # Other types end the recursion - they should be leaves
285    return e
286
287
288def generate_fedid(subj, bits=2048, log=None, dir=None, trace=sys.stderr):
289    """
290    Create a new certificate and derive a fedid from it.
291
292    The fedid and the certificte are returned as a tuple.
293    """
294
295    keypath = None
296    certpath = None
297    try:
298        try:
299            kd, keypath = tempfile.mkstemp(dir=dir, prefix="key",
300                    suffix=".pem")
301            cd, certpath = tempfile.mkstemp(dir=dir, prefix="cert",
302                    suffix=".pem")
303
304            cmd = ["/usr/bin/openssl", "req", "-text", "-newkey", 
305                    "rsa:%d" % bits, "-keyout", keypath,  "-nodes", 
306                    "-subj", "/CN=%s" % subj, "-x509", "-days", "30", 
307                    "-out", certpath]
308
309            if log:
310                log.debug("[generate_fedid] %s" % " ".join(cmd))
311
312            if trace: call_out = trace
313            else: 
314                log.debug("set to dev/null")
315                call_out = open("/dev/null", "w")
316
317            rv = subprocess.call(cmd, stdout=call_out, stderr=call_out)
318            log.debug("rv = %d" % rv)
319            if rv == 0:
320                cert = ""
321                for p in (certpath, keypath):
322                    f = open(p)
323                    for line in f:
324                        cert += line
325               
326                fid = fedid(file=certpath)
327                return (fid, cert)
328            else:
329                return (None, None)
330        except IOError, e:
331            raise e
332    finally:
333        if keypath: os.remove(keypath)
334        if certpath: os.remove(certpath)
335
336
337def make_soap_handler(typecode, method, constructor, body_name):
338    """
339    Generate the handler code to unpack and pack SOAP requests and responses
340    and call the given method.
341
342    The code to decapsulate and encapsulate parameters encoded in SOAP is the
343    same modulo a few parameters.  This is basically a stub compiler for
344    calling a fedd service trhough a soap interface.  The parameters are the
345    typecode of the request parameters, the method to call (usually a bound
346    instance of a method on a fedd service providing class), the constructor of
347    a response packet and the name of the body element of that packet.  The
348    handler takes a ParsedSoap object (the request) and returns an instance of
349    the class created by constructor containing the response.  Failures of the
350    constructor or badly created constructors will result in None being
351    returned.
352    """
353    def handler(ps, fid):
354        req = ps.Parse(typecode)
355
356        msg = method(unpack_soap(req), fedid)
357
358        resp = constructor()
359        set_element = getattr(resp, "set_element_%s" % body_name, None)
360        if set_element and callable(set_element):
361            try:
362                set_element(pack_soap(resp, body_name, msg))
363                return resp
364            except (NameError, TypeError):
365                return None
366        else:
367            return None
368
369    return handler
370
371def make_xmlrpc_handler(method, body_name, input_binaries=('fedid',), 
372        output_binaries=('fedid',)):
373    """
374    Generate the handler code to unpack and pack SOAP requests and responses
375    and call the given method.
376
377    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
378    service is largely the same.  This helper creates such a handler.  The
379    parameters are the method name, the name of the body struct that contains
380    the response asn the list of fields that are encoded as Binary objects in
381    the input and in the output.  A handler is created that takes the params
382    response from an xm,lrpclib.loads on the incoming rpc and a fedid and
383    responds with a hash representing the struct ro be returned to the other
384    side.  On error None is returned.
385    """
386    def handler(params, fid):
387        p = decapsulate_binaries(params[0], input_binaries)
388        msg = method(p, fedid)
389
390        if msg != None:
391            return encapsulate_binaries({ body_name: msg }, output_binaries)
392        else:
393            return None
394
395    return handler
Note: See TracBrowser for help on using the repository browser.