=== modified file 'DBUS-API' --- DBUS-API 2011-11-24 19:09:29 +0000 +++ DBUS-API 2011-11-26 23:08:17 +0000 @@ -112,7 +112,7 @@ Disable(). f) The date and time this client will be disabled, as an RFC 3339 - string, or an empty string if this has not happened. + string, or an empty string if this is not scheduled. g) The date and time of the last approval request, as an RFC 3339 string, or an empty string if this has not happened. === modified file 'Makefile' --- Makefile 2011-10-22 00:46:35 +0000 +++ Makefile 2011-11-26 22:22:20 +0000 @@ -26,12 +26,16 @@ version=1.4.1 SED=sed +USER=$(firstword $(subst :, ,$(shell getent passwd _mandos || getent passwd nobody || echo 65534))) +GROUP=$(firstword $(subst :, ,$(shell getent group _mandos || getent group nobody || echo 65534))) + ## Use these settings for a traditional /usr/local install # PREFIX=$(DESTDIR)/usr/local # CONFDIR=$(DESTDIR)/etc/mandos # KEYDIR=$(DESTDIR)/etc/mandos/keys # MANDIR=$(PREFIX)/man # INITRAMFSTOOLS=$(DESTDIR)/etc/initramfs-tools +# STATEDIR=$(DESTDIR)/var/lib/mandos ## ## These settings are for a package-type install @@ -40,6 +44,7 @@ KEYDIR=$(DESTDIR)/etc/keys/mandos MANDIR=$(PREFIX)/share/man INITRAMFSTOOLS=$(DESTDIR)/usr/share/initramfs-tools +STATEDIR=$(DESTDIR)/var/lib/mandos ## GNUTLS_CFLAGS=$(shell pkg-config --cflags-only-I gnutls) @@ -230,7 +235,7 @@ distclean: clean mostlyclean: clean maintainer-clean: clean - -rm --force --recursive keydir confdir + -rm --force --recursive keydir confdir statedir check: all ./mandos --check @@ -260,7 +265,8 @@ # Run the server with a local config run-server: confdir/mandos.conf confdir/clients.conf - ./mandos --debug --no-dbus --configdir=confdir $(SERVERARGS) + ./mandos --debug --no-dbus --configdir=confdir \ + --statedir=statedir $(SERVERARGS) # Used by run-server confdir/mandos.conf: mandos.conf @@ -271,6 +277,8 @@ install --mode=u=rw $< $@ # Add a client password ./mandos-keygen --dir keydir --password >> $@ +statedir: + install --directory statedir install: install-server install-client-nokey @@ -281,6 +289,8 @@ install-server: doc install --directory $(CONFDIR) + install --directory --mode=u=rwx --owner=$(USER) \ + --group=$(GROUP) $(STATEDIR) install --mode=u=rwx,go=rx mandos $(PREFIX)/sbin/mandos install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ mandos-ctl === modified file 'clients.conf' --- clients.conf 2011-09-19 09:42:55 +0000 +++ clients.conf 2011-11-26 23:08:17 +0000 @@ -30,6 +30,9 @@ # How long one approval will last. ;approval_duration = 1s +# Whether this client is enabled by default +;enabled = True + ;#### ;# Example client === modified file 'debian/control' --- debian/control 2011-10-11 19:36:00 +0000 +++ debian/control 2011-11-26 20:59:56 +0000 @@ -17,7 +17,8 @@ Architecture: all Depends: ${misc:Depends}, python (>=2.6), python-gnutls, python-dbus, python-avahi, python-gobject, avahi-daemon, adduser, - python-urwid, python (>=2.7) | python-argparse + python-urwid, python (>=2.7) | python-argparse, + python-gnupginterface Recommends: fping Description: server giving encrypted passwords to Mandos clients This is the server part of the Mandos system, which allows === modified file 'debian/mandos.dirs' --- debian/mandos.dirs 2010-09-15 17:33:14 +0000 +++ debian/mandos.dirs 2011-11-26 22:22:20 +0000 @@ -4,3 +4,4 @@ etc/default etc/dbus-1/system.d usr/sbin +var/lib/mandos === modified file 'debian/mandos.postinst' --- debian/mandos.postinst 2011-10-10 20:29:58 +0000 +++ debian/mandos.postinst 2011-11-26 22:22:20 +0000 @@ -35,11 +35,12 @@ --disabled-password --gecos "Mandos password system" \ _mandos fi + chown _mandos:_mandos /var/lib/mandos ;; - + abort-upgrade|abort-deconfigure|abort-remove) ;; - + *) echo "$0 called with unknown argument '$1'" 1>&2 exit 1 === modified file 'mandos' --- mandos 2011-11-26 19:17:31 +0000 +++ mandos 2011-11-26 23:08:17 +0000 @@ -63,7 +63,8 @@ import cPickle as pickle import multiprocessing import types -import hashlib +import binascii +import tempfile import dbus import dbus.service @@ -74,7 +75,7 @@ import ctypes.util import xml.dom.minidom import inspect -import Crypto.Cipher.AES +import GnuPGInterface try: SO_BINDTODEVICE = socket.SO_BINDTODEVICE @@ -86,7 +87,7 @@ version = "1.4.1" -stored_state_path = "/var/lib/mandos/clients.pickle" +stored_state_file = "clients.pickle" logger = logging.getLogger() syslogger = (logging.handlers.SysLogHandler @@ -127,6 +128,85 @@ logger.setLevel(level) +class CryptoError(Exception): + pass + + +class Crypto(object): + """A simple class for OpenPGP symmetric encryption & decryption""" + def __init__(self): + self.gnupg = GnuPGInterface.GnuPG() + self.tempdir = tempfile.mkdtemp(prefix="mandos-") + self.gnupg = GnuPGInterface.GnuPG() + self.gnupg.options.meta_interactive = False + self.gnupg.options.homedir = self.tempdir + self.gnupg.options.extra_args.extend(['--force-mdc', + '--quiet']) + + def __enter__(self): + return self + + def __exit__ (self, exc_type, exc_value, traceback): + self._cleanup() + return False + + def __del__(self): + self._cleanup() + + def _cleanup(self): + if self.tempdir is not None: + # Delete contents of tempdir + for root, dirs, files in os.walk(self.tempdir, + topdown = False): + for filename in files: + os.remove(os.path.join(root, filename)) + for dirname in dirs: + os.rmdir(os.path.join(root, dirname)) + # Remove tempdir + os.rmdir(self.tempdir) + self.tempdir = None + + def password_encode(self, password): + # Passphrase can not be empty and can not contain newlines or + # NUL bytes. So we prefix it and hex encode it. + return b"mandos" + binascii.hexlify(password) + + def encrypt(self, data, password): + self.gnupg.passphrase = self.password_encode(password) + with open(os.devnull) as devnull: + try: + proc = self.gnupg.run(['--symmetric'], + create_fhs=['stdin', 'stdout'], + attach_fhs={'stderr': devnull}) + with contextlib.closing(proc.handles['stdin']) as f: + f.write(data) + with contextlib.closing(proc.handles['stdout']) as f: + ciphertext = f.read() + proc.wait() + except IOError as e: + raise CryptoError(e) + self.gnupg.passphrase = None + return ciphertext + + def decrypt(self, data, password): + self.gnupg.passphrase = self.password_encode(password) + with open(os.devnull) as devnull: + try: + proc = self.gnupg.run(['--decrypt'], + create_fhs=['stdin', 'stdout'], + attach_fhs={'stderr': devnull}) + with contextlib.closing(proc.handles['stdin'] ) as f: + f.write(data) + with contextlib.closing(proc.handles['stdout']) as f: + decrypted_plaintext = f.read() + proc.wait() + except IOError as e: + raise CryptoError(e) + self.gnupg.passphrase = None + return decrypted_plaintext + + + class AvahiError(Exception): def __init__(self, value, *args, **kwargs): self.value = value @@ -335,7 +415,7 @@ last_checker_status: integer between 0 and 255 reflecting exit status of last checker. -1 reflects crashed checker, or None. - last_enabled: datetime.datetime(); (UTC) + last_enabled: datetime.datetime(); (UTC) or None name: string; from the config file, used in log messages and D-Bus identifiers secret: bytestring; sent verbatim (over TLS) to client @@ -393,9 +473,12 @@ % self.name) self.host = config.get("host", "") self.created = datetime.datetime.utcnow() - self.enabled = True + self.enabled = config.get("enabled", True) self.last_approval_request = None - self.last_enabled = datetime.datetime.utcnow() + if self.enabled: + self.last_enabled = datetime.datetime.utcnow() + else: + self.last_enabled = None self.last_checked_ok = None self.last_checker_status = None self.timeout = string_to_delta(config["timeout"]) @@ -405,7 +488,10 @@ self.checker = None self.checker_initiator_tag = None self.disable_initiator_tag = None - self.expires = datetime.datetime.utcnow() + self.timeout + if self.enabled: + self.expires = datetime.datetime.utcnow() + self.timeout + else: + self.expires = None self.checker_callback_tag = None self.checker_command = config["checker"] self.current_checker_command = None @@ -614,57 +700,6 @@ if error.errno != errno.ESRCH: # No such process raise self.checker = None - - # Encrypts a client secret and stores it in a varible - # encrypted_secret - def encrypt_secret(self, key): - # Encryption-key need to be of a specific size, so we hash - # inputed key - hasheng = hashlib.sha256() - hasheng.update(key) - encryptionkey = hasheng.digest() - - # Create validation hash so we know at decryption if it was - # sucessful - hasheng = hashlib.sha256() - hasheng.update(self.secret) - validationhash = hasheng.digest() - - # Encrypt secret - iv = os.urandom(Crypto.Cipher.AES.block_size) - ciphereng = Crypto.Cipher.AES.new(encryptionkey, - Crypto.Cipher.AES.MODE_CFB, - iv) - ciphertext = ciphereng.encrypt(validationhash+self.secret) - self.encrypted_secret = (ciphertext, iv) - - # Decrypt a encrypted client secret - def decrypt_secret(self, key): - # Decryption-key need to be of a specific size, so we hash inputed key - hasheng = hashlib.sha256() - hasheng.update(key) - encryptionkey = hasheng.digest() - - # Decrypt encrypted secret - ciphertext, iv = self.encrypted_secret - ciphereng = Crypto.Cipher.AES.new(encryptionkey, - Crypto.Cipher.AES.MODE_CFB, - iv) - plain = ciphereng.decrypt(ciphertext) - - # Validate decrypted secret to know if it was succesful - hasheng = hashlib.sha256() - validationhash = plain[:hasheng.digest_size] - secret = plain[hasheng.digest_size:] - hasheng.update(secret) - - # if validation fails, we use key as new secret. Otherwhise, - # we use the decrypted secret - if hasheng.digest() == validationhash: - self.secret = secret - else: - self.secret = key - del self.encrypted_secret def dbus_service_property(dbus_interface, signature="v", @@ -1249,7 +1284,7 @@ # Created - property @dbus_service_property(_interface, signature="s", access="read") def Created_dbus_property(self): - return dbus.String(datetime_to_dbus(self.created)) + return datetime_to_dbus(self.created) # LastEnabled - property @dbus_service_property(_interface, signature="s", access="read") @@ -1331,11 +1366,12 @@ self.interval = datetime.timedelta(0, 0, 0, value) if getattr(self, "checker_initiator_tag", None) is None: return - # Reschedule checker run - gobject.source_remove(self.checker_initiator_tag) - self.checker_initiator_tag = (gobject.timeout_add - (value, self.start_checker)) - self.start_checker() # Start one now, too + if self.enabled: + # Reschedule checker run + gobject.source_remove(self.checker_initiator_tag) + self.checker_initiator_tag = (gobject.timeout_add + (value, self.start_checker)) + self.start_checker() # Start one now, too # Checker - property @dbus_service_property(_interface, signature="s", @@ -1618,7 +1654,7 @@ # Convert the buffer to a Python bytestring fpr = ctypes.string_at(buf, buf_len.value) # Convert the bytestring to hexadecimal notation - hex_fpr = ''.join("%02X" % ord(char) for char in fpr) + hex_fpr = binascii.hexlify(fpr).upper() return hex_fpr @@ -1943,7 +1979,9 @@ dest="use_ipv6", help="Do not use IPv6") parser.add_argument("--no-restore", action="store_false", dest="restore", help="Do not restore stored" - " state", default=True) + " state") + parser.add_argument("--statedir", metavar="DIR", + help="Directory to save/restore state in") options = parser.parse_args() @@ -1963,6 +2001,8 @@ "use_dbus": "True", "use_ipv6": "True", "debuglevel": "", + "restore": "True", + "statedir": "/var/lib/mandos" } # Parse config file for server-global settings @@ -1985,7 +2025,8 @@ # options, if set. for option in ("interface", "address", "port", "debug", "priority", "servicename", "configdir", - "use_dbus", "use_ipv6", "debuglevel", "restore"): + "use_dbus", "use_ipv6", "debuglevel", "restore", + "statedir"): value = getattr(options, option) if value is not None: server_settings[option] = value @@ -2003,6 +2044,8 @@ debuglevel = server_settings["debuglevel"] use_dbus = server_settings["use_dbus"] use_ipv6 = server_settings["use_ipv6"] + stored_state_path = os.path.join(server_settings["statedir"], + stored_state_file) if debug: initlogger(logging.DEBUG) @@ -2141,6 +2184,9 @@ "approved_by_default": lambda section: client_config.getboolean(section, "approved_by_default"), + "enabled": + lambda section: + client_config.getboolean(section, "enabled"), } # Construct a new dict of client settings of this form: # { client_name: {setting_name: value, ...}, ...} @@ -2166,71 +2212,86 @@ (stored_state)) os.remove(stored_state_path) except IOError as e: - logger.warning("Could not load persistant state: {0}" + logger.warning("Could not load persistent state: {0}" .format(e)) if e.errno != errno.ENOENT: raise - for client in clients_data: - client_name = client["name"] - - # Decide which value to use after restoring saved state. - # We have three different values: Old config file, - # new config file, and saved state. - # New config value takes precedence if it differs from old - # config value, otherwise use saved state. - for name, value in client_settings[client_name].items(): + with Crypto() as crypt: + for client in clients_data: + client_name = client["name"] + + # Decide which value to use after restoring saved state. + # We have three different values: Old config file, + # new config file, and saved state. + # New config value takes precedence if it differs from old + # config value, otherwise use saved state. + for name, value in client_settings[client_name].items(): + try: + # For each value in new config, check if it + # differs from the old config value (Except for + # the "secret" attribute) + if (name != "secret" and + value != old_client_settings[client_name] + [name]): + setattr(client, name, value) + except KeyError: + pass + + # Clients who has passed its expire date can still be + # enabled if its last checker was sucessful. Clients + # whose checker failed before we stored its state is + # assumed to have failed all checkers during downtime. + if client["enabled"] and client["last_checked_ok"]: + if ((datetime.datetime.utcnow() + - client["last_checked_ok"]) + > client["interval"]): + if client["last_checker_status"] != 0: + client["enabled"] = False + else: + client["expires"] = (datetime.datetime + .utcnow() + + client["timeout"]) + + client["changedstate"] = (multiprocessing_manager + .Condition + (multiprocessing_manager + .Lock())) + if use_dbus: + new_client = (ClientDBusTransitional.__new__ + (ClientDBusTransitional)) + tcp_server.clients[client_name] = new_client + new_client.bus = bus + for name, value in client.iteritems(): + setattr(new_client, name, value) + client_object_name = unicode(client_name).translate( + {ord("."): ord("_"), + ord("-"): ord("_")}) + new_client.dbus_object_path = (dbus.ObjectPath + ("/clients/" + + client_object_name)) + DBusObjectWithProperties.__init__(new_client, + new_client.bus, + new_client + .dbus_object_path) + else: + tcp_server.clients[client_name] = (Client.__new__ + (Client)) + for name, value in client.iteritems(): + setattr(tcp_server.clients[client_name], + name, value) + try: - # For each value in new config, check if it differs - # from the old config value (Except for the "secret" - # attribute) - if (name != "secret" and - value != old_client_settings[client_name][name]): - setattr(client, name, value) - except KeyError: - pass - - # Clients who has passed its expire date, can still be enabled - # if its last checker was sucessful. Clients who checkers - # failed before we stored it state is asumed to had failed - # checker during downtime. - if client["enabled"] and client["last_checked_ok"]: - if ((datetime.datetime.utcnow() - - client["last_checked_ok"]) > client["interval"]): - if client["last_checker_status"] != 0: - client["enabled"] = False - else: - client["expires"] = (datetime.datetime.utcnow() - + client["timeout"]) - - client["changedstate"] = (multiprocessing_manager - .Condition(multiprocessing_manager - .Lock())) - if use_dbus: - new_client = (ClientDBusTransitional.__new__ - (ClientDBusTransitional)) - tcp_server.clients[client_name] = new_client - new_client.bus = bus - for name, value in client.iteritems(): - setattr(new_client, name, value) - client_object_name = unicode(client_name).translate( - {ord("."): ord("_"), - ord("-"): ord("_")}) - new_client.dbus_object_path = (dbus.ObjectPath - ("/clients/" - + client_object_name)) - DBusObjectWithProperties.__init__(new_client, - new_client.bus, - new_client - .dbus_object_path) - else: - tcp_server.clients[client_name] = Client.__new__(Client) - for name, value in client.iteritems(): - setattr(tcp_server.clients[client_name], name, value) - - tcp_server.clients[client_name].decrypt_secret( - client_settings[client_name]["secret"]) - + tcp_server.clients[client_name].secret = ( + crypt.decrypt(tcp_server.clients[client_name] + .encrypted_secret, + client_settings[client_name] + ["secret"])) + except CryptoError: + # If decryption fails, we use secret from new settings + tcp_server.clients[client_name].secret = ( + client_settings[client_name]["secret"]) + # Create/remove clients based on new changes made to config for clientname in set(old_client_settings) - set(client_settings): del tcp_server.clients[clientname] @@ -2333,35 +2394,38 @@ # based on what config file has. If config file is # removed/edited, old secret will thus be unrecovable. clients = [] - for client in tcp_server.clients.itervalues(): - client.encrypt_secret(client_settings[client.name] - ["secret"]) - - client_dict = {} - - # A list of attributes that will not be stored when - # shutting down. - exclude = set(("bus", "changedstate", "secret")) - for name, typ in inspect.getmembers(dbus.service.Object): - exclude.add(name) - - client_dict["encrypted_secret"] = client.encrypted_secret - for attr in client.client_structure: - if attr not in exclude: - client_dict[attr] = getattr(client, attr) - - clients.append(client_dict) - del client_settings[client.name]["secret"] - + with Crypto() as crypt: + for client in tcp_server.clients.itervalues(): + key = client_settings[client.name]["secret"] + client.encrypted_secret = crypt.encrypt(client.secret, + key) + client_dict = {} + + # A list of attributes that will not be stored when + # shutting down. + exclude = set(("bus", "changedstate", "secret")) + for name, typ in (inspect.getmembers + (dbus.service.Object)): + exclude.add(name) + + client_dict["encrypted_secret"] = (client + .encrypted_secret) + for attr in client.client_structure: + if attr not in exclude: + client_dict[attr] = getattr(client, attr) + + clients.append(client_dict) + del client_settings[client.name]["secret"] + try: with os.fdopen(os.open(stored_state_path, os.O_CREAT|os.O_WRONLY|os.O_TRUNC, 0600), "wb") as stored_state: pickle.dump((clients, client_settings), stored_state) - except IOError as e: - logger.warning("Could not save persistant state: {0}" + except (IOError, OSError) as e: + logger.warning("Could not save persistent state: {0}" .format(e)) - if e.errno != errno.ENOENT: + if e.errno not in (errno.ENOENT, errno.EACCES): raise # Delete all clients, and settings from config @@ -2387,7 +2451,6 @@ # Need to initiate checking of clients if client.enabled: client.init_checker() - tcp_server.enable() tcp_server.server_activate() === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2011-10-10 20:29:58 +0000 +++ mandos-clients.conf.xml 2011-11-26 23:08:17 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/clients.conf"> - + %common; ]> @@ -344,6 +344,20 @@ + + + + + Whether this client should be enabled by default. The + default is true. + + + + === modified file 'mandos-options.xml' --- mandos-options.xml 2011-11-09 17:16:03 +0000 +++ mandos-options.xml 2011-11-26 22:22:20 +0000 @@ -85,10 +85,16 @@ to the server if this option is turned off. Only advanced users should consider changing this option. - + This option controls whether the server will restore any state from last time it ran. Default is to try restore last state. + + Directory to save (and restore) state in. Default is + /var/lib/mandos. + + === modified file 'mandos.conf' --- mandos.conf 2009-02-13 05:38:21 +0000 +++ mandos.conf 2011-11-26 22:22:20 +0000 @@ -4,33 +4,27 @@ # These are the default values for the server, uncomment and change # them if needed. - # If "interface" is set, the server will only listen to a specific # network interface. ;interface = - # If "address" is set, the server will only listen to a specific # address. This must currently be an IPv6 address; an IPv4 address # can be specified using the "::FFFF:192.0.2.3" syntax. Also, if this # is a link-local address, an interface should be set above. ;address = - # If "port" is set, the server to bind to that port. By default, the # server will listen to an arbitrary port. ;port = - # If "debug" is true, the server will run in the foreground and print # a lot of debugging information. ;debug = False - # GnuTLS priority for the TLS handshake. See gnutls_priority_init(3). ;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP - # Zeroconf service name. You need to change this if you for some # reason want to run more than one server on the same *host*. # If there are name collisions on the same *network*, the server will @@ -42,3 +36,9 @@ # Whether to use IPv6. (Changing this is NOT recommended.) ;use_ipv6 = True + +# Whether to restore saved state on startup +;restore = True + +# The directory where state is saved +;statedir = /var/lib/mandos === modified file 'mandos.conf.xml' --- mandos.conf.xml 2011-10-05 16:00:56 +0000 +++ mandos.conf.xml 2011-11-26 22:22:20 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/mandos.conf"> - + %common; ]> @@ -154,6 +154,25 @@ + + + + + + + + + + + + + + @@ -198,6 +217,8 @@ servicename = Daena use_dbus = False use_ipv6 = True +restore = True +statedir = /var/lib/mandos === modified file 'mandos.xml' --- mandos.xml 2011-11-09 17:16:03 +0000 +++ mandos.xml 2011-11-26 22:22:20 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -96,6 +96,9 @@ + + &COMMANDNAME; @@ -284,6 +287,14 @@ + + + + + + + @@ -478,6 +489,17 @@ + /var/lib/mandos + + + Directory where persistent state will be saved. Change + this with the option. See + also the option. + + + + /dev/log @@ -507,11 +529,6 @@ backtrace. This could be considered a feature. - 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 no fine-grained control over logging and debug output. @@ -536,9 +553,9 @@ Run the server in debug mode, read configuration files from - the ~/mandos directory, and use the - Zeroconf service name Test to not collide with - any other official Mandos server on this host: + the ~/mandos directory, + and use the Zeroconf service name Test to not + collide with any other official Mandos server on this host: @@ -593,21 +610,6 @@ compromised if they are gone for too long. - If a client is compromised, its downtime should be duly noted - 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 mandos-client 8mandos.