source: fedd/fedd_util.py @ 4ed10ae

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

Proxy key additions working

  • Property mode set to 100644
File size: 12.9 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 strip_unicode(obj):
290    """Walk through a message and convert all strings to non-unicode strings"""
291    if isinstance(obj, dict):
292        for k in obj.keys():
293            obj[k] = strip_unicode(obj[k])
294        return obj
295    elif isinstance(obj, basestring):
296        return str(obj)
297    elif getattr(obj, "__iter__", None):
298        return [ strip_unicode(x) for x in obj]
299    else:
300        return obj
301
302def make_unicode(obj):
303    """Walk through a message and convert all strings to unicode"""
304    if isinstance(obj, dict):
305        for k in obj.keys():
306            obj[k] = make_unicode(obj[k])
307        return obj
308    elif isinstance(obj, basestring):
309        return unicode(obj)
310    elif getattr(obj, "__iter__", None):
311        return [ make_unicode(x) for x in obj]
312    else:
313        return obj
314
315
316def generate_fedid(subj, bits=2048, log=None, dir=None, trace=sys.stderr):
317    """
318    Create a new certificate and derive a fedid from it.
319
320    The fedid and the certificte are returned as a tuple.
321    """
322
323    keypath = None
324    certpath = None
325    try:
326        try:
327            kd, keypath = tempfile.mkstemp(dir=dir, prefix="key",
328                    suffix=".pem")
329            cd, certpath = tempfile.mkstemp(dir=dir, prefix="cert",
330                    suffix=".pem")
331
332            cmd = ["/usr/bin/openssl", "req", "-text", "-newkey", 
333                    "rsa:%d" % bits, "-keyout", keypath,  "-nodes", 
334                    "-subj", "/CN=%s" % subj, "-x509", "-days", "30", 
335                    "-out", certpath]
336
337            if log:
338                log.debug("[generate_fedid] %s" % " ".join(cmd))
339
340            if trace: call_out = trace
341            else: 
342                log.debug("set to dev/null")
343                call_out = open("/dev/null", "w")
344
345            rv = subprocess.call(cmd, stdout=call_out, stderr=call_out)
346            log.debug("rv = %d" % rv)
347            if rv == 0:
348                cert = ""
349                for p in (certpath, keypath):
350                    f = open(p)
351                    for line in f:
352                        cert += line
353               
354                fid = fedid(file=certpath)
355                return (fid, cert)
356            else:
357                return (None, None)
358        except IOError, e:
359            raise e
360    finally:
361        if keypath: os.remove(keypath)
362        if certpath: os.remove(certpath)
363
364
365def make_soap_handler(typecode, method, constructor, body_name):
366    """
367    Generate the handler code to unpack and pack SOAP requests and responses
368    and call the given method.
369
370    The code to decapsulate and encapsulate parameters encoded in SOAP is the
371    same modulo a few parameters.  This is basically a stub compiler for
372    calling a fedd service trhough a soap interface.  The parameters are the
373    typecode of the request parameters, the method to call (usually a bound
374    instance of a method on a fedd service providing class), the constructor of
375    a response packet and the name of the body element of that packet.  The
376    handler takes a ParsedSoap object (the request) and returns an instance of
377    the class created by constructor containing the response.  Failures of the
378    constructor or badly created constructors will result in None being
379    returned.
380    """
381    def handler(ps, fid):
382        req = ps.Parse(typecode)
383
384        msg = method(unpack_soap(req), fedid)
385
386        resp = constructor()
387        set_element = getattr(resp, "set_element_%s" % body_name, None)
388        if set_element and callable(set_element):
389            try:
390                set_element(pack_soap(resp, body_name, msg))
391                return resp
392            except (NameError, TypeError):
393                return None
394        else:
395            return None
396
397    return handler
398
399def make_xmlrpc_handler(method, body_name, input_binaries=('fedid',), 
400        output_binaries=('fedid',)):
401    """
402    Generate the handler code to unpack and pack SOAP requests and responses
403    and call the given method.
404
405    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
406    service is largely the same.  This helper creates such a handler.  The
407    parameters are the method name, the name of the body struct that contains
408    the response asn the list of fields that are encoded as Binary objects in
409    the input and in the output.  A handler is created that takes the params
410    response from an xm,lrpclib.loads on the incoming rpc and a fedid and
411    responds with a hash representing the struct ro be returned to the other
412    side.  On error None is returned.
413    """
414    def handler(params, fid):
415        p = decapsulate_binaries(params[0], input_binaries)
416        msg = method(p, fedid)
417
418        if msg != None:
419            return encapsulate_binaries({ body_name: msg }, output_binaries)
420        else:
421            return None
422
423    return handler
424
425
426def set_log_level(config, sect, log):
427    """ Set the logging level to the value passed in sect of config."""
428    # The getattr sleight of hand finds the logging level constant
429    # corrersponding to the string.  We're a little paranoid to avoid user
430    # mayhem.
431    if config.has_option(sect, "log_level"):
432        level_str = config.get(sect, "log_level")
433        try:
434            level = int(getattr(logging, level_str.upper(), -1))
435
436            if  logging.DEBUG <= level <= logging.CRITICAL:
437                log.setLevel(level)
438            else:
439                log.error("Bad experiment_log value: %s" % level_str)
440
441        except ValueError:
442            log.error("Bad experiment_log value: %s" % level_str)
443
Note: See TracBrowser for help on using the repository browser.