source: fedd/fedd_util.py @ 11a08b0

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

decent logging

  • Property mode set to 100644
File size: 11.5 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=None):
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 = ["openssl", "req", "-text", "-newkey", "rsa:%d" % bits, 
305                    "-keyout", keypath,  "-nodes", "-subj", "/CN=%s" % subj,
306                    "-x509", "-days", "30", "-out", certpath]
307
308            if log:
309                log.debug("[generate_fedid] %s" % " ".join(cmd))
310
311            if trace: call_out = trace
312            else: call_out = open("/dev/null", "w")
313
314            rv = subprocess.call(cmd, stdout=call_out, stderr=call_out)
315            if rv == 0:
316                cert = ""
317                for p in (certpath, keypath):
318                    f = open(p)
319                    for line in f:
320                        cert += line
321               
322                fid = fedid(file=certpath)
323                return (fid, cert)
324            else:
325                return None
326        except IOError, e:
327            raise e
328    finally:
329        if keypath: os.remove(keypath)
330        if certpath: os.remove(certpath)
331
332
333def make_soap_handler(typecode, method, constructor, body_name):
334    """
335    Generate the handler code to unpack and pack SOAP requests and responses
336    and call the given method.
337
338    The code to decapsulate and encapsulate parameters encoded in SOAP is the
339    same modulo a few parameters.  This is basically a stub compiler for
340    calling a fedd service trhough a soap interface.  The parameters are the
341    typecode of the request parameters, the method to call (usually a bound
342    instance of a method on a fedd service providing class), the constructor of
343    a response packet and the name of the body element of that packet.  The
344    handler takes a ParsedSoap object (the request) and returns an instance of
345    the class created by constructor containing the response.  Failures of the
346    constructor or badly created constructors will result in None being
347    returned.
348    """
349    def handler(ps, fid):
350        req = ps.Parse(typecode)
351
352        msg = method(unpack_soap(req), fedid)
353
354        resp = constructor()
355        set_element = getattr(resp, "set_element_%s" % body_name, None)
356        if set_element and callable(set_element):
357            try:
358                set_element(pack_soap(resp, body_name, msg))
359                return resp
360            except (NameError, TypeError):
361                return None
362        else:
363            return None
364
365    return handler
366
367def make_xmlrpc_handler(method, body_name, input_binaries=('fedid',), 
368        output_binaries=('fedid',)):
369    """
370    Generate the handler code to unpack and pack SOAP requests and responses
371    and call the given method.
372
373    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
374    service is largely the same.  This helper creates such a handler.  The
375    parameters are the method name, the name of the body struct that contains
376    the response asn the list of fields that are encoded as Binary objects in
377    the input and in the output.  A handler is created that takes the params
378    response from an xm,lrpclib.loads on the incoming rpc and a fedid and
379    responds with a hash representing the struct ro be returned to the other
380    side.  On error None is returned.
381    """
382    def handler(params, fid):
383        p = decapsulate_binaries(params[0], input_binaries)
384        msg = method(p, fedid)
385
386        if msg != None:
387            return encapsulate_binaries({ body_name: msg }, output_binaries)
388        else:
389            return None
390
391    return handler
Note: See TracBrowser for help on using the repository browser.