source: fedd/fedd_util.py @ 2c6128f

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

Add support for a real fedkit tar file rather than the ad hoc script stuff.

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