/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: Björn Påhlsson
  • Date: 2011-11-09 11:16:17 UTC
  • mto: (518.2.5 persistent-state-gpgme)
  • mto: This revision was merged to the branch mainline in revision 520.
  • Revision ID: belorn@fukt.bsnet.se-20111109111617-jpey2sy4dupqzabc
Persistent state: New feature. Client state is now stored when mandos
                  server exits, and restored when starting up. Secrets
                  are encrypted with key based on clients config file.
--no-restore: New argument to mandos server. Disables restore
              functionallity
minor changes: clients are now dicts!

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
66
67
 
67
68
import dbus
68
69
import dbus.service
73
74
import ctypes.util
74
75
import xml.dom.minidom
75
76
import inspect
 
77
import Crypto.Cipher.AES
76
78
 
77
79
try:
78
80
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
85
87
 
86
88
version = "1.4.1"
87
89
 
 
90
stored_state_path = "/var/lib/mandos/clients.pickle"
 
91
 
88
92
#logger = logging.getLogger('mandos')
89
93
logger = logging.Logger('mandos')
90
94
syslogger = (logging.handlers.SysLogHandler
289
293
                     instance %(name)s can be used in the command.
290
294
    checker_initiator_tag: a gobject event source tag, or None
291
295
    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
292
298
    current_checker_command: string; current running checker_command
293
 
    disable_hook:  If set, called by disable() as disable_hook(self)
294
299
    disable_initiator_tag: a gobject event source tag, or None
295
300
    enabled:    bool()
296
301
    fingerprint: string (40 or 32 hexadecimal digits); used to
299
304
    interval:   datetime.timedelta(); How often to start a new checker
300
305
    last_approval_request: datetime.datetime(); (UTC) or None
301
306
    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.
302
309
    last_enabled: datetime.datetime(); (UTC)
303
310
    name:       string; from the config file, used in log messages and
304
311
                        D-Bus identifiers
331
338
    def approval_delay_milliseconds(self):
332
339
        return _timedelta_to_milliseconds(self.approval_delay)
333
340
    
334
 
    def __init__(self, name = None, disable_hook=None, config=None):
 
341
    def __init__(self, name = None, config=None):
335
342
        """Note: the 'checker' key in 'config' sets the
336
343
        'checker_command' attribute and *not* the 'checker'
337
344
        attribute."""
357
364
                            % self.name)
358
365
        self.host = config.get("host", "")
359
366
        self.created = datetime.datetime.utcnow()
360
 
        self.enabled = False
 
367
        self.enabled = True
361
368
        self.last_approval_request = None
362
 
        self.last_enabled = None
 
369
        self.last_enabled = datetime.datetime.utcnow()
363
370
        self.last_checked_ok = None
 
371
        self.last_checker_status = 0
364
372
        self.timeout = string_to_delta(config["timeout"])
365
373
        self.extended_timeout = string_to_delta(config
366
374
                                                ["extended_timeout"])
367
375
        self.interval = string_to_delta(config["interval"])
368
 
        self.disable_hook = disable_hook
369
376
        self.checker = None
370
377
        self.checker_initiator_tag = None
371
378
        self.disable_initiator_tag = None
372
 
        self.expires = None
 
379
        self.expires = datetime.datetime.utcnow() + self.timeout
373
380
        self.checker_callback_tag = None
374
381
        self.checker_command = config["checker"]
375
382
        self.current_checker_command = None
376
 
        self.last_connect = None
377
383
        self._approved = None
378
384
        self.approved_by_default = config.get("approved_by_default",
379
385
                                              True)
385
391
        self.changedstate = (multiprocessing_manager
386
392
                             .Condition(multiprocessing_manager
387
393
                                        .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
    
388
403
    
389
404
    def send_changedstate(self):
390
405
        self.changedstate.acquire()
397
412
            # Already enabled
398
413
            return
399
414
        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
406
415
        self.expires = datetime.datetime.utcnow() + self.timeout
407
 
        self.disable_initiator_tag = (gobject.timeout_add
408
 
                                   (self.timeout_milliseconds(),
409
 
                                    self.disable))
410
416
        self.enabled = True
411
417
        self.last_enabled = datetime.datetime.utcnow()
412
 
        # Also start a new checker *right now*.
413
 
        self.start_checker()
 
418
        self.init_checker()
414
419
    
415
420
    def disable(self, quiet=True):
416
421
        """Disable this client."""
428
433
            gobject.source_remove(self.checker_initiator_tag)
429
434
            self.checker_initiator_tag = None
430
435
        self.stop_checker()
431
 
        if self.disable_hook:
432
 
            self.disable_hook(self)
433
436
        self.enabled = False
434
437
        # Do not run this again if called by a gobject.timeout_add
435
438
        return False
436
439
    
437
440
    def __del__(self):
438
 
        self.disable_hook = None
439
441
        self.disable()
440
 
    
 
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
        
441
457
    def checker_callback(self, pid, condition, command):
442
458
        """The checker has completed, so take appropriate actions."""
443
459
        self.checker_callback_tag = None
444
460
        self.checker = None
445
461
        if os.WIFEXITED(condition):
446
 
            exitstatus = os.WEXITSTATUS(condition)
447
 
            if exitstatus == 0:
 
462
            self.last_checker_status =  os.WEXITSTATUS(condition)
 
463
            if self.last_checker_status == 0:
448
464
                logger.info("Checker for %(name)s succeeded",
449
465
                            vars(self))
450
466
                self.checked_ok()
452
468
                logger.info("Checker for %(name)s failed",
453
469
                            vars(self))
454
470
        else:
 
471
            self.last_checker_status = -1
455
472
            logger.warning("Checker for %(name)s crashed?",
456
473
                           vars(self))
457
474
    
568
585
                raise
569
586
        self.checker = None
570
587
 
 
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
 
571
634
 
572
635
def dbus_service_property(dbus_interface, signature="v",
573
636
                          access="readwrite", byte_arrays=False):
874
937
    # dbus.service.Object doesn't use super(), so we can't either.
875
938
    
876
939
    def __init__(self, bus = None, *args, **kwargs):
 
940
        self.bus = bus
 
941
        Client.__init__(self, *args, **kwargs)
 
942
 
877
943
        self._approvals_pending = 0
878
 
        self.bus = bus
879
 
        Client.__init__(self, *args, **kwargs)
880
944
        # Only now, when this client is initialized, can it show up on
881
945
        # the D-Bus
882
946
        client_object_name = unicode(self.name).translate(
1624
1688
        self.enabled = False
1625
1689
        self.clients = clients
1626
1690
        if self.clients is None:
1627
 
            self.clients = set()
 
1691
            self.clients = {}
1628
1692
        self.use_dbus = use_dbus
1629
1693
        self.gnutls_priority = gnutls_priority
1630
1694
        IPv6_TCPServer.__init__(self, server_address,
1677
1741
            fpr = request[1]
1678
1742
            address = request[2]
1679
1743
            
1680
 
            for c in self.clients:
 
1744
            for c in self.clients.itervalues():
1681
1745
                if c.fingerprint == fpr:
1682
1746
                    client = c
1683
1747
                    break
1851
1915
                        " system bus interface")
1852
1916
    parser.add_argument("--no-ipv6", action="store_false",
1853
1917
                        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
 
1854
1922
    options = parser.parse_args()
1855
1923
    
1856
1924
    if options.check:
1891
1959
    # options, if set.
1892
1960
    for option in ("interface", "address", "port", "debug",
1893
1961
                   "priority", "servicename", "configdir",
1894
 
                   "use_dbus", "use_ipv6", "debuglevel"):
 
1962
                   "use_dbus", "use_ipv6", "debuglevel", "restore"):
1895
1963
        value = getattr(options, option)
1896
1964
        if value is not None:
1897
1965
            server_settings[option] = value
2038
2106
    if use_dbus:
2039
2107
        client_class = functools.partial(ClientDBusTransitional,
2040
2108
                                         bus = bus)
2041
 
    def client_config_items(config, section):
2042
 
        special_settings = {
2043
 
            "approved_by_default":
2044
 
                lambda: config.getboolean(section,
2045
 
                                          "approved_by_default"),
2046
 
            }
2047
 
        for name, value in config.items(section):
 
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():
2048
2149
            try:
2049
 
                yield (name, special_settings[name]())
 
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)
2050
2155
            except KeyError:
2051
 
                yield (name, value)
 
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]))
2052
2202
    
2053
 
    tcp_server.clients.update(set(
2054
 
            client_class(name = section,
2055
 
                         config= dict(client_config_items(
2056
 
                        client_config, section)))
2057
 
            for section in client_config.sections()))
 
2203
 
2058
2204
    if not tcp_server.clients:
2059
2205
        logger.warning("No clients defined")
2060
2206
        
2103
2249
            def GetAllClients(self):
2104
2250
                "D-Bus method"
2105
2251
                return dbus.Array(c.dbus_object_path
2106
 
                                  for c in tcp_server.clients)
 
2252
                                  for c in
 
2253
                                  tcp_server.clients.itervalues())
2107
2254
            
2108
2255
            @dbus.service.method(_interface,
2109
2256
                                 out_signature="a{oa{sv}}")
2111
2258
                "D-Bus method"
2112
2259
                return dbus.Dictionary(
2113
2260
                    ((c.dbus_object_path, c.GetAll(""))
2114
 
                     for c in tcp_server.clients),
 
2261
                     for c in tcp_server.clients.itervalues()),
2115
2262
                    signature="oa{sv}")
2116
2263
            
2117
2264
            @dbus.service.method(_interface, in_signature="o")
2118
2265
            def RemoveClient(self, object_path):
2119
2266
                "D-Bus method"
2120
 
                for c in tcp_server.clients:
 
2267
                for c in tcp_server.clients.itervalues():
2121
2268
                    if c.dbus_object_path == object_path:
2122
 
                        tcp_server.clients.remove(c)
 
2269
                        del tcp_server.clients[c.name]
2123
2270
                        c.remove_from_connection()
2124
2271
                        # Don't signal anything except ClientRemoved
2125
2272
                        c.disable(quiet=True)
2139
2286
        service.cleanup()
2140
2287
        
2141
2288
        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
2142
2323
        while tcp_server.clients:
2143
 
            client = tcp_server.clients.pop()
 
2324
            name, client = tcp_server.clients.popitem()
2144
2325
            if use_dbus:
2145
2326
                client.remove_from_connection()
2146
 
            client.disable_hook = None
2147
2327
            # Don't signal anything except ClientRemoved
2148
2328
            client.disable(quiet=True)
2149
2329
            if use_dbus:
2150
2330
                # Emit D-Bus signal
2151
2331
                mandos_dbus_service.ClientRemoved(client
2152
 
                                                  .dbus_object_path,
2153
 
                                                  client.name)
 
2332
                                              .dbus_object_path,
 
2333
                                              client.name)
 
2334
        client_settings.clear()
2154
2335
    
2155
2336
    atexit.register(cleanup)
2156
2337
    
2157
 
    for client in tcp_server.clients:
 
2338
    for client in tcp_server.clients.itervalues():
2158
2339
        if use_dbus:
2159
2340
            # Emit D-Bus signal
2160
2341
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
2161
 
        client.enable()
 
2342
        # Need to initiate checking of clients
 
2343
        if client.enabled:
 
2344
            client.init_checker()
 
2345
 
2162
2346
    
2163
2347
    tcp_server.enable()
2164
2348
    tcp_server.server_activate()