/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2011-10-10 20:30:55 UTC
  • mfrom: (505.1.13 teddy)
  • Revision ID: teddy@recompile.se-20111010203055-a4crqy7gw2ln252r
Miscellaneous fixes prompted by lintian:

* debian/control (Conflicts): Changed to "Breaks:".
* debian/copyright: Updated format.
* debian/mandos-client.postinst: Use "set -e" instead of "#!/bin/sh -e".
* debian/mandos-client.postrm: - '' -
* debian/mandos.postinst: - '' -
* debian/mandos.prerm: Consistent magic.
* mandos: Small comment change.
* mandos-clients.conf.xml (OPTIONS/extended_timeout): Fix spelling.

Show diffs side-by-side

added added

removed removed

Lines of Context:
63
63
import cPickle as pickle
64
64
import multiprocessing
65
65
import types
66
 
import hashlib
67
66
 
68
67
import dbus
69
68
import dbus.service
74
73
import ctypes.util
75
74
import xml.dom.minidom
76
75
import inspect
77
 
import Crypto.Cipher.AES
78
76
 
79
77
try:
80
78
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
85
83
        SO_BINDTODEVICE = None
86
84
 
87
85
 
88
 
version = "1.4.1"
89
 
 
90
 
stored_state_path = "/var/lib/mandos/clients.pickle"
 
86
version = "1.4.0"
91
87
 
92
88
#logger = logging.getLogger('mandos')
93
89
logger = logging.Logger('mandos')
293
289
                     instance %(name)s can be used in the command.
294
290
    checker_initiator_tag: a gobject event source tag, or None
295
291
    created:    datetime.datetime(); (UTC) object creation
296
 
    client_structure: Object describing what attributes a client has
297
 
                      and is used for storing the client at exit
298
292
    current_checker_command: string; current running checker_command
 
293
    disable_hook:  If set, called by disable() as disable_hook(self)
299
294
    disable_initiator_tag: a gobject event source tag, or None
300
295
    enabled:    bool()
301
296
    fingerprint: string (40 or 32 hexadecimal digits); used to
304
299
    interval:   datetime.timedelta(); How often to start a new checker
305
300
    last_approval_request: datetime.datetime(); (UTC) or None
306
301
    last_checked_ok: datetime.datetime(); (UTC) or None
307
 
    Last_checker_status: integer between 0 and 255 reflecting exit status
308
 
                         of last checker. -1 reflect crashed checker.
309
302
    last_enabled: datetime.datetime(); (UTC)
310
303
    name:       string; from the config file, used in log messages and
311
304
                        D-Bus identifiers
338
331
    def approval_delay_milliseconds(self):
339
332
        return _timedelta_to_milliseconds(self.approval_delay)
340
333
    
341
 
    def __init__(self, name = None, config=None):
 
334
    def __init__(self, name = None, disable_hook=None, config=None):
342
335
        """Note: the 'checker' key in 'config' sets the
343
336
        'checker_command' attribute and *not* the 'checker'
344
337
        attribute."""
364
357
                            % self.name)
365
358
        self.host = config.get("host", "")
366
359
        self.created = datetime.datetime.utcnow()
367
 
        self.enabled = True
 
360
        self.enabled = False
368
361
        self.last_approval_request = None
369
 
        self.last_enabled = datetime.datetime.utcnow()
 
362
        self.last_enabled = None
370
363
        self.last_checked_ok = None
371
 
        self.last_checker_status = 0
372
364
        self.timeout = string_to_delta(config["timeout"])
373
365
        self.extended_timeout = string_to_delta(config
374
366
                                                ["extended_timeout"])
375
367
        self.interval = string_to_delta(config["interval"])
 
368
        self.disable_hook = disable_hook
376
369
        self.checker = None
377
370
        self.checker_initiator_tag = None
378
371
        self.disable_initiator_tag = None
379
 
        self.expires = datetime.datetime.utcnow() + self.timeout
 
372
        self.expires = None
380
373
        self.checker_callback_tag = None
381
374
        self.checker_command = config["checker"]
382
375
        self.current_checker_command = None
 
376
        self.last_connect = None
383
377
        self._approved = None
384
378
        self.approved_by_default = config.get("approved_by_default",
385
379
                                              True)
391
385
        self.changedstate = (multiprocessing_manager
392
386
                             .Condition(multiprocessing_manager
393
387
                                        .Lock()))
394
 
        self.client_structure = [attr for attr in self.__dict__.iterkeys() if not attr.startswith("_")]
395
 
        self.client_structure.append("client_structure")
396
 
 
397
 
 
398
 
        for name, t in inspect.getmembers(type(self),
399
 
                                          lambda obj: isinstance(obj, property)):
400
 
            if not name.startswith("_"):
401
 
                self.client_structure.append(name)
402
 
    
403
388
    
404
389
    def send_changedstate(self):
405
390
        self.changedstate.acquire()
412
397
            # Already enabled
413
398
            return
414
399
        self.send_changedstate()
 
400
        # Schedule a new checker to be started an 'interval' from now,
 
401
        # and every interval from then on.
 
402
        self.checker_initiator_tag = (gobject.timeout_add
 
403
                                      (self.interval_milliseconds(),
 
404
                                       self.start_checker))
 
405
        # Schedule a disable() when 'timeout' has passed
415
406
        self.expires = datetime.datetime.utcnow() + self.timeout
 
407
        self.disable_initiator_tag = (gobject.timeout_add
 
408
                                   (self.timeout_milliseconds(),
 
409
                                    self.disable))
416
410
        self.enabled = True
417
411
        self.last_enabled = datetime.datetime.utcnow()
418
 
        self.init_checker()
 
412
        # Also start a new checker *right now*.
 
413
        self.start_checker()
419
414
    
420
415
    def disable(self, quiet=True):
421
416
        """Disable this client."""
433
428
            gobject.source_remove(self.checker_initiator_tag)
434
429
            self.checker_initiator_tag = None
435
430
        self.stop_checker()
 
431
        if self.disable_hook:
 
432
            self.disable_hook(self)
436
433
        self.enabled = False
437
434
        # Do not run this again if called by a gobject.timeout_add
438
435
        return False
439
436
    
440
437
    def __del__(self):
 
438
        self.disable_hook = None
441
439
        self.disable()
442
 
 
443
 
    def init_checker(self):
444
 
        # Schedule a new checker to be started an 'interval' from now,
445
 
        # and every interval from then on.
446
 
        self.checker_initiator_tag = (gobject.timeout_add
447
 
                                      (self.interval_milliseconds(),
448
 
                                       self.start_checker))
449
 
        # Schedule a disable() when 'timeout' has passed
450
 
        self.disable_initiator_tag = (gobject.timeout_add
451
 
                                   (self.timeout_milliseconds(),
452
 
                                    self.disable))
453
 
        # Also start a new checker *right now*.
454
 
        self.start_checker()
455
 
 
456
 
        
 
440
    
457
441
    def checker_callback(self, pid, condition, command):
458
442
        """The checker has completed, so take appropriate actions."""
459
443
        self.checker_callback_tag = None
460
444
        self.checker = None
461
445
        if os.WIFEXITED(condition):
462
 
            self.last_checker_status =  os.WEXITSTATUS(condition)
463
 
            if self.last_checker_status == 0:
 
446
            exitstatus = os.WEXITSTATUS(condition)
 
447
            if exitstatus == 0:
464
448
                logger.info("Checker for %(name)s succeeded",
465
449
                            vars(self))
466
450
                self.checked_ok()
468
452
                logger.info("Checker for %(name)s failed",
469
453
                            vars(self))
470
454
        else:
471
 
            self.last_checker_status = -1
472
455
            logger.warning("Checker for %(name)s crashed?",
473
456
                           vars(self))
474
457
    
481
464
        if timeout is None:
482
465
            timeout = self.timeout
483
466
        self.last_checked_ok = datetime.datetime.utcnow()
484
 
        if self.disable_initiator_tag is not None:
485
 
            gobject.source_remove(self.disable_initiator_tag)
486
 
        if getattr(self, "enabled", False):
487
 
            self.disable_initiator_tag = (gobject.timeout_add
488
 
                                          (_timedelta_to_milliseconds
489
 
                                           (timeout), self.disable))
490
 
            self.expires = datetime.datetime.utcnow() + timeout
 
467
        gobject.source_remove(self.disable_initiator_tag)
 
468
        self.disable_initiator_tag = (gobject.timeout_add
 
469
                                      (_timedelta_to_milliseconds
 
470
                                       (timeout), self.disable))
 
471
        self.expires = datetime.datetime.utcnow() + timeout
491
472
    
492
473
    def need_approval(self):
493
474
        self.last_approval_request = datetime.datetime.utcnow()
585
566
                raise
586
567
        self.checker = None
587
568
 
588
 
    # Encrypts a client secret and stores it in a varible encrypted_secret
589
 
    def encrypt_secret(self, key):
590
 
        # Encryption-key need to be specific size, so we hash inputed key
591
 
        hasheng = hashlib.sha256()
592
 
        hasheng.update(key)
593
 
        encryptionkey = hasheng.digest()
594
 
 
595
 
        # Create validation hash so we know at decryption if it was sucessful
596
 
        hasheng = hashlib.sha256()
597
 
        hasheng.update(self.secret)
598
 
        validationhash = hasheng.digest()
599
 
 
600
 
        # Encrypt secret
601
 
        iv = os.urandom(Crypto.Cipher.AES.block_size)
602
 
        ciphereng = Crypto.Cipher.AES.new(encryptionkey,
603
 
                                        Crypto.Cipher.AES.MODE_CFB, iv)
604
 
        ciphertext = ciphereng.encrypt(validationhash+self.secret)
605
 
        self.encrypted_secret = (ciphertext, iv)
606
 
 
607
 
    # Decrypt a encrypted client secret
608
 
    def decrypt_secret(self, key):
609
 
        # Decryption-key need to be specific size, so we hash inputed key
610
 
        hasheng = hashlib.sha256()
611
 
        hasheng.update(key)
612
 
        encryptionkey = hasheng.digest()
613
 
 
614
 
        # Decrypt encrypted secret
615
 
        ciphertext, iv = self.encrypted_secret
616
 
        ciphereng = Crypto.Cipher.AES.new(encryptionkey,
617
 
                                        Crypto.Cipher.AES.MODE_CFB, iv)
618
 
        plain = ciphereng.decrypt(ciphertext)
619
 
 
620
 
        # Validate decrypted secret to know if it was succesful
621
 
        hasheng = hashlib.sha256()
622
 
        validationhash = plain[:hasheng.digest_size]
623
 
        secret = plain[hasheng.digest_size:]
624
 
        hasheng.update(secret)
625
 
 
626
 
        # if validation fails, we use key as new secret. Otherwhise, we use
627
 
        # the decrypted secret
628
 
        if hasheng.digest() == validationhash:
629
 
            self.secret = secret
630
 
        else:
631
 
            self.secret = key
632
 
        del self.encrypted_secret
633
 
 
634
569
 
635
570
def dbus_service_property(dbus_interface, signature="v",
636
571
                          access="readwrite", byte_arrays=False):
937
872
    # dbus.service.Object doesn't use super(), so we can't either.
938
873
    
939
874
    def __init__(self, bus = None, *args, **kwargs):
 
875
        self._approvals_pending = 0
940
876
        self.bus = bus
941
877
        Client.__init__(self, *args, **kwargs)
942
 
 
943
 
        self._approvals_pending = 0
944
878
        # Only now, when this client is initialized, can it show up on
945
879
        # the D-Bus
946
880
        client_object_name = unicode(self.name).translate(
957
891
        """ Modify a variable so that it's a property which announces
958
892
        its changes to DBus.
959
893
 
960
 
        transform_fun: Function that takes a value and a variant_level
961
 
                       and transforms it to a D-Bus type.
 
894
        transform_fun: Function that takes a value and transforms it
 
895
                       to a D-Bus type.
962
896
        dbus_name: D-Bus name of the variable
963
897
        type_func: Function that transform the value before sending it
964
898
                   to the D-Bus.  Default: no transform
971
905
                    type_func(getattr(self, attrname, None))
972
906
                    != type_func(value)):
973
907
                    dbus_value = transform_func(type_func(value),
974
 
                                                variant_level
975
 
                                                =variant_level)
 
908
                                                variant_level)
976
909
                    self.PropertyChanged(dbus.String(dbus_name),
977
910
                                         dbus_value)
978
911
            setattr(self, attrname, value)
1254
1187
        gobject.source_remove(self.disable_initiator_tag)
1255
1188
        self.disable_initiator_tag = None
1256
1189
        self.expires = None
1257
 
        time_to_die = _timedelta_to_milliseconds((self
1258
 
                                                  .last_checked_ok
1259
 
                                                  + self.timeout)
1260
 
                                                 - datetime.datetime
1261
 
                                                 .utcnow())
 
1190
        time_to_die = (self.
 
1191
                       _timedelta_to_milliseconds((self
 
1192
                                                   .last_checked_ok
 
1193
                                                   + self.timeout)
 
1194
                                                  - datetime.datetime
 
1195
                                                  .utcnow()))
1262
1196
        if time_to_die <= 0:
1263
1197
            # The timeout has passed
1264
1198
            self.disable()
1688
1622
        self.enabled = False
1689
1623
        self.clients = clients
1690
1624
        if self.clients is None:
1691
 
            self.clients = {}
 
1625
            self.clients = set()
1692
1626
        self.use_dbus = use_dbus
1693
1627
        self.gnutls_priority = gnutls_priority
1694
1628
        IPv6_TCPServer.__init__(self, server_address,
1741
1675
            fpr = request[1]
1742
1676
            address = request[2]
1743
1677
            
1744
 
            for c in self.clients.itervalues():
 
1678
            for c in self.clients:
1745
1679
                if c.fingerprint == fpr:
1746
1680
                    client = c
1747
1681
                    break
1915
1849
                        " system bus interface")
1916
1850
    parser.add_argument("--no-ipv6", action="store_false",
1917
1851
                        dest="use_ipv6", help="Do not use IPv6")
1918
 
    parser.add_argument("--no-restore", action="store_false",
1919
 
                        dest="restore", help="Do not restore old state",
1920
 
                        default=True)
1921
 
 
1922
1852
    options = parser.parse_args()
1923
1853
    
1924
1854
    if options.check:
1959
1889
    # options, if set.
1960
1890
    for option in ("interface", "address", "port", "debug",
1961
1891
                   "priority", "servicename", "configdir",
1962
 
                   "use_dbus", "use_ipv6", "debuglevel", "restore"):
 
1892
                   "use_dbus", "use_ipv6", "debuglevel"):
1963
1893
        value = getattr(options, option)
1964
1894
        if value is not None:
1965
1895
            server_settings[option] = value
2106
2036
    if use_dbus:
2107
2037
        client_class = functools.partial(ClientDBusTransitional,
2108
2038
                                         bus = bus)
2109
 
    
2110
 
    special_settings = {
2111
 
        # Some settings need to be accessd by special methods;
2112
 
        # booleans need .getboolean(), etc.  Here is a list of them:
2113
 
        "approved_by_default":
2114
 
            lambda section:
2115
 
            client_config.getboolean(section, "approved_by_default"),
2116
 
        }
2117
 
    # Construct a new dict of client settings of this form:
2118
 
    # { client_name: {setting_name: value, ...}, ...}
2119
 
    # with exceptions for any special settings as defined above
2120
 
    client_settings = dict((clientname,
2121
 
                           dict((setting,
2122
 
                                 (value if setting not in special_settings
2123
 
                                  else special_settings[setting](clientname)))
2124
 
                                for setting, value in client_config.items(clientname)))
2125
 
                          for clientname in client_config.sections())
2126
 
    
2127
 
    old_client_settings = {}
2128
 
    clients_data = []
2129
 
 
2130
 
    if server_settings["restore"]:
2131
 
        try:
2132
 
            with open(stored_state_path, "rb") as stored_state:
2133
 
                clients_data, old_client_settings = pickle.load(stored_state)
2134
 
            os.remove(stored_state_path)
2135
 
        except IOError as e:
2136
 
            logger.warning("Could not load persistant state: {0}".format(e))
2137
 
            if e.errno != errno.ENOENT:
2138
 
                raise
2139
 
 
2140
 
    for client in clients_data:
2141
 
        client_name = client["name"]
2142
 
        
2143
 
        # Decide which value to use after restoring saved state.
2144
 
        # We have three different values: Old config file,
2145
 
        # new config file, and saved state.
2146
 
        # New config value takes precedence if it differs from old
2147
 
        # config value, otherwise use saved state.
2148
 
        for name, value in client_settings[client_name].items():
 
2039
    def client_config_items(config, section):
 
2040
        special_settings = {
 
2041
            "approved_by_default":
 
2042
                lambda: config.getboolean(section,
 
2043
                                          "approved_by_default"),
 
2044
            }
 
2045
        for name, value in config.items(section):
2149
2046
            try:
2150
 
                # For each value in new config, check if it differs
2151
 
                # from the old config value (Except for the "secret"
2152
 
                # attribute)
2153
 
                if name != "secret" and value != old_client_settings[client_name][name]:
2154
 
                    setattr(client, name, value)
 
2047
                yield (name, special_settings[name]())
2155
2048
            except KeyError:
2156
 
                pass
2157
 
 
2158
 
        # Clients who has passed its expire date, can still be enabled if its
2159
 
        # last checker was sucessful. Clients who checkers failed before we
2160
 
        # stored it state is asumed to had failed checker during downtime.
2161
 
        if client["enabled"] and client["last_checked_ok"]:
2162
 
            if ((datetime.datetime.utcnow() - client["last_checked_ok"])
2163
 
                > client["interval"]):
2164
 
                if client["last_checker_status"] != 0:
2165
 
                    client["enabled"] = False
2166
 
                else:
2167
 
                    client["expires"] = datetime.datetime.utcnow() + client["timeout"]
2168
 
 
2169
 
        client["changedstate"] = (multiprocessing_manager
2170
 
                                  .Condition(multiprocessing_manager
2171
 
                                             .Lock()))
2172
 
        if use_dbus:
2173
 
            new_client = ClientDBusTransitional.__new__(ClientDBusTransitional)
2174
 
            tcp_server.clients[client_name] = new_client
2175
 
            new_client.bus = bus
2176
 
            for name, value in client.iteritems():
2177
 
                setattr(new_client, name, value)
2178
 
            client_object_name = unicode(client_name).translate(
2179
 
                {ord("."): ord("_"),
2180
 
                 ord("-"): ord("_")})
2181
 
            new_client.dbus_object_path = (dbus.ObjectPath
2182
 
                                     ("/clients/" + client_object_name))
2183
 
            DBusObjectWithProperties.__init__(new_client,
2184
 
                                              new_client.bus,
2185
 
                                              new_client.dbus_object_path)
2186
 
        else:
2187
 
            tcp_server.clients[client_name] = Client.__new__(Client)
2188
 
            for name, value in client.iteritems():
2189
 
                setattr(tcp_server.clients[client_name], name, value)
2190
 
                
2191
 
        tcp_server.clients[client_name].decrypt_secret(
2192
 
            client_settings[client_name]["secret"])            
2193
 
        
2194
 
    # Create/remove clients based on new changes made to config
2195
 
    for clientname in set(old_client_settings) - set(client_settings):
2196
 
        del tcp_server.clients[clientname]
2197
 
    for clientname in set(client_settings) - set(old_client_settings):
2198
 
        tcp_server.clients[clientname] = (client_class(name = clientname,
2199
 
                                                       config =
2200
 
                                                       client_settings
2201
 
                                                       [clientname]))
 
2049
                yield (name, value)
2202
2050
    
2203
 
 
 
2051
    tcp_server.clients.update(set(
 
2052
            client_class(name = section,
 
2053
                         config= dict(client_config_items(
 
2054
                        client_config, section)))
 
2055
            for section in client_config.sections()))
2204
2056
    if not tcp_server.clients:
2205
2057
        logger.warning("No clients defined")
2206
2058
        
2249
2101
            def GetAllClients(self):
2250
2102
                "D-Bus method"
2251
2103
                return dbus.Array(c.dbus_object_path
2252
 
                                  for c in
2253
 
                                  tcp_server.clients.itervalues())
 
2104
                                  for c in tcp_server.clients)
2254
2105
            
2255
2106
            @dbus.service.method(_interface,
2256
2107
                                 out_signature="a{oa{sv}}")
2258
2109
                "D-Bus method"
2259
2110
                return dbus.Dictionary(
2260
2111
                    ((c.dbus_object_path, c.GetAll(""))
2261
 
                     for c in tcp_server.clients.itervalues()),
 
2112
                     for c in tcp_server.clients),
2262
2113
                    signature="oa{sv}")
2263
2114
            
2264
2115
            @dbus.service.method(_interface, in_signature="o")
2265
2116
            def RemoveClient(self, object_path):
2266
2117
                "D-Bus method"
2267
 
                for c in tcp_server.clients.itervalues():
 
2118
                for c in tcp_server.clients:
2268
2119
                    if c.dbus_object_path == object_path:
2269
 
                        del tcp_server.clients[c.name]
 
2120
                        tcp_server.clients.remove(c)
2270
2121
                        c.remove_from_connection()
2271
2122
                        # Don't signal anything except ClientRemoved
2272
2123
                        c.disable(quiet=True)
2286
2137
        service.cleanup()
2287
2138
        
2288
2139
        multiprocessing.active_children()
2289
 
        if not (tcp_server.clients or client_settings):
2290
 
            return
2291
 
 
2292
 
        # Store client before exiting. Secrets are encrypted with key based
2293
 
        # on what config file has. If config file is removed/edited, old
2294
 
        # secret will thus be unrecovable.
2295
 
        clients = []
2296
 
        for client in tcp_server.clients.itervalues():
2297
 
            client.encrypt_secret(client_settings[client.name]["secret"])
2298
 
 
2299
 
            client_dict = {}
2300
 
 
2301
 
            # A list of attributes that will not be stored when shuting down.
2302
 
            exclude = set(("bus", "changedstate", "secret"))            
2303
 
            for name, typ in inspect.getmembers(dbus.service.Object):
2304
 
                exclude.add(name)
2305
 
                
2306
 
            client_dict["encrypted_secret"] = client.encrypted_secret
2307
 
            for attr in client.client_structure:
2308
 
                if attr not in exclude:
2309
 
                    client_dict[attr] = getattr(client, attr)
2310
 
 
2311
 
            clients.append(client_dict) 
2312
 
            del client_settings[client.name]["secret"]
2313
 
            
2314
 
        try:
2315
 
            with os.fdopen(os.open(stored_state_path, os.O_CREAT|os.O_WRONLY|os.O_TRUNC, 0600), "wb") as stored_state:
2316
 
                pickle.dump((clients, client_settings), stored_state)
2317
 
        except IOError as e:
2318
 
            logger.warning("Could not save persistant state: {0}".format(e))
2319
 
            if e.errno != errno.ENOENT:
2320
 
                raise
2321
 
 
2322
 
        # Delete all clients, and settings from config
2323
2140
        while tcp_server.clients:
2324
 
            name, client = tcp_server.clients.popitem()
 
2141
            client = tcp_server.clients.pop()
2325
2142
            if use_dbus:
2326
2143
                client.remove_from_connection()
 
2144
            client.disable_hook = None
2327
2145
            # Don't signal anything except ClientRemoved
2328
2146
            client.disable(quiet=True)
2329
2147
            if use_dbus:
2330
2148
                # Emit D-Bus signal
2331
2149
                mandos_dbus_service.ClientRemoved(client
2332
 
                                              .dbus_object_path,
2333
 
                                              client.name)
2334
 
        client_settings.clear()
 
2150
                                                  .dbus_object_path,
 
2151
                                                  client.name)
2335
2152
    
2336
2153
    atexit.register(cleanup)
2337
2154
    
2338
 
    for client in tcp_server.clients.itervalues():
 
2155
    for client in tcp_server.clients:
2339
2156
        if use_dbus:
2340
2157
            # Emit D-Bus signal
2341
2158
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
2342
 
        # Need to initiate checking of clients
2343
 
        if client.enabled:
2344
 
            client.init_checker()
2345
 
 
 
2159
        client.enable()
2346
2160
    
2347
2161
    tcp_server.enable()
2348
2162
    tcp_server.server_activate()