=== modified file 'TODO' --- TODO 2009-11-01 00:10:28 +0000 +++ TODO 2009-12-25 23:13:47 +0000 @@ -68,6 +68,7 @@ secret ** TODO Persistent state /var/lib/mandos/* +** TODO Support RFC 3339 time duration syntax * mandos.xml ** [[file:mandos.xml::XXX][Document D-Bus interface]] @@ -77,16 +78,14 @@ * mandos-ctl *** Handle "no D-Bus server" and/or "no Mandos server found" better *** [#B] --dump option +** TODO Support RFC 3339 time duration syntax * TODO mandos-dispatch Listens for specified D-Bus signals and spawns shell commands with arguments. * mandos-monitor -** D-Bus main loop w/ signal receiver ** Urwid client data displayer -*** Urwid scaffolding -*** Client Widgets *** Properties popup * mandos-keygen === modified file 'clients.conf' --- clients.conf 2009-01-08 03:54:06 +0000 +++ clients.conf 2009-12-25 23:13:47 +0000 @@ -2,15 +2,15 @@ # values, so uncomment and change them if you want different ones. [DEFAULT] -# How long until a client is considered invalid - that is, ineligible -# to get the data this server holds. +# How long until a client is disabled and not be allowed to get the +# data this server holds. ;timeout = 1h # How often to run the checker to confirm that a client is still up. # Note: a new checker will not be started if an old one is still # running. The server will wait for a checker to complete until the -# above "timeout" occurs, at which time the client will be marked -# invalid, and any running checker killed. +# above "timeout" occurs, at which time the client will be disabled, +# and any running checker killed. ;interval = 5m # What command to run as "the checker". === modified file 'mandos' --- mandos 2009-11-15 10:12:09 +0000 +++ mandos 2009-12-25 23:13:47 +0000 @@ -55,10 +55,11 @@ import logging import logging.handlers import pwd -from contextlib import closing +import contextlib import struct import fcntl import functools +import cPickle as pickle import dbus import dbus.service @@ -242,7 +243,7 @@ enabled: bool() last_checked_ok: datetime.datetime(); (UTC) or None timeout: datetime.timedelta(); How long from last_checked_ok - until this client is invalid + until this client is disabled interval: datetime.timedelta(); How often to start a new checker disable_hook: If set, called by disable() as disable_hook(self) checker: subprocess.Popen(); a running checker process used @@ -290,10 +291,9 @@ if u"secret" in config: self.secret = config[u"secret"].decode(u"base64") elif u"secfile" in config: - with closing(open(os.path.expanduser - (os.path.expandvars - (config[u"secfile"])), - "rb")) as secfile: + with open(os.path.expanduser(os.path.expandvars + (config[u"secfile"])), + "rb") as secfile: self.secret = secfile.read() else: raise TypeError(u"No secret or secfile for client %s" @@ -396,8 +396,8 @@ # client would inevitably timeout, since no checker would get # a chance to run to completion. If we instead leave running # checkers alone, the checker would have to take more time - # than 'timeout' for the client to be declared invalid, which - # is as it should be. + # than 'timeout' for the client to be disabled, which is as it + # should be. # If a checker exists, make sure it is not a zombie try: @@ -475,16 +475,6 @@ if error.errno != errno.ESRCH: # No such process raise self.checker = None - - def still_valid(self): - """Has the timeout not yet passed for this client?""" - if not getattr(self, u"enabled", False): - return False - now = datetime.datetime.utcnow() - if self.last_checked_ok is None: - return now < (self.created + self.timeout) - else: - return now < (self.last_checked_ok + self.timeout) def dbus_service_property(dbus_interface, signature=u"v", @@ -499,6 +489,11 @@ dbus.service.method, except there is only "signature", since the type from Get() and the type sent to Set() is the same. """ + # Encoding deeply encoded byte arrays is not supported yet by the + # "Set" method, so we fail early here: + if byte_arrays and signature != u"ay": + raise ValueError(u"Byte arrays not supported for non-'ay'" + u" signature %r" % signature) def decorator(func): func._dbus_is_property = True func._dbus_interface = dbus_interface @@ -590,6 +585,10 @@ if prop._dbus_access == u"read": raise DBusPropertyAccessException(property_name) if prop._dbus_get_args_options[u"byte_arrays"]: + # The byte_arrays option is not supported yet on + # signatures other than "ay". + if prop._dbus_signature != u"ay": + raise ValueError value = dbus.ByteArray(''.join(unichr(byte) for byte in value)) prop(value) @@ -990,9 +989,13 @@ def handle(self): logger.info(u"TCP connection from: %s", unicode(self.client_address)) - logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1]) + logger.debug(u"IPC Pipe FD: %d", self.server.child_pipe[1]) # Open IPC pipe to parent process - with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc: + with contextlib.nested(os.fdopen(self.server.child_pipe[1], + u"w", 1), + os.fdopen(self.server.parent_pipe[0], + u"r", 0)) as (ipc, + ipc_return): session = (gnutls.connection .ClientSession(self.request, gnutls.connection @@ -1033,38 +1036,40 @@ return logger.debug(u"Handshake succeeded") try: - fpr = self.fingerprint(self.peer_certificate(session)) - except (TypeError, gnutls.errors.GNUTLSError), error: - logger.warning(u"Bad certificate: %s", error) - session.bye() - return - logger.debug(u"Fingerprint: %s", fpr) - - for c in self.server.clients: - if c.fingerprint == fpr: - client = c - break - else: - ipc.write(u"NOTFOUND %s %s\n" - % (fpr, unicode(self.client_address))) - session.bye() - return - # Have to check if client.still_valid(), since it is - # possible that the client timed out while establishing - # the GnuTLS session. - if not client.still_valid(): - ipc.write(u"INVALID %s\n" % client.name) - session.bye() - return - ipc.write(u"SENDING %s\n" % client.name) - sent_size = 0 - while sent_size < len(client.secret): - sent = session.send(client.secret[sent_size:]) - logger.debug(u"Sent: %d, remaining: %d", - sent, len(client.secret) - - (sent_size + sent)) - sent_size += sent - session.bye() + try: + fpr = self.fingerprint(self.peer_certificate + (session)) + except (TypeError, gnutls.errors.GNUTLSError), error: + logger.warning(u"Bad certificate: %s", error) + return + logger.debug(u"Fingerprint: %s", fpr) + + for c in self.server.clients: + if c.fingerprint == fpr: + client = c + break + else: + ipc.write(u"NOTFOUND %s %s\n" + % (fpr, unicode(self.client_address))) + return + # Have to check if client.enabled, since it is + # possible that the client was disabled since the + # GnuTLS session was established. + ipc.write(u"GETATTR enabled %s\n" % fpr) + enabled = pickle.load(ipc_return) + if not enabled: + ipc.write(u"DISABLED %s\n" % client.name) + return + ipc.write(u"SENDING %s\n" % client.name) + sent_size = 0 + while sent_size < len(client.secret): + sent = session.send(client.secret[sent_size:]) + logger.debug(u"Sent: %d, remaining: %d", + sent, len(client.secret) + - (sent_size + sent)) + sent_size += sent + finally: + session.bye() @staticmethod def peer_certificate(session): @@ -1130,24 +1135,28 @@ return hex_fpr -class ForkingMixInWithPipe(socketserver.ForkingMixIn, object): - """Like socketserver.ForkingMixIn, but also pass a pipe.""" +class ForkingMixInWithPipes(socketserver.ForkingMixIn, object): + """Like socketserver.ForkingMixIn, but also pass a pipe pair.""" def process_request(self, request, client_address): """Overrides and wraps the original process_request(). This function creates a new pipe in self.pipe """ - self.pipe = os.pipe() - super(ForkingMixInWithPipe, + self.child_pipe = os.pipe() # Child writes here + self.parent_pipe = os.pipe() # Parent writes here + super(ForkingMixInWithPipes, self).process_request(request, client_address) - os.close(self.pipe[1]) # close write end - self.add_pipe(self.pipe[0]) - def add_pipe(self, pipe): + # Close unused ends for parent + os.close(self.parent_pipe[0]) # close read end + os.close(self.child_pipe[1]) # close write end + self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1]) + def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd): """Dummy function; override as necessary""" - os.close(pipe) - - -class IPv6_TCPServer(ForkingMixInWithPipe, + os.close(child_pipe_fd) + os.close(parent_pipe_fd) + + +class IPv6_TCPServer(ForkingMixInWithPipes, socketserver.TCPServer, object): """IPv6-capable TCP server. Accepts 'None' as address and/or port @@ -1238,11 +1247,15 @@ return socketserver.TCPServer.server_activate(self) def enable(self): self.enabled = True - def add_pipe(self, pipe): + def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd): # Call "handle_ipc" for both data and EOF events - gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP, - self.handle_ipc) - def handle_ipc(self, source, condition, file_objects={}): + gobject.io_add_watch(child_pipe_fd, + gobject.IO_IN | gobject.IO_HUP, + functools.partial(self.handle_ipc, + reply_fd + =parent_pipe_fd)) + def handle_ipc(self, source, condition, reply_fd=None, + file_objects={}): condition_names = { gobject.IO_IN: u"IN", # There is data to read. gobject.IO_OUT: u"OUT", # Data can be written (without @@ -1260,16 +1273,20 @@ logger.debug(u"Handling IPC: FD = %d, condition = %s", source, conditions_string) - # Turn the pipe file descriptor into a Python file object + # Turn the pipe file descriptors into Python file objects if source not in file_objects: file_objects[source] = os.fdopen(source, u"r", 1) + if reply_fd not in file_objects: + file_objects[reply_fd] = os.fdopen(reply_fd, u"w", 0) # Read a line from the file object cmdline = file_objects[source].readline() if not cmdline: # Empty line means end of file - # close the IPC pipe + # close the IPC pipes file_objects[source].close() del file_objects[source] + file_objects[reply_fd].close() + del file_objects[reply_fd] # Stop calling this function return False @@ -1286,16 +1303,16 @@ if self.use_dbus: # Emit D-Bus signal mandos_dbus_service.ClientNotFound(fpr, address) - elif cmd == u"INVALID": + elif cmd == u"DISABLED": for client in self.clients: if client.name == args: - logger.warning(u"Client %s is invalid", args) + logger.warning(u"Client %s is disabled", args) if self.use_dbus: # Emit D-Bus signal client.Rejected() break else: - logger.error(u"Unknown client %s is invalid", args) + logger.error(u"Unknown client %s is disabled", args) elif cmd == u"SENDING": for client in self.clients: if client.name == args: @@ -1308,6 +1325,19 @@ else: logger.error(u"Sending secret to unknown client %s", args) + elif cmd == u"GETATTR": + attr_name, fpr = args.split(None, 1) + for client in self.clients: + if client.fingerprint == fpr: + attr_value = getattr(client, attr_name, None) + logger.debug("IPC reply: %r", attr_value) + pickle.dump(attr_value, file_objects[reply_fd]) + break + else: + logger.error(u"Client %s on address %s requesting " + u"attribute %s not found", fpr, address, + attr_name) + pickle.dump(None, file_objects[reply_fd]) else: logger.error(u"Unknown IPC command: %r", cmdline) @@ -1368,7 +1398,7 @@ def if_nametoindex(interface): "Get an interface index the hard way, i.e. using fcntl()" SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h - with closing(socket.socket()) as s: + with contextlib.closing(socket.socket()) as s: ifreq = fcntl.ioctl(s, SIOCGIFINDEX, struct.pack(str(u"16s16x"), interface)) @@ -1607,7 +1637,7 @@ daemon() try: - with closing(pidfile): + with pidfile: pid = os.getpid() pidfile.write(str(pid) + "\n") del pidfile @@ -1631,8 +1661,8 @@ dbus.service.Object.__init__(self, bus, u"/") _interface = u"se.bsnet.fukt.Mandos" - @dbus.service.signal(_interface, signature=u"oa{sv}") - def ClientAdded(self, objpath, properties): + @dbus.service.signal(_interface, signature=u"o") + def ClientAdded(self, objpath): "D-Bus signal" pass @@ -1700,8 +1730,7 @@ for client in tcp_server.clients: if use_dbus: # Emit D-Bus signal - mandos_dbus_service.ClientAdded(client.dbus_object_path, - client.GetAll(u"")) + mandos_dbus_service.ClientAdded(client.dbus_object_path) client.enable() tcp_server.enable() === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2009-09-17 01:21:27 +0000 +++ mandos-clients.conf.xml 2009-12-25 23:13:47 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/clients.conf"> - + %common; ]> @@ -63,9 +63,8 @@ >mandos 8, read by it at startup. The file needs to list all clients that should be able to use - the service. All clients listed will be regarded as valid, even - if a client was declared invalid in a previous run of the - server. + the service. All clients listed will be regarded as enabled, + even if a client was disabled in a previous run of the server. The format starts with a [section @@ -110,9 +109,8 @@ The timeout is how long the server will wait (for either a successful checker run or a client receiving its secret) - until a client is considered invalid - that is, ineligible - to get the data this server holds. By default Mandos will - use 1 hour. + until a client is disabled and not allowed to get the data + this server holds. By default Mandos will use 1 hour. The TIME is specified as a @@ -143,8 +141,8 @@ not be started if an old one is still running. The server will wait for a checker to complete until the above timeout occurs, at which - time the client will be marked invalid, and any running - checker killed. The default interval is 5 minutes. + time the client will be disabled, and any running checker + killed. The default interval is 5 minutes. The format of TIME is the same === modified file 'mandos-ctl' --- mandos-ctl 2009-10-26 21:16:16 +0000 +++ mandos-ctl 2009-12-25 23:13:47 +0000 @@ -128,8 +128,8 @@ help="Start checker for client") parser.add_option("--stop-checker", action="store_true", help="Stop checker for client") -parser.add_option("-V", "--is-valid", action="store_true", - help="Check if client is still valid") +parser.add_option("-V", "--is-enabled", action="store_true", + help="Check if client is enabled") parser.add_option("-r", "--remove", action="store_true", help="Remove client") parser.add_option("-c", "--checker", type="string", @@ -178,7 +178,7 @@ client.StartChecker(dbus_interface=client_interface) if options.stop_checker: client.StopChecker(dbus_interface=client_interface) - if options.is_valid: + if options.is_enabled: sys.exit(0 if client.Get(client_interface, u"enabled", dbus_interface=dbus.PROPERTIES_IFACE) === modified file 'mandos-monitor' --- mandos-monitor 2009-11-15 10:12:09 +0000 +++ mandos-monitor 2009-12-25 23:13:47 +0000 @@ -19,6 +19,10 @@ import UserList +import locale + +locale.setlocale(locale.LC_ALL, u'') + # Some useful constants domain = 'se.bsnet.fukt' server_interface = domain + '.Mandos' @@ -38,26 +42,20 @@ properties and calls a hook function when any of them are changed. """ - def __init__(self, proxy_object=None, properties=None, *args, - **kwargs): + def __init__(self, proxy_object=None, *args, **kwargs): self.proxy = proxy_object # Mandos Client proxy object - if properties is None: - self.properties = dict() - else: - self.properties = properties + self.properties = dict() self.proxy.connect_to_signal(u"PropertyChanged", self.property_changed, client_interface, byte_arrays=True) - - if properties is None: - self.properties.update(self.proxy.GetAll(client_interface, - dbus_interface = - dbus.PROPERTIES_IFACE)) + + self.properties.update( + self.proxy.GetAll(client_interface, + dbus_interface = dbus.PROPERTIES_IFACE)) super(MandosClientPropertyCache, self).__init__( - proxy_object=proxy_object, - properties=properties, *args, **kwargs) + proxy_object=proxy_object, *args, **kwargs) def property_changed(self, property=None, value=None): """This is called whenever we get a PropertyChanged signal @@ -424,17 +422,18 @@ return self.remove_client(client, path) - def add_new_client(self, path, properties): + def add_new_client(self, path): client_proxy_object = self.bus.get_object(self.busname, path) self.add_client(MandosClientWidget(server_proxy_object =self.mandos_serv, proxy_object =client_proxy_object, - properties=properties, update_hook =self.refresh, delete_hook - =self.remove_client), + =self.remove_client, + logger + =self.log_message), path=path) def add_client(self, client, path=None): @@ -562,6 +561,7 @@ ui = UserInterface() try: ui.run() -except: +except Exception, e: + ui.log_message(unicode(e)) ui.screen.stop() raise === modified file 'mandos.xml' --- mandos.xml 2009-09-17 01:21:27 +0000 +++ mandos.xml 2009-12-25 23:13:47 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -453,10 +453,9 @@ backtrace. This could be considered a feature. - Currently, if a client is declared invalid due to - having timed out, the server does not record this fact onto - permanent storage. This has some security implications, see - . + Currently, if a client is disabled due to having timed out, the + server does not record this fact onto permanent storage. This + has some security implications, see . There is currently no way of querying the server of the current @@ -549,19 +548,18 @@ If a client is compromised, its downtime should be duly noted - by the server which would therefore declare the client - invalid. But if the server was ever restarted, it would - re-read its client list from its configuration file and again - regard all clients therein as valid, and hence eligible to - receive their passwords. Therefore, be careful when - restarting servers if it is suspected that a client has, in - fact, been compromised by parties who may now be running a - fake Mandos client with the keys from the non-encrypted - initial RAM image of the client host. What - should be done in that case (if restarting the server program - really is necessary) is to stop the server program, edit the - configuration file to omit any suspect clients, and restart - the server program. + by the server which would therefore disable the client. But + if the server was ever restarted, it would re-read its client + list from its configuration file and again regard all clients + therein as enabled, and hence eligible to receive their + passwords. Therefore, be careful when restarting servers if + it is suspected that a client has, in fact, been compromised + by parties who may now be running a fake Mandos client with + the keys from the non-encrypted initial RAM + image of the client host. What should be done in that case + (if restarting the server program really is necessary) is to + stop the server program, edit the configuration file to omit + any suspect clients, and restart the server program. For more details on client-side security, see