source: fedd/federation/util.py @ 816daef

compt_changesinfo-ops
Last change on this file since 816daef was 816daef, checked in by Ted Faber <faber@…>, 9 years ago

New SSL error code complicates self-signed certs

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