=== 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