55
import logging
55
import logging
56
import logging.handlers
56
import logging.handlers
57
import pwd
57
import pwd
58
from contextlib import closing
58
import contextlib
59
import struct
59
import struct
60
import fcntl
60
import fcntl
61
import functools
61
import functools
62
import cPickle as pickle
63
import dbus
64
import dbus
64
import dbus.service
65
import dbus.service
242
enabled: bool()
243
enabled: bool()
243
last_checked_ok: datetime.datetime(); (UTC) or None
244
last_checked_ok: datetime.datetime(); (UTC) or None
244
timeout: datetime.timedelta(); How long from last_checked_ok
245
timeout: datetime.timedelta(); How long from last_checked_ok
245
until this client is invalid
246
until this client is disabled
246
interval: datetime.timedelta(); How often to start a new checker
247
interval: datetime.timedelta(); How often to start a new checker
247
disable_hook: If set, called by disable() as disable_hook(self)
248
disable_hook: If set, called by disable() as disable_hook(self)
248
checker: subprocess.Popen(); a running checker process used
249
checker: subprocess.Popen(); a running checker process used
290
if u"secret" in config:
291
if u"secret" in config:
291
self.secret = config[u"secret"].decode(u"base64")
292
self.secret = config[u"secret"].decode(u"base64")
292
elif u"secfile" in config:
293
elif u"secfile" in config:
293
with closing(open(os.path.expanduser
294
with open(os.path.expanduser(os.path.expandvars
294
(os.path.expandvars
295
(config[u"secfile"])),
295
(config[u"secfile"])),
296
"rb") as secfile:
297
self.secret = secfile.read()
297
self.secret = secfile.read()
299
raise TypeError(u"No secret or secfile for client %s"
299
raise TypeError(u"No secret or secfile for client %s"
396
# client would inevitably timeout, since no checker would get
396
# client would inevitably timeout, since no checker would get
397
# a chance to run to completion. If we instead leave running
397
# a chance to run to completion. If we instead leave running
398
# checkers alone, the checker would have to take more time
398
# checkers alone, the checker would have to take more time
399
# than 'timeout' for the client to be declared invalid, which
399
# than 'timeout' for the client to be disabled, which is as it
400
# is as it should be.
400
# should be.
402
# If a checker exists, make sure it is not a zombie
402
# If a checker exists, make sure it is not a zombie
475
if error.errno != errno.ESRCH: # No such process
475
if error.errno != errno.ESRCH: # No such process
477
self.checker = None
477
self.checker = None
479
def still_valid(self):
480
"""Has the timeout not yet passed for this client?"""
481
if not getattr(self, u"enabled", False):
483
now = datetime.datetime.utcnow()
484
if self.last_checked_ok is None:
485
return now < (self.created + self.timeout)
487
return now < (self.last_checked_ok + self.timeout)
490
def dbus_service_property(dbus_interface, signature=u"v",
480
def dbus_service_property(dbus_interface, signature=u"v",
499
dbus.service.method, except there is only "signature", since the
489
dbus.service.method, except there is only "signature", since the
500
type from Get() and the type sent to Set() is the same.
490
type from Get() and the type sent to Set() is the same.
492
# Encoding deeply encoded byte arrays is not supported yet by the
493
# "Set" method, so we fail early here:
494
if byte_arrays and signature != u"ay":
495
raise ValueError(u"Byte arrays not supported for non-'ay'"
496
u" signature %r" % signature)
502
def decorator(func):
497
def decorator(func):
503
func._dbus_is_property = True
498
func._dbus_is_property = True
504
func._dbus_interface = dbus_interface
499
func._dbus_interface = dbus_interface
590
if prop._dbus_access == u"read":
585
if prop._dbus_access == u"read":
591
raise DBusPropertyAccessException(property_name)
586
raise DBusPropertyAccessException(property_name)
592
if prop._dbus_get_args_options[u"byte_arrays"]:
587
if prop._dbus_get_args_options[u"byte_arrays"]:
588
# The byte_arrays option is not supported yet on
589
# signatures other than "ay".
590
if prop._dbus_signature != u"ay":
593
value = dbus.ByteArray(''.join(unichr(byte)
592
value = dbus.ByteArray(''.join(unichr(byte)
594
for byte in value))
593
for byte in value))
595
prop(value)
594
prop(value)
990
def handle(self):
989
def handle(self):
991
logger.info(u"TCP connection from: %s",
990
logger.info(u"TCP connection from: %s",
992
unicode(self.client_address))
991
unicode(self.client_address))
993
logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
992
logger.debug(u"IPC Pipe FD: %d", self.server.child_pipe[1])
994
# Open IPC pipe to parent process
993
# Open IPC pipe to parent process
995
with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
994
with contextlib.nested(os.fdopen(self.server.child_pipe[1],
996
os.fdopen(self.server.parent_pipe[0],
996
session = (gnutls.connection
999
session = (gnutls.connection
997
.ClientSession(self.request,
1000
.ClientSession(self.request,
998
gnutls.connection
1001
gnutls.connection
1034
logger.debug(u"Handshake succeeded")
1037
logger.debug(u"Handshake succeeded")
1036
fpr = self.fingerprint(self.peer_certificate(session))
1039
try:
1037
except (TypeError, gnutls.errors.GNUTLSError), error:
1040
fpr = self.fingerprint(self.peer_certificate
1038
logger.warning(u"Bad certificate: %s", error)
1041
(session))
1039
session.bye()
1042
except (TypeError, gnutls.errors.GNUTLSError), error:
1040
return
1043
logger.warning(u"Bad certificate: %s", error)
1041
logger.debug(u"Fingerprint: %s", fpr)
1044
return
1042
1045
logger.debug(u"Fingerprint: %s", fpr)
1043
for c in self.server.clients:
1046
1044
if c.fingerprint == fpr:
1047
for c in self.server.clients:
1045
client = c
1048
if c.fingerprint == fpr:
1046
break
1049
client = c
1048
ipc.write(u"NOTFOUND %s %s\n"
1051
else:
1049
% (fpr, unicode(self.client_address)))
1052
ipc.write(u"NOTFOUND %s %s\n"
1050
session.bye()
1053
% (fpr, unicode(self.client_address)))
1052
# Have to check if client.still_valid(), since it is
1055
# Have to check if client.enabled, since it is
1053
# possible that the client timed out while establishing
1056
# possible that the client was disabled since the
1054
# the GnuTLS session.
1057
# GnuTLS session was established.
1055
if not client.still_valid():
1058
ipc.write(u"GETATTR enabled %s\n" % fpr)
1056
ipc.write(u"INVALID %s\n" % client.name)
1059
enabled = pickle.load(ipc_return)
1057
session.bye()
1060
if not enabled:
1058
return
1061
ipc.write(u"DISABLED %s\n" % client.name)
1059
ipc.write(u"SENDING %s\n" % client.name)
1062
return
1060
sent_size = 0
1063
ipc.write(u"SENDING %s\n" % client.name)
1061
while sent_size < len(client.secret):
1064
sent_size = 0
1062
sent = session.send(client.secret[sent_size:])
1065
while sent_size < len(client.secret):
1063
logger.debug(u"Sent: %d, remaining: %d",
1066
sent = session.send(client.secret[sent_size:])
1064
sent, len(client.secret)
1067
logger.debug(u"Sent: %d, remaining: %d",
1065
- (sent_size + sent))
1068
sent, len(client.secret)
1066
sent_size += sent
1069
- (sent_size + sent))
1067
session.bye()
1070
sent_size += sent
1069
@staticmethod
1074
@staticmethod
1070
def peer_certificate(session):
1075
def peer_certificate(session):
1130
return hex_fpr
1135
return hex_fpr
1133
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
1138
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
1134
"""Like socketserver.ForkingMixIn, but also pass a pipe."""
1139
"""Like socketserver.ForkingMixIn, but also pass a pipe pair."""
1135
def process_request(self, request, client_address):
1140
def process_request(self, request, client_address):
1136
"""Overrides and wraps the original process_request().
1141
"""Overrides and wraps the original process_request().
1138
This function creates a new pipe in self.pipe
1143
This function creates a new pipe in self.pipe
1140
self.pipe = os.pipe()
1145
self.child_pipe = os.pipe() # Child writes here
1141
super(ForkingMixInWithPipe,
1146
self.parent_pipe = os.pipe() # Parent writes here
1147
super(ForkingMixInWithPipes,
1142
self).process_request(request, client_address)
1148
self).process_request(request, client_address)
1143
os.close(self.pipe[1]) # close write end
1149
# Close unused ends for parent
1144
self.add_pipe(self.pipe[0])
1150
os.close(self.parent_pipe[0]) # close read end
1145
def add_pipe(self, pipe):
1151
os.close(self.child_pipe[1]) # close write end
1152
self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1])
1153
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1146
"""Dummy function; override as necessary"""
1154
"""Dummy function; override as necessary"""
1147
os.close(pipe)
1155
os.close(child_pipe_fd)
1148
1156
os.close(parent_pipe_fd)
1150
class IPv6_TCPServer(ForkingMixInWithPipe,
1158
1159
class IPv6_TCPServer(ForkingMixInWithPipes,
1151
socketserver.TCPServer, object):
1160
socketserver.TCPServer, object):
1152
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
1161
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
1238
return socketserver.TCPServer.server_activate(self)
1247
return socketserver.TCPServer.server_activate(self)
1239
def enable(self):
1248
def enable(self):
1240
self.enabled = True
1249
self.enabled = True
1241
def add_pipe(self, pipe):
1250
def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1242
# Call "handle_ipc" for both data and EOF events
1251
# Call "handle_ipc" for both data and EOF events
1243
gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP,
1252
gobject.io_add_watch(child_pipe_fd,
1244
self.handle_ipc)
1253
gobject.IO_IN | gobject.IO_HUP,
1245
def handle_ipc(self, source, condition, file_objects={}):
1254
functools.partial(self.handle_ipc,
1257
def handle_ipc(self, source, condition, reply_fd=None,
1246
condition_names = {
1259
condition_names = {
1247
gobject.IO_IN: u"IN", # There is data to read.
1260
gobject.IO_IN: u"IN", # There is data to read.
1248
gobject.IO_OUT: u"OUT", # Data can be written (without
1261
gobject.IO_OUT: u"OUT", # Data can be written (without
1260
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1273
logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1261
conditions_string)
1274
conditions_string)
1263
# Turn the pipe file descriptor into a Python file object
1276
# Turn the pipe file descriptors into Python file objects
1264
if source not in file_objects:
1277
if source not in file_objects:
1265
file_objects[source] = os.fdopen(source, u"r", 1)
1278
file_objects[source] = os.fdopen(source, u"r", 1)
1279
if reply_fd not in file_objects:
1280
file_objects[reply_fd] = os.fdopen(reply_fd, u"w", 0)
1267
# Read a line from the file object
1282
# Read a line from the file object
1268
cmdline = file_objects[source].readline()
1283
cmdline = file_objects[source].readline()
1269
if not cmdline: # Empty line means end of file
1284
if not cmdline: # Empty line means end of file
1270
# close the IPC pipe
1285
# close the IPC pipes
1271
file_objects[source].close()
1286
file_objects[source].close()
1272
del file_objects[source]
1287
del file_objects[source]
1288
file_objects[reply_fd].close()
1289
del file_objects[reply_fd]
1274
# Stop calling this function
1291
# Stop calling this function
1275
return False
1292
return False
1286
if self.use_dbus:
1303
if self.use_dbus:
1287
# Emit D-Bus signal
1304
# Emit D-Bus signal
1288
mandos_dbus_service.ClientNotFound(fpr, address)
1305
mandos_dbus_service.ClientNotFound(fpr, address)
1289
elif cmd == u"INVALID":
1306
elif cmd == u"DISABLED":
1290
for client in self.clients:
1307
for client in self.clients:
1291
if client.name == args:
1308
if client.name == args:
1292
logger.warning(u"Client %s is invalid", args)
1309
logger.warning(u"Client %s is disabled", args)
1293
if self.use_dbus:
1310
if self.use_dbus:
1294
# Emit D-Bus signal
1311
# Emit D-Bus signal
1295
client.Rejected()
1312
client.Rejected()
1298
logger.error(u"Unknown client %s is invalid", args)
1315
logger.error(u"Unknown client %s is disabled", args)
1299
elif cmd == u"SENDING":
1316
elif cmd == u"SENDING":
1300
for client in self.clients:
1317
for client in self.clients:
1301
if client.name == args:
1318
if client.name == args:
1309
logger.error(u"Sending secret to unknown client %s",
1326
logger.error(u"Sending secret to unknown client %s",
1328
elif cmd == u"GETATTR":
1329
attr_name, fpr = args.split(None, 1)
1330
for client in self.clients:
1331
if client.fingerprint == fpr:
1332
attr_value = getattr(client, attr_name, None)
1333
logger.debug("IPC reply: %r", attr_value)
1334
pickle.dump(attr_value, file_objects[reply_fd])
1337
logger.error(u"Client %s on address %s requesting "
1338
u"attribute %s not found", fpr, address,
1340
pickle.dump(None, file_objects[reply_fd])
1312
logger.error(u"Unknown IPC command: %r", cmdline)
1342
logger.error(u"Unknown IPC command: %r", cmdline)
1368
def if_nametoindex(interface):
1398
def if_nametoindex(interface):
1369
"Get an interface index the hard way, i.e. using fcntl()"
1399
"Get an interface index the hard way, i.e. using fcntl()"
1370
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1400
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
1371
with closing(socket.socket()) as s:
1401
with contextlib.closing(socket.socket()) as s:
1372
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1402
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1373
struct.pack(str(u"16s16x"),
1403
struct.pack(str(u"16s16x"),
1374
interface))
1404
interface))
1607
daemon()
1637
daemon()
1610
with closing(pidfile):
1640
with pidfile:
1611
pid = os.getpid()
1641
pid = os.getpid()
1612
pidfile.write(str(pid) + "\n")
1642
pidfile.write(str(pid) + "\n")
1613
del pidfile
1643
del pidfile
1631
dbus.service.Object.__init__(self, bus, u"/")
1661
dbus.service.Object.__init__(self, bus, u"/")
1632
_interface = u"se.bsnet.fukt.Mandos"
1662
_interface = u"se.bsnet.fukt.Mandos"
1634
@dbus.service.signal(_interface, signature=u"oa{sv}")
1664
@dbus.service.signal(_interface, signature=u"o")
1635
def ClientAdded(self, objpath, properties):
1665
def ClientAdded(self, objpath):
1636
"D-Bus signal"
1666
"D-Bus signal"
1700
for client in tcp_server.clients:
1730
for client in tcp_server.clients:
1701
if use_dbus:
1731
if use_dbus:
1702
# Emit D-Bus signal
1732
# Emit D-Bus signal
1703
mandos_dbus_service.ClientAdded(client.dbus_object_path,
1733
mandos_dbus_service.ClientAdded(client.dbus_object_path)
1705
client.enable()
1734
client.enable()
1707
tcp_server.enable()
1736
tcp_server.enable()
2
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
2
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
3
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
3
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
4
<!ENTITY COMMANDNAME "mandos">
4
<!ENTITY COMMANDNAME "mandos">
5
<!ENTITY TIMESTAMP "2009-09-17">
5
<!ENTITY TIMESTAMP "2009-12-09">
6
<!ENTITY % common SYSTEM "common.ent">
6
<!ENTITY % common SYSTEM "common.ent">
453
backtrace. This could be considered a feature.
453
backtrace. This could be considered a feature.
456
Currently, if a client is declared <quote>invalid</quote> due to
456
Currently, if a client is disabled due to having timed out, the
457
having timed out, the server does not record this fact onto
457
server does not record this fact onto permanent storage. This
458
permanent storage. This has some security implications, see
458
has some security implications, see <xref linkend="clients"/>.
459
<xref linkend="clients"/>.
462
There is currently no way of querying the server of the current
461
There is currently no way of querying the server of the current
551
If a client is compromised, its downtime should be duly noted
550
If a client is compromised, its downtime should be duly noted
552
by the server which would therefore declare the client
551
by the server which would therefore disable the client. But
553
invalid. But if the server was ever restarted, it would
552
if the server was ever restarted, it would re-read its client
554
re-read its client list from its configuration file and again
553
list from its configuration file and again regard all clients
555
regard all clients therein as valid, and hence eligible to
554
therein as enabled, and hence eligible to receive their
556
receive their passwords. Therefore, be careful when
555
passwords. Therefore, be careful when restarting servers if
557
restarting servers if it is suspected that a client has, in
556
it is suspected that a client has, in fact, been compromised
558
fact, been compromised by parties who may now be running a
557
by parties who may now be running a fake Mandos client with
559
fake Mandos client with the keys from the non-encrypted
558
the keys from the non-encrypted initial <acronym>RAM</acronym>
560
initial <acronym>RAM</acronym> image of the client host. What
559
image of the client host. What should be done in that case
561
should be done in that case (if restarting the server program
560
(if restarting the server program really is necessary) is to
562
really is necessary) is to stop the server program, edit the
561
stop the server program, edit the configuration file to omit
563
configuration file to omit any suspect clients, and restart
562
any suspect clients, and restart the server program.
567
For more details on client-side security, see
565
For more details on client-side security, see