source: fedd/federation/util.py @ 353db8c

axis_examplecompt_changesinfo-ops
Last change on this file since 353db8c was 353db8c, checked in by Ted Faber <faber@…>, 13 years ago

Vairous ABAC tweaks, mostly concerned with making key splitting less visible.

  • Property mode set to 100644
File size: 10.8 KB
Line 
1#!/usr/local/bin/python
2
3import re
4import os
5import string
6import logging
7import pickle
8
9import httplib
10
11from socket import sslerror
12
13from M2Crypto import SSL
14from M2Crypto.SSL import SSLError
15from fedid import fedid
16from service_error import service_error
17from urlparse import urlparse
18
19
20# If this is an old enough version of M2Crypto.SSL that has an
21# ssl_verify_callback that doesn't allow 0-length signed certs, create a
22# version of that callback that does.  This is edited from the original in
23# M2Crypto.SSL.cb.  This version also elides the printing to stderr.
24if not getattr(SSL.cb, 'ssl_verify_callback_allow_unknown_ca', None):
25    from M2Crypto.SSL.Context import map
26    from M2Crypto import m2
27
28    def ssl_verify_callback(ssl_ctx_ptr, x509_ptr, errnum, errdepth, ok):
29        unknown_issuer = [
30            m2.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
31            m2.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE,
32            m2.X509_V_ERR_CERT_UNTRUSTED,
33            m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT
34            ]
35        ssl_ctx = map()[ssl_ctx_ptr]
36
37        if errnum in unknown_issuer: 
38            if ssl_ctx.get_allow_unknown_ca():
39                ok = 1
40        # CRL checking goes here...
41        if ok:
42            if ssl_ctx.get_verify_depth() >= errdepth:
43                ok = 1
44            else:
45                ok = 0
46        return ok
47else:
48    def ssl_verify_callback(ssl_ctx_ptr, x509_ptr, errnum, errdepth, ok):
49        raise ValueError("This should never be called")
50
51class fedd_ssl_context(SSL.Context):
52    """
53    Simple wrapper around an M2Crypto.SSL.Context to initialize it for fedd.
54    """
55    def __init__(self, my_cert, trusted_certs=None, password=None):
56        """
57        construct a fedd_ssl_context
58
59        @param my_cert: PEM file with my certificate in it
60        @param trusted_certs: PEM file with trusted certs in it (optional)
61        """
62        SSL.Context.__init__(self)
63
64        # load_cert takes a callback to get a password, not a password, so if
65        # the caller provided a password, this creates a nonce callback using a
66        # lambda form.
67        if password != None and not callable(password):
68            # This is cute.  password = lambda *args: password produces a
69            # function object that returns itself rather than one that returns
70            # the object itself.  This is because password is an object
71            # reference and after the assignment it's a lambda.  So we assign
72            # to a temp.
73            pwd = str(password)
74            password =lambda *args: pwd
75
76        # The calls to str below (and above) are because the underlying SSL
77        # stuff is intolerant of unicode.
78        if password != None:
79            self.load_cert(str(my_cert), callback=password)
80        else:
81            self.load_cert(str(my_cert))
82
83        # If no trusted certificates are specified, allow unknown CAs.
84        if trusted_certs: 
85            self.load_verify_locations(trusted_certs)
86            self.set_verify(SSL.verify_peer, 10)
87        else:
88            # More legacy code.  Recent versions of M2Crypto express the
89            # allow_unknown_ca option through a callback turned to allow it.
90            # Older versions use a standard callback that respects the
91            # attribute.  This should work under both regines.
92            callb = getattr(SSL.cb, 'ssl_verify_callback_allow_unknown_ca', 
93                    ssl_verify_callback)
94            self.set_allow_unknown_ca(True)
95            self.set_verify(SSL.verify_peer, 10, callback=callb)
96
97def read_simple_accessdb(fn, auth, mask=[]):
98    """
99    Read a simple access database.  Each line is a fedid (of the form
100    fedid:hexstring) and a comma separated list of atributes to be assigned to
101    it.  This parses out the fedids and adds the attributes to the authorizer.
102    comments (preceded with a #) and blank lines are ignored.  Exceptions (e.g.
103    file exceptions and ValueErrors from badly parsed lines) are propagated.
104    """
105
106    rv = [ ]
107    lineno = 0
108    fedid_line = re.compile("fedid:([" + string.hexdigits + "]+)\s+" +\
109            "(\w+\s*(,\s*\w+)*)")
110
111    # If a single string came in, make it a list
112    if isinstance(mask, basestring): mask = [ mask ]
113
114    f = open(fn, 'r')
115    for line in f:
116        lineno += 1
117        line = line.strip()
118        if line.startswith('#') or len(line) == 0: 
119            continue
120        m = fedid_line.match(line)
121        if m :
122            fid = fedid(hexstr=m.group(1))
123            for a in [ a.strip() for a in m.group(2).split(",") \
124                    if not mask or a.strip() in mask ]:
125                auth.set_attribute(fid, a.strip())
126        else:
127            raise ValueError("Badly formatted line in accessdb: %s line %d" %\
128                    (fn, lineno))
129    f.close()
130    return rv
131       
132
133def pack_id(id):
134    """
135    Return a dictionary with the field name set by the id type.  Handy for
136    creating dictionaries to be converted to messages.
137    """
138    if isinstance(id, fedid): return { 'fedid': id }
139    elif id.startswith("http:") or id.startswith("https:"): return { 'uri': id }
140    else: return { 'localname': id}
141
142def unpack_id(id):
143    """return id as a type determined by the key"""
144    for k in ("localname", "fedid", "uri", "kerberosUsername"):
145        if id.has_key(k): return id[k]
146    return None
147
148def set_log_level(config, sect, log):
149    """ Set the logging level to the value passed in sect of config."""
150    # The getattr sleight of hand finds the logging level constant
151    # corrersponding to the string.  We're a little paranoid to avoid user
152    # mayhem.
153    if config.has_option(sect, "log_level"):
154        level_str = config.get(sect, "log_level")
155        try:
156            level = int(getattr(logging, level_str.upper(), -1))
157
158            if  logging.DEBUG <= level <= logging.CRITICAL:
159                log.setLevel(level)
160            else:
161                log.error("Bad experiment_log value: %s" % level_str)
162
163        except ValueError:
164            log.error("Bad experiment_log value: %s" % level_str)
165
166def copy_file(src, dest, size=1024):
167    """
168    Exceedingly simple file copy.  Throws an EnvironmentError if there's a problem.
169    """
170    s = open(src,'r')
171    d = open(dest, 'w')
172
173    buf = s.read(size)
174    while buf != "":
175        d.write(buf)
176        buf = s.read(size)
177    s.close()
178    d.close()
179
180def get_url(url, cf, destdir, fn=None, max_retries=5):
181    """
182    Get data from a federated data store.  This presents the client cert/fedid
183    to the http server.  We retry up to max_retries times.
184    """
185    po = urlparse(url)
186    if not fn:
187        fn = po.path.rpartition('/')[2]
188    retries = 0
189    ok = False
190    failed_exception = None
191    while not ok and retries < 5:
192        try:
193            conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
194                    cert_file=cf, key_file=cf)
195            conn.putrequest('GET', po.path)
196            conn.endheaders()
197            response = conn.getresponse()
198
199            lf = open("%s/%s" % (destdir, fn), "w")
200            buf = response.read(4096)
201            while buf:
202                lf.write(buf)
203                buf = response.read(4096)
204            lf.close()
205            ok = True
206        except EnvironmentError, e:
207            failed_excpetion = e
208            retries += 1
209        except httplib.HTTPException, e:
210            failed_exception = e
211            retries += 1
212        except sslerror, e:
213            failed_exception = e
214            retries += 1
215        except SSLError, e:
216            failed_exception = e
217            retries += 1
218
219    if retries > 5 and failed_exception:
220        raise failed_excpetion
221
222# Functions to manipulate composite testbed names
223def testbed_base(tb):
224    """
225    Simple code to get the base testebd name.
226    """
227    i = tb.find('/')
228    if i == -1: return tb
229    else: return tb[0:i]
230
231def testbed_suffix(tb):
232    """
233    Simple code to get a testbed suffix, if nay.  No suffix returns None.
234    """
235    i = tb.find('/')
236    if i != -1: return tb[i+1:]
237    else: return None
238
239def split_testbed(tb):
240    """
241    Return a testbed and a suffix as a tuple.  No suffix returns None for that
242    field
243    """
244
245    i = tb.find('/')
246    if i != -1: return (tb[0:i], tb[i+1:])
247    else: return (tb, None)
248
249def join_testbed(base, suffix=None):
250    """
251    Build a testbed with suffix.  If base is itself a tuple, combine them,
252    otherwise combine the two.
253    """
254    if isinstance(base, tuple):
255        if len(base) == 2:
256            return '/'.join(base)
257        else:
258            raise RuntimeError("Too many tuple elements for join_testbed")
259    else:
260        if suffix:
261            return '/'.join((base, suffix))
262        else:
263            return base
264
265def abac_pem_type(cert):
266    key_re = re.compile('\s*-----BEGIN RSA PRIVATE KEY-----$')
267    cert_re = re.compile('\s*-----BEGIN CERTIFICATE-----$') 
268    type = None
269    f = open(cert, 'r')
270    for line in f:
271        if key_re.match(line):
272            if type is None: type = 'key'
273            elif type == 'cert': type = 'both'
274        elif cert_re.match(line):
275            if type is None: type = 'cert'
276            elif type == 'key': type = 'both'
277        if type == 'both': break
278    f.close()
279    return type
280
281def abac_split_cert(cert, keyfile=None, certfile=None):
282    """
283    Split the certificate file in cert into a certificate file and a key file
284    in cf and kf respectively.  The ABAC tools generally cannot handle combined
285    certificates/keys.  If kf anc cf are given, they are used, otherwise tmp
286    files are created.  Created tmp files must be deleted.  Problems opening or
287    writing files will cause exceptions.
288    """
289    class diversion:
290        '''
291        Wraps up the reqular expression to start and end a diversion, as well as
292        the open file that gets the lines.
293        '''
294        def __init__(self, start, end, fn):
295            self.start = re.compile(start)
296            self.end = re.compile(end)
297            # Open the file securely with minimal permissions. NB file cannot
298            # exist before this call.
299            self.f = os.fdopen(os.open(fn,
300                (os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_EXCL), 0600),
301                'w')
302
303        def close(self):
304            self.f.close()
305
306    if not keyfile:
307        f, keyfile = mkstemp(suffix=".pem")
308        os.close(f);
309    if not certfile:
310        f, certfile = mkstemp(suffix=".pem")
311        os.close(f);
312
313    # Initialize the diversions
314    divs = [diversion(s, e, fn) for s, e,fn in (
315        ('\s*-----BEGIN RSA PRIVATE KEY-----$', 
316            '\s*-----END RSA PRIVATE KEY-----$',
317            keyfile),
318        ('\s*-----BEGIN CERTIFICATE-----$', 
319            '\s*-----END CERTIFICATE-----$',
320            certfile))]
321
322    # walk through the file, beginning a diversion when a start regexp
323    # matches until the end regexp matches.  While in the two regexps,
324    # print each line to the open diversion file (including the two
325    # matches).
326    active = None
327    f = open(cert, 'r')
328    for l in f:
329        if active:
330            if active.end.match(l):
331                print >>active.f, l,
332                active = None
333        else:
334            for d in divs:
335                if d.start.match(l):
336                    active = d
337                    break
338        if active: print >>active.f, l,
339
340    # This is probably unnecessary.  Close all the diversion files.
341    for d in divs: d.close()
342    return keyfile, certfile
343
344def find_pickle_problem(o, st=None):
345    """
346    Debugging routine to figure out what doesn't pickle from a dict full of
347    dicts and lists.  It tries to walk down the lists and dicts and pickle each
348    atom.  If something fails to pickle, it prints an approximation of a stack
349    trace through the data structure.
350    """
351    if st is None: st = [ ]
352
353    if isinstance(o, dict):
354        for k, i in o.items():
355            st.append(k)
356            find_pickle_problem(i, st)
357            st.pop()
358    elif isinstance(o, list):
359        st.append('list')
360        for i in o:
361            find_pickle_problem(i, st)
362        st.pop()
363    else:
364        try:
365            pickle.dumps(o)
366        except pickle.PicklingError, e:
367            print >>sys.stderr, "<PicklingError>"
368            print >>sys.stderr, st
369            print >>sys.stderr, o
370            print >>sys.stderr, e
371            print >>sys.stderr, "</PicklingError>"
372
Note: See TracBrowser for help on using the repository browser.