=== added directory '.bzr-builddeb' === added file '.bzr-builddeb/default.conf' --- .bzr-builddeb/default.conf 1970-01-01 00:00:00 +0000 +++ .bzr-builddeb/default.conf 2008-09-17 00:34:09 +0000 @@ -0,0 +1,2 @@ +[BUILDDEB] +split = True === added file '.bzrignore' --- .bzrignore 1970-01-01 00:00:00 +0000 +++ .bzrignore 2008-10-03 09:32:30 +0000 @@ -0,0 +1,14 @@ +*.5 +*.8 +*.8mandos +confdir +debian/po/messages.mo +debian/po/templates.pot +keydir +man +plugin-runner +plugins.d/askpass-fifo +plugins.d/mandos-client +plugins.d/password-prompt +plugins.d/splashy +plugins.d/usplash === added file 'DBUS-API' --- DBUS-API 1970-01-01 00:00:00 +0000 +++ DBUS-API 2010-09-26 18:32:58 +0000 @@ -0,0 +1,172 @@ + -*- mode: org; coding: utf-8 -*- + + Mandos Server D-Bus Interface + +This file documents the D-Bus interface to the Mandos server. + +* Bus: System bus + Bus name: "se.bsnet.fukt.Mandos" + + +* Object Paths: + + | Path | Object | + |-----------------------+-------------------| + | "/" | The Mandos Server | + | "/clients/CLIENTNAME" | Mandos Client | + + +* Mandos Server Interface: + Interface name: "se.bsnet.fukt.Mandos" + +** Methods: +*** GetAllClients() → (ao: Clients) + Returns an array of all client D-Bus object paths + +*** GetAllClientsWithProperties() → (a{oa{sv}}: ClientProperties) + Returns an array of all clients and all their properties + +*** RemoveClient(o: ObjectPath) → nothing + Removes a client + +** Signals: +*** ClientAdded(o: ObjectPath) + A new client was added. + +*** ClientNotFound(s: Fingerprint, s: Address) + A client connected from Address using Fingerprint, but was + rejected because it was not found in the server. The fingerprint + is represented as a string of hexadecimal digits. The address is + an IPv4 or IPv6 address in its normal string format. + +*** ClientRemoved(o: ObjectPath, s: Name) + A client named Name on ObjectPath was removed. + + +* Mandos Client Interface: + Interface name: "se.bsnet.fukt.Mandos.Client" + +** Methods +*** Approve(b: Approve) → nothing + Approve or deny a connected client waiting for approval. If + denied, a client will not be sent its secret. + +*** CheckedOK() → nothing + Assert that this client has been checked and found to be alive. + This will restart the timeout before disabling this client. See + also the "LastCheckedOK" property. + +*** Disable() → nothing + Disable this client. See also the "Enabled" property. + +*** Enable() → nothing + Enable this client. See also the "Enabled" property. + +*** StartChecker() → nothing + Start a new checker for this client, if none is currently + running. See also the "CheckerRunning" property. + +*** StopChecker() → nothing + Abort a running checker process for this client, if any. See also + the "CheckerRunning" property. + +** Properties + + Note: Many of these properties directly correspond to a setting in + "clients.conf", in which case they are fully documented in + mandos-clients.conf(5). + + | Name | Type | Access | clients.conf | + |-------------------------+------+------------+---------------------| + | ApprovedByDefault | b | Read/Write | approved_by_default | + | ApprovalDelay (a) | t | Read/Write | approval_delay | + | ApprovalDuration (a) | t | Read/Write | approval_duration | + | ApprovalPending (b) | b | Read | N/A | + | Checker | s | Read/Write | checker | + | CheckerRunning (c) | b | Read/Write | N/A | + | Created (d) | s | Read | N/A | + | Enabled (e) | b | Read/Write | N/A | + | Fingerprint | s | Read | fingerprint | + | Host | s | Read/Write | host | + | Interval (a) | t | Read/Write | interval | + | LastApprovalRequest (f) | s | Read | N/A | + | LastCheckedOK (g) | s | Read/Write | N/A | + | LastEnabled (h) | s | Read | N/A | + | Name | s | Read | (Section name) | + | ObjectPath | o | Read | N/A | + | Secret (i) | ay | Write | secret (or secfile) | + | Timeout (a) | t | Read/Write | timeout | + + a) Represented as milliseconds. + + b) An approval is currently pending. + + c) Setting this property is equivalent to calling StartChecker() or + StopChecker(). + + d) The creation time of this client object, as a RFC 3339 string. + + e) Setting this property is equivalent to calling Enable() or + Disable(). + + f) The time of the last approval request, as a RFC 3339 string, or + an empty string if this has not happened. + + g) The last time a checker was successful, as a RFC 3339 string, or + an empty string if this has not happened. Setting this property + is equivalent to calling CheckedOK(), i.e. the current time is + set, regardless of the string sent. Please always use an empty + string when setting this property, to allow for possible future + expansion. + + h) The last time this client was enabled, as a RFC 3339 string, or + an empty string if this has not happened. + + i) A raw byte array, not hexadecimal digits. + +** Signals +*** CheckerCompleted(n: Exitcode, x: Waitstatus, s: Command) + A checker (Command) has completed. Exitcode is either the exit + code or -1 for abnormal exit. In any case, the full Waitstatus + (as from wait(2)) is also available. + +*** CheckerStarted(s: Command) + A checker command (Command) has just been started. + +*** GotSecret() + This client has been sent its secret. + +*** NeedApproval(t: Timeout, b: ApprovedByDefault) + This client will be approved or denied in exactly Timeout + milliseconds, depending on ApprovedByDefault. Approve() can now + usefully be called on this client object. + +*** PropertyChanged(s: Property, v: Value) + The Property on this client has changed to Value. + +*** Rejected(s: Reason) + This client was not given its secret for a specified Reason. + +* Copyright + + Copyright © 2010 Teddy Hogeborn + Copyright © 2010 Björn Påhlsson + +** License: + + 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 + . + + +#+STARTUP: showall === added file 'INSTALL' --- INSTALL 1970-01-01 00:00:00 +0000 +++ INSTALL 2009-02-15 09:28:06 +0000 @@ -0,0 +1,133 @@ +-*- org -*- + +* Prerequisites + +** Operating System + + Debian 5.0 "lenny" or Ubuntu 8.04 "Hardy Heron". + + This is mostly for the support scripts which make sure that the + client is installed and started in the initial RAM disk environment + and that the initrd.img file is automatically made unreadable. The + server and client programs themselves *could* be run in other + distributions, but they *are* specific to GNU/Linux systems, and + are not intended to be portable to other Unixes. + +** Libraries + + The following libraries and packages are needed. (It is possible + that it might work with older versions of some of these, but these + versions are confirmed to work. Newer versions are almost + certainly OK.) + +*** Documentation + These are required to build the manual pages for both the server + and client: + + + DocBook 4.5 http://www.docbook.org/ + Note: DocBook 5.0 is not compatible. + + DocBook XSL stylesheets 1.71.0 + http://wiki.docbook.org/topic/DocBookXslStylesheets + + Package names: + docbook docbook-xsl + + 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/ + + Python 2.5 http://www.python.org/ + + Python-GnuTLS 1.1.5 http://pypi.python.org/pypi/python-gnutls/ + + dbus-python 0.82.4 http://dbus.freedesktop.org/doc/dbus-python/ + + python-ctypes 1.0.0 http://pypi.python.org/pypi/ctypes + + PyGObject 2.14.2 http://library.gnome.org/devel/pygobject/ + + Strongly recommended: + + fping 2.4b2-to-ipv6 http://www.fping.com/ + + Package names: + python-gnutls avahi-daemon python python-avahi python-dbus + python-ctypes python-gobject + +*** Mandos Client + + initramfs-tools 0.85i + http://packages.qa.debian.org/i/initramfs-tools.html + + GnuTLS 2.4 http://www.gnu.org/software/gnutls/ + + Avahi 0.6.16 http://www.avahi.org/ + + GnuPG 1.4.9 http://www.gnupg.org/ + + GPGME 1.1.6 http://www.gnupg.org/related_software/gpgme/ + + Package names: + initramfs-tools libgnutls-dev libavahi-core-dev gnupg + libgpgme11-dev + +* Installing the Mandos server + + 1. Do "make doc". + + 2. On the computer to run as a Mandos server, run the following + command: + For Debian: su -c 'make install-server' + For Ubuntu: sudo make install-server + + (This creates a configuration without any clients configured; you + need an actually configured client to do that; see below.) + +* Installing the Mandos client. + + 1. Do "make all doc". + + 2. On the computer to run as a Mandos client, run the following + command: + For Debian: su -c 'make install-client' + For Ubuntu: sudo make install-client + + This will also create an OpenPGP key, which will take some time + and entropy, so be patient. + + 3. Run the following command: + For Debian: su -c 'mandos-keygen --password' + For Ubuntu: sudo mandos-keygen --password + + When prompted, enter the password/passphrase for the encrypted + root file system on this client computer. The command will + output a section of text, starting with a [section header]. Copy + and append this to the file "/etc/mandos/clients.conf" *on the + server computer*. + + 4. Configure the client to use the correct network interface. The + default is "eth0", and if this needs to be adjusted, it will be + necessary to edit /etc/mandos/plugin-runner.conf to uncomment and + change the line there. If that file is changed, the initrd.img + file must be updated, possibly using the following command: + + # update-initramfs -k all -u + + 5. On the server computer, start the server by running the command + For Debian: su -c 'invoke-rc.d mandos start' + For Ubuntu: sudo invoke-rc.d mandos start + + At this point, it is possible to verify that the correct password + will be received by the client by running the command: + + # /usr/lib/mandos/plugins.d/mandos-client \ + --pubkey=/etc/keys/mandos/pubkey.txt \ + --seckey=/etc/keys/mandos/seckey.txt; echo + + This command should retrieve the password from the server, + decrypt it, and output it to standard output. + + After this, the client computer should be able to reboot without + needing a password entered on the console, as long as it does not + take more than an hour to reboot. + +* Further customizations + + You may want to tighten or loosen the timeouts in the server + configuration files; see mandos.conf(5) and mandos-clients.conf(5). + If IPsec is not used, it is suggested that a more cryptographically + secure checker program is used and configured, since without IPsec + ping packets can be faked. === modified file 'Makefile' --- Makefile 2008-08-19 23:44:17 +0000 +++ Makefile 2010-09-23 20:14:08 +0000 @@ -1,39 +1,63 @@ WARN=-O -Wall -Wformat=2 -Winit-self -Wmissing-include-dirs \ -Wswitch-default -Wswitch-enum -Wunused-parameter \ - -Wstrict-aliasing=2 -Wextra -Wfloat-equal -Wundef -Wshadow \ + -Wstrict-aliasing=1 -Wextra -Wfloat-equal -Wundef -Wshadow \ -Wunsafe-loop-optimizations -Wpointer-arith \ -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings \ -Wconversion -Wstrict-prototypes -Wold-style-definition \ - -Wpacked -Wnested-externs -Wunreachable-code -Winline \ - -Wvolatile-register-var -DEBUG=-ggdb3 + -Wpacked -Wnested-externs -Winline -Wvolatile-register-var +# -Wunreachable-code +#DEBUG=-ggdb3 # For info about _FORTIFY_SOURCE, see -# -FORTIFY=-D_FORTIFY_SOURCE=2 # -fstack-protector-all +# +# and . +FORTIFY=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC +LINK_FORTIFY_LD=-z relro -z now +LINK_FORTIFY= + +# If BROKEN_PIE is set, do not build with -pie +ifndef BROKEN_PIE +FORTIFY += -fPIE +LINK_FORTIFY += -pie +endif #COVERAGE=--coverage OPTIMIZE=-Os LANGUAGE=-std=gnu99 -# PREFIX=/usr/local -PREFIX=/usr -# CONFDIR=/usr/local/lib/mandos -CONFDIR=/etc/mandos -# MANDIR=/usr/local/man -MANDIR=/usr/share/man - -GNUTLS_CFLAGS=$(shell libgnutls-config --cflags) -GNUTLS_LIBS=$(shell libgnutls-config --libs) +htmldir=man +version=1.0.14 +SED=sed + +## 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 +## + +## These settings are for a package-type install +PREFIX=$(DESTDIR)/usr +CONFDIR=$(DESTDIR)/etc/mandos +KEYDIR=$(DESTDIR)/etc/keys/mandos +MANDIR=$(PREFIX)/share/man +INITRAMFSTOOLS=$(DESTDIR)/usr/share/initramfs-tools +## + +GNUTLS_CFLAGS=$(shell pkg-config --cflags-only-I gnutls) +GNUTLS_LIBS=$(shell pkg-config --libs gnutls) AVAHI_CFLAGS=$(shell pkg-config --cflags-only-I avahi-core) AVAHI_LIBS=$(shell pkg-config --libs avahi-core) -GPGME_CFLAGS=$(shell gpgme-config --cflags) -GPGME_LIBS=$(shell gpgme-config --libs) +GPGME_CFLAGS=$(shell gpgme-config --cflags; getconf LFS_CFLAGS) +GPGME_LIBS=$(shell gpgme-config --libs; getconf LFS_LIBS; \ + getconf LFS_LDFLAGS) # Do not change these two CFLAGS=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) \ - $(LANGUAGE) $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(GPGME_CFLAGS) -LDFLAGS=$(COVERAGE) + $(LANGUAGE) $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(GPGME_CFLAGS) \ + -DVERSION='"$(version)"' +LDFLAGS=$(COVERAGE) $(LINK_FORTIFY) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) -# Commands to format a DocBook refentry document into a manual page -DOCBOOKTOMAN=cd $(dir $^); xsltproc --nonet --xinclude \ +# Commands to format a DocBook document into a manual page +DOCBOOKTOMAN=$(strip cd $(dir $<); xsltproc --nonet --xinclude \ --param man.charmap.use.subset 0 \ --param make.year.ranges 1 \ --param make.single.year.ranges 1 \ @@ -41,148 +65,364 @@ --param man.authors.section.enabled 0 \ /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \ $(notdir $<); \ - $(MANPOST) $(notdir $@) -# DocBook-to-man post-processing to fix a \n escape bug -MANPOST=sed --in-place --expression='s,\\en,\en,g;s,\\een,\\en,g' - -PLUGINS=plugins.d/password-prompt plugins.d/password-request -PROGS=plugin-runner $(PLUGINS) -DOCS=mandos.8 plugin-runner.8mandos mandos-keygen.8 \ - plugins.d/password-request.8mandos \ - plugins.d/password-prompt.8mandos mandos.conf.5 \ - mandos-clients.conf.5 - -objects=$(addsuffix .o,$(PROGS)) - -all: $(PROGS) + $(MANPOST) $(notdir $@)) +# DocBook-to-man post-processing to fix a '\n' escape bug +MANPOST=$(SED) --in-place --expression='s,\\\\en,\\en,g;s,\\n,\\en,g' + +DOCBOOKTOHTML=$(strip xsltproc --nonet --xinclude \ + --param make.year.ranges 1 \ + --param make.single.year.ranges 1 \ + --param man.output.quietly 1 \ + --param man.authors.section.enabled 0 \ + --param citerefentry.link 1 \ + --output $@ \ + /usr/share/xml/docbook/stylesheet/nwalsh/xhtml/docbook.xsl \ + $<; $(HTMLPOST) $@) +# Fix citerefentry links +HTMLPOST=$(SED) --in-place \ + --expression='s/\(\)\([^<]*\)\(<\/span>(\)\([^)]*\)\()<\/span><\/a>\)/\1\3.\5\2\3\4\5\6/g' + +PLUGINS=plugins.d/password-prompt plugins.d/mandos-client \ + plugins.d/usplash plugins.d/splashy plugins.d/askpass-fifo \ + plugins.d/plymouth +CPROGS=plugin-runner $(PLUGINS) +PROGS=mandos mandos-keygen mandos-ctl mandos-monitor $(CPROGS) +DOCS=mandos.8 mandos-keygen.8 mandos-monitor.8 mandos-ctl.8 \ + mandos.conf.5 mandos-clients.conf.5 plugin-runner.8mandos \ + plugins.d/mandos-client.8mandos \ + plugins.d/password-prompt.8mandos plugins.d/usplash.8mandos \ + plugins.d/splashy.8mandos plugins.d/askpass-fifo.8mandos \ + plugins.d/plymouth.8mandos + +htmldocs=$(addsuffix .xhtml,$(DOCS)) + +objects=$(addsuffix .o,$(CPROGS)) + +all: $(PROGS) mandos.lsm doc: $(DOCS) -%.5: %.xml - $(DOCBOOKTOMAN) - -%.8: %.xml - $(DOCBOOKTOMAN) - -%.8mandos: %.xml - $(DOCBOOKTOMAN) - -mandos.8: mandos.xml mandos-options.xml - $(DOCBOOKTOMAN) - -mandos.conf.5: mandos.conf.xml mandos-options.xml - $(DOCBOOKTOMAN) - -plugins.d/password-request: plugins.d/password-request.o - $(LINK.o) $(GNUTLS_LIBS) $(AVAHI_LIBS) $(GPGME_LIBS) \ - $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ - -.PHONY : all doc clean distclean run-client run-server install \ +html: $(htmldocs) + +%.5: %.xml common.ent legalnotice.xml + $(DOCBOOKTOMAN) +%.5.xhtml: %.xml common.ent legalnotice.xml + $(DOCBOOKTOHTML) + +%.8: %.xml common.ent legalnotice.xml + $(DOCBOOKTOMAN) +%.8.xhtml: %.xml common.ent legalnotice.xml + $(DOCBOOKTOHTML) + +%.8mandos: %.xml common.ent legalnotice.xml + $(DOCBOOKTOMAN) +%.8mandos.xhtml: %.xml common.ent legalnotice.xml + $(DOCBOOKTOHTML) + +mandos.8: mandos.xml common.ent mandos-options.xml overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos.8.xhtml: mandos.xml common.ent mandos-options.xml \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + +mandos-keygen.8: mandos-keygen.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-keygen.8.xhtml: mandos-keygen.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +mandos-monitor.8: mandos-monitor.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-monitor.8.xhtml: mandos-monitor.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +mandos-ctl.8: mandos-ctl.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-ctl.8.xhtml: mandos-ctl.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +mandos.conf.5: mandos.conf.xml common.ent mandos-options.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos.conf.5.xhtml: mandos.conf.xml common.ent mandos-options.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +plugin-runner.8mandos: plugin-runner.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +plugin-runner.8mandos.xhtml: plugin-runner.xml common.ent \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + +plugins.d/mandos-client.8mandos: plugins.d/mandos-client.xml \ + common.ent \ + mandos-options.xml \ + overview.xml legalnotice.xml + $(DOCBOOKTOMAN) +plugins.d/mandos-client.8mandos.xhtml: plugins.d/mandos-client.xml \ + common.ent \ + mandos-options.xml \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + +# Update all these files with version number $(version) +common.ent: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\($$/\1$(version)">/' \ + $@) + +mandos: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos-keygen: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(VERSION="\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos-ctl: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos-monitor: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos.lsm: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(Version:\).*/\1\t$(version)/' \ + $@) + $(strip $(SED) --in-place \ + --expression='s/^\(Entered-date:\).*/\1\t$(shell date --rfc-3339=date --reference=Makefile)/' \ + $@) + $(strip $(SED) --in-place \ + --expression='s/\(mandos_\)[0-9.]\+\(\.orig\.tar\.gz\)/\1$(version)\2/' \ + $@) + +plugins.d/mandos-client: plugins.d/mandos-client.c + $(LINK.c) $(GNUTLS_LIBS) $(AVAHI_LIBS) $(GPGME_LIBS) $(strip\ + ) $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +.PHONY : all doc html clean distclean run-client run-server install \ install-server install-client uninstall uninstall-server \ uninstall-client purge purge-server purge-client clean: - -rm --force $(PROGS) $(objects) $(DOCS) core + -rm --force $(CPROGS) $(objects) $(htmldocs) $(DOCS) core distclean: clean mostlyclean: clean maintainer-clean: clean - -rm --force --recursive keydir + -rm --force --recursive keydir confdir -check: +check: all ./mandos --check -run-client: all - -mkdir keydir - -./mandos-keygen --dir keydir +# Run the client with a local config and key +run-client: all keydir/seckey.txt keydir/pubkey.txt + @echo "###################################################################" + @echo "# The following error messages are harmless and can be safely #" + @echo "# ignored. The messages are caused by not running as root, but #" + @echo "# you should NOT run \"make run-client\" as root unless you also #" + @echo "# unpacked and compiled Mandos as root, which is NOT recommended. #" + @echo "# From plugin-runner: setuid: Operation not permitted #" + @echo "# From askpass-fifo: mkfifo: Permission denied #" + @echo "# From mandos-client: setuid: Operation not permitted #" + @echo "# seteuid: Operation not permitted #" + @echo "# klogctl: Operation not permitted #" + @echo "###################################################################" ./plugin-runner --plugin-dir=plugins.d \ - --options-for=password-request:--keydir=keydir - -run-server: - ./mandos --debug --configdir=. - -install: install-server install-client + --config-file=plugin-runner.conf \ + --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt \ + $(CLIENTARGS) + +# Used by run-client +keydir/seckey.txt keydir/pubkey.txt: mandos-keygen + install --directory keydir + ./mandos-keygen --dir keydir --force + +# Run the server with a local config +run-server: confdir/mandos.conf confdir/clients.conf + @echo "#################################################################" + @echo "# NOTE: Please IGNORE the error about \"Could not open file #" + @echo "# u'/var/run/mandos.pid'\" - it is harmless and is caused by #" + @echo "# the server not running as root. Do NOT run \"make run-server\" #" + @echo "# server as root if you didn't also unpack and compile it thus. #" + @echo "#################################################################" + ./mandos --debug --no-dbus --configdir=confdir $(SERVERARGS) + +# Used by run-server +confdir/mandos.conf: mandos.conf + install --directory confdir + install --mode=u=rw,go=r $^ $@ +confdir/clients.conf: clients.conf keydir/seckey.txt + install --directory confdir + install --mode=u=rw $< $@ +# Add a client password + ./mandos-keygen --dir keydir --password >> $@ + +install: install-server install-client-nokey + +install-html: html + install --directory $(htmldir) + install --mode=u=rw,go=r --target-directory=$(htmldir) \ + $(htmldocs) install-server: doc - mkdir --mode=0755 --parents $(CONFDIR) $(MANDIR)/man5 \ - $(MANDIR)/man8 - install --mode=0755 mandos $(PREFIX)/sbin/mandos - install --mode=0644 --target-directory=$(CONFDIR) mandos.conf - install --mode=0640 --target-directory=$(CONFDIR) \ + install --directory $(CONFDIR) + install --mode=u=rwx,go=rx mandos $(PREFIX)/sbin/mandos + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ + mandos-ctl + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ + mandos-monitor + install --mode=u=rw,go=r --target-directory=$(CONFDIR) \ + mandos.conf + install --mode=u=rw --target-directory=$(CONFDIR) \ clients.conf + install --mode=u=rw,go=r dbus-mandos.conf \ + $(DESTDIR)/etc/dbus-1/system.d/mandos.conf + install --mode=u=rwx,go=rx init.d-mandos \ + $(DESTDIR)/etc/init.d/mandos + install --mode=u=rw,go=r default-mandos \ + $(DESTDIR)/etc/default/mandos + if [ -z $(DESTDIR) ]; then \ + update-rc.d mandos defaults 25 15;\ + fi gzip --best --to-stdout mandos.8 \ > $(MANDIR)/man8/mandos.8.gz + gzip --best --to-stdout mandos-monitor.8 \ + > $(MANDIR)/man8/mandos-monitor.8.gz + gzip --best --to-stdout mandos-ctl.8 \ + > $(MANDIR)/man8/mandos-ctl.8.gz gzip --best --to-stdout mandos.conf.5 \ > $(MANDIR)/man5/mandos.conf.5.gz gzip --best --to-stdout mandos-clients.conf.5 \ > $(MANDIR)/man5/mandos-clients.conf.5.gz -install-client: all doc /usr/share/initramfs-tools/hooks/. - mkdir --mode=0755 --parents $(PREFIX)/lib/mandos $(CONFDIR) \ - $(MANDIR)/man8 - -mkdir --mode=0700 $(PREFIX)/lib/mandos/plugins.d - chmod u=rwx,g=,o= $(PREFIX)/lib/mandos/plugins.d - install --mode=0755 --target-directory=$(PREFIX)/lib/mandos \ - plugin-runner - install --mode=0755 --target-directory=$(PREFIX)/sbin \ +install-client-nokey: all doc + install --directory $(PREFIX)/lib/mandos $(CONFDIR) + install --directory --mode=u=rwx $(KEYDIR) \ + $(PREFIX)/lib/mandos/plugins.d + if [ "$(CONFDIR)" != "$(PREFIX)/lib/mandos" ]; then \ + install --mode=u=rwx \ + --directory "$(CONFDIR)/plugins.d"; \ + fi + install --mode=u=rwx,go=rx \ + --target-directory=$(PREFIX)/lib/mandos plugin-runner + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ mandos-keygen - install --mode=0755 \ + install --mode=u=rwx,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/password-prompt - install --mode=4755 \ - --target-directory=$(PREFIX)/lib/mandos/plugins.d \ - plugins.d/password-request + install --mode=u=rwxs,go=rx \ + --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + plugins.d/mandos-client + install --mode=u=rwxs,go=rx \ + --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + plugins.d/usplash + install --mode=u=rwxs,go=rx \ + --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + plugins.d/splashy + install --mode=u=rwxs,go=rx \ + --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + plugins.d/askpass-fifo + install --mode=u=rwxs,go=rx \ + --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + plugins.d/plymouth install initramfs-tools-hook \ - /usr/share/initramfs-tools/hooks/mandos - install initramfs-tools-hook-conf \ - /usr/share/initramfs-tools/conf-hooks.d/mandos + $(INITRAMFSTOOLS)/hooks/mandos + install --mode=u=rw,go=r initramfs-tools-hook-conf \ + $(INITRAMFSTOOLS)/conf-hooks.d/mandos install initramfs-tools-script \ - /usr/share/initramfs-tools/scripts/local-top/mandos + $(INITRAMFSTOOLS)/scripts/init-premount/mandos + install --mode=u=rw,go=r plugin-runner.conf $(CONFDIR) gzip --best --to-stdout mandos-keygen.8 \ > $(MANDIR)/man8/mandos-keygen.8.gz gzip --best --to-stdout plugin-runner.8mandos \ > $(MANDIR)/man8/plugin-runner.8mandos.gz + gzip --best --to-stdout plugins.d/mandos-client.8mandos \ + > $(MANDIR)/man8/mandos-client.8mandos.gz gzip --best --to-stdout plugins.d/password-prompt.8mandos \ > $(MANDIR)/man8/password-prompt.8mandos.gz - gzip --best --to-stdout plugins.d/password-request.8mandos \ - > $(MANDIR)/man8/password-request.8mandos.gz - -$(PREFIX)/sbin/mandos-keygen + gzip --best --to-stdout plugins.d/usplash.8mandos \ + > $(MANDIR)/man8/usplash.8mandos.gz + gzip --best --to-stdout plugins.d/splashy.8mandos \ + > $(MANDIR)/man8/splashy.8mandos.gz + gzip --best --to-stdout plugins.d/askpass-fifo.8mandos \ + > $(MANDIR)/man8/askpass-fifo.8mandos.gz + gzip --best --to-stdout plugins.d/plymouth.8mandos \ + > $(MANDIR)/man8/plymouth.8mandos.gz + +install-client: install-client-nokey +# Post-installation stuff + -$(PREFIX)/sbin/mandos-keygen --dir "$(KEYDIR)" update-initramfs -k all -u + echo "Now run mandos-keygen --password --dir $(KEYDIR)" uninstall: uninstall-server uninstall-client -uninstall-server: $(PREFIX)/sbin/mandos +uninstall-server: -rm --force $(PREFIX)/sbin/mandos \ + $(PREFIX)/sbin/mandos-ctl \ + $(PREFIX)/sbin/mandos-monitor \ $(MANDIR)/man8/mandos.8.gz \ + $(MANDIR)/man8/mandos-monitor.8.gz \ + $(MANDIR)/man8/mandos-ctl.8.gz \ $(MANDIR)/man5/mandos.conf.5.gz \ $(MANDIR)/man5/mandos-clients.conf.5.gz + update-rc.d -f mandos remove -rmdir $(CONFDIR) uninstall-client: # Refuse to uninstall client if /etc/crypttab is explicitly configured # to use it. ! grep --regexp='^ *[^ #].*keyscript=[^,=]*/mandos/' \ - /etc/crypttab + $(DESTDIR)/etc/crypttab -rm --force $(PREFIX)/sbin/mandos-keygen \ $(PREFIX)/lib/mandos/plugin-runner \ $(PREFIX)/lib/mandos/plugins.d/password-prompt \ - $(PREFIX)/lib/mandos/plugins.d/password-request \ - /usr/share/initramfs-tools/hooks/mandos \ - /usr/share/initramfs-tools/conf-hooks.d/mandos \ + $(PREFIX)/lib/mandos/plugins.d/mandos-client \ + $(PREFIX)/lib/mandos/plugins.d/usplash \ + $(PREFIX)/lib/mandos/plugins.d/splashy \ + $(PREFIX)/lib/mandos/plugins.d/askpass-fifo \ + $(PREFIX)/lib/mandos/plugins.d/plymouth \ + $(INITRAMFSTOOLS)/hooks/mandos \ + $(INITRAMFSTOOLS)/conf-hooks.d/mandos \ + $(INITRAMFSTOOLS)/scripts/init-premount/mandos \ + $(MANDIR)/man8/mandos-keygen.8.gz \ $(MANDIR)/man8/plugin-runner.8mandos.gz \ - $(MANDIR)/man8/mandos-keygen.8.gz \ + $(MANDIR)/man8/mandos-client.8mandos.gz $(MANDIR)/man8/password-prompt.8mandos.gz \ - $(MANDIR)/man8/password-request.8mandos.gz + $(MANDIR)/man8/usplash.8mandos.gz \ + $(MANDIR)/man8/splashy.8mandos.gz \ + $(MANDIR)/man8/askpass-fifo.8mandos.gz \ + $(MANDIR)/man8/plymouth.8mandos.gz \ -rmdir $(PREFIX)/lib/mandos/plugins.d $(CONFDIR)/plugins.d \ - $(PREFIX)/lib/mandos $(CONFDIR) + $(PREFIX)/lib/mandos $(CONFDIR) $(KEYDIR) update-initramfs -k all -u purge: purge-server purge-client purge-server: uninstall-server - -rm --force $(CONFDIR)/mandos.conf $(CONFDIR)/clients.conf + -rm --force $(CONFDIR)/mandos.conf $(CONFDIR)/clients.conf \ + $(DESTDIR)/etc/dbus-1/system.d/mandos.conf + $(DESTDIR)/etc/default/mandos \ + $(DESTDIR)/etc/init.d/mandos \ + $(DESTDIR)/var/run/mandos.pid -rmdir $(CONFDIR) purge-client: uninstall-client - -rm --force $(CONFDIR)/seckey.txt $(CONFDIR)/pubkey.txt - -rmdir $(CONFDIR) $(CONFDIR)/plugins.d + -shred --remove $(KEYDIR)/seckey.txt + -rm --force $(CONFDIR)/plugin-runner.conf \ + $(KEYDIR)/pubkey.txt $(KEYDIR)/seckey.txt + -rmdir $(KEYDIR) $(CONFDIR)/plugins.d $(CONFDIR) === added file 'NEWS' --- NEWS 1970-01-01 00:00:00 +0000 +++ NEWS 2010-09-09 18:16:14 +0000 @@ -0,0 +1,127 @@ +This NEWS file records noteworthy changes, very tersely. +See the manual for detailed information. + +Version 1.0.14 (2009-10-25) +Enable building without -pie and -fPIE if BROKEN_PIE is set. + +Version 1.0.13 (2009-10-22) +* Client +** Security bug fix: If Mandos server is also installed, do not copy + its config files (with encrypted passwords) into the initrd.img-* + files. + +Version 1.0.12 (2009-09-17) +* Client +** Bug fix: Allow network interface renaming by "udev" by taking down + the network interface after using it. +** Bug fix: User-supplied plugins are now installed correctly. +** Bug fix: If usplash was used but the password was instead provided + by the Mandos server, the usplash daemon used to ignore the first + command passed to it. This has been fixed. +** Bug fix: Make the "--userid" and "--groupid" options in + "plugin-runner.conf" work. +* Server +** Bug fix: Fix the LSB header in the init.d script to make dependency + based booting work. +** A client receiving its password now also counts as if a checker was + run successfully (i.e. the timeout timer is reset). + +Version 1.0.11 (2009-05-23) +* Client +** Bug fix: Use "pkg-config" instead of old "libgnutls-config". + +Version 1.0.10 (2009-05-17) +* Client +** Security bug fix: Fix permissions on initrd.img-*.bak files when + upgrading from older versions. + +Version 1.0.9 (2009-05-17) +* Client +** Security bug fix: Fix permissions on initrd.img file when + installing new linux-image-* packages calling mkinitramfs-kpkg (all + version lower than 2.6.28-1-* does this). + +Version 1.0.8 (2009-02-25) +* Client +** Bug fix: Fix missing quote characters in initramfs-tools-hook. + +Version 1.0.7 (2009-02-24) +* Client +** Bug fix: Do not depend on GNU awk. + +Version 1.0.6 (2009-02-13) +* Server +** Fix bug where server would stop responding, with a zombie checker +** Support for disabling IPv6 (only for advanced users) +** Fix bug which made server not change group ID + +* Client +** Bug fix: Fix permission for /lib64 (on relevant architechtures). +** Add support for IPv4 addresses. +** Add support in mandos-client for not bringing up a network + interface by specifying an empty string to "--interface". +** Make password prompt on boot not be mangled by kernel log messages + about network interface. +** Get network interface from initramfs.conf and/or from kernel + command line. +** If set by "ip=" kernel command line, configure network on boot. +** Support connecting directly using "mandos=connect" kernel command. + line option, provided network is configured using "ip=". +** Fix bug which made plugin-runner and mandos-client not change group + ID. +** Fix bug where the "--options-for" option of plugin-runner would + truncate the value at the first colon character. +** Fix bug where plugin-runner would not go to fallback if all plugins + failed. +** Fix bug where mandos-client would not clean temporary directory on + a signal or on certain file systems. +** Bug fix: remove bashism in /bin/sh script "mandos-keygen". + +Version 1.0.5 (2009-01-17) +* Client +** Fix small memory leak in plugin-runner. + +Version 1.0.4 (2009-01-15) +* Server +** Only find matched user/group pairs when searching for suitable + nonprivileged user/group to switch to. + +* Client +** New kernel parameter "mandos=off" makes client not run at boot. +** Fix linking errors and compilation warnings on AMD64. +** Parse numbers in command line options better. +** The splashy and usplash plugins are more robust while traversing + /proc, and will not abort if a process suddenly disappears. + +Version 1.0.3 (2009-01-06) +* Server +** Now tries to change to user and group "_mandos" before falling back + to trying the old values "mandos", "nobody:nogroup", and "65534". +** Now does not abort on startup even if no clients are defined in + clients.conf. + +* Client +** Plugins named "*.dpkg-bak" are now ignored. +** Hopefully fixed compilation failure on some architectures where the + C compiler does not recognize the "-z" option as a linker option. + +Version 1.0.2 (2008-10-17) +* mandos-keygen now signs the encrypted key blobs. This signature is + not currently verified by mandos-client, but this may change in the + future. + +Version 1.0.1 (2008-10-07) +* Server +** Expand environment variables and ~user in clients.conf's "secfile" + The "secfile" option in /etc/mandos/clients.conf now expands + "~user/foo" and "$ENVVAR" strings. + +* Client (plugin-runner, plugins, etc.) +** Manual pages for the usplash, splashy, and askpass-fifo plugins. + All plugins now have man pages. +** More secure compilation and linking flags. + All programs are now compiled with "-fstack-protector-all -fPIE + -pie", and linked using "-z relro -pie" for additional security. + +* There is now a "NEWS" file (this one), giving a history of + noteworthy changes. === added file 'README' --- README 1970-01-01 00:00:00 +0000 +++ README 2010-09-26 18:32:58 +0000 @@ -0,0 +1,182 @@ +-*- org -*- + +* Mandos + - Have your cake and eat it too! + + You know how it is. You’ve heard of it happening. The Man comes + and takes away your servers, your friends’ servers, the servers of + everybody in the same hosting facility. The servers of their + neighbors, and their neighbors’ friends. The servers of people who + owe them money. And like *that*, they’re gone. And you doubt + you’ll ever see them again. + + That is why your servers have encrypted root file systems. However, + there’s a downside. There’s no going around it: rebooting is a + pain. Dragging out that rarely-used keyboard and screen and + unraveling cables behind your servers to plug them in to type in + that password is messy, especially if you have many servers. There + are some people who do clever things like using serial line consoles + and daisy-chain it to the next server, and keep all the servers + connected in a ring with serial cables, which will work, if your + servers are physically close enough. There are also other + out-of-band management solutions, but with *all* these, you still + have to be on hand and manually type in the password at boot time. + Otherwise the server just sits there, waiting for a password. + + Wouldn’t it be great if you could have the security of encrypted + root file systems and still have servers that could boot up + automatically if there was a short power outage while you were + asleep? That you could reboot at will, without having someone run + over to the server to type in the password? + + Well, with Mandos, you (almost) can! The gain in convenience will + only be offset by a small loss in security. The setup is as + follows: + + The server will still have its encrypted root file system. The + password to this file system will be stored on another computer + (henceforth known as the Mandos server) on the same local network. + The password will *not* be stored in plaintext, but encrypted with + OpenPGP. To decrypt this password, a key is needed. This key (the + Mandos client key) will not be stored there, but back on the + original server (henceforth known as the Mandos client) in the + initial RAM disk image. Oh, and all network Mandos client/server + communications will be encrypted, using TLS (SSL). + + So, at boot time, the Mandos client will ask for its encrypted data + over the network, decrypt it to get the password, use it to decrypt + the root file, and continue booting. + + Now, of course the initial RAM disk image is not on the encrypted + root file system, so anyone who had physical access could take the + Mandos client computer offline and read the disk with their own + tools to get the authentication keys used by a client. *But*, by + then the Mandos server should notice that the original server has + been offline for too long, and will no longer give out the encrypted + key. The timing here is the only real weak point, and the method, + frequency and timeout of the server’s checking can be adjusted to + any desired level of paranoia + + (The encrypted keys on the Mandos server is on its normal file + system, so those are safe, provided the root file system of *that* + server is encrypted.) + +* FAQ - couldn’t the security be defeated by... + +** Grabbing the Mandos client key from the initrd *really quickly*? + This, as mentioned above, is the only real weak point. But if you + set the timing values tight enough, this will be really difficult + to do. An attacker would have to physically disassemble the client + computer, extract the key from the initial RAM disk image, and then + connect to a *still online* Mandos server to get the encrypted key, + and do all this *before* the Mandos server timeout kicks in and the + Mandos server refuses to give out the key to anyone. + + Now, as the typical procedure seems to be to barge in and turn off + and grab *all* computers, to maybe look at them months later, this + is not likely. If someone does that, the whole system *will* lock + itself up completely, since Mandos servers are no longer running. + + For sophisticated attackers who *could* do the clever thing, *and* + had physical access to the server for enough time, it would be + simpler to get a key for an encrypted file system by using hardware + memory scanners and reading it right off the memory bus. + +** Replay attacks? + Nope, the network stuff is all done over TLS, which provides + protection against that. + +** Man-in-the-middle? + No. The server only gives out the passwords to clients which have + *in the TLS handshake* proven that they do indeed hold the OpenPGP + private key corresponding to that client. + +** Physically grabbing the Mandos server computer? + You could protect *that* computer the old-fashioned way, with a + must-type-in-the-password-at-boot method. Or you could have two + computers be the Mandos server for each other. + + Multiple Mandos servers can coexist on a network without any + trouble. They do not clash, and clients will try all available + servers. This means that if just one reboots then the other can + bring it back up, but if both reboot at the same time they will + stay down until someone types in the password on one of them. + +** Faking ping replies? + The default for the server is to use "fping", the replies to which + could be faked to eliminate the timeout. But this could easily be + changed to any shell command, with any security measures you like. + It could, for instance, be changed to an SSH command with strict + keychecking, which could not be faked. Or IPsec could be used for + the ping packets, making them secure. + +* Security Summary + So, in summary: The only weakness in the Mandos system is from + people who have: + 1. The power to come in and physically take your servers, *and* + 2. The cunning and patience to do it carefully, one at a time, and + *quickly*, faking Mandos client/server responses for each one + before the timeout. + + While there are some who may be threatened by people who have *both* + these attributes, they do not, probably, constitute the majority. + + If you *do* face such opponents, you must figure that they could + just as well open your servers and read the file system keys right + off the memory by running wires to the memory bus. + + What Mandos is designed to protect against is *not* such determined, + focused, and competent attacks, but against the early morning knock + on your door and the sudden absence of all the servers in your + server room. Which it does nicely. + +* The Plugin System + In the early designs, the mandos-client(8mandos) program (which + retrieves a password from the Mandos server) also prompted for a + password on the terminal, in case a Mandos server could not be + found. Other ways of retrieving a password could easily be + envisoned, but this multiplicity of purpose was seen to be too + complex to be a viable way to continue. Instead, the original + program was separated into mandos-client(8mandos) and + password-prompt(8mandos), and a plugin-runner(8mandos) exist to run + them both in parallel, allowing the first successful plugin to + provide the password. This opened up for any number of additional + plugins to run, all competing to be the first to find a password and + provide it to the plugin runner. + + Four additional plugins are provided: + * plymouth(8mandos) + This prompts for a password when using plymouth(8). + * usplash(8mandos) + This prompts for a password when using usplash(8). + * splashy(8mandos) + This prompts for a password when using splashy(8). + * askpass-fifo(8mandos) + To provide compatibility with the "askpass" program from + cryptsetup, this plugin listens to the same FIFO as askpass would + do. + + More plugins can easily be written and added by the system + administrator; see the section called "WRITING PLUGINS" in + plugin-runner(8mandos) to learn the plugin requirements. + +* Copyright + + Copyright © 2008-2010 Teddy Hogeborn + Copyright © 2008-2010 Björn Påhlsson + +** License: + + 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 + . === modified file 'TODO' --- TODO 2008-08-18 05:57:11 +0000 +++ TODO 2010-09-26 18:32:58 +0000 @@ -1,141 +1,134 @@ -*- org -*- -* [#A] README file +* Use _attribute_((nonnull)) wherever possible. + +* mandos-client +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name +** TODO [#B] Retry a server which has a non-definite reply: +*** A closed connection during the TLS handshake +*** A TCP timeout +** TODO [#B] Use capabilities instead of seteuid(). +** TODO [#A] Retry --connect forever + +* splashy +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name + +* usplash +** TODO [#A] Make it work again +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name +** TODO Use [[info:libc:Argz%20Functions][argz_extract]] + +* askpass-fifo +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name +** TODO [#B] Drop privileges after opening FIFO. + +* password-prompt +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name +** TODO [#B] lock stdin (with flock()?) + +* TODO [#B] passdev * plugin-runner -** [#B] Add more comments to code -** [#B] Add more if(debug) calls -** [#B] Seperate more code to function for more readability -** [#A] Man page: man8/plugin-runner.8mandos -*** EXIT STATUS -*** EXAMPLES - Examples of normal usage, debug usage, debugging single or all - plugins, etc. -*** FILES -*** SECURITY - Note the danger of using this program, since you might lock - yourself out of your system without any means of entering the root - file system password. This is, however, very unlikely considering - the fallback to getpass(3). -*** BUGS -*** SEE ALSO - Explaining text on what you can read - -* password-request -** [#A] Man page: man8/password-request.8mandos -*** SYNOPSIS - Document short options -*** DESCRIPTION - State that this command is not meant to be invoked directly, but - is run as a plugin from mandos-client(8) and only run in the - initrd environment, not the real system. -*** PURPOSE - As in mandos.xml -*** OVERVIEW - As in mandos.xml -*** EXIT STATUS -*** ENVIRONMENT - Note that it does *not* currently use cryptsource or crypttarget. -*** FILES - Describe the key files and the key ring files. Also note that - they should normally have been automatically created. -*** BUGS -*** EXAMPLE - Examples of normal usage, debug usage, debugging by connecting - directly, etc. -*** SECURITY -*** SEE ALSO - Update from mandos.xml -** [#B] Temporarily lower kernel log level - for less printouts during sucessfull boot. -** IPv4 support -** use strsep instead of strtok? -** Do not depend on GnuPG key rings on disk - This would mean creating new GnuPG key rings with GPGME by - importing the key files from scratch on every program start. -** Keydir move: /etc/mandos -> /etc/keys/mandos - Must create in preinst if not pre-depending on cryptsetup - -* password-prompt -** [#A] Man page: man8/password-prompt.8mandos -*** SYNOPSIS - Document short options -*** DESCRIPTION - Note that this is more or less a simple getpass(3) wrapper, even - though actual use of getpass(3) is not guaranteed. -*** EXIT STATUS -*** ENVIRONMENT - Document use of "cryptsource" and "crypttarget". -*** FILES -*** BUGS -*** EXAMPLE - Examples of normal usage, debug usage, with a prefix, etc. -*** SECURITY - Not much to do here but it is noteworthy to state the danger of - not having a fallback option. -*** SEE ALSO - Refer to mandos-client(8mandos) and password-request(8mandos) - and also, perhaps, to cryptsetup(8)? -** Use getpass(3)? - Man page says "obsolete", but [[info:libc:getpass][GNU LibC Manual: Reading Passwords]] - does not. See also [[http://sources.redhat.com/ml/libc-alpha/2003-05/msg00251.html][Marcus Brinkmann: Re: getpass obsolete?]] and - [[http://article.gmane.org/gmane.comp.lib.glibc.alpha/4906][Petter Reinholdtsen: Re: getpass obsolete?]], and especially also - [[http://www.steve.org.uk/Reference/Unix/faq_4.html#SEC48][Unix Programming FAQ 3.1 How can I make my program not echo input?]] +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#C] use same file name rules as run-parts(8) +** kernel command line option for debug info +** TODO [#B] Use openat() * mandos (server) -** [#A] Config file man page: man5/mandos.conf (mandos.conf) -** [#A] Config file man page: man5/mandos-clients.conf (clients.conf) -** [#A] /etc/init.d/mandos-server :teddy: -** [#B] Log level :bugs: -** /etc/mandos/clients.d/*.conf - Watch this directory and add/remove/update clients? -** config for TXT record -** [#B] Run-time communication with server :bugs: - Probably using D-Bus - See also [[*Mandos-tools]] -** Implement --foreground :bugs: - [[info:standards:Option%20Table][Table of Long Options]] -** Implement --socket - [[info:standards:Option%20Table][Table of Long Options]] -** Date+time on console log messages :bugs: +** TODO [#B] Log level :BUGS: +** TODO Persistent state :BUGS: + /var/lib/mandos/* +*** TODO /etc/mandos/clients.d/*.conf + Watch this directory and add/remove/update clients? +** TODO [#C] config for TXT record +** TODO Log level option + syslogger.setLevel(logging.WARNING) + + SetLogLevel D-Bus call +** TODO Implement --foreground :BUGS: + [[info:standards:Option%20Table][Table of Long Options]] +** TODO Implement --socket + [[info:standards:Option%20Table][Table of Long Options]] +** TODO Date+time on console log messages :BUGS: Is this the default? - -* Mandos-tools/utilities - All of this probably using D-Bus -** List clients -** Disable client -** Enable client - -* Installer -** Client-side -*** Update initrd.img after installation - This seems to use some kind of "trigger" system -*** Keydir move: /etc/mandos -> /etc/keys/mandos - Must create in preinst if not pre-depending on cryptsetup -*** mandos-keygen -**** [#A] Output cut-and-paste ready snippet for clients.conf. -** Server-side -*** [#A] Create mandos user and group for server -*** [#A] Create /var/run/mandos directory with perm and ownership - -* [#A] Package +** TODO [#C] DBusServiceObjectUsingSuper +** TODO [#B] Global enable/disable flag +** TODO [#B] By-client countdown on secrets given +** TODO [#B] Fix problem with fsck taking a really long time + Whenever a client successfully gets a secret it could get a + one-time timeout boost to allow for an fsck-incurred delay +** TODO [#A] Delay before client receives key + This would give an operator opportunity to cancel the request if + desired. +** TODO [#A] Client manual approval mode + A client needs manual approval on the server before it gets the + secret +** TODO [#B] Support RFC 3339 time duration syntax +** More D-Bus methods +*** NeedsApproval(50, True) -> timeout, default approve + Default approval is configurable, but True by default + + Approve(True) -> approve sending saved + + Approve(False) -> Close client connection immediately +*** NeedsPassword(50) - Timeout, default disapprove + + SetPass(u"gazonk", True) -> Approval, persistent + + Approve(False) -> Close client connection immediately +** TODO [#C] python-parsedatetime +** TODO [#C] systemd/launchd + http://0pointer.de/blog/projects/systemd.html +** TODO Separate logging logic to own object +** TODO make clients to a dict! +** TODO [#A] Limit approval_delay to max gnutls/tls timeout value +** TODO [#B] break the wait on approval_delay if connection dies +** TODO Generate Client.runtime_expansions from client options + extra +** TODO Allow %%(checker)s as a runtime expansion + +* mandos.xml +** Add mandos contact info in manual pages + +* 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 +** TODO help should be toggable +** Urwid client data displayer + Better view of client data in the listing +*** Properties popup +** Nicer crashes. Stack traces Messes up shell. +*** Print a nice "We are sorry" message, save stack trace to log. +** Show timeout countdown for approval + +* mandos-keygen +** TODO Loop until passwords match when run interactively +** TODO "--secfile" option + Using the "secfile" option instead of "secret" +** TODO [#B] "--test" option + For testing decryption before rebooting. + +* Makefile +** TODO Add "--Xlinker --as-needed" + http://udrepper.livejournal.com/19395.html +** TODO [#C] Implement DEB_BUILD_OPTIONS + http://www.debian.org/doc/debian-policy/ch-source.html#s-debianrules-options + +* Package ** /usr/share/initramfs-tools/hooks/mandos -*** Do not install in initrd.img if configured not to. - Use "/etc/initramfs-tools/conf.d/mandos"? Definitely a debconf - question. -** /etc/bash_completion.d/mandos +*** TODO [#C] use same file name rules as run-parts(8) +*** TODO [#C] Do not install in initrd.img if configured not to. + Use "/etc/initramfs-tools/hooksconf.d/mandos"? +** TODO [#C] /etc/bash_completion.d/mandos From XML sources directly? -** unperish -** bzr-builddeb - -* INSTALL file - -* Web site - -* Mailing list - -* Announce project on news - [[news:comp.os.linux.announce]] + +* Side Stuff +** TODO Locate which packet move the other bin/sh when busy box is deactivated +** TODO contact owner of packet, and ask them to have that shell static in position regardless of busybox #+STARTUP: showall === modified file 'clients.conf' --- clients.conf 2008-08-10 20:35:01 +0000 +++ clients.conf 2010-09-12 03:00:40 +0000 @@ -2,20 +2,29 @@ # 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 -# "timeout" above 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". ;checker = fping -q -- %%(host)s +# Whether to approve a client by default after the approval delay. +;approved_by_default = True + +# How long to wait for approval. +;approval_delay = 0s + +# How long one approval will last. +;approval_duration = 1s + ;#### ;# Example client @@ -43,7 +52,6 @@ ; 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm ; 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O ; QlnHIvPzEArRQLo= -; =iHhv ; ;# Host name; used only by the checker, not used by the server itself. ;host = foo.example.org @@ -56,11 +64,16 @@ ;fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 ; ;# If "secret" is not specified, a file can be read for the data. -;;secfile = /etc/mandos/bar-secret.txt.asc +;secfile = /etc/mandos/bar-secret.bin ; ;# An IP address for host is also fine, if the checker accepts it. ;host = 192.0.2.3 ; ;# Parameters from the [DEFAULT] section can be overridden per client. ;interval = 5m +; +;# This client requires manual approval before it receives its secret. +;approved_by_default = False +;# Require approval within 30 seconds. +;approval_delay = 30s ;#### === added file 'common.ent' --- common.ent 1970-01-01 00:00:00 +0000 +++ common.ent 2010-09-09 18:16:14 +0000 @@ -0,0 +1,3 @@ + + + === added file 'dbus-mandos.conf' --- dbus-mandos.conf 1970-01-01 00:00:00 +0000 +++ dbus-mandos.conf 2009-11-09 07:35:16 +0000 @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + === added directory 'debian' === added file 'debian/changelog' --- debian/changelog 1970-01-01 00:00:00 +0000 +++ debian/changelog 2010-09-09 18:16:14 +0000 @@ -0,0 +1,155 @@ +mandos (1.0.14-1) unstable; urgency=low (HIGH on mips and mipsel) + + * New upstream release. + * debian/rules: Build with BROKEN_PIE set on mips and mipsel + architectures - fixes FTBFS there. + + -- Teddy Hogeborn Sun, 25 Oct 2009 20:10:09 +0100 + +mandos (1.0.13-1) unstable; urgency=high + + * New upstream release. + * Do not copy unnecessary files to initrd (Closes: #551907) + + -- Teddy Hogeborn Thu, 22 Oct 2009 00:53:21 +0200 + +mandos (1.0.12-1) unstable; urgency=low + + * New upstream release. + * init.d-mandos: Correct dependencies (Closes: #546928) + * debian/control (Standards-Version): Changed to "3.8.3". + * debian/mandos-client.README.Debian: Improved wording and formatting. + Updated location of nfsroot.txt. + * debian/mandos.README.Debian: Improved wording and formatting. + * debian/mandos-client.postinst (configure): Don't look for user and + group with the old name if upgrading from a new enough version. + * debian/mandos.postinst (configure): - '' - + * debian/mandos-client.README.Debian: Added text about non-usability of + pseudo-network interfaces. + + -- Teddy Hogeborn Thu, 17 Sep 2009 15:03:59 +0200 + +mandos (1.0.11-1) unstable; urgency=low + + * debian/control (Standards-Version): Changed to "3.8.1". + * Makefile (GNUTLS_CFLAGS, GNUTLS_CFLAGS): Use "pkg-config" instead of + the old "libgnutls-config" script. (Closes: #529836) + + -- Teddy Hogeborn Sat, 23 May 2009 07:12:20 +0200 + +mandos (1.0.10-1) unstable; urgency=low + + * New upstream release. + * debian/mandos-client.postinst (update_initramfs): Fix permissions of + old initrd.img-*.bak files. + + -- Teddy Hogeborn Sun, 17 May 2009 04:56:35 +0200 + +mandos (1.0.9-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 17 May 2009 02:59:45 +0200 + +mandos (1.0.8-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Wed, 25 Feb 2009 02:26:57 +0100 + +mandos (1.0.7-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Tue, 24 Feb 2009 12:58:06 +0100 + +mandos (1.0.6-1) unstable; urgency=low + + * New upstream release. + * debian/mandos-client.postinst: Converted to Bourne shell. Also + minor message change. + * debian/mandos-client.postrm: Minor message change. + * debian/mandos.postinst: Converted to Bourne shell. Also minor + message change. + * debian/mandos.prerm: Minor message change. + * debian/rules (install-indep): Removed "--no-start" from + dh_installinit. + * debian/mandos-client.lintian-overrides: Remove obsolete override for + unbreakable line in plugin-runner manual page. + * debian/control (mandos/Depends): Added "python-gobject". + * debian/mandos-client.dirs: Change + "usr/share/initramfs-tools/scripts/local-top" to + "usr/share/initramfs-tools/scripts/init-premount". + * debian/mandos-client.README.Debian: Add reference to initramfs.conf + and nfsroot.txt. New section about the new non-local connection + feature. + + -- Teddy Hogeborn Fri, 13 Feb 2009 09:27:25 +0100 + +mandos (1.0.5-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sat, 17 Jan 2009 02:26:00 +0100 + +mandos (1.0.4-1) unstable; urgency=low + + * New upstream release. + * debian/watch: New file. + * debian/mandos-client.README.Debian: Document new "mandos=off" kernel + parameter. + + -- Teddy Hogeborn Thu, 15 Jan 2009 05:49:22 +0100 + +mandos (1.0.3-2) unstable; urgency=low + + * Removed some now-unused debconf files. + * Changed postinst scripts to not source debconf/confmodule. + * Removed po-debconf from build-depends. + + -- Teddy Hogeborn Tue, 06 Jan 2009 21:28:20 +0100 + +mandos (1.0.3-1) unstable; urgency=low + + * New upstream release. + * Add -Xlinker to linker flags to fix FTBFS for some architectures. + Thanks to Thiemo Seufer for the report and + fix. (Closes: #509398) + * Remove debconf use altogether, thereby stopping debconf abuse. Thanks + to Christian Perrier . (Closes: #509653) + * Add NEWS file to /usr/share/doc directories. + * Use and create "_mandos" user+group. Rename old user+group created by + older versions of this package. + * Fix manual pages by adding build-depend on "docbook-xml". + + -- Teddy Hogeborn Tue, 06 Jan 2009 01:21:20 +0100 + +mandos (1.0.2-1) unstable; urgency=low + + * New upstream release. + * debian/copyright: Rewritten to conform to + . + + -- Teddy Hogeborn Fri, 17 Oct 2008 20:42:12 +0200 + +mandos (1.0.1-1) unstable; urgency=low + + * New upstream release. + * Separate /usr/share/doc/mandos-client/README.Debian into sections with + headlines. Add instructions on how to test the server and verify the + password. + + -- Teddy Hogeborn Tue, 07 Oct 2008 23:07:23 +0200 + +mandos (1.0-2) unstable; urgency=low + + * Added comments in debian/*.lintian-overrides files. Added Debian + revison number to version number. + + -- Teddy Hogeborn Wed, 01 Oct 2008 17:23:35 +0200 + +mandos (1.0-1) unstable; urgency=low + + * Initial Release. (Closes: #500727). + + -- Teddy Hogeborn Tue, 30 Sep 2008 21:58:43 +0200 === added file 'debian/compat' --- debian/compat 1970-01-01 00:00:00 +0000 +++ debian/compat 2008-09-17 00:34:09 +0000 @@ -0,0 +1,1 @@ +7 === added file 'debian/control' --- debian/control 1970-01-01 00:00:00 +0000 +++ debian/control 2010-09-26 18:32:58 +0000 @@ -0,0 +1,54 @@ +Source: mandos +Section: admin +Priority: extra +Maintainer: Mandos Maintainers +Uploaders: Teddy Hogeborn , + Björn Påhlsson +Build-Depends: debhelper (>= 7), docbook-xml, docbook-xsl, + libavahi-core-dev, libgpgme11-dev, libgnutls-dev, xsltproc, + pkg-config +Standards-Version: 3.8.3 +Vcs-Bzr: http://ftp.fukt.bsnet.se/pub/mandos/trunk +Vcs-Browser: http://bzr.fukt.bsnet.se/loggerhead/mandos/trunk/files +Homepage: http://www.fukt.bsnet.se/mandos + +Package: mandos +Architecture: all +Depends: ${misc:Depends}, python (>=2.5), python-gnutls, python-dbus, + python-avahi, python-gobject, avahi-daemon, adduser, + python-urwid, python (>=2.6) | python-multiprocessing +Recommends: fping +Description: a server giving encrypted passwords to Mandos clients + This is the server part of the Mandos system, which allows + computers to have encrypted root file systems and at the + same time be capable of remote and/or unattended reboots. + . + The computers run a small client program in the initial RAM + disk environment which will communicate with a server over a + network. All network communication is encrypted using TLS. + The clients are identified by the server using an OpenPGP + key; each client has one unique to it. The server sends the + clients an encrypted password. The encrypted password is + decrypted by the clients using the same OpenPGP key, and the + password is then used to unlock the root file system, + whereupon the computers can continue booting normally. + +Package: mandos-client +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, cryptsetup, + gnupg (<< 2) +Enhances: cryptsetup +Description: do unattended reboots with an encrypted root file system + This is the client part of the Mandos system, which allows + computers to have encrypted root file systems and at the + same time be capable of remote and/or unattended reboots. + . + The computers run a small client program in the initial RAM + disk environment which will communicate with a server over a + network. All network communication is encrypted using TLS. + The clients are identified by the server using an OpenPGP + key; each client has one unique to it. The server sends the + clients an encrypted password. The encrypted password is + decrypted by the clients using the same OpenPGP key, and the + password is then used to unlock the root file system, + whereupon the computers can continue booting normally. === added file 'debian/copyright' --- debian/copyright 1970-01-01 00:00:00 +0000 +++ debian/copyright 2010-09-26 18:32:58 +0000 @@ -0,0 +1,26 @@ +Format-Specification: + http://wiki.debian.org/Proposals/CopyrightFormat?action=recall&rev=233 +Upstream-Name: Mandos +Upstream-Maintainer: Mandos Maintainers +Upstream-Source: + +Files: * +Copyright: Copyright © 2008-2010 Teddy Hogeborn +Copyright: Copyright © 2008-2010 Björn Påhlsson +License: GPL-3+ + 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 + . + . + On Debian systems, the complete text of the GNU General Public + License can be found in "/usr/share/common-licenses/GPL". === added file 'debian/mandos-client.README.Debian' --- debian/mandos-client.README.Debian 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.README.Debian 2010-09-26 18:32:58 +0000 @@ -0,0 +1,89 @@ +* Choose the Client Network Interface + + Please make sure that the correct network interface is specified in + the DEVICE setting in the "/etc/initramfs-tools/initramfs.conf" + file. If the setting is empty, the interface will be autodetected + at boot time, which may not be correct. *If* the DEVICE setting is + changed, it will be necessary to update the initrd image by running + the command + + update-initramfs -k all -u + + The device can 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", + available in the "linux-doc-*" package. + + Note that since this network interface is used in the initial RAM + disk environment, the network interface *must* exist at that stage. + Thus, the interface can *not* be a pseudo-interface such as "br0" or + "tun0"; instead, a real interface (such as "eth0") must be used. + +* Adding a Client Password to the Server + + The server must be given a password to give back to the client on + boot time. This password must be a one which can be used to unlock + the root file system device. On the *client*, run this command: + + mandos-keygen --password + + It will prompt for a password and output a config file section. + This output should be copied to the Mandos server and added to the + file "/etc/mandos/clients.conf" there. + +* Testing that it Works (Without Rebooting) + + After the server has been started with this client's key added, it + is possible to verify that the correct password will be received by + this client by running the command, on the client: + + /usr/lib/mandos/plugins.d/mandos-client \ + --pubkey=/etc/keys/mandos/pubkey.txt \ + --seckey=/etc/keys/mandos/seckey.txt; echo + + This command should retrieve the password from the server, decrypt + it, and output it to standard output. There it can be verified to + be the correct password, before rebooting. + +* User-Supplied Plugins + + Any plugins found in "/etc/mandos/plugins.d" will override and add + to the normal Mandos plugins. When adding or changing plugins, do + not forget to update the initital RAM disk image: + + update-initramfs -k all -u + +* Do *NOT* Edit "/etc/crypttab" + + It is NOT necessary to edit "/etc/crypttab" to specify + "/usr/lib/mandos/plugin-runner" as a keyscript for the root file + system; if no keyscript is given for the root file system, the + Mandos client will be the new default way for getting a password for + the root file system when booting. + +* Emergency Escape + + If it ever should be necessary, the Mandos client can be temporarily + prevented from running at startup by passing the parameter + "mandos=off" to the kernel. + +* Non-local Connection (Not Using ZeroConf) + + If the "ip=" kernel command line option is used to specify a + complete IP address and device name, as noted above, it then becomes + possible to specify a specific IP address and port to connect to, + instead of using ZeroConf. The syntax for doing this is + "mandos=connect::". + + Warning: this will cause the client to make exactly one attempt at + connecting, and then fail if it does not succeed. + + For very advanced users, it it possible to specify simply + "mandos=connect" on the kernel command line to make the system only + set up the network (using the data in the "ip=" option) and not pass + any extra "--connect" options to mandos-client at boot. For this to + work, "--options-for=mandos-client:--connect=
:" needs + to be manually added to the file "/etc/mandos/plugin-runner.conf". + + -- Teddy Hogeborn , Sun, 26 Sep 2010 20:08:05 +0200 === added file 'debian/mandos-client.dirs' --- debian/mandos-client.dirs 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.dirs 2009-02-07 04:50:39 +0000 @@ -0,0 +1,5 @@ +usr/share/man/man8 +usr/sbin +usr/share/initramfs-tools/hooks +usr/share/initramfs-tools/conf-hooks.d +usr/share/initramfs-tools/scripts/init-premount === added file 'debian/mandos-client.docs' --- debian/mandos-client.docs 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.docs 2008-10-18 11:17:22 +0000 @@ -0,0 +1,3 @@ +NEWS +README +TODO === added file 'debian/mandos-client.links' --- debian/mandos-client.links 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.links 2008-09-19 13:50:22 +0000 @@ -0,0 +1,1 @@ +usr/share/man/man8/plugin-runner.8mandos.gz usr/share/man/man5/plugin-runner.conf.5mandos.gz === added file 'debian/mandos-client.lintian-overrides' --- debian/mandos-client.lintian-overrides 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.lintian-overrides 2009-01-18 06:41:57 +0000 @@ -0,0 +1,27 @@ +# This directory contains secret client key files. +# +mandos-client binary: non-standard-dir-perm etc/keys/mandos/ 0700 != 0755 + +# The directory /usr/lib/mandos/plugins.d contains setuid binaries +# which are not meant to be run outside an initial RAM disk +# environment (except for test purposes). It would be insecure to +# allow anyone to run them. +# +mandos-client binary: non-standard-dir-perm usr/lib/mandos/plugins.d/ 0700 != 0755 + +# These binaries must be setuid root, since they need root powers, but +# are started by plugin-runner(8mandos), which runs all plugins as +# user/group "_mandos". These binaries are not run in a running +# system, but in an initial RAM disk environment. Here they are +# protected from non-root access by the directory permissions, above. +# +mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/mandos-client 4755 root/root +mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/askpass-fifo 4755 root/root +mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/splashy 4755 root/root +mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/usplash 4755 root/root + +# The directory /etc/mandos/plugins.d can be used by local system +# administrators to place plugins in, overriding and complementing +# /usr/lib/mandos/plugins.d, and must be likewise protected. +# +mandos-client binary: non-standard-dir-perm etc/mandos/plugins.d/ 0700 != 0755 === added file 'debian/mandos-client.postinst' --- debian/mandos-client.postinst 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.postinst 2009-05-24 23:36:15 +0000 @@ -0,0 +1,81 @@ +#!/bin/sh -e +# This script can be called in the following ways: +# +# After the package was installed: +# configure +# +# +# If prerm fails during upgrade or fails on failed upgrade: +# abort-upgrade +# +# If prerm fails during deconfiguration of a package: +# abort-deconfigure in-favour +# removing +# +# If prerm fails during replacement due to conflict: +# abort-remove in-favour + +# Update the initial RAM file system image +update_initramfs() +{ + if [ -x /usr/sbin/update-initramfs ]; then + update-initramfs -u -k all + fi + + if dpkg --compare-versions "$2" lt-nl "1.0.10-1"; then + # Make old initrd.img files unreadable too, in case they were + # created with mandos-client 1.0.8 or older. + find /boot -maxdepth 1 -type f -name "initrd.img-*.bak" \ + -print0 | xargs --null --no-run-if-empty chmod o-r + fi +} + +# Add user and group +add_mandos_user(){ + # Rename old "mandos" user and group + if dpkg --compare-versions "$2" lt "1.0.3-1"; then + case "`getent passwd mandos`" in + *:Mandos\ password\ system,,,:/nonexistent:/bin/false) + usermod --login _mandos mandos + groupmod --new-name _mandos mandos + return + ;; + esac + fi + # Create new user and group + if ! getent passwd _mandos >/dev/null; then + adduser --system --force-badname --quiet --home /nonexistent \ + --no-create-home --group --disabled-password \ + --gecos "Mandos password system" _mandos + fi +} + +# Create client key pair +create_key(){ + if [ -r /etc/keys/mandos/pubkey.txt \ + -a -r /etc/keys/mandos/seckey.txt ]; then + return 0 + fi + if [ -x /usr/sbin/mandos-keygen ]; then + mandos-keygen + fi +} + +case "$1" in + configure) + add_mandos_user "$@" + create_key "$@" + update_initramfs "$@" + ;; + abort-upgrade|abort-deconfigure|abort-remove) + ;; + + *) + echo "$0 called with unknown argument '$1'" 1>&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 === added file 'debian/mandos-client.postrm' --- debian/mandos-client.postrm 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.postrm 2009-01-18 00:16:57 +0000 @@ -0,0 +1,60 @@ +#!/bin/sh -e +# This script can be called in the following ways: +# +# After the package was removed: +# remove +# +# After the package was purged: +# purge +# +# After the package was upgraded: +# upgrade +# if that fails: +# failed-upgrade +# +# +# After all of the packages files have been replaced: +# disappear +# +# +# If preinst fails during install: +# abort-install +# +# If preinst fails during upgrade of removed package: +# abort-install +# +# If preinst fails during upgrade: +# abort-upgrade + + +# Update the initial RAM file system image +update_initramfs() +{ + if [ -x /usr/sbin/update-initramfs ]; then + update-initramfs -u -k all + fi +} + +case "$1" in + remove) + update_initramfs + ;; + + purge) + shred --remove /etc/keys/mandos/seckey.txt 2>/dev/null || : + rm --force /etc/mandos/plugin-runner.conf \ + /etc/keys/mandos/pubkey.txt \ + /etc/keys/mandos/seckey.txt 2>/dev/null + ;; + upgrade|failed-upgrade|disappear|abort-install|abort-upgrade) + ;; + + *) + echo "$0 called with unknown argument '$1'" 1>&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 === added file 'debian/mandos.README.Debian' --- debian/mandos.README.Debian 1970-01-01 00:00:00 +0000 +++ debian/mandos.README.Debian 2009-09-08 06:28:20 +0000 @@ -0,0 +1,10 @@ +The Mandos server is useless without at least one configured client in +/etc/mandos/clients.conf. To create one, install the "mandos-client" +package on a client computer, and run the command + + # mandos-keygen --password + +there to get a config file stanza. Append the output of that command +to the file "/etc/mandos/clients.conf" on the Mandos server. + + -- Teddy Hogeborn , Tue, 8 Sep 2009 06:57:45 +0200 === added file 'debian/mandos.dirs' --- debian/mandos.dirs 1970-01-01 00:00:00 +0000 +++ debian/mandos.dirs 2010-09-15 17:33:14 +0000 @@ -0,0 +1,6 @@ +usr/share/man/man5 +usr/share/man/man8 +etc/init.d +etc/default +etc/dbus-1/system.d +usr/sbin === added file 'debian/mandos.docs' --- debian/mandos.docs 1970-01-01 00:00:00 +0000 +++ debian/mandos.docs 2010-09-12 03:00:40 +0000 @@ -0,0 +1,4 @@ +NEWS +README +TODO +DBUS-API === added file 'debian/mandos.lintian-overrides' --- debian/mandos.lintian-overrides 1970-01-01 00:00:00 +0000 +++ debian/mandos.lintian-overrides 2008-10-01 15:29:01 +0000 @@ -0,0 +1,4 @@ +# This config file will normally have encrypted secret client keys in +# it, so it must be kept unreadable for non-root users. +# +mandos binary: non-standard-file-perm etc/mandos/clients.conf 0600 != 0644 === added file 'debian/mandos.postinst' --- debian/mandos.postinst 1970-01-01 00:00:00 +0000 +++ debian/mandos.postinst 2009-05-24 23:28:04 +0000 @@ -0,0 +1,49 @@ +#!/bin/sh -e +# This script can be called in the following ways: +# +# After the package was installed: +# configure +# +# +# If prerm fails during upgrade or fails on failed upgrade: +# abort-upgrade +# +# If prerm fails during deconfiguration of a package: +# abort-deconfigure in-favour +# removing +# +# If prerm fails during replacement due to conflict: +# abort-remove in-favour + +case "$1" in + configure) + # Rename old "mandos" user and group + if dpkg --compare-versions "$2" lt "1.0.3-1"; then + case "`getent passwd mandos`" in + *:Mandos\ password\ system,,,:/nonexistent:/bin/false) + usermod --login _mandos mandos + groupmod --new-name _mandos mandos + ;; + esac + fi + # Create new user and group + if ! getent passwd _mandos >/dev/null; then + adduser --system --force-badname --quiet \ + --home /nonexistent --no-create-home --group \ + --disabled-password --gecos "Mandos password system" \ + _mandos + fi + ;; + + abort-upgrade|abort-deconfigure|abort-remove) + ;; + + *) + echo "$0 called with unknown argument '$1'" 1>&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 === added file 'debian/mandos.prerm' --- debian/mandos.prerm 1970-01-01 00:00:00 +0000 +++ debian/mandos.prerm 2009-01-18 00:16:57 +0000 @@ -0,0 +1,38 @@ +#! /bin/sh +# prerm script for mandos +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * 'remove' +# * 'upgrade' +# * 'failed-upgrade' +# * 'remove' 'in-favour' +# * 'deconfigure' 'in-favour' +# 'removing' +# +# for details, see /usr/share/doc/packaging-manual/ + +case "$1" in + remove|deconfigure) + if [ -x /etc/init.d/mandos ]; then + if [ -x /usr/sbin/invoke-rc.d ]; then + invoke-rc.d mandos stop + else + /etc/init.d/mandos stop + fi + fi + ;; + upgrade|failed-upgrade) + ;; + *) + echo "prerm called with unknown argument '$1'" >&2 + exit 0 + ;; +esac + +#DEBHELPER# + +exit 0 === added directory 'debian/po' === added file 'debian/rules' --- debian/rules 1970-01-01 00:00:00 +0000 +++ debian/rules 2010-09-09 18:16:14 +0000 @@ -0,0 +1,106 @@ +#!/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_auto_build -- all doc + touch $@ + +build-indep: build-indep-stamp +build-indep-stamp: configure-stamp + 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 + 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_link + dh_strip + dh_compress + dh_fixperms --exclude etc/keys/mandos \ + --exclude etc/mandos/clients.conf \ + --exclude etc/mandos/plugins.d \ + --exclude usr/lib/mandos/plugins.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 === added file 'debian/watch' --- debian/watch 1970-01-01 00:00:00 +0000 +++ debian/watch 2010-09-15 17:17:46 +0000 @@ -0,0 +1,2 @@ +version=3 +ftp://ftp.fukt.bsnet.se/pub/mandos/mandos[-_]([^\s]+?)(?:\.orig)?\.tar\.(?:gz|bz2|7z|xz) === added file 'default-mandos' --- default-mandos 1970-01-01 00:00:00 +0000 +++ default-mandos 2008-09-17 00:34:09 +0000 @@ -0,0 +1,7 @@ +# Directory where configuration files are located. Default is +# "/etc/mandos". +# +#CONFIGDIR=/etc/mandos + +# Additional options that are passed to the Daemon. +DAEMON_ARGS="" === added file 'init.d-mandos' --- init.d-mandos 1970-01-01 00:00:00 +0000 +++ init.d-mandos 2009-09-16 23:28:39 +0000 @@ -0,0 +1,159 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: mandos +# Required-Start: $remote_fs $syslog avahi +# Required-Stop: $remote_fs $syslog avahi +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Mandos server +# Description: Gives encrypted passwords to Mandos clients +### END INIT INFO + +# Author: Teddy Hogeborn +# Author: Björn Påhlsson +# +# Please remove the "Author" lines above and replace them +# with your own name if you copy and modify this script. + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Mandos root file system password server" +NAME=mandos +DAEMON=/usr/sbin/$NAME +DAEMON_ARGS="" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +if [ -n "$CONFIGDIR" ]; then + DAEMON_ARGS="$DAEMON_ARGS --configdir $CONFIGDIR" +fi + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: === modified file 'initramfs-tools-hook' --- initramfs-tools-hook 2008-08-14 02:24:59 +0000 +++ initramfs-tools-hook 2010-09-09 18:16:14 +0000 @@ -29,15 +29,36 @@ . /usr/share/initramfs-tools/hook-functions -if [ -d /usr/lib/mandos ]; then - prefix=/usr -elif [ -d /usr/local/lib/mandos ]; then - prefix=/usr/local -else +for d in /usr /usr/local; do + if [ -d "$d"/lib/mandos ]; then + prefix="$d" + break + fi +done +if [ -z "$prefix" ]; then # Mandos not found exit 1 fi +for d in /etc/keys/mandos /etc/mandos/keys; do + if [ -d "$d" ]; then + keydir="$d" + break + fi +done +if [ -z "$keydir" ]; then + # Mandos key directory not found + exit 1 +fi + +set `{ getent passwd _mandos \ + || getent passwd nobody \ + || echo ::65534:65534:::; } \ + | cut --delimiter=: --fields=3,4 --only-delimited \ + --output-delimiter=" "` +mandos_user="$1" +mandos_group="$2" + # The Mandos network client uses the network auto_add_modules net # The Mandos network client uses IPv6 @@ -49,11 +70,13 @@ PLUGINDIR="${MANDOSDIR}/plugins.d" # Make directories -mkdir --parents "${DESTDIR}${CONFDIR}" -mkdir --parents "${DESTDIR}${PLUGINDIR}" +install --directory --mode=u=rwx,go=rx "${DESTDIR}${CONFDIR}" \ + "${DESTDIR}${MANDOSDIR}" +install --owner=${mandos_user} --group=${mandos_group} --directory \ + --mode=u=rwx "${DESTDIR}${PLUGINDIR}" # Copy the Mandos plugin runner -copy_exec "$prefix"/lib/mandos/plugin-runner "${DESTDIR}${MANDOSDIR}" +copy_exec "$prefix"/lib/mandos/plugin-runner "${MANDOSDIR}" # Copy the plugins @@ -65,8 +88,10 @@ continue fi case "$base" in - *~|.*|\#*\#|*.dpkg-old|*.dpkg-new|*.dpkg-divert) : ;; - *) copy_exec "$file" "${PLUGINDIR}";; + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") echo "W: Mandos client plugin directory is empty." >&2 ;; + *) copy_exec "$file" "${PLUGINDIR}" ;; esac done @@ -74,31 +99,47 @@ for file in /etc/mandos/plugins.d/*; do base="`basename \"$file\"`" case "$base" in - *~|.*|*.dpkg-old|*.dpkg-new|*.dpkg-divert) : ;; - *) copy_exec "$file" "${PLUGINDIR}";; + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) copy_exec "$file" "${PLUGINDIR}" ;; esac done # GPGME needs /usr/bin/gpg -if ! [ -e "${DESTDIR}/usr/bin/gpg" ] \ - && [ -n "`ls \"${DESTDIR}\"/usr/lib/libgpgme.so* 2>/dev/null`" ]; then +if [ ! -e "${DESTDIR}/usr/bin/gpg" \ + -a -n "`ls \"${DESTDIR}\"/usr/lib/libgpgme.so* \ + 2>/dev/null`" ]; then copy_exec /usr/bin/gpg fi -# Key files -for file in /etc/mandos/*; do - if [ -d "$file" ]; then - continue - fi - cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}" -done -# Create key ring files -gpg --no-random-seed-file --quiet --batch --no-tty \ - --no-default-keyring --no-options \ - --homedir "${DESTDIR}${CONFDIR}" --no-permission-warning \ - --import-options import-minimal \ - --import "${DESTDIR}${CONFDIR}/seckey.txt" -chown nobody "${DESTDIR}${CONFDIR}/secring.gpg" +# Config files +for file in /etc/mandos/plugin-runner.conf; do + if [ -d "$file" ]; then + continue + fi + cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}" +done + +if [ ${mandos_user} != 65534 ]; then + sed --in-place --expression="1i--userid=${mandos_user}" \ + "${DESTDIR}${CONFDIR}/plugin-runner.conf" +fi + +if [ ${mandos_group} != 65534 ]; then + sed --in-place --expression="1i--groupid=${mandos_group}" \ + "${DESTDIR}${CONFDIR}/plugin-runner.conf" +fi + +# Key files +for file in "$keydir"/*; do + if [ -d "$file" ]; then + continue + fi + cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}" + chown ${mandos_user}:${mandos_group} \ + "${DESTDIR}${CONFDIR}/`basename \"$file\"`" +done # /lib/mandos/plugin-runner will drop priviliges, but needs access to # its plugin directory and its config file. However, since almost all @@ -112,7 +153,7 @@ # condition. This umask is set by "initramfs-tools-hook-conf", # installed as "/usr/share/initramfs-tools/conf-hooks.d/mandos".) # -for full in "${PLUGINDIR}" "${CONFDIR}"; do +for full in "${MANDOSDIR}" "${CONFDIR}"; do while [ "$full" != "/" ]; do chmod a+rX "${DESTDIR}$full" full="`dirname \"$full\"`" @@ -122,8 +163,13 @@ # Reset some other things to sane permissions which we have # inadvertently affected with our umask setting. for dir in / /bin /etc /keyscripts /sbin /scripts /usr /usr/bin; do - chmod a+rX "${DESTDIR}$dir" + if [ -d "${DESTDIR}$dir" ]; then + chmod a+rX "${DESTDIR}$dir" + fi done -for dir in /lib /usr/lib; do - chmod --recursive a+rX "${DESTDIR}$dir" +for dir in "${DESTDIR}"/lib* "${DESTDIR}"/usr/lib*; do + if [ -d "$dir" ]; then + find "$dir" \! -perm -u+rw,g+r -prune -or -print0 \ + | xargs --null --no-run-if-empty chmod a+rX + fi done === modified file 'initramfs-tools-hook-conf' --- initramfs-tools-hook-conf 2008-08-12 19:22:34 +0000 +++ initramfs-tools-hook-conf 2009-05-17 00:50:09 +0000 @@ -1,1 +1,13 @@ +# -*- shell-script -*- + +# if mkinitramfs is started by mkinitramfs-kpkg, mkinitramfs-kpkg has +# already touched the initrd file with umask 022 before we had a +# chance to affect it. We cannot allow a readable initrd file, +# therefore we must fix this now. +if [ -e "${outfile}" ] \ + && [ `stat --format=%s "${outfile}"` -eq 0 ]; then + rm "${outfile}" + (umask 027; touch "${outfile}") +fi + UMASK=027 === modified file 'initramfs-tools-script' --- initramfs-tools-script 2008-08-14 02:24:59 +0000 +++ initramfs-tools-script 2009-09-16 23:28:39 +0000 @@ -6,25 +6,102 @@ # # This script should be installed as -# "/usr/share/initramfs-tools/scripts/local-top/mandos" which will -# eventually be "/scripts/local-top/mandos" in the initrd.img file. +# "/usr/share/initramfs-tools/scripts/init-premount/mandos" which will +# eventually be "/scripts/init-premount/mandos" in the initrd.img +# file. -# No initramfs pre-requirements; we must instead run BEFORE cryptroot. -# This is not a problem, since cryptroot forces itself to run LAST. -PREREQ="" +PREREQ="udev" prereqs() { - echo "$PREREQ" + echo "$PREREQ" } case $1 in prereqs) - prereqs - exit 0 - ;; -esac - -test -w /conf/conf.d/cryptroot + prereqs + exit 0 + ;; +esac + +. /scripts/functions + +for param in `cat /proc/cmdline`; do + case "$param" in + ip=*) IPOPTS="${param#ip=}" ;; + mandos=*) + # Split option line on commas + old_ifs="$IFS" + IFS="$IFS," + for mpar in ${param#mandos=}; do + IFS="$old_ifs" + case "$mpar" in + off) exit 0 ;; + connect) connect="" ;; + connect:*) connect="${mpar#connect:}" ;; + *) log_warning_msg "$0: Bad option ${mpar}" ;; + esac + done + unset mpar + IFS="$old_ifs" + unset old_ifs + ;; + esac +done +unset param + +chmod a=rwxt /tmp + +test -r /conf/conf.d/cryptroot +test -w /conf/conf.d + +# Get DEVICE from /conf/initramfs.conf and other files +. /conf/initramfs.conf +for conf in /conf/conf.d/*; do + [ -f ${conf} ] && . ${conf} +done +if [ -e /conf/param.conf ]; then + . /conf/param.conf +fi + +# Override DEVICE from sixth field of ip= kernel option, if passed +case "$IPOPTS" in + *:*:*:*:*:*) # At least six fields + # Remove the first five fields + device="${IPOPTS#*:*:*:*:*:}" + # Remove all fields except the first one + DEVICE="${device%%:*}" + ;; +esac + +# Add device setting (if any) to plugin-runner.conf +if [ "${DEVICE+set}" = set ]; then + # Did we get the device from an ip= option? + if [ "${device+set}" = set ]; then + # Let ip= option override local config; append: + cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf + + --options-for=mandos-client:--interface=${DEVICE} +EOF + else + # Prepend device setting so any later options would override: + sed -i -e \ + '1i--options-for=mandos-client:--interface='"${DEVICE}" \ + /conf/conf.d/mandos/plugin-runner.conf + fi +fi +unset device + +# If we are connecting directly, run "configure_networking" (from +# /scripts/functions); it needs IPOPTS and DEVICE +if [ "${connect+set}" = set ]; then + configure_networking + if [ -n "$connect" ]; then + cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf + + --options-for=mandos-client:--connect=${connect} +EOF + fi +fi # Do not replace cryptroot file unless we need to. replace_cryptroot=no === added file 'legalnotice.xml' --- legalnotice.xml 1970-01-01 00:00:00 +0000 +++ legalnotice.xml 2008-09-06 17:24:58 +0000 @@ -0,0 +1,27 @@ + + + + + This manual page 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 manual page 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 + http://www.gnu.org/licenses/. + + === modified file 'mandos' --- mandos 2008-08-18 23:55:28 +0000 +++ mandos 2010-09-26 18:32:58 +0000 @@ -6,12 +6,13 @@ # This program is partly derived from an example program for an Avahi # service publisher, downloaded from # . This includes the -# methods "add" and "remove" in the "AvahiService" class, the -# "server_state_changed" and "entry_group_state_changed" functions, -# and some lines in "main". +# methods "add", "remove", "server_state_changed", +# "entry_group_state_changed", "cleanup", and "activate" in the +# "AvahiService" class, and some lines in "main". # # Everything else is -# Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson +# Copyright © 2008-2010 Teddy Hogeborn +# Copyright © 2008-2010 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 @@ -24,17 +25,17 @@ # 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 . +# along with this program. If not, see +# . # # Contact the authors at . # -from __future__ import division +from __future__ import division, with_statement, absolute_import -import SocketServer +import SocketServer as socketserver import socket -import select -from optparse import OptionParser +import optparse import datetime import errno import gnutls.crypto @@ -43,44 +44,67 @@ import gnutls.library.functions import gnutls.library.constants import gnutls.library.types -import ConfigParser +import ConfigParser as configparser import sys import re import os import signal -from sets import Set import subprocess import atexit import stat import logging import logging.handlers +import pwd +import contextlib +import struct +import fcntl +import functools +import cPickle as pickle +import multiprocessing import dbus +import dbus.service import gobject import avahi from dbus.mainloop.glib import DBusGMainLoop import ctypes - -version = "1.0" - -logger = logging.Logger('mandos') -syslogger = logging.handlers.SysLogHandler\ - (facility = logging.handlers.SysLogHandler.LOG_DAEMON, - address = "/dev/log") -syslogger.setFormatter(logging.Formatter\ - ('Mandos: %(levelname)s: %(message)s')) +import ctypes.util +import xml.dom.minidom +import inspect + +try: + SO_BINDTODEVICE = socket.SO_BINDTODEVICE +except AttributeError: + try: + from IN import SO_BINDTODEVICE + except ImportError: + SO_BINDTODEVICE = None + + +version = "1.0.14" + +#logger = logging.getLogger(u'mandos') +logger = logging.Logger(u'mandos') +syslogger = (logging.handlers.SysLogHandler + (facility = logging.handlers.SysLogHandler.LOG_DAEMON, + address = "/dev/log")) +syslogger.setFormatter(logging.Formatter + (u'Mandos [%(process)d]: %(levelname)s:' + u' %(message)s')) logger.addHandler(syslogger) console = logging.StreamHandler() -console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:' - ' %(message)s')) +console.setFormatter(logging.Formatter(u'%(name)s [%(process)d]:' + u' %(levelname)s:' + u' %(message)s')) logger.addHandler(console) class AvahiError(Exception): - def __init__(self, value): + def __init__(self, value, *args, **kwargs): self.value = value - def __str__(self): - return repr(self.value) + super(AvahiError, self).__init__(value, *args, **kwargs) + def __unicode__(self): + return unicode(repr(self.value)) class AvahiServiceError(AvahiError): pass @@ -91,11 +115,12 @@ class AvahiService(object): """An Avahi (Zeroconf) service. + Attributes: interface: integer; avahi.IF_UNSPEC or an interface index. Used to optionally bind to the specified interface. - name: string; Example: 'Mandos' - type: string; Example: '_mandos._tcp'. + name: string; Example: u'Mandos' + type: string; Example: u'_mandos._tcp'. See port: integer; what port to announce TXT: list of strings; TXT record for the service @@ -104,211 +129,307 @@ max_renames: integer; maximum number of renames rename_count: integer; counter so we only rename after collisions a sensible number of times + group: D-Bus Entry Group + server: D-Bus Server + bus: dbus.SystemBus() """ def __init__(self, interface = avahi.IF_UNSPEC, name = None, - type = None, port = None, TXT = None, domain = "", - host = "", max_renames = 32768): + servicetype = None, port = None, TXT = None, + domain = u"", host = u"", max_renames = 32768, + protocol = avahi.PROTO_UNSPEC, bus = None): self.interface = interface self.name = name - self.type = type + self.type = servicetype self.port = port - if TXT is None: - self.TXT = [] - else: - self.TXT = TXT + self.TXT = TXT if TXT is not None else [] self.domain = domain self.host = host self.rename_count = 0 self.max_renames = max_renames + self.protocol = protocol + self.group = None # our entry group + self.server = None + self.bus = bus def rename(self): """Derived from the Avahi example code""" if self.rename_count >= self.max_renames: - logger.critical(u"No suitable service name found after %i" - u" retries, exiting.", rename_count) - raise AvahiServiceError("Too many renames") - self.name = server.GetAlternativeServiceName(self.name) - logger.info(u"Changing name to %r ...", str(self.name)) - syslogger.setFormatter(logging.Formatter\ - ('Mandos (%s): %%(levelname)s:' - ' %%(message)s' % self.name)) + logger.critical(u"No suitable Zeroconf service name found" + u" after %i retries, exiting.", + self.rename_count) + raise AvahiServiceError(u"Too many renames") + self.name = unicode(self.server.GetAlternativeServiceName(self.name)) + logger.info(u"Changing Zeroconf service name to %r ...", + self.name) + syslogger.setFormatter(logging.Formatter + (u'Mandos (%s) [%%(process)d]:' + u' %%(levelname)s: %%(message)s' + % self.name)) self.remove() - self.add() + try: + self.add() + except dbus.exceptions.DBusException, error: + logger.critical(u"DBusException: %s", error) + self.cleanup() + os._exit(1) self.rename_count += 1 def remove(self): """Derived from the Avahi example code""" - if group is not None: - group.Reset() + if self.group is not None: + self.group.Reset() def add(self): """Derived from the Avahi example code""" - global group - if group is None: - group = dbus.Interface\ - (bus.get_object(avahi.DBUS_NAME, - server.EntryGroupNew()), - avahi.DBUS_INTERFACE_ENTRY_GROUP) - group.connect_to_signal('StateChanged', - entry_group_state_changed) - logger.debug(u"Adding service '%s' of type '%s' ...", - service.name, service.type) - group.AddService( - self.interface, # interface - avahi.PROTO_INET6, # protocol - dbus.UInt32(0), # flags - self.name, self.type, - self.domain, self.host, - dbus.UInt16(self.port), - avahi.string_array_to_txt_array(self.TXT)) - group.Commit() - -# From the Avahi example code: -group = None # our entry group -# End of Avahi example code + if self.group is None: + self.group = dbus.Interface( + self.bus.get_object(avahi.DBUS_NAME, + self.server.EntryGroupNew()), + avahi.DBUS_INTERFACE_ENTRY_GROUP) + self.group.connect_to_signal('StateChanged', + self + .entry_group_state_changed) + logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...", + self.name, self.type) + self.group.AddService( + self.interface, + self.protocol, + dbus.UInt32(0), # flags + self.name, self.type, + self.domain, self.host, + dbus.UInt16(self.port), + avahi.string_array_to_txt_array(self.TXT)) + self.group.Commit() + def entry_group_state_changed(self, state, error): + """Derived from the Avahi example code""" + logger.debug(u"Avahi entry group state change: %i", state) + + if state == avahi.ENTRY_GROUP_ESTABLISHED: + logger.debug(u"Zeroconf service established.") + elif state == avahi.ENTRY_GROUP_COLLISION: + logger.warning(u"Zeroconf service name collision.") + self.rename() + elif state == avahi.ENTRY_GROUP_FAILURE: + logger.critical(u"Avahi: Error in group state changed %s", + unicode(error)) + raise AvahiGroupError(u"State changed: %s" + % unicode(error)) + def cleanup(self): + """Derived from the Avahi example code""" + if self.group is not None: + self.group.Free() + self.group = None + def server_state_changed(self, state): + """Derived from the Avahi example code""" + logger.debug(u"Avahi server state change: %i", state) + if state == avahi.SERVER_COLLISION: + logger.error(u"Zeroconf server name collision") + self.remove() + elif state == avahi.SERVER_RUNNING: + self.add() + def activate(self): + """Derived from the Avahi example code""" + if self.server is None: + self.server = dbus.Interface( + self.bus.get_object(avahi.DBUS_NAME, + avahi.DBUS_PATH_SERVER), + avahi.DBUS_INTERFACE_SERVER) + self.server.connect_to_signal(u"StateChanged", + self.server_state_changed) + self.server_state_changed(self.server.GetState()) class Client(object): """A representation of a client host served by this server. + Attributes: - name: string; from the config file, used in log messages + _approved: bool(); 'None' if not yet approved/disapproved + approval_delay: datetime.timedelta(); Time to wait for approval + approval_duration: datetime.timedelta(); Duration of one approval + checker: subprocess.Popen(); a running checker process used + to see if the client lives. + 'None' if no process is running. + checker_callback_tag: a gobject event source tag, or None + checker_command: string; External command which is run to check + if client lives. %() expansions are done at + runtime with vars(self) as dict, so that for + instance %(name)s can be used in the command. + checker_initiator_tag: a gobject event source tag, or None + created: datetime.datetime(); (UTC) object creation + current_checker_command: string; current running checker_command + disable_hook: If set, called by disable() as disable_hook(self) + disable_initiator_tag: a gobject event source tag, or None + enabled: bool() fingerprint: string (40 or 32 hexadecimal digits); used to uniquely identify the client - secret: bytestring; sent verbatim (over TLS) to client - host: string; available for use by the checker command - created: datetime.datetime(); object creation, not client host - last_checked_ok: datetime.datetime() or None if not yet checked OK - timeout: datetime.timedelta(); How long from last_checked_ok - until this client is invalid - interval: datetime.timedelta(); How often to start a new checker - stop_hook: If set, called by stop() as stop_hook(self) - checker: subprocess.Popen(); a running checker process used - to see if the client lives. - 'None' if no process is running. - checker_initiator_tag: a gobject event source tag, or None - stop_initiator_tag: - '' - - checker_callback_tag: - '' - - checker_command: string; External command which is run to check if - client lives. %() expansions are done at - runtime with vars(self) as dict, so that for - instance %(name)s can be used in the command. - Private attibutes: - _timeout: Real variable for 'timeout' - _interval: Real variable for 'interval' - _timeout_milliseconds: Used when calling gobject.timeout_add() - _interval_milliseconds: - '' - + host: string; available for use by the checker command + interval: datetime.timedelta(); How often to start a new checker + last_approval_request: datetime.datetime(); (UTC) or None + last_checked_ok: datetime.datetime(); (UTC) or None + last_enabled: datetime.datetime(); (UTC) + name: string; from the config file, used in log messages and + D-Bus identifiers + secret: bytestring; sent verbatim (over TLS) to client + timeout: datetime.timedelta(); How long from last_checked_ok + until this client is disabled + runtime_expansions: Allowed attributes for runtime expansion. """ - def _set_timeout(self, timeout): - "Setter function for 'timeout' attribute" - self._timeout = timeout - self._timeout_milliseconds = ((self.timeout.days - * 24 * 60 * 60 * 1000) - + (self.timeout.seconds * 1000) - + (self.timeout.microseconds - // 1000)) - timeout = property(lambda self: self._timeout, - _set_timeout) - del _set_timeout - def _set_interval(self, interval): - "Setter function for 'interval' attribute" - self._interval = interval - self._interval_milliseconds = ((self.interval.days - * 24 * 60 * 60 * 1000) - + (self.interval.seconds - * 1000) - + (self.interval.microseconds - // 1000)) - interval = property(lambda self: self._interval, - _set_interval) - del _set_interval - def __init__(self, name = None, stop_hook=None, config={}): + + runtime_expansions = (u"approval_delay", u"approval_duration", + u"created", u"enabled", u"fingerprint", + u"host", u"interval", u"last_checked_ok", + u"last_enabled", u"name", u"timeout") + + @staticmethod + def _timedelta_to_milliseconds(td): + "Convert a datetime.timedelta() to milliseconds" + return ((td.days * 24 * 60 * 60 * 1000) + + (td.seconds * 1000) + + (td.microseconds // 1000)) + + def timeout_milliseconds(self): + "Return the 'timeout' attribute in milliseconds" + return self._timedelta_to_milliseconds(self.timeout) + + def interval_milliseconds(self): + "Return the 'interval' attribute in milliseconds" + return self._timedelta_to_milliseconds(self.interval) + + def approval_delay_milliseconds(self): + return self._timedelta_to_milliseconds(self.approval_delay) + + def __init__(self, name = None, disable_hook=None, config=None): """Note: the 'checker' key in 'config' sets the 'checker_command' attribute and *not* the 'checker' attribute.""" self.name = name + if config is None: + config = {} logger.debug(u"Creating client %r", self.name) # Uppercase and remove spaces from fingerprint for later # comparison purposes with return value from the fingerprint() # function - self.fingerprint = config["fingerprint"].upper()\ - .replace(u" ", u"") + self.fingerprint = (config[u"fingerprint"].upper() + .replace(u" ", u"")) logger.debug(u" Fingerprint: %s", self.fingerprint) - if "secret" in config: - self.secret = config["secret"].decode(u"base64") - elif "secfile" in config: - sf = open(config["secfile"]) - self.secret = sf.read() - sf.close() + if u"secret" in config: + self.secret = config[u"secret"].decode(u"base64") + elif u"secfile" in config: + 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" % self.name) - self.host = config.get("host", "") - self.created = datetime.datetime.now() + self.host = config.get(u"host", u"") + self.created = datetime.datetime.utcnow() + self.enabled = False + self.last_approval_request = None + self.last_enabled = None self.last_checked_ok = None - self.timeout = string_to_delta(config["timeout"]) - self.interval = string_to_delta(config["interval"]) - self.stop_hook = stop_hook + self.timeout = string_to_delta(config[u"timeout"]) + self.interval = string_to_delta(config[u"interval"]) + self.disable_hook = disable_hook self.checker = None self.checker_initiator_tag = None - self.stop_initiator_tag = None + self.disable_initiator_tag = None self.checker_callback_tag = None - self.check_command = config["checker"] - def start(self): + self.checker_command = config[u"checker"] + self.current_checker_command = None + self.last_connect = None + self._approved = None + self.approved_by_default = config.get(u"approved_by_default", + True) + self.approvals_pending = 0 + self.approval_delay = string_to_delta( + config[u"approval_delay"]) + self.approval_duration = string_to_delta( + config[u"approval_duration"]) + self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock()) + + def send_changedstate(self): + self.changedstate.acquire() + self.changedstate.notify_all() + self.changedstate.release() + + def enable(self): """Start this client's checker and timeout hooks""" + if getattr(self, u"enabled", False): + # Already enabled + return + self.send_changedstate() + self.last_enabled = datetime.datetime.utcnow() # Schedule a new checker to be started an 'interval' from now, # and every interval from then on. - self.checker_initiator_tag = gobject.timeout_add\ - (self._interval_milliseconds, - self.start_checker) + self.checker_initiator_tag = (gobject.timeout_add + (self.interval_milliseconds(), + self.start_checker)) + # Schedule a disable() when 'timeout' has passed + self.disable_initiator_tag = (gobject.timeout_add + (self.timeout_milliseconds(), + self.disable)) + self.enabled = True # Also start a new checker *right now*. self.start_checker() - # Schedule a stop() when 'timeout' has passed - self.stop_initiator_tag = gobject.timeout_add\ - (self._timeout_milliseconds, - self.stop) - def stop(self): - """Stop this client. - The possibility that a client might be restarted is left open, - but not currently used.""" - # If this client doesn't have a secret, it is already stopped. - if hasattr(self, "secret") and self.secret: - logger.info(u"Stopping client %s", self.name) - self.secret = None - else: + + def disable(self, quiet=True): + """Disable this client.""" + if not getattr(self, "enabled", False): return False - if getattr(self, "stop_initiator_tag", False): - gobject.source_remove(self.stop_initiator_tag) - self.stop_initiator_tag = None - if getattr(self, "checker_initiator_tag", False): + if not quiet: + self.send_changedstate() + if not quiet: + logger.info(u"Disabling client %s", self.name) + if getattr(self, u"disable_initiator_tag", False): + gobject.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = None + if getattr(self, u"checker_initiator_tag", False): gobject.source_remove(self.checker_initiator_tag) self.checker_initiator_tag = None self.stop_checker() - if self.stop_hook: - self.stop_hook(self) + if self.disable_hook: + self.disable_hook(self) + self.enabled = False # Do not run this again if called by a gobject.timeout_add return False + def __del__(self): - self.stop_hook = None - self.stop() - def checker_callback(self, pid, condition): + self.disable_hook = None + self.disable() + + def checker_callback(self, pid, condition, command): """The checker has completed, so take appropriate actions.""" - now = datetime.datetime.now() self.checker_callback_tag = None self.checker = None - if os.WIFEXITED(condition) \ - and (os.WEXITSTATUS(condition) == 0): - logger.info(u"Checker for %(name)s succeeded", - vars(self)) - self.last_checked_ok = now - gobject.source_remove(self.stop_initiator_tag) - self.stop_initiator_tag = gobject.timeout_add\ - (self._timeout_milliseconds, - self.stop) - elif not os.WIFEXITED(condition): + if os.WIFEXITED(condition): + exitstatus = os.WEXITSTATUS(condition) + if exitstatus == 0: + logger.info(u"Checker for %(name)s succeeded", + vars(self)) + self.checked_ok() + else: + logger.info(u"Checker for %(name)s failed", + vars(self)) + else: logger.warning(u"Checker for %(name)s crashed?", vars(self)) - else: - logger.info(u"Checker for %(name)s failed", - vars(self)) + + def checked_ok(self): + """Bump up the timeout for this client. + + This should only be called when the client has been seen, + alive and well. + """ + self.last_checked_ok = datetime.datetime.utcnow() + gobject.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = (gobject.timeout_add + (self.timeout_milliseconds(), + self.disable)) + + def need_approval(self): + self.last_approval_request = datetime.datetime.utcnow() + def start_checker(self): """Start a new checker subprocess if one is not running. + If a checker already exists, leave it running and do nothing.""" # The reason for not killing a running checker is that if we @@ -317,329 +438,1219 @@ # 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: + pid, status = os.waitpid(self.checker.pid, os.WNOHANG) + except (AttributeError, OSError), error: + if (isinstance(error, OSError) + and error.errno != errno.ECHILD): + raise error + else: + if pid: + logger.warning(u"Checker was a zombie") + gobject.source_remove(self.checker_callback_tag) + self.checker_callback(pid, status, + self.current_checker_command) + # Start a new checker if needed if self.checker is None: try: - # In case check_command has exactly one % operator - command = self.check_command % self.host + # In case checker_command has exactly one % operator + command = self.checker_command % self.host except TypeError: # Escape attributes for the shell - escaped_attrs = dict((key, re.escape(str(val))) - for key, val in - vars(self).iteritems()) + escaped_attrs = dict( + (attr, + re.escape(unicode(str(getattr(self, attr, u"")), + errors= + u'replace'))) + for attr in + self.runtime_expansions) + try: - command = self.check_command % escaped_attrs + command = self.checker_command % escaped_attrs except TypeError, error: logger.error(u'Could not format string "%s":' - u' %s', self.check_command, error) + u' %s', self.checker_command, error) return True # Try again later + self.current_checker_command = command try: logger.info(u"Starting checker %r for %s", command, self.name) + # We don't need to redirect stdout and stderr, since + # 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.) self.checker = subprocess.Popen(command, close_fds=True, - shell=True, cwd="/") - self.checker_callback_tag = gobject.child_watch_add\ - (self.checker.pid, - self.checker_callback) - except subprocess.OSError, error: + shell=True, cwd=u"/") + 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) + if pid: + gobject.source_remove(self.checker_callback_tag) + self.checker_callback(pid, status, command) + except OSError, error: logger.error(u"Failed to start subprocess: %s", error) # Re-run this periodically if run by gobject.timeout_add return True + def stop_checker(self): """Force the checker process, if any, to stop.""" if self.checker_callback_tag: gobject.source_remove(self.checker_callback_tag) self.checker_callback_tag = None - if getattr(self, "checker", None) is None: + if getattr(self, u"checker", None) is None: return logger.debug(u"Stopping checker for %(name)s", vars(self)) try: os.kill(self.checker.pid, signal.SIGTERM) - #os.sleep(0.5) + #time.sleep(0.5) #if self.checker.poll() is None: # os.kill(self.checker.pid, signal.SIGKILL) except OSError, error: 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?""" - now = datetime.datetime.now() + +def dbus_service_property(dbus_interface, signature=u"v", + access=u"readwrite", byte_arrays=False): + """Decorators for marking methods of a DBusObjectWithProperties to + become properties on the D-Bus. + + The decorated method will be called with no arguments by "Get" + and with one argument by "Set". + + The parameters, where they are supported, are the same as + 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 + func._dbus_signature = signature + func._dbus_access = access + func._dbus_name = func.__name__ + if func._dbus_name.endswith(u"_dbus_property"): + func._dbus_name = func._dbus_name[:-14] + func._dbus_get_args_options = {u'byte_arrays': byte_arrays } + return func + return decorator + + +class DBusPropertyException(dbus.exceptions.DBusException): + """A base class for D-Bus property-related exceptions + """ + def __unicode__(self): + return unicode(str(self)) + + +class DBusPropertyAccessException(DBusPropertyException): + """A property's access permissions disallows an operation. + """ + pass + + +class DBusPropertyNotFound(DBusPropertyException): + """An attempt was made to access a non-existing property. + """ + pass + + +class DBusObjectWithProperties(dbus.service.Object): + """A D-Bus object with properties. + + Classes inheriting from this can use the dbus_service_property + decorator to expose methods as D-Bus properties. It exposes the + standard Get(), Set(), and GetAll() methods on the D-Bus. + """ + + @staticmethod + def _is_dbus_property(obj): + return getattr(obj, u"_dbus_is_property", False) + + def _get_all_dbus_properties(self): + """Returns a generator of (name, attribute) pairs + """ + return ((prop._dbus_name, prop) + for name, prop in + inspect.getmembers(self, self._is_dbus_property)) + + def _get_dbus_property(self, interface_name, property_name): + """Returns a bound method if one exists which is a D-Bus + property with the specified name and interface. + """ + for name in (property_name, + property_name + u"_dbus_property"): + prop = getattr(self, name, None) + if (prop is None + or not self._is_dbus_property(prop) + or prop._dbus_name != property_name + or (interface_name and prop._dbus_interface + and interface_name != prop._dbus_interface)): + continue + return prop + # No such property + raise DBusPropertyNotFound(self.dbus_object_path + u":" + + interface_name + u"." + + property_name) + + @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss", + out_signature=u"v") + def Get(self, interface_name, property_name): + """Standard D-Bus property Get() method, see D-Bus standard. + """ + prop = self._get_dbus_property(interface_name, property_name) + if prop._dbus_access == u"write": + raise DBusPropertyAccessException(property_name) + value = prop() + if not hasattr(value, u"variant_level"): + return value + return type(value)(value, variant_level=value.variant_level+1) + + @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv") + def Set(self, interface_name, property_name, value): + """Standard D-Bus property Set() method, see D-Bus standard. + """ + prop = self._get_dbus_property(interface_name, property_name) + 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) + + @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s", + out_signature=u"a{sv}") + def GetAll(self, interface_name): + """Standard D-Bus property GetAll() method, see D-Bus + standard. + + Note: Will not include properties with access="write". + """ + all = {} + for name, prop in self._get_all_dbus_properties(): + if (interface_name + and interface_name != prop._dbus_interface): + # Interface non-empty but did not match + continue + # Ignore write-only properties + if prop._dbus_access == u"write": + continue + value = prop() + if not hasattr(value, u"variant_level"): + all[name] = value + continue + all[name] = type(value)(value, variant_level= + value.variant_level+1) + return dbus.Dictionary(all, signature=u"sv") + + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + out_signature=u"s", + path_keyword='object_path', + connection_keyword='connection') + def Introspect(self, object_path, connection): + """Standard D-Bus method, overloaded to insert property tags. + """ + xmlstring = dbus.service.Object.Introspect(self, object_path, + connection) + try: + document = xml.dom.minidom.parseString(xmlstring) + def make_tag(document, name, prop): + e = document.createElement(u"property") + e.setAttribute(u"name", name) + e.setAttribute(u"type", prop._dbus_signature) + e.setAttribute(u"access", prop._dbus_access) + return e + for if_tag in document.getElementsByTagName(u"interface"): + for tag in (make_tag(document, name, prop) + for name, prop + in self._get_all_dbus_properties() + if prop._dbus_interface + == if_tag.getAttribute(u"name")): + if_tag.appendChild(tag) + # Add the names to the return values for the + # "org.freedesktop.DBus.Properties" methods + if (if_tag.getAttribute(u"name") + == u"org.freedesktop.DBus.Properties"): + for cn in if_tag.getElementsByTagName(u"method"): + if cn.getAttribute(u"name") == u"Get": + for arg in cn.getElementsByTagName(u"arg"): + if (arg.getAttribute(u"direction") + == u"out"): + arg.setAttribute(u"name", u"value") + elif cn.getAttribute(u"name") == u"GetAll": + for arg in cn.getElementsByTagName(u"arg"): + if (arg.getAttribute(u"direction") + == u"out"): + arg.setAttribute(u"name", u"props") + xmlstring = document.toxml(u"utf-8") + document.unlink() + except (AttributeError, xml.dom.DOMException, + xml.parsers.expat.ExpatError), error: + logger.error(u"Failed to override Introspection method", + error) + return xmlstring + + +class ClientDBus(Client, DBusObjectWithProperties): + """A Client class using D-Bus + + Attributes: + dbus_object_path: dbus.ObjectPath + bus: dbus.SystemBus() + """ + + runtime_expansions = (Client.runtime_expansions + + (u"dbus_object_path",)) + + # dbus.service.Object doesn't use super(), so we can't either. + + def __init__(self, bus = None, *args, **kwargs): + self._approvals_pending = 0 + self.bus = bus + Client.__init__(self, *args, **kwargs) + # Only now, when this client is initialized, can it show up on + # the D-Bus + client_object_name = unicode(self.name).translate( + {ord(u"."): ord(u"_"), + ord(u"-"): ord(u"_")}) + self.dbus_object_path = (dbus.ObjectPath + (u"/clients/" + client_object_name)) + DBusObjectWithProperties.__init__(self, self.bus, + self.dbus_object_path) + + def _get_approvals_pending(self): + return self._approvals_pending + def _set_approvals_pending(self, value): + old_value = self._approvals_pending + self._approvals_pending = value + bval = bool(value) + if (hasattr(self, "dbus_object_path") + and bval is not bool(old_value)): + dbus_bool = dbus.Boolean(bval, variant_level=1) + self.PropertyChanged(dbus.String(u"ApprovalPending"), + dbus_bool) + + approvals_pending = property(_get_approvals_pending, + _set_approvals_pending) + del _get_approvals_pending, _set_approvals_pending + + @staticmethod + def _datetime_to_dbus(dt, variant_level=0): + """Convert a UTC datetime.datetime() to a D-Bus type.""" + return dbus.String(dt.isoformat(), + variant_level=variant_level) + + def enable(self): + oldstate = getattr(self, u"enabled", False) + r = Client.enable(self) + if oldstate != self.enabled: + # Emit D-Bus signals + self.PropertyChanged(dbus.String(u"Enabled"), + dbus.Boolean(True, variant_level=1)) + self.PropertyChanged( + dbus.String(u"LastEnabled"), + self._datetime_to_dbus(self.last_enabled, + variant_level=1)) + return r + + def disable(self, quiet = False): + oldstate = getattr(self, u"enabled", False) + r = Client.disable(self, quiet=quiet) + if not quiet and oldstate != self.enabled: + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Enabled"), + dbus.Boolean(False, variant_level=1)) + return r + + def __del__(self, *args, **kwargs): + try: + self.remove_from_connection() + except LookupError: + pass + if hasattr(DBusObjectWithProperties, u"__del__"): + DBusObjectWithProperties.__del__(self, *args, **kwargs) + Client.__del__(self, *args, **kwargs) + + def checker_callback(self, pid, condition, command, + *args, **kwargs): + self.checker_callback_tag = None + self.checker = None + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"CheckerRunning"), + dbus.Boolean(False, variant_level=1)) + if os.WIFEXITED(condition): + exitstatus = os.WEXITSTATUS(condition) + # Emit D-Bus signal + self.CheckerCompleted(dbus.Int16(exitstatus), + dbus.Int64(condition), + dbus.String(command)) + else: + # Emit D-Bus signal + self.CheckerCompleted(dbus.Int16(-1), + dbus.Int64(condition), + dbus.String(command)) + + return Client.checker_callback(self, pid, condition, command, + *args, **kwargs) + + def checked_ok(self, *args, **kwargs): + r = Client.checked_ok(self, *args, **kwargs) + # Emit D-Bus signal + self.PropertyChanged( + dbus.String(u"LastCheckedOK"), + (self._datetime_to_dbus(self.last_checked_ok, + variant_level=1))) + return r + + def need_approval(self, *args, **kwargs): + r = Client.need_approval(self, *args, **kwargs) + # Emit D-Bus signal + self.PropertyChanged( + dbus.String(u"LastApprovalRequest"), + (self._datetime_to_dbus(self.last_approval_request, + variant_level=1))) + return r + + def start_checker(self, *args, **kwargs): + old_checker = self.checker + if self.checker is not None: + old_checker_pid = self.checker.pid + else: + old_checker_pid = None + r = Client.start_checker(self, *args, **kwargs) + # Only if new checker process was started + if (self.checker is not None + and old_checker_pid != self.checker.pid): + # Emit D-Bus signal + self.CheckerStarted(self.current_checker_command) + self.PropertyChanged( + dbus.String(u"CheckerRunning"), + dbus.Boolean(True, variant_level=1)) + return r + + def stop_checker(self, *args, **kwargs): + old_checker = getattr(self, u"checker", None) + r = Client.stop_checker(self, *args, **kwargs) + if (old_checker is not None + and getattr(self, u"checker", None) is None): + self.PropertyChanged(dbus.String(u"CheckerRunning"), + dbus.Boolean(False, variant_level=1)) + return r + + def _reset_approved(self): + self._approved = None + return False + + def approve(self, value=True): + self.send_changedstate() + self._approved = value + gobject.timeout_add(self._timedelta_to_milliseconds + (self.approval_duration), + self._reset_approved) + + + ## D-Bus methods, signals & properties + _interface = u"se.bsnet.fukt.Mandos.Client" + + ## Signals + + # CheckerCompleted - signal + @dbus.service.signal(_interface, signature=u"nxs") + def CheckerCompleted(self, exitcode, waitstatus, command): + "D-Bus signal" + pass + + # CheckerStarted - signal + @dbus.service.signal(_interface, signature=u"s") + def CheckerStarted(self, command): + "D-Bus signal" + pass + + # PropertyChanged - signal + @dbus.service.signal(_interface, signature=u"sv") + def PropertyChanged(self, property, value): + "D-Bus signal" + pass + + # GotSecret - signal + @dbus.service.signal(_interface) + def GotSecret(self): + """D-Bus signal + Is sent after a successful transfer of secret from the Mandos + server to mandos-client + """ + pass + + # Rejected - signal + @dbus.service.signal(_interface, signature=u"s") + def Rejected(self, reason): + "D-Bus signal" + pass + + # NeedApproval - signal + @dbus.service.signal(_interface, signature=u"tb") + def NeedApproval(self, timeout, default): + "D-Bus signal" + return self.need_approval() + + ## Methods + + # Approve - method + @dbus.service.method(_interface, in_signature=u"b") + def Approve(self, value): + self.approve(value) + + # CheckedOK - method + @dbus.service.method(_interface) + def CheckedOK(self): + return self.checked_ok() + + # Enable - method + @dbus.service.method(_interface) + def Enable(self): + "D-Bus method" + self.enable() + + # StartChecker - method + @dbus.service.method(_interface) + def StartChecker(self): + "D-Bus method" + self.start_checker() + + # Disable - method + @dbus.service.method(_interface) + def Disable(self): + "D-Bus method" + self.disable() + + # StopChecker - method + @dbus.service.method(_interface) + def StopChecker(self): + self.stop_checker() + + ## Properties + + # ApprovalPending - property + @dbus_service_property(_interface, signature=u"b", access=u"read") + def ApprovalPending_dbus_property(self): + return dbus.Boolean(bool(self.approvals_pending)) + + # ApprovedByDefault - property + @dbus_service_property(_interface, signature=u"b", + access=u"readwrite") + def ApprovedByDefault_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.approved_by_default) + self.approved_by_default = bool(value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"ApprovedByDefault"), + dbus.Boolean(value, variant_level=1)) + + # ApprovalDelay - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def ApprovalDelay_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.approval_delay_milliseconds()) + self.approval_delay = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"ApprovalDelay"), + dbus.UInt64(value, variant_level=1)) + + # ApprovalDuration - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def ApprovalDuration_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self._timedelta_to_milliseconds( + self.approval_duration)) + self.approval_duration = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"ApprovalDuration"), + dbus.UInt64(value, variant_level=1)) + + # Name - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def Name_dbus_property(self): + return dbus.String(self.name) + + # Fingerprint - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def Fingerprint_dbus_property(self): + return dbus.String(self.fingerprint) + + # Host - property + @dbus_service_property(_interface, signature=u"s", + access=u"readwrite") + def Host_dbus_property(self, value=None): + if value is None: # get + return dbus.String(self.host) + self.host = value + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Host"), + dbus.String(value, variant_level=1)) + + # Created - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def Created_dbus_property(self): + return dbus.String(self._datetime_to_dbus(self.created)) + + # LastEnabled - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def LastEnabled_dbus_property(self): + if self.last_enabled is None: + return dbus.String(u"") + return dbus.String(self._datetime_to_dbus(self.last_enabled)) + + # Enabled - property + @dbus_service_property(_interface, signature=u"b", + access=u"readwrite") + def Enabled_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.enabled) + if value: + self.enable() + else: + self.disable() + + # LastCheckedOK - property + @dbus_service_property(_interface, signature=u"s", + access=u"readwrite") + def LastCheckedOK_dbus_property(self, value=None): + if value is not None: + self.checked_ok() + return if self.last_checked_ok is None: - return now < (self.created + self.timeout) - else: - return now < (self.last_checked_ok + self.timeout) - - -def peer_certificate(session): - "Return the peer's OpenPGP certificate as a bytestring" - # If not an OpenPGP certificate... - if gnutls.library.functions.gnutls_certificate_type_get\ - (session._c_object) \ - != gnutls.library.constants.GNUTLS_CRT_OPENPGP: - # ...do the normal thing - return session.peer_certificate - list_size = ctypes.c_uint() - cert_list = gnutls.library.functions.gnutls_certificate_get_peers\ - (session._c_object, ctypes.byref(list_size)) - if list_size.value == 0: - return None - cert = cert_list[0] - return ctypes.string_at(cert.data, cert.size) - - -def fingerprint(openpgp): - "Convert an OpenPGP bytestring to a hexdigit fingerprint string" - # New GnuTLS "datum" with the OpenPGP public key - datum = gnutls.library.types.gnutls_datum_t\ - (ctypes.cast(ctypes.c_char_p(openpgp), - ctypes.POINTER(ctypes.c_ubyte)), - ctypes.c_uint(len(openpgp))) - # New empty GnuTLS certificate - crt = gnutls.library.types.gnutls_openpgp_crt_t() - gnutls.library.functions.gnutls_openpgp_crt_init\ - (ctypes.byref(crt)) - # Import the OpenPGP public key into the certificate - gnutls.library.functions.gnutls_openpgp_crt_import\ - (crt, ctypes.byref(datum), - gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW) - # New buffer for the fingerprint - buffer = ctypes.create_string_buffer(20) - buffer_length = ctypes.c_size_t() - # Get the fingerprint from the certificate into the buffer - gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\ - (crt, ctypes.byref(buffer), ctypes.byref(buffer_length)) - # Deinit the certificate - gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) - # Convert the buffer to a Python bytestring - fpr = ctypes.string_at(buffer, buffer_length.value) - # Convert the bytestring to hexadecimal notation - hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr) - return hex_fpr - - -class tcp_handler(SocketServer.BaseRequestHandler, object): - """A TCP request handler class. - Instantiated by IPv6_TCPServer for each request to handle it. + return dbus.String(u"") + return dbus.String(self._datetime_to_dbus(self + .last_checked_ok)) + + # LastApprovalRequest - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def LastApprovalRequest_dbus_property(self): + if self.last_approval_request is None: + return dbus.String(u"") + return dbus.String(self. + _datetime_to_dbus(self + .last_approval_request)) + + # Timeout - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def Timeout_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.timeout_milliseconds()) + self.timeout = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Timeout"), + dbus.UInt64(value, variant_level=1)) + if getattr(self, u"disable_initiator_tag", None) is None: + return + # Reschedule timeout + gobject.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = None + time_to_die = (self. + _timedelta_to_milliseconds((self + .last_checked_ok + + self.timeout) + - datetime.datetime + .utcnow())) + if time_to_die <= 0: + # The timeout has passed + self.disable() + else: + self.disable_initiator_tag = (gobject.timeout_add + (time_to_die, self.disable)) + + # Interval - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def Interval_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.interval_milliseconds()) + self.interval = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Interval"), + dbus.UInt64(value, variant_level=1)) + if getattr(self, u"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 + + # Checker - property + @dbus_service_property(_interface, signature=u"s", + access=u"readwrite") + def Checker_dbus_property(self, value=None): + if value is None: # get + return dbus.String(self.checker_command) + self.checker_command = value + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Checker"), + dbus.String(self.checker_command, + variant_level=1)) + + # CheckerRunning - property + @dbus_service_property(_interface, signature=u"b", + access=u"readwrite") + def CheckerRunning_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.checker is not None) + if value: + self.start_checker() + else: + self.stop_checker() + + # ObjectPath - property + @dbus_service_property(_interface, signature=u"o", access=u"read") + def ObjectPath_dbus_property(self): + return self.dbus_object_path # is already a dbus.ObjectPath + + # Secret = property + @dbus_service_property(_interface, signature=u"ay", + access=u"write", byte_arrays=True) + def Secret_dbus_property(self, value): + self.secret = str(value) + + del _interface + + +class ProxyClient(object): + def __init__(self, child_pipe, fpr, address): + self._pipe = child_pipe + self._pipe.send(('init', fpr, address)) + if not self._pipe.recv(): + raise KeyError() + + def __getattribute__(self, name): + if(name == '_pipe'): + return super(ProxyClient, self).__getattribute__(name) + self._pipe.send(('getattr', name)) + data = self._pipe.recv() + if data[0] == 'data': + return data[1] + if data[0] == 'function': + def func(*args, **kwargs): + self._pipe.send(('funcall', name, args, kwargs)) + return self._pipe.recv()[1] + return func + + def __setattr__(self, name, value): + if(name == '_pipe'): + return super(ProxyClient, self).__setattr__(name, value) + self._pipe.send(('setattr', name, value)) + + +class ClientHandler(socketserver.BaseRequestHandler, object): + """A class to handle client connections. + + Instantiated once for each connection to handle it. Note: This will run in its own forked process.""" def handle(self): - logger.info(u"TCP connection from: %s", - unicode(self.client_address)) - session = gnutls.connection.ClientSession\ - (self.request, gnutls.connection.X509Credentials()) - - line = self.request.makefile().readline() - logger.debug(u"Protocol version: %r", line) - try: - if int(line.strip().split()[0]) > 1: - raise RuntimeError - except (ValueError, IndexError, RuntimeError), error: - logger.error(u"Unknown protocol version: %s", error) - return - - # Note: gnutls.connection.X509Credentials is really a generic - # GnuTLS certificate credentials object so long as no X.509 - # keys are added to it. Therefore, we can use it here despite - # using OpenPGP certificates. - - #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC", - # "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP", - # "+DHE-DSS")) - priority = "NORMAL" # Fallback default, since this - # MUST be set. - if self.server.settings["priority"]: - priority = self.server.settings["priority"] - gnutls.library.functions.gnutls_priority_set_direct\ - (session._c_object, priority, None); - - try: - session.handshake() - except gnutls.errors.GNUTLSError, error: - logger.warning(u"Handshake failed: %s", error) - # Do not run session.bye() here: the session is not - # established. Just abandon the request. - return - try: - fpr = fingerprint(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) - client = None - for c in self.server.clients: - if c.fingerprint == fpr: - client = c - break - if not client: - logger.warning(u"Client not found for fingerprint: %s", - fpr) - 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(): - logger.warning(u"Client %(name)s is invalid", - vars(client)) - session.bye() - return - 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() - - -class IPv6_TCPServer(SocketServer.ForkingTCPServer, object): - """IPv6 TCP server. Accepts 'None' as address and/or port. + with contextlib.closing(self.server.child_pipe) as child_pipe: + logger.info(u"TCP connection from: %s", + unicode(self.client_address)) + logger.debug(u"Pipe FD: %d", + self.server.child_pipe.fileno()) + + session = (gnutls.connection + .ClientSession(self.request, + gnutls.connection + .X509Credentials())) + + # Note: gnutls.connection.X509Credentials is really a + # generic GnuTLS certificate credentials object so long as + # no X.509 keys are added to it. Therefore, we can use it + # here despite using OpenPGP certificates. + + #priority = u':'.join((u"NONE", u"+VERS-TLS1.1", + # u"+AES-256-CBC", u"+SHA1", + # u"+COMP-NULL", u"+CTYPE-OPENPGP", + # u"+DHE-DSS")) + # Use a fallback default, since this MUST be set. + priority = self.server.gnutls_priority + if priority is None: + priority = u"NORMAL" + (gnutls.library.functions + .gnutls_priority_set_direct(session._c_object, + priority, None)) + + # Start communication using the Mandos protocol + # Get protocol number + line = self.request.makefile().readline() + logger.debug(u"Protocol version: %r", line) + try: + if int(line.strip().split()[0]) > 1: + raise RuntimeError + except (ValueError, IndexError, RuntimeError), error: + logger.error(u"Unknown protocol version: %s", error) + return + + # Start GnuTLS connection + try: + session.handshake() + except gnutls.errors.GNUTLSError, error: + logger.warning(u"Handshake failed: %s", error) + # Do not run session.bye() here: the session is not + # established. Just abandon the request. + return + logger.debug(u"Handshake succeeded") + + approval_required = False + try: + 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) + + try: + client = ProxyClient(child_pipe, fpr, + self.client_address) + except KeyError: + return + + if client.approval_delay: + delay = client.approval_delay + client.approvals_pending += 1 + approval_required = True + + while True: + if not client.enabled: + logger.warning(u"Client %s is disabled", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Disabled") + return + + if client._approved or not client.approval_delay: + #We are approved or approval is disabled + break + elif client._approved is None: + logger.info(u"Client %s needs approval", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.NeedApproval( + client.approval_delay_milliseconds(), + client.approved_by_default) + else: + logger.warning(u"Client %s was not approved", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Denied") + return + + #wait until timeout or approved + #x = float(client._timedelta_to_milliseconds(delay)) + time = datetime.datetime.now() + client.changedstate.acquire() + client.changedstate.wait(float(client._timedelta_to_milliseconds(delay) / 1000)) + client.changedstate.release() + time2 = datetime.datetime.now() + if (time2 - time) >= delay: + if not client.approved_by_default: + logger.warning("Client %s timed out while" + " waiting for approval", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Approval timed out") + return + else: + break + else: + delay -= time2 - time + + sent_size = 0 + while sent_size < len(client.secret): + try: + sent = session.send(client.secret[sent_size:]) + except (gnutls.errors.GNUTLSError), error: + logger.warning("gnutls send failed") + return + logger.debug(u"Sent: %d, remaining: %d", + sent, len(client.secret) + - (sent_size + sent)) + sent_size += sent + + logger.info(u"Sending secret to %s", client.name) + # bump the timeout as if seen + client.checked_ok() + if self.server.use_dbus: + # Emit D-Bus signal + client.GotSecret() + + finally: + if approval_required: + client.approvals_pending -= 1 + try: + session.bye() + except (gnutls.errors.GNUTLSError), error: + logger.warning("GnuTLS bye failed") + + @staticmethod + def peer_certificate(session): + "Return the peer's OpenPGP certificate as a bytestring" + # If not an OpenPGP certificate... + if (gnutls.library.functions + .gnutls_certificate_type_get(session._c_object) + != gnutls.library.constants.GNUTLS_CRT_OPENPGP): + # ...do the normal thing + return session.peer_certificate + list_size = ctypes.c_uint(1) + cert_list = (gnutls.library.functions + .gnutls_certificate_get_peers + (session._c_object, ctypes.byref(list_size))) + if not bool(cert_list) and list_size.value != 0: + raise gnutls.errors.GNUTLSError(u"error getting peer" + u" certificate") + if list_size.value == 0: + return None + cert = cert_list[0] + return ctypes.string_at(cert.data, cert.size) + + @staticmethod + def fingerprint(openpgp): + "Convert an OpenPGP bytestring to a hexdigit fingerprint" + # New GnuTLS "datum" with the OpenPGP public key + datum = (gnutls.library.types + .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp), + ctypes.POINTER + (ctypes.c_ubyte)), + ctypes.c_uint(len(openpgp)))) + # New empty GnuTLS certificate + crt = gnutls.library.types.gnutls_openpgp_crt_t() + (gnutls.library.functions + .gnutls_openpgp_crt_init(ctypes.byref(crt))) + # Import the OpenPGP public key into the certificate + (gnutls.library.functions + .gnutls_openpgp_crt_import(crt, ctypes.byref(datum), + gnutls.library.constants + .GNUTLS_OPENPGP_FMT_RAW)) + # Verify the self signature in the key + crtverify = ctypes.c_uint() + (gnutls.library.functions + .gnutls_openpgp_crt_verify_self(crt, 0, + ctypes.byref(crtverify))) + if crtverify.value != 0: + gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) + raise (gnutls.errors.CertificateSecurityError + (u"Verify failed")) + # New buffer for the fingerprint + buf = ctypes.create_string_buffer(20) + buf_len = ctypes.c_size_t() + # Get the fingerprint from the certificate into the buffer + (gnutls.library.functions + .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf), + ctypes.byref(buf_len))) + # Deinit the certificate + gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) + # Convert the buffer to a Python bytestring + fpr = ctypes.string_at(buf, buf_len.value) + # Convert the bytestring to hexadecimal notation + hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr) + return hex_fpr + + +class MultiprocessingMixIn(object): + """Like socketserver.ThreadingMixIn, but with multiprocessing""" + def sub_process_main(self, request, address): + try: + self.finish_request(request, address) + except: + self.handle_error(request, address) + self.close_request(request) + + def process_request(self, request, address): + """Start a new process to process the request.""" + multiprocessing.Process(target = self.sub_process_main, + args = (request, address)).start() + +class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object): + """ adds a pipe to the MixIn """ + def process_request(self, request, client_address): + """Overrides and wraps the original process_request(). + + This function creates a new pipe in self.pipe + """ + parent_pipe, self.child_pipe = multiprocessing.Pipe() + + super(MultiprocessingMixInWithPipe, + self).process_request(request, client_address) + self.child_pipe.close() + self.add_pipe(parent_pipe) + + def add_pipe(self, parent_pipe): + """Dummy function; override as necessary""" + pass + +class IPv6_TCPServer(MultiprocessingMixInWithPipe, + socketserver.TCPServer, object): + """IPv6-capable TCP server. Accepts 'None' as address and/or port + Attributes: - settings: Server settings - clients: Set() of Client objects + enabled: Boolean; whether this server is activated yet + interface: None or a network interface name (string) + use_ipv6: Boolean; to use IPv6 or not """ - address_family = socket.AF_INET6 - def __init__(self, *args, **kwargs): - if "settings" in kwargs: - self.settings = kwargs["settings"] - del kwargs["settings"] - if "clients" in kwargs: - self.clients = kwargs["clients"] - del kwargs["clients"] - return super(type(self), self).__init__(*args, **kwargs) + def __init__(self, server_address, RequestHandlerClass, + interface=None, use_ipv6=True): + self.interface = interface + if use_ipv6: + self.address_family = socket.AF_INET6 + socketserver.TCPServer.__init__(self, server_address, + RequestHandlerClass) def server_bind(self): """This overrides the normal server_bind() function to bind to an interface if one was specified, and also NOT to bind to an address or port if they were not specified.""" - if self.settings["interface"]: - # 25 is from /usr/include/asm-i486/socket.h - SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25) - try: - self.socket.setsockopt(socket.SOL_SOCKET, - SO_BINDTODEVICE, - self.settings["interface"]) - except socket.error, error: - if error[0] == errno.EPERM: - logger.error(u"No permission to" - u" bind to interface %s", - self.settings["interface"]) - else: - raise error + if self.interface is not None: + if SO_BINDTODEVICE is None: + logger.error(u"SO_BINDTODEVICE does not exist;" + u" cannot bind to interface %s", + self.interface) + else: + try: + self.socket.setsockopt(socket.SOL_SOCKET, + SO_BINDTODEVICE, + str(self.interface + + u'\0')) + except socket.error, error: + if error[0] == errno.EPERM: + logger.error(u"No permission to" + u" bind to interface %s", + self.interface) + elif error[0] == errno.ENOPROTOOPT: + logger.error(u"SO_BINDTODEVICE not available;" + u" cannot bind to interface %s", + self.interface) + else: + raise # Only bind(2) the socket if we really need to. if self.server_address[0] or self.server_address[1]: if not self.server_address[0]: - in6addr_any = "::" - self.server_address = (in6addr_any, + if self.address_family == socket.AF_INET6: + any_address = u"::" # in6addr_any + else: + any_address = socket.INADDR_ANY + self.server_address = (any_address, self.server_address[1]) elif not self.server_address[1]: self.server_address = (self.server_address[0], 0) -# if self.settings["interface"]: +# if self.interface: # self.server_address = (self.server_address[0], # 0, # port # 0, # flowinfo # if_nametoindex -# (self.settings -# ["interface"])) - return super(type(self), self).server_bind() +# (self.interface)) + return socketserver.TCPServer.server_bind(self) + + +class MandosServer(IPv6_TCPServer): + """Mandos server. + + Attributes: + clients: set of Client objects + gnutls_priority GnuTLS priority string + use_dbus: Boolean; to emit D-Bus signals or not + + Assumes a gobject.MainLoop event loop. + """ + def __init__(self, server_address, RequestHandlerClass, + interface=None, use_ipv6=True, clients=None, + gnutls_priority=None, use_dbus=True): + self.enabled = False + self.clients = clients + if self.clients is None: + self.clients = set() + self.use_dbus = use_dbus + self.gnutls_priority = gnutls_priority + IPv6_TCPServer.__init__(self, server_address, + RequestHandlerClass, + interface = interface, + use_ipv6 = use_ipv6) + def server_activate(self): + if self.enabled: + return socketserver.TCPServer.server_activate(self) + def enable(self): + self.enabled = True + def add_pipe(self, parent_pipe): + # Call "handle_ipc" for both data and EOF events + gobject.io_add_watch(parent_pipe.fileno(), + gobject.IO_IN | gobject.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe = parent_pipe)) + + def handle_ipc(self, source, condition, parent_pipe=None, + client_object=None): + condition_names = { + gobject.IO_IN: u"IN", # There is data to read. + gobject.IO_OUT: u"OUT", # Data can be written (without + # blocking). + gobject.IO_PRI: u"PRI", # There is urgent data to read. + gobject.IO_ERR: u"ERR", # Error condition. + gobject.IO_HUP: u"HUP" # Hung up (the connection has been + # broken, usually for pipes and + # sockets). + } + conditions_string = ' | '.join(name + for cond, name in + condition_names.iteritems() + if cond & condition) + # error or the other end of multiprocessing.Pipe has closed + if condition & (gobject.IO_ERR | condition & gobject.IO_HUP): + return False + + # Read a request from the child + request = parent_pipe.recv() + command = request[0] + + if command == 'init': + fpr = request[1] + address = request[2] + + for c in self.clients: + if c.fingerprint == fpr: + client = c + break + else: + logger.warning(u"Client not found for fingerprint: %s, ad" + u"dress: %s", fpr, address) + if self.use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientNotFound(fpr, address[0]) + parent_pipe.send(False) + return False + + gobject.io_add_watch(parent_pipe.fileno(), + gobject.IO_IN | gobject.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe = parent_pipe, + client_object = client)) + parent_pipe.send(True) + # remove the old hook in favor of the new above hook on same fileno + return False + if command == 'funcall': + funcname = request[1] + args = request[2] + kwargs = request[3] + + parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs))) + + if command == 'getattr': + attrname = request[1] + if callable(client_object.__getattribute__(attrname)): + parent_pipe.send(('function',)) + else: + parent_pipe.send(('data', client_object.__getattribute__(attrname))) + + if command == 'setattr': + attrname = request[1] + value = request[2] + setattr(client_object, attrname, value) + + return True def string_to_delta(interval): """Parse a string and return a datetime.timedelta - - >>> string_to_delta('7d') + + >>> string_to_delta(u'7d') datetime.timedelta(7) - >>> string_to_delta('60s') + >>> string_to_delta(u'60s') datetime.timedelta(0, 60) - >>> string_to_delta('60m') + >>> string_to_delta(u'60m') datetime.timedelta(0, 3600) - >>> string_to_delta('24h') + >>> string_to_delta(u'24h') datetime.timedelta(1) >>> string_to_delta(u'1w') datetime.timedelta(7) + >>> string_to_delta(u'5m 30s') + datetime.timedelta(0, 330) """ - try: - suffix=unicode(interval[-1]) - value=int(interval[:-1]) - if suffix == u"d": - delta = datetime.timedelta(value) - elif suffix == u"s": - delta = datetime.timedelta(0, value) - elif suffix == u"m": - delta = datetime.timedelta(0, 0, 0, 0, value) - elif suffix == u"h": - delta = datetime.timedelta(0, 0, 0, 0, 0, value) - elif suffix == u"w": - delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) - else: - raise ValueError - except (ValueError, IndexError): - raise ValueError - return delta - - -def server_state_changed(state): - """Derived from the Avahi example code""" - if state == avahi.SERVER_COLLISION: - logger.error(u"Server name collision") - service.remove() - elif state == avahi.SERVER_RUNNING: - service.add() - - -def entry_group_state_changed(state, error): - """Derived from the Avahi example code""" - logger.debug(u"state change: %i", state) - - if state == avahi.ENTRY_GROUP_ESTABLISHED: - logger.debug(u"Service established.") - elif state == avahi.ENTRY_GROUP_COLLISION: - logger.warning(u"Service name collision.") - service.rename() - elif state == avahi.ENTRY_GROUP_FAILURE: - logger.critical(u"Error in group state changed %s", - unicode(error)) - raise AvahiGroupError("State changed: %s", str(error)) + timevalue = datetime.timedelta(0) + for s in interval.split(): + try: + suffix = unicode(s[-1]) + value = int(s[:-1]) + if suffix == u"d": + delta = datetime.timedelta(value) + elif suffix == u"s": + delta = datetime.timedelta(0, value) + elif suffix == u"m": + delta = datetime.timedelta(0, 0, 0, 0, value) + elif suffix == u"h": + delta = datetime.timedelta(0, 0, 0, 0, 0, value) + elif suffix == u"w": + delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) + else: + raise ValueError(u"Unknown suffix %r" % suffix) + except (ValueError, IndexError), e: + raise ValueError(e.message) + timevalue += delta + return timevalue + def if_nametoindex(interface): - """Call the C function if_nametoindex(), or equivalent""" + """Call the C function if_nametoindex(), or equivalent + + Note: This function cannot accept a unicode string.""" global if_nametoindex try: - if "ctypes.util" not in sys.modules: - import ctypes.util - if_nametoindex = ctypes.cdll.LoadLibrary\ - (ctypes.util.find_library("c")).if_nametoindex + if_nametoindex = (ctypes.cdll.LoadLibrary + (ctypes.util.find_library(u"c")) + .if_nametoindex) except (OSError, AttributeError): - if "struct" not in sys.modules: - import struct - if "fcntl" not in sys.modules: - import fcntl + logger.warning(u"Doing if_nametoindex the hard way") def if_nametoindex(interface): "Get an interface index the hard way, i.e. using fcntl()" SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h - s = socket.socket() - ifreq = fcntl.ioctl(s, SIOCGIFINDEX, - struct.pack("16s16x", interface)) - s.close() - interface_index = struct.unpack("I", ifreq[16:20])[0] + with contextlib.closing(socket.socket()) as s: + ifreq = fcntl.ioctl(s, SIOCGIFINDEX, + struct.pack(str(u"16s16x"), + interface)) + interface_index = struct.unpack(str(u"I"), + ifreq[16:20])[0] return interface_index return if_nametoindex(interface) def daemon(nochdir = False, noclose = False): """See daemon(3). Standard BSD Unix function. + This should really exist as os.daemon, but it doesn't (yet).""" if os.fork(): sys.exit() os.setsid() if not nochdir: - os.chdir("/") + os.chdir(u"/") if os.fork(): sys.exit() if not noclose: @@ -647,7 +1658,8 @@ null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR) if not stat.S_ISCHR(os.fstat(null).st_mode): raise OSError(errno.ENODEV, - "/dev/null not a character device") + u"%s not a character device" + % os.path.devnull) os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) os.dup2(null, sys.stderr.fileno()) @@ -656,30 +1668,38 @@ def main(): - global main_loop_started - main_loop_started = False - - parser = OptionParser(version = "%%prog %s" % version) - parser.add_option("-i", "--interface", type="string", - metavar="IF", help="Bind to interface IF") - parser.add_option("-a", "--address", type="string", - help="Address to listen for requests on") - parser.add_option("-p", "--port", type="int", - help="Port number to receive requests on") - parser.add_option("--check", action="store_true", default=False, - help="Run self-test") - parser.add_option("--debug", action="store_true", - help="Debug mode; run in foreground and log to" - " terminal") - parser.add_option("--priority", type="string", help="GnuTLS" - " priority string (see GnuTLS documentation)") - parser.add_option("--servicename", type="string", metavar="NAME", - help="Zeroconf service name") - parser.add_option("--configdir", type="string", - default="/etc/mandos", metavar="DIR", - help="Directory to search for configuration" - " files") - (options, args) = parser.parse_args() + + ################################################################## + # Parsing of options, both command line and config file + + parser = optparse.OptionParser(version = "%%prog %s" % version) + parser.add_option("-i", u"--interface", type=u"string", + metavar="IF", help=u"Bind to interface IF") + parser.add_option("-a", u"--address", type=u"string", + help=u"Address to listen for requests on") + parser.add_option("-p", u"--port", type=u"int", + help=u"Port number to receive requests on") + parser.add_option("--check", action=u"store_true", + help=u"Run self-test") + parser.add_option("--debug", action=u"store_true", + help=u"Debug mode; run in foreground and log to" + u" terminal") + parser.add_option("--debuglevel", type=u"string", metavar="Level", + help=u"Debug level for stdout output") + parser.add_option("--priority", type=u"string", help=u"GnuTLS" + u" priority string (see GnuTLS documentation)") + parser.add_option("--servicename", type=u"string", + metavar=u"NAME", help=u"Zeroconf service name") + parser.add_option("--configdir", type=u"string", + default=u"/etc/mandos", metavar=u"DIR", + help=u"Directory to search for configuration" + u" files") + parser.add_option("--no-dbus", action=u"store_false", + dest=u"use_dbus", help=u"Do not provide D-Bus" + u" system bus interface") + parser.add_option("--no-ipv6", action=u"store_false", + dest=u"use_ipv6", help=u"Do not use IPv6") + options = parser.parse_args()[0] if options.check: import doctest @@ -687,166 +1707,341 @@ sys.exit() # Default values for config file for server-global settings - server_defaults = { "interface": "", - "address": "", - "port": "", - "debug": "False", - "priority": - "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP", - "servicename": "Mandos", + server_defaults = { u"interface": u"", + u"address": u"", + u"port": u"", + u"debug": u"False", + u"priority": + u"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP", + u"servicename": u"Mandos", + u"use_dbus": u"True", + u"use_ipv6": u"True", + u"debuglevel": u"", } # Parse config file for server-global settings - server_config = ConfigParser.SafeConfigParser(server_defaults) + server_config = configparser.SafeConfigParser(server_defaults) del server_defaults - server_config.read(os.path.join(options.configdir, "mandos.conf")) + server_config.read(os.path.join(options.configdir, + u"mandos.conf")) # Convert the SafeConfigParser object to a dict server_settings = server_config.defaults() - # Use getboolean on the boolean config option - server_settings["debug"] = server_config.getboolean\ - ("DEFAULT", "debug") + # Use the appropriate methods on the non-string config options + for option in (u"debug", u"use_dbus", u"use_ipv6"): + server_settings[option] = server_config.getboolean(u"DEFAULT", + option) + if server_settings["port"]: + server_settings["port"] = server_config.getint(u"DEFAULT", + u"port") del server_config # Override the settings from the config file with command line # options, if set. - for option in ("interface", "address", "port", "debug", - "priority", "servicename", "configdir"): + for option in (u"interface", u"address", u"port", u"debug", + u"priority", u"servicename", u"configdir", + u"use_dbus", u"use_ipv6", u"debuglevel"): value = getattr(options, option) if value is not None: server_settings[option] = value del options + # Force all strings to be unicode + for option in server_settings.keys(): + if type(server_settings[option]) is str: + server_settings[option] = unicode(server_settings[option]) # Now we have our good server settings in "server_settings" - debug = server_settings["debug"] - + ################################################################## + + # For convenience + debug = server_settings[u"debug"] + debuglevel = server_settings[u"debuglevel"] + use_dbus = server_settings[u"use_dbus"] + use_ipv6 = server_settings[u"use_ipv6"] + + if server_settings[u"servicename"] != u"Mandos": + syslogger.setFormatter(logging.Formatter + (u'Mandos (%s) [%%(process)d]:' + u' %%(levelname)s: %%(message)s' + % server_settings[u"servicename"])) + + # Parse config file with clients + client_defaults = { u"timeout": u"1h", + u"interval": u"5m", + u"checker": u"fping -q -- %%(host)s", + u"host": u"", + u"approval_delay": u"0s", + u"approval_duration": u"1s", + } + client_config = configparser.SafeConfigParser(client_defaults) + client_config.read(os.path.join(server_settings[u"configdir"], + u"clients.conf")) + + global mandos_dbus_service + mandos_dbus_service = None + + tcp_server = MandosServer((server_settings[u"address"], + server_settings[u"port"]), + ClientHandler, + interface=(server_settings[u"interface"] + or None), + use_ipv6=use_ipv6, + gnutls_priority= + server_settings[u"priority"], + use_dbus=use_dbus) if not debug: + pidfilename = u"/var/run/mandos.pid" + try: + pidfile = open(pidfilename, u"w") + except IOError: + logger.error(u"Could not open file %r", pidfilename) + + try: + uid = pwd.getpwnam(u"_mandos").pw_uid + gid = pwd.getpwnam(u"_mandos").pw_gid + except KeyError: + try: + uid = pwd.getpwnam(u"mandos").pw_uid + gid = pwd.getpwnam(u"mandos").pw_gid + except KeyError: + try: + uid = pwd.getpwnam(u"nobody").pw_uid + gid = pwd.getpwnam(u"nobody").pw_gid + except KeyError: + uid = 65534 + gid = 65534 + try: + os.setgid(gid) + os.setuid(uid) + except OSError, error: + if error[0] != errno.EPERM: + raise error + + if not debug and not debuglevel: syslogger.setLevel(logging.WARNING) console.setLevel(logging.WARNING) - - if server_settings["servicename"] != "Mandos": - syslogger.setFormatter(logging.Formatter\ - ('Mandos (%s): %%(levelname)s:' - ' %%(message)s' - % server_settings["servicename"])) - - # Parse config file with clients - client_defaults = { "timeout": "1h", - "interval": "5m", - "checker": "fping -q -- %%(host)s", - } - client_config = ConfigParser.SafeConfigParser(client_defaults) - client_config.read(os.path.join(server_settings["configdir"], - "clients.conf")) - - global service - service = AvahiService(name = server_settings["servicename"], - type = "_mandos._tcp", ); - if server_settings["interface"]: - service.interface = if_nametoindex(server_settings["interface"]) + if debuglevel: + level = getattr(logging, debuglevel.upper()) + syslogger.setLevel(level) + console.setLevel(level) + + if debug: + # Enable all possible GnuTLS debugging + + # "Use a log level over 10 to enable all debugging options." + # - GnuTLS manual + gnutls.library.functions.gnutls_global_set_log_level(11) + + @gnutls.library.types.gnutls_log_func + def debug_gnutls(level, string): + logger.debug(u"GnuTLS: %s", string[:-1]) + + (gnutls.library.functions + .gnutls_global_set_log_function(debug_gnutls)) + + # Redirect stdin so all checkers get /dev/null + null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR) + os.dup2(null, sys.stdin.fileno()) + if null > 2: + os.close(null) + else: + # No console logging + logger.removeHandler(console) + global main_loop - global bus - global server # From the Avahi example code DBusGMainLoop(set_as_default=True ) main_loop = gobject.MainLoop() bus = dbus.SystemBus() - server = dbus.Interface( - bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ), - avahi.DBUS_INTERFACE_SERVER ) # End of Avahi example code - - clients = Set() - def remove_from_clients(client): - clients.remove(client) - if not clients: - logger.critical(u"No clients left, exiting") - sys.exit() - - clients.update(Set(Client(name = section, - stop_hook = remove_from_clients, - config - = dict(client_config.items(section))) - for section in client_config.sections())) - if not clients: - logger.critical(u"No clients defined") - sys.exit(1) - + if use_dbus: + try: + bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", + bus, do_not_queue=True) + except dbus.exceptions.NameExistsException, e: + logger.error(unicode(e) + u", disabling D-Bus") + use_dbus = False + server_settings[u"use_dbus"] = False + tcp_server.use_dbus = False + protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET + service = AvahiService(name = server_settings[u"servicename"], + servicetype = u"_mandos._tcp", + protocol = protocol, bus = bus) + if server_settings["interface"]: + service.interface = (if_nametoindex + (str(server_settings[u"interface"]))) + if not debug: - logger.removeHandler(console) + # Close all input and output, do double fork, etc. daemon() - - pidfilename = "/var/run/mandos/mandos.pid" - pid = os.getpid() - try: - pidfile = open(pidfilename, "w") - pidfile.write(str(pid) + "\n") - pidfile.close() - del pidfile - except IOError, err: - logger.error(u"Could not write %s file with PID %d", - pidfilename, os.getpid()) + + global multiprocessing_manager + multiprocessing_manager = multiprocessing.Manager() + + client_class = Client + if use_dbus: + client_class = functools.partial(ClientDBus, bus = bus) + def client_config_items(config, section): + special_settings = { + "approved_by_default": + lambda: config.getboolean(section, + "approved_by_default"), + } + for name, value in config.items(section): + try: + yield (name, special_settings[name]()) + except KeyError: + yield (name, value) + + tcp_server.clients.update(set( + client_class(name = section, + config= dict(client_config_items( + client_config, section))) + for section in client_config.sections())) + if not tcp_server.clients: + logger.warning(u"No clients defined") + + if not debug: + try: + with pidfile: + pid = os.getpid() + pidfile.write(str(pid) + "\n") + del pidfile + except IOError: + logger.error(u"Could not write to file %r with PID %d", + pidfilename, pid) + except NameError: + # "pidfile" was never created + pass + del pidfilename + + signal.signal(signal.SIGINT, signal.SIG_IGN) + + signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit()) + signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit()) + + if use_dbus: + class MandosDBusService(dbus.service.Object): + """A D-Bus proxy object""" + def __init__(self): + dbus.service.Object.__init__(self, bus, u"/") + _interface = u"se.bsnet.fukt.Mandos" + + @dbus.service.signal(_interface, signature=u"o") + def ClientAdded(self, objpath): + "D-Bus signal" + pass + + @dbus.service.signal(_interface, signature=u"ss") + def ClientNotFound(self, fingerprint, address): + "D-Bus signal" + pass + + @dbus.service.signal(_interface, signature=u"os") + def ClientRemoved(self, objpath, name): + "D-Bus signal" + pass + + @dbus.service.method(_interface, out_signature=u"ao") + def GetAllClients(self): + "D-Bus method" + return dbus.Array(c.dbus_object_path + for c in tcp_server.clients) + + @dbus.service.method(_interface, + out_signature=u"a{oa{sv}}") + def GetAllClientsWithProperties(self): + "D-Bus method" + return dbus.Dictionary( + ((c.dbus_object_path, c.GetAll(u"")) + for c in tcp_server.clients), + signature=u"oa{sv}") + + @dbus.service.method(_interface, in_signature=u"o") + def RemoveClient(self, object_path): + "D-Bus method" + for c in tcp_server.clients: + if c.dbus_object_path == object_path: + tcp_server.clients.remove(c) + c.remove_from_connection() + # Don't signal anything except ClientRemoved + c.disable(quiet=True) + # Emit D-Bus signal + self.ClientRemoved(object_path, c.name) + return + raise KeyError(object_path) + + del _interface + + mandos_dbus_service = MandosDBusService() def cleanup(): "Cleanup function; run on exit" - global group - # From the Avahi example code - if not group is None: - group.Free() - group = None - # End of Avahi example code + service.cleanup() - while clients: - client = clients.pop() - client.stop_hook = None - client.stop() + while tcp_server.clients: + client = tcp_server.clients.pop() + if use_dbus: + client.remove_from_connection() + client.disable_hook = None + # Don't signal anything except ClientRemoved + client.disable(quiet=True) + if use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientRemoved(client.dbus_object_path, + client.name) atexit.register(cleanup) - if not debug: - signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit()) - signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit()) - - for client in clients: - client.start() - - tcp_server = IPv6_TCPServer((server_settings["address"], - server_settings["port"]), - tcp_handler, - settings=server_settings, - clients=clients) + for client in tcp_server.clients: + if use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientAdded(client.dbus_object_path) + client.enable() + + tcp_server.enable() + tcp_server.server_activate() + # Find out what port we got service.port = tcp_server.socket.getsockname()[1] - logger.info(u"Now listening on address %r, port %d, flowinfo %d," - u" scope_id %d" % tcp_server.socket.getsockname()) + if use_ipv6: + logger.info(u"Now listening on address %r, port %d," + " flowinfo %d, scope_id %d" + % tcp_server.socket.getsockname()) + else: # IPv4 + logger.info(u"Now listening on address %r, port %d" + % tcp_server.socket.getsockname()) #service.interface = tcp_server.socket.getsockname()[3] try: # From the Avahi example code - server.connect_to_signal("StateChanged", server_state_changed) try: - server_state_changed(server.GetState()) + service.activate() except dbus.exceptions.DBusException, error: logger.critical(u"DBusException: %s", error) + cleanup() sys.exit(1) # End of Avahi example code gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN, lambda *args, **kwargs: - tcp_server.handle_request\ - (*args[2:], **kwargs) or True) + (tcp_server.handle_request + (*args[2:], **kwargs) or True)) logger.debug(u"Starting main loop") - main_loop_started = True main_loop.run() except AvahiError, error: - logger.critical(u"AvahiError: %s" + unicode(error)) + logger.critical(u"AvahiError: %s", error) + cleanup() sys.exit(1) except KeyboardInterrupt: if debug: - print + print >> sys.stderr + logger.debug(u"Server received KeyboardInterrupt") + logger.debug(u"Server exiting") + # Must run before the D-Bus bus name gets deregistered + cleanup() if __name__ == '__main__': main() === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2008-08-09 01:39:09 +0000 +++ mandos-clients.conf.xml 2010-09-26 18:32:58 +0000 @@ -1,19 +1,20 @@ - - + /etc/mandos/clients.conf"> + + +%common; ]> - + - &CONFNAME; - - &CONFNAME; - &VERSION; + Mandos Manual + + Mandos + &version; + &TIMESTAMP; Björn @@ -32,33 +33,14 @@ 2008 - Teddy Hogeborn & Björn Påhlsson + 2009 + 2010 + Teddy Hogeborn + Björn Påhlsson - - - This manual page 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 manual page 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 - . - - + - + &CONFNAME; 5 @@ -67,146 +49,355 @@ &CONFNAME; - Configuration file for Mandos clients + Configuration file for the Mandos server - + - - &CONFPATH; - + &CONFPATH; - + DESCRIPTION - The file &CONFPATH; is the configuration file for mandos where - each client that will be abel to use the service need to be - specified. The configuration file is looked on at the startup of - the service, so to reenable timedout clients one need to only - restart the server. The format starts with a section under [] - which is eather [DEFAULT] or a client - name. Values is set through the use of VAR = VALUE pair. Values - may not be empty. + The file &CONFPATH; is a configuration file for 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 enabled, + even if a client was disabled in a previous run of the server. + + + The format starts with a [section + header] which is either + [DEFAULT] or [client + name]. The client + name can be anything, and is not tied to a host + name. Following the section header is any number of + option=value entries, + with continuations in the style of RFC 822. option: value is also accepted. Note that + leading whitespace is removed from values. Values can contain + format strings which refer to other values in the same section, + or values in the DEFAULT section (see ). Lines beginning with # + or ; are ignored and may be used to provide + comments. - - - DEFAULTS - - The paramters for [DEFAULT] are: - - + + + OPTIONS + + Note: all option values are subject to + start time expansion, see . + + + Unknown options are ignored. The used options are as follows: + + - timeout - - - This option allows you to override the default timeout - that clients will get. By default mandos will use 1hr. - - - - - - interval - - - This option allows you to override the default interval - used between checkups for disconnected clients. By default - mandos will use 5m. - - - - - - checker - + + + + This option is optional. + + + How long to wait for external approval before resorting to + use the value. The + default is 0s, i.e. not to wait. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + This option is optional. + + + How long an external approval lasts. The default is 1 + second. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + Whether to approve a client by default after + the . The default + is True. + + + + + + + + + This option is optional. + This option allows you to override the default shell - command that the server will use to check up if the client - is still up. By default mandos will "fping -q -- %%(host)s" - - - - - - - - - CLIENTS - - The paramters for clients are: - - - - - - fingerprint - - - This option sets the openpgp fingerprint that identifies - the public certificate that clients authenticates themself - through gnutls. The string need to be in hex-decimal form. - - - - - - secret - - - Base 64 encoded OpenPGP encrypted password encrypted by - the clients openpgp certificate. - - - - - - secfile - - - Base 64 encoded OpenPGP encrypted password encrypted by - the clients openpgp certificate as a binary file. - - - - - - host - - - Host name that can be used in for checking that the client is up. - - - - - - checker - - - Shell command that the server will use to check up if a - client is still up. - - - - - - timeout - - - Duration that a client can be down whitout be removed from - the client list. - - - - - - - - - EXAMPLES + command that the server will use to check if the client is + still up. Any output of the command will be ignored, only + the exit code is checked: If the exit code of the command + is zero, the client is considered up. The command will be + run using /bin/sh + , so + PATH will be searched. The default + value for the checker command is fping %%(host)s. + + + In addition to normal start time expansion, this option + will also be subject to runtime expansion; see . + + + + + + + + + This option is required. + + + This option sets the OpenPGP fingerprint that identifies + the public key that clients authenticate themselves with + through TLS. The string needs to be in hexidecimal form, + but spaces or upper/lower case are not significant. + + + + + + + + + This option is optional, but highly + recommended unless the + option is modified to a + non-standard value without %%(host)s in it. + + + Host name for this client. This is not used by the server + directly, but can be, and is by default, used by the + checker. See the option. + + + + + + + + + This option is optional. + + + 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 below + timeout occurs, at which + time the client will be disabled, and any running checker + killed. The default interval is 5 minutes. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + This option is only used if is not + specified, in which case this option is + required. + + + Similar to the , except the secret + data is in an external file. The contents of the file + should not be base64-encoded, but + will be sent to clients verbatim. + + + File names of the form ~user/foo/bar + and $ENVVAR/foo/bar + are supported. + + + + + + + + + If this option is not specified, the option is required + to be present. + + + If present, this option must be set to a string of + base64-encoded binary data. It will be decoded and sent + to the client matching the above + . This should, of course, be + OpenPGP encrypted data, decryptable only by the client. + The program mandos-keygen8 can, using its + option, be used to generate + this, if desired. + + + Note: this value of this option will probably be very + long. A useful feature to avoid having unreadably-long + lines is that a line beginning with white space adds to + the value of the previous line, RFC 822-style. + + + + + + + + + This option is optional. + + + 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 disabled and not allowed to get the data + this server holds. By default Mandos will use 1 hour. + + + 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. + + + + + + + + + EXPANSION + + There are two forms of expansion: Start time expansion and + runtime expansion. + + + START TIME EXPANSION + + Any string in an option value of the form + %(foo)s will be replaced by the value of the option + foo either in the same section, or, if it + does not exist there, the [DEFAULT] + section. This is done at start time, when the configuration + file is read. + + + Note that this means that, in order to include an actual + percent character (%) in an option value, two + percent characters in a row (%%) must be + entered. + + + + RUNTIME EXPANSION + + This is currently only done for the checker + option. + + + Any string in an option value of the form + %%(foo)s will be replaced by the value of the attribute + foo of the internal + Client object. See the + source code for details, and let the authors know of any + attributes that are useful so they may be preserved to any new + versions of this software. + + + Note that this means that, in order to include an actual + percent character (%) in a + checker option, four + percent characters in a row (%%%%) must be + entered. Also, a bad format here will lead to an immediate + but silent run-time fatal exit; debug + mode is needed to expose an error of this kind. + + + + + + + FILES + + The file described here is &CONFPATH; + + + + + BUGS + + The format for specifying times for timeout + and interval is not very good. + + + The difference between + %%(foo)s and + %(foo)s is + obscure. + + + + + EXAMPLE [DEFAULT] @@ -214,9 +405,9 @@ interval = 5m checker = fping -q -- %%(host)s -[example_client] +# Client "foo" +[foo] fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920 - secret = hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234 REJMVv7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+N @@ -233,18 +424,34 @@ 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O QlnHIvPzEArRQLo= - =iHhv +host = foo.example.org +interval = 1m -host = localhost -interval = 5m +# Client "bar" +[bar] +fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 +secfile = /etc/mandos/bar-secret +timeout = 15m +approved_by_default = False +approval_delay = 30s - + - - FILES + + SEE ALSO - The file described here is &CONFPATH; + mandos-keygen + 8, + mandos.conf + 5, + mandos + 8 + + + + + === added file 'mandos-ctl' --- mandos-ctl 1970-01-01 00:00:00 +0000 +++ mandos-ctl 2010-09-26 18:32:58 +0000 @@ -0,0 +1,337 @@ +#!/usr/bin/python +# -*- mode: python; coding: utf-8 -*- +# +# Mandos Monitor - Control and monitor the Mandos server +# +# Copyright © 2008-2010 Teddy Hogeborn +# Copyright © 2008-2010 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 . +# + +from __future__ import division +import sys +import dbus +from optparse import OptionParser +import locale +import datetime +import re +import os + +locale.setlocale(locale.LC_ALL, u'') + +tablewords = { + 'Name': u'Name', + 'Enabled': u'Enabled', + 'Timeout': u'Timeout', + 'LastCheckedOK': u'Last Successful Check', + 'LastApprovalRequest': u'Last Approval Request', + 'Created': u'Created', + 'Interval': u'Interval', + 'Host': u'Host', + 'Fingerprint': u'Fingerprint', + 'CheckerRunning': u'Check Is Running', + 'LastEnabled': u'Last Enabled', + 'ApprovalPending': u'Approval Is Pending', + 'ApprovedByDefault': u'Approved By Default', + 'ApprovalDelay': u"Approval Delay", + 'ApprovalDuration': u"Approval Duration", + 'Checker': u'Checker', + } +defaultkeywords = ('Name', 'Enabled', 'Timeout', 'LastCheckedOK') +domain = 'se.bsnet.fukt' +busname = domain + '.Mandos' +server_path = '/' +server_interface = domain + '.Mandos' +client_interface = domain + '.Mandos.Client' +version = "1.0.14" + +def timedelta_to_milliseconds(td): + "Convert a datetime.timedelta object to milliseconds" + return ((td.days * 24 * 60 * 60 * 1000) + + (td.seconds * 1000) + + (td.microseconds // 1000)) + +def milliseconds_to_string(ms): + td = datetime.timedelta(0, 0, 0, ms) + return (u"%(days)s%(hours)02d:%(minutes)02d:%(seconds)02d" + % { "days": "%dT" % td.days if td.days else "", + "hours": td.seconds // 3600, + "minutes": (td.seconds % 3600) // 60, + "seconds": td.seconds % 60, + }) + + +def string_to_delta(interval): + """Parse a string and return a datetime.timedelta + + >>> string_to_delta('7d') + datetime.timedelta(7) + >>> string_to_delta('60s') + datetime.timedelta(0, 60) + >>> string_to_delta('60m') + datetime.timedelta(0, 3600) + >>> string_to_delta('24h') + datetime.timedelta(1) + >>> string_to_delta(u'1w') + datetime.timedelta(7) + >>> string_to_delta('5m 30s') + datetime.timedelta(0, 330) + """ + timevalue = datetime.timedelta(0) + regexp = re.compile("\d+[dsmhw]") + + for s in regexp.findall(interval): + try: + suffix = unicode(s[-1]) + value = int(s[:-1]) + if suffix == u"d": + delta = datetime.timedelta(value) + elif suffix == u"s": + delta = datetime.timedelta(0, value) + elif suffix == u"m": + delta = datetime.timedelta(0, 0, 0, 0, value) + elif suffix == u"h": + delta = datetime.timedelta(0, 0, 0, 0, 0, value) + elif suffix == u"w": + delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) + else: + raise ValueError + except (ValueError, IndexError): + raise ValueError + timevalue += delta + return timevalue + +def print_clients(clients, keywords): + def valuetostring(value, keyword): + if type(value) is dbus.Boolean: + return u"Yes" if value else u"No" + if keyword in (u"Timeout", u"Interval", u"ApprovalDelay", + u"ApprovalDuration"): + return milliseconds_to_string(value) + return unicode(value) + + # Create format string to print table rows + format_string = u' '.join(u'%%-%ds' % + max(len(tablewords[key]), + max(len(valuetostring(client[key], + key)) + for client in + clients)) + for key in keywords) + # Print header line + print format_string % tuple(tablewords[key] for key in keywords) + for client in clients: + print format_string % tuple(valuetostring(client[key], key) + for key in keywords) + +def has_actions(options): + return any((options.enable, + options.disable, + options.bump_timeout, + options.start_checker, + options.stop_checker, + options.is_enabled, + options.remove, + options.checker is not None, + options.timeout is not None, + options.interval is not None, + options.approved_by_default is not None, + options.approval_delay is not None, + options.approval_duration is not None, + options.host is not None, + options.secret is not None, + options.approve, + options.deny)) + +def main(): + parser = OptionParser(version = "%%prog %s" % version) + parser.add_option("-a", "--all", action="store_true", + help="Select all clients") + parser.add_option("-v", "--verbose", action="store_true", + help="Print all fields") + parser.add_option("-e", "--enable", action="store_true", + help="Enable client") + parser.add_option("-d", "--disable", action="store_true", + help="disable client") + parser.add_option("-b", "--bump-timeout", action="store_true", + help="Bump timeout for client") + parser.add_option("--start-checker", action="store_true", + help="Start checker for client") + parser.add_option("--stop-checker", action="store_true", + help="Stop checker for client") + 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", + help="Set checker command for client") + parser.add_option("-t", "--timeout", type="string", + help="Set timeout for client") + parser.add_option("-i", "--interval", type="string", + help="Set checker interval for client") + parser.add_option("--approve-by-default", action="store_true", + dest=u"approved_by_default", + help="Set client to be approved by default") + parser.add_option("--deny-by-default", action="store_false", + dest=u"approved_by_default", + help="Set client to be denied by default") + parser.add_option("--approval-delay", type="string", + help="Set delay before client approve/deny") + parser.add_option("--approval-duration", type="string", + help="Set duration of one client approval") + parser.add_option("-H", "--host", type="string", + help="Set host for client") + parser.add_option("-s", "--secret", type="string", + help="Set password blob (file) for client") + parser.add_option("-A", "--approve", action="store_true", + help="Approve any current client request") + parser.add_option("-D", "--deny", action="store_true", + help="Deny any current client request") + options, client_names = parser.parse_args() + + if has_actions(options) and not client_names and not options.all: + parser.error('Options require clients names or --all.') + if options.verbose and has_actions(options): + parser.error('--verbose can only be used alone or with' + ' --all.') + if options.all and not has_actions(options): + parser.error('--all requires an action.') + + try: + bus = dbus.SystemBus() + mandos_dbus_objc = bus.get_object(busname, server_path) + except dbus.exceptions.DBusException: + print >> sys.stderr, "Could not connect to Mandos server" + sys.exit(1) + + mandos_serv = dbus.Interface(mandos_dbus_objc, + dbus_interface = server_interface) + + #block stderr since dbus library prints to stderr + null = os.open(os.path.devnull, os.O_RDWR) + stderrcopy = os.dup(sys.stderr.fileno()) + os.dup2(null, sys.stderr.fileno()) + os.close(null) + try: + try: + mandos_clients = mandos_serv.GetAllClientsWithProperties() + finally: + #restore stderr + os.dup2(stderrcopy, sys.stderr.fileno()) + os.close(stderrcopy) + except dbus.exceptions.DBusException, e: + print >> sys.stderr, "Access denied: Accessing mandos server through dbus." + sys.exit(1) + + # Compile dict of (clients: properties) to process + clients={} + + if options.all or not client_names: + clients = dict((bus.get_object(busname, path), properties) + for path, properties in + mandos_clients.iteritems()) + else: + for name in client_names: + for path, client in mandos_clients.iteritems(): + if client['Name'] == name: + client_objc = bus.get_object(busname, path) + clients[client_objc] = client + break + else: + print >> sys.stderr, "Client not found on server: %r" % name + sys.exit(1) + + if not has_actions(options) and clients: + if options.verbose: + keywords = ('Name', 'Enabled', 'Timeout', + 'LastCheckedOK', 'Created', 'Interval', + 'Host', 'Fingerprint', 'CheckerRunning', + 'LastEnabled', 'ApprovalPending', + 'ApprovedByDefault', + 'LastApprovalRequest', 'ApprovalDelay', + 'ApprovalDuration', 'Checker') + else: + keywords = defaultkeywords + + print_clients(clients.values(), keywords) + else: + # Process each client in the list by all selected options + for client in clients: + if options.remove: + mandos_serv.RemoveClient(client.__dbus_object_path__) + if options.enable: + client.Enable(dbus_interface=client_interface) + if options.disable: + client.Disable(dbus_interface=client_interface) + if options.bump_timeout: + client.CheckedOK(dbus_interface=client_interface) + if options.start_checker: + client.StartChecker(dbus_interface=client_interface) + if options.stop_checker: + client.StopChecker(dbus_interface=client_interface) + if options.is_enabled: + sys.exit(0 if client.Get(client_interface, + u"Enabled", + dbus_interface=dbus.PROPERTIES_IFACE) + else 1) + if options.checker: + client.Set(client_interface, u"Checker", options.checker, + dbus_interface=dbus.PROPERTIES_IFACE) + if options.host: + client.Set(client_interface, u"Host", options.host, + dbus_interface=dbus.PROPERTIES_IFACE) + if options.interval: + client.Set(client_interface, u"Interval", + timedelta_to_milliseconds + (string_to_delta(options.interval)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approval_delay: + client.Set(client_interface, u"ApprovalDelay", + timedelta_to_milliseconds + (string_to_delta(options. + approval_delay)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approval_duration: + client.Set(client_interface, u"ApprovalDuration", + timedelta_to_milliseconds + (string_to_delta(options. + approval_duration)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.timeout: + client.Set(client_interface, u"Timeout", + timedelta_to_milliseconds + (string_to_delta(options.timeout)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.secret: + client.Set(client_interface, u"Secret", + dbus.ByteArray(open(options.secret, + u'rb').read()), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approved_by_default is not None: + client.Set(client_interface, u"ApprovedByDefault", + dbus.Boolean(options + .approved_by_default), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approve: + client.Approve(dbus.Boolean(True), + dbus_interface=client_interface) + elif options.deny: + client.Approve(dbus.Boolean(False), + dbus_interface=client_interface) + +if __name__ == '__main__': + main() === added file 'mandos-ctl.xml' --- mandos-ctl.xml 1970-01-01 00:00:00 +0000 +++ mandos-ctl.xml 2010-09-25 23:52:17 +0000 @@ -0,0 +1,570 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2010 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Control the operation of the Mandos server + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CLIENT + + + + + &COMMANDNAME; + + + + + + + CLIENT + + + + + &COMMANDNAME; + + + + + CLIENT + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + &COMMANDNAME; is a program to control the + operation of the Mandos server mandos8. + + + This program can be used to change client settings, approve or + deny client requests, and to remove clients from the server. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + + Show a help message and exit + + + + + + + + + + Enable client(s). An enabled client will be eligble to + receive its secret. + + + + + + + + + + Disable client(s). A disabled client will not be eligble + to receive its secret, and no checkers will be started for + it. + + + + + + + + + Bump the timeout of the specified client(s), just as if a + checker had completed successfully for it/them. + + + + + + + + + Start a new checker now for the specified client(s). + + + + + + + + + Stop any running checker for the specified client(s). + + + + + + + + + + Remove the specified client(s) from the server. + + + + + + + + + + Set the checker option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the timeout option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the interval option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + + Set the approved_by_default option of + the specified client(s) to True or + False, respectively; see + mandos-clients.conf5. + + + + + + + + + Set the approval_delay option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + Set the approval_duration option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + + Set the host option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the secfile option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Approve client(s) if currently waiting for approval. + + + + + + + + + + Deny client(s) if currently waiting for approval. + + + + + + + + + + Make the client-modifying options modify all clients. + + + + + + + + + + Show all client settings, not just a subset. + + + + + + + + + + Check if a single client is enabled or not, and exit with + a successful exit status only if the client is enabled. + + + + + + + + + OVERVIEW + + + This program is a small utility to generate new OpenPGP keys for + new Mandos clients, and to generate sections for inclusion in + clients.conf on the server. + + + + + EXIT STATUS + + If the option is used, the exit + status will be 0 only if the specified client is enabled. + + + + + + + + + + + EXAMPLE + + + To list all clients: + + + &COMMANDNAME; + + + + + + To list all settings for the clients + named foo1.example.org and foo2.example.org: + + + + +&COMMANDNAME; --verbose foo1.example.org foo2.example.org + + + + + + + To enable all clients: + + + &COMMANDNAME; --enable --all + + + + + + To change timeout and interval value for the clients + named foo1.example.org and foo2.example.org: + + + + +&COMMANDNAME; --timeout="5m" --interval="1m" foo1.example.org foo2.example.org + + + + + + + To approve all clients currently waiting for it: + + + &COMMANDNAME; --approve --all + + + + + + SECURITY + + This program must be permitted to access the Mandos server via + the D-Bus interface. This normally requires the root user, but + could be configured otherwise by reconfiguring the D-Bus server. + + + + + SEE ALSO + + mandos + 8, + mandos-clients.conf + 5, + mandos-monitor + 8 + + + +
+ + + + + === modified file 'mandos-keygen' --- mandos-keygen 2008-08-18 05:24:20 +0000 +++ mandos-keygen 2010-09-26 18:32:58 +0000 @@ -2,7 +2,8 @@ # # Mandos key generator - create a new OpenPGP key for a Mandos client # -# Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson +# Copyright © 2008-2010 Teddy Hogeborn +# Copyright © 2008-2010 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 @@ -20,53 +21,84 @@ # Contact the authors at . # -VERSION="1.0" +VERSION="1.0.14" -KEYDIR="/etc/mandos" +KEYDIR="/etc/keys/mandos" KEYTYPE=DSA -KEYLENGTH=1024 -KEYNAME="`hostname --fqdn`" +KEYLENGTH=2048 +SUBKEYTYPE=ELG-E +SUBKEYLENGTH=2048 +KEYNAME="`hostname --fqdn 2>/dev/null || hostname`" KEYEMAIL="" KEYCOMMENT="Mandos client key" KEYEXPIRE=0 FORCE=no KEYCOMMENT_ORIG="$KEYCOMMENT" +mode=keygen + +if [ ! -d "$KEYDIR" ]; then + KEYDIR="/etc/mandos/keys" +fi # Parse options -TEMP=`getopt --options vhd:t:l:n:e:c:x:f \ - --longoptions version,help,dir:,type:,length:,name:,email:,comment:,expire:,force \ +TEMP=`getopt --options vhpF:d:t:l:s:L:n:e:c:x:f \ + --longoptions version,help,password,passfile:,dir:,type:,length:,subtype:,sublength:,name:,email:,comment:,expire:,force \ --name "$0" -- "$@"` help(){ +basename="`basename $0`" cat <&2 exit 1 fi -if [ -w "$KEYDIR" ]; then :; else - echo "Directory $KEYDIR not writeable" >&2 - exit 1 -fi - -if [ -z "$KEYTYPE" ]; then - echo "Empty key type" >&2 - exit 1 -fi - -if [ -z "$KEYNAME" ]; then - echo "Empty key name" >&2 - exit 1 -fi - -if [ -z "$KEYLENGTH" ] || [ "$KEYLENGTH" -lt 512 ]; then - echo "Invalid key length" >&2 - exit 1 -fi - -if [ -z "$KEYEXPIRE" ]; then - echo "Empty key expiration" >&2 - exit 1 -fi - -# Make FORCE be 0 or 1 -case "$FORCE" in - [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) FORCE=1;; - [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) FORCE=0;; -esac - -if { [ -e "$SECKEYFILE" ] || [ -e "$PUBKEYFILE" ]; } \ - && [ "$FORCE" -eq 0 ]; then - echo "Refusing to overwrite old key files; use --force" >&2 - exit 1 -fi - -# Set lines for GnuPG batch file -if [ -n "$KEYCOMMENT" ]; then - KEYCOMMENTLINE="Name-Comment: $KEYCOMMENT" -fi -if [ -n "$KEYEMAIL" ]; then - KEYEMAILLINE="Name-Email: $KEYEMAIL" -fi - -# Create temp files -BATCHFILE="`mktemp -t mandos-gpg-batch.XXXXXXXXXX`" -SECRING="`mktemp -t mandos-gpg-secring.XXXXXXXXXX`" -PUBRING="`mktemp -t mandos-gpg-pubring.XXXXXXXXXX`" +if [ ! -r "$KEYDIR" ]; then + echo "Directory $KEYDIR not readable" >&2 + exit 1 +fi + +if [ "$mode" = keygen ]; then + if [ ! -w "$KEYDIR" ]; then + echo "Directory $KEYDIR not writeable" >&2 + exit 1 + fi + if [ -z "$KEYTYPE" ]; then + echo "Empty key type" >&2 + exit 1 + fi + + if [ -z "$KEYNAME" ]; then + echo "Empty key name" >&2 + exit 1 + fi + + if [ -z "$KEYLENGTH" ] || [ "$KEYLENGTH" -lt 512 ]; then + echo "Invalid key length" >&2 + exit 1 + fi + + if [ -z "$KEYEXPIRE" ]; then + echo "Empty key expiration" >&2 + exit 1 + fi + + # Make FORCE be 0 or 1 + case "$FORCE" in + [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) FORCE=1;; + [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) FORCE=0;; + esac + + if [ \( -e "$SECKEYFILE" -o -e "$PUBKEYFILE" \) \ + -a "$FORCE" -eq 0 ]; then + echo "Refusing to overwrite old key files; use --force" >&2 + exit 1 + fi + + # Set lines for GnuPG batch file + if [ -n "$KEYCOMMENT" ]; then + KEYCOMMENTLINE="Name-Comment: $KEYCOMMENT" + fi + if [ -n "$KEYEMAIL" ]; then + KEYEMAILLINE="Name-Email: $KEYEMAIL" + fi + + # Create temporary gpg batch file + BATCHFILE="`mktemp -t mandos-keygen-batch.XXXXXXXXXX`" +fi + +if [ "$mode" = password ]; then + # Create temporary encrypted password file + SECFILE="`mktemp -t mandos-keygen-secfile.XXXXXXXXXX`" +fi + +# Create temporary key ring directory +RINGDIR="`mktemp -d -t mandos-keygen-keyrings.XXXXXXXXXX`" # Remove temporary files on exit -trap "rm --force $PUBRING $BATCHFILE; shred --remove $SECRING" EXIT - -# Create batch file for GnuPG -cat >"$BATCHFILE" </dev/null; then - shred --remove "$SECKEYFILE" -fi -if cp --backup=numbered --force "$PUBKEYFILE" "$PUBKEYFILE" \ - 2>/dev/null; then - rm --force "$PUBKEYFILE" -fi - -FILECOMMENT="Mandos client key for $KEYNAME" -if [ "$KEYCOMMENT" != "$KEYCOMMENT_ORIG" ]; then - FILECOMMENT="$FILECOMMENT ($KEYCOMMENT)" -fi - -if [ -n "$KEYEMAIL" ]; then - FILECOMMENT="$FILECOMMENT <$KEYEMAIL>" -fi - -# Export keys from key rings to key files -gpg --no-random-seed-file --quiet --batch --no-tty --armor \ - --no-default-keyring --no-options --secret-keyring "$SECRING" \ - --keyring "$PUBRING" --export-options export-minimal \ - --comment "$FILECOMMENT" --output "$SECKEYFILE" \ - --export-secret-keys -gpg --no-random-seed-file --quiet --batch --no-tty --armor \ - --no-default-keyring --no-options --secret-keyring "$SECRING" \ - --keyring "$PUBRING" --export-options export-minimal \ - --comment "$FILECOMMENT" --output "$PUBKEYFILE" \ - --export +trap " +set +e; \ +test -n \"$SECFILE\" && shred --remove \"$SECFILE\"; \ +shred --remove \"$RINGDIR\"/sec*; +test -n \"$BATCHFILE\" && rm --force \"$BATCHFILE\"; \ +rm --recursive --force \"$RINGDIR\"; +stty echo; \ +" EXIT + +umask 077 + +if [ "$mode" = keygen ]; then + # Create batch file for GnuPG + cat >"$BATCHFILE" <<-EOF + Key-Type: $KEYTYPE + Key-Length: $KEYLENGTH + #Key-Usage: encrypt,sign,auth + Subkey-Type: $SUBKEYTYPE + Subkey-Length: $SUBKEYLENGTH + #Subkey-Usage: encrypt,sign,auth + Name-Real: $KEYNAME + $KEYCOMMENTLINE + $KEYEMAILLINE + Expire-Date: $KEYEXPIRE + #Preferences: + #Handle: + #%pubring pubring.gpg + #%secring secring.gpg + %commit + EOF + + if tty --quiet; then + cat <<-EOF + Note: Due to entropy requirements, key generation could take + anything from a few minutes to SEVERAL HOURS. Please be + patient and/or supply the system with more entropy if needed. + EOF + echo -n "Started: " + date + fi + + # Generate a new key in the key rings + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always \ + --gen-key "$BATCHFILE" + rm --force "$BATCHFILE" + + if tty --quiet; then + echo -n "Finished: " + date + fi + + # Backup any old key files + if cp --backup=numbered --force "$SECKEYFILE" "$SECKEYFILE" \ + 2>/dev/null; then + shred --remove "$SECKEYFILE" + fi + if cp --backup=numbered --force "$PUBKEYFILE" "$PUBKEYFILE" \ + 2>/dev/null; then + rm --force "$PUBKEYFILE" + fi + + FILECOMMENT="Mandos client key for $KEYNAME" + if [ "$KEYCOMMENT" != "$KEYCOMMENT_ORIG" ]; then + FILECOMMENT="$FILECOMMENT ($KEYCOMMENT)" + fi + + if [ -n "$KEYEMAIL" ]; then + FILECOMMENT="$FILECOMMENT <$KEYEMAIL>" + fi + + # Export key from key rings to key files + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --armor --export-options export-minimal \ + --comment "$FILECOMMENT" --output "$SECKEYFILE" \ + --export-secret-keys + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --armor --export-options export-minimal \ + --comment "$FILECOMMENT" --output "$PUBKEYFILE" --export +fi + +if [ "$mode" = password ]; then + # Import key into temporary key rings + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always --armor \ + --import "$SECKEYFILE" + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always --armor \ + --import "$PUBKEYFILE" + + # Get fingerprint of key + FINGERPRINT="`gpg --quiet --batch --no-tty --no-options \ + --enable-dsa2 --homedir \"$RINGDIR\" --trust-model always \ + --fingerprint --with-colons \ + | sed --quiet \ + --expression='/^fpr:/{s/^fpr:.*:\\([0-9A-Z]*\\):\$/\\1/p;q}'`" + + test -n "$FINGERPRINT" + + FILECOMMENT="Encrypted password for a Mandos client" + + if [ -n "$PASSFILE" ]; then + cat "$PASSFILE" + else + stty -echo + echo -n "Enter passphrase: " >&2 + first="$(head --lines=1 | tr --delete '\n')" + echo >&2 + echo -n "Repeat passphrase: " >&2 + second="$(head --lines=1 | tr --delete '\n')" + echo >&2 + stty echo + if [ "$first" != "$second" ]; then + echo "Passphrase mismatch" >&2 + touch "$RINGDIR"/mismatch + else + echo -n "$first" + fi + fi | gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always --armor --encrypt \ + --sign --recipient "$FINGERPRINT" --comment "$FILECOMMENT" \ + > "$SECFILE" + if [ -e "$RINGDIR"/mismatch ]; then + rm --force "$RINGDIR"/mismatch + exit 1 + fi + + cat <<-EOF + [$KEYNAME] + host = $KEYNAME + fingerprint = $FINGERPRINT + secret = + EOF + sed --quiet --expression=' + /^-----BEGIN PGP MESSAGE-----$/,/^-----END PGP MESSAGE-----$/{ + /^$/,${ + # Remove 24-bit Radix-64 checksum + s/=....$// + # Indent four spaces + /^[^-]/s/^/ /p + } + }' < "$SECFILE" +fi trap - EXIT +set +e +# Remove the password file, if any +if [ -n "$SECFILE" ]; then + shred --remove "$SECFILE" +fi # Remove the key rings -shred --remove "$SECRING" -rm --force "$PUBRING" +shred --remove "$RINGDIR"/sec* +rm --recursive --force "$RINGDIR" === modified file 'mandos-keygen.xml' --- mandos-keygen.xml 2008-08-19 23:44:17 +0000 +++ mandos-keygen.xml 2009-01-04 21:54:55 +0000 @@ -1,16 +1,19 @@ - + + + +%common; ]> - &COMMANDNAME; - - &COMMANDNAME; - &VERSION; + Mandos Manual + + Mandos + &version; + &TIMESTAMP; Björn @@ -29,33 +32,13 @@ 2008 - Teddy Hogeborn & Björn Påhlsson + 2009 + Teddy Hogeborn + Björn Påhlsson - - - This manual page 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 manual page 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 - . - - + - + &COMMANDNAME; 8 @@ -64,188 +47,256 @@ &COMMANDNAME; - Generate keys for password-request - 8mandos + Generate key and password for Mandos client and server. - + &COMMANDNAME; - - - directory - - - - type - - - - bits - - - - NAME - - - - EMAIL - - - - COMMENT - - - - TIME - - - - - - - &COMMANDNAME; - - - directory - - - - type - - - - bits - - - - NAME - - - - EMAIL - - - - COMMENT - - - - TIME - - - - - - - &COMMANDNAME; - - - - - - - &COMMANDNAME; - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + FILE + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + - + DESCRIPTION &COMMANDNAME; is a program to generate the - OpenPGP keys used by - password-request - 8mandos. The keys are + OpenPGP key used by + mandos-client + 8mandos. The key is normally written to /etc/mandos for later installation into the - initrd image, but this, like most things, can be changed with - command line options. + initrd image, but this, and most other things, can be changed + with command line options. + + + This program can also be used with the + or + options to generate a ready-made section for + clients.conf (see + mandos-clients.conf + 5). PURPOSE - The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. - OPTIONS - + - -h, --help + + Show a help message and exit - - - -d, --dir - directory - - - Target directory for key files. - - - - - - -t, --type - type - - - Key type. Default is DSA. - - - - - - -l, --length - bits - - - Key length in bits. Default is 1024. - - - - - - -e, --email - address + + + + + + + Target directory for key files. Default is + /etc/mandos. + + + + + + + + + + Key type. Default is DSA. + + + + + + + + + + Key length in bits. Default is 2048. + + + + + + + + + + Subkey type. Default is ELG-E (Elgamal + encryption-only). + + + + + + + + + + Subkey length in bits. Default is 2048. + + + + + + + Email address of key. Default is empty. - + - -c, --comment - comment + + Comment field for key. The default value is - "Mandos client key". + Mandos client key. - + - -x, --expire - time + + Key expire time. Default is no expiration. See @@ -254,32 +305,65 @@ - - - -f, --force - - - Force overwriting old keys. + + + + + + + Force overwriting old key. + + + + + + + + + Prompt for a password and encrypt it with the key already + present in either /etc/mandos or the + directory specified with the + option. Outputs, on standard output, a section suitable + for inclusion in mandos-clients.conf8. The host name or the name + specified with the option is used + for the section header. All other options are ignored, + and no key is created. + + + + + + + + + The same as , but read from + FILE, not the terminal. - + OVERVIEW - This program is a small program to generate new OpenPGP keys for - new Mandos clients. + This program is a small utility to generate new OpenPGP keys for + new Mandos clients, and to generate sections for inclusion in + clients.conf on the server. - + EXIT STATUS - The exit status will be 0 if new keys were successfully created, - otherwise not. + The exit status will be 0 if a new key (or password, if the + option was used) was successfully + created, otherwise not. @@ -287,7 +371,7 @@ ENVIRONMENT - TMPDIR + TMPDIR If set, temporary files will be created here. See @@ -299,7 +383,7 @@ - + FILES Use the option to change where @@ -336,14 +420,13 @@ - - - BUGS - - None are known at this time. - - - + + + + + + + EXAMPLE @@ -351,47 +434,78 @@ Normal invocation needs no options: - mandos-keygen + &COMMANDNAME; - Create keys in another directory and of another type. Force + Create key in another directory and of another type. Force overwriting old key files: -mandos-keygen --dir ~/keydir --type RSA --force +&COMMANDNAME; --dir ~/keydir --type RSA --force + + + + + + Prompt for a password, encrypt it with the key in + /etc/mandos and output a section suitable + for clients.conf. + + + &COMMANDNAME; --password + + + + + Prompt for a password, encrypt it with the key in the + client-key directory and output a section + suitable for clients.conf. + + + + +&COMMANDNAME; --password --dir client-key - + SECURITY - The and - options can be used to create keys of insufficient security. If - in doubt, leave them to the default values. + The , , + , and + options can be used to create keys of low security. If in + doubt, leave them to the default values. - The key expire time is not guaranteed to be honored by - mandos + The key expire time is not guaranteed to be + honored by mandos 8. - + SEE ALSO - password-request - 8mandos, - mandos - 8, and gpg - 1 + 1, + mandos-clients.conf + 5, + mandos + 8, + mandos-client + 8mandos + + + + + === added file 'mandos-monitor' --- mandos-monitor 1970-01-01 00:00:00 +0000 +++ mandos-monitor 2010-09-26 18:32:58 +0000 @@ -0,0 +1,733 @@ +#!/usr/bin/python +# -*- mode: python; coding: utf-8 -*- +# +# Mandos Monitor - Control and monitor the Mandos server +# +# Copyright © 2009,2010 Teddy Hogeborn +# Copyright © 2009,2010 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 . +# + +from __future__ import division, absolute_import, with_statement + +import sys +import os +import signal + +import datetime + +import urwid.curses_display +import urwid + +from dbus.mainloop.glib import DBusGMainLoop +import gobject + +import dbus + +import UserList + +import locale + +locale.setlocale(locale.LC_ALL, u'') + +import logging +logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL) + +# Some useful constants +domain = 'se.bsnet.fukt' +server_interface = domain + '.Mandos' +client_interface = domain + '.Mandos.Client' +version = "1.0.15" + +# 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: + return None + d, t = iso.split(u"T", 1) + year, month, day = d.split(u"-", 2) + hour, minute, second = t.split(u":", 2) + second, fraction = divmod(float(second), 1) + return datetime.datetime(int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), # Whole seconds + int(fraction*1000000)) # Microseconds + +class MandosClientPropertyCache(object): + """This wraps a Mandos Client D-Bus proxy object, caches the + properties and calls a hook function when any of them are + changed. + """ + def __init__(self, proxy_object=None, *args, **kwargs): + self.proxy = proxy_object # Mandos Client proxy object + + self.properties = dict() + self.proxy.connect_to_signal(u"PropertyChanged", + self.property_changed, + client_interface, + byte_arrays=True) + + self.properties.update( + self.proxy.GetAll(client_interface, + dbus_interface = dbus.PROPERTIES_IFACE)) + + #XXX This break good super behaviour! +# super(MandosClientPropertyCache, self).__init__( +# *args, **kwargs) + + def property_changed(self, property=None, value=None): + """This is called whenever we get a PropertyChanged signal + It updates the changed property in the "properties" dict. + """ + # Update properties dict with new value + self.properties[property] = value + + +class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache): + """A Mandos Client which is visible on the screen. + """ + + def __init__(self, server_proxy_object=None, update_hook=None, + delete_hook=None, logger=None, *args, **kwargs): + # Called on update + self.update_hook = update_hook + # Called on delete + self.delete_hook = delete_hook + # Mandos Server proxy object + self.server_proxy_object = server_proxy_object + # Logger + self.logger = logger + + self._update_timer_callback_tag = None + self._update_timer_callback_lock = 0 + self.last_checker_failed = False + + # The widget shown normally + self._text_widget = urwid.Text(u"") + # The widget shown when we have focus + self._focus_text_widget = urwid.Text(u"") + super(MandosClientWidget, self).__init__( + update_hook=update_hook, delete_hook=delete_hook, + *args, **kwargs) + self.update() + self.opened = False + + last_checked_ok = isoformat_to_datetime(self.properties + [u"LastCheckedOK"]) + if last_checked_ok is None: + self.last_checker_failed = True + else: + self.last_checker_failed = ((datetime.datetime.utcnow() + - last_checked_ok) + > datetime.timedelta + (milliseconds= + self.properties + [u"Interval"])) + + if self.last_checker_failed: + self.using_timer(True) + + if self.need_approval: + self.using_timer(True) + + self.proxy.connect_to_signal(u"CheckerCompleted", + self.checker_completed, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"CheckerStarted", + self.checker_started, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"GotSecret", + self.got_secret, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"NeedApproval", + self.need_approval, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"Rejected", + self.rejected, + client_interface, + byte_arrays=True) + + def property_changed(self, property=None, value=None): + super(self, MandosClientWidget).property_changed(property, + value) + if property == u"ApprovalPending": + using_timer(bool(value)) + + def using_timer(self, flag): + """Call this method with True or False when timer should be + activated or deactivated. + """ + old = self._update_timer_callback_lock + if flag: + self._update_timer_callback_lock += 1 + else: + self._update_timer_callback_lock -= 1 + if old == 0 and self._update_timer_callback_lock: + self._update_timer_callback_tag = (gobject.timeout_add + (1000, + self.update_timer)) + elif old and self._update_timer_callback_lock == 0: + gobject.source_remove(self._update_timer_callback_tag) + self._update_timer_callback_tag = None + + def checker_completed(self, exitstatus, condition, command): + if exitstatus == 0: + if self.last_checker_failed: + self.last_checker_failed = False + self.using_timer(False) + #self.logger(u'Checker for client %s (command "%s")' + # u' was successful' + # % (self.properties[u"Name"], command)) + self.update() + return + # Checker failed + if not self.last_checker_failed: + self.last_checker_failed = True + self.using_timer(True) + if os.WIFEXITED(condition): + self.logger(u'Checker for client %s (command "%s")' + u' failed with exit code %s' + % (self.properties[u"Name"], command, + os.WEXITSTATUS(condition))) + elif os.WIFSIGNALED(condition): + self.logger(u'Checker for client %s (command "%s")' + u' was killed by signal %s' + % (self.properties[u"Name"], command, + os.WTERMSIG(condition))) + elif os.WCOREDUMP(condition): + self.logger(u'Checker for client %s (command "%s")' + u' dumped core' + % (self.properties[u"Name"], command)) + else: + self.logger(u'Checker for client %s completed' + u' mysteriously') + self.update() + + def checker_started(self, command): + #self.logger(u'Client %s started checker "%s"' + # % (self.properties[u"Name"], unicode(command))) + pass + + def got_secret(self): + self.last_checker_failed = False + self.logger(u'Client %s received its secret' + % self.properties[u"Name"]) + + def need_approval(self, timeout, default): + if not default: + message = u'Client %s needs approval within %s seconds' + else: + message = u'Client %s will get its secret in %s seconds' + self.logger(message + % (self.properties[u"Name"], timeout/1000)) + self.using_timer(True) + + def rejected(self, reason): + self.logger(u'Client %s was rejected; reason: %s' + % (self.properties[u"Name"], reason)) + + def selectable(self): + """Make this a "selectable" widget. + This overrides the method from urwid.FlowWidget.""" + return True + + def rows(self, (maxcol,), focus=False): + """How many rows this widget will occupy might depend on + whether we have focus or not. + This overrides the method from urwid.FlowWidget""" + return self.current_widget(focus).rows((maxcol,), focus=focus) + + def current_widget(self, focus=False): + if focus or self.opened: + return self._focus_widget + return self._widget + + def update(self): + "Called when what is visible on the screen should be updated." + # How to add standout mode to a style + with_standout = { u"normal": u"standout", + u"bold": u"bold-standout", + u"underline-blink": + u"underline-blink-standout", + u"bold-underline-blink": + u"bold-underline-blink-standout", + } + + # Rebuild focus and non-focus widgets using current properties + + # Base part of a client. Name! + base = (u'%(name)s: ' + % {u"name": self.properties[u"Name"]}) + if not self.properties[u"Enabled"]: + message = u"DISABLED" + elif self.properties[u"ApprovalPending"]: + timeout = datetime.timedelta(milliseconds + = self.properties + [u"ApprovalDelay"]) + last_approval_request = isoformat_to_datetime( + self.properties[u"LastApprovalRequest"]) + if last_approval_request is not None: + timer = timeout - (datetime.datetime.utcnow() + - last_approval_request) + else: + timer = datetime.timedelta() + if self.properties[u"ApprovedByDefault"]: + message = u"Approval in %s. (d)eny?" + else: + message = u"Denial in %s. (a)pprove?" + message = message % unicode(timer).rsplit(".", 1)[0] + elif self.last_checker_failed: + timeout = datetime.timedelta(milliseconds + = self.properties + [u"Timeout"]) + last_ok = isoformat_to_datetime( + max((self.properties[u"LastCheckedOK"] + or self.properties[u"Created"]), + self.properties[u"LastEnabled"])) + timer = timeout - (datetime.datetime.utcnow() - last_ok) + message = (u'A checker has failed! Time until client' + u' gets disabled: %s' + % unicode(timer).rsplit(".", 1)[0]) + else: + message = u"enabled" + self._text = "%s%s" % (base, message) + + if not urwid.supports_unicode(): + self._text = self._text.encode("ascii", "replace") + textlist = [(u"normal", self._text)] + self._text_widget.set_text(textlist) + self._focus_text_widget.set_text([(with_standout[text[0]], + text[1]) + if isinstance(text, tuple) + else text + for text in textlist]) + self._widget = self._text_widget + self._focus_widget = urwid.AttrWrap(self._focus_text_widget, + "standout") + # Run update hook, if any + if self.update_hook is not None: + self.update_hook() + + def update_timer(self): + "called by gobject" + self.update() + return True # Keep calling this + + def delete(self): + if self._update_timer_callback_tag is not None: + gobject.source_remove(self._update_timer_callback_tag) + self._update_timer_callback_tag = None + if self.delete_hook is not None: + self.delete_hook(self) + + def render(self, (maxcol,), focus=False): + """Render differently if we have focus. + This overrides the method from urwid.FlowWidget""" + return self.current_widget(focus).render((maxcol,), + focus=focus) + + def keypress(self, (maxcol,), key): + """Handle keys. + This overrides the method from urwid.FlowWidget""" + if key == u"+": + self.proxy.Enable(dbus_interface = client_interface) + elif key == u"-": + self.proxy.Disable(dbus_interface = client_interface) + elif key == u"a": + self.proxy.Approve(dbus.Boolean(True, variant_level=1), + dbus_interface = client_interface) + elif key == u"d": + self.proxy.Approve(dbus.Boolean(False, variant_level=1), + dbus_interface = client_interface) + elif key == u"r" or key == u"_" or key == u"ctrl k": + self.server_proxy_object.RemoveClient(self.proxy + .object_path) + elif key == u"s": + self.proxy.StartChecker(dbus_interface = client_interface) + elif key == u"S": + self.proxy.StopChecker(dbus_interface = client_interface) + elif key == u"C": + self.proxy.CheckedOK(dbus_interface = client_interface) + # xxx +# elif key == u"p" or key == "=": +# self.proxy.pause() +# elif key == u"u" or key == ":": +# self.proxy.unpause() +# elif key == u"RET": +# self.open() + else: + return key + + def property_changed(self, property=None, value=None, + *args, **kwargs): + """Call self.update() if old value is not new value. + This overrides the method from MandosClientPropertyCache""" + property_name = unicode(property) + old_value = self.properties.get(property_name) + super(MandosClientWidget, self).property_changed( + property=property, value=value, *args, **kwargs) + if self.properties.get(property_name) != old_value: + self.update() + + +class ConstrainedListBox(urwid.ListBox): + """Like a normal urwid.ListBox, but will consume all "up" or + "down" key presses, thus not allowing any containing widgets to + use them as an excuse to shift focus away from this widget. + """ + def keypress(self, (maxcol, maxrow), key): + ret = super(ConstrainedListBox, self).keypress((maxcol, + maxrow), key) + if ret in (u"up", u"down"): + return + return ret + + +class UserInterface(object): + """This is the entire user interface - the whole screen + with boxes, lists of client widgets, etc. + """ + def __init__(self, max_log_length=1000): + DBusGMainLoop(set_as_default=True) + + self.screen = urwid.curses_display.Screen() + + self.screen.register_palette(( + (u"normal", + u"default", u"default", None), + (u"bold", + u"default", u"default", u"bold"), + (u"underline-blink", + u"default", u"default", u"underline"), + (u"standout", + u"default", u"default", u"standout"), + (u"bold-underline-blink", + u"default", u"default", (u"bold", u"underline")), + (u"bold-standout", + u"default", u"default", (u"bold", u"standout")), + (u"underline-blink-standout", + u"default", u"default", (u"underline", u"standout")), + (u"bold-underline-blink-standout", + u"default", u"default", (u"bold", u"underline", + u"standout")), + )) + + if urwid.supports_unicode(): + self.divider = u"─" # \u2500 + #self.divider = u"━" # \u2501 + else: + #self.divider = u"-" # \u002d + self.divider = u"_" # \u005f + + self.screen.start() + + self.size = self.screen.get_cols_rows() + + self.clients = urwid.SimpleListWalker([]) + self.clients_dict = {} + + # We will add Text widgets to this list + self.log = [] + self.max_log_length = max_log_length + + # We keep a reference to the log widget so we can remove it + # from the ListWalker without it getting destroyed + self.logbox = ConstrainedListBox(self.log) + + # This keeps track of whether self.uilist currently has + # self.logbox in it or not + self.log_visible = True + self.log_wrap = u"any" + + self.rebuild() + self.log_message_raw((u"bold", + u"Mandos Monitor version " + version)) + self.log_message_raw((u"bold", + u"q: Quit ?: Help")) + + self.busname = domain + '.Mandos' + self.main_loop = gobject.MainLoop() + self.bus = dbus.SystemBus() + mandos_dbus_objc = self.bus.get_object( + self.busname, u"/", follow_name_owner_changes=True) + self.mandos_serv = dbus.Interface(mandos_dbus_objc, + dbus_interface + = server_interface) + try: + mandos_clients = (self.mandos_serv + .GetAllClientsWithProperties()) + except dbus.exceptions.DBusException: + mandos_clients = dbus.Dictionary() + + (self.mandos_serv + .connect_to_signal(u"ClientRemoved", + self.find_and_remove_client, + dbus_interface=server_interface, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal(u"ClientAdded", + self.add_new_client, + dbus_interface=server_interface, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal(u"ClientNotFound", + self.client_not_found, + dbus_interface=server_interface, + byte_arrays=True)) + for path, client in mandos_clients.iteritems(): + 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=client, + update_hook + =self.refresh, + delete_hook + =self.remove_client, + logger + =self.log_message), + path=path) + + def client_not_found(self, fingerprint, address): + self.log_message((u"Client with address %s and fingerprint %s" + u" could not be found" % (address, + fingerprint))) + + def rebuild(self): + """This rebuilds the User Interface. + Call this when the widget layout needs to change""" + self.uilist = [] + #self.uilist.append(urwid.ListBox(self.clients)) + self.uilist.append(urwid.Frame(ConstrainedListBox(self. + clients), + #header=urwid.Divider(), + header=None, + footer= + urwid.Divider(div_char= + self.divider))) + if self.log_visible: + self.uilist.append(self.logbox) + pass + self.topwidget = urwid.Pile(self.uilist) + + def log_message(self, message): + timestamp = datetime.datetime.now().isoformat() + self.log_message_raw(timestamp + u": " + message) + + def log_message_raw(self, markup): + """Add a log message to the log buffer.""" + self.log.append(urwid.Text(markup, wrap=self.log_wrap)) + if (self.max_log_length + and len(self.log) > self.max_log_length): + del self.log[0:len(self.log)-self.max_log_length-1] + self.logbox.set_focus(len(self.logbox.body.contents), + coming_from=u"above") + self.refresh() + + def toggle_log_display(self): + """Toggle visibility of the log buffer.""" + self.log_visible = not self.log_visible + self.rebuild() + #self.log_message(u"Log visibility changed to: " + # + unicode(self.log_visible)) + + def change_log_display(self): + """Change type of log display. + Currently, this toggles wrapping of text lines.""" + if self.log_wrap == u"clip": + self.log_wrap = u"any" + else: + self.log_wrap = u"clip" + for textwidget in self.log: + textwidget.set_wrap_mode(self.log_wrap) + #self.log_message(u"Wrap mode: " + self.log_wrap) + + def find_and_remove_client(self, path, name): + """Find an client from its object path and remove it. + + This is connected to the ClientRemoved signal from the + Mandos server object.""" + try: + client = self.clients_dict[path] + except KeyError: + # not found? + return + self.remove_client(client, path) + + 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, + update_hook + =self.refresh, + delete_hook + =self.remove_client, + logger + =self.log_message), + path=path) + + def add_client(self, client, path=None): + self.clients.append(client) + if path is None: + path = client.proxy.object_path + self.clients_dict[path] = client + self.clients.sort(None, lambda c: c.properties[u"Name"]) + self.refresh() + + def remove_client(self, client, path=None): + self.clients.remove(client) + 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): + """Redraw the screen""" + canvas = self.topwidget.render(self.size, focus=True) + self.screen.draw_screen(self.size, canvas) + + def run(self): + """Start the main loop and exit when it's done.""" + self.refresh() + self._input_callback_tag = (gobject.io_add_watch + (sys.stdin.fileno(), + gobject.IO_IN, + self.process_input)) + self.main_loop.run() + # Main loop has finished, we should close everything now + gobject.source_remove(self._input_callback_tag) + self.screen.stop() + + def stop(self): + self.main_loop.quit() + + def process_input(self, source, condition): + keys = self.screen.get_input() + translations = { u"ctrl n": u"down", # Emacs + u"ctrl p": u"up", # Emacs + u"ctrl v": u"page down", # Emacs + u"meta v": u"page up", # Emacs + u" ": u"page down", # less + u"f": u"page down", # less + u"b": u"page up", # less + u"j": u"down", # vi + u"k": u"up", # vi + } + for key in keys: + try: + key = translations[key] + except KeyError: # :-) + pass + + if key == u"q" or key == u"Q": + self.stop() + break + elif key == u"window resize": + self.size = self.screen.get_cols_rows() + self.refresh() + elif key == u"\f": # Ctrl-L + self.refresh() + elif key == u"l" or key == u"D": + self.toggle_log_display() + self.refresh() + elif key == u"w" or key == u"i": + self.change_log_display() + self.refresh() + elif key == u"?" or key == u"f1" or key == u"esc": + if not self.log_visible: + self.log_visible = True + self.rebuild() + self.log_message_raw((u"bold", + u" ". + join((u"q: Quit", + u"?: Help", + u"l: Log window toggle", + u"TAB: Switch window", + u"w: Wrap (log)")))) + self.log_message_raw((u"bold", + u" " + .join((u"Clients:", + u"+: Enable", + u"-: Disable", + u"r: Remove", + u"s: Start new checker", + u"S: Stop checker", + u"C: Checker OK", + u"a: Approve", + u"d: Deny")))) + self.refresh() + elif key == u"tab": + if self.topwidget.get_focus() is self.logbox: + self.topwidget.set_focus(0) + else: + self.topwidget.set_focus(self.logbox) + self.refresh() + #elif (key == u"end" or key == u"meta >" or key == u"G" + # or key == u">"): + # pass # xxx end-of-buffer + #elif (key == u"home" or key == u"meta <" or key == u"g" + # or key == u"<"): + # pass # xxx beginning-of-buffer + #elif key == u"ctrl e" or key == u"$": + # pass # xxx move-end-of-line + #elif key == u"ctrl a" or key == u"^": + # pass # xxx move-beginning-of-line + #elif key == u"ctrl b" or key == u"meta (" or key == u"h": + # pass # xxx left + #elif key == u"ctrl f" or key == u"meta )" or key == u"l": + # pass # xxx right + #elif key == u"a": + # pass # scroll up log + #elif key == u"z": + # pass # scroll down log + elif self.topwidget.selectable(): + self.topwidget.keypress(self.size, key) + self.refresh() + return True + +ui = UserInterface() +try: + ui.run() +except KeyboardInterrupt: + ui.screen.stop() +except Exception, e: + ui.log_message(unicode(e)) + ui.screen.stop() + raise === added file 'mandos-monitor.xml' --- mandos-monitor.xml 1970-01-01 00:00:00 +0000 +++ mandos-monitor.xml 2010-09-14 18:22:03 +0000 @@ -0,0 +1,233 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2010 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Text-based GUI to control the Mandos server. + + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + &COMMANDNAME; is an interactive program to + monitor and control the operations of the Mandos server (see + mandos8). + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OVERVIEW + + + This program is used to monitor and control the Mandos server. + In particular, it can be used to approve Mandos clients which + have been configured to require approval. It also shows all + significant events reported by the Mandos server. + + + + + KEYS + + This program is used to monitor and control the Mandos server. + In particular, it can be used to approve Mandos clients which + have been configured to require approval. It also shows all + significant events reported by the Mandos server. + + + Global Keys + + Keys + Function + + + + q, Q + Quit + + + Ctrl-L + Redraw screen + + + ?, F1 + Show help + + + l, D + Toggle log window + + + TAB + Switch window + + + w, i + Toggle log window line wrap + + + Up, Ctrl-P, k + Move up a line + + + Down, Ctrl-N, j + Move down a line + + + PageUp, Meta-V, b + Move up a page + + + PageDown, Ctrl-V, SPACE, f + Move down a page + +
+ + Client List Keys + + Keys + Function + + + + + + Enable client + + + - + Disable client + + + a + Approve client + + + d + Deny client + + + r, _, Ctrl-K + Remove client + + + s + Start checker for client + + + S + Stop checker for client + + + C + Force a successful check for this client. + +
+
+ + + BUGS + + This program can currently only be used to monitor and control a + Mandos server with the default D-Bus service name of + Mandos. + + + + + EXAMPLE + + + This program takes no options: + + + &COMMANDNAME; + + + + + + SECURITY + + This program must be permitted to access the Mandos server via + the D-Bus interface. This normally requires the root user, but + could be configured otherwise by reconfiguring the D-Bus server. + + + + + SEE ALSO + + mandos + 8, + mandos-ctl + 8 + + + +
+ + + + + === modified file 'mandos-options.xml' --- mandos-options.xml 2008-08-19 23:44:17 +0000 +++ mandos-options.xml 2009-02-13 05:38:21 +0000 @@ -1,10 +1,12 @@ -
@@ -12,22 +14,24 @@ If this is specified, the server will only announce the service - and listen to requests on network interface - IF. Default is to use all available - interfaces. Note: a failure to bind to the - specified interface is not considered critical, and the server - does not exit. + and listen to requests on the specified network interface. + Default is to use all available interfaces. Note: a failure to bind to the specified + interface is not considered critical, and the server will not + exit, but instead continue normally. - If this option is used, the server will only listen to a specific - address. If a link-local address is specified, an interface - should be set, since a link-local address is only valid on a - single interface. By default, the server will listen to all - available addresses. Also, this must currently be an IPv6 - address; an IPv4 address can only be specified using the - ::FFFF:192.0.2.3 format. + If this option is used, the server will only listen to the + specified IPv6 address. If a link-local address is specified, an + interface should be set, since a link-local address is only valid + on a single interface. By default, the server will listen to all + available addresses. If set, this must normally be an IPv6 + address; an IPv4 address can only be specified using IPv4-mapped + IPv6 address syntax: ::FFFF:192.0.2.3. (Only if IPv6 usage is + disabled (see below) must this be an IPv4 + address.) @@ -38,29 +42,48 @@ If the server is run in debug mode, it will run in the foreground - and print a lot of debugging information. The default is - not to run in debug mode. + and print a lot of debugging information. The default is to + not run in debug mode. - GnuTLS priority string for the TLS handshake with the clients. - The default is - SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP. - See gnutls_priority_init - 3 for the - syntax. Warning: changing this may make the - TLS handshake fail, making communication with clients impossible. + GnuTLS priority string for the TLS handshake. + The default is SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP. See + gnutls_priority_init + 3 for the syntax. + Warning: changing this may make the + TLS handshake fail, making server-client + communication impossible. Zeroconf service name. The default is Mandos. This only needs to be - changed this if it, for some reason, is necessary to run more than - one server on the same host, which would not + changed if for some reason is would be necessary to run more than + one server on the same host. This would not normally be useful. If there are name collisions on the same network, the newer server will automatically rename itself to Mandos #2, and so on; therefore, this option is not needed in that case. + + This option controls whether the server will provide a D-Bus + system bus interface. The default is to provide such an + interface. + + + + This option controls whether the server will use IPv6 sockets and + addresses. The default is to use IPv6. This option should + never normally be turned off, even in + IPv4-only environments. This is because + mandos-client + 8mandos will normally use + IPv6 link-local addresses, and will not be able to find or connect + to the server if this option is turned off. Only + advanced users should consider changing this option. + +
=== modified file 'mandos.conf' --- mandos.conf 2008-08-18 23:55:28 +0000 +++ mandos.conf 2009-02-13 05:38:21 +0000 @@ -36,3 +36,9 @@ # If there are name collisions on the same *network*, the server will # rename itself to "Mandos #2", etc. ;servicename = Mandos + +# Whether to provide a D-Bus system bus interface or not +;use_dbus = True + +# Whether to use IPv6. (Changing this is NOT recommended.) +;use_ipv6 = True === modified file 'mandos.conf.xml' --- mandos.conf.xml 2008-08-19 23:44:17 +0000 +++ mandos.conf.xml 2009-02-25 01:14:29 +0000 @@ -1,18 +1,20 @@ - + /etc/mandos/mandos.conf"> - + + +%common; ]> - &CONFNAME; + Mandos Manual - &CONFNAME; - &VERSION; + Mandos + &version; + &TIMESTAMP; Björn @@ -31,34 +33,13 @@ 2008 + 2009 Teddy Hogeborn Björn Påhlsson - - - This manual page 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 manual page 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 - . - - + - + &CONFNAME; 5 @@ -70,100 +51,108 @@ Configuration file for the Mandos server - + - - &CONFPATH; - + &CONFPATH; - + DESCRIPTION The file &CONFPATH; is a simple configuration file for mandos 8, and is read by it at - startup. The configuration file starts with - [DEFAULT] on a line by itself, - followed by any number of - option=value - entries, with continuations in the style of RFC 822. - option: - value is also accepted. Note - that leading whitespace is removed from values. Lines beginning - with # or ; are ignored and may be - used to provide comments. + startup. The configuration file starts with [DEFAULT] on a line by itself, followed by + any number of option=value entries, + with continuations in the style of RFC 822. option: value is also accepted. Note that + leading whitespace is removed from values. Lines beginning with + # or ; are ignored and may be used + to provide comments. - + OPTIONS - interface + - interface = IF - - + - address + - address = ADDRESS - - + - port + - port = PORT - - + - debug - - debug = { + >false | off } + - + - priority + - priority = PRIORITY - - + - servicename + - servicename = NAME - + + + + + + + + + + + + + + @@ -179,7 +168,7 @@ The [DEFAULT] is necessary because the Python built-in module ConfigParser - requres it. + requires it. @@ -201,12 +190,77 @@ [DEFAULT] # A configuration example interface = eth0 -address = 2001:db8:f983:bd0b:30de:ae4a:71f2:f672 +address = fe80::aede:48ff:fe71:f6f2 port = 1025 debug = true priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP servicename = Daena +use_dbus = False +use_ipv6 = True + + + SEE ALSO + + gnutls_priority_init3, + mandos + 8, + mandos-clients.conf + 5 + + + + + + RFC 4291: IP Version 6 Addressing + Architecture + + + + + Section 2.2: Text Representation of + Addresses + + + + Section 2.5.5.2: IPv4-Mapped IPv6 + Address + + + + Section 2.5.6, Link-Local IPv6 Unicast + Addresses + + + The clients use IPv6 link-local addresses, which are + immediately usable since a link-local addresses is + automatically assigned to a network interface when it + is brought up. + + + + + + + + + Zeroconf + + + + Zeroconf is the network protocol standard used by clients + for finding the Mandos server on the local network. + + + + + + + + + + === added file 'mandos.lsm' --- mandos.lsm 1970-01-01 00:00:00 +0000 +++ mandos.lsm 2010-09-14 18:22:03 +0000 @@ -0,0 +1,22 @@ +Begin4 +Title: Mandos +Version: 1.0.14 +Entered-date: 2010-09-14 +Description: The Mandos system allows computers to have encrypted +root file systems and at the same time be capable of remote and/or +unattended reboots. +Keywords: boot, encryption, luks, cryptsetup, network, openpgp, +tls, dm-crypt +Author: teddy@fukt.bsnet.se (Teddy Hogeborn), + belorn@fukt.bsnet.se (Björn Påhlsson) +Maintained-by: teddy@fukt.bsnet.se (Teddy Hogeborn), + belorn@fukt.bsnet.se (Björn Påhlsson) +Primary-site: http://www.fukt.bsnet.se/mandos + 103K mandos_1.0.14.orig.tar.gz +Alternate-site: ftp://ftp.fukt.bsnet.se/pub/mandos + 103K mandos_1.0.14.orig.tar.gz +Platforms: Requires GCC, GNU libC, Avahi, GnuPG, Python 2.5, and +various other libraries. While made for Debian GNU/Linux, it is +probably portable to other distributions, but not other Unixes. +Copying-policy: GNU General Public License version 3.0 or later +End === modified file 'mandos.xml' --- mandos.xml 2008-08-19 23:44:17 +0000 +++ mandos.xml 2010-09-26 18:32:58 +0000 @@ -1,16 +1,19 @@ + + +%common; ]> - - &COMMANDNAME; + + Mandos Manual - &COMMANDNAME; - &VERSION; + Mandos + &version; + &TIMESTAMP; Björn @@ -29,34 +32,14 @@ 2008 + 2009 + 2010 Teddy Hogeborn Björn Påhlsson - - - This manual page 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 manual page 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 - . - - + - + &COMMANDNAME; 8 @@ -65,48 +48,66 @@ &COMMANDNAME; - Sends encrypted passwords to authenticated Mandos clients + Gives encrypted passwords to authenticated Mandos clients - + &COMMANDNAME; - --interfaceIF - --addressADDRESS - --portPORT - --priorityPRIORITY - --servicenameNAME - --configdirDIRECTORY - --debug - - - &COMMANDNAME; - -iIF - -aADDRESS - -pPORT - --priorityPRIORITY - --servicenameNAME - --configdirDIRECTORY - --debug + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; - -h - --help + + &COMMANDNAME; - --version + &COMMANDNAME; - --check + - + DESCRIPTION @@ -121,60 +122,63 @@ Any authenticated client is then given the stored pre-encrypted password for that specific client. - PURPOSE - The purpose of this is to enable remote and unattended rebooting of client host computer with an encrypted root file system. See for details. - OPTIONS - - -h, --help + + Show a help message and exit - + - -i, --interface - IF + + NAME + + NAME - + - -a, --address - ADDRESS + + - + - -p, --port - PORT + + - + - --check + Run the server’s self-tests. This includes any unit @@ -182,34 +186,34 @@ - + - --debug + - + - --priority - PRIORITY + - + - --servicename NAME - + - + - --configdir DIR - + Directory to search for configuration files. Default is @@ -221,28 +225,45 @@ - + - --version + Prints the program version and exit. + + + + + + + See also . + + + + + + + + + + - + OVERVIEW This program is the server part. It is a normal server program and will run in a normal system environment, not in an initial - RAM disk environment. + RAM disk environment. - + NETWORK PROTOCOL @@ -300,21 +321,44 @@ - + CHECKING The server will, by default, continually check that the clients are still up. If a client has not been confirmed as being up for some time, the client is assumed to be compromised and is no - longer eligible to receive the encrypted password. The timeout, + longer eligible to receive the encrypted password. (Manual + intervention is required to re-enable a client.) The timeout, checker program, and interval between checks can be configured both globally and per client; see mandos-clients.conf - 5. - - - + 5. A client successfully + receiving its password will also be treated as a successful + checker run. + + + + + APPROVAL + + The server can be configured to require manual approval for a + client before it is sent its secret. The delay to wait for such + approval and the default action (approve or deny) can be + configured both globally and per client; see + mandos-clients.conf + 5. By default all clients + will be approved immediately without delay. + + + This can be used to deny a client its secret if not manually + approved within a specified time. It can also be used to make + the server delay before giving a client its secret, allowing + optional manual denying of this specific client. + + + + LOGGING @@ -324,7 +368,17 @@ and also show them on the console. - + + + D-BUS INTERFACE + + The server will by default provide a D-Bus system bus interface. + This interface will only be accessible by the root user or a + Mandos-specific user, if such a user exists. For documentation + of the D-Bus API, see the file DBUS-API. + + + EXIT STATUS @@ -332,12 +386,12 @@ critical error is encountered. - + ENVIRONMENT - PATH + PATH To start the configured checker (see - - + + FILES Use the option to change where @@ -382,11 +436,11 @@ - /var/run/mandos/mandos.pid + /var/run/mandos.pid - The file containing the process id of - &COMMANDNAME;. + The file containing the process id of the + &COMMANDNAME; process started last. @@ -420,15 +474,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 - . - - - There is currently no way of querying the server of the current - status of clients, other than analyzing its syslog output. + 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. @@ -437,7 +485,11 @@ Debug mode is conflated with running in the foreground. - The console log messages does not show a timestamp. + The console log messages do not show a time stamp. + + + This server does not check the expire time of clients’ OpenPGP + keys. @@ -448,7 +500,7 @@ Normal invocation needs no options: - mandos + &COMMANDNAME; @@ -461,7 +513,7 @@ -mandos --debug --configdir ~/mandos --servicename Test +&COMMANDNAME; --debug --configdir ~/mandos --servicename Test @@ -473,24 +525,24 @@ -mandos --interface eth7 --address fe80::aede:48ff:fe71:f6f2 +&COMMANDNAME; --interface eth7 --address fe80::aede:48ff:fe71:f6f2 - + SECURITY - + SERVER Running this &COMMANDNAME; server program should not in itself present any security risk to the host - computer running it. The program does not need any special - privileges to run, and is designed to run as a non-root user. + computer running it. The program switches to a non-root user + soon after startup. - + CLIENTS The server only gives out its stored data to clients which @@ -503,7 +555,7 @@ mandos-clients.conf 5) must be made non-readable by anyone - except the user running the server. + except the user starting the server (usually root). As detailed in , the status of all @@ -512,49 +564,43 @@ 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 - password-request + mandos-client 8mandos. - + SEE ALSO + + + mandos-clients.conf + 5, + mandos.conf + 5, + mandos-client + 8mandos, + sh1 + + - - password-request - 8mandos - - - - - This is the actual program which talks to this server. - Note that it is normally not invoked directly, and is only - run in the initial RAM disk environment, and not on a - fully started system. - - - - - Zeroconf @@ -577,8 +623,8 @@ - GnuTLS + GnuTLS @@ -590,23 +636,40 @@ - RFC 4291: IP Version 6 Addressing - Architecture, section 2.5.6, Link-Local IPv6 - Unicast Addresses + RFC 4291: IP Version 6 Addressing + Architecture - - The clients use IPv6 link-local addresses, which are - immediately usable since a link-local addresses is - automatically assigned to a network interfaces when it is - brought up. - + + + Section 2.2: Text Representation of + Addresses + + + + Section 2.5.5.2: IPv4-Mapped IPv6 + Address + + + + Section 2.5.6, Link-Local IPv6 Unicast + Addresses + + + The clients use IPv6 link-local addresses, which are + immediately usable since a link-local addresses is + automatically assigned to a network interfaces when it + is brought up. + + + + - RFC 4346: The Transport Layer Security - (TLS) Protocol Version 1.1 + RFC 4346: The Transport Layer Security (TLS) + Protocol Version 1.1 @@ -616,8 +679,7 @@ - RFC 4880: OpenPGP Message - Format + RFC 4880: OpenPGP Message Format @@ -627,8 +689,8 @@ - RFC 5081: Using OpenPGP Keys for - Transport Layer Security + RFC 5081: Using OpenPGP Keys for Transport Layer + Security @@ -640,3 +702,8 @@ + + + + + === modified file 'overview.xml' --- overview.xml 2008-08-19 13:25:14 +0000 +++ overview.xml 2008-09-13 15:36:18 +0000 @@ -1,15 +1,17 @@ - - This is part of the Mandos system for allowing host computers to - have encrypted root file systems and also be capable of remote and - unattended reboots. The host computers run a small client program - in the initial RAM disk environment which will communicate with a - server over a network. The clients are identified by the server - using a OpenPGP key; each client has one unique to it. The server - sends the clients an encrypted password. The encrypted password is - decrypted by the clients using the same OpenPGP key, and the - password is then used to unlock the root file system, whereupon the - host computers can continue booting normally. + This is part of the Mandos system for allowing computers to have + encrypted root file systems and at the same time be capable of + remote and/or unattended reboots. The computers run a small client + program in the initial RAM disk environment which + will communicate with a server over a network. All network + communication is encrypted using TLS. The + clients are identified by the server using an OpenPGP key; each + client has one unique to it. The server sends the clients an + encrypted password. The encrypted password is decrypted by the + clients using the same OpenPGP key, and the password is then used to + unlock the root file system, whereupon the computers can continue + booting normally. === modified file 'plugin-runner.c' --- plugin-runner.c 2008-08-16 18:15:07 +0000 +++ plugin-runner.c 2010-09-09 22:06:10 +0000 @@ -1,8 +1,9 @@ -/* -*- coding: utf-8 -*- */ +/* -*- coding: utf-8; mode: c; mode: orgtbl -*- */ /* * Mandos plugin runner - Run Mandos plugins * - * Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson + * Copyright © 2008,2009 Teddy Hogeborn + * Copyright © 2008,2009 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 @@ -22,14 +23,14 @@ */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), getline(), - asprintf() */ + asprintf(), O_CLOEXEC */ #include /* size_t, NULL */ -#include /* malloc(), exit(), EXIT_FAILURE, - EXIT_SUCCESS, realloc() */ +#include /* malloc(), exit(), EXIT_SUCCESS, + realloc() */ #include /* bool, true, false */ -#include /* perror, popen(), fileno(), - fprintf(), stderr, STDOUT_FILENO */ -#include /* DIR, opendir(), stat(), struct +#include /* fileno(), fprintf(), + stderr, STDOUT_FILENO */ +#include /* DIR, fdopendir(), stat(), struct stat, waitpid(), WIFEXITED(), WEXITSTATUS(), wait(), pid_t, uid_t, gid_t, getuid(), getgid(), @@ -37,52 +38,49 @@ #include /* fd_set, select(), FD_ZERO(), FD_SET(), FD_ISSET(), FD_CLR */ #include /* wait(), waitpid(), WIFEXITED(), - WEXITSTATUS() */ + WEXITSTATUS(), WTERMSIG(), + WCOREDUMP() */ #include /* struct stat, stat(), S_ISREG() */ #include /* and, or, not */ -#include /* DIR, struct dirent, opendir(), +#include /* DIR, struct dirent, fdopendir(), readdir(), closedir(), dirfd() */ #include /* struct stat, stat(), S_ISREG(), fcntl(), setuid(), setgid(), F_GETFD, F_SETFD, FD_CLOEXEC, access(), pipe(), fork(), close() - dup2, STDOUT_FILENO, _exit(), + dup2(), STDOUT_FILENO, _exit(), execv(), write(), read(), close() */ #include /* fcntl(), F_GETFD, F_SETFD, FD_CLOEXEC */ -#include /* strsep, strlen(), asprintf() */ +#include /* strsep, strlen(), asprintf(), + strsignal(), strcmp(), strncmp() */ #include /* errno */ #include /* struct argp_option, struct argp_state, struct argp, argp_parse(), ARGP_ERR_UNKNOWN, - ARGP_KEY_END, ARGP_KEY_ARG, error_t */ + ARGP_KEY_END, ARGP_KEY_ARG, + error_t */ #include /* struct sigaction, sigemptyset(), sigaddset(), sigaction(), sigprocmask(), SIG_BLOCK, SIGCHLD, - SIG_UNBLOCK, kill() */ + SIG_UNBLOCK, kill(), sig_atomic_t + */ #include /* errno, EBADF */ +#include /* intmax_t, PRIdMAX, strtoimax() */ +#include /* EX_OSERR, EX_USAGE, EX_IOERR, + EX_CONFIG, EX_UNAVAILABLE, EX_OK */ +#include /* errno */ +#include /* error() */ #define BUFFER_SIZE 256 -#define ARGFILE "/conf/conf.d/mandos/plugin-runner.conf" - -const char *argp_program_version = "plugin-runner 1.0"; + +#define PDIR "/lib/mandos/plugins.d" +#define AFILE "/conf/conf.d/mandos/plugin-runner.conf" + +const char *argp_program_version = "plugin-runner " VERSION; const char *argp_program_bug_address = ""; -struct process; - -typedef struct process{ - pid_t pid; - int fd; - char *buffer; - size_t buffer_size; - size_t buffer_length; - bool eof; - bool completed; - int status; - struct process *next; -} process; - typedef struct plugin{ char *name; /* can be NULL or any plugin name */ char **argv; @@ -90,49 +88,85 @@ char **environ; int envc; bool disabled; + + /* Variables used for running processes*/ + pid_t pid; + int fd; + char *buffer; + size_t buffer_size; + size_t buffer_length; + bool eof; + volatile sig_atomic_t completed; + int status; struct plugin *next; } plugin; -static plugin *getplugin(char *name, plugin **plugin_list){ - for (plugin *p = *plugin_list; p != NULL; p = p->next){ - if ((p->name == name) - or (p->name and name and (strcmp(p->name, name) == 0))){ +static plugin *plugin_list = NULL; + +/* Gets an existing plugin based on name, + or if none is found, creates a new one */ +static plugin *getplugin(char *name){ + /* Check for existing plugin with that name */ + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if((p->name == name) + or (p->name and name and (strcmp(p->name, name) == 0))){ return p; } } /* Create a new plugin */ - plugin *new_plugin = malloc(sizeof(plugin)); - if (new_plugin == NULL){ - return NULL; - } - char *copy_name = strdup(name); - if(copy_name == NULL){ - return NULL; - } - - *new_plugin = (plugin) { .name = copy_name, - .argc = 1, - .envc = 0, - .disabled = false, - .next = *plugin_list }; - - new_plugin->argv = malloc(sizeof(char *) * 2); - if (new_plugin->argv == NULL){ + plugin *new_plugin = NULL; + do { + new_plugin = malloc(sizeof(plugin)); + } while(new_plugin == NULL and errno == EINTR); + if(new_plugin == NULL){ + return NULL; + } + char *copy_name = NULL; + if(name != NULL){ + do { + copy_name = strdup(name); + } while(copy_name == NULL and errno == EINTR); + if(copy_name == NULL){ + int e = errno; + free(new_plugin); + errno = e; + return NULL; + } + } + + *new_plugin = (plugin){ .name = copy_name, + .argc = 1, + .disabled = false, + .next = plugin_list }; + + do { + new_plugin->argv = malloc(sizeof(char *) * 2); + } while(new_plugin->argv == NULL and errno == EINTR); + if(new_plugin->argv == NULL){ + int e = errno; + free(copy_name); free(new_plugin); + errno = e; return NULL; } new_plugin->argv[0] = copy_name; new_plugin->argv[1] = NULL; - - new_plugin->environ = malloc(sizeof(char *)); + + do { + new_plugin->environ = malloc(sizeof(char *)); + } while(new_plugin->environ == NULL and errno == EINTR); if(new_plugin->environ == NULL){ + int e = errno; + free(copy_name); free(new_plugin->argv); free(new_plugin); + errno = e; return NULL; } new_plugin->environ[0] = NULL; + /* Append the new plugin to the list */ - *plugin_list = new_plugin; + plugin_list = new_plugin; return new_plugin; } @@ -140,14 +174,19 @@ static bool add_to_char_array(const char *new, char ***array, int *len){ /* Resize the pointed-to array to hold one more pointer */ - *array = realloc(*array, sizeof(char *) - * (size_t) ((*len) + 2)); + do { + *array = realloc(*array, sizeof(char *) + * (size_t) ((*len) + 2)); + } while(*array == NULL and errno == EINTR); /* Malloc check */ if(*array == NULL){ return false; } /* Make a copy of the new string */ - char *copy = strdup(new); + char *copy; + do { + copy = strdup(new); + } while(copy == NULL and errno == EINTR); if(copy == NULL){ return false; } @@ -168,58 +207,87 @@ } /* Add to a plugin's environment */ -static bool add_environment(plugin *p, const char *def){ +static bool add_environment(plugin *p, const char *def, bool replace){ if(p == NULL){ return false; } + /* namelen = length of name of environment variable */ + size_t namelen = (size_t)(strchrnul(def, '=') - def); + /* Search for this environment variable */ + for(char **e = p->environ; *e != NULL; e++){ + if(strncmp(*e, def, namelen + 1) == 0){ + /* It already exists */ + if(replace){ + char *new; + do { + new = realloc(*e, strlen(def) + 1); + } while(new == NULL and errno == EINTR); + if(new == NULL){ + return false; + } + *e = new; + strcpy(*e, def); + } + return true; + } + } return add_to_char_array(def, &(p->environ), &(p->envc)); } - /* * Based on the example in the GNU LibC manual chapter 13.13 "File * Descriptor Flags". - * *Note File Descriptor Flags:(libc)Descriptor Flags. + | [[info:libc:Descriptor%20Flags][File Descriptor Flags]] | */ -static int set_cloexec_flag(int fd) -{ - int ret = fcntl(fd, F_GETFD, 0); +static int set_cloexec_flag(int fd){ + int ret = (int)TEMP_FAILURE_RETRY(fcntl(fd, F_GETFD, 0)); /* If reading the flags failed, return error indication now. */ if(ret < 0){ return ret; } /* Store modified flag word in the descriptor. */ - return fcntl(fd, F_SETFD, ret | FD_CLOEXEC); + return (int)TEMP_FAILURE_RETRY(fcntl(fd, F_SETFD, + ret | FD_CLOEXEC)); } -process *process_list = NULL; -/* Mark a process as completed when it exits, and save its exit +/* Mark processes as completed when they exit, and save their exit status. */ -void handle_sigchld(__attribute__((unused)) int sig){ - process *proc = process_list; - int status; - pid_t pid = wait(&status); - if(pid == -1){ - perror("wait"); - return; - } - while(proc != NULL and proc->pid != pid){ - proc = proc->next; - } - if(proc == NULL){ - /* Process not found in process list */ - return; - } - proc->status = status; - proc->completed = true; +static void handle_sigchld(__attribute__((unused)) int sig){ + int old_errno = errno; + while(true){ + plugin *proc = plugin_list; + int status; + pid_t pid = waitpid(-1, &status, WNOHANG); + if(pid == 0){ + /* Only still running child processes */ + break; + } + if(pid == -1){ + if(errno == ECHILD){ + /* No child processes */ + break; + } + error(0, errno, "waitpid"); + } + + /* A child exited, find it in process_list */ + while(proc != NULL and proc->pid != pid){ + proc = proc->next; + } + if(proc == NULL){ + /* Process not found in process list */ + continue; + } + proc->status = status; + proc->completed = 1; + } + errno = old_errno; } -bool print_out_password(const char *buffer, size_t length){ +/* Prints out a password to stdout */ +static bool print_out_password(const char *buffer, size_t length){ ssize_t ret; - if(length>0 and buffer[length-1] == '\n'){ - length--; - } for(size_t written = 0; written < length; written += (size_t)ret){ ret = TEMP_FAILURE_RETRY(write(STDOUT_FILENO, buffer + written, length - written)); @@ -230,46 +298,45 @@ return true; } -char **add_to_argv(char **argv, int *argc, char *arg){ - if (argv == NULL){ - *argc = 1; - argv = malloc(sizeof(char*) * 2); - if(argv == NULL){ - return NULL; +/* Removes and free a plugin from the plugin list */ +static void free_plugin(plugin *plugin_node){ + + for(char **arg = plugin_node->argv; *arg != NULL; arg++){ + free(*arg); + } + free(plugin_node->argv); + for(char **env = plugin_node->environ; *env != NULL; env++){ + free(*env); + } + free(plugin_node->environ); + free(plugin_node->buffer); + + /* Removes the plugin from the singly-linked list */ + if(plugin_node == plugin_list){ + /* First one - simple */ + plugin_list = plugin_list->next; + } else { + /* Second one or later */ + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if(p->next == plugin_node){ + p->next = plugin_node->next; + break; + } } - argv[0] = NULL; /* Will be set to argv[0] in main before parsing */ - argv[1] = NULL; - } - *argc += 1; - argv = realloc(argv, sizeof(char *) - * ((unsigned int) *argc + 1)); - if(argv == NULL){ - return NULL; - } - argv[*argc-1] = arg; - argv[*argc] = NULL; - return argv; + } + + free(plugin_node); } -static void free_plugin_list(plugin *plugin_list){ - for(plugin *next = plugin_list; plugin_list != NULL; plugin_list = next){ - next = plugin_list->next; - free(plugin_list->name); - for(char **arg = plugin_list->argv; *arg != NULL; arg++){ - free(*arg); - } - free(plugin_list->argv); - for(char **env = plugin_list->environ; *env != NULL; env++){ - free(*env); - } - free(plugin_list->environ); - free(plugin_list); - } +static void free_plugin_list(void){ + while(plugin_list != NULL){ + free_plugin(plugin_list); + } } int main(int argc, char *argv[]){ - const char *plugindir = "/lib/mandos/plugins.d"; - const char *argfile = ARGFILE; + char *plugindir = NULL; + char *argfile = NULL; FILE *conffp; size_t d_name_len; DIR *dir = NULL; @@ -277,6 +344,7 @@ struct stat st; fd_set rfds_all; int ret, maxfd = 0; + ssize_t sret; uid_t uid = 65534; gid_t gid = 65534; bool debug = false; @@ -291,14 +359,14 @@ sigemptyset(&sigchld_action.sa_mask); ret = sigaddset(&sigchld_action.sa_mask, SIGCHLD); if(ret == -1){ - perror("sigaddset"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; goto fallback; } ret = sigaction(SIGCHLD, &sigchld_action, &old_sigchld_action); if(ret == -1){ - perror("sigaction"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto fallback; } @@ -307,168 +375,274 @@ { .name = "global-options", .key = 'g', .arg = "OPTION[,OPTION[,...]]", .doc = "Options passed to all plugins" }, - { .name = "global-envs", .key = 'e', + { .name = "global-env", .key = 'G', .arg = "VAR=value", .doc = "Environment variable passed to all plugins" }, { .name = "options-for", .key = 'o', .arg = "PLUGIN:OPTION[,OPTION[,...]]", .doc = "Options passed only to specified plugin" }, - { .name = "envs-for", .key = 'f', + { .name = "env-for", .key = 'E', .arg = "PLUGIN:ENV=value", .doc = "Environment variable passed to specified plugin" }, { .name = "disable", .key = 'd', .arg = "PLUGIN", .doc = "Disable a specific plugin", .group = 1 }, + { .name = "enable", .key = 'e', + .arg = "PLUGIN", + .doc = "Enable a specific plugin", .group = 1 }, { .name = "plugin-dir", .key = 128, .arg = "DIRECTORY", .doc = "Specify a different plugin directory", .group = 2 }, - { .name = "userid", .key = 129, - .arg = "ID", .flags = 0, - .doc = "User ID the plugins will run as", .group = 2 }, - { .name = "groupid", .key = 130, - .arg = "ID", .flags = 0, - .doc = "Group ID the plugins will run as", .group = 2 }, - { .name = "debug", .key = 131, - .doc = "Debug mode", .group = 3 }, + { .name = "config-file", .key = 129, + .arg = "FILE", + .doc = "Specify a different configuration file", .group = 2 }, + { .name = "userid", .key = 130, + .arg = "ID", .flags = 0, + .doc = "User ID the plugins will run as", .group = 3 }, + { .name = "groupid", .key = 131, + .arg = "ID", .flags = 0, + .doc = "Group ID the plugins will run as", .group = 3 }, + { .name = "debug", .key = 132, + .doc = "Debug mode", .group = 4 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, { .name = NULL } }; - error_t parse_opt (int key, char *arg, struct argp_state *state) { - /* Get the INPUT argument from `argp_parse', which we know is a - pointer to our plugin list pointer. */ - plugin **plugins = state->input; - switch (key) { - case 'g': - if (arg != NULL){ - char *p; - while((p = strsep(&arg, ",")) != NULL){ - if(p[0] == '\0'){ - continue; - } - if(not add_argument(getplugin(NULL, plugins), p)){ - perror("add_argument"); - return ARGP_ERR_UNKNOWN; - } - } - } - break; - case 'e': - if(arg == NULL){ - break; - } - { - char *envdef = strdup(arg); - if(envdef == NULL){ - break; - } - if(not add_environment(getplugin(NULL, plugins), envdef)){ - perror("add_environment"); - } - } - break; - case 'o': - if (arg != NULL){ - char *p_name = strsep(&arg, ":"); - if(p_name[0] == '\0'){ - break; - } - char *opt = strsep(&arg, ":"); - if(opt[0] == '\0'){ - break; - } - if(opt != NULL){ - char *p; - while((p = strsep(&opt, ",")) != NULL){ - if(p[0] == '\0'){ - continue; - } - if(not add_argument(getplugin(p_name, plugins), p)){ - perror("add_argument"); - return ARGP_ERR_UNKNOWN; - } - } - } - } - break; - case 'f': - if(arg == NULL){ - break; - } + error_t parse_opt(int key, char *arg, struct argp_state *state){ + errno = 0; + switch(key){ + char *tmp; + intmax_t tmpmax; + case 'g': /* --global-options */ + { + char *plugin_option; + while((plugin_option = strsep(&arg, ",")) != NULL){ + if(not add_argument(getplugin(NULL), plugin_option)){ + break; + } + } + } + break; + case 'G': /* --global-env */ + add_environment(getplugin(NULL), arg, true); + break; + case 'o': /* --options-for */ + { + char *option_list = strchr(arg, ':'); + if(option_list == NULL){ + argp_error(state, "No colon in \"%s\"", arg); + errno = EINVAL; + break; + } + *option_list = '\0'; + option_list++; + if(arg[0] == '\0'){ + argp_error(state, "Empty plugin name"); + errno = EINVAL; + break; + } + char *option; + while((option = strsep(&option_list, ",")) != NULL){ + if(not add_argument(getplugin(arg), option)){ + break; + } + } + } + break; + case 'E': /* --env-for */ { char *envdef = strchr(arg, ':'); if(envdef == NULL){ - break; - } - char *p_name = strndup(arg, (size_t) (envdef-arg)); - if(p_name == NULL){ - break; - } + argp_error(state, "No colon in \"%s\"", arg); + errno = EINVAL; + break; + } + *envdef = '\0'; envdef++; - if(not add_environment(getplugin(p_name, plugins), envdef)){ - perror("add_environment"); - } - } - break; - case 'd': - if (arg != NULL){ - plugin *p = getplugin(arg, plugins); - if(p == NULL){ - return ARGP_ERR_UNKNOWN; - } - p->disabled = true; - } - break; - case 128: - plugindir = arg; - break; - case 129: - uid = (uid_t)strtol(arg, NULL, 10); - break; - case 130: - gid = (gid_t)strtol(arg, NULL, 10); - break; - case 131: + if(arg[0] == '\0'){ + argp_error(state, "Empty plugin name"); + errno = EINVAL; + break; + } + add_environment(getplugin(arg), envdef, true); + } + break; + case 'd': /* --disable */ + { + plugin *p = getplugin(arg); + if(p != NULL){ + p->disabled = true; + } + } + break; + case 'e': /* --enable */ + { + plugin *p = getplugin(arg); + if(p != NULL){ + p->disabled = false; + } + } + break; + case 128: /* --plugin-dir */ + free(plugindir); + plugindir = strdup(arg); + break; + case 129: /* --config-file */ + /* This is already done by parse_opt_config_file() */ + break; + case 130: /* --userid */ + tmpmax = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmpmax != (uid_t)tmpmax){ + argp_error(state, "Bad user ID number: \"%s\", using %" + PRIdMAX, arg, (intmax_t)uid); + break; + } + uid = (uid_t)tmpmax; + break; + case 131: /* --groupid */ + tmpmax = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmpmax != (gid_t)tmpmax){ + argp_error(state, "Bad group ID number: \"%s\", using %" + PRIdMAX, arg, (intmax_t)gid); + break; + } + gid = (gid_t)tmpmax; + break; + case 132: /* --debug */ debug = true; break; - case ARGP_KEY_ARG: - fprintf(stderr, "Ignoring unknown argument \"%s\"\n", arg); - break; - case ARGP_KEY_END: - break; - default: - return ARGP_ERR_UNKNOWN; - } - return 0; - } - - plugin *plugin_list = NULL; - - struct argp argp = { .options = options, .parser = parse_opt, - .args_doc = "[+PLUS_SEPARATED_OPTIONS]", + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ + argp_state_help(state, state->out_stream, ARGP_HELP_STD_HELP); + case -3: /* --usage */ + state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_OK); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(EXIT_SUCCESS); + break; +/* + * When adding more options before this line, remember to also add a + * "case" to the "parse_opt_config_file" function below. + */ + case ARGP_KEY_ARG: + /* Cryptsetup always passes an argument, which is an empty + string if "none" was specified in /etc/crypttab. So if + argument was empty, we ignore it silently. */ + if(arg[0] == '\0'){ + break; + } + default: + return ARGP_ERR_UNKNOWN; + } + return errno; /* Set to 0 at start */ + } + + /* This option parser is the same as parse_opt() above, except it + ignores everything but the --config-file option. */ + error_t parse_opt_config_file(int key, char *arg, + __attribute__((unused)) + struct argp_state *state){ + errno = 0; + switch(key){ + case 'g': /* --global-options */ + case 'G': /* --global-env */ + case 'o': /* --options-for */ + case 'E': /* --env-for */ + case 'd': /* --disable */ + case 'e': /* --enable */ + case 128: /* --plugin-dir */ + break; + case 129: /* --config-file */ + free(argfile); + argfile = strdup(arg); + break; + case 130: /* --userid */ + case 131: /* --groupid */ + case 132: /* --debug */ + case '?': /* --help */ + case -3: /* --usage */ + case 'V': /* --version */ + case ARGP_KEY_ARG: + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, + .parser = parse_opt_config_file, + .args_doc = "", .doc = "Mandos plugin runner -- Run plugins" }; - ret = argp_parse (&argp, argc, argv, 0, 0, &plugin_list); - if (ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - exitstatus = EXIT_FAILURE; - goto fallback; - } - - conffp = fopen(argfile, "r"); + /* Parse using parse_opt_config_file() in order to get the custom + config file location, if any. */ + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_USAGE; + goto fallback; + } + + /* Reset to the normal argument parser */ + argp.parser = parse_opt; + + /* Open the configfile if available */ + if(argfile == NULL){ + conffp = fopen(AFILE, "r"); + } else { + conffp = fopen(argfile, "r"); + } if(conffp != NULL){ char *org_line = NULL; char *p, *arg, *new_arg, *line; size_t size = 0; - ssize_t sret; const char whitespace_delims[] = " \r\t\f\v\n"; const char comment_delim[] = "#"; - + + custom_argc = 1; + custom_argv = malloc(sizeof(char*) * 2); + if(custom_argv == NULL){ + error(0, errno, "malloc"); + exitstatus = EX_OSERR; + goto fallback; + } + custom_argv[0] = argv[0]; + custom_argv[1] = NULL; + + /* for each line in the config file, strip whitespace and ignore + commented text */ while(true){ sret = getline(&org_line, &size, conffp); if(sret == -1){ break; } - + line = org_line; arg = strsep(&line, comment_delim); while((p = strsep(&arg, whitespace_delims)) != NULL){ @@ -476,35 +650,84 @@ continue; } new_arg = strdup(p); - custom_argv = add_to_argv(custom_argv, &custom_argc, new_arg); - if (custom_argv == NULL){ - perror("add_to_argv"); - exitstatus = EXIT_FAILURE; - goto fallback; - } + if(new_arg == NULL){ + error(0, errno, "strdup"); + exitstatus = EX_OSERR; + free(org_line); + goto fallback; + } + + custom_argc += 1; + custom_argv = realloc(custom_argv, sizeof(char *) + * ((unsigned int) custom_argc + 1)); + if(custom_argv == NULL){ + error(0, errno, "realloc"); + exitstatus = EX_OSERR; + free(org_line); + goto fallback; + } + custom_argv[custom_argc-1] = new_arg; + custom_argv[custom_argc] = NULL; } } + do { + ret = fclose(conffp); + } while(ret == EOF and errno == EINTR); + if(ret == EOF){ + error(0, errno, "fclose"); + exitstatus = EX_IOERR; + goto fallback; + } free(org_line); - } else{ + } else { /* Check for harmful errors and go to fallback. Other errors might not affect opening plugins */ - if (errno == EMFILE or errno == ENFILE or errno == ENOMEM){ - perror("fopen"); - exitstatus = EXIT_FAILURE; + if(errno == EMFILE or errno == ENFILE or errno == ENOMEM){ + error(0, errno, "fopen"); + exitstatus = EX_OSERR; goto fallback; } } - + /* If there were any arguments from the configuration file, pass + them to parser as command line arguments */ if(custom_argv != NULL){ - custom_argv[0] = argv[0]; - ret = argp_parse (&argp, custom_argc, custom_argv, 0, 0, &plugin_list); - if (ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - exitstatus = EXIT_FAILURE; + ret = argp_parse(&argp, custom_argc, custom_argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_CONFIG; goto fallback; } } + /* Parse actual command line arguments, to let them override the + config file */ + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_USAGE; + goto fallback; + } + if(debug){ for(plugin *p = plugin_list; p != NULL; p=p->next){ fprintf(stderr, "Plugin: %s has %d arguments\n", @@ -512,53 +735,82 @@ for(char **a = p->argv; *a != NULL; a++){ fprintf(stderr, "\tArg: %s\n", *a); } - fprintf(stderr, "...and %u environment variables\n", p->envc); + fprintf(stderr, "...and %d environment variables\n", p->envc); for(char **a = p->environ; *a != NULL; a++){ fprintf(stderr, "\t%s\n", *a); } } } + /* Strip permissions down to nobody */ + setgid(gid); + if(ret == -1){ + error(0, errno, "setgid"); + } ret = setuid(uid); - if (ret == -1){ - perror("setuid"); - } - - setgid(gid); - if (ret == -1){ - perror("setgid"); - } - - dir = opendir(plugindir); - if(dir == NULL){ - perror("Could not open plugin dir"); - exitstatus = EXIT_FAILURE; - goto fallback; - } - - /* Set the FD_CLOEXEC flag on the directory, if possible */ + if(ret == -1){ + error(0, errno, "setuid"); + } + + /* Open plugin directory with close_on_exec flag */ { - int dir_fd = dirfd(dir); - if(dir_fd >= 0){ - ret = set_cloexec_flag(dir_fd); - if(ret < 0){ - perror("set_cloexec_flag"); - exitstatus = EXIT_FAILURE; - goto fallback; - } + int dir_fd = -1; + if(plugindir == NULL){ + dir_fd = open(PDIR, O_RDONLY | +#ifdef O_CLOEXEC + O_CLOEXEC +#else /* not O_CLOEXEC */ + 0 +#endif /* not O_CLOEXEC */ + ); + } else { + dir_fd = open(plugindir, O_RDONLY | +#ifdef O_CLOEXEC + O_CLOEXEC +#else /* not O_CLOEXEC */ + 0 +#endif /* not O_CLOEXEC */ + ); + } + if(dir_fd == -1){ + error(0, errno, "Could not open plugin dir"); + exitstatus = EX_UNAVAILABLE; + goto fallback; + } + +#ifndef O_CLOEXEC + /* Set the FD_CLOEXEC flag on the directory */ + ret = set_cloexec_flag(dir_fd); + if(ret < 0){ + error(0, errno, "set_cloexec_flag"); + TEMP_FAILURE_RETRY(close(dir_fd)); + exitstatus = EX_OSERR; + goto fallback; + } +#endif /* O_CLOEXEC */ + + dir = fdopendir(dir_fd); + if(dir == NULL){ + error(0, errno, "Could not open plugin dir"); + TEMP_FAILURE_RETRY(close(dir_fd)); + exitstatus = EX_OSERR; + goto fallback; } } FD_ZERO(&rfds_all); + /* Read and execute any executable in the plugin directory*/ while(true){ - dirst = readdir(dir); + do { + dirst = readdir(dir); + } while(dirst == NULL and errno == EINTR); - // All directory entries have been processed + /* All directory entries have been processed */ if(dirst == NULL){ - if (errno == EBADF){ - perror("readdir"); - exitstatus = EXIT_FAILURE; + if(errno == EBADF){ + error(0, errno, "readdir"); + exitstatus = EX_IOERR; goto fallback; } break; @@ -566,7 +818,7 @@ d_name_len = strlen(dirst->d_name); - // Ignore dotfiles, backup files and other junk + /* Ignore dotfiles, backup files and other junk */ { bool bad_name = false; @@ -574,6 +826,7 @@ const char const *bad_suffixes[] = { "~", "#", ".dpkg-new", ".dpkg-old", + ".dpkg-bak", ".dpkg-divert", NULL }; for(const char **pre = bad_prefixes; *pre != NULL; pre++){ size_t pre_len = strlen(*pre); @@ -587,15 +840,13 @@ break; } } - if(bad_name){ continue; } - for(const char **suf = bad_suffixes; *suf != NULL; suf++){ size_t suf_len = strlen(*suf); if((d_name_len >= suf_len) - and (strcmp((dirst->d_name)+d_name_len-suf_len, *suf) + and (strcmp((dirst->d_name) + d_name_len-suf_len, *suf) == 0)){ if(debug){ fprintf(stderr, "Ignoring plugin dir entry \"%s\"" @@ -610,22 +861,31 @@ continue; } } - + char *filename; - ret = asprintf(&filename, "%s/%s", plugindir, dirst->d_name); + if(plugindir == NULL){ + ret = (int)TEMP_FAILURE_RETRY(asprintf(&filename, PDIR "/%s", + dirst->d_name)); + } else { + ret = (int)TEMP_FAILURE_RETRY(asprintf(&filename, "%s/%s", + plugindir, + dirst->d_name)); + } if(ret < 0){ - perror("asprintf"); + error(0, errno, "asprintf"); continue; } - ret = stat(filename, &st); - if (ret == -1){ - perror("stat"); + ret = (int)TEMP_FAILURE_RETRY(stat(filename, &st)); + if(ret == -1){ + error(0, errno, "stat"); free(filename); continue; } - if (not S_ISREG(st.st_mode) or (access(filename, X_OK) != 0)){ + /* Ignore non-executable files */ + if(not S_ISREG(st.st_mode) + or (TEMP_FAILURE_RETRY(access(filename, X_OK)) != 0)){ if(debug){ fprintf(stderr, "Ignoring plugin dir entry \"%s\"" " with bad type or mode\n", filename); @@ -633,9 +893,10 @@ free(filename); continue; } - plugin *p = getplugin(dirst->d_name, &plugin_list); + + plugin *p = getplugin(dirst->d_name); if(p == NULL){ - perror("getplugin"); + error(0, errno, "getplugin"); free(filename); continue; } @@ -649,17 +910,17 @@ } { /* Add global arguments to argument list for this plugin */ - plugin *g = getplugin(NULL, &plugin_list); + plugin *g = getplugin(NULL); if(g != NULL){ for(char **a = g->argv + 1; *a != NULL; a++){ if(not add_argument(p, *a)){ - perror("add_argument"); + error(0, errno, "add_argument"); } } /* Add global environment variables */ for(char **e = g->environ; *e != NULL; e++){ - if(not add_environment(p, *e)){ - perror("add_environment"); + if(not add_environment(p, *e, false)){ + error(0, errno, "add_environment"); } } } @@ -669,67 +930,68 @@ process, too. */ if(p->environ[0] != NULL){ for(char **e = environ; *e != NULL; e++){ - char *copy = strdup(*e); - if(copy == NULL){ - perror("strdup"); - continue; - } - if(not add_environment(p, copy)){ - perror("add_environment"); + if(not add_environment(p, *e, false)){ + error(0, errno, "add_environment"); } } } - int pipefd[2]; - ret = pipe(pipefd); - if (ret == -1){ - perror("pipe"); - exitstatus = EXIT_FAILURE; + int pipefd[2]; + ret = (int)TEMP_FAILURE_RETRY(pipe(pipefd)); + if(ret == -1){ + error(0, errno, "pipe"); + exitstatus = EX_OSERR; goto fallback; } + /* Ask OS to automatic close the pipe on exec */ ret = set_cloexec_flag(pipefd[0]); if(ret < 0){ - perror("set_cloexec_flag"); - exitstatus = EXIT_FAILURE; + error(0, errno, "set_cloexec_flag"); + exitstatus = EX_OSERR; goto fallback; } ret = set_cloexec_flag(pipefd[1]); if(ret < 0){ - perror("set_cloexec_flag"); - exitstatus = EXIT_FAILURE; + error(0, errno, "set_cloexec_flag"); + exitstatus = EX_OSERR; goto fallback; } /* Block SIGCHLD until process is safely in process list */ - ret = sigprocmask (SIG_BLOCK, &sigchld_action.sa_mask, NULL); + ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_BLOCK, + &sigchld_action.sa_mask, + NULL)); if(ret < 0){ - perror("sigprocmask"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; goto fallback; } - // Starting a new process to be watched - pid_t pid = fork(); + /* Starting a new process to be watched */ + pid_t pid; + do { + pid = fork(); + } while(pid == -1 and errno == EINTR); if(pid == -1){ - perror("fork"); - exitstatus = EXIT_FAILURE; + error(0, errno, "fork"); + exitstatus = EX_OSERR; goto fallback; } if(pid == 0){ /* this is the child process */ ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); if(ret < 0){ - perror("sigaction"); - _exit(EXIT_FAILURE); + error(0, errno, "sigaction"); + _exit(EX_OSERR); } - ret = sigprocmask (SIG_UNBLOCK, &sigchld_action.sa_mask, NULL); + ret = sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL); if(ret < 0){ - perror("sigprocmask"); - _exit(EXIT_FAILURE); + error(0, errno, "sigprocmask"); + _exit(EX_OSERR); } - + ret = dup2(pipefd[1], STDOUT_FILENO); /* replace our stdout */ if(ret == -1){ - perror("dup2"); - _exit(EXIT_FAILURE); + error(0, errno, "dup2"); + _exit(EX_OSERR); } if(dirfd(dir) < 0){ @@ -739,224 +1001,258 @@ } if(p->environ[0] == NULL){ if(execv(filename, p->argv) < 0){ - perror("execv"); - _exit(EXIT_FAILURE); + error(0, errno, "execv for %s", filename); + _exit(EX_OSERR); } } else { if(execve(filename, p->argv, p->environ) < 0){ - perror("execve"); - _exit(EXIT_FAILURE); + error(0, errno, "execve for %s", filename); + _exit(EX_OSERR); } } /* no return */ } - /* parent process */ + /* Parent process */ + TEMP_FAILURE_RETRY(close(pipefd[1])); /* Close unused write end of + pipe */ free(filename); - close(pipefd[1]); /* close unused write end of pipe */ - process *new_process = malloc(sizeof(process)); - if (new_process == NULL){ - perror("malloc"); - ret = sigprocmask (SIG_UNBLOCK, &sigchld_action.sa_mask, NULL); + plugin *new_plugin = getplugin(dirst->d_name); + if(new_plugin == NULL){ + error(0, errno, "getplugin"); + ret = (int)(TEMP_FAILURE_RETRY + (sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, + NULL))); if(ret < 0){ - perror("sigprocmask"); + error(0, errno, "sigprocmask"); } - exitstatus = EXIT_FAILURE; + exitstatus = EX_OSERR; goto fallback; } - *new_process = (struct process){ .pid = pid, - .fd = pipefd[0], - .next = process_list }; - // List handling - process_list = new_process; + new_plugin->pid = pid; + new_plugin->fd = pipefd[0]; + /* Unblock SIGCHLD so signal handler can be run if this process has already completed */ - ret = sigprocmask (SIG_UNBLOCK, &sigchld_action.sa_mask, NULL); + ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_UNBLOCK, + &sigchld_action.sa_mask, + NULL)); if(ret < 0){ - perror("sigprocmask"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; goto fallback; } - FD_SET(new_process->fd, &rfds_all); + FD_SET(new_plugin->fd, &rfds_all); /* Spurious warning from + -Wconversion */ - if (maxfd < new_process->fd){ - maxfd = new_process->fd; + if(maxfd < new_plugin->fd){ + maxfd = new_plugin->fd; } - } - free_plugin_list(plugin_list); - - closedir(dir); + TEMP_FAILURE_RETRY(closedir(dir)); dir = NULL; - - if (process_list == NULL){ - fprintf(stderr, "No plugin processes started. Incorrect plugin" - " directory?\n"); - process_list = NULL; + free_plugin(getplugin(NULL)); + + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if(p->pid != 0){ + break; + } + if(p->next == NULL){ + fprintf(stderr, "No plugin processes started. Incorrect plugin" + " directory?\n"); + free_plugin_list(); + } } - while(process_list){ + + /* Main loop while running plugins exist */ + while(plugin_list){ fd_set rfds = rfds_all; int select_ret = select(maxfd+1, &rfds, NULL, NULL, NULL); - if (select_ret == -1){ - perror("select"); - exitstatus = EXIT_FAILURE; + if(select_ret == -1 and errno != EINTR){ + error(0, errno, "select"); + exitstatus = EX_OSERR; goto fallback; } /* OK, now either a process completed, or something can be read from one of them */ - for(process *proc = process_list; proc ; proc = proc->next){ + for(plugin *proc = plugin_list; proc != NULL;){ /* Is this process completely done? */ - if(proc->eof and proc->completed){ + if(proc->completed and proc->eof){ /* Only accept the plugin output if it exited cleanly */ if(not WIFEXITED(proc->status) or WEXITSTATUS(proc->status) != 0){ /* Bad exit by plugin */ + if(debug){ if(WIFEXITED(proc->status)){ - fprintf(stderr, "Plugin %u exited with status %d\n", - (unsigned int) (proc->pid), + fprintf(stderr, "Plugin %s [%" PRIdMAX "] exited with" + " status %d\n", proc->name, + (intmax_t) (proc->pid), WEXITSTATUS(proc->status)); - } else if(WIFSIGNALED(proc->status)) { - fprintf(stderr, "Plugin %u killed by signal %d\n", - (unsigned int) (proc->pid), - WTERMSIG(proc->status)); + } else if(WIFSIGNALED(proc->status)){ + fprintf(stderr, "Plugin %s [%" PRIdMAX "] killed by" + " signal %d: %s\n", proc->name, + (intmax_t) (proc->pid), + WTERMSIG(proc->status), + strsignal(WTERMSIG(proc->status))); } else if(WCOREDUMP(proc->status)){ - fprintf(stderr, "Plugin %d dumped core\n", - (unsigned int) (proc->pid)); + fprintf(stderr, "Plugin %s [%" PRIdMAX "] dumped" + " core\n", proc->name, (intmax_t) (proc->pid)); } } + /* Remove the plugin */ - FD_CLR(proc->fd, &rfds_all); + FD_CLR(proc->fd, &rfds_all); /* Spurious warning from + -Wconversion */ + /* Block signal while modifying process_list */ - ret = sigprocmask (SIG_BLOCK, &sigchld_action.sa_mask, NULL); + ret = (int)TEMP_FAILURE_RETRY(sigprocmask + (SIG_BLOCK, + &sigchld_action.sa_mask, + NULL)); if(ret < 0){ - perror("sigprocmask"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; goto fallback; } - /* Delete this process entry from the list */ - if(process_list == proc){ - /* First one - simple */ - process_list = proc->next; - } else { - /* Second one or later */ - for(process *p = process_list; p != NULL; p = p->next){ - if(p->next == proc){ - p->next = proc->next; - break; - } - } - } + + plugin *next_plugin = proc->next; + free_plugin(proc); + proc = next_plugin; + /* We are done modifying process list, so unblock signal */ - ret = sigprocmask (SIG_UNBLOCK, &sigchld_action.sa_mask, - NULL); + ret = (int)(TEMP_FAILURE_RETRY + (sigprocmask(SIG_UNBLOCK, + &sigchld_action.sa_mask, NULL))); if(ret < 0){ - perror("sigprocmask"); - } - free(proc->buffer); - free(proc); - /* We deleted this process from the list, so we can't go - proc->next. Therefore, start over from the beginning of - the process list */ - break; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; + goto fallback; + } + + if(plugin_list == NULL){ + break; + } + + continue; } + /* This process exited nicely, so print its buffer */ - - bool bret = print_out_password(proc->buffer, proc->buffer_length); + + bool bret = print_out_password(proc->buffer, + proc->buffer_length); if(not bret){ - perror("print_out_password"); - exitstatus = EXIT_FAILURE; + error(0, errno, "print_out_password"); + exitstatus = EX_IOERR; } goto fallback; } + /* This process has not completed. Does it have any output? */ - if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ + if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ /* Spurious + warning from + -Wconversion */ /* This process had nothing to say at this time */ + proc = proc->next; continue; } /* Before reading, make the process' data buffer large enough */ if(proc->buffer_length + BUFFER_SIZE > proc->buffer_size){ proc->buffer = realloc(proc->buffer, proc->buffer_size + (size_t) BUFFER_SIZE); - if (proc->buffer == NULL){ - perror("malloc"); - exitstatus = EXIT_FAILURE; + if(proc->buffer == NULL){ + error(0, errno, "malloc"); + exitstatus = EX_OSERR; goto fallback; } proc->buffer_size += BUFFER_SIZE; } /* Read from the process */ - ret = read(proc->fd, proc->buffer + proc->buffer_length, - BUFFER_SIZE); - if(ret < 0){ + sret = TEMP_FAILURE_RETRY(read(proc->fd, + proc->buffer + + proc->buffer_length, + BUFFER_SIZE)); + if(sret < 0){ /* Read error from this process; ignore the error */ + proc = proc->next; continue; } - if(ret == 0){ + if(sret == 0){ /* got EOF */ proc->eof = true; } else { - proc->buffer_length += (size_t) ret; + proc->buffer_length += (size_t) sret; } } } - - + + fallback: - if(process_list == NULL or exitstatus != EXIT_SUCCESS){ - /* Fallback if all plugins failed, none are found or an error occured */ + if(plugin_list == NULL or (exitstatus != EXIT_SUCCESS + and exitstatus != EX_OK)){ + /* Fallback if all plugins failed, none are found or an error + occured */ bool bret; fprintf(stderr, "Going to fallback mode using getpass(3)\n"); char *passwordbuffer = getpass("Password: "); - bret = print_out_password(passwordbuffer, strlen(passwordbuffer)); + size_t len = strlen(passwordbuffer); + /* Strip trailing newline */ + if(len > 0 and passwordbuffer[len-1] == '\n'){ + passwordbuffer[len-1] = '\0'; /* not strictly necessary */ + len--; + } + bret = print_out_password(passwordbuffer, len); if(not bret){ - perror("print_out_password"); - exitstatus = EXIT_FAILURE; + error(0, errno, "print_out_password"); + exitstatus = EX_IOERR; } } /* Restore old signal handler */ ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); if(ret == -1){ - perror("sigaction"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; } - + if(custom_argv != NULL){ - for(char **arg = custom_argv; *arg != NULL; arg++){ + for(char **arg = custom_argv+1; *arg != NULL; arg++){ free(*arg); } free(custom_argv); } - free_plugin_list(plugin_list); if(dir != NULL){ closedir(dir); } - /* Free the process list and kill the processes */ - for(process *next; process_list != NULL; process_list = next){ - next = process_list->next; - close(process_list->fd); - ret = kill(process_list->pid, SIGTERM); - if(ret == -1 and errno != ESRCH){ - /* set-uid proccesses migth not get closed */ - perror("kill"); + /* Kill the processes */ + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if(p->pid != 0){ + close(p->fd); + ret = kill(p->pid, SIGTERM); + if(ret == -1 and errno != ESRCH){ + /* Set-uid proccesses might not get closed */ + error(0, errno, "kill"); + } } - free(process_list->buffer); - free(process_list); } /* Wait for any remaining child processes to terminate */ - do{ + do { ret = wait(NULL); } while(ret >= 0); if(errno != ECHILD){ - perror("wait"); + error(0, errno, "wait"); } + free_plugin_list(); + + free(plugindir); + free(argfile); + return exitstatus; } === added file 'plugin-runner.conf' --- plugin-runner.conf 1970-01-01 00:00:00 +0000 +++ plugin-runner.conf 2009-04-17 08:26:17 +0000 @@ -0,0 +1,10 @@ +## This is the configuration file for plugin-runner(8mandos). This +## file should be installed as "/etc/mandos/plugin-runner.conf", and +## will be copied to "/conf/conf.d/mandos/plugin-runner.conf" in the +## initrd.img file. +## +## After editing this file, the initrd image file must be updated for +## the changes to take effect! + +## Example: +#--options-for=mandos-client:--debug === modified file 'plugin-runner.xml' --- plugin-runner.xml 2008-08-16 20:31:21 +0000 +++ plugin-runner.xml 2009-01-18 06:41:57 +0000 @@ -1,18 +1,19 @@ - - + + + +%common; ]> - + - &COMMANDNAME; - - &COMMANDNAME; - &VERSION; + Mandos Manual + + Mandos + &version; + &TIMESTAMP; Björn @@ -31,33 +32,13 @@ 2008 - Teddy Hogeborn & Björn Påhlsson + 2009 + Teddy Hogeborn + Björn Påhlsson - - - This manual page 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 manual page 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 - . - - + - + &COMMANDNAME; 8mandos @@ -66,209 +47,594 @@ &COMMANDNAME; - get password for encrypted rootdisk + Run Mandos plugins, pass data from first to succeed. - + &COMMANDNAME; - --global-optionsOPTIONS - --options-forPLUGIN:OPTIONS - --disablePLUGIN - --groupidID - --useridID - --plugin-dirDIRECTORY - --debug - - - &COMMANDNAME; - --help - - - &COMMANDNAME; - --usage - - - &COMMANDNAME; - --version - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + - + DESCRIPTION - &COMMANDNAME; is a plugin runner that waits - for any of its plugins to return sucessfull with a password, and - passes it to cryptsetup as stdout message. This command is not - meant to be invoked directly, but is instead meant to be run by - cryptsetup by being specified in /etc/crypttab as a keyscript - and subsequlently started in the initrd environment. See + &COMMANDNAME; is a program which is meant to + be specified as a keyscript for the root disk in crypttab - 5 for more information on - keyscripts. - - - - plugins is looked for in the plugins directory which by default will be - /conf/conf.d/mandos/plugins.d if not changed by option --plugin-dir. - - + 5. The aim of this + program is therefore to output a password, which then + cryptsetup + 8 will use to unlock the + root disk. + + + This program is not meant to be invoked directly, but can be in + order to test it. Note that any password obtained will simply + be output on standard output. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + OPTIONS - -g,--global-options - OPTIONS - - - Global options given to all plugins as additional start - arguments. Options are specified with a -o flag followed - by a comma separated string of options. - - - - - - -o, --options-for - PLUGIN:OPTION - - - - Plugin specific options given to the plugin as additional - start arguments. Options are specified with a -o flag - followed by a comma separated string of options. - - - - - - -d, --disable - PLUGIN - - - - Disable a specific plugin - - - - - - --groupid ID - - - - Group ID the plugins will run as - - - - - - --userid ID - - - - User ID the plugins will run as - - - - - - --plugin-dir DIRECTORY - - - - Specify a different plugin directory - - - - - - --debug - - - Debug mode - - - - - - -?, --help - - - Gives a help message - - - - - - --usage - - - Gives a short usage message - - - - - - -V, --version - - - Prints the program version - - - + + + + + This option will add an environment variable setting to + all plugins. This will override any inherited environment + variable. + + + + + + + + + + This option will add an environment variable setting to + the PLUGIN plugin. This will + override any inherited environment variables or + environment variables specified using + . + + + + + + + + + + Pass some options to all plugins. + OPTIONS is a comma separated + list of options. This is not a very useful option, except + for specifying the + option to all plugins. + + + + + + + + + + Pass some options to a specific plugin. PLUGIN is the name (file basename) of a + plugin, and OPTIONS is a comma + separated list of options. + + + Note that since options are not split on whitespace, the + way to pass, to the plugin + foo, the option + with the option argument + baz is either + --options-for=foo:--bar=baz or + --options-for=foo:--bar,baz. Using + --options-for="foo:--bar baz". will + not work. + + + + + + + + + + Disable the plugin named + PLUGIN. The plugin will not be + started. + + + + + + + + + + Re-enable the plugin named + PLUGIN. This is only useful to + undo a previous option, maybe + from the configuration file. + + + + + + + + + Change to group ID ID on + startup. The default is 65534. All plugins will be + started using this group ID. Note: + This must be a number, not a name. + + + + + + + + + Change to user ID ID on + startup. The default is 65534. All plugins will be + started using this user ID. Note: + This must be a number, not a name. + + + + + + + + + Specify a different plugin directory. The default is + /lib/mandos/plugins.d, which will + exist in the initial RAM disk + environment. + + + + + + + + + Specify a different file to read additional options from. + See . Other command line options + will override options specified in the file. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + The default is to not run in debug + mode. + + + The plugins will not be affected by + this option. Use + + if complete debugging eruption is desired. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + - + + + OVERVIEW + + + This program will run on the client side in the initial + RAM disk environment, and is responsible for + getting a password. It does this by running plugins, one of + which will normally be the actual client program communicating + with the server. + + + + PLUGINS + + This program will get a password by running a number of + plugins, which are simply executable + programs in a directory in the initial RAM + disk environment. The default directory is + /lib/mandos/plugins.d, but this can be + changed with the option. The + plugins are started in parallel, and the first plugin to output + a password and exit with a successful exit + code will make this plugin-runner output the password from that + plugin, stop any other plugins, and exit. + + + + WRITING PLUGINS + + A plugin is simply a program which prints a password to its + standard output and then exits with a successful (zero) exit + status. If the exit status is not zero, any output on + standard output will be ignored by the plugin runner. Any + output on its standard error channel will simply be passed to + the standard error of the plugin runner, usually the system + console. + + + If the password is a single-line, manually entered passprase, + a final trailing newline character should + not be printed. + + + The plugin will run in the initial RAM disk environment, so + care must be taken not to depend on any files or running + services not available there. + + + The plugin must exit cleanly and free all allocated resources + upon getting the TERM signal, since this is what the plugin + runner uses to stop all other plugins when one plugin has + output a password and exited cleanly. + + + The plugin must not use resources, like for instance reading + from the standard input, without knowing that no other plugin + is also using it. + + + It is useful, but not required, for the plugin to take the + option. + + + + + + FALLBACK + + If no plugins succeed, this program will, as a fallback, ask for + a password on the console using getpass3, + and output it. This is not meant to be the normal mode of + operation, as there is a separate plugin for getting a password + from the console. + + + EXIT STATUS - - - - + Exit status of this program is zero if no errors were + encountered, and otherwise not. The fallback (see ) may or may not have succeeded in either + case. + + + + + ENVIRONMENT + + This program does not use any environment variables itself, it + only passes on its environment to all the plugins. The + environment passed to plugins can be modified using the + and + options. + + + + FILES - - - - - NOTES - + + + /conf/conf.d/mandos/plugin-runner.conf + + + Since this program will be run as a keyscript, there is + little to no opportunity to pass command line arguments + to it. Therefore, it will also + read this file and use its contents as + whitespace-separated command line options. Also, + everything from a # character to the end + of a line is ignored. + + + This program is meant to run in the initial RAM disk + environment, so that is where this file is assumed to + exist. The file does not need to exist in the normal + file system. + + + This file will be processed before + the normal command line options, so the latter can + override the former, if need be. + + + This file name is the default; the file to read for + arguments can be changed using the + option. + + + + BUGS + The option is ignored when + specified from within a configuration file. - - + + - EXAMPLES - - + EXAMPLE + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Run the program, but not the plugins, in debug mode: + + + + + &COMMANDNAME; --debug + + + + + + Run all plugins, but run the foo plugin in + debug mode: + + + + + &COMMANDNAME; --options-for=foo:--debug + + + + + + Run all plugins, but not the program, in debug mode: + + + + + &COMMANDNAME; --global-options=--debug + + + + + + Run plugins from a different directory, read a different + configuration file, and add two options to the + mandos-client + 8mandos plugin: + + + + +cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/mandos/plugins.d --options-for=mandos-client:--pubkey=pubkey.txt,--seckey=seckey.txt + + + - SECURITY + This program will, when starting, try to switch to another user. + If it is started as root, it will succeed, and will by default + switch to user and group 65534, which are assumed to be + non-privileged. This user and group is then what all plugins + will be started as. Therefore, the only way to run a plugin as + a privileged user is to have the set-user-ID or set-group-ID bit + set on the plugin executable file (see + execve2 + ). + + + If this program is used as a keyscript in crypttab5 + , there is a slight risk that if this program + fails to work, there might be no way to boot the system except + for booting from another media and editing the initial RAM disk + image to not run this program. This is, however, unlikely, + since the password-prompt8mandos + plugin will read a password from the console in + case of failure of the other plugins, and this plugin runner + will also, in case of catastrophic failure, itself fall back to + asking and outputting a password on the console (see ). - + SEE ALSO + cryptsetup + 8, + crypttab + 5, + execve + 2, mandos - 8, - password-request - 8mandos, - password-prompt - 8mandos, and - cryptsetup - 8 + 8, + password-prompt + 8mandos, + mandos-client + 8mandos - + + + + + + === added file 'plugins.d/askpass-fifo.c' --- plugins.d/askpass-fifo.c 1970-01-01 00:00:00 +0000 +++ plugins.d/askpass-fifo.c 2010-09-26 18:32:58 +0000 @@ -0,0 +1,174 @@ +/* -*- coding: utf-8 -*- */ +/* + * Askpass-FIFO - Read a password from a FIFO and output it + * + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 . + */ + +#define _GNU_SOURCE /* TEMP_FAILURE_RETRY() */ +#include /* ssize_t */ +#include /* mkfifo(), S_IRUSR, S_IWUSR */ +#include /* and */ +#include /* errno, EACCES, ENOTDIR, ELOOP, + ENAMETOOLONG, ENOSPC, EROFS, + ENOENT, EEXIST, EFAULT, EMFILE, + ENFILE, ENOMEM, EBADF, EINVAL, EIO, + EISDIR, EFBIG */ +#include /* error() */ +#include /* EXIT_FAILURE, NULL, size_t, free(), + realloc(), EXIT_SUCCESS */ +#include /* open(), O_RDONLY */ +#include /* read(), close(), write(), + STDOUT_FILENO */ +#include /* EX_OSERR, EX_OSFILE, + EX_UNAVAILABLE, EX_IOERR */ + + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + int ret = 0; + ssize_t sret; + + /* Create FIFO */ + const char passfifo[] = "/lib/cryptsetup/passfifo"; + ret = mkfifo(passfifo, S_IRUSR | S_IWUSR); + if(ret == -1){ + int e = errno; + error(0, errno, "mkfifo"); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + return EX_OSFILE; + case ENAMETOOLONG: + case ENOSPC: + case EROFS: + default: + return EX_OSERR; + case ENOENT: + return EX_UNAVAILABLE; /* no "/lib/cryptsetup"? */ + case EEXIST: + break; /* not an error */ + } + } + + /* Open FIFO */ + int fifo_fd = open(passfifo, O_RDONLY); + if(fifo_fd == -1){ + int e = errno; + error(0, errno, "open"); + switch(e){ + case EACCES: + case ENOENT: + case EFAULT: + return EX_UNAVAILABLE; + case ENAMETOOLONG: + case EMFILE: + case ENFILE: + case ENOMEM: + default: + return EX_OSERR; + case ENOTDIR: + case ELOOP: + return EX_OSFILE; + } + } + + /* Read from FIFO */ + char *buf = NULL; + size_t buf_len = 0; + { + size_t buf_allocated = 0; + const size_t blocksize = 1024; + do { + if(buf_len + blocksize > buf_allocated){ + char *tmp = realloc(buf, buf_allocated + blocksize); + if(tmp == NULL){ + error(0, errno, "realloc"); + free(buf); + return EX_OSERR; + } + buf = tmp; + buf_allocated += blocksize; + } + sret = read(fifo_fd, buf + buf_len, buf_allocated - buf_len); + if(sret == -1){ + int e = errno; + free(buf); + errno = e; + error(0, errno, "read"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + default: + return EX_OSERR; + case EIO: + return EX_IOERR; + case EISDIR: + return EX_UNAVAILABLE; + } + } + buf_len += (size_t)sret; + } while(sret != 0); + } + + /* Close FIFO */ + close(fifo_fd); + + /* Print password to stdout */ + size_t written = 0; + while(written < buf_len){ + sret = write(STDOUT_FILENO, buf + written, buf_len - written); + if(sret == -1){ + int e = errno; + free(buf); + errno = e; + error(0, errno, "write"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + return EX_OSFILE; + case EFBIG: + case EIO: + case ENOSPC: + default: + return EX_IOERR; + } + } + written += (size_t)sret; + } + free(buf); + + ret = close(STDOUT_FILENO); + if(ret == -1){ + int e = errno; + error(0, errno, "close"); + switch(e){ + case EBADF: + return EX_OSFILE; + case EIO: + default: + return EX_IOERR; + } + } + return EXIT_SUCCESS; +} === added file 'plugins.d/askpass-fifo.xml' --- plugins.d/askpass-fifo.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/askpass-fifo.xml 2009-01-04 21:54:55 +0000 @@ -0,0 +1,162 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2008 + 2009 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to get a password from a + FIFO. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program reads a password from a FIFO and + outputs it to standard output. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + This program is meant to be imitate a feature of the + askpass program, so that programs written to + interface with it can keep working under the + Mandos system. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + FILES + + + /lib/cryptsetup/passfifo + + + This is the FIFO where this program + will read the password. If it does not exist, it will be + created. + + + + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + The only thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + fifo + 7, + plugin-runner + 8mandos + + +
+ + + + + === renamed file 'plugins.d/password-request.c' => 'plugins.d/mandos-client.c' --- plugins.d/password-request.c 2008-08-17 22:42:28 +0000 +++ plugins.d/mandos-client.c 2010-09-12 18:12:11 +0000 @@ -1,6 +1,6 @@ /* -*- coding: utf-8 -*- */ /* - * Mandos client - get and decrypt data from a Mandos server + * Mandos-client - get and decrypt data from a Mandos server * * This program is partly derived from an example program for an Avahi * service browser, downloaded from @@ -9,7 +9,8 @@ * "browse_callback", and parts of "main". * * Everything else is - * Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson + * Copyright © 2008,2009 Teddy Hogeborn + * Copyright © 2008,2009 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 @@ -29,45 +30,64 @@ */ /* Needed by GPGME, specifically gpgme_data_seek() */ +#ifndef _LARGEFILE_SOURCE #define _LARGEFILE_SOURCE +#endif +#ifndef _FILE_OFFSET_BITS #define _FILE_OFFSET_BITS 64 +#endif #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), asprintf() */ #include /* fprintf(), stderr, fwrite(), - stdout, ferror() */ + stdout, ferror(), remove() */ #include /* uint16_t, uint32_t */ #include /* NULL, size_t, ssize_t */ -#include /* free(), EXIT_SUCCESS, EXIT_FAILURE, - srand() */ -#include /* bool, true */ +#include /* free(), EXIT_SUCCESS, srand(), + strtof(), abort() */ +#include /* bool, false, true */ #include /* memset(), strcmp(), strlen(), strerror(), asprintf(), strcpy() */ -#include /* ioctl */ +#include /* ioctl */ #include /* socket(), inet_pton(), sockaddr, sockaddr_in6, PF_INET6, - SOCK_STREAM, INET6_ADDRSTRLEN, - uid_t, gid_t */ -#include /* PRIu16 */ + SOCK_STREAM, uid_t, gid_t, open(), + opendir(), DIR */ +#include /* open() */ #include /* socket(), struct sockaddr_in6, - struct in6_addr, inet_pton(), - connect() */ + inet_pton(), connect() */ +#include /* open() */ +#include /* opendir(), struct dirent, readdir() + */ +#include /* PRIu16, PRIdMAX, intmax_t, + strtoimax() */ #include /* assert() */ #include /* perror(), errno */ -#include /* time() */ +#include /* nanosleep(), time(), sleep() */ #include /* ioctl, ifreq, SIOCGIFFLAGS, IFF_UP, SIOCSIFFLAGS, if_indextoname(), if_nametoindex(), IF_NAMESIZE */ +#include /* IN6_IS_ADDR_LINKLOCAL, + INET_ADDRSTRLEN, INET6_ADDRSTRLEN + */ #include /* close(), SEEK_SET, off_t, write(), - getuid(), getgid(), setuid(), - setgid() */ -#include + getuid(), getgid(), seteuid(), + setgid(), pause() */ #include /* inet_pton(), htons */ -#include /* not, and */ +#include /* not, or, and */ #include /* struct argp_option, error_t, struct argp_state, struct argp, argp_parse(), ARGP_KEY_ARG, ARGP_KEY_END, ARGP_ERR_UNKNOWN */ +#include /* sigemptyset(), sigaddset(), + sigaction(), SIGTERM, sig_atomic_t, + raise() */ +#include /* EX_OSERR, EX_USAGE, EX_UNAVAILABLE, + EX_NOHOST, EX_IOERR, EX_PROTOCOL */ + +#ifdef __linux__ +#include /* klogctl() */ +#endif /* __linux__ */ /* Avahi */ /* All Avahi types, constants and functions @@ -86,7 +106,8 @@ gnutls_* init_gnutls_session(), GNUTLS_* */ -#include /* gnutls_certificate_set_openpgp_key_file(), +#include + /* gnutls_certificate_set_openpgp_key_file(), GNUTLS_OPENPGP_FMT_BASE64 */ /* GPGME */ @@ -98,11 +119,16 @@ #define BUFFER_SIZE 256 +#define PATHDIR "/conf/conf.d/mandos" +#define SECKEY "seckey.txt" +#define PUBKEY "pubkey.txt" + bool debug = false; -static const char *keydir = "/conf/conf.d/mandos"; static const char mandos_protocol_version[] = "1"; -const char *argp_program_version = "password-request 1.0"; +const char *argp_program_version = "mandos-client " VERSION; const char *argp_program_bug_address = ""; +static const char sys_class_net[] = "/sys/class/net"; +char *connect_to = NULL; /* Used for passing in values through the Avahi callback functions */ typedef struct { @@ -112,18 +138,27 @@ unsigned int dh_bits; gnutls_dh_params_t dh_params; const char *priority; + gpgme_ctx_t ctx; } mandos_context; +/* global context so signal handler can reach it*/ +mandos_context mc = { .simple_poll = NULL, .server = NULL, + .dh_bits = 1024, .priority = "SECURE256" + ":!CTYPE-X.509:+CTYPE-OPENPGP" }; + +sig_atomic_t quit_now = 0; +int signal_received = 0; + /* - * Make room in "buffer" for at least BUFFER_SIZE additional bytes. - * "buffer_capacity" is how much is currently allocated, + * Make additional room in "buffer" for at least BUFFER_SIZE more + * bytes. "buffer_capacity" is how much is currently allocated, * "buffer_length" is how much is already used. */ -size_t adjustbuffer(char **buffer, size_t buffer_length, +size_t incbuffer(char **buffer, size_t buffer_length, size_t buffer_capacity){ - if (buffer_length + BUFFER_SIZE > buffer_capacity){ + if(buffer_length + BUFFER_SIZE > buffer_capacity){ *buffer = realloc(*buffer, buffer_capacity + BUFFER_SIZE); - if (buffer == NULL){ + if(buffer == NULL){ return 0; } buffer_capacity += BUFFER_SIZE; @@ -132,58 +167,119 @@ } /* - * Decrypt OpenPGP data using keyrings in HOMEDIR. - * Returns -1 on error + * Initialize GPGME. */ -static ssize_t pgp_packet_decrypt (const char *cryptotext, - size_t crypto_size, - char **plaintext, - const char *homedir){ - gpgme_data_t dh_crypto, dh_plain; - gpgme_ctx_t ctx; +static bool init_gpgme(const char *seckey, + const char *pubkey, const char *tempdir){ gpgme_error_t rc; - ssize_t ret; - size_t plaintext_capacity = 0; - ssize_t plaintext_length = 0; gpgme_engine_info_t engine_info; - if (debug){ - fprintf(stderr, "Trying to decrypt OpenPGP data\n"); + + /* + * Helper function to insert pub and seckey to the engine keyring. + */ + bool import_key(const char *filename){ + int ret; + int fd; + gpgme_data_t pgp_data; + + fd = (int)TEMP_FAILURE_RETRY(open(filename, O_RDONLY)); + if(fd == -1){ + perror("open"); + return false; + } + + rc = gpgme_data_new_from_fd(&pgp_data, fd); + if(rc != GPG_ERR_NO_ERROR){ + fprintf(stderr, "bad gpgme_data_new_from_fd: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; + } + + rc = gpgme_op_import(mc.ctx, pgp_data); + if(rc != GPG_ERR_NO_ERROR){ + fprintf(stderr, "bad gpgme_op_import: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; + } + + ret = (int)TEMP_FAILURE_RETRY(close(fd)); + if(ret == -1){ + perror("close"); + } + gpgme_data_release(pgp_data); + return true; + } + + if(debug){ + fprintf(stderr, "Initializing GPGME\n"); } /* Init GPGME */ gpgme_check_version(NULL); rc = gpgme_engine_check_version(GPGME_PROTOCOL_OpenPGP); - if (rc != GPG_ERR_NO_ERROR){ + if(rc != GPG_ERR_NO_ERROR){ fprintf(stderr, "bad gpgme_engine_check_version: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); - return -1; + return false; } - /* Set GPGME home directory for the OpenPGP engine only */ - rc = gpgme_get_engine_info (&engine_info); - if (rc != GPG_ERR_NO_ERROR){ + /* Set GPGME home directory for the OpenPGP engine only */ + rc = gpgme_get_engine_info(&engine_info); + if(rc != GPG_ERR_NO_ERROR){ fprintf(stderr, "bad gpgme_get_engine_info: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); - return -1; + return false; } while(engine_info != NULL){ if(engine_info->protocol == GPGME_PROTOCOL_OpenPGP){ gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP, - engine_info->file_name, homedir); + engine_info->file_name, tempdir); break; } engine_info = engine_info->next; } if(engine_info == NULL){ - fprintf(stderr, "Could not set GPGME home dir to %s\n", homedir); - return -1; + fprintf(stderr, "Could not set GPGME home dir to %s\n", tempdir); + return false; + } + + /* Create new GPGME "context" */ + rc = gpgme_new(&(mc.ctx)); + if(rc != GPG_ERR_NO_ERROR){ + fprintf(stderr, "bad gpgme_new: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; + } + + if(not import_key(pubkey) or not import_key(seckey)){ + return false; + } + + return true; +} + +/* + * Decrypt OpenPGP data. + * Returns -1 on error + */ +static ssize_t pgp_packet_decrypt(const char *cryptotext, + size_t crypto_size, + char **plaintext){ + gpgme_data_t dh_crypto, dh_plain; + gpgme_error_t rc; + ssize_t ret; + size_t plaintext_capacity = 0; + ssize_t plaintext_length = 0; + + if(debug){ + fprintf(stderr, "Trying to decrypt OpenPGP data\n"); } /* Create new GPGME data buffer from memory cryptotext */ rc = gpgme_data_new_from_mem(&dh_crypto, cryptotext, crypto_size, 0); - if (rc != GPG_ERR_NO_ERROR){ + if(rc != GPG_ERR_NO_ERROR){ fprintf(stderr, "bad gpgme_data_new_from_mem: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); return -1; @@ -191,52 +287,35 @@ /* Create new empty GPGME data buffer for the plaintext */ rc = gpgme_data_new(&dh_plain); - if (rc != GPG_ERR_NO_ERROR){ + if(rc != GPG_ERR_NO_ERROR){ fprintf(stderr, "bad gpgme_data_new: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); gpgme_data_release(dh_crypto); return -1; } - /* Create new GPGME "context" */ - rc = gpgme_new(&ctx); - if (rc != GPG_ERR_NO_ERROR){ - fprintf(stderr, "bad gpgme_new: %s: %s\n", - gpgme_strsource(rc), gpgme_strerror(rc)); - plaintext_length = -1; - goto decrypt_end; - } - /* Decrypt data from the cryptotext data buffer to the plaintext data buffer */ - rc = gpgme_op_decrypt(ctx, dh_crypto, dh_plain); - if (rc != GPG_ERR_NO_ERROR){ + rc = gpgme_op_decrypt(mc.ctx, dh_crypto, dh_plain); + if(rc != GPG_ERR_NO_ERROR){ fprintf(stderr, "bad gpgme_op_decrypt: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); plaintext_length = -1; - goto decrypt_end; - } - - if(debug){ - fprintf(stderr, "Decryption of OpenPGP data succeeded\n"); - } - - if (debug){ - gpgme_decrypt_result_t result; - result = gpgme_op_decrypt_result(ctx); - if (result == NULL){ - fprintf(stderr, "gpgme_op_decrypt_result failed\n"); - } else { - fprintf(stderr, "Unsupported algorithm: %s\n", - result->unsupported_algorithm); - fprintf(stderr, "Wrong key usage: %u\n", - result->wrong_key_usage); - if(result->file_name != NULL){ - fprintf(stderr, "File name: %s\n", result->file_name); - } - gpgme_recipient_t recipient; - recipient = result->recipients; - if(recipient){ + if(debug){ + gpgme_decrypt_result_t result; + result = gpgme_op_decrypt_result(mc.ctx); + if(result == NULL){ + fprintf(stderr, "gpgme_op_decrypt_result failed\n"); + } else { + fprintf(stderr, "Unsupported algorithm: %s\n", + result->unsupported_algorithm); + fprintf(stderr, "Wrong key usage: %u\n", + result->wrong_key_usage); + if(result->file_name != NULL){ + fprintf(stderr, "File name: %s\n", result->file_name); + } + gpgme_recipient_t recipient; + recipient = result->recipients; while(recipient != NULL){ fprintf(stderr, "Public key algorithm: %s\n", gpgme_pubkey_algo_name(recipient->pubkey_algo)); @@ -248,22 +327,27 @@ } } } + goto decrypt_end; + } + + if(debug){ + fprintf(stderr, "Decryption of OpenPGP data succeeded\n"); } /* Seek back to the beginning of the GPGME plaintext data buffer */ - if (gpgme_data_seek(dh_plain, (off_t) 0, SEEK_SET) == -1){ - perror("pgpme_data_seek"); + if(gpgme_data_seek(dh_plain, (off_t)0, SEEK_SET) == -1){ + perror("gpgme_data_seek"); plaintext_length = -1; goto decrypt_end; } *plaintext = NULL; while(true){ - plaintext_capacity = adjustbuffer(plaintext, + plaintext_capacity = incbuffer(plaintext, (size_t)plaintext_length, plaintext_capacity); - if (plaintext_capacity == 0){ - perror("adjustbuffer"); + if(plaintext_capacity == 0){ + perror("incbuffer"); plaintext_length = -1; goto decrypt_end; } @@ -271,7 +355,7 @@ ret = gpgme_data_read(dh_plain, *plaintext + plaintext_length, BUFFER_SIZE); /* Print the data, if any */ - if (ret == 0){ + if(ret == 0){ /* EOF */ break; } @@ -282,7 +366,7 @@ } plaintext_length += ret; } - + if(debug){ fprintf(stderr, "Decrypted password is: "); for(ssize_t i = 0; i < plaintext_length; i++){ @@ -301,9 +385,10 @@ return plaintext_length; } -static const char * safer_gnutls_strerror (int value) { - const char *ret = gnutls_strerror (value); /* Spurious warning */ - if (ret == NULL) +static const char * safer_gnutls_strerror(int value){ + const char *ret = gnutls_strerror(value); /* Spurious warning from + -Wunreachable-code */ + if(ret == NULL) ret = "(unknown)"; return ret; } @@ -314,8 +399,7 @@ fprintf(stderr, "GnuTLS: %s", string); } -static int init_gnutls_global(mandos_context *mc, - const char *pubkeyfilename, +static int init_gnutls_global(const char *pubkeyfilename, const char *seckeyfilename){ int ret; @@ -324,13 +408,13 @@ } ret = gnutls_global_init(); - if (ret != GNUTLS_E_SUCCESS) { - fprintf (stderr, "GnuTLS global_init: %s\n", - safer_gnutls_strerror(ret)); + if(ret != GNUTLS_E_SUCCESS){ + fprintf(stderr, "GnuTLS global_init: %s\n", + safer_gnutls_strerror(ret)); return -1; } - if (debug){ + if(debug){ /* "Use a log level over 10 to enable all debugging options." * - GnuTLS manual */ @@ -339,95 +423,112 @@ } /* OpenPGP credentials */ - gnutls_certificate_allocate_credentials(&mc->cred); - if (ret != GNUTLS_E_SUCCESS){ - fprintf (stderr, "GnuTLS memory error: %s\n", /* Spurious - warning */ - safer_gnutls_strerror(ret)); - gnutls_global_deinit (); + gnutls_certificate_allocate_credentials(&mc.cred); + if(ret != GNUTLS_E_SUCCESS){ + fprintf(stderr, "GnuTLS memory error: %s\n", /* Spurious warning + from + -Wunreachable-code + */ + safer_gnutls_strerror(ret)); + gnutls_global_deinit(); return -1; } if(debug){ - fprintf(stderr, "Attempting to use OpenPGP certificate %s" - " and keyfile %s as GnuTLS credentials\n", pubkeyfilename, + fprintf(stderr, "Attempting to use OpenPGP public key %s and" + " secret key %s as GnuTLS credentials\n", pubkeyfilename, seckeyfilename); } ret = gnutls_certificate_set_openpgp_key_file - (mc->cred, pubkeyfilename, seckeyfilename, + (mc.cred, pubkeyfilename, seckeyfilename, GNUTLS_OPENPGP_FMT_BASE64); - if (ret != GNUTLS_E_SUCCESS) { + if(ret != GNUTLS_E_SUCCESS){ fprintf(stderr, "Error[%d] while reading the OpenPGP key pair ('%s'," " '%s')\n", ret, pubkeyfilename, seckeyfilename); - fprintf(stdout, "The GnuTLS error is: %s\n", + fprintf(stderr, "The GnuTLS error is: %s\n", safer_gnutls_strerror(ret)); goto globalfail; } /* GnuTLS server initialization */ - ret = gnutls_dh_params_init(&mc->dh_params); - if (ret != GNUTLS_E_SUCCESS) { - fprintf (stderr, "Error in GnuTLS DH parameter initialization:" - " %s\n", safer_gnutls_strerror(ret)); - goto globalfail; - } - ret = gnutls_dh_params_generate2(mc->dh_params, mc->dh_bits); - if (ret != GNUTLS_E_SUCCESS) { - fprintf (stderr, "Error in GnuTLS prime generation: %s\n", - safer_gnutls_strerror(ret)); - goto globalfail; - } - - gnutls_certificate_set_dh_params(mc->cred, mc->dh_params); - + ret = gnutls_dh_params_init(&mc.dh_params); + if(ret != GNUTLS_E_SUCCESS){ + fprintf(stderr, "Error in GnuTLS DH parameter initialization:" + " %s\n", safer_gnutls_strerror(ret)); + goto globalfail; + } + ret = gnutls_dh_params_generate2(mc.dh_params, mc.dh_bits); + if(ret != GNUTLS_E_SUCCESS){ + fprintf(stderr, "Error in GnuTLS prime generation: %s\n", + safer_gnutls_strerror(ret)); + goto globalfail; + } + + gnutls_certificate_set_dh_params(mc.cred, mc.dh_params); + return 0; - + globalfail: - - gnutls_certificate_free_credentials(mc->cred); + + gnutls_certificate_free_credentials(mc.cred); gnutls_global_deinit(); + gnutls_dh_params_deinit(mc.dh_params); return -1; - } -static int init_gnutls_session(mandos_context *mc, - gnutls_session_t *session){ +static int init_gnutls_session(gnutls_session_t *session){ int ret; /* GnuTLS session creation */ - ret = gnutls_init(session, GNUTLS_SERVER); - if (ret != GNUTLS_E_SUCCESS){ + do { + ret = gnutls_init(session, GNUTLS_SERVER); + if(quit_now){ + return -1; + } + } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); + if(ret != GNUTLS_E_SUCCESS){ fprintf(stderr, "Error in GnuTLS session initialization: %s\n", safer_gnutls_strerror(ret)); } { const char *err; - ret = gnutls_priority_set_direct(*session, mc->priority, &err); - if (ret != GNUTLS_E_SUCCESS) { + do { + ret = gnutls_priority_set_direct(*session, mc.priority, &err); + if(quit_now){ + gnutls_deinit(*session); + return -1; + } + } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); + if(ret != GNUTLS_E_SUCCESS){ fprintf(stderr, "Syntax error at: %s\n", err); fprintf(stderr, "GnuTLS error: %s\n", safer_gnutls_strerror(ret)); - gnutls_deinit (*session); + gnutls_deinit(*session); return -1; } } - ret = gnutls_credentials_set(*session, GNUTLS_CRD_CERTIFICATE, - mc->cred); - if (ret != GNUTLS_E_SUCCESS) { + do { + ret = gnutls_credentials_set(*session, GNUTLS_CRD_CERTIFICATE, + mc.cred); + if(quit_now){ + gnutls_deinit(*session); + return -1; + } + } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); + if(ret != GNUTLS_E_SUCCESS){ fprintf(stderr, "Error setting GnuTLS credentials: %s\n", safer_gnutls_strerror(ret)); - gnutls_deinit (*session); + gnutls_deinit(*session); return -1; } /* ignore client certificate if any. */ - gnutls_certificate_server_set_request (*session, - GNUTLS_CERT_IGNORE); + gnutls_certificate_server_set_request(*session, GNUTLS_CERT_IGNORE); - gnutls_dh_set_prime_bits (*session, mc->dh_bits); + gnutls_dh_set_prime_bits(*session, mc.dh_bits); return 0; } @@ -439,66 +540,137 @@ /* Called when a Mandos server is found */ static int start_mandos_communication(const char *ip, uint16_t port, AvahiIfIndex if_index, - mandos_context *mc){ - int ret, tcp_sd; - union { struct sockaddr in; struct sockaddr_in6 in6; } to; + int af){ + int ret, tcp_sd = -1; + ssize_t sret; + union { + struct sockaddr_in in; + struct sockaddr_in6 in6; + } to; char *buffer = NULL; - char *decrypted_buffer; + char *decrypted_buffer = NULL; size_t buffer_length = 0; size_t buffer_capacity = 0; - ssize_t decrypted_buffer_size; size_t written; - int retval = 0; - char interface[IF_NAMESIZE]; + int retval = -1; gnutls_session_t session; - - ret = init_gnutls_session (mc, &session); - if (ret != 0){ + int pf; /* Protocol family */ + + errno = 0; + + if(quit_now){ + errno = EINTR; + return -1; + } + + switch(af){ + case AF_INET6: + pf = PF_INET6; + break; + case AF_INET: + pf = PF_INET; + break; + default: + fprintf(stderr, "Bad address family: %d\n", af); + errno = EINVAL; + return -1; + } + + ret = init_gnutls_session(&session); + if(ret != 0){ return -1; } if(debug){ - fprintf(stderr, "Setting up a tcp connection to %s, port %" PRIu16 + fprintf(stderr, "Setting up a TCP connection to %s, port %" PRIu16 "\n", ip, port); } - tcp_sd = socket(PF_INET6, SOCK_STREAM, 0); - if(tcp_sd < 0) { + tcp_sd = socket(pf, SOCK_STREAM, 0); + if(tcp_sd < 0){ + int e = errno; perror("socket"); - return -1; + errno = e; + goto mandos_end; } - - if(debug){ - if(if_indextoname((unsigned int)if_index, interface) == NULL){ - perror("if_indextoname"); - return -1; - } - fprintf(stderr, "Binding to interface %s\n", interface); + + if(quit_now){ + errno = EINTR; + goto mandos_end; } memset(&to, 0, sizeof(to)); - to.in6.sin6_family = AF_INET6; - /* It would be nice to have a way to detect if we were passed an - IPv4 address here. Now we assume an IPv6 address. */ - ret = inet_pton(AF_INET6, ip, &to.in6.sin6_addr); - if (ret < 0 ){ + if(af == AF_INET6){ + to.in6.sin6_family = (sa_family_t)af; + ret = inet_pton(af, ip, &to.in6.sin6_addr); + } else { /* IPv4 */ + to.in.sin_family = (sa_family_t)af; + ret = inet_pton(af, ip, &to.in.sin_addr); + } + if(ret < 0 ){ + int e = errno; perror("inet_pton"); - return -1; + errno = e; + goto mandos_end; } if(ret == 0){ + int e = errno; fprintf(stderr, "Bad address: %s\n", ip); - return -1; - } - to.in6.sin6_port = htons(port); /* Spurious warning */ + errno = e; + goto mandos_end; + } + if(af == AF_INET6){ + to.in6.sin6_port = htons(port); /* Spurious warnings from + -Wconversion and + -Wunreachable-code */ + + if(IN6_IS_ADDR_LINKLOCAL /* Spurious warnings from */ + (&to.in6.sin6_addr)){ /* -Wstrict-aliasing=2 or lower and + -Wunreachable-code*/ + if(if_index == AVAHI_IF_UNSPEC){ + fprintf(stderr, "An IPv6 link-local address is incomplete" + " without a network interface\n"); + errno = EINVAL; + goto mandos_end; + } + /* Set the network interface number as scope */ + to.in6.sin6_scope_id = (uint32_t)if_index; + } + } else { + to.in.sin_port = htons(port); /* Spurious warnings from + -Wconversion and + -Wunreachable-code */ + } - to.in6.sin6_scope_id = (uint32_t)if_index; + if(quit_now){ + errno = EINTR; + goto mandos_end; + } if(debug){ - fprintf(stderr, "Connection to: %s, port %" PRIu16 "\n", ip, - port); - char addrstr[INET6_ADDRSTRLEN] = ""; - if(inet_ntop(to.in6.sin6_family, &(to.in6.sin6_addr), addrstr, - sizeof(addrstr)) == NULL){ + if(af == AF_INET6 and if_index != AVAHI_IF_UNSPEC){ + char interface[IF_NAMESIZE]; + if(if_indextoname((unsigned int)if_index, interface) == NULL){ + perror("if_indextoname"); + } else { + fprintf(stderr, "Connection to: %s%%%s, port %" PRIu16 "\n", + ip, interface, port); + } + } else { + fprintf(stderr, "Connection to: %s, port %" PRIu16 "\n", ip, + port); + } + char addrstr[(INET_ADDRSTRLEN > INET6_ADDRSTRLEN) ? + INET_ADDRSTRLEN : INET6_ADDRSTRLEN] = ""; + const char *pcret; + if(af == AF_INET6){ + pcret = inet_ntop(af, &(to.in6.sin6_addr), addrstr, + sizeof(addrstr)); + } else { + pcret = inet_ntop(af, &(to.in.sin_addr), addrstr, + sizeof(addrstr)); + } + if(pcret == NULL){ perror("inet_ntop"); } else { if(strcmp(addrstr, ip) != 0){ @@ -507,101 +679,156 @@ } } - ret = connect(tcp_sd, &to.in, sizeof(to)); - if (ret < 0){ - perror("connect"); - return -1; - } - + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + if(af == AF_INET6){ + ret = connect(tcp_sd, &to.in6, sizeof(to)); + } else { + ret = connect(tcp_sd, &to.in, sizeof(to)); /* IPv4 */ + } + if(ret < 0){ + if ((errno != ECONNREFUSED and errno != ENETUNREACH) or debug){ + int e = errno; + perror("connect"); + errno = e; + } + goto mandos_end; + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + const char *out = mandos_protocol_version; written = 0; - while (true){ + while(true){ size_t out_size = strlen(out); - ret = TEMP_FAILURE_RETRY(write(tcp_sd, out + written, + ret = (int)TEMP_FAILURE_RETRY(write(tcp_sd, out + written, out_size - written)); - if (ret == -1){ + if(ret == -1){ + int e = errno; perror("write"); - retval = -1; + errno = e; goto mandos_end; } written += (size_t)ret; if(written < out_size){ continue; } else { - if (out == mandos_protocol_version){ + if(out == mandos_protocol_version){ written = 0; out = "\r\n"; } else { break; } } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } } - + if(debug){ fprintf(stderr, "Establishing TLS session with %s\n", ip); } - gnutls_transport_set_ptr (session, (gnutls_transport_ptr_t) tcp_sd); - - do{ - ret = gnutls_handshake (session); + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + gnutls_transport_set_ptr(session, (gnutls_transport_ptr_t) tcp_sd); + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + do { + ret = gnutls_handshake(session); + if(quit_now){ + errno = EINTR; + goto mandos_end; + } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); - if (ret != GNUTLS_E_SUCCESS){ + if(ret != GNUTLS_E_SUCCESS){ if(debug){ fprintf(stderr, "*** GnuTLS Handshake failed ***\n"); - gnutls_perror (ret); + gnutls_perror(ret); } - retval = -1; + errno = EPROTO; goto mandos_end; } /* Read OpenPGP packet that contains the wanted password */ if(debug){ - fprintf(stderr, "Retrieving pgp encrypted password from %s\n", + fprintf(stderr, "Retrieving OpenPGP encrypted password from %s\n", ip); } - + while(true){ - buffer_capacity = adjustbuffer(&buffer, buffer_length, + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + buffer_capacity = incbuffer(&buffer, buffer_length, buffer_capacity); - if (buffer_capacity == 0){ - perror("adjustbuffer"); - retval = -1; - goto mandos_end; - } - - ret = gnutls_record_recv(session, buffer+buffer_length, - BUFFER_SIZE); - if (ret == 0){ + if(buffer_capacity == 0){ + int e = errno; + perror("incbuffer"); + errno = e; + goto mandos_end; + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + sret = gnutls_record_recv(session, buffer+buffer_length, + BUFFER_SIZE); + if(sret == 0){ break; } - if (ret < 0){ - switch(ret){ + if(sret < 0){ + switch(sret){ case GNUTLS_E_INTERRUPTED: case GNUTLS_E_AGAIN: break; case GNUTLS_E_REHANDSHAKE: - do{ - ret = gnutls_handshake (session); + do { + ret = gnutls_handshake(session); + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); - if (ret < 0){ + if(ret < 0){ fprintf(stderr, "*** GnuTLS Re-handshake failed ***\n"); - gnutls_perror (ret); - retval = -1; + gnutls_perror(ret); + errno = EPROTO; goto mandos_end; } break; default: fprintf(stderr, "Unknown error while reading data from" " encrypted session with Mandos server\n"); - retval = -1; - gnutls_bye (session, GNUTLS_SHUT_RDWR); + gnutls_bye(session, GNUTLS_SHUT_RDWR); + errno = EIO; goto mandos_end; } } else { - buffer_length += (size_t) ret; + buffer_length += (size_t) sret; } } @@ -609,47 +836,80 @@ fprintf(stderr, "Closing TLS session\n"); } - gnutls_bye (session, GNUTLS_SHUT_RDWR); - - if (buffer_length > 0){ + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + do { + ret = gnutls_bye(session, GNUTLS_SHUT_RDWR); + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); + + if(buffer_length > 0){ + ssize_t decrypted_buffer_size; decrypted_buffer_size = pgp_packet_decrypt(buffer, buffer_length, - &decrypted_buffer, - keydir); - if (decrypted_buffer_size >= 0){ + &decrypted_buffer); + if(decrypted_buffer_size >= 0){ + written = 0; while(written < (size_t) decrypted_buffer_size){ - ret = (int)fwrite (decrypted_buffer + written, 1, - (size_t)decrypted_buffer_size - written, - stdout); + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + ret = (int)fwrite(decrypted_buffer + written, 1, + (size_t)decrypted_buffer_size - written, + stdout); if(ret == 0 and ferror(stdout)){ + int e = errno; if(debug){ fprintf(stderr, "Error writing encrypted data: %s\n", strerror(errno)); } - retval = -1; - break; + errno = e; + goto mandos_end; } written += (size_t)ret; } - free(decrypted_buffer); - } else { - retval = -1; + retval = 0; } } /* Shutdown procedure */ mandos_end: - free(buffer); - close(tcp_sd); - gnutls_deinit (session); + { + int e = errno; + free(decrypted_buffer); + free(buffer); + if(tcp_sd >= 0){ + ret = (int)TEMP_FAILURE_RETRY(close(tcp_sd)); + } + if(ret == -1){ + if(e == 0){ + e = errno; + } + perror("close"); + } + gnutls_deinit(session); + if(quit_now){ + e = EINTR; + retval = -1; + } + errno = e; + } return retval; } static void resolve_callback(AvahiSServiceResolver *r, AvahiIfIndex interface, - AVAHI_GCC_UNUSED AvahiProtocol protocol, + AvahiProtocol proto, AvahiResolverEvent event, const char *name, const char *type, @@ -660,19 +920,22 @@ AVAHI_GCC_UNUSED AvahiStringList *txt, AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, - void* userdata) { - mandos_context *mc = userdata; + AVAHI_GCC_UNUSED void* userdata){ assert(r); /* Called whenever a service has been resolved successfully or timed out */ - switch (event) { + if(quit_now){ + return; + } + + switch(event){ default: case AVAHI_RESOLVER_FAILURE: fprintf(stderr, "(Avahi Resolver) Failed to resolve service '%s'" " of type '%s' in domain '%s': %s\n", name, type, domain, - avahi_strerror(avahi_server_errno(mc->server))); + avahi_strerror(avahi_server_errno(mc.server))); break; case AVAHI_RESOLVER_FOUND: @@ -681,41 +944,45 @@ avahi_address_snprint(ip, sizeof(ip), address); if(debug){ fprintf(stderr, "Mandos server \"%s\" found on %s (%s, %" - PRIu16 ") on port %d\n", name, host_name, ip, - interface, port); + PRIdMAX ") on port %" PRIu16 "\n", name, host_name, + ip, (intmax_t)interface, port); } - int ret = start_mandos_communication(ip, port, interface, mc); - if (ret == 0){ - avahi_simple_poll_quit(mc->simple_poll); + int ret = start_mandos_communication(ip, port, interface, + avahi_proto_to_af(proto)); + if(ret == 0){ + avahi_simple_poll_quit(mc.simple_poll); } } } avahi_s_service_resolver_free(r); } -static void browse_callback( AvahiSServiceBrowser *b, - AvahiIfIndex interface, - AvahiProtocol protocol, - AvahiBrowserEvent event, - const char *name, - const char *type, - const char *domain, - AVAHI_GCC_UNUSED AvahiLookupResultFlags - flags, - void* userdata) { - mandos_context *mc = userdata; +static void browse_callback(AvahiSServiceBrowser *b, + AvahiIfIndex interface, + AvahiProtocol protocol, + AvahiBrowserEvent event, + const char *name, + const char *type, + const char *domain, + AVAHI_GCC_UNUSED AvahiLookupResultFlags + flags, + AVAHI_GCC_UNUSED void* userdata){ assert(b); /* Called whenever a new services becomes available on the LAN or is removed from the LAN */ - switch (event) { + if(quit_now){ + return; + } + + switch(event){ default: case AVAHI_BROWSER_FAILURE: fprintf(stderr, "(Avahi browser) %s\n", - avahi_strerror(avahi_server_errno(mc->server))); - avahi_simple_poll_quit(mc->simple_poll); + avahi_strerror(avahi_server_errno(mc.server))); + avahi_simple_poll_quit(mc.simple_poll); return; case AVAHI_BROWSER_NEW: @@ -724,12 +991,11 @@ the callback function is called the Avahi server will free the resolver for us. */ - if (!(avahi_s_service_resolver_new(mc->server, interface, - protocol, name, type, domain, - AVAHI_PROTO_INET6, 0, - resolve_callback, mc))) + if(avahi_s_service_resolver_new(mc.server, interface, protocol, + name, type, domain, protocol, 0, + resolve_callback, NULL) == NULL) fprintf(stderr, "Avahi: Failed to resolve service '%s': %s\n", - name, avahi_strerror(avahi_server_errno(mc->server))); + name, avahi_strerror(avahi_server_errno(mc.server))); break; case AVAHI_BROWSER_REMOVE: @@ -744,309 +1010,809 @@ } } -/* Combines file name and path and returns the malloced new - string. some sane checks could/should be added */ -static char *combinepath(const char *first, const char *second){ +/* stop main loop after sigterm has been called */ +static void handle_sigterm(int sig){ + if(quit_now){ + return; + } + quit_now = 1; + signal_received = sig; + int old_errno = errno; + if(mc.simple_poll != NULL){ + avahi_simple_poll_quit(mc.simple_poll); + } + errno = old_errno; +} + +/* + * This function determines if a directory entry in /sys/class/net + * corresponds to an acceptable network device. + * (This function is passed to scandir(3) as a filter function.) + */ +int good_interface(const struct dirent *if_entry){ + ssize_t ssret; + char *flagname = NULL; + int ret = asprintf(&flagname, "%s/%s/flags", sys_class_net, + if_entry->d_name); + if(ret < 0){ + perror("asprintf"); + return 0; + } + if(if_entry->d_name[0] == '.'){ + return 0; + } + int flags_fd = (int)TEMP_FAILURE_RETRY(open(flagname, O_RDONLY)); + if(flags_fd == -1){ + perror("open"); + return 0; + } + typedef short ifreq_flags; /* ifreq.ifr_flags in netdevice(7) */ + /* read line from flags_fd */ + ssize_t to_read = (sizeof(ifreq_flags)*2)+3; /* "0x1003\n" */ + char *flagstring = malloc((size_t)to_read+1); /* +1 for final \0 */ + flagstring[(size_t)to_read] = '\0'; + if(flagstring == NULL){ + perror("malloc"); + close(flags_fd); + return 0; + } + while(to_read > 0){ + ssret = (ssize_t)TEMP_FAILURE_RETRY(read(flags_fd, flagstring, + (size_t)to_read)); + if(ssret == -1){ + perror("read"); + free(flagstring); + close(flags_fd); + return 0; + } + to_read -= ssret; + if(ssret == 0){ + break; + } + } + close(flags_fd); + intmax_t tmpmax; char *tmp; - int ret = asprintf(&tmp, "%s/%s", first, second); - if(ret < 0){ - return NULL; - } - return tmp; + errno = 0; + tmpmax = strtoimax(flagstring, &tmp, 0); + if(errno != 0 or tmp == flagstring or (*tmp != '\0' + and not (isspace(*tmp))) + or tmpmax != (ifreq_flags)tmpmax){ + if(debug){ + fprintf(stderr, "Invalid flags \"%s\" for interface \"%s\"\n", + flagstring, if_entry->d_name); + } + free(flagstring); + return 0; + } + free(flagstring); + ifreq_flags flags = (ifreq_flags)tmpmax; + /* Reject the loopback device */ + if(flags & IFF_LOOPBACK){ + if(debug){ + fprintf(stderr, "Rejecting loopback interface \"%s\"\n", + if_entry->d_name); + } + return 0; + } + /* Accept point-to-point devices only if connect_to is specified */ + if(connect_to != NULL and (flags & IFF_POINTOPOINT)){ + if(debug){ + fprintf(stderr, "Accepting point-to-point interface \"%s\"\n", + if_entry->d_name); + } + return 1; + } + /* Otherwise, reject non-broadcast-capable devices */ + if(not (flags & IFF_BROADCAST)){ + if(debug){ + fprintf(stderr, "Rejecting non-broadcast interface \"%s\"\n", + if_entry->d_name); + } + return 0; + } + /* Accept this device */ + if(debug){ + fprintf(stderr, "Interface \"%s\" is acceptable\n", + if_entry->d_name); + } + return 1; } - int main(int argc, char *argv[]){ - AvahiSServiceBrowser *sb = NULL; - int error; - int ret; - int exitcode = EXIT_SUCCESS; - const char *interface = "eth0"; - struct ifreq network; - int sd; - uid_t uid; - gid_t gid; - char *connect_to = NULL; - AvahiIfIndex if_index = AVAHI_IF_UNSPEC; - char *pubkeyfilename = NULL; - char *seckeyfilename = NULL; - const char *pubkeyname = "pubkey.txt"; - const char *seckeyname = "seckey.txt"; - mandos_context mc = { .simple_poll = NULL, .server = NULL, - .dh_bits = 1024, .priority = "SECURE256"}; - bool gnutls_initalized = false; - - { - struct argp_option options[] = { - { .name = "debug", .key = 128, - .doc = "Debug mode", .group = 3 }, - { .name = "connect", .key = 'c', - .arg = "IP", - .doc = "Connect directly to a sepcified mandos server", - .group = 1 }, - { .name = "interface", .key = 'i', - .arg = "INTERFACE", - .doc = "Interface that Avahi will conntect through", - .group = 1 }, - { .name = "keydir", .key = 'd', - .arg = "KEYDIR", - .doc = "Directory where the openpgp keyring is", - .group = 1 }, - { .name = "seckey", .key = 's', - .arg = "SECKEY", - .doc = "Secret openpgp key for gnutls authentication", - .group = 1 }, - { .name = "pubkey", .key = 'p', - .arg = "PUBKEY", - .doc = "Public openpgp key for gnutls authentication", - .group = 2 }, - { .name = "dh-bits", .key = 129, - .arg = "BITS", - .doc = "dh-bits to use in gnutls communication", - .group = 2 }, - { .name = "priority", .key = 130, - .arg = "PRIORITY", - .doc = "GNUTLS priority", .group = 1 }, - { .name = NULL } - }; - - - error_t parse_opt (int key, char *arg, - struct argp_state *state) { - /* Get the INPUT argument from `argp_parse', which we know is - a pointer to our plugin list pointer. */ - switch (key) { - case 128: - debug = true; - break; - case 'c': - connect_to = arg; - break; - case 'i': - interface = arg; - break; - case 'd': - keydir = arg; - break; - case 's': - seckeyname = arg; - break; - case 'p': - pubkeyname = arg; - break; - case 129: - errno = 0; - mc.dh_bits = (unsigned int) strtol(arg, NULL, 10); - if (errno){ - perror("strtol"); - exit(EXIT_FAILURE); - } - break; - case 130: - mc.priority = arg; - break; - case ARGP_KEY_ARG: - argp_usage (state); - case ARGP_KEY_END: - break; - default: - return ARGP_ERR_UNKNOWN; - } - return 0; - } - - struct argp argp = { .options = options, .parser = parse_opt, - .args_doc = "", - .doc = "Mandos client -- Get and decrypt" - " passwords from mandos server" }; - ret = argp_parse (&argp, argc, argv, 0, 0, NULL); - if (ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); + AvahiSServiceBrowser *sb = NULL; + int error; + int ret; + intmax_t tmpmax; + char *tmp; + int exitcode = EXIT_SUCCESS; + const char *interface = ""; + struct ifreq network; + int sd = -1; + bool take_down_interface = false; + uid_t uid; + gid_t gid; + char tempdir[] = "/tmp/mandosXXXXXX"; + bool tempdir_created = false; + AvahiIfIndex if_index = AVAHI_IF_UNSPEC; + const char *seckey = PATHDIR "/" SECKEY; + const char *pubkey = PATHDIR "/" PUBKEY; + + bool gnutls_initialized = false; + bool gpgme_initialized = false; + float delay = 2.5f; + + struct sigaction old_sigterm_action = { .sa_handler = SIG_DFL }; + struct sigaction sigterm_action = { .sa_handler = handle_sigterm }; + + uid = getuid(); + gid = getgid(); + + /* Lower any group privileges we might have, just to be safe */ + errno = 0; + ret = setgid(gid); + if(ret == -1){ + perror("setgid"); + } + + /* Lower user privileges (temporarily) */ + errno = 0; + ret = seteuid(uid); + if(ret == -1){ + perror("seteuid"); + } + + if(quit_now){ + goto end; + } + + { + struct argp_option options[] = { + { .name = "debug", .key = 128, + .doc = "Debug mode", .group = 3 }, + { .name = "connect", .key = 'c', + .arg = "ADDRESS:PORT", + .doc = "Connect directly to a specific Mandos server", + .group = 1 }, + { .name = "interface", .key = 'i', + .arg = "NAME", + .doc = "Network interface that will be used to search for" + " Mandos servers", + .group = 1 }, + { .name = "seckey", .key = 's', + .arg = "FILE", + .doc = "OpenPGP secret key file base name", + .group = 1 }, + { .name = "pubkey", .key = 'p', + .arg = "FILE", + .doc = "OpenPGP public key file base name", + .group = 2 }, + { .name = "dh-bits", .key = 129, + .arg = "BITS", + .doc = "Bit length of the prime number used in the" + " Diffie-Hellman key exchange", + .group = 2 }, + { .name = "priority", .key = 130, + .arg = "STRING", + .doc = "GnuTLS priority string for the TLS handshake", + .group = 1 }, + { .name = "delay", .key = 131, + .arg = "SECONDS", + .doc = "Maximum delay to wait for interface startup", + .group = 2 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, + { .name = NULL } + }; + + error_t parse_opt(int key, char *arg, + struct argp_state *state){ + errno = 0; + switch(key){ + case 128: /* --debug */ + debug = true; + break; + case 'c': /* --connect */ + connect_to = arg; + break; + case 'i': /* --interface */ + interface = arg; + break; + case 's': /* --seckey */ + seckey = arg; + break; + case 'p': /* --pubkey */ + pubkey = arg; + break; + case 129: /* --dh-bits */ + errno = 0; + tmpmax = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmpmax != (typeof(mc.dh_bits))tmpmax){ + argp_error(state, "Bad number of DH bits"); + } + mc.dh_bits = (typeof(mc.dh_bits))tmpmax; + break; + case 130: /* --priority */ + mc.priority = arg; + break; + case 131: /* --delay */ + errno = 0; + delay = strtof(arg, &tmp); + if(errno != 0 or tmp == arg or *tmp != '\0'){ + argp_error(state, "Bad delay"); + } + break; + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + argp_state_help(state, state->out_stream, + (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) + & ~(unsigned int)ARGP_HELP_EXIT_OK); + case -3: /* --usage */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(argp_err_exit_status); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, .parser = parse_opt, + .args_doc = "", + .doc = "Mandos client -- Get and decrypt" + " passwords from a Mandos server" }; + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_HELP, 0, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + perror("argp_parse"); + exitcode = EX_OSERR; + goto end; + case EINVAL: + exitcode = EX_USAGE; + goto end; + } + } + + if(not debug){ + avahi_set_log_function(empty_log); + } + + if(interface[0] == '\0'){ + struct dirent **direntries; + ret = scandir(sys_class_net, &direntries, good_interface, + alphasort); + if(ret >= 1){ + /* Pick the first good interface */ + interface = strdup(direntries[0]->d_name); + if(debug){ + fprintf(stderr, "Using interface \"%s\"\n", interface); + } + if(interface == NULL){ + perror("malloc"); + free(direntries); exitcode = EXIT_FAILURE; goto end; } - } - - pubkeyfilename = combinepath(keydir, pubkeyname); - if (pubkeyfilename == NULL){ - perror("combinepath"); - exitcode = EXIT_FAILURE; - goto end; - } - - seckeyfilename = combinepath(keydir, seckeyname); - if (seckeyfilename == NULL){ - perror("combinepath"); - exitcode = EXIT_FAILURE; - goto end; - } - - ret = init_gnutls_global(&mc, pubkeyfilename, seckeyfilename); - if (ret == -1){ - fprintf(stderr, "init_gnutls_global failed\n"); - exitcode = EXIT_FAILURE; - goto end; + free(direntries); } else { - gnutls_initalized = true; - } - - /* If the interface is down, bring it up */ - { - sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); - if(sd < 0) { - perror("socket"); - exitcode = EXIT_FAILURE; - goto end; - } - strcpy(network.ifr_name, interface); - ret = ioctl(sd, SIOCGIFFLAGS, &network); - if(ret == -1){ - perror("ioctl SIOCGIFFLAGS"); - exitcode = EXIT_FAILURE; - goto end; - } - if((network.ifr_flags & IFF_UP) == 0){ - network.ifr_flags |= IFF_UP; - ret = ioctl(sd, SIOCSIFFLAGS, &network); - if(ret == -1){ - perror("ioctl SIOCSIFFLAGS"); - exitcode = EXIT_FAILURE; - goto end; - } - } - close(sd); - } - - uid = getuid(); - gid = getgid(); - - ret = setuid(uid); - if (ret == -1){ - perror("setuid"); - } - - setgid(gid); - if (ret == -1){ - perror("setgid"); - } - + free(direntries); + fprintf(stderr, "Could not find a network interface\n"); + exitcode = EXIT_FAILURE; + goto end; + } + } + + /* Initialize Avahi early so avahi_simple_poll_quit() can be called + from the signal handler */ + /* Initialize the pseudo-RNG for Avahi */ + srand((unsigned int) time(NULL)); + mc.simple_poll = avahi_simple_poll_new(); + if(mc.simple_poll == NULL){ + fprintf(stderr, "Avahi: Failed to create simple poll object.\n"); + exitcode = EX_UNAVAILABLE; + goto end; + } + + sigemptyset(&sigterm_action.sa_mask); + ret = sigaddset(&sigterm_action.sa_mask, SIGINT); + if(ret == -1){ + perror("sigaddset"); + exitcode = EX_OSERR; + goto end; + } + ret = sigaddset(&sigterm_action.sa_mask, SIGHUP); + if(ret == -1){ + perror("sigaddset"); + exitcode = EX_OSERR; + goto end; + } + ret = sigaddset(&sigterm_action.sa_mask, SIGTERM); + if(ret == -1){ + perror("sigaddset"); + exitcode = EX_OSERR; + goto end; + } + /* Need to check if the handler is SIG_IGN before handling: + | [[info:libc:Initial Signal Actions]] | + | [[info:libc:Basic Signal Handling]] | + */ + ret = sigaction(SIGINT, NULL, &old_sigterm_action); + if(ret == -1){ + perror("sigaction"); + return EX_OSERR; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &sigterm_action, NULL); + if(ret == -1){ + perror("sigaction"); + exitcode = EX_OSERR; + goto end; + } + } + ret = sigaction(SIGHUP, NULL, &old_sigterm_action); + if(ret == -1){ + perror("sigaction"); + return EX_OSERR; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &sigterm_action, NULL); + if(ret == -1){ + perror("sigaction"); + exitcode = EX_OSERR; + goto end; + } + } + ret = sigaction(SIGTERM, NULL, &old_sigterm_action); + if(ret == -1){ + perror("sigaction"); + return EX_OSERR; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &sigterm_action, NULL); + if(ret == -1){ + perror("sigaction"); + exitcode = EX_OSERR; + goto end; + } + } + + /* If the interface is down, bring it up */ + if(strcmp(interface, "none") != 0){ if_index = (AvahiIfIndex) if_nametoindex(interface); if(if_index == 0){ fprintf(stderr, "No such interface: \"%s\"\n", interface); - exit(EXIT_FAILURE); - } - - if(connect_to != NULL){ - /* Connect directly, do not use Zeroconf */ - /* (Mainly meant for debugging) */ - char *address = strrchr(connect_to, ':'); - if(address == NULL){ - fprintf(stderr, "No colon in address\n"); - exitcode = EXIT_FAILURE; - goto end; - } - errno = 0; - uint16_t port = (uint16_t) strtol(address+1, NULL, 10); - if(errno){ - perror("Bad port number"); - exitcode = EXIT_FAILURE; - goto end; - } - *address = '\0'; - address = connect_to; - ret = start_mandos_communication(address, port, if_index, &mc); - if(ret < 0){ - exitcode = EXIT_FAILURE; - } else { - exitcode = EXIT_SUCCESS; - } - goto end; - } - - if (not debug){ - avahi_set_log_function(empty_log); - } - - /* Initialize the pseudo-RNG for Avahi */ - srand((unsigned int) time(NULL)); - - /* Allocate main Avahi loop object */ - mc.simple_poll = avahi_simple_poll_new(); - if (mc.simple_poll == NULL) { - fprintf(stderr, "Avahi: Failed to create simple poll" - " object.\n"); - exitcode = EXIT_FAILURE; - goto end; - } - - { - AvahiServerConfig config; - /* Do not publish any local Zeroconf records */ - avahi_server_config_init(&config); - config.publish_hinfo = 0; - config.publish_addresses = 0; - config.publish_workstation = 0; - config.publish_domain = 0; - - /* Allocate a new server */ - mc.server = avahi_server_new(avahi_simple_poll_get - (mc.simple_poll), &config, NULL, - NULL, &error); - - /* Free the Avahi configuration data */ - avahi_server_config_free(&config); - } - - /* Check if creating the Avahi server object succeeded */ - if (mc.server == NULL) { - fprintf(stderr, "Failed to create Avahi server: %s\n", - avahi_strerror(error)); - exitcode = EXIT_FAILURE; - goto end; - } - - /* Create the Avahi service browser */ - sb = avahi_s_service_browser_new(mc.server, if_index, - AVAHI_PROTO_INET6, - "_mandos._tcp", NULL, 0, - browse_callback, &mc); - if (sb == NULL) { - fprintf(stderr, "Failed to create service browser: %s\n", - avahi_strerror(avahi_server_errno(mc.server))); - exitcode = EXIT_FAILURE; - goto end; - } - - /* Run the main loop */ - - if (debug){ - fprintf(stderr, "Starting Avahi loop search\n"); - } - - avahi_simple_poll_loop(mc.simple_poll); - + exitcode = EX_UNAVAILABLE; + goto end; + } + + if(quit_now){ + goto end; + } + + /* Re-raise priviliges */ + errno = 0; + ret = seteuid(0); + if(ret == -1){ + perror("seteuid"); + } + +#ifdef __linux__ + /* Lower kernel loglevel to KERN_NOTICE to avoid KERN_INFO + messages about the network interface to mess up the prompt */ + ret = klogctl(8, NULL, 5); + bool restore_loglevel = true; + if(ret == -1){ + restore_loglevel = false; + perror("klogctl"); + } +#endif /* __linux__ */ + + sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); + if(sd < 0){ + perror("socket"); + exitcode = EX_OSERR; +#ifdef __linux__ + if(restore_loglevel){ + ret = klogctl(7, NULL, 0); + if(ret == -1){ + perror("klogctl"); + } + } +#endif /* __linux__ */ + /* Lower privileges */ + errno = 0; + ret = seteuid(uid); + if(ret == -1){ + perror("seteuid"); + } + goto end; + } + strcpy(network.ifr_name, interface); + ret = ioctl(sd, SIOCGIFFLAGS, &network); + if(ret == -1){ + perror("ioctl SIOCGIFFLAGS"); +#ifdef __linux__ + if(restore_loglevel){ + ret = klogctl(7, NULL, 0); + if(ret == -1){ + perror("klogctl"); + } + } +#endif /* __linux__ */ + exitcode = EX_OSERR; + /* Lower privileges */ + errno = 0; + ret = seteuid(uid); + if(ret == -1){ + perror("seteuid"); + } + goto end; + } + if((network.ifr_flags & IFF_UP) == 0){ + network.ifr_flags |= IFF_UP; + take_down_interface = true; + ret = ioctl(sd, SIOCSIFFLAGS, &network); + if(ret == -1){ + take_down_interface = false; + perror("ioctl SIOCSIFFLAGS +IFF_UP"); + exitcode = EX_OSERR; +#ifdef __linux__ + if(restore_loglevel){ + ret = klogctl(7, NULL, 0); + if(ret == -1){ + perror("klogctl"); + } + } +#endif /* __linux__ */ + /* Lower privileges */ + errno = 0; + ret = seteuid(uid); + if(ret == -1){ + perror("seteuid"); + } + goto end; + } + } + /* sleep checking until interface is running */ + for(int i=0; i < delay * 4; i++){ + ret = ioctl(sd, SIOCGIFFLAGS, &network); + if(ret == -1){ + perror("ioctl SIOCGIFFLAGS"); + } else if(network.ifr_flags & IFF_RUNNING){ + break; + } + struct timespec sleeptime = { .tv_nsec = 250000000 }; + ret = nanosleep(&sleeptime, NULL); + if(ret == -1 and errno != EINTR){ + perror("nanosleep"); + } + } + if(not take_down_interface){ + /* We won't need the socket anymore */ + ret = (int)TEMP_FAILURE_RETRY(close(sd)); + if(ret == -1){ + perror("close"); + } + } +#ifdef __linux__ + if(restore_loglevel){ + /* Restores kernel loglevel to default */ + ret = klogctl(7, NULL, 0); + if(ret == -1){ + perror("klogctl"); + } + } +#endif /* __linux__ */ + /* Lower privileges */ + errno = 0; + if(take_down_interface){ + /* Lower privileges */ + ret = seteuid(uid); + if(ret == -1){ + perror("seteuid"); + } + } else { + /* Lower privileges permanently */ + ret = setuid(uid); + if(ret == -1){ + perror("setuid"); + } + } + } + + if(quit_now){ + goto end; + } + + ret = init_gnutls_global(pubkey, seckey); + if(ret == -1){ + fprintf(stderr, "init_gnutls_global failed\n"); + exitcode = EX_UNAVAILABLE; + goto end; + } else { + gnutls_initialized = true; + } + + if(quit_now){ + goto end; + } + + tempdir_created = true; + if(mkdtemp(tempdir) == NULL){ + tempdir_created = false; + perror("mkdtemp"); + goto end; + } + + if(quit_now){ + goto end; + } + + if(not init_gpgme(pubkey, seckey, tempdir)){ + fprintf(stderr, "init_gpgme failed\n"); + exitcode = EX_UNAVAILABLE; + goto end; + } else { + gpgme_initialized = true; + } + + if(quit_now){ + goto end; + } + + if(connect_to != NULL){ + /* Connect directly, do not use Zeroconf */ + /* (Mainly meant for debugging) */ + char *address = strrchr(connect_to, ':'); + if(address == NULL){ + fprintf(stderr, "No colon in address\n"); + exitcode = EX_USAGE; + goto end; + } + + if(quit_now){ + goto end; + } + + uint16_t port; + errno = 0; + tmpmax = strtoimax(address+1, &tmp, 10); + if(errno != 0 or tmp == address+1 or *tmp != '\0' + or tmpmax != (uint16_t)tmpmax){ + fprintf(stderr, "Bad port number\n"); + exitcode = EX_USAGE; + goto end; + } + + if(quit_now){ + goto end; + } + + port = (uint16_t)tmpmax; + *address = '\0'; + address = connect_to; + /* Colon in address indicates IPv6 */ + int af; + if(strchr(address, ':') != NULL){ + af = AF_INET6; + } else { + af = AF_INET; + } + + if(quit_now){ + goto end; + } + + while(not quit_now){ + ret = start_mandos_communication(address, port, if_index, af); + if(quit_now or ret == 0){ + break; + } + sleep(15); + }; + + if (not quit_now){ + exitcode = EXIT_SUCCESS; + } + + goto end; + } + + if(quit_now){ + goto end; + } + + { + AvahiServerConfig config; + /* Do not publish any local Zeroconf records */ + avahi_server_config_init(&config); + config.publish_hinfo = 0; + config.publish_addresses = 0; + config.publish_workstation = 0; + config.publish_domain = 0; + + /* Allocate a new server */ + mc.server = avahi_server_new(avahi_simple_poll_get + (mc.simple_poll), &config, NULL, + NULL, &error); + + /* Free the Avahi configuration data */ + avahi_server_config_free(&config); + } + + /* Check if creating the Avahi server object succeeded */ + if(mc.server == NULL){ + fprintf(stderr, "Failed to create Avahi server: %s\n", + avahi_strerror(error)); + exitcode = EX_UNAVAILABLE; + goto end; + } + + if(quit_now){ + goto end; + } + + /* Create the Avahi service browser */ + sb = avahi_s_service_browser_new(mc.server, if_index, + AVAHI_PROTO_UNSPEC, "_mandos._tcp", + NULL, 0, browse_callback, NULL); + if(sb == NULL){ + fprintf(stderr, "Failed to create service browser: %s\n", + avahi_strerror(avahi_server_errno(mc.server))); + exitcode = EX_UNAVAILABLE; + goto end; + } + + if(quit_now){ + goto end; + } + + /* Run the main loop */ + + if(debug){ + fprintf(stderr, "Starting Avahi loop search\n"); + } + + avahi_simple_poll_loop(mc.simple_poll); + end: - - if (debug){ - fprintf(stderr, "%s exiting\n", argv[0]); - } - - /* Cleanup things */ - if (sb != NULL) - avahi_s_service_browser_free(sb); - - if (mc.server != NULL) - avahi_server_free(mc.server); - - if (mc.simple_poll != NULL) - avahi_simple_poll_free(mc.simple_poll); - free(pubkeyfilename); - free(seckeyfilename); - - if (gnutls_initalized){ - gnutls_certificate_free_credentials(mc.cred); - gnutls_global_deinit (); - } - - return exitcode; + + if(debug){ + fprintf(stderr, "%s exiting\n", argv[0]); + } + + /* Cleanup things */ + if(sb != NULL) + avahi_s_service_browser_free(sb); + + if(mc.server != NULL) + avahi_server_free(mc.server); + + if(mc.simple_poll != NULL) + avahi_simple_poll_free(mc.simple_poll); + + if(gnutls_initialized){ + gnutls_certificate_free_credentials(mc.cred); + gnutls_global_deinit(); + gnutls_dh_params_deinit(mc.dh_params); + } + + if(gpgme_initialized){ + gpgme_release(mc.ctx); + } + + /* Take down the network interface */ + if(take_down_interface){ + /* Re-raise priviliges */ + errno = 0; + ret = seteuid(0); + if(ret == -1){ + perror("seteuid"); + } + if(geteuid() == 0){ + ret = ioctl(sd, SIOCGIFFLAGS, &network); + if(ret == -1){ + perror("ioctl SIOCGIFFLAGS"); + } else if(network.ifr_flags & IFF_UP) { + network.ifr_flags &= ~(short)IFF_UP; /* clear flag */ + ret = ioctl(sd, SIOCSIFFLAGS, &network); + if(ret == -1){ + perror("ioctl SIOCSIFFLAGS -IFF_UP"); + } + } + ret = (int)TEMP_FAILURE_RETRY(close(sd)); + if(ret == -1){ + perror("close"); + } + /* Lower privileges permanently */ + errno = 0; + ret = setuid(uid); + if(ret == -1){ + perror("setuid"); + } + } + } + + /* Removes the temp directory used by GPGME */ + if(tempdir_created){ + DIR *d; + struct dirent *direntry; + d = opendir(tempdir); + if(d == NULL){ + if(errno != ENOENT){ + perror("opendir"); + } + } else { + while(true){ + direntry = readdir(d); + if(direntry == NULL){ + break; + } + /* Skip "." and ".." */ + if(direntry->d_name[0] == '.' + and (direntry->d_name[1] == '\0' + or (direntry->d_name[1] == '.' + and direntry->d_name[2] == '\0'))){ + continue; + } + char *fullname = NULL; + ret = asprintf(&fullname, "%s/%s", tempdir, + direntry->d_name); + if(ret < 0){ + perror("asprintf"); + continue; + } + ret = remove(fullname); + if(ret == -1){ + fprintf(stderr, "remove(\"%s\"): %s\n", fullname, + strerror(errno)); + } + free(fullname); + } + closedir(d); + } + ret = rmdir(tempdir); + if(ret == -1 and errno != ENOENT){ + perror("rmdir"); + } + } + + if(quit_now){ + sigemptyset(&old_sigterm_action.sa_mask); + old_sigterm_action.sa_handler = SIG_DFL; + ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, + &old_sigterm_action, + NULL)); + if(ret == -1){ + perror("sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + perror("raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return exitcode; } === renamed file 'plugins.d/password-request.xml' => 'plugins.d/mandos-client.xml' --- plugins.d/password-request.xml 2008-08-18 05:24:20 +0000 +++ plugins.d/mandos-client.xml 2010-09-26 18:32:58 +0000 @@ -1,18 +1,19 @@ - - + - + + + +%common; ]> - + - &COMMANDNAME; - - &COMMANDNAME; - &VERSION; + Mandos Manual + + Mandos + &version; + &TIMESTAMP; Björn @@ -31,33 +32,13 @@ 2008 - Teddy Hogeborn & Björn Påhlsson + 2009 + Teddy Hogeborn + Björn Påhlsson - - - This manual page 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 manual page 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 - . - - + - + &COMMANDNAME; 8mandos @@ -66,252 +47,608 @@ &COMMANDNAME; - Client for mandos + Client for Mandos - + &COMMANDNAME; - --connectIP - --keydirKEYDIR - --interfaceINTERFACE - --pubkeyPUBKEY - --seckeySECKEY - --priorityPRIORITY - --dh-bitsBITS - --debug - - - &COMMANDNAME; - --help - - - &COMMANDNAME; - --usage - - - &COMMANDNAME; - --version - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + - + DESCRIPTION - &COMMANDNAME; is a mandos plugin that works - like a client program that through avahi detects mandos servers, - sets up a gnutls connect and request a encrypted password. Any - passwords given is automaticly decrypted and passed to - cryptsetup. - - + &COMMANDNAME; is a client program that + communicates with mandos8 + to get a password. In slightly more detail, this client program + brings up a network interface, uses the interface’s IPv6 + link-local address to get network connectivity, uses Zeroconf to + find servers on the local network, and communicates with servers + using TLS with an OpenPGP key to ensure authenticity and + confidentiality. This client program keeps running, trying all + servers on the network, until it receives a satisfactory reply + or a TERM signal is received. If no servers are found, or after + all servers have been tried, it waits indefinitely for new + servers to appear. + + + This program is not meant to be run directly; it is really meant + to run as a plugin of the Mandos + plugin-runner + 8mandos, which runs in the + initial RAM disk environment because it is + specified as a keyscript in the + crypttab5 + file. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + OPTIONS - Commonly not invoked as command lines but from configuration - file of plugin runner. + This program is commonly not invoked from the command line; it + is normally started by the Mandos + plugin runner, see plugin-runner8mandos + . Any command line options this program accepts + are therefore normally provided by the plugin runner, and not + directly. - + - -c, --connect= - IP - - - Connect directly to a specified mandos server - - - - - - -d, --keydir= - KEYDIR - - - Directory where the openpgp keyring is - - - - - - -i, --interface= - INTERFACE - - - Interface that Avahi will conntect through - - - - - - -p, --pubkey= - PUBKEY - - - Public openpgp key for gnutls authentication - - - - - - -s, --seckey= - SECKEY - - - Secret openpgp key for gnutls authentication - - - - - - --priority=PRIORITY - - - - GNUTLS priority - - - - - - --dh-bits=BITS - - - - dh-bits to use in gnutls communication - - - - - - --debug - - - Debug mode - - - - - - -?, --help - - - Gives a help message - - - - - - --usage - - - Gives a short usage message - - - - - - -V, --version - - - Prints the program version - - - + + + + + Do not use Zeroconf to locate servers. Connect directly + to only one specified Mandos + server. Note that an IPv6 address has colon characters in + it, so the last colon character is + assumed to separate the address from the port number. + + + This option is normally only useful for testing and + debugging. + + + + + + + + + + Network interface that will be brought up and scanned for + Mandos servers to connect to. The default is the empty + string, which will automatically choose an appropriate + interface. + + + If the option is used, this + specifies the interface to use to connect to the address + given. + + + Note that since this program will normally run in the + initial RAM disk environment, the interface must be an + interface which exists at that stage. Thus, the interface + can not be a pseudo-interface such as br0 + or tun0; such interfaces will not exist + until much later in the boot process, and can not be used + by this program. + + + NAME can be the string + none; this will not use + any specific interface, and will not bring up an interface + on startup. This is not recommended, and only meant for + advanced users. + + + + + + + + + + OpenPGP public key file name. The default name is + /conf/conf.d/mandos/pubkey.txt. + + + + + + + + + + OpenPGP secret key file name. The default name is + /conf/conf.d/mandos/seckey.txt. + + + + + + + + + + + + + + + + Sets the number of bits to use for the prime number in the + TLS Diffie-Hellman key exchange. Default is 1024. + + + + + + + + + After bringing the network interface up, the program waits + for the interface to arrive in a running + state before proceeding. During this time, the kernel log + level will be lowered to reduce clutter on the system + console, alleviating any other plugins which might be + using the system console. This option sets the upper + limit of seconds to wait. The default is 2.5 seconds. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + + + It will also enable debug mode in the Avahi and GnuTLS + libraries, making them print large amounts of debugging + output. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + - + + + OVERVIEW + + + This program is the client part. It is a plugin started by + plugin-runner + 8mandos which will run in + an initial RAM disk environment. + + + This program could, theoretically, be used as a keyscript in + /etc/crypttab, but it would then be + impossible to enter a password for the encrypted root disk at + the console, since this program does not read from the console + at all. This is why a separate plugin runner ( + plugin-runner + 8mandos) is used to run + both this program and others in in parallel, + one of which will prompt for passwords on + the system console. + + + EXIT STATUS + This program will exit with a successful (zero) exit status if a + server could be found and the password received from it could be + successfully decrypted and output on standard output. The + program will exit with a non-zero exit status only if a critical + error occurs. Otherwise, it will forever connect to new + Mandos servers as they appear, trying + to get a decryptable password and print it. - + ENVIRONMENT + This program does not use any environment variables, not even + the ones provided by cryptsetup8 + . - - - + + + FILES - - - - - - BUGS - - - - + + + /conf/conf.d/mandos/pubkey.txt + /conf/conf.d/mandos/seckey.txt + + + OpenPGP public and private key files, in ASCII + Armor format. These are the default file names, + they can be changed with the and + options. + + + + + + + + + + + + EXAMPLE + Note that normally, command line options will not be given + directly, but via options for the Mandos plugin-runner + 8mandos. + + + Normal invocation needs no options, if the network interface + is eth0: + + + &COMMANDNAME; + + + + + Search for Mandos servers (and connect to them) using another + interface: + + + + &COMMANDNAME; --interface eth1 + + + + + Run in debug mode, and use a custom key: + + + + +&COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt + + + + + + Run in debug mode, with a custom key, and do not use Zeroconf + to locate a server; connect directly to the IPv6 link-local + address fe80::aede:48ff:fe71:f6f2, port 4711, + using interface eth2: + + + + +&COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt --connect fe80::aede:48ff:fe71:f6f2:4711 --interface eth2 + + + - + SECURITY + This program is set-uid to root, but will switch back to the + original (and presumably non-privileged) user and group after + bringing up the network interface. + + + To use this program for its intended purpose (see ), the password for the root file system will + have to be given out to be stored in a server computer, after + having been encrypted using an OpenPGP key. This encrypted data + which will be stored in a server can only be decrypted by the + OpenPGP key, and the data will only be given out to those + clients who can prove they actually have that key. This key, + however, is stored unencrypted on the client side in its initial + RAM disk image file system. This is normally + readable by all, but this is normally fixed during installation + of this program; file permissions are set so that no-one is able + to read that file. + + + The only remaining weak point is that someone with physical + access to the client hard drive might turn off the client + computer, read the OpenPGP keys directly from the hard drive, + and communicate with the server. To safeguard against this, the + server is supposed to notice the client disappearing and stop + giving out the encrypted data. Therefore, it is important to + set the timeout and checker interval values tightly on the + server. See mandos8. + + + It will also help if the checker program on the server is + configured to request something from the client which can not be + spoofed by someone else on the network, unlike unencrypted + ICMP echo (ping) replies. + + + Note: This makes it completely insecure to + have Mandos clients which dual-boot + to another operating system which is not + trusted to keep the initial RAM disk image + confidential. - + SEE ALSO - - - mandos - 8 - - - - plugin-runner - 8mandos - - - - password-prompt - 8mandos - - - - Zeroconf - - - - Avahi - - - - GnuTLS - - - - - GPGME - - - - RFC 4880: OpenPGP Message - Format - - - - RFC 5081: Using OpenPGP Keys for - Transport Layer Security - - - - RFC 4291: IP Version 6 Addressing - Architecture, section 2.5.6, Link-Local IPv6 - Unicast Addresses - - + + cryptsetup + 8, + crypttab + 5, + mandos + 8, + password-prompt + 8mandos, + plugin-runner + 8mandos + + + + + Zeroconf + + + + Zeroconf is the network protocol standard used for finding + Mandos servers on the local network. + + + + + + Avahi + + + + Avahi is the library this program calls to find Zeroconf + services. + + + + + + GnuTLS + + + + GnuTLS is the library this client uses to implement TLS for + communicating securely with the server, and at the same time + send the public OpenPGP key to the server. + + + + + + GPGME + + + + GPGME is the library used to decrypt the OpenPGP data sent + by the server. + + + + + + RFC 4291: IP Version 6 Addressing + Architecture + + + + + Section 2.2: Text Representation of + Addresses + + + + Section 2.5.5.2: IPv4-Mapped IPv6 + Address + + + + Section 2.5.6, Link-Local IPv6 Unicast + Addresses + + + 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 + is brought up. + + + + + + + + + RFC 4346: The Transport Layer Security (TLS) + Protocol Version 1.1 + + + + TLS 1.1 is the protocol implemented by GnuTLS. + + + + + + RFC 4880: OpenPGP Message Format + + + + The data received from the server is binary encrypted + OpenPGP data. + + + + + + RFC 5081: Using OpenPGP Keys for Transport Layer + Security + + + + This is implemented by GnuTLS and used by this program so + that OpenPGP keys can be used. + + + + - + + + + + + === modified file 'plugins.d/password-prompt.c' --- plugins.d/password-prompt.c 2008-08-16 16:58:31 +0000 +++ plugins.d/password-prompt.c 2010-09-26 18:32:58 +0000 @@ -1,8 +1,9 @@ -/* -*- coding: utf-8 -*- */ +/* -*- coding: utf-8; mode: c; mode: orgtbl -*- */ /* - * Passprompt - Read a password from the terminal and print it - * - * Copyright © 2007-2008 Teddy Hogeborn & Björn Påhlsson + * Password-prompt - Read a password from the terminal and print it + * + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 @@ -18,8 +19,7 @@ * along with this program. If not, see * . * - * Contact the authors at and - * . + * Contact the authors at . */ #define _GNU_SOURCE /* getline() */ @@ -32,35 +32,47 @@ #include /* sig_atomic_t, raise(), struct sigaction, sigemptyset(), sigaction(), sigaddset(), SIGINT, - SIGQUIT, SIGHUP, SIGTERM */ + SIGQUIT, SIGHUP, SIGTERM, + raise() */ #include /* NULL, size_t, ssize_t */ #include /* ssize_t */ #include /* EXIT_SUCCESS, EXIT_FAILURE, - getopt_long, getenv() */ + getenv() */ #include /* fprintf(), stderr, getline(), - stdin, feof(), perror(), fputc(), - stdout, getopt_long */ -#include /* errno, EINVAL */ + stdin, feof(), fputc() + */ +#include /* errno, EBADF, ENOTTY, EINVAL, + EFAULT, EFBIG, EIO, ENOSPC, EINTR + */ +#include /* error() */ #include /* or, not */ #include /* bool, false, true */ -#include /* strlen, rindex, strncmp, strcmp */ +#include /* strlen, rindex */ #include /* struct argp_option, struct argp_state, struct argp, argp_parse(), error_t, ARGP_KEY_ARG, ARGP_KEY_END, ARGP_ERR_UNKNOWN */ +#include /* EX_SOFTWARE, EX_OSERR, + EX_UNAVAILABLE, EX_IOERR, EX_OK */ -volatile bool quit_now = false; +volatile sig_atomic_t quit_now = 0; +int signal_received; bool debug = false; -const char *argp_program_version = "password-prompt 1.0"; +const char *argp_program_version = "password-prompt " VERSION; const char *argp_program_bug_address = ""; -static void termination_handler(__attribute__((unused))int signum){ - quit_now = true; +static void termination_handler(int signum){ + if(quit_now){ + return; + } + quit_now = 1; + signal_received = signum; } int main(int argc, char **argv){ - ssize_t ret; + ssize_t sret; + int ret; size_t n; struct termios t_new, t_old; char *buffer = NULL; @@ -73,112 +85,173 @@ struct argp_option options[] = { { .name = "prefix", .key = 'p', .arg = "PREFIX", .flags = 0, - .doc = "Prefix used before the passprompt", .group = 2 }, + .doc = "Prefix shown before the prompt", .group = 2 }, { .name = "debug", .key = 128, .doc = "Debug mode", .group = 3 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, { .name = NULL } }; - - error_t parse_opt (int key, char *arg, struct argp_state *state) { - /* Get the INPUT argument from `argp_parse', which we know is a - pointer to our plugin list pointer. */ - switch (key) { + + error_t parse_opt (int key, char *arg, struct argp_state *state){ + errno = 0; + switch (key){ case 'p': prefix = arg; break; case 128: debug = true; break; - case ARGP_KEY_ARG: - argp_usage (state); - break; - case ARGP_KEY_END: + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + argp_state_help(state, state->out_stream, + (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) + & ~(unsigned int)ARGP_HELP_EXIT_OK); + case -3: /* --usage */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(argp_err_exit_status); break; default: return ARGP_ERR_UNKNOWN; } - return 0; + return errno; } - + struct argp argp = { .options = options, .parser = parse_opt, .args_doc = "", - .doc = "Mandos Passprompt -- Provides a passprompt" }; - ret = argp_parse (&argp, argc, argv, 0, 0, NULL); - if (ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - return EXIT_FAILURE; + .doc = "Mandos password-prompt -- Read and" + " output a password" }; + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_HELP, NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + return EX_OSERR; + case EINVAL: + return EX_USAGE; } } - - if (debug){ + + if(debug){ fprintf(stderr, "Starting %s\n", argv[0]); } - if (debug){ + if(debug){ fprintf(stderr, "Storing current terminal attributes\n"); } - if (tcgetattr(STDIN_FILENO, &t_old) != 0){ - return EXIT_FAILURE; + if(tcgetattr(STDIN_FILENO, &t_old) != 0){ + int e = errno; + error(0, errno, "tcgetattr"); + switch(e){ + case EBADF: + case ENOTTY: + return EX_UNAVAILABLE; + default: + return EX_OSERR; + } } sigemptyset(&new_action.sa_mask); - sigaddset(&new_action.sa_mask, SIGINT); - sigaddset(&new_action.sa_mask, SIGHUP); - sigaddset(&new_action.sa_mask, SIGTERM); + ret = sigaddset(&new_action.sa_mask, SIGINT); + if(ret == -1){ + error(0, errno, "sigaddset"); + return EX_OSERR; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + error(0, errno, "sigaddset"); + return EX_OSERR; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + error(0, errno, "sigaddset"); + return EX_OSERR; + } + /* Need to check if the handler is SIG_IGN before handling: + | [[info:libc:Initial Signal Actions]] | + | [[info:libc:Basic Signal Handling]] | + */ ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } - if (old_action.sa_handler != SIG_IGN){ + if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } } ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } - if (old_action.sa_handler != SIG_IGN){ + if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } } ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } - if (old_action.sa_handler != SIG_IGN){ + if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } } - if (debug){ + if(debug){ fprintf(stderr, "Removing echo flag from terminal attributes\n"); } t_new = t_old; - t_new.c_lflag &= ~ECHO; - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ - perror("tcsetattr-echo"); - return EXIT_FAILURE; + t_new.c_lflag &= ~(tcflag_t)ECHO; + if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ + int e = errno; + error(0, errno, "tcsetattr-echo"); + switch(e){ + case EBADF: + case ENOTTY: + return EX_UNAVAILABLE; + case EINVAL: + default: + return EX_OSERR; + } } - - if (debug){ + + if(debug){ fprintf(stderr, "Waiting for input from stdin \n"); } while(true){ - if (quit_now){ + if(quit_now){ + if(debug){ + fprintf(stderr, "Interrupted by signal, exiting.\n"); + } status = EXIT_FAILURE; break; } @@ -187,52 +260,135 @@ fprintf(stderr, "%s ", prefix); } { - const char *cryptsource = getenv("cryptsource"); - const char *crypttarget = getenv("crypttarget"); - const char *const prompt - = "Enter passphrase to unlock the disk"; + const char *cryptsource = getenv("CRYPTTAB_SOURCE"); + const char *crypttarget = getenv("CRYPTTAB_NAME"); + /* Before cryptsetup 1.1.0~rc2 */ + if(cryptsource == NULL){ + cryptsource = getenv("cryptsource"); + } + if(crypttarget == NULL){ + crypttarget = getenv("crypttarget"); + } + const char *const prompt1 = "Unlocking the disk"; + const char *const prompt2 = "Enter passphrase"; if(cryptsource == NULL){ if(crypttarget == NULL){ - fprintf(stderr, "%s: ", prompt); + fprintf(stderr, "%s to unlock the disk: ", prompt2); } else { - fprintf(stderr, "%s (%s): ", prompt, crypttarget); + fprintf(stderr, "%s (%s)\n%s: ", prompt1, crypttarget, + prompt2); } } else { if(crypttarget == NULL){ - fprintf(stderr, "%s %s: ", prompt, cryptsource); + fprintf(stderr, "%s %s\n%s: ", prompt1, cryptsource, + prompt2); } else { - fprintf(stderr, "%s %s (%s): ", prompt, cryptsource, - crypttarget); + fprintf(stderr, "%s %s (%s)\n%s: ", prompt1, cryptsource, + crypttarget, prompt2); } } } - ret = getline(&buffer, &n, stdin); - if (ret > 0){ - fprintf(stdout, "%s", buffer); + sret = getline(&buffer, &n, stdin); + if(sret > 0){ status = EXIT_SUCCESS; + /* Make n = data size instead of allocated buffer size */ + n = (size_t)sret; + /* Strip final newline */ + if(n > 0 and buffer[n-1] == '\n'){ + buffer[n-1] = '\0'; /* not strictly necessary */ + n--; + } + size_t written = 0; + while(written < n){ + sret = write(STDOUT_FILENO, buffer + written, n - written); + if(sret < 0){ + int e = errno; + error(0, errno, "write"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + case EFBIG: + case EIO: + case ENOSPC: + default: + status = EX_IOERR; + break; + case EINTR: + status = EXIT_FAILURE; + break; + } + break; + } + written += (size_t)sret; + } + sret = close(STDOUT_FILENO); + if(sret == -1){ + int e = errno; + error(0, errno, "close"); + switch(e){ + case EBADF: + status = EX_OSFILE; + break; + case EIO: + default: + status = EX_IOERR; + break; + } + } break; } - if (ret < 0){ - if (errno != EINTR and not feof(stdin)){ - perror("getline"); - status = EXIT_FAILURE; + if(sret < 0){ + int e = errno; + if(errno != EINTR and not feof(stdin)){ + error(0, errno, "getline"); + switch(e){ + case EBADF: + status = EX_UNAVAILABLE; + case EIO: + case EINVAL: + default: + status = EX_IOERR; + break; + } break; } } - /* if(ret == 0), then the only sensible thing to do is to retry to + /* if(sret == 0), then the only sensible thing to do is to retry to read from stdin */ fputc('\n', stderr); + if(debug and not quit_now){ + /* If quit_now is nonzero, we were interrupted by a signal, and + will print that later, so no need to show this too. */ + fprintf(stderr, "getline() returned 0, retrying.\n"); + } } - if (debug){ + free(buffer); + + if(debug){ fprintf(stderr, "Restoring terminal attributes\n"); } - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ - perror("tcsetattr+echo"); - } - - if (debug){ - fprintf(stderr, "%s is exiting\n", argv[0]); + if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ + error(0, errno, "tcsetattr+echo"); + } + + if(quit_now){ + sigemptyset(&old_action.sa_mask); + old_action.sa_handler = SIG_DFL; + ret = sigaction(signal_received, &old_action, NULL); + if(ret == -1){ + error(0, errno, "sigaction"); + } + raise(signal_received); + } + + if(debug){ + fprintf(stderr, "%s is exiting with status %d\n", argv[0], + status); + } + if(status == EXIT_SUCCESS or status == EX_OK){ + fputc('\n', stderr); } return status; === modified file 'plugins.d/password-prompt.xml' --- plugins.d/password-prompt.xml 2008-08-18 05:24:20 +0000 +++ plugins.d/password-prompt.xml 2009-10-30 16:23:43 +0000 @@ -1,18 +1,19 @@ - - + + + +%common; ]> - + - &COMMANDNAME; - - &COMMANDNAME; - &VERSION; + Mandos Manual + + Mandos + &version; + &TIMESTAMP; Björn @@ -31,33 +32,13 @@ 2008 - Teddy Hogeborn & Björn Påhlsson + 2009 + Teddy Hogeborn + Björn Påhlsson - - - This manual page 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 manual page 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 - . - - + - + &COMMANDNAME; 8mandos @@ -65,146 +46,263 @@ &COMMANDNAME; - - Passprompt for luks during boot sequence - + Prompt for a password and output it. &COMMANDNAME; - --prefixPREFIX - --debug - - - &COMMANDNAME; - --help - - - &COMMANDNAME; - --usage - - - &COMMANDNAME; - --version - + + + PREFIX + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + - + DESCRIPTION - &COMMANDNAME; is a terminal program that ask for - passwords during boot sequence. It is a plugin to - mandos, and is used as a fallback and - alternative to retriving passwords from a mandos server. During - boot sequence the user is prompted for the disk password, and - when a password is given it then gets forwarded to - LUKS. + All &COMMANDNAME; does is prompt for a + password and output any given password to standard output. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + This program is little more than a getpass3 + wrapper, although actual use of that function is not guaranteed + or implied. OPTIONS - Commonly not invoked as command lines but from configuration - file of plugin runner. + This program is commonly not invoked from the command line; it + is normally started by the Mandos + plugin runner, see plugin-runner8mandos + . Any command line options this program accepts + are therefore normally provided by the plugin runner, and not + directly. - + - -p, --prefix=PREFIX - - - - Prefix used before the passprompt - - - - - - --debug - - - Debug mode - - - - - - -?, --help - - - Gives a help message - - - - - - --usage - - - Gives a short usage message - - - - - - -V, --version - - - Prints the program version - - - + + + + + Prefix string shown before the password prompt. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + - + EXIT STATUS + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program has encountered an error, and any output so far could be + corrupt and/or truncated, and should therefore be ignored. - + ENVIRONMENT - - - - - - FILES - - + + + CRYPTTAB_SOURCE + CRYPTTAB_NAME + + + If set, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which will + normally have inherited them from + /scripts/local-top/cryptroot in the + initial RAM disk environment, which will + have set them from parsing kernel arguments and + /conf/conf.d/cryptroot (also in the + initial RAM disk environment), which in turn will have been + created when the initial RAM disk image was created by + /usr/share/initramfs-tools/hooks/cryptroot, by + extracting the information of the root file system from + /etc/crypttab. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter. + + + + BUGS + None are known at this time. - - + + EXAMPLE + Note that normally, command line options will not be given + directly, but via options for the Mandos plugin-runner + 8mandos. + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Show a prefix before the prompt; in this case, a host name. + It might be useful to be reminded of which host needs a + password, in case of KVM switches, etc. + + + + +&COMMANDNAME; --prefix=host.example.org: + + + + + + Run in debug mode. + + + + &COMMANDNAME; --debug + + - + SECURITY + On its own, this program is very simple, and does not exactly + present any security risks. The one thing that could be + considered worthy of note is this: This program is meant to be + run by plugin-runner8mandos + , and will, when run standalone, outside, in a + normal environment, immediately output on its standard output + any presumably secret password it just received. Therefore, + when running this program standalone (which should never + normally be done), take care not to type in any real secret + password by force of habit, since it would then immediately be + shown as output. + + + To further alleviate any risk of being locked out of a system, + the plugin-runner + 8mandos has a fallback + mode which does the same thing as this program, only with less + features. - + SEE ALSO - mandos - 8, - plugin-runner - 8mandos and - password-request + crypttab + 5 + mandos-client 8mandos + plugin-runner + 8mandos, - - + + + + + + === added file 'plugins.d/plymouth.c' --- plugins.d/plymouth.c 1970-01-01 00:00:00 +0000 +++ plugins.d/plymouth.c 2010-09-26 18:32:58 +0000 @@ -0,0 +1,431 @@ +/* -*- coding: utf-8 -*- */ +/* + * Usplash - Read a password from usplash and output it + * + * Copyright © 2010 Teddy Hogeborn + * Copyright © 2010 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 . + */ + +#define _GNU_SOURCE /* asprintf(), TEMP_FAILURE_RETRY() */ +#include /* sig_atomic_t, struct sigaction, + sigemptyset(), sigaddset(), SIGINT, + SIGHUP, SIGTERM, sigaction(), + kill(), SIG_IGN */ +#include /* bool, false, true */ +#include /* open(), O_RDONLY */ +#include /* and, or, not*/ +#include /* size_t, ssize_t, pid_t, struct + dirent, waitpid() */ +#include /* waitpid() */ +#include /* NULL */ +#include /* strchr(), memcmp() */ +#include /* asprintf(), perror(), fopen(), + fscanf() */ +#include /* close(), readlink(), read(), + fork(), setsid(), chdir(), dup2(), + STDERR_FILENO, execv(), access() */ +#include /* free(), EXIT_FAILURE, realloc(), + EXIT_SUCCESS, malloc(), _exit(), + getenv() */ +#include /* scandir(), alphasort() */ +#include /* intmax_t, strtoumax(), SCNuMAX */ +#include /* struct stat, lstat() */ +#include /* EX_OSERR, EX_UNAVAILABLE */ +#include /* error() */ +#include /* TEMP_FAILURE_RETRY */ +#include /* argz_count(), argz_extract() */ + +sig_atomic_t interrupted_by_signal = 0; +const char plymouth_pid[] = "/dev/.initramfs/plymouth.pid"; +const char plymouth_path[] = "/bin/plymouth"; +const char plymouthd_path[] = "/sbin/plymouthd"; +const char *plymouthd_default_argv[] = {"/sbin/plymouthd", + "--mode=boot", + "--attach-to-session", + "--pid-file=" + "/dev/.initramfs/" + "plymouth.pid", + NULL }; + +static void termination_handler(__attribute__((unused))int signum){ + if(interrupted_by_signal){ + return; + } + interrupted_by_signal = 1; +} + +/* Create prompt string */ +char *makeprompt(void){ + int ret = 0; + char *prompt; + const char *const cryptsource = getenv("cryptsource"); + const char *const crypttarget = getenv("crypttarget"); + const char prompt_start[] = "Enter passphrase to unlock the disk"; + + if(cryptsource == NULL){ + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s: ", prompt_start); + } else { + ret = asprintf(&prompt, "%s (%s): ", prompt_start, + crypttarget); + } + } else { + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); + } else { + ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, + cryptsource, crypttarget); + } + } + if(ret == -1){ + return NULL; + } + return prompt; +} + +void kill_and_wait(pid_t pid){ + TEMP_FAILURE_RETRY(kill(pid, SIGTERM)); + TEMP_FAILURE_RETRY(waitpid(pid, NULL, 0)); +} + +bool become_a_daemon(void){ + int ret = setuid(geteuid()); + if(ret == -1){ + error(0, errno, "setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + error(0, errno, "chdir"); + return false; + } + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ + if(ret == -1){ + error(0, errno, "dup2"); + return false; + } + return true; +} + +bool exec_and_wait(pid_t *pid_return, const char *path, + const char **argv, bool interruptable, + bool daemonize){ + int status; + int ret; + pid_t pid; + pid = fork(); + if(pid == -1){ + error(0, errno, "fork"); + return false; + } + if(pid == 0){ + /* Child */ + if(daemonize){ + if(not become_a_daemon()){ + _exit(EX_OSERR); + } + } + + char **new_argv = NULL; + char *tmp; + int i = 0; + for (; argv[i]!=(char *)NULL; i++){ + tmp = realloc(new_argv, sizeof(const char *) * ((size_t)i + 1)); + if (tmp == NULL){ + error(0, errno, "realloc"); + free(new_argv); + _exit(EX_OSERR); + } + new_argv = (char **)tmp; + new_argv[i] = strdup(argv[i]); + } + new_argv[i] = (char *) NULL; + + execv(path, (char *const *)new_argv); + error(0, errno, "execv"); + _exit(EXIT_FAILURE); + } + if(pid_return != NULL){ + *pid_return = pid; + } + do { + ret = waitpid(pid, &status, 0); + } while(ret == -1 and errno == EINTR + and ((not interrupted_by_signal) + or (not interruptable))); + if(interrupted_by_signal and interruptable){ + return false; + } + if(ret == -1){ + error(0, errno, "waitpid"); + return false; + } + if(WIFEXITED(status) and (WEXITSTATUS(status) == 0)){ + return true; + } + return false; +} + +int is_plymouth(const struct dirent *proc_entry){ + int ret; + { + uintmax_t maxvalue; + char *tmp; + errno = 0; + maxvalue = strtoumax(proc_entry->d_name, &tmp, 10); + + if(errno != 0 or *tmp != '\0' + or maxvalue != (uintmax_t)((pid_t)maxvalue)){ + return 0; + } + } + char exe_target[sizeof(plymouth_path)]; + char *exe_link; + ret = asprintf(&exe_link, "/proc/%s/exe", proc_entry->d_name); + if(ret == -1){ + error(0, errno, "asprintf"); + return 0; + } + + struct stat exe_stat; + ret = lstat(exe_link, &exe_stat); + if(ret == -1){ + free(exe_link); + if(errno != ENOENT){ + error(0, errno, "lstat"); + } + return 0; + } + + if(not S_ISLNK(exe_stat.st_mode) + or exe_stat.st_uid != 0 + or exe_stat.st_gid != 0){ + free(exe_link); + return 0; + } + + ssize_t sret = readlink(exe_link, exe_target, sizeof(exe_target)); + free(exe_link); + if((sret != (ssize_t)sizeof(plymouth_path)-1) or + (memcmp(plymouth_path, exe_target, + sizeof(plymouth_path)-1) != 0)){ + return 0; + } + return 1; +} + +pid_t get_pid(void){ + int ret; + FILE *pidfile = fopen(plymouth_pid, "r"); + uintmax_t maxvalue = 0; + if(pidfile != NULL){ + ret = fscanf(pidfile, "%" SCNuMAX, &maxvalue); + if(ret != 1){ + maxvalue = 0; + } + fclose(pidfile); + } + if(maxvalue == 0){ + struct dirent **direntries; + ret = scandir("/proc", &direntries, is_plymouth, alphasort); + sscanf(direntries[0]->d_name, "%" SCNuMAX, &maxvalue); + } + pid_t pid; + pid = (pid_t)maxvalue; + if((uintmax_t)pid == maxvalue){ + return pid; + } + + return 0; +} + +const char **getargv(pid_t pid){ + int cl_fd; + char *cmdline_filename; + ssize_t sret; + int ret; + + ret = asprintf(&cmdline_filename, "/proc/%" PRIuMAX "/cmdline", + (uintmax_t)pid); + if(ret == -1){ + error(0, errno, "asprintf"); + return NULL; + } + + /* Open /proc//cmdline */ + cl_fd = open(cmdline_filename, O_RDONLY); + free(cmdline_filename); + if(cl_fd == -1){ + error(0, errno, "open"); + return NULL; + } + + size_t cmdline_allocated = 0; + size_t cmdline_len = 0; + char *cmdline = NULL; + char *tmp; + const size_t blocksize = 1024; + do { + /* Allocate more space? */ + if(cmdline_len + blocksize > cmdline_allocated){ + tmp = realloc(cmdline, cmdline_allocated + blocksize); + if(tmp == NULL){ + error(0, errno, "realloc"); + free(cmdline); + close(cl_fd); + return NULL; + } + cmdline = tmp; + cmdline_allocated += blocksize; + } + + /* Read data */ + sret = read(cl_fd, cmdline + cmdline_len, + cmdline_allocated - cmdline_len); + if(sret == -1){ + error(0, errno, "read"); + free(cmdline); + close(cl_fd); + return NULL; + } + cmdline_len += (size_t)sret; + } while(sret != 0); + ret = close(cl_fd); + if(ret == -1){ + error(0, errno, "close"); + free(cmdline); + return NULL; + } + + /* we got cmdline and cmdline_len, ignore rest... */ + char **argv = malloc((argz_count(cmdline, cmdline_len) + 1) + * sizeof(char *)); /* Get number of args */ + if(argv == NULL){ + error(0, errno, "argv = malloc()"); + free(cmdline); + return NULL; + } + argz_extract(cmdline, cmdline_len, argv); /* Create argv */ + return (const char **)argv; +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + char *prompt; + char *prompt_arg; + pid_t plymouth_command_pid; + int ret; + bool bret; + + /* test -x /bin/plymouth */ + ret = access(plymouth_path, X_OK); + if(ret == -1){ + /* Plymouth is probably not installed. Don't print an error + message, just exit. */ + exit(EX_UNAVAILABLE); + } + + { /* Add signal handlers */ + struct sigaction old_action, + new_action = { .sa_handler = termination_handler, + .sa_flags = 0 }; + sigemptyset(&new_action.sa_mask); + for(int *sig = (int[]){ SIGINT, SIGHUP, SIGTERM, 0 }; + *sig != 0; sig++){ + ret = sigaddset(&new_action.sa_mask, *sig); + if(ret == -1){ + error(EX_OSERR, errno, "sigaddset"); + } + ret = sigaction(*sig, NULL, &old_action); + if(ret == -1){ + error(EX_OSERR, errno, "sigaction"); + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(*sig, &new_action, NULL); + if(ret == -1){ + error(EX_OSERR, errno, "sigaction"); + } + } + } + } + + /* plymouth --ping */ + bret = exec_and_wait(&plymouth_command_pid, plymouth_path, + (const char *[]) + { plymouth_path, "--ping", NULL }, + true, false); + if(not bret){ + if(interrupted_by_signal){ + kill_and_wait(plymouth_command_pid); + exit(EXIT_FAILURE); + } + /* Plymouth is probably not running. Don't print an error + message, just exit. */ + exit(EX_UNAVAILABLE); + } + + prompt = makeprompt(); + ret = asprintf(&prompt_arg, "--prompt=%s", prompt); + free(prompt); + if(ret == -1){ + error(EX_OSERR, errno, "asprintf"); + } + + /* plymouth ask-for-password --prompt="$prompt" */ + bret = exec_and_wait(&plymouth_command_pid, + plymouth_path, (const char *[]) + { plymouth_path, "ask-for-password", + prompt_arg, NULL }, + true, false); + free(prompt_arg); + if(bret){ + exit(EXIT_SUCCESS); + } + if(not interrupted_by_signal){ + /* exec_and_wait failed for some other reason */ + exit(EXIT_FAILURE); + } + kill_and_wait(plymouth_command_pid); + + const char **plymouthd_argv; + pid_t pid = get_pid(); + if(pid == 0){ + error(0, 0, "plymouthd pid not found"); + plymouthd_argv = plymouthd_default_argv; + } else { + plymouthd_argv = getargv(pid); + } + + bret = exec_and_wait(NULL, plymouth_path, (const char *[]) + { plymouth_path, "quit", NULL }, + false, false); + if(not bret){ + exit(EXIT_FAILURE); + } + bret = exec_and_wait(NULL, plymouthd_path, plymouthd_argv, + false, true); + if(not bret){ + exit(EXIT_FAILURE); + } + exec_and_wait(NULL, plymouth_path, (const char *[]) + { plymouth_path, "show-splash", NULL }, + false, false); + exit(EXIT_FAILURE); +} === added file 'plugins.d/plymouth.xml' --- plugins.d/plymouth.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/plymouth.xml 2010-09-26 18:32:58 +0000 @@ -0,0 +1,278 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2010 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to use plymouth to get a + password. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program prompts for a password using + plymouth8 + and outputs any given password to standard + output. If no plymouth8 + process can be found, this program will immediately exit with an + exit code indicating failure. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + If this program is killed (presumably by + plugin-runner + 8mandos because some other + plugin provided the password), it cannot tell + plymouth8 + to abort requesting a password, because + plymouth + 8 does not support this. + Therefore, this program will then kill the + running plymouth + 8 process and start a + new one using the same command line + arguments as the old one was using. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + ENVIRONMENT + + + cryptsource + crypttarget + + + If set, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which will + normally have inherited them from + /scripts/local-top/cryptroot in the + initial RAM disk environment, which will + have set them from parsing kernel arguments and + /conf/conf.d/cryptroot (also in the + initial RAM disk environment), which in turn will have been + created when the initial RAM disk image was created by + /usr/share/initramfs-tools/hooks/cryptroot, by + extracting the information of the root file system from + /etc/crypttab. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter. + + + + + + + + FILES + + + /bin/plymouth + + + This is the command run to retrieve a password from + plymouth + 8. + + + + + /proc + + + To find the running plymouth8 + , this directory will be searched for + numeric entries which will be assumed to be directories. + In all those directories, the exe and + cmdline entries will be used to + determine the name of the running binary, effective user + and group ID, and the command line + arguments. See proc5 + . + + + + + /sbin/plymouthd + + + This is the name of the binary which will be searched for + in the process list. See plymouth8 + . + + + + + + + + BUGS + + Killing the plymouth8 + daemon and starting a new one is ugly, but necessary as long as + it does not support aborting a password request. + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + If this program is killed by a signal, it will kill the process + ID which at the start of this program was + determined to run plymouth8 + as root (see also ). There is a very + slight risk that, in the time between those events, that process + ID was freed and then taken up by another + process; the wrong process would then be killed. Now, this + program can only be killed by the user who started it; see + plugin-runner + 8mandos. This program + should therefore be started by a completely separate + non-privileged user, and no other programs should be allowed to + run as that special user. This means that it is not recommended + to use the user "nobody" to start this program, as other + possibly less trusted programs could be running as "nobody", and + they would then be able to kill this program, triggering the + killing of the process ID which may or may not + be plymouth + 8. + + + The only other thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + crypttab + 5, + plugin-runner + 8mandos, + proc + 5, + plymouth + 8 + + +
+ + + + + === added file 'plugins.d/splashy.c' --- plugins.d/splashy.c 1970-01-01 00:00:00 +0000 +++ plugins.d/splashy.c 2010-09-26 18:32:58 +0000 @@ -0,0 +1,442 @@ +/* -*- coding: utf-8 -*- */ +/* + * Splashy - Read a password from splashy and output it + * + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 . + */ + +#define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), asprintf() */ +#include /* sig_atomic_t, struct sigaction, + sigemptyset(), sigaddset(), SIGINT, + SIGHUP, SIGTERM, sigaction, + SIG_IGN, kill(), SIGKILL */ +#include /* NULL */ +#include /* getenv() */ +#include /* asprintf() */ +#include /* EXIT_FAILURE, free(), + EXIT_SUCCESS */ +#include /* pid_t, DIR, struct dirent, + ssize_t */ +#include /* opendir(), readdir(), closedir() */ +#include /* intmax_t, strtoimax() */ +#include /* struct stat, lstat(), S_ISLNK */ +#include /* not, or, and */ +#include /* readlink(), fork(), execl(), + sleep(), dup2() STDERR_FILENO, + STDOUT_FILENO, _exit(), + pause() */ +#include /* memcmp() */ +#include /* errno, EACCES, ENOTDIR, ELOOP, + ENOENT, ENAMETOOLONG, EMFILE, + ENFILE, ENOMEM, ENOEXEC, EINVAL, + E2BIG, EFAULT, EIO, ETXTBSY, + EISDIR, ELIBBAD, EPERM, EINTR, + ECHILD */ +#include /* error() */ +#include /* waitpid(), WIFEXITED(), + WEXITSTATUS() */ +#include /* EX_OSERR, EX_OSFILE, + EX_UNAVAILABLE */ + +sig_atomic_t interrupted_by_signal = 0; +int signal_received; + +static void termination_handler(int signum){ + if(interrupted_by_signal){ + return; + } + interrupted_by_signal = 1; + signal_received = signum; +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + int ret = 0; + char *prompt = NULL; + DIR *proc_dir = NULL; + pid_t splashy_pid = 0; + pid_t splashy_command_pid = 0; + int exitstatus = EXIT_FAILURE; + + /* Create prompt string */ + { + const char *const cryptsource = getenv("cryptsource"); + const char *const crypttarget = getenv("crypttarget"); + const char *const prompt_start = "getpass " + "Enter passphrase to unlock the disk"; + + if(cryptsource == NULL){ + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s: ", prompt_start); + } else { + ret = asprintf(&prompt, "%s (%s): ", prompt_start, + crypttarget); + } + } else { + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); + } else { + ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, + cryptsource, crypttarget); + } + } + if(ret == -1){ + prompt = NULL; + exitstatus = EX_OSERR; + goto failure; + } + } + + /* Find splashy process */ + { + const char splashy_name[] = "/sbin/splashy"; + proc_dir = opendir("/proc"); + if(proc_dir == NULL){ + int e = errno; + error(0, errno, "opendir"); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + case ENOENT: + default: + exitstatus = EX_OSFILE; + break; + case ENAMETOOLONG: + case EMFILE: + case ENFILE: + case ENOMEM: + exitstatus = EX_OSERR; + break; + } + goto failure; + } + for(struct dirent *proc_ent = readdir(proc_dir); + proc_ent != NULL; + proc_ent = readdir(proc_dir)){ + pid_t pid; + { + intmax_t tmpmax; + char *tmp; + errno = 0; + tmpmax = strtoimax(proc_ent->d_name, &tmp, 10); + if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0' + or tmpmax != (pid_t)tmpmax){ + /* Not a process */ + continue; + } + pid = (pid_t)tmpmax; + } + /* Find the executable name by doing readlink() on the + /proc//exe link */ + char exe_target[sizeof(splashy_name)]; + ssize_t sret; + { + char *exe_link; + ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); + if(ret == -1){ + error(0, errno, "asprintf"); + exitstatus = EX_OSERR; + goto failure; + } + + /* Check that it refers to a symlink owned by root:root */ + struct stat exe_stat; + ret = lstat(exe_link, &exe_stat); + if(ret == -1){ + if(errno == ENOENT){ + free(exe_link); + continue; + } + int e = errno; + error(0, errno, "lstat"); + free(exe_link); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + default: + exitstatus = EX_OSFILE; + break; + case ENAMETOOLONG: + exitstatus = EX_OSERR; + break; + } + goto failure; + } + if(not S_ISLNK(exe_stat.st_mode) + or exe_stat.st_uid != 0 + or exe_stat.st_gid != 0){ + free(exe_link); + continue; + } + + sret = readlink(exe_link, exe_target, sizeof(exe_target)); + free(exe_link); + } + if((sret == ((ssize_t)sizeof(exe_target)-1)) + and (memcmp(splashy_name, exe_target, + sizeof(exe_target)-1) == 0)){ + splashy_pid = pid; + break; + } + } + closedir(proc_dir); + proc_dir = NULL; + } + if(splashy_pid == 0){ + exitstatus = EX_UNAVAILABLE; + goto failure; + } + + /* Set up the signal handler */ + { + struct sigaction old_action, + new_action = { .sa_handler = termination_handler, + .sa_flags = 0 }; + sigemptyset(&new_action.sa_mask); + ret = sigaddset(&new_action.sa_mask, SIGINT); + if(ret == -1){ + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; + goto failure; + } + ret = sigaction(SIGINT, NULL, &old_action); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &new_action, NULL); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + } + ret = sigaction(SIGHUP, NULL, &old_action); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &new_action, NULL); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + } + ret = sigaction(SIGTERM, NULL, &old_action); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &new_action, NULL); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + } + } + + if(interrupted_by_signal){ + goto failure; + } + + /* Fork off the splashy command to prompt for password */ + splashy_command_pid = fork(); + if(splashy_command_pid != 0 and interrupted_by_signal){ + goto failure; + } + if(splashy_command_pid == -1){ + error(0, errno, "fork"); + exitstatus = EX_OSERR; + goto failure; + } + /* Child */ + if(splashy_command_pid == 0){ + if(not interrupted_by_signal){ + const char splashy_command[] = "/sbin/splashy_update"; + execl(splashy_command, splashy_command, prompt, (char *)NULL); + int e = errno; + error(0, errno, "execl"); + switch(e){ + case EACCES: + case ENOENT: + case ENOEXEC: + case EINVAL: + _exit(EX_UNAVAILABLE); + case ENAMETOOLONG: + case E2BIG: + case ENOMEM: + case EFAULT: + case EIO: + case EMFILE: + case ENFILE: + case ETXTBSY: + default: + _exit(EX_OSERR); + case ENOTDIR: + case ELOOP: + case EISDIR: + case ELIBBAD: + case EPERM: + _exit(EX_OSFILE); + } + } + free(prompt); + _exit(EXIT_FAILURE); + } + + /* Parent */ + free(prompt); + prompt = NULL; + + if(interrupted_by_signal){ + goto failure; + } + + /* Wait for command to complete */ + { + int status; + do { + ret = waitpid(splashy_command_pid, &status, 0); + } while(ret == -1 and errno == EINTR + and not interrupted_by_signal); + if(interrupted_by_signal){ + goto failure; + } + if(ret == -1){ + error(0, errno, "waitpid"); + if(errno == ECHILD){ + splashy_command_pid = 0; + } + } else { + /* The child process has exited */ + splashy_command_pid = 0; + if(WIFEXITED(status) and WEXITSTATUS(status) == 0){ + return EXIT_SUCCESS; + } + } + } + + failure: + + free(prompt); + + if(proc_dir != NULL){ + TEMP_FAILURE_RETRY(closedir(proc_dir)); + } + + if(splashy_command_pid != 0){ + TEMP_FAILURE_RETRY(kill(splashy_command_pid, SIGTERM)); + + TEMP_FAILURE_RETRY(kill(splashy_pid, SIGTERM)); + sleep(2); + while(TEMP_FAILURE_RETRY(kill(splashy_pid, 0)) == 0){ + TEMP_FAILURE_RETRY(kill(splashy_pid, SIGKILL)); + sleep(1); + } + pid_t new_splashy_pid = (pid_t)TEMP_FAILURE_RETRY(fork()); + if(new_splashy_pid == 0){ + /* Child; will become new splashy process */ + + /* Make the effective user ID (root) the only user ID instead of + the real user ID (_mandos) */ + ret = setuid(geteuid()); + if(ret == -1){ + error(0, errno, "setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + error(0, errno, "chdir"); + } +/* if(fork() != 0){ */ +/* _exit(EXIT_SUCCESS); */ +/* } */ + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace stdout */ + if(ret == -1){ + error(0, errno, "dup2"); + _exit(EX_OSERR); + } + + execl("/sbin/splashy", "/sbin/splashy", "boot", (char *)NULL); + { + int e = errno; + error(0, errno, "execl"); + switch(e){ + case EACCES: + case ENOENT: + case ENOEXEC: + default: + _exit(EX_UNAVAILABLE); + case ENAMETOOLONG: + case E2BIG: + case ENOMEM: + _exit(EX_OSERR); + case ENOTDIR: + case ELOOP: + _exit(EX_OSFILE); + } + } + } + } + + if(interrupted_by_signal){ + struct sigaction signal_action; + sigemptyset(&signal_action.sa_mask); + signal_action.sa_handler = SIG_DFL; + ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, + &signal_action, NULL)); + if(ret == -1){ + error(0, errno, "sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + error(0, errno, "raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return exitstatus; +} === added file 'plugins.d/splashy.xml' --- plugins.d/splashy.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/splashy.xml 2009-01-04 21:54:55 +0000 @@ -0,0 +1,283 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2008 + 2009 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to use splashy to get a + password. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program prompts for a password using + splashy_update + 8 and outputs any given + password to standard output. If no splashy8 + process can be found, this program will immediately exit with an + exit code indicating failure. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + If this program is killed (presumably by + plugin-runner + 8mandos because some other + plugin provided the password), it cannot tell + splashy8 + to abort requesting a password, because + splashy + 8 does not support this. + Therefore, this program will then kill the + running splashy + 8 process and start a + new one, using boot as the only argument. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + ENVIRONMENT + + + cryptsource + crypttarget + + + If set, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which will + normally have inherited them from + /scripts/local-top/cryptroot in the + initial RAM disk environment, which will + have set them from parsing kernel arguments and + /conf/conf.d/cryptroot (also in the + initial RAM disk environment), which in turn will have been + created when the initial RAM disk image was created by + /usr/share/initramfs-tools/hooks/cryptroot, by + extracting the information of the root file system from + /etc/crypttab. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter. + + + + + + + + FILES + + + /sbin/splashy_update + + + This is the command run to retrieve a password from + splashy + 8. See + splashy_update8 + . + + + + + /proc + + + To find the running splashy8 + , this directory will be searched for + numeric entries which will be assumed to be directories. + In all those directories, the exe + entry will be used to determine the name of the running + binary and the effective user and group + ID of the process. See + proc5. + + + + + /sbin/splashy + + + This is the name of the binary which will be searched for + in the process list. See splashy8 + . + + + + + + + + BUGS + + Killing splashy + 8 and starting a new one + is ugly, but necessary as long as it does not support aborting a + password request. + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + If this program is killed by a signal, it will kill the process + ID which at the start of this program was + determined to run splashy8 + as root (see also ). There is a very + slight risk that, in the time between those events, that process + ID was freed and then taken up by another + process; the wrong process would then be killed. Now, this + program can only be killed by the user who started it; see + plugin-runner + 8mandos. This program + should therefore be started by a completely separate + non-privileged user, and no other programs should be allowed to + run as that special user. This means that it is not recommended + to use the user "nobody" to start this program, as other + possibly less trusted programs could be running as "nobody", and + they would then be able to kill this program, triggering the + killing of the process ID which may or may not + be splashy + 8. + + + The only other thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + crypttab + 5, + plugin-runner + 8mandos, + proc + 5, + splashy + 8, + splashy_update + 8 + + +
+ + + + + === removed file 'plugins.d/usplash' --- plugins.d/usplash 2008-08-14 02:24:59 +0000 +++ plugins.d/usplash 1970-01-01 00:00:00 +0000 @@ -1,42 +0,0 @@ -#!/bin/sh -e - -# If not on a tty, then get rid of possibly disrupting stderr output -if ! tty -s; then - exec 2>/dev/null -fi - -test -x /sbin/usplash - -usplash="`pidof usplash -o $$`" -test -n "$usplash" - -# We get some variables from cryptsetup: -# $cryptsource the device node, like "/dev/sda3" -# $crypttarget the device mapper name, like "sda3_crypt". - -prompt="Enter passphrase to unlock" -if [ -n "$crypttarget" ]; then - prompt="$prompt the disk $crypttarget" -fi -if [ -n "$cryptsource" ]; then - prompt="$prompt ($cryptsource)" -fi - -splash_input_password(){ - test -p /dev/.initramfs/usplash_outfifo || return 1 - /sbin/usplash_write "INPUTQUIET $1" || return 1 - cat /dev/.initramfs/usplash_outfifo 2> /dev/null || return 1 -} - -# Usplash keeps waiting for input even if some other plugin provided -# the password, so we must kill it -trap "kill -TERM $usplash; sleep 2; kill -KILL $usplash; - kill -TERM $$" TERM HUP - -password="`splash_input_password \"$prompt: \" password`" - -trap - TERM - -/sbin/usplash_write "TIMEOUT 15" - -echo -n "$password" === added file 'plugins.d/usplash.c' --- plugins.d/usplash.c 1970-01-01 00:00:00 +0000 +++ plugins.d/usplash.c 2010-09-26 18:32:58 +0000 @@ -0,0 +1,658 @@ +/* -*- coding: utf-8 -*- */ +/* + * Usplash - Read a password from usplash and output it + * + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 . + */ + +#define _GNU_SOURCE /* asprintf(), TEMP_FAILURE_RETRY() */ +#include /* sig_atomic_t, struct sigaction, + sigemptyset(), sigaddset(), SIGINT, + SIGHUP, SIGTERM, sigaction(), + SIG_IGN, kill(), SIGKILL */ +#include /* bool, false, true */ +#include /* open(), O_WRONLY, O_RDONLY */ +#include /* and, or, not*/ +#include /* errno, EINTR */ +#include +#include /* size_t, ssize_t, pid_t, DIR, struct + dirent */ +#include /* NULL */ +#include /* strlen(), memcmp() */ +#include /* asprintf()*/ +#include /* close(), write(), readlink(), + read(), STDOUT_FILENO, sleep(), + fork(), setuid(), geteuid(), + setsid(), chdir(), dup2(), + STDERR_FILENO, execv() */ +#include /* free(), EXIT_FAILURE, realloc(), + EXIT_SUCCESS, malloc(), _exit(), + getenv() */ +#include /* opendir(), readdir(), closedir() */ +#include /* intmax_t, strtoimax() */ +#include /* struct stat, lstat(), S_ISLNK */ +#include /* EX_OSERR, EX_UNAVAILABLE */ +#include /* argz_count(), argz_extract() */ + +sig_atomic_t interrupted_by_signal = 0; +int signal_received; +const char usplash_name[] = "/sbin/usplash"; + +static void termination_handler(int signum){ + if(interrupted_by_signal){ + return; + } + interrupted_by_signal = 1; + signal_received = signum; +} + +static bool usplash_write(int *fifo_fd_r, + const char *cmd, const char *arg){ + /* + * usplash_write(&fd, "TIMEOUT", "15") will write "TIMEOUT 15\0" + * usplash_write(&fd, "PULSATE", NULL) will write "PULSATE\0" + * SEE ALSO + * usplash_write(8) + */ + int ret; + if(*fifo_fd_r == -1){ + ret = open("/dev/.initramfs/usplash_fifo", O_WRONLY); + if(ret == -1){ + return false; + } + *fifo_fd_r = ret; + } + + const char *cmd_line; + size_t cmd_line_len; + char *cmd_line_alloc = NULL; + if(arg == NULL){ + cmd_line = cmd; + cmd_line_len = strlen(cmd) + 1; + } else { + do { + ret = asprintf(&cmd_line_alloc, "%s %s", cmd, arg); + if(ret == -1){ + int e = errno; + TEMP_FAILURE_RETRY(close(*fifo_fd_r)); + errno = e; + return false; + } + } while(ret == -1); + cmd_line = cmd_line_alloc; + cmd_line_len = (size_t)ret + 1; + } + + size_t written = 0; + ssize_t sret = 0; + while(written < cmd_line_len){ + sret = write(*fifo_fd_r, cmd_line + written, + cmd_line_len - written); + if(sret == -1){ + int e = errno; + TEMP_FAILURE_RETRY(close(*fifo_fd_r)); + free(cmd_line_alloc); + errno = e; + return false; + } + written += (size_t)sret; + } + free(cmd_line_alloc); + + return true; +} + +/* Create prompt string */ +char *makeprompt(void){ + int ret = 0; + char *prompt; + const char *const cryptsource = getenv("cryptsource"); + const char *const crypttarget = getenv("crypttarget"); + const char prompt_start[] = "Enter passphrase to unlock the disk"; + + if(cryptsource == NULL){ + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s: ", prompt_start); + } else { + ret = asprintf(&prompt, "%s (%s): ", prompt_start, + crypttarget); + } + } else { + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); + } else { + ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, + cryptsource, crypttarget); + } + } + if(ret == -1){ + return NULL; + } + return prompt; +} + +pid_t find_usplash(char **cmdline_r, size_t *cmdline_len_r){ + int ret = 0; + ssize_t sret = 0; + char *cmdline = NULL; + size_t cmdline_len = 0; + DIR *proc_dir = opendir("/proc"); + if(proc_dir == NULL){ + error(0, errno, "opendir"); + return -1; + } + errno = 0; + for(struct dirent *proc_ent = readdir(proc_dir); + proc_ent != NULL; + proc_ent = readdir(proc_dir)){ + pid_t pid; + { + intmax_t tmpmax; + char *tmp; + tmpmax = strtoimax(proc_ent->d_name, &tmp, 10); + if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0' + or tmpmax != (pid_t)tmpmax){ + /* Not a process */ + errno = 0; + continue; + } + pid = (pid_t)tmpmax; + } + /* Find the executable name by doing readlink() on the + /proc//exe link */ + char exe_target[sizeof(usplash_name)]; + { + /* create file name string */ + char *exe_link; + ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); + if(ret == -1){ + error(0, errno, "asprintf"); + goto fail_find_usplash; + } + + /* Check that it refers to a symlink owned by root:root */ + struct stat exe_stat; + ret = lstat(exe_link, &exe_stat); + if(ret == -1){ + if(errno == ENOENT){ + free(exe_link); + continue; + } + error(0, errno, "lstat"); + free(exe_link); + goto fail_find_usplash; + } + if(not S_ISLNK(exe_stat.st_mode) + or exe_stat.st_uid != 0 + or exe_stat.st_gid != 0){ + free(exe_link); + continue; + } + + sret = readlink(exe_link, exe_target, sizeof(exe_target)); + free(exe_link); + } + /* Compare executable name */ + if((sret != ((ssize_t)sizeof(exe_target)-1)) + or (memcmp(usplash_name, exe_target, + sizeof(exe_target)-1) != 0)){ + /* Not it */ + continue; + } + /* Found usplash */ + /* Read and save the command line of usplash in "cmdline" */ + { + /* Open /proc//cmdline */ + int cl_fd; + { + char *cmdline_filename; + ret = asprintf(&cmdline_filename, "/proc/%s/cmdline", + proc_ent->d_name); + if(ret == -1){ + error(0, errno, "asprintf"); + goto fail_find_usplash; + } + cl_fd = open(cmdline_filename, O_RDONLY); + free(cmdline_filename); + if(cl_fd == -1){ + error(0, errno, "open"); + goto fail_find_usplash; + } + } + size_t cmdline_allocated = 0; + char *tmp; + const size_t blocksize = 1024; + do { + /* Allocate more space? */ + if(cmdline_len + blocksize > cmdline_allocated){ + tmp = realloc(cmdline, cmdline_allocated + blocksize); + if(tmp == NULL){ + error(0, errno, "realloc"); + close(cl_fd); + goto fail_find_usplash; + } + cmdline = tmp; + cmdline_allocated += blocksize; + } + /* Read data */ + sret = read(cl_fd, cmdline + cmdline_len, + cmdline_allocated - cmdline_len); + if(sret == -1){ + error(0, errno, "read"); + close(cl_fd); + goto fail_find_usplash; + } + cmdline_len += (size_t)sret; + } while(sret != 0); + ret = close(cl_fd); + if(ret == -1){ + error(0, errno, "close"); + goto fail_find_usplash; + } + } + /* Close directory */ + ret = closedir(proc_dir); + if(ret == -1){ + error(0, errno, "closedir"); + goto fail_find_usplash; + } + /* Success */ + *cmdline_r = cmdline; + *cmdline_len_r = cmdline_len; + return pid; + } + + fail_find_usplash: + + free(cmdline); + if(proc_dir != NULL){ + int e = errno; + closedir(proc_dir); + errno = e; + } + return 0; +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + int ret = 0; + ssize_t sret; + int fifo_fd = -1; + int outfifo_fd = -1; + char *buf = NULL; + size_t buf_len = 0; + pid_t usplash_pid = -1; + bool usplash_accessed = false; + int status = EXIT_FAILURE; /* Default failure exit status */ + + char *prompt = makeprompt(); + if(prompt == NULL){ + status = EX_OSERR; + goto failure; + } + + /* Find usplash process */ + char *cmdline = NULL; + size_t cmdline_len = 0; + usplash_pid = find_usplash(&cmdline, &cmdline_len); + if(usplash_pid == 0){ + status = EX_UNAVAILABLE; + goto failure; + } + + /* Set up the signal handler */ + { + struct sigaction old_action, + new_action = { .sa_handler = termination_handler, + .sa_flags = 0 }; + sigemptyset(&new_action.sa_mask); + ret = sigaddset(&new_action.sa_mask, SIGINT); + if(ret == -1){ + error(0, errno, "sigaddset"); + status = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + error(0, errno, "sigaddset"); + status = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + error(0, errno, "sigaddset"); + status = EX_OSERR; + goto failure; + } + ret = sigaction(SIGINT, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + } + ret = sigaction(SIGHUP, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + } + ret = sigaction(SIGTERM, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + } + } + + usplash_accessed = true; + /* Write command to FIFO */ + if(not usplash_write(&fifo_fd, "TIMEOUT", "0")){ + if(errno != EINTR){ + error(0, errno, "usplash_write"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + if(not usplash_write(&fifo_fd, "INPUTQUIET", prompt)){ + if(errno != EINTR){ + error(0, errno, "usplash_write"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + free(prompt); + prompt = NULL; + + /* Read reply from usplash */ + /* Open FIFO */ + outfifo_fd = open("/dev/.initramfs/usplash_outfifo", O_RDONLY); + if(outfifo_fd == -1){ + if(errno != EINTR){ + error(0, errno, "open"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + /* Read from FIFO */ + size_t buf_allocated = 0; + const size_t blocksize = 1024; + do { + /* Allocate more space */ + if(buf_len + blocksize > buf_allocated){ + char *tmp = realloc(buf, buf_allocated + blocksize); + if(tmp == NULL){ + if(errno != EINTR){ + error(0, errno, "realloc"); + status = EX_OSERR; + } + goto failure; + } + buf = tmp; + buf_allocated += blocksize; + } + sret = read(outfifo_fd, buf + buf_len, + buf_allocated - buf_len); + if(sret == -1){ + if(errno != EINTR){ + error(0, errno, "read"); + status = EX_OSERR; + } + TEMP_FAILURE_RETRY(close(outfifo_fd)); + goto failure; + } + if(interrupted_by_signal){ + break; + } + + buf_len += (size_t)sret; + } while(sret != 0); + ret = close(outfifo_fd); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "close"); + status = EX_OSERR; + } + goto failure; + } + outfifo_fd = -1; + + if(interrupted_by_signal){ + goto failure; + } + + if(not usplash_write(&fifo_fd, "TIMEOUT", "15")){ + if(errno != EINTR){ + error(0, errno, "usplash_write"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + ret = close(fifo_fd); + if(ret == -1){ + if(errno != EINTR){ + error(0, errno, "close"); + status = EX_OSERR; + } + goto failure; + } + fifo_fd = -1; + + /* Print password to stdout */ + size_t written = 0; + while(written < buf_len){ + do { + sret = write(STDOUT_FILENO, buf + written, buf_len - written); + if(sret == -1){ + if(errno != EINTR){ + error(0, errno, "write"); + status = EX_OSERR; + } + goto failure; + } + } while(sret == -1); + + if(interrupted_by_signal){ + goto failure; + } + written += (size_t)sret; + } + free(buf); + buf = NULL; + + if(interrupted_by_signal){ + goto failure; + } + + free(cmdline); + return EXIT_SUCCESS; + + failure: + + free(buf); + + free(prompt); + + /* If usplash was never accessed, we can stop now */ + if(not usplash_accessed){ + return status; + } + + /* Close FIFO */ + if(fifo_fd != -1){ + ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); + if(ret == -1 and errno != EINTR){ + error(0, errno, "close"); + } + fifo_fd = -1; + } + + /* Close output FIFO */ + if(outfifo_fd != -1){ + ret = (int)TEMP_FAILURE_RETRY(close(outfifo_fd)); + if(ret == -1){ + error(0, errno, "close"); + } + } + + /* Create argv for new usplash*/ + char **cmdline_argv = malloc((argz_count(cmdline, cmdline_len) + 1) + * sizeof(char *)); /* Count args */ + if(cmdline_argv == NULL){ + error(0, errno, "malloc"); + return status; + } + argz_extract(cmdline, cmdline_len, cmdline_argv); /* Create argv */ + + /* Kill old usplash */ + kill(usplash_pid, SIGTERM); + sleep(2); + while(kill(usplash_pid, 0) == 0){ + kill(usplash_pid, SIGKILL); + sleep(1); + } + + pid_t new_usplash_pid = fork(); + if(new_usplash_pid == 0){ + /* Child; will become new usplash process */ + + /* Make the effective user ID (root) the only user ID instead of + the real user ID (_mandos) */ + ret = setuid(geteuid()); + if(ret == -1){ + error(0, errno, "setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + error(0, errno, "chdir"); + _exit(EX_OSERR); + } +/* if(fork() != 0){ */ +/* _exit(EXIT_SUCCESS); */ +/* } */ + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ + if(ret == -1){ + error(0, errno, "dup2"); + _exit(EX_OSERR); + } + + execv(usplash_name, cmdline_argv); + if(not interrupted_by_signal){ + error(0, errno, "execv"); + } + free(cmdline); + free(cmdline_argv); + _exit(EX_OSERR); + } + free(cmdline); + free(cmdline_argv); + sleep(2); + if(not usplash_write(&fifo_fd, "PULSATE", NULL)){ + if(errno != EINTR){ + error(0, errno, "usplash_write"); + } + } + + /* Close FIFO (again) */ + if(fifo_fd != -1){ + ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); + if(ret == -1 and errno != EINTR){ + error(0, errno, "close"); + } + fifo_fd = -1; + } + + if(interrupted_by_signal){ + struct sigaction signal_action = { .sa_handler = SIG_DFL }; + sigemptyset(&signal_action.sa_mask); + ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, + &signal_action, NULL)); + if(ret == -1){ + error(0, errno, "sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + error(0, errno, "raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return status; +} === added file 'plugins.d/usplash.xml' --- plugins.d/usplash.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/usplash.xml 2009-01-04 21:54:55 +0000 @@ -0,0 +1,297 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2008 + 2009 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to use usplash to get a + password. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program prompts for a password using + usplash8 + and outputs any given password to standard + output. If no usplash8 + process can be found, this program will immediately exit with an + exit code indicating failure. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + If this program is killed (presumably by + plugin-runner + 8mandos because some other + plugin provided the password), it cannot tell + usplash8 + to abort requesting a password, because + usplash + 8 does not support this. + Therefore, this program will then kill the + running usplash + 8 process and start a + new one using the same command line + arguments as the old one was using. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + ENVIRONMENT + + + cryptsource + crypttarget + + + If set, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which will + normally have inherited them from + /scripts/local-top/cryptroot in the + initial RAM disk environment, which will + have set them from parsing kernel arguments and + /conf/conf.d/cryptroot (also in the + initial RAM disk environment), which in turn will have been + created when the initial RAM disk image was created by + /usr/share/initramfs-tools/hooks/cryptroot, by + extracting the information of the root file system from + /etc/crypttab. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter. + + + + + + + + FILES + + + /dev/.initramfs/usplash_fifo + + + This is the FIFO to where this program + will write the commands for usplash8 + . See fifo7 + . + + + + + /dev/.initramfs/usplash_outfifo + + + This is the FIFO where this program + will read the password from usplash8 + . See fifo7 + . + + + + + /proc + + + To find the running usplash8 + , this directory will be searched for + numeric entries which will be assumed to be directories. + In all those directories, the exe and + cmdline entries will be used to + determine the name of the running binary, effective user + and group ID, and the command line + arguments. See proc5 + . + + + + + /sbin/usplash + + + This is the name of the binary which will be searched for + in the process list. See usplash8 + . + + + + + + + + BUGS + + Killing usplash + 8 and starting a new one + is ugly, but necessary as long as it does not support aborting a + password request. + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + If this program is killed by a signal, it will kill the process + ID which at the start of this program was + determined to run usplash8 + as root (see also ). There is a very + slight risk that, in the time between those events, that process + ID was freed and then taken up by another + process; the wrong process would then be killed. Now, this + program can only be killed by the user who started it; see + plugin-runner + 8mandos. This program + should therefore be started by a completely separate + non-privileged user, and no other programs should be allowed to + run as that special user. This means that it is not recommended + to use the user "nobody" to start this program, as other + possibly less trusted programs could be running as "nobody", and + they would then be able to kill this program, triggering the + killing of the process ID which may or may not + be usplash + 8. + + + The only other thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + crypttab + 5, + fifo + 7, + plugin-runner + 8mandos, + proc + 5, + usplash + 8 + + +
+ + + + +