=== modified file 'INSTALL' --- INSTALL 2011-03-08 19:09:03 +0000 +++ INSTALL 2013-06-23 15:13:06 +0000 @@ -12,7 +12,7 @@ server and client programs themselves *could* be run in other distributions, but they *are* specific to GNU/Linux systems, and are not written with portabillity to other Unixes in mind. - + ** Libraries The following libraries and packages are needed. (It is possible @@ -35,7 +35,7 @@ To build just the documentation, run the command "make doc". Then the manual page "mandos.8", for example, can be read by running "man -l mandos.8". - + *** Mandos Server + GnuTLS 2.4 http://www.gnu.org/software/gnutls/ + Avahi 0.6.16 http://www.avahi.org/ @@ -51,7 +51,7 @@ Package names: python-gnutls avahi-daemon python python-avahi python-dbus python-gobject python-urwid - + *** Mandos Client + initramfs-tools 0.85i http://packages.qa.debian.org/i/initramfs-tools.html === modified file 'Makefile' --- Makefile 2012-06-17 22:26:40 +0000 +++ Makefile 2012-06-22 23:33:56 +0000 @@ -241,6 +241,7 @@ check: all ./mandos --check + ./mandos-ctl --check # Run the client with a local config and key run-client: all keydir/seckey.txt keydir/pubkey.txt === modified file 'TODO' --- TODO 2012-06-17 14:55:31 +0000 +++ TODO 2013-06-23 15:30:34 +0000 @@ -2,6 +2,9 @@ * [[http://www.undeadly.org/cgi?action=article&sid=20110530221728][OpenBSD]] +* Testing +** python-nemu + * mandos-applet * mandos-client @@ -47,7 +50,6 @@ ** TODO [#C] DBusServiceObjectUsingSuper ** TODO [#B] Global enable/disable flag ** TODO [#B] By-client countdown on number of secrets given -** TODO [#B] Support RFC 3339 time duration syntax ** D-Bus Client method NeedsPassword(50) - Timeout, default disapprove + SetPass(u"gazonk", True) -> Approval, persistent + Approve(False) -> Close client connection immediately @@ -62,17 +64,20 @@ ** TODO Allow %%(checker)s as a runtime expansion ** TODO Use python-tlslite? ** TODO D-Bus AddClient() method on server object -** TODO Use org.freedesktop.DBus.Method.NoReply annotation on async methods. -** TODO Emit [[http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties][org.freedesktop.DBus.Properties.PropertiesChanged]] signal +** TODO Use org.freedesktop.DBus.Method.NoReply annotation on async methods. :2: +** TODO Emit [[http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties][org.freedesktop.DBus.Properties.PropertiesChanged]] signal :2: TODO Deprecate se.recompile.Mandos.Client.PropertyChanged - annotate! TODO Can use "invalidates" annotation to also emit on changed secret. -** TODO Support [[http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager][org.freedesktop.DBus.ObjectManager]] interface on server object +** TODO Support [[http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager][org.freedesktop.DBus.ObjectManager]] interface on server object :2: Deprecate methods GetAllClients(), GetAllClientsWithProperties() and signals ClientAdded and ClientRemoved. ** TODO Save state periodically to recover better from hard shutdowns ** TODO CheckerCompleted method, deprecate CheckedOK ** TODO Secret Service API? http://standards.freedesktop.org/secret-service/ +** TODO Remove D-Bus interfaces with old domain name :2: +** TODO Remove old string_to_delta format :2: +** TODO --no-zeroconf (only valid if port or socket is set) * mandos.xml ** Add mandos contact info in manual pages @@ -80,7 +85,7 @@ * 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 Remove old string_to_delta format :2: * TODO mandos-dispatch Listens for specified D-Bus signals and spawns shell commands with @@ -92,7 +97,7 @@ Better view of client data in the listing *** Properties popup ** Print a nice "We are sorry" message, save stack trace to log. -** Show timeout countdown for approval +** Rename module "gobject" to "GObject". * mandos-keygen ** TODO "--secfile" option === modified file 'clients.conf' --- clients.conf 2011-11-26 23:08:17 +0000 +++ clients.conf 2012-06-23 00:58:49 +0000 @@ -4,19 +4,19 @@ # How long until a client is disabled and not be allowed to get the # data this server holds. -;timeout = 5m +;timeout = PT5M # 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 disabled, # and any running checker killed. -;interval = 2m +;interval = PT2M # Extended timeout is an added timeout that is given once after a # password has been sent sucessfully to a client. This allows for # additional delays caused by file system checks and quota checks. -;extended_timeout = 15m +;extended_timeout = PT15M # What command to run as "the checker". ;checker = fping -q -- %%(host)s @@ -25,10 +25,10 @@ ;approved_by_default = True # How long to wait for approval. -;approval_delay = 0s +;approval_delay = PT0S # How long one approval will last. -;approval_duration = 1s +;approval_duration = PT1S # Whether this client is enabled by default ;enabled = True @@ -78,10 +78,10 @@ ;host = 192.0.2.3 ; ;# Parameters from the [DEFAULT] section can be overridden per client. -;interval = 1m +;interval = PT1M ; ;# This client requires manual approval before it receives its secret. ;approved_by_default = False ;# Require approval within 30 seconds. -;approval_delay = 30s +;approval_delay = PT30S ;#### === modified file 'debian/control' --- debian/control 2012-05-24 18:10:10 +0000 +++ debian/control 2013-10-05 19:34:40 +0000 @@ -4,7 +4,7 @@ Maintainer: Mandos Maintainers Uploaders: Teddy Hogeborn , Björn Påhlsson -Build-Depends: debhelper (>= 7), docbook-xml, docbook-xsl, +Build-Depends: debhelper (>= 8.9.7), docbook-xml, docbook-xsl, libavahi-core-dev, libgpgme11-dev, libgnutls-dev, xsltproc, pkg-config Standards-Version: 3.9.3 @@ -17,8 +17,7 @@ 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-gnupginterface + python-urwid, python (>=2.7) | python-argparse, gnupg (<< 2) 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-client.README.Debian' --- debian/mandos-client.README.Debian 2012-06-16 19:30:08 +0000 +++ debian/mandos-client.README.Debian 2013-09-29 15:52:19 +0000 @@ -45,7 +45,7 @@ The device can also be overridden at boot time on the Linux kernel command line using the sixth colon-separated field of the "ip=" option; for exact syntax, read the documentation in the file - "/usr/share/doc/linux-doc-*/Documentation/filesystems/nfsroot.txt", + "/usr/share/doc/linux-doc-*/Documentation/filesystems/nfs/nfsroot.txt", available in the "linux-doc-*" package. Note that since the network interfaces are used in the initial RAM @@ -90,4 +90,4 @@ work, "--options-for=mandos-client:--connect=
:" needs to be manually added to the file "/etc/mandos/plugin-runner.conf". - -- Teddy Hogeborn , Sat, 16 Jun 2012 13:09:58 +0200 + -- Teddy Hogeborn , Sun, 23 Jun 2013 17:31:53 +0200 === modified file 'debian/rules' --- debian/rules 2012-06-01 21:48:12 +0000 +++ debian/rules 2013-10-05 19:34:40 +0000 @@ -1,108 +1,29 @@ #!/usr/bin/make -f -# Sample debian/rules that uses debhelper. -# -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. -# -# Modified to make a template file for a multi-binary package with separated -# build-arch and build-indep targets by Bill Allombert 2001 - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -# This has to be exported to make some magic below work. -export DH_OPTIONS - -# -pie was broken briefly on the mips and mipsel architectures, see -# -BINUTILS_V := $(shell dpkg-query --showformat='$${Version}' \ - --show binutils) -ifeq (yes,$(shell dpkg --compare-versions $(BINUTILS_V) lt 2.20-3 \ - && dpkg --compare-versions $(BINUTILS_V) ge 2.19.1-1 \ - && echo yes)) - ifneq (,$(strip $(findstring :$(DEB_HOST_ARCH):,:mips:mipsel:) \ - $(findstring :$(DEB_BUILD_ARCH):,:mips:mipsel:))) - BROKEN_PIE := yes - export BROKEN_PIE - endif -endif - -configure: configure-stamp -configure-stamp: - dh_testdir - touch configure-stamp - -build: build-arch build-indep - -build-arch: build-arch-stamp -build-arch-stamp: configure-stamp +%: + dh $@ + +override_dh_auto_build-arch: LC_ALL=en_US.utf8 dh_auto_build -- all doc - touch $@ - -build-indep: build-indep-stamp -build-indep-stamp: configure-stamp - LC_ALL=en_US.UTF-8 dh_auto_build -- doc - touch $@ - -clean: - dh_testdir - dh_testroot - rm -f build-arch-stamp build-indep-stamp configure-stamp - dh_auto_clean - dh_clean - -install: install-indep install-arch -install-indep: - dh_testdir - dh_testroot - dh_prep - dh_installdirs --indep - $(MAKE) DESTDIR=$(CURDIR)/debian/mandos install-server - dh_lintian + +override_dh_auto_build-indep: + LC_ALL=en_US.utf8 dh_auto_build -- doc + +override_dh_installinit-indep: dh_installinit --onlyscripts \ --update-rcd-params="defaults 25 15" - dh_install --indep - -install-arch: - dh_testdir - dh_testroot - dh_prep - dh_installdirs --same-arch - $(MAKE) DESTDIR=$(CURDIR)/debian/mandos-client install-client-nokey - dh_lintian - dh_install --same-arch - -binary-common: - dh_testdir - dh_testroot - dh_installchangelogs - dh_installdocs - dh_installexamples - dh_link - dh_strip - dh_compress + +override_dh_auto_install-indep: + $(MAKE) DESTDIR=$(CURDIR)/debian/mandos install-server + +override_dh_auto_install-arch: + $(MAKE) DESTDIR=$(CURDIR)/debian/mandos-client \ + install-client-nokey + +override_dh_fixperms: dh_fixperms --exclude etc/keys/mandos \ --exclude etc/mandos/clients.conf \ --exclude etc/mandos/plugins.d \ --exclude usr/lib/mandos/plugins.d \ --exclude usr/share/doc/mandos-client/examples/network-hooks.d - dh_installdeb - dh_shlibdeps - dh_gencontrol - dh_md5sums - dh_builddeb - -# Build architecture independant packages using the common target. -binary-indep: build-indep install-indep - $(MAKE) -f debian/rules DH_OPTIONS=--indep binary-common - -# Build architecture dependant packages using the common target. -binary-arch: build-arch install-arch - $(MAKE) -f debian/rules DH_OPTIONS=--same-arch binary-common - -binary: binary-arch binary-indep - -.PHONY: build clean binary-indep binary-arch binary install \ - install-indep install-arch configure + chmod --recursive g-w -- \ + "$(CURDIR)/debian/mandos-client/usr/share/doc/mandos-client/examples/network-hooks.d" === modified file 'initramfs-tools-hook' --- initramfs-tools-hook 2011-11-29 18:19:31 +0000 +++ initramfs-tools-hook 2013-10-13 01:49:18 +0000 @@ -3,7 +3,7 @@ # This script will be run by 'mkinitramfs' when it creates the image. # Its job is to decide which files to install, then install them into # the staging area, where the initramfs is being created. This -# happens when a new 'linux-image' package is installed, or when the +# happens when a new 'linux-image' package is installed, or when an # administrator runs 'update-initramfs' by hand to update an initramfs # image. @@ -147,12 +147,18 @@ fi done -# GPGME needs /usr/bin/gpg -if [ ! -e "${DESTDIR}/usr/bin/gpg" \ - -a -n "`ls \"${DESTDIR}\"/usr/lib/libgpgme.so* \ - 2>/dev/null`" ]; then - copy_exec /usr/bin/gpg -fi +# GPGME needs GnuPG +libgpgme11_version="`dpkg-query --showformat='${Version}' --show libgpgme11`" +if dpkg --compare-versions "$libgpgme11_version" ge 1.4.1-0.1; then + gpg=/usr/bin/gpg2 +else + gpg=/usr/bin/gpg +fi +if [ ! -e "${DESTDIR}$gpg" ]; then + copy_exec "$gpg" +fi +unset gpg +unset libgpgme11_version # Config files for file in /etc/mandos/plugin-runner.conf; do === added file 'initramfs-unpack' --- initramfs-unpack 1970-01-01 00:00:00 +0000 +++ initramfs-unpack 2013-10-13 15:43:42 +0000 @@ -0,0 +1,67 @@ +#!/bin/bash +# +# Initramfs unpacker - unpacks initramfs images into /tmp +# +# Copyright © 2013 Teddy Hogeborn +# Copyright © 2013 Björn Påhlsson +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# . +# +# Contact the authors at . + +cpio="cpio --extract --make-directories --unconditional --preserve-modification-time" + +if [ -z "$*" ]; then + set -- /boot/initrd.img-* +fi + +for imgfile in "$@"; do + if ! [ -f "$imgfile" ]; then + echo "Error: Not an existing file: $imgfile" >&2 + continue + fi + imgdir="${TMPDIR:-/tmp}/${imgfile##*/}" + if [ -d "$imgdir" ]; then + rm --recursive -- "$imgdir" + fi + mkdir --parents "$imgdir" + # Does this image contain microcode? + if $cpio --quiet --list --file="$imgfile" >/dev/null 2>&1; then + # Number of bytes to skip to get to the compressed archive + skip=$(($(LANG=C $cpio --io-size=1 --list --file="$imgfile" 2>&1 \ + | sed --quiet --expression='s/^\([0-9]\+\) blocks$/\1/p')+8)) + catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer" + else + catimg="cat -- $imgfile" + fi + # Determine the compression method + if { $catimg 2>/dev/null | zcat --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="zcat" + elif { $catimg 2>/dev/null | bzip2 --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="bzip2 --stdout --decompress" + elif { $catimg 2>/dev/null | lzop --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="lzop --stdout --decompress" + else + echo "Error: Could not determine type of $imgfile" >&2 + continue + fi + $catimg 2>/dev/null | $decomp | ( cd -- "$imgdir" && $cpio --quiet ) + if [ ${PIPESTATUS[-1]} -eq 0 ]; then + echo "$imgfile unpacked into $imgdir" + fi +done === modified file 'mandos' --- mandos 2012-06-17 22:26:40 +0000 +++ mandos 2013-06-23 15:13:06 +0000 @@ -68,6 +68,7 @@ import binascii import tempfile import itertools +import collections import dbus import dbus.service @@ -78,7 +79,6 @@ import ctypes.util import xml.dom.minidom import inspect -import GnuPGInterface try: SO_BINDTODEVICE = socket.SO_BINDTODEVICE @@ -139,14 +139,12 @@ class PGPEngine(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', - '--no-use-agent']) + self.gnupgargs = ['--batch', + '--home', self.tempdir, + '--force-mdc', + '--quiet', + '--no-use-agent'] def __enter__(self): return self @@ -177,37 +175,40 @@ return b"mandos" + binascii.hexlify(password) def encrypt(self, data, password): - self.gnupg.passphrase = self.password_encode(password) - with open(os.devnull, "w") 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 PGPError(e) - self.gnupg.passphrase = None + passphrase = self.password_encode(password) + with tempfile.NamedTemporaryFile(dir=self.tempdir + ) as passfile: + passfile.write(passphrase) + passfile.flush() + proc = subprocess.Popen(['gpg', '--symmetric', + '--passphrase-file', + passfile.name] + + self.gnupgargs, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE) + ciphertext, err = proc.communicate(input = data) + if proc.returncode != 0: + raise PGPError(err) return ciphertext def decrypt(self, data, password): - self.gnupg.passphrase = self.password_encode(password) - with open(os.devnull, "w") 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 PGPError(e) - self.gnupg.passphrase = None + passphrase = self.password_encode(password) + with tempfile.NamedTemporaryFile(dir = self.tempdir + ) as passfile: + passfile.write(passphrase) + passfile.flush() + proc = subprocess.Popen(['gpg', '--decrypt', + '--passphrase-file', + passfile.name] + + self.gnupgargs, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE) + decrypted_plaintext, err = proc.communicate(input + = data) + if proc.returncode != 0: + raise PGPError(err) return decrypted_plaintext @@ -233,7 +234,7 @@ Used to optionally bind to the specified interface. name: string; Example: 'Mandos' type: string; Example: '_mandos._tcp'. - See + See port: integer; what port to announce TXT: list of strings; TXT record for the service domain: string; Domain to publish on, default to .local if empty. @@ -439,6 +440,7 @@ runtime_expansions: Allowed attributes for runtime expansion. expires: datetime.datetime(); time (UTC) when a client will be disabled, or None + server_settings: The server_settings dict from main() """ runtime_expansions = ("approval_delay", "approval_duration", @@ -446,13 +448,13 @@ "fingerprint", "host", "interval", "last_approval_request", "last_checked_ok", "last_enabled", "name", "timeout") - client_defaults = { "timeout": "5m", - "extended_timeout": "15m", - "interval": "2m", + client_defaults = { "timeout": "PT5M", + "extended_timeout": "PT15M", + "interval": "PT2M", "checker": "fping -q -- %%(host)s", "host": "", - "approval_delay": "0s", - "approval_duration": "1s", + "approval_delay": "PT0S", + "approval_duration": "PT1S", "approved_by_default": "True", "enabled": "True", } @@ -519,8 +521,11 @@ return settings - def __init__(self, settings, name = None): + def __init__(self, settings, name = None, server_settings=None): self.name = name + if server_settings is None: + server_settings = {} + self.server_settings = server_settings # adding all client settings for setting, value in settings.iteritems(): setattr(self, setting, value) @@ -710,19 +715,37 @@ # in normal mode, that is already done by daemon(), # and in debug mode we don't want to. (Stdin is # always replaced by /dev/null.) + # The exception is when not debugging but nevertheless + # running in the foreground; use the previously + # created wnull. + popen_args = {} + if (not self.server_settings["debug"] + and self.server_settings["foreground"]): + popen_args.update({"stdout": wnull, + "stderr": wnull }) self.checker = subprocess.Popen(command, close_fds=True, - shell=True, cwd="/") + shell=True, cwd="/", + **popen_args) except OSError as error: logger.error("Failed to start subprocess", exc_info=error) + return True self.checker_callback_tag = (gobject.child_watch_add (self.checker.pid, self.checker_callback, data=command)) # The checker may have completed before the gobject # watch was added. Check for this. - pid, status = os.waitpid(self.checker.pid, os.WNOHANG) + try: + pid, status = os.waitpid(self.checker.pid, os.WNOHANG) + except OSError as error: + if error.errno == errno.ECHILD: + # This should never happen + logger.error("Child process vanished", + exc_info=error) + return True + raise if pid: gobject.source_remove(self.checker_callback_tag) self.checker_callback(pid, status, command) @@ -1068,8 +1091,8 @@ interface_names.add(alt_interface) # Is this a D-Bus signal? if getattr(attribute, "_dbus_is_signal", False): - # Extract the original non-method function by - # black magic + # Extract the original non-method undecorated + # function by black magic nonmethod_func = (dict( zip(attribute.func_code.co_freevars, attribute.__closure__))["func"] @@ -1968,7 +1991,7 @@ if self.address_family == socket.AF_INET6: any_address = "::" # in6addr_any else: - any_address = socket.INADDR_ANY + any_address = "0.0.0.0" # INADDR_ANY self.server_address = (any_address, self.server_address[1]) elif not self.server_address[1]: @@ -2090,6 +2113,105 @@ return True +def rfc3339_duration_to_delta(duration): + """Parse an RFC 3339 "duration" and return a datetime.timedelta + + >>> rfc3339_duration_to_delta("P7D") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT60S") + datetime.timedelta(0, 60) + >>> rfc3339_duration_to_delta("PT60M") + datetime.timedelta(0, 3600) + >>> rfc3339_duration_to_delta("PT24H") + datetime.timedelta(1) + >>> rfc3339_duration_to_delta("P1W") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT5M30S") + datetime.timedelta(0, 330) + >>> rfc3339_duration_to_delta("P1DT3M20S") + datetime.timedelta(1, 200) + """ + + # Parsing an RFC 3339 duration with regular expressions is not + # possible - there would have to be multiple places for the same + # values, like seconds. The current code, while more esoteric, is + # cleaner without depending on a parsing library. If Python had a + # built-in library for parsing we would use it, but we'd like to + # avoid excessive use of external libraries. + + # New type for defining tokens, syntax, and semantics all-in-one + Token = collections.namedtuple("Token", + ("regexp", # To match token; if + # "value" is not None, + # must have a "group" + # containing digits + "value", # datetime.timedelta or + # None + "followers")) # Tokens valid after + # this token + # RFC 3339 "duration" tokens, syntax, and semantics; taken from + # the "duration" ABNF definition in RFC 3339, Appendix A. + token_end = Token(re.compile(r"$"), None, frozenset()) + token_second = Token(re.compile(r"(\d+)S"), + datetime.timedelta(seconds=1), + frozenset((token_end,))) + token_minute = Token(re.compile(r"(\d+)M"), + datetime.timedelta(minutes=1), + frozenset((token_second, token_end))) + token_hour = Token(re.compile(r"(\d+)H"), + datetime.timedelta(hours=1), + frozenset((token_minute, token_end))) + token_time = Token(re.compile(r"T"), + None, + frozenset((token_hour, token_minute, + token_second))) + token_day = Token(re.compile(r"(\d+)D"), + datetime.timedelta(days=1), + frozenset((token_time, token_end))) + token_month = Token(re.compile(r"(\d+)M"), + datetime.timedelta(weeks=4), + frozenset((token_day, token_end))) + token_year = Token(re.compile(r"(\d+)Y"), + datetime.timedelta(weeks=52), + frozenset((token_month, token_end))) + token_week = Token(re.compile(r"(\d+)W"), + datetime.timedelta(weeks=1), + frozenset((token_end,))) + token_duration = Token(re.compile(r"P"), None, + frozenset((token_year, token_month, + token_day, token_time, + token_week))), + # Define starting values + value = datetime.timedelta() # Value so far + found_token = None + followers = frozenset(token_duration,) # Following valid tokens + s = duration # String left to parse + # Loop until end token is found + while found_token is not token_end: + # Search for any currently valid tokens + for token in followers: + match = token.regexp.match(s) + if match is not None: + # Token found + if token.value is not None: + # Value found, parse digits + factor = int(match.group(1), 10) + # Add to value so far + value += factor * token.value + # Strip token from string + s = token.regexp.sub("", s, 1) + # Go to found token + found_token = token + # Set valid next tokens + followers = found_token.followers + break + else: + # No currently valid tokens were found + raise ValueError("Invalid RFC 3339 duration") + # End token found + return value + + def string_to_delta(interval): """Parse a string and return a datetime.timedelta @@ -2106,6 +2228,12 @@ >>> string_to_delta('5m 30s') datetime.timedelta(0, 330) """ + + try: + return rfc3339_duration_to_delta(interval) + except ValueError: + pass + timevalue = datetime.timedelta(0) for s in interval.split(): try: @@ -2174,7 +2302,7 @@ help="Run self-test") parser.add_argument("--debug", action="store_true", help="Debug mode; run in foreground and log" - " to terminal") + " to terminal", default=None) parser.add_argument("--debuglevel", metavar="LEVEL", help="Debug level for stdout output") parser.add_argument("--priority", help="GnuTLS" @@ -2187,19 +2315,20 @@ " files") parser.add_argument("--no-dbus", action="store_false", dest="use_dbus", help="Do not provide D-Bus" - " system bus interface") + " system bus interface", default=None) parser.add_argument("--no-ipv6", action="store_false", - dest="use_ipv6", help="Do not use IPv6") + dest="use_ipv6", help="Do not use IPv6", + default=None) parser.add_argument("--no-restore", action="store_false", dest="restore", help="Do not restore stored" - " state") + " state", default=None) parser.add_argument("--socket", type=int, help="Specify a file descriptor to a network" " socket to use instead of creating one") parser.add_argument("--statedir", metavar="DIR", help="Directory to save/restore state in") parser.add_argument("--foreground", action="store_true", - help="Run in foreground") + help="Run in foreground", default=None) options = parser.parse_args() @@ -2214,7 +2343,7 @@ "port": "", "debug": "False", "priority": - "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP", + "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224", "servicename": "Mandos", "use_dbus": "True", "use_ipv6": "True", @@ -2264,6 +2393,10 @@ for option in server_settings.keys(): if type(server_settings[option]) is str: server_settings[option] = unicode(server_settings[option]) + # Force all boolean options to be boolean + for option in ("debug", "use_dbus", "use_ipv6", "restore", + "foreground"): + server_settings[option] = bool(server_settings[option]) # Debug implies foreground if server_settings["debug"]: server_settings["foreground"] = True @@ -2409,6 +2542,14 @@ old_client_settings = {} clients_data = {} + # This is used to redirect stdout and stderr for checker processes + global wnull + wnull = open(os.devnull, "w") # A writable /dev/null + # Only used if server is running in foreground but not in debug + # mode + if debug or not foreground: + wnull.close() + # Get client data and settings from last running state. if server_settings["restore"]: try: @@ -2430,6 +2571,10 @@ with PGPEngine() as pgp: for client_name, client in clients_data.iteritems(): + # Skip removed clients + if client_name not in client_settings: + continue + # Decide which value to use after restoring saved state. # We have three different values: Old config file, # new config file, and saved state. @@ -2497,7 +2642,8 @@ # Create all client objects for client_name, client in clients_data.iteritems(): tcp_server.clients[client_name] = client_class( - name = client_name, settings = client) + name = client_name, settings = client, + server_settings = server_settings) if not tcp_server.clients: logger.warning("No clients defined") @@ -2586,6 +2732,7 @@ service.cleanup() multiprocessing.active_children() + wnull.close() if not (tcp_server.clients or client_settings): return @@ -2603,7 +2750,7 @@ # A list of attributes that can not be pickled # + secret. exclude = set(("bus", "changedstate", "secret", - "checker")) + "checker", "server_settings")) for name, typ in (inspect.getmembers (dbus.service.Object)): exclude.add(name) === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2012-05-26 22:48:45 +0000 +++ mandos-clients.conf.xml 2012-06-23 00:58:49 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/clients.conf"> - + %common; ]> @@ -335,17 +335,12 @@ option. - The TIME is specified as a - space-separated number of values, each of which is a - number and a one-character suffix. The suffix must be one - of d, s, m, - h, and w for days, seconds, - minutes, hours, and weeks, respectively. The values are - added together to give the total time value, so all of - 330s, - 110s 110s 110s, and - 5m 30s will give a value - of five minutes and thirty seconds. + The TIME is specified as an RFC + 3339 duration; for example + P1Y2M3DT4H5M6S meaning + one year, two months, three days, four hours, five + minutes, and six seconds. Some values can be omitted, see + RFC 3339 Appendix A for details. @@ -465,8 +460,8 @@ [DEFAULT] -timeout = 5m -interval = 2m +timeout = PT5M +interval = PT2M checker = fping -q -- %%(host)s # Client "foo" @@ -489,15 +484,15 @@ 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O QlnHIvPzEArRQLo= host = foo.example.org -interval = 1m +interval = PT1M # Client "bar" [bar] fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 secfile = /etc/mandos/bar-secret -timeout = 15m +timeout = PT15M approved_by_default = False -approval_delay = 30s +approval_delay = PT30S @@ -516,6 +511,20 @@ fping 8 + + + + RFC 3339: Date and Time on the Internet: + Timestamps + + + + The time intervals are in the "duration" format, as + specified in ABNF in Appendix A of RFC 3339. + + + + === modified file 'mandos-ctl' --- mandos-ctl 2012-06-17 22:26:40 +0000 +++ mandos-ctl 2012-11-14 21:03:24 +0000 @@ -29,12 +29,15 @@ from future_builtins import * import sys -import dbus import argparse import locale import datetime import re import os +import collections +import doctest + +import dbus locale.setlocale(locale.LC_ALL, "") @@ -80,6 +83,106 @@ seconds = td.seconds % 60, )) + +def rfc3339_duration_to_delta(duration): + """Parse an RFC 3339 "duration" and return a datetime.timedelta + + >>> rfc3339_duration_to_delta("P7D") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT60S") + datetime.timedelta(0, 60) + >>> rfc3339_duration_to_delta("PT60M") + datetime.timedelta(0, 3600) + >>> rfc3339_duration_to_delta("PT24H") + datetime.timedelta(1) + >>> rfc3339_duration_to_delta("P1W") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT5M30S") + datetime.timedelta(0, 330) + >>> rfc3339_duration_to_delta("P1DT3M20S") + datetime.timedelta(1, 200) + """ + + # Parsing an RFC 3339 duration with regular expressions is not + # possible - there would have to be multiple places for the same + # values, like seconds. The current code, while more esoteric, is + # cleaner without depending on a parsing library. If Python had a + # built-in library for parsing we would use it, but we'd like to + # avoid excessive use of external libraries. + + # New type for defining tokens, syntax, and semantics all-in-one + Token = collections.namedtuple("Token", + ("regexp", # To match token; if + # "value" is not None, + # must have a "group" + # containing digits + "value", # datetime.timedelta or + # None + "followers")) # Tokens valid after + # this token + # RFC 3339 "duration" tokens, syntax, and semantics; taken from + # the "duration" ABNF definition in RFC 3339, Appendix A. + token_end = Token(re.compile(r"$"), None, frozenset()) + token_second = Token(re.compile(r"(\d+)S"), + datetime.timedelta(seconds=1), + frozenset((token_end,))) + token_minute = Token(re.compile(r"(\d+)M"), + datetime.timedelta(minutes=1), + frozenset((token_second, token_end))) + token_hour = Token(re.compile(r"(\d+)H"), + datetime.timedelta(hours=1), + frozenset((token_minute, token_end))) + token_time = Token(re.compile(r"T"), + None, + frozenset((token_hour, token_minute, + token_second))) + token_day = Token(re.compile(r"(\d+)D"), + datetime.timedelta(days=1), + frozenset((token_time, token_end))) + token_month = Token(re.compile(r"(\d+)M"), + datetime.timedelta(weeks=4), + frozenset((token_day, token_end))) + token_year = Token(re.compile(r"(\d+)Y"), + datetime.timedelta(weeks=52), + frozenset((token_month, token_end))) + token_week = Token(re.compile(r"(\d+)W"), + datetime.timedelta(weeks=1), + frozenset((token_end,))) + token_duration = Token(re.compile(r"P"), None, + frozenset((token_year, token_month, + token_day, token_time, + token_week))), + # Define starting values + value = datetime.timedelta() # Value so far + found_token = None + followers = frozenset(token_duration,) # Following valid tokens + s = duration # String left to parse + # Loop until end token is found + while found_token is not token_end: + # Search for any currently valid tokens + for token in followers: + match = token.regexp.match(s) + if match is not None: + # Token found + if token.value is not None: + # Value found, parse digits + factor = int(match.group(1), 10) + # Add to value so far + value += factor * token.value + # Strip token from string + s = token.regexp.sub("", s, 1) + # Go to found token + found_token = token + # Set valid next tokens + followers = found_token.followers + break + else: + # No currently valid tokens were found + raise ValueError("Invalid RFC 3339 duration") + # End token found + return value + + def string_to_delta(interval): """Parse a string and return a datetime.timedelta @@ -96,8 +199,14 @@ >>> string_to_delta("5m 30s") datetime.timedelta(0, 330) """ + + try: + return rfc3339_duration_to_delta(interval) + except ValueError: + pass + value = datetime.timedelta(0) - regexp = re.compile("(\d+)([dsmhw]?)") + regexp = re.compile(r"(\d+)([dsmhw]?)") for num, suffix in regexp.findall(interval): if suffix == "d": @@ -207,6 +316,8 @@ help="Approve any current client request") parser.add_argument("-D", "--deny", action="store_true", help="Deny any current client request") + parser.add_argument("--check", action="store_true", + help="Run self-test") parser.add_argument("client", nargs="*", help="Client name") options = parser.parse_args() @@ -217,6 +328,10 @@ " --all.") if options.all and not has_actions(options): parser.error("--all requires an action.") + + if options.check: + fail_count, test_count = doctest.testmod() + sys.exit(0 if fail_count == 0 else 1) try: bus = dbus.SystemBus() === modified file 'mandos-ctl.xml' --- mandos-ctl.xml 2011-12-31 23:05:34 +0000 +++ mandos-ctl.xml 2012-06-22 23:33:56 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -195,6 +195,10 @@ + + &COMMANDNAME; + + @@ -476,6 +480,15 @@ + + + + + Run self-tests. This includes any unit tests, etc. + + + + === modified file 'mandos-keygen' --- mandos-keygen 2012-06-17 22:26:40 +0000 +++ mandos-keygen 2013-09-29 15:52:19 +0000 @@ -24,10 +24,10 @@ VERSION="1.6.0" KEYDIR="/etc/keys/mandos" -KEYTYPE=DSA -KEYLENGTH=2048 -SUBKEYTYPE=ELG-E -SUBKEYLENGTH=2048 +KEYTYPE=RSA +KEYLENGTH=4096 +SUBKEYTYPE=RSA +SUBKEYLENGTH=4096 KEYNAME="`hostname --fqdn 2>/dev/null || hostname`" KEYEMAIL="" KEYCOMMENT="Mandos client key" @@ -60,13 +60,13 @@ -v, --version Show program's version number and exit -h, --help Show this help message and exit -d DIR, --dir DIR Target directory for key files - -t TYPE, --type TYPE Key type. Default is DSA. + -t TYPE, --type TYPE Key type. Default is RSA. -l BITS, --length BITS - Key length in bits. Default is 2048. + Key length in bits. Default is 4096. -s TYPE, --subtype TYPE Subkey type. Default is ELG-E. -L BITS, --sublength BITS - Subkey length in bits. Default is 2048. + Subkey length in bits. Default is 4096. -n NAME, --name NAME Name of key. Default is the FQDN. -e ADDRESS, --email ADDRESS Email address of key. Default is empty. @@ -294,9 +294,11 @@ cat "$PASSFILE" else tty --quiet && stty -echo - read -p "Enter passphrase: " first + echo -n "Enter passphrase: " + read first tty --quiet && echo >&2 - read -p "Repeat passphrase: " second + echo -n "Repeat passphrase: " + read second if tty --quiet; then echo >&2 stty echo === modified file 'mandos-keygen.xml' --- mandos-keygen.xml 2011-12-31 23:05:34 +0000 +++ mandos-keygen.xml 2013-09-29 15:52:19 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -227,7 +227,7 @@ TYPE - Key type. Default is DSA. + Key type. Default is RSA. @@ -239,7 +239,7 @@ BITS - Key length in bits. Default is 2048. + Key length in bits. Default is 4096. @@ -251,7 +251,7 @@ KEYTYPE - Subkey type. Default is ELG-E (Elgamal + Subkey type. Default is RSA (Elgamal encryption-only). @@ -264,7 +264,7 @@ BITS - Subkey length in bits. Default is 2048. + Subkey length in bits. Default is 4096. === modified file 'mandos-monitor' --- mandos-monitor 2012-06-17 22:26:40 +0000 +++ mandos-monitor 2013-05-22 20:00:18 +0000 @@ -25,8 +25,10 @@ from __future__ import (division, absolute_import, print_function, unicode_literals) - -from future_builtins import * +try: + from future_builtins import * +except ImportError: + pass import sys import os @@ -38,14 +40,18 @@ import urwid from dbus.mainloop.glib import DBusGMainLoop -import gobject +try: + import gobject +except ImportError: + from gi.repository import GObject as gobject import dbus -import UserList - import locale +if sys.version_info[0] == 2: + str = unicode + locale.setlocale(locale.LC_ALL, '') import logging @@ -57,14 +63,6 @@ client_interface = domain + '.Mandos.Client' version = "1.6.0" -# Always run in monochrome mode -urwid.curses_display.curses.has_colors = lambda : False - -# Urwid doesn't support blinking, but we want it. Since we have no -# use for underline on its own, we make underline also always blink. -urwid.curses_display.curses.A_UNDERLINE |= ( - urwid.curses_display.curses.A_BLINK) - def isoformat_to_datetime(iso): "Parse an ISO 8601 date string to a datetime.datetime()" if not iso: @@ -210,7 +208,7 @@ to log in the future. """ #self.logger('Client {0} started checker "{1}"' # .format(self.properties["Name"], - # unicode(command))) + # str(command))) pass def got_secret(self): @@ -279,7 +277,7 @@ message = "Approval in {0}. (d)eny?" else: message = "Denial in {0}. (a)pprove?" - message = message.format(unicode(timer).rsplit(".", 1)[0]) + message = message.format(str(timer).rsplit(".", 1)[0]) self.using_timer(True) elif self.properties["LastCheckerStatus"] != 0: # When checker has failed, show timer until client expires @@ -293,7 +291,7 @@ datetime.timedelta()) message = ('A checker has failed! Time until client' ' gets disabled: {0}' - .format(unicode(timer).rsplit(".", 1)[0])) + .format(str(timer).rsplit(".", 1)[0])) self.using_timer(True) else: message = "enabled" @@ -382,7 +380,7 @@ def property_changed(self, property=None, **kwargs): """Call self.update() if old value is not new value. This overrides the method from MandosClientPropertyCache""" - property_name = unicode(property) + property_name = str(property) old_value = self.properties.get(property_name) super(MandosClientWidget, self).property_changed( property=property, **kwargs) @@ -415,20 +413,21 @@ ("normal", "default", "default", None), ("bold", - "default", "default", "bold"), + "bold", "default", "bold"), ("underline-blink", - "default", "default", "underline"), + "underline,blink", "default", "underline,blink"), ("standout", - "default", "default", "standout"), + "standout", "default", "standout"), ("bold-underline-blink", - "default", "default", ("bold", "underline")), + "bold,underline,blink", "default", "bold,underline,blink"), ("bold-standout", - "default", "default", ("bold", "standout")), + "bold,standout", "default", "bold,standout"), ("underline-blink-standout", - "default", "default", ("underline", "standout")), + "underline,blink,standout", "default", + "underline,blink,standout"), ("bold-underline-blink-standout", - "default", "default", ("bold", "underline", - "standout")), + "bold,underline,blink,standout", "default", + "bold,underline,blink,standout"), )) if urwid.supports_unicode(): @@ -489,6 +488,7 @@ self.topwidget = urwid.Pile(self.uilist) def log_message(self, message): + """Log message formatted with timestamp""" timestamp = datetime.datetime.now().isoformat() self.log_message_raw(timestamp + ": " + message) @@ -507,7 +507,7 @@ self.log_visible = not self.log_visible self.rebuild() #self.log_message("Log visibility changed to: " - # + unicode(self.log_visible)) + # + str(self.log_visible)) def change_log_display(self): """Change type of log display. @@ -553,7 +553,7 @@ if path is None: path = client.proxy.object_path self.clients_dict[path] = client - self.clients.sort(None, lambda c: c.properties["Name"]) + self.clients.sort(key=lambda c: c.properties["Name"]) self.refresh() def remove_client(self, client, path=None): @@ -561,11 +561,6 @@ if path is None: path = client.proxy.object_path del self.clients_dict[path] - if not self.clients_dict: - # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker - # is completely emptied, we need to recreate it. - self.clients = urwid.SimpleListWalker([]) - self.rebuild() self.refresh() def refresh(self): @@ -584,7 +579,10 @@ try: mandos_clients = (self.mandos_serv .GetAllClientsWithProperties()) + if not mandos_clients: + self.log_message_raw(("bold", "Note: Server has no clients.")) except dbus.exceptions.DBusException: + self.log_message_raw(("bold", "Note: No Mandos server running.")) mandos_clients = dbus.Dictionary() (self.mandos_serv @@ -602,7 +600,7 @@ self.client_not_found, dbus_interface=server_interface, byte_arrays=True)) - for path, client in mandos_clients.iteritems(): + for path, client in mandos_clients.items(): client_proxy_object = self.bus.get_object(self.busname, path) self.add_client(MandosClientWidget(server_proxy_object @@ -720,7 +718,7 @@ ui.run() except KeyboardInterrupt: ui.screen.stop() -except Exception, e: - ui.log_message(unicode(e)) +except Exception as e: + ui.log_message(str(e)) ui.screen.stop() raise === modified file 'mandos-options.xml' --- mandos-options.xml 2012-06-17 14:55:31 +0000 +++ mandos-options.xml 2013-06-23 15:13:06 +0000 @@ -49,8 +49,9 @@ GnuTLS priority string for the TLS handshake. The default is SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP. See - gnutls_priority_init + >SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224. + See gnutls_priority_init 3 for the syntax. Warning: changing this may make the TLS handshake fail, making server-client === modified file 'mandos.conf' --- mandos.conf 2012-06-17 14:55:31 +0000 +++ mandos.conf 2013-06-23 15:13:06 +0000 @@ -23,7 +23,7 @@ ;debug = False # GnuTLS priority for the TLS handshake. See gnutls_priority_init(3). -;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP +;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224 # Zeroconf service name. You need to change this if you for some # reason want to run more than one server on the same *host*. === modified file 'plugin-runner.c' --- plugin-runner.c 2011-12-31 23:05:34 +0000 +++ plugin-runner.c 2013-08-27 21:47:35 +0000 @@ -771,7 +771,7 @@ } /* Lower permissions */ - setgid(gid); + ret = setgid(gid); if(ret == -1){ error(0, errno, "setgid"); } === modified file 'plugins.d/mandos-client.c' --- plugins.d/mandos-client.c 2012-06-17 12:23:31 +0000 +++ plugins.d/mandos-client.c 2013-10-05 19:34:40 +0000 @@ -187,7 +187,7 @@ TEMP_FAILURE_RETRY(fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name)); - return TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); + return (int)TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); } /* @@ -2331,7 +2331,7 @@ fprintf_plus(stderr, "Retrying in %d seconds\n", (int)retry_interval); } - sleep((int)retry_interval); + sleep((unsigned int)retry_interval); } if (not quit_now){ === modified file 'plugins.d/mandos-client.xml' --- plugins.d/mandos-client.xml 2012-06-17 02:30:59 +0000 +++ plugins.d/mandos-client.xml 2013-06-23 15:13:06 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -512,7 +512,7 @@ It is not necessary to print any non-executable files already in the network hook directory, these will be copied implicitly if they otherwise satisfy the name - requirement. + requirements. @@ -666,7 +666,7 @@ - Normal invocation needs no options, if the network interface + Normal invocation needs no options, if the network interfaces can be automatically determined: @@ -675,8 +675,8 @@ - Search for Mandos servers (and connect to them) using another - interface: + Search for Mandos servers (and connect to them) using one + specific interface: @@ -846,7 +846,7 @@ This client uses IPv6 link-local addresses, which are immediately usable since a link-local addresses is - automatically assigned to a network interfaces when it + automatically assigned to a network interface when it is brought up. === modified file 'plugins.d/password-prompt.c' --- plugins.d/password-prompt.c 2011-12-31 23:05:34 +0000 +++ plugins.d/password-prompt.c 2013-10-05 19:34:40 +0000 @@ -79,7 +79,7 @@ TEMP_FAILURE_RETRY(fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name)); - return TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); + return (int)TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); } /* Function to use when printing errors */ === modified file 'plugins.d/plymouth.c' --- plugins.d/plymouth.c 2011-12-31 23:05:34 +0000 +++ plugins.d/plymouth.c 2013-08-27 21:47:35 +0000 @@ -156,7 +156,7 @@ __attribute__((nonnull (2, 3))) bool exec_and_wait(pid_t *pid_return, const char *path, - const char **argv, bool interruptable, + const char * const *argv, bool interruptable, bool daemonize){ int status; int ret; @@ -312,7 +312,7 @@ return 0; } -const char **getargv(pid_t pid){ +const char * const * getargv(pid_t pid){ int cl_fd; char *cmdline_filename; ssize_t sret; @@ -379,7 +379,7 @@ return NULL; } argz_extract(cmdline, cmdline_len, argv); /* Create argv */ - return (const char **)argv; + return (const char * const *)argv; } int main(__attribute__((unused))int argc, @@ -460,7 +460,7 @@ } kill_and_wait(plymouth_command_pid); - const char **plymouthd_argv; + const char * const *plymouthd_argv; pid_t pid = get_pid(); if(pid == 0){ error_plus(0, 0, "plymouthd pid not found");