=== 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 '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-25 06:44:13 +0000 +++ Makefile 2009-05-23 05:59:52 +0000 @@ -1,38 +1,55 @@ 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 +FORTIFY=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC -fPIE +LINK_FORTIFY_LD=-z relro -fPIE +LINK_FORTIFY=-pie #COVERAGE=--coverage OPTIMIZE=-Os LANGUAGE=-std=gnu99 -# PREFIX=/usr/local +htmldir=man +version=1.0.11 +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=/usr/local/lib/mandos CONFDIR=$(DESTDIR)/etc/mandos -# MANDIR=/usr/local/man -MANDIR=$(DESTDIR)/usr/share/man +KEYDIR=$(DESTDIR)/etc/keys/mandos +MANDIR=$(PREFIX)/share/man +INITRAMFSTOOLS=$(DESTDIR)/usr/share/initramfs-tools +## -GNUTLS_CFLAGS=$(shell libgnutls-config --cflags) -GNUTLS_LIBS=$(shell libgnutls-config --libs) +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 +# Commands to format a DocBook document into a manual page DOCBOOKTOMAN=cd $(dir $<); xsltproc --nonet --xinclude \ --param man.charmap.use.subset 0 \ --param make.year.ranges 1 \ @@ -42,94 +59,194 @@ /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,\\n,\\en,g' - -PLUGINS=plugins.d/password-prompt plugins.d/password-request -PROGS=plugin-runner $(PLUGINS) +# 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=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 +CPROGS=plugin-runner $(PLUGINS) +PROGS=mandos mandos-keygen mandos-ctl $(CPROGS) DOCS=mandos.8 plugin-runner.8mandos mandos-keygen.8 \ - plugins.d/password-request.8mandos \ + plugins.d/mandos-client.8mandos \ plugins.d/password-prompt.8mandos mandos.conf.5 \ - mandos-clients.conf.5 - -objects=$(addsuffix .o,$(PROGS)) - -all: $(PROGS) + plugins.d/usplash.8mandos plugins.d/splashy.8mandos \ + plugins.d/askpass-fifo.8mandos mandos-clients.conf.5 + +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 +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.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 + $(SED) --in-place \ + --expression='s/^\($$/\1$(version)">/' \ + $@ + +mandos: Makefile + $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@ + +mandos-keygen: Makefile + $(SED) --in-place \ + --expression='s/^\(VERSION="\)[^"]*"$$/\1$(version)"/' \ + $@ + +mandos-ctl: Makefile + $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@ + +mandos.lsm: Makefile + $(SED) --in-place \ + --expression='s/^\(Version:\).*/\1\t$(version)/' \ + $@ + $(SED) --in-place \ + --expression='s/^\(Entered-date:\).*/\1\t$(shell date --rfc-3339=date --reference=Makefile)/' \ + $@ + $(SED) --in-place \ + --expression='s/\(mandos_\)[0-9.]\+\(\.orig\.tar\.gz\)/\1$(version)\2/' \ + $@ + +plugins.d/mandos-client: plugins.d/mandos-client.o $(LINK.o) $(GNUTLS_LIBS) $(AVAHI_LIBS) $(GPGME_LIBS) \ $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ -.PHONY : all doc clean distclean run-client run-server install \ +.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 confdir -check: +check: all ./mandos --check -# Run the server with a local key -run-client: all keydir/seckey.txt keydir/pubkey.txt \ - keydir/secring.gpg keydir/pubring.gpg +# Run the client with a local config and key +run-client: all keydir/seckey.txt keydir/pubkey.txt ./plugin-runner --plugin-dir=plugins.d \ - --options-for=password-request:--keydir=keydir + --config-file=plugin-runner.conf \ + --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt \ + $(CLIENTARGS) # Used by run-client -keydir/secring.gpg: keydir/seckey.txt - gpg --homedir $(dir $<) --import $^ -keydir/pubring.gpg: keydir/pubkey.txt - gpg --homedir $(dir $<) --import $^ 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 - ./mandos --debug --configdir=confdir + ./mandos --debug --no-dbus --configdir=confdir $(SERVERARGS) # Used by run-server confdir/mandos.conf: mandos.conf install --directory confdir - install $^ $@ + install --mode=u=rw,go=r $^ $@ confdir/clients.conf: clients.conf keydir/seckey.txt install --directory confdir - install clients.conf $@ + install --mode=u=rw $< $@ # Add a client password ./mandos-keygen --dir keydir --password >> $@ -install: install-server install-client +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 - install --directory --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=rw,go=r --target-directory=$(CONFDIR) \ + mandos.conf + install --mode=u=rw --target-directory=$(CONFDIR) \ clients.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.conf.5 \ @@ -137,72 +254,108 @@ gzip --best --to-stdout mandos-clients.conf.5 \ > $(MANDIR)/man5/mandos-clients.conf.5.gz -install-client: all doc /usr/share/initramfs-tools/hooks/. - install --directory --parents $(PREFIX)/lib/mandos \ - $(CONFDIR) $(MANDIR)/man8 - install --directory --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 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/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/mandos-client.8mandos \ + > $(MANDIR)/man8/mandos-client.8mandos.gz + 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 + +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 \ $(MANDIR)/man8/mandos.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 \ + $(INITRAMFSTOOLS)/hooks/mandos \ + $(INITRAMFSTOOLS)/conf-hooks.d/mandos \ + $(INITRAMFSTOOLS)/scripts/init-premount/mandos \ $(MANDIR)/man8/plugin-runner.8mandos.gz \ $(MANDIR)/man8/mandos-keygen.8.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/mandos-client.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/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 2009-05-23 05:59:52 +0000 @@ -0,0 +1,102 @@ +This NEWS file records noteworthy changes, very tersely. +See the manual for detailed information. + +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 2009-02-23 11:52:42 +0000 @@ -0,0 +1,180 @@ +-*- 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 reboots 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. + + Three additional plugins are provided: + * 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,2009 Teddy Hogeborn + Copyright © 2008,2009 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-25 07:52:35 +0000 +++ TODO 2009-09-17 11:42:12 +0000 @@ -1,144 +1,90 @@ -*- org -*- -* [#A] README file +* mandos-client +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#B] Prefix all debug output with argv[0] +** 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(). + +* splashy +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] + +* usplash +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] + +* askpass-fifo +** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] +** TODO [#B] Drop privileges after opening FIFO. + +* password-prompt +** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] * 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 -*** ENVIRONMENT - Environment is modified according to options and passed to plugins -*** EXAMPLE - 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) * mandos (server) -** [#A] /etc/init.d/mandos-server :teddy: -** [#B] Log level :bugs: -** /etc/mandos/clients.d/*.conf +** TODO [#B] Log level :BUGS: +** TODO /etc/mandos/clients.d/*.conf Watch this directory and add/remove/update clients? -** config for TXT record -** [#B] Run-time communication with server :bugs: +** TODO config for TXT record +** TODO [#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: +*** Client class +*** Main server + + SetLogLevel + syslogger.setLevel(logging.WARNING) + + [[http://log.ometer.com/2007-05.html][Best D-Bus practices]] +** 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 -**** "--passfile" option - Using the "secfile" option instead of "secret" -**** [#A] "--test" option - For testing decryption before rebooting. -** Server-side -*** [#A] Create mandos user and group for server -*** [#A] Create /var/run/mandos directory with perm and ownership - -* [#A] Package +** TODO DBusServiceObjectUsingSuper +** Global enable/disable flag +** By-client countdown on secrets given +** 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 + +* mandos.xml +** [[file:mandos.xml::XXX][Document D-Bus interface]] + +* Provide and install /etc/dbus-1/system.d/mandos.conf + +* mandos-ctl +*** Handle "no D-Bus server" and/or "no Mandos server found" better +*** [#B] --dump option + +* mandos-monitor +** D-Bus mail loop w/ signal receiver +** Snack/Newt client data displayer +*** Client Widgets +*** Properties popup + +* 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 +** 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]] #+STARTUP: showall === modified file 'clients.conf' --- clients.conf 2008-08-22 00:16:20 +0000 +++ clients.conf 2009-01-08 03:54:06 +0000 @@ -14,7 +14,7 @@ ;interval = 5m # What command to run as "the checker". -;checker = fping -q -- %(host)s +;checker = fping -q -- %%(host)s ;#### @@ -43,7 +43,6 @@ ; 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm ; 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O ; QlnHIvPzEArRQLo= -; =iHhv ; ;# Host name; used only by the checker, not used by the server itself. ;host = foo.example.org @@ -56,7 +55,7 @@ ;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 === added file 'common.ent' --- common.ent 1970-01-01 00:00:00 +0000 +++ common.ent 2009-05-23 05:59:52 +0000 @@ -0,0 +1,3 @@ + + + === added directory 'debian' === added file 'debian/changelog' --- debian/changelog 1970-01-01 00:00:00 +0000 +++ debian/changelog 2009-05-23 05:59:52 +0000 @@ -0,0 +1,124 @@ +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 2009-09-17 01:21:27 +0000 @@ -0,0 +1,53 @@ +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, gnupg (< 2), + adduser +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 +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 2009-01-04 21:54:55 +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,2009 Teddy Hogeborn +Copyright: Copyright © 2008,2009 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 2009-09-08 06:28:20 +0000 @@ -0,0 +1,87 @@ +* Choose the Client Network Interface + + You MUST make sure that the correct network interface is specified + in the DEVICE setting in the "/etc/initramfs-tools/initramfs.conf" + file. *If* this 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 , Tue, 8 Sep 2009 08:25:58 +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 2008-09-17 00:34:09 +0000 @@ -0,0 +1,5 @@ +usr/share/man/man5 +usr/share/man/man8 +etc/init.d +etc/default +usr/sbin === added file 'debian/mandos.docs' --- debian/mandos.docs 1970-01-01 00:00:00 +0000 +++ debian/mandos.docs 2008-10-18 11:17:22 +0000 @@ -0,0 +1,3 @@ +NEWS +README +TODO === 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 2009-01-18 00:18:50 +0000 @@ -0,0 +1,92 @@ +#!/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 + +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 2009-01-15 02:52:02 +0000 @@ -0,0 +1,2 @@ +version=3 +ftp://ftp.fukt.bsnet.se/pub/mandos/mandos[-_]([^\s]+?)(?:\.orig)?\.tar\.(?:gz|bz2|7z) === 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-24 23:18:18 +0000 +++ initramfs-tools-hook 2009-09-07 23:48:17 +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 +# Config 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 --armor \ - --no-default-keyring --no-options --enable-dsa2 \ - --homedir "${DESTDIR}${CONFDIR}" --no-permission-warning \ - --trust-model always --import-options import-minimal \ - --import "${DESTDIR}${CONFDIR}/seckey.txt" -chown nobody "${DESTDIR}${CONFDIR}/secring.gpg" + +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-24 10:52:46 +0000 +++ mandos 2009-08-30 03:10:29 +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,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 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,62 @@ 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 +from contextlib import closing +import struct +import fcntl +import functools 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 + +try: + SO_BINDTODEVICE = socket.SO_BINDTODEVICE +except AttributeError: + try: + from IN import SO_BINDTODEVICE + except ImportError: + SO_BINDTODEVICE = None + + +version = "1.0.11" + +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 +110,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 +124,265 @@ 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 = self.server.GetAlternativeServiceName(self.name) + logger.info(u"Changing Zeroconf service name to %r ...", + unicode(self.name)) + syslogger.setFormatter(logging.Formatter + (u'Mandos (%s) [%%(process)d]:' + u' %%(levelname)s: %%(message)s' + % self.name)) self.remove() self.add() 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 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""" + 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 + name: string; from the config file, used in log messages and + D-Bus identifiers 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. + secret: bytestring; sent verbatim (over TLS) to client + host: string; available for use by the checker command + created: datetime.datetime(); (UTC) object creation + last_enabled: datetime.datetime(); (UTC) + enabled: bool() + last_checked_ok: datetime.datetime(); (UTC) or None + timeout: datetime.timedelta(); How long from last_checked_ok + until this client is invalid + interval: datetime.timedelta(); How often to start a new checker + disable_hook: If set, called by disable() as disable_hook(self) + checker: subprocess.Popen(); a running checker process used + 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: - '' - + disable_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: - '' - + current_checker_command: string; current running checker_command """ - 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={}): + + @staticmethod + def _datetime_to_milliseconds(dt): + "Convert a datetime.datetime() to milliseconds" + return ((dt.days * 24 * 60 * 60 * 1000) + + (dt.seconds * 1000) + + (dt.microseconds // 1000)) + + def timeout_milliseconds(self): + "Return the 'timeout' attribute in milliseconds" + return self._datetime_to_milliseconds(self.timeout) + + def interval_milliseconds(self): + "Return the 'interval' attribute in milliseconds" + return self._datetime_to_milliseconds(self.interval) + + 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 closing(open(os.path.expanduser + (os.path.expandvars + (config[u"secfile"])))) 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_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 + + def enable(self): """Start this client's checker and timeout hooks""" + if getattr(self, u"enabled", False): + # Already enabled + return + 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)) # 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: + # Schedule a disable() when 'timeout' has passed + self.disable_initiator_tag = (gobject.timeout_add + (self.timeout_milliseconds(), + self.disable)) + self.enabled = True + + def disable(self): + """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): + 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 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 @@ -319,21 +393,35 @@ # 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. + + # If a checker exists, make sure it is not a zombie + if self.checker is not None: + pid, status = os.waitpid(self.checker.pid, os.WNOHANG) + 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))) + escaped_attrs = dict((key, + re.escape(unicode(str(val), + errors= + u'replace'))) for key, val in vars(self).iteritems()) 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) @@ -343,21 +431,29 @@ # 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) + 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: @@ -369,219 +465,645 @@ 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() + if not getattr(self, u"enabled", False): + return False + now = datetime.datetime.utcnow() if self.last_checked_ok is None: return now < (self.created + self.timeout) else: return now < (self.last_checked_ok + self.timeout) -def 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) - # Verify the self signature in the key - crtverify = ctypes.c_uint(); - gnutls.library.functions.gnutls_openpgp_crt_verify_self\ - (crt, ctypes.c_uint(0), ctypes.byref(crtverify)) - if crtverify.value != 0: - gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) - raise gnutls.errors.CertificateSecurityError("Verify failed") - # 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. +class ClientDBus(Client, dbus.service.Object): + """A Client class using D-Bus + + Attributes: + dbus_object_path: dbus.ObjectPath + bus: dbus.SystemBus() + """ + # dbus.service.Object doesn't use super(), so we can't either. + + def __init__(self, bus = None, *args, **kwargs): + self.bus = bus + Client.__init__(self, *args, **kwargs) + # Only now, when this client is initialized, can it show up on + # the D-Bus + self.dbus_object_path = (dbus.ObjectPath + (u"/clients/" + + self.name.replace(u".", u"_"))) + dbus.service.Object.__init__(self, self.bus, + self.dbus_object_path) + + @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"last_enabled"), + self._datetime_to_dbus(self.last_enabled, + variant_level=1)) + return r + + def disable(self, signal = True): + oldstate = getattr(self, u"enabled", False) + r = Client.disable(self) + if signal 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(dbus.service.Object, u"__del__"): + dbus.service.Object.__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"checker_running"), + 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"last_checked_ok"), + (self._datetime_to_dbus(self.last_checked_ok, + 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"checker_running"), + 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"checker_running"), + dbus.Boolean(False, variant_level=1)) + return r + + ## D-Bus methods & signals + _interface = u"se.bsnet.fukt.Mandos.Client" + + # CheckedOK - method + @dbus.service.method(_interface) + def CheckedOK(self): + return self.checked_ok() + + # 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 + + # GetAllProperties - method + @dbus.service.method(_interface, out_signature=u"a{sv}") + def GetAllProperties(self): + "D-Bus method" + return dbus.Dictionary({ + dbus.String(u"name"): + dbus.String(self.name, variant_level=1), + dbus.String(u"fingerprint"): + dbus.String(self.fingerprint, variant_level=1), + dbus.String(u"host"): + dbus.String(self.host, variant_level=1), + dbus.String(u"created"): + self._datetime_to_dbus(self.created, + variant_level=1), + dbus.String(u"last_enabled"): + (self._datetime_to_dbus(self.last_enabled, + variant_level=1) + if self.last_enabled is not None + else dbus.Boolean(False, variant_level=1)), + dbus.String(u"enabled"): + dbus.Boolean(self.enabled, variant_level=1), + dbus.String(u"last_checked_ok"): + (self._datetime_to_dbus(self.last_checked_ok, + variant_level=1) + if self.last_checked_ok is not None + else dbus.Boolean (False, variant_level=1)), + dbus.String(u"timeout"): + dbus.UInt64(self.timeout_milliseconds(), + variant_level=1), + dbus.String(u"interval"): + dbus.UInt64(self.interval_milliseconds(), + variant_level=1), + dbus.String(u"checker"): + dbus.String(self.checker_command, + variant_level=1), + dbus.String(u"checker_running"): + dbus.Boolean(self.checker is not None, + variant_level=1), + dbus.String(u"object_path"): + dbus.ObjectPath(self.dbus_object_path, + variant_level=1) + }, signature=u"sv") + + # IsStillValid - method + @dbus.service.method(_interface, out_signature=u"b") + def IsStillValid(self): + return self.still_valid() + + # PropertyChanged - signal + @dbus.service.signal(_interface, signature=u"sv") + def PropertyChanged(self, property, value): + "D-Bus signal" + pass + + # ReceivedSecret - signal + @dbus.service.signal(_interface) + def ReceivedSecret(self): + "D-Bus signal" + pass + + # Rejected - signal + @dbus.service.signal(_interface) + def Rejected(self): + "D-Bus signal" + pass + + # SetChecker - method + @dbus.service.method(_interface, in_signature=u"s") + def SetChecker(self, checker): + "D-Bus setter method" + self.checker_command = checker + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"checker"), + dbus.String(self.checker_command, + variant_level=1)) + + # SetHost - method + @dbus.service.method(_interface, in_signature=u"s") + def SetHost(self, host): + "D-Bus setter method" + self.host = host + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"host"), + dbus.String(self.host, variant_level=1)) + + # SetInterval - method + @dbus.service.method(_interface, in_signature=u"t") + def SetInterval(self, milliseconds): + self.interval = datetime.timedelta(0, 0, 0, milliseconds) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"interval"), + (dbus.UInt64(self.interval_milliseconds(), + variant_level=1))) + + # SetSecret - method + @dbus.service.method(_interface, in_signature=u"ay", + byte_arrays=True) + def SetSecret(self, secret): + "D-Bus setter method" + self.secret = str(secret) + + # SetTimeout - method + @dbus.service.method(_interface, in_signature=u"t") + def SetTimeout(self, milliseconds): + self.timeout = datetime.timedelta(0, 0, 0, milliseconds) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"timeout"), + (dbus.UInt64(self.timeout_milliseconds(), + variant_level=1))) + + # 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() + + del _interface + + +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. + unicode(self.client_address)) + logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1]) + # Open IPC pipe to parent process + with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc: + 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 = 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)) + + 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") + try: + fpr = self.fingerprint(self.peer_certificate(session)) + except (TypeError, gnutls.errors.GNUTLSError), error: + logger.warning(u"Bad certificate: %s", error) + session.bye() + return + logger.debug(u"Fingerprint: %s", fpr) + + for c in self.server.clients: + if c.fingerprint == fpr: + client = c + break + else: + ipc.write(u"NOTFOUND %s %s\n" + % (fpr, unicode(self.client_address))) + session.bye() + return + # Have to check if client.still_valid(), since it is + # possible that the client timed out while establishing + # the GnuTLS session. + if not client.still_valid(): + ipc.write(u"INVALID %s\n" % client.name) + session.bye() + return + ipc.write(u"SENDING %s\n" % client.name) + sent_size = 0 + while sent_size < len(client.secret): + sent = session.send(client.secret[sent_size:]) + logger.debug(u"Sent: %d, remaining: %d", + sent, len(client.secret) + - (sent_size + sent)) + sent_size += sent + session.bye() + + @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 ForkingMixInWithPipe(socketserver.ForkingMixIn, object): + """Like socketserver.ForkingMixIn, but also pass a pipe.""" + def process_request(self, request, client_address): + """Overrides and wraps the original process_request(). + + This function creates a new pipe in self.pipe + """ + self.pipe = os.pipe() + super(ForkingMixInWithPipe, + self).process_request(request, client_address) + os.close(self.pipe[1]) # close write end + self.add_pipe(self.pipe[0]) + def add_pipe(self, pipe): + """Dummy function; override as necessary""" + os.close(pipe) + + +class IPv6_TCPServer(ForkingMixInWithPipe, + 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 + 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, pipe): + # Call "handle_ipc" for both data and EOF events + gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP, + self.handle_ipc) + def handle_ipc(self, source, condition, file_objects={}): + 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) + logger.debug(u"Handling IPC: FD = %d, condition = %s", source, + conditions_string) + + # Turn the pipe file descriptor into a Python file object + if source not in file_objects: + file_objects[source] = os.fdopen(source, u"r", 1) + + # Read a line from the file object + cmdline = file_objects[source].readline() + if not cmdline: # Empty line means end of file + # close the IPC pipe + file_objects[source].close() + del file_objects[source] + + # Stop calling this function + return False + + logger.debug(u"IPC command: %r", cmdline) + + # Parse and act on command + cmd, args = cmdline.rstrip(u"\r\n").split(None, 1) + + if cmd == u"NOTFOUND": + logger.warning(u"Client not found for fingerprint: %s", + args) + if self.use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientNotFound(args) + elif cmd == u"INVALID": + for client in self.clients: + if client.name == args: + logger.warning(u"Client %s is invalid", args) + if self.use_dbus: + # Emit D-Bus signal + client.Rejected() + break + else: + logger.error(u"Unknown client %s is invalid", args) + elif cmd == u"SENDING": + for client in self.clients: + if client.name == args: + logger.info(u"Sending secret to %s", client.name) + client.checked_ok() + if self.use_dbus: + # Emit D-Bus signal + client.ReceivedSecret() + break + else: + logger.error(u"Sending secret to unknown client %s", + args) + else: + logger.error(u"Unknown IPC command: %r", cmdline) + + # Keep calling this function + 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('5m 30s') + >>> string_to_delta(u'5m 30s') datetime.timedelta(0, 330) """ timevalue = datetime.timedelta(0) for s in interval.split(): try: - suffix=unicode(s[-1]) - value=int(s[:-1]) + suffix = unicode(s[-1]) + value = int(s[:-1]) if suffix == u"d": delta = datetime.timedelta(value) elif suffix == u"s": @@ -600,62 +1122,39 @@ return timevalue -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)) - 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 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: @@ -663,7 +1162,7 @@ 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"/dev/null not a character device") os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) os.dup2(null, sys.stderr.fileno()) @@ -672,30 +1171,36 @@ 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("--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 @@ -703,91 +1208,151 @@ 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", } # 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"): 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"] + use_dbus = server_settings[u"use_dbus"] + use_ipv6 = server_settings[u"use_ipv6"] if not debug: 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"])) + 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 = { "timeout": "1h", - "interval": "5m", - "checker": "fping -q -- %(host)s", - "host": "", + client_defaults = { u"timeout": u"1h", + u"interval": u"5m", + u"checker": u"fping -q -- %%(host)s", + u"host": u"", } - 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"]) + 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"], + use_ipv6=use_ipv6, + gnutls_priority= + server_settings[u"priority"], + use_dbus=use_dbus) + 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 + + # Enable all possible GnuTLS debugging + if debug: + # "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)) 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: + bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus) + 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"]))) + + client_class = Client + if use_dbus: + client_class = functools.partial(ClientDBus, bus = bus) + tcp_server.clients.update(set( + client_class(name = section, + config= dict(client_config.items(section))) + for section in client_config.sections())) + if not tcp_server.clients: + logger.warning(u"No clients defined") if debug: # Redirect stdin so all checkers get /dev/null @@ -801,30 +1366,27 @@ # 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() + with closing(pidfile): + pid = os.getpid() + pidfile.write(str(pid) + "\n") del pidfile - except IOError, err: - logger.error(u"Could not write %s file with PID %d", - pidfilename, os.getpid()) + 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 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() + client.disable_hook = None + client.disable() atexit.register(cleanup) @@ -833,26 +1395,87 @@ 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) + 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"oa{sv}") + def ClientAdded(self, objpath, properties): + "D-Bus signal" + pass + + @dbus.service.signal(_interface, signature=u"s") + def ClientNotFound(self, fingerprint): + "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.GetAllProperties()) + 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(signal=False) + # Emit D-Bus signal + self.ClientRemoved(object_path, c.name) + return + raise KeyError + + del _interface + + mandos_dbus_service = MandosDBusService() + + for client in tcp_server.clients: + if use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientAdded(client.dbus_object_path, + client.GetAllProperties()) + 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) sys.exit(1) @@ -860,18 +1483,19 @@ 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) sys.exit(1) except KeyboardInterrupt: if debug: - print + print >> sys.stderr + logger.debug(u"Server received KeyboardInterrupt") + logger.debug(u"Server exiting") if __name__ == '__main__': main() === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2008-08-25 07:52:35 +0000 +++ mandos-clients.conf.xml 2009-09-17 01:21:27 +0000 @@ -1,17 +1,20 @@ - + /etc/mandos/clients.conf"> + + +%common; ]> - + - &CONFNAME; + Mandos Manual - &CONFNAME; - &VERSION; + Mandos + &version; + &TIMESTAMP; Björn @@ -30,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 @@ -69,13 +51,11 @@ Configuration file for the Mandos server - + - - &CONFPATH; - + &CONFPATH; - + DESCRIPTION @@ -115,22 +95,24 @@ start time expansion, see . - Uknown options are ignored. The used options are as follows: + Unknown options are ignored. The used options are as follows: - + - + - timeout + - timeout = TIME - - - The timeout is how long the server will wait for a - successful checker run until a client is considered - invalid - that is, ineligible to get the data this server - holds. By default Mandos will use 1 hour. + + 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 considered invalid - that is, ineligible + to get the data this server holds. By default Mandos will + use 1 hour. The TIME is specified as a @@ -147,13 +129,14 @@ - + - interval + - interval = TIME - + + This option is optional. + How often to run the checker to confirm that a client is still up. Note: a new checker will @@ -168,14 +151,15 @@ as for timeout above. - - + + - checker + - checker = COMMAND - + + This option is optional. + This option allows you to override the default shell command that the server will use to check if the client is @@ -187,7 +171,7 @@ PATH will be searched. The default value for the checker command is fping %(host)s. + >-- %%(host)s. In addition to normal start time expansion, this option @@ -198,11 +182,12 @@ - fingerprint + - fingerprint = HEXSTRING - + + This option is required. + This option sets the OpenPGP fingerprint that identifies the public key that clients authenticate themselves with @@ -213,11 +198,14 @@ - secret + - secret = BASE64_ENCODED_DATA - + + 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 @@ -236,39 +224,42 @@ lines is that a line beginning with white space adds to the value of the previous line, RFC 822-style. - - If this option is not specified, the option is used instead, but one of them - must be present. - - - - - - secfile - - secfile = FILENAME - - - The same as , but 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. - - - This option is only used, and must be - present, if is not specified. - - - - - - host - - host = STRING - + + + + + + + + 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. + + + + + + + + + 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 @@ -278,7 +269,7 @@ - + EXPANSION @@ -327,11 +318,11 @@ 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 track down an error of this kind. + mode is needed to expose an error of this kind. - - + + FILES @@ -361,7 +352,7 @@ [DEFAULT] timeout = 1h interval = 5m -checker = fping -q -- %(host)s +checker = fping -q -- %%(host)s # Client "foo" [foo] @@ -390,21 +381,24 @@ fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 secfile = /etc/mandos/bar-secret timeout = 15m - - + SEE ALSO - - mandos - 8, - mandos-keygen - 8, - mandos.conf - 5 + mandos-keygen + 8, + mandos.conf + 5, + mandos + 8 + + + + + === added file 'mandos-ctl' --- mandos-ctl 1970-01-01 00:00:00 +0000 +++ mandos-ctl 2009-08-30 03:10:29 +0000 @@ -0,0 +1,193 @@ +#!/usr/bin/python +# -*- mode: python; coding: utf-8 -*- + +from __future__ import division +import sys +import dbus +from optparse import OptionParser +import locale +import datetime +import re + +locale.setlocale(locale.LC_ALL, u'') + +tablewords = { + 'name': u'Name', + 'enabled': u'Enabled', + 'timeout': u'Timeout', + 'last_checked_ok': u'Last Successful Check', + 'created': u'Created', + 'interval': u'Interval', + 'host': u'Host', + 'fingerprint': u'Fingerprint', + 'checker_running': u'Check Is Running', + 'last_enabled': u'Last Enabled', + 'checker': u'Checker', + } +defaultkeywords = ('name', 'enabled', 'timeout', 'last_checked_ok', + 'checker') +domain = 'se.bsnet.fukt' +busname = domain + '.Mandos' +server_path = '/' +server_interface = domain + '.Mandos' +client_interface = domain + '.Mandos.Client' +version = "1.0.11" +bus = dbus.SystemBus() +mandos_dbus_objc = bus.get_object(busname, server_path) +mandos_serv = dbus.Interface(mandos_dbus_objc, + dbus_interface = server_interface) +mandos_clients = mandos_serv.GetAllClientsWithProperties() + +def datetime_to_milliseconds(dt): + "Return the 'timeout' attribute in milliseconds" + return ((dt.days * 24 * 60 * 60 * 1000) + + (dt.seconds * 1000) + + (dt.microseconds // 1000)) + +def milliseconds_to_string(ms): + td = datetime.timedelta(0, 0, 0, ms) + return "%s%02d:%02d:%02d" % (("%dT" % td.days) if td.days else "", # days + td.seconds // 3600, # hours + (td.seconds % 3600) // 60, # minutes + (td.seconds % 60)) # seconds + + +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): + def valuetostring(value, keyword): + if type(value) is dbus.Boolean: + return u"Yes" if value else u"No" + if keyword in ("timeout", "interval"): + return milliseconds_to_string(value) + return unicode(value) + + 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 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) + +parser = OptionParser(version = "%%prog %s" % version) +parser.add_option("-a", "--all", 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-valid", action="store_true", + help="Check if client is still valid") +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("-H", "--host", type="string", + help="Set host for client") +parser.add_option("-s", "--secret", type="string", + help="Set password blob (file) for client") +options, client_names = parser.parse_args() + +# Compile list of clients to process +clients=[] +for name in client_names: + for path, client in mandos_clients.iteritems(): + if client['name'] == name: + client_objc = bus.get_object(busname, path) + clients.append(dbus.Interface(client_objc, + dbus_interface + = client_interface)) + break + else: + print >> sys.stderr, "Client not found on server: %r" % name + sys.exit(1) + +if not clients and mandos_clients.values(): + keywords = defaultkeywords + if options.all: + keywords = ('name', 'enabled', 'timeout', 'last_checked_ok', + 'created', 'interval', 'host', 'fingerprint', + 'checker_running', 'last_enabled', 'checker') + print_clients(mandos_clients.values()) + +# 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() + if options.disable: + client.Disable() + if options.bump_timeout: + client.BumpTimeout() + if options.start_checker: + client.StartChecker() + if options.stop_checker: + client.StopChecker() + if options.is_valid: + sys.exit(0 if client.IsStillValid() else 1) + if options.checker: + client.SetChecker(options.checker) + if options.host: + client.SetHost(options.host) + if options.interval: + client.SetInterval(datetime_to_milliseconds + (string_to_delta(options.interval))) + if options.timeout: + client.SetTimeout(datetime_to_milliseconds + (string_to_delta(options.timeout))) + if options.secret: + client.SetSecret(dbus.ByteArray(open(options.secret, 'rb').read())) + === modified file 'mandos-keygen' --- mandos-keygen 2008-08-25 03:53:42 +0000 +++ mandos-keygen 2009-05-23 05:59:52 +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,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 published by @@ -20,14 +21,14 @@ # Contact the authors at . # -VERSION="1.0" +VERSION="1.0.11" -KEYDIR="/etc/mandos" +KEYDIR="/etc/keys/mandos" KEYTYPE=DSA KEYLENGTH=2048 SUBKEYTYPE=ELG-E SUBKEYLENGTH=2048 -KEYNAME="`hostname --fqdn`" +KEYNAME="`hostname --fqdn 2>/dev/null || hostname`" KEYEMAIL="" KEYCOMMENT="Mandos client key" KEYEXPIRE=0 @@ -35,9 +36,13 @@ 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,password,dir:,type:,length:,subtype:,sublength:,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(){ @@ -49,6 +54,7 @@ $basename [ OPTIONS ] Encrypted password creation: $basename { -p | --password } [ --name NAME ] [ --dir DIR] + $basename { -F | --passfile } FILE [ --name NAME ] [ --dir DIR] Key creation options: -v, --version Show program's version number and exit @@ -64,18 +70,22 @@ -n NAME, --name NAME Name of key. Default is the FQDN. -e ADDRESS, --email ADDRESS Email address of key. Default is empty. - -c COMMENT, --comment COMMENT + -c TEXT, --comment TEXT Comment field for key. The default value is "Mandos client key". -x TIME, --expire TIME Key expire time. Default is no expiration. See gpg(1) for syntax. - -f, --force Force overwriting old keys. + -f, --force Force overwriting old key files. Password creation options: - -p, --password Create an encrypted password using the keys in - the key directory. All options other than - --keydir and --name are ignored. + -p, --password Create an encrypted password using the key in + the key directory. All options other than + --dir and --name are ignored. + -F FILE, --passfile FILE + Encrypt a password from FILE using the key in + the key directory. All options other than + --dir and --name are ignored. EOF } @@ -83,6 +93,7 @@ while :; do case "$1" in -p|--password) mode=password; shift;; + -F|--passfile) mode=password; PASSFILE="$2"; shift 2;; -d|--dir) KEYDIR="$2"; shift 2;; -t|--type) KEYTYPE="$2"; shift 2;; -s|--subtype) SUBKEYTYPE="$2"; shift 2;; @@ -108,21 +119,20 @@ PUBKEYFILE="$KEYDIR/pubkey.txt" # Check for some invalid values -if [ -d "$KEYDIR" ]; then :; else +if [ ! -d "$KEYDIR" ]; then echo "$KEYDIR not a directory" >&2 exit 1 fi -if [ -w "$KEYDIR" ]; then :; else - echo "Directory $KEYDIR not writeable" >&2 - exit 1 -fi - -if [ "$mode" = password -a -e "$KEYDIR/trustdb.gpg.lock" ]; then - echo "Key directory has locked trustdb; aborting." >&2 +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 @@ -137,7 +147,7 @@ echo "Invalid key length" >&2 exit 1 fi - + if [ -z "$KEYEXPIRE" ]; then echo "Empty key expiration" >&2 exit 1 @@ -149,8 +159,8 @@ [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) FORCE=0;; esac - if { [ -e "$SECKEYFILE" ] || [ -e "$PUBKEYFILE" ]; } \ - && [ "$FORCE" -eq 0 ]; then + if [ \( -e "$SECKEYFILE" -o -e "$PUBKEYFILE" \) \ + -a "$FORCE" -eq 0 ]; then echo "Refusing to overwrite old key files; use --force" >&2 exit 1 fi @@ -162,37 +172,30 @@ if [ -n "$KEYEMAIL" ]; then KEYEMAILLINE="Name-Email: $KEYEMAIL" fi - + # Create temporary gpg batch file - BATCHFILE="`mktemp -t mandos-gpg-batch.XXXXXXXXXX`" + BATCHFILE="`mktemp -t mandos-keygen-batch.XXXXXXXXXX`" fi if [ "$mode" = password ]; then # Create temporary encrypted password file - SECFILE="`mktemp -t mandos-gpg-secfile.XXXXXXXXXX`" -fi - -# Create temporary key rings -SECRING="`mktemp -t mandos-gpg-secring.XXXXXXXXXX`" -PUBRING="`mktemp -t mandos-gpg-pubring.XXXXXXXXXX`" - -if [ "$mode" = password ]; then - # If a trustdb.gpg file does not already exist, schedule it for - # deletion when we are done. - if ! [ -e "$KEYDIR/trustdb.gpg" ]; then - TRUSTDB="$KEYDIR/trustdb.gpg" - fi -fi + 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 " set +e; \ -rm --force $PUBRING ${PUBRING}~ $BATCHFILE $TRUSTDB; \ -shred --remove $SECRING $SECFILE; \ +test -n \"$SECFILE\" && shred --remove \"$SECFILE\"; \ +shred --remove \"$RINGDIR\"/sec*; +test -n \"$BATCHFILE\" && rm --force \"$BATCHFILE\"; \ +rm --recursive --force \"$RINGDIR\"; stty echo; \ " EXIT -umask 027 +umask 077 if [ "$mode" = keygen ]; then # Create batch file for GnuPG @@ -209,18 +212,17 @@ Expire-Date: $KEYEXPIRE #Preferences: #Handle: - %pubring $PUBRING - %secring $SECRING + #%pubring pubring.gpg + #%secring secring.gpg %commit EOF # Generate a new key in the key rings - gpg --no-random-seed-file --quiet --batch --no-tty \ - --no-default-keyring --no-options --enable-dsa2 \ - --secret-keyring "$SECRING" --keyring "$PUBRING" \ + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always \ --gen-key "$BATCHFILE" rm --force "$BATCHFILE" - + # Backup any old key files if cp --backup=numbered --force "$SECKEYFILE" "$SECKEYFILE" \ 2>/dev/null; then @@ -240,64 +242,68 @@ 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 --enable-dsa2 \ - --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 --enable-dsa2 \ - --secret-keyring "$SECRING" --keyring "$PUBRING" \ - --export-options export-minimal --comment "$FILECOMMENT" \ - --output "$PUBKEYFILE" --export + # 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 keys into temporary key rings - gpg --no-random-seed-file --quiet --batch --no-tty --armor \ - --no-default-keyring --no-options --enable-dsa2 \ - --homedir "$KEYDIR" --no-permission-warning \ - --secret-keyring "$SECRING" --keyring "$PUBRING" \ - --trust-model always --import "$SECKEYFILE" - gpg --no-random-seed-file --quiet --batch --no-tty --armor \ - --no-default-keyring --no-options --enable-dsa2 \ - --homedir "$KEYDIR" --no-permission-warning \ - --secret-keyring "$SECRING" --keyring "$PUBRING" \ - --trust-model always --import "$PUBKEYFILE" - + # 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 --no-random-seed-file --quiet --batch --no-tty \ - --armor --no-default-keyring --no-options --enable-dsa2 \ - --homedir \"$KEYDIR\" --no-permission-warning \ - --secret-keyring \"$SECRING\" --keyring \"$PUBRING\" \ - --trust-model always --fingerprint --with-colons \ - | sed -n -e '/^fpr:/{s/^fpr:.*:\\([0-9A-Z]*\\):\$/\\1/p;q}'`" + 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" - stty -echo - echo -n "Enter passphrase: " >&2 - sed -e '1q' \ - | gpg --no-random-seed-file --batch --no-tty --armor \ - --no-default-keyring --no-options --enable-dsa2 \ - --homedir "$KEYDIR" --no-permission-warning \ - --secret-keyring "$SECRING" --keyring "$PUBRING" \ - --trust-model always --encrypt --recipient "$FINGERPRINT" \ - --comment "$FILECOMMENT" \ + if [ -n "$PASSFILE" ]; then + cat "$PASSFILE" + else + stty -echo + echo -n "Enter passphrase: " >&2 + first="$(head --lines=1 | tr --delete '\n')" + echo -n -e "\nRepeat passphrase: " >&2 + second="$(head --lines=1 | tr --delete '\n')" + echo >&2 + stty echo + if [ "$first" != "$second" ]; then + echo -e "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" - echo >&2 - stty echo + if [ -e "$RINGDIR"/mismatch ]; then + rm --force "$RINGDIR"/mismatch + exit 1 + fi cat <<-EOF [$KEYNAME] host = $KEYNAME fingerprint = $FINGERPRINT secret = -EOF - sed -n -e ' + EOF + sed --quiet --expression=' /^-----BEGIN PGP MESSAGE-----$/,/^-----END PGP MESSAGE-----$/{ /^$/,${ # Remove 24-bit Radix-64 checksum @@ -316,9 +322,5 @@ shred --remove "$SECFILE" fi # Remove the key rings -shred --remove "$SECRING" -rm --force "$PUBRING" "${PUBRING}~" -# Remove the trustdb, if one did not exist when we started -if [ -n "$TRUSTDB" ]; then - rm --force "$TRUSTDB" -fi +shred --remove "$RINGDIR"/sec* +rm --recursive --force "$RINGDIR" === modified file 'mandos-keygen.xml' --- mandos-keygen.xml 2008-08-25 03:53:42 +0000 +++ mandos-keygen.xml 2009-01-04 21:54:55 +0000 @@ -1,16 +1,19 @@ + + +%common; ]> - &COMMANDNAME; + Mandos Manual - &COMMANDNAME; - &VERSION; + Mandos + &version; + &TIMESTAMP; Björn @@ -29,34 +32,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 - . - - + - + &COMMANDNAME; 8 @@ -65,174 +47,169 @@ &COMMANDNAME; - Generate keys for password-request - 8mandos + Generate key and password for Mandos client and server. - + &COMMANDNAME; - - - directory - - - - type - - - - bits - - - - type - - - - bits - - - - NAME - - - - EMAIL - - - - COMMENT - - - - TIME - - - - - - - &COMMANDNAME; - - - directory - - - - type - - - - bits - - - - type - - - - bits - - - - NAME - - - - EMAIL - - - - COMMENT - - - - TIME - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + - - - - - directory - - - - NAME + + + 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. - It can also be used to generate ready-made sections for + This program can also be used with the + or + options to generate a ready-made section for + clients.conf (see mandos-clients.conf - 5 using the - option. + 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. Default is @@ -240,30 +217,36 @@ - + - -t, --type - type + + Key type. Default is DSA. - + - -l, --length - bits + + Key length in bits. Default is 2048. - + - -s, --subtype - type + + Subkey type. Default is ELG-E (Elgamal @@ -271,30 +254,36 @@ - + - -L, --sublength - bits + + Subkey length in bits. Default is 2048. - + - -e, --email - address + + Email address of key. Default is empty. - + - -c, --comment - comment + + Comment field for key. The default value is @@ -302,10 +291,12 @@ - + - -x, --expire - time + + Key expire time. Default is no expiration. See @@ -314,18 +305,19 @@ - + - -f, --force + + - Force overwriting old keys. + Force overwriting old key. - -p, --password + + Prompt for a password and encrypt it with the key already @@ -337,27 +329,41 @@ >8. The host name or the name specified with the option is used for the section header. All other options are ignored, - and no keys are created. + and no key is created. + + + + + + + + + The same as , but read from + FILE, not the terminal. - + OVERVIEW This program is a small utility to generate new OpenPGP keys for - new Mandos clients. + 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. @@ -365,7 +371,7 @@ ENVIRONMENT - TMPDIR + TMPDIR If set, temporary files will be created here. See @@ -377,7 +383,7 @@ - + FILES Use the option to change where @@ -414,14 +420,13 @@ - - - BUGS - - None are known at this time. - - - + + + + + + + EXAMPLE @@ -429,48 +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. + 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, + gpg + 1, + mandos-clients.conf + 5, mandos 8, - gpg - 1 + mandos-client + 8mandos + + + + + === modified file 'mandos-options.xml' --- mandos-options.xml 2008-08-25 06:44:13 +0000 +++ mandos-options.xml 2009-02-13 05:38:21 +0000 @@ -5,6 +5,8 @@
@@ -15,8 +17,8 @@ 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 does not - exit, but will instead continue normally. + interface is not considered critical, and the server will not + exit, but instead continue normally. @@ -24,9 +26,12 @@ 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 be an IPv6 address; an - IPv4 address can only be specified using the ::FFFF:192.0.2.3 format. + 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.) @@ -42,24 +47,43 @@ - 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-25 06:44:13 +0000 +++ mandos.conf.xml 2009-02-25 01:14:29 +0000 @@ -1,17 +1,20 @@ - + /etc/mandos/mandos.conf"> + + +%common; ]> - &CONFNAME; + Mandos Manual - &CONFNAME; - &VERSION; + Mandos + &version; + &TIMESTAMP; Björn @@ -30,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 @@ -69,13 +51,11 @@ Configuration file for the Mandos server - + - - &CONFPATH; - + &CONFPATH; - + DESCRIPTION @@ -93,76 +73,86 @@ # or ; are ignored and may be used to provide comments. - + OPTIONS - interface + - interface = NAME - - + - address + - address = ADDRESS - - + - port + - port = NUMBER - - + - debug - - debug = { + >false | off } + - + - priority + - priority = STRING - - + - servicename + - servicename = NAME - + + + + + + + + + + + + + + @@ -178,7 +168,7 @@ The [DEFAULT] is necessary because the Python built-in module ConfigParser - requres it. + requires it. @@ -200,11 +190,13 @@ [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 @@ -212,11 +204,63 @@ SEE ALSO - - mandos - 8, - mandos-clients.conf - 5 + 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 2009-05-23 05:59:52 +0000 @@ -0,0 +1,22 @@ +Begin4 +Title: Mandos +Version: 1.0.11 +Entered-date: 2009-05-23 +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 + 99K mandos_1.0.11.orig.tar.gz +Alternate-site: ftp://ftp.fukt.bsnet.se/pub/mandos + 99K mandos_1.0.11.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-25 06:44:13 +0000 +++ mandos.xml 2009-09-17 01:21:27 +0000 @@ -1,16 +1,19 @@ + + +%common; ]> - - &COMMANDNAME; + + Mandos Manual - &COMMANDNAME; - &VERSION; + Mandos + &version; + &TIMESTAMP; Björn @@ -29,34 +32,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 - . - - + - + &COMMANDNAME; 8 @@ -65,48 +47,66 @@ &COMMANDNAME; - Sends encrypted passwords to authenticated Mandos clients + Gives encrypted passwords to authenticated Mandos clients - + &COMMANDNAME; - --interfaceNAME - --addressADDRESS - --portPORT - --priorityPRIORITY - --servicenameNAME - --configdirDIRECTORY - --debug - - - &COMMANDNAME; - -iNAME - -aADDRESS - -pPORT - --priorityPRIORITY - --servicenameNAME - --configdirDIRECTORY - --debug + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; - -h - --help + + &COMMANDNAME; - --version + &COMMANDNAME; - --check + - + DESCRIPTION @@ -121,60 +121,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 NAME + + NAME + + NAME - + - -a, --address - ADDRESS + + - + - -p, --port - PORT + + - + - --check + Run the server’s self-tests. This includes any unit @@ -182,34 +185,34 @@ - + - --debug + - + - --priority - PRIORITY + - + - --servicename NAME - + - + - --configdir DIR - + Directory to search for configuration files. Default is @@ -221,28 +224,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 +320,24 @@ - + 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. - + LOGGING @@ -324,7 +347,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. + + + + EXIT STATUS @@ -332,12 +365,12 @@ critical error is encountered. - + ENVIRONMENT - PATH + PATH To start the configured checker (see - - + + FILES Use the option to change where @@ -382,7 +415,7 @@ - /var/run/mandos/mandos.pid + /var/run/mandos.pid The file containing the process id of @@ -423,7 +456,7 @@ 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 @@ -437,7 +470,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 +485,7 @@ Normal invocation needs no options: - mandos + &COMMANDNAME; @@ -461,7 +498,7 @@ -mandos --debug --configdir ~/mandos --servicename Test +&COMMANDNAME; --debug --configdir ~/mandos --servicename Test @@ -473,24 +510,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 +540,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 @@ -520,29 +557,29 @@ 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 + 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-clients.conf - 5, - password-request + mandos-client 8mandos, sh1 @@ -572,8 +609,8 @@ - GnuTLS + GnuTLS @@ -585,23 +622,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 @@ -611,8 +665,7 @@ - RFC 4880: OpenPGP Message - Format + RFC 4880: OpenPGP Message Format @@ -622,8 +675,8 @@ - RFC 5081: Using OpenPGP Keys for - Transport Layer Security + RFC 5081: Using OpenPGP Keys for Transport Layer + Security @@ -635,3 +688,8 @@ + + + + + === modified file 'overview.xml' --- overview.xml 2008-08-23 07:17:28 +0000 +++ overview.xml 2008-09-13 15:36:18 +0000 @@ -1,15 +1,17 @@ - This is part of the Mandos system for allowing computers to have - encrypted root file systems and also be capable of remote and - unattended reboots. The 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 computers can - continue booting normally. + 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-24 23:33:02 +0000 +++ plugin-runner.c 2009-09-10 06:28:14 +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 @@ -27,8 +28,8 @@ #include /* malloc(), exit(), EXIT_FAILURE, EXIT_SUCCESS, realloc() */ #include /* bool, true, false */ -#include /* perror, popen(), fileno(), - fprintf(), stderr, STDOUT_FILENO */ +#include /* perror, fileno(), fprintf(), + stderr, STDOUT_FILENO */ #include /* DIR, opendir(), stat(), struct stat, waitpid(), WIFEXITED(), WEXITSTATUS(), wait(), pid_t, @@ -37,7 +38,8 @@ #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(), @@ -46,12 +48,13 @@ 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() */ #include /* errno */ #include /* struct argp_option, struct argp_state, struct argp, @@ -61,29 +64,19 @@ #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() */ #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; - volatile bool completed; - volatile int status; - struct process *next; -} process; - typedef struct plugin{ char *name; /* can be NULL or any plugin name */ char **argv; @@ -91,45 +84,69 @@ 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 exiting 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){ + 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){ - copy_name = strdup(name); + do { + copy_name = strdup(name); + } while(copy_name == NULL and errno == EINTR); if(copy_name == NULL){ + free(new_plugin); return NULL; } } - *new_plugin = (plugin) { .name = copy_name, - .argc = 1, - .envc = 0, - .disabled = false, - .next = *plugin_list }; + *new_plugin = (plugin){ .name = copy_name, + .argc = 1, + .disabled = false, + .next = plugin_list }; - new_plugin->argv = malloc(sizeof(char *) * 2); - if (new_plugin->argv == NULL){ + do { + new_plugin->argv = malloc(sizeof(char *) * 2); + } while(new_plugin->argv == NULL and errno == EINTR); + if(new_plugin->argv == NULL){ free(copy_name); free(new_plugin); 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){ free(copy_name); free(new_plugin->argv); @@ -137,8 +154,9 @@ 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; } @@ -146,14 +164,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; } @@ -174,37 +197,56 @@ } /* 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 processes as completed when they exit, and save their exit status. */ -void handle_sigchld(__attribute__((unused)) int sig){ +static void handle_sigchld(__attribute__((unused)) int sig){ + int old_errno = errno; while(true){ - process *proc = process_list; + plugin *proc = plugin_list; int status; pid_t pid = waitpid(-1, &status, WNOHANG); if(pid == 0){ @@ -212,13 +254,13 @@ break; } if(pid == -1){ - if (errno != ECHILD){ - perror("waitpid"); + if(errno == ECHILD){ + /* No child processes */ + break; } - /* No child processes */ - break; + perror("waitpid"); } - + /* A child exited, find it in process_list */ while(proc != NULL and proc->pid != pid){ proc = proc->next; @@ -228,15 +270,14 @@ continue; } proc->status = status; - proc->completed = true; + 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)); @@ -247,46 +288,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 != NULL; plugin_list = next){ - next = plugin_list->next; - 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; @@ -294,6 +334,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; @@ -324,90 +365,81 @@ { .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 }, { .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'){ + error_t parse_opt(int key, char *arg, __attribute__((unused)) + struct argp_state *state){ + switch(key){ + char *tmp; + intmax_t tmpmax; + case 'g': /* --global-options */ + if(arg != NULL){ + char *plugin_option; + while((plugin_option = strsep(&arg, ",")) != NULL){ + if(plugin_option[0] == '\0'){ continue; } - if(not add_argument(getplugin(NULL, plugins), p)){ + if(not add_argument(getplugin(NULL), plugin_option)){ perror("add_argument"); return ARGP_ERR_UNKNOWN; } } } break; - case 'e': + case 'G': /* --global-env */ if(arg == NULL){ break; } - { - char *envdef = strdup(arg); - if(envdef == NULL){ - break; - } - if(not add_environment(getplugin(NULL, plugins), envdef)){ - perror("add_environment"); - } + if(not add_environment(getplugin(NULL), arg, true)){ + 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; - } + case 'o': /* --options-for */ + if(arg != NULL){ + char *plugin_name = strsep(&arg, ":"); + if(plugin_name[0] == '\0'){ + break; + } + char *plugin_option; + while((plugin_option = strsep(&arg, ",")) != NULL){ + if(not add_argument(getplugin(plugin_name), plugin_option)){ + perror("add_argument"); + return ARGP_ERR_UNKNOWN; } } } break; - case 'f': + case 'E': /* --env-for */ if(arg == NULL){ break; } @@ -416,76 +448,166 @@ if(envdef == NULL){ break; } - char *p_name = strndup(arg, (size_t) (envdef-arg)); - if(p_name == NULL){ - break; - } - envdef++; - if(not add_environment(getplugin(p_name, plugins), envdef)){ + *envdef = '\0'; + if(not add_environment(getplugin(arg), envdef+1, true)){ perror("add_environment"); } } break; - case 'd': - if (arg != NULL){ - plugin *p = getplugin(arg, plugins); + case 'd': /* --disable */ + if(arg != NULL){ + plugin *p = getplugin(arg); 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: + case 'e': /* --enable */ + if(arg != NULL){ + plugin *p = getplugin(arg); + if(p == NULL){ + return ARGP_ERR_UNKNOWN; + } + p->disabled = false; + } + break; + case 128: /* --plugin-dir */ + free(plugindir); + plugindir = strdup(arg); + if(plugindir == NULL){ + perror("strdup"); + } + break; + case 129: /* --config-file */ + /* This is already done by parse_opt_config_file() */ + break; + case 130: /* --userid */ + errno = 0; + tmpmax = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmpmax != (uid_t)tmpmax){ + fprintf(stderr, "Bad user ID number: \"%s\", using %" + PRIdMAX "\n", arg, (intmax_t)uid); + } else { + uid = (uid_t)tmpmax; + } + break; + case 131: /* --groupid */ + errno = 0; + tmpmax = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmpmax != (gid_t)tmpmax){ + fprintf(stderr, "Bad group ID number: \"%s\", using %" + PRIdMAX "\n", arg, (intmax_t)gid); + } else { + 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]", +/* + * 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'){ + fprintf(stderr, "Ignoring unknown argument \"%s\"\n", arg); + } + break; + case ARGP_KEY_END: + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; + } + + /* 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){ + 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); + if(argfile == NULL){ + perror("strdup"); + } + break; + case 130: /* --userid */ + case 131: /* --groupid */ + case 132: /* --debug */ + case ARGP_KEY_ARG: + case ARGP_KEY_END: + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; + } + + 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){ + /* 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, 0, NULL); + if(ret == ARGP_ERR_UNKNOWN){ fprintf(stderr, "Unknown error while parsing arguments\n"); exitstatus = EXIT_FAILURE; goto fallback; } - - conffp = fopen(argfile, "r"); + + /* 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){ + perror("malloc"); + exitstatus = EXIT_FAILURE; + 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){ @@ -493,36 +615,65 @@ 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){ + perror("strdup"); + exitstatus = EXIT_FAILURE; + free(org_line); + goto fallback; + } + + custom_argc += 1; + custom_argv = realloc(custom_argv, sizeof(char *) + * ((unsigned int) custom_argc + 1)); + if(custom_argv == NULL){ + perror("realloc"); + exitstatus = EXIT_FAILURE; + 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){ + perror("fclose"); + exitstatus = EXIT_FAILURE; + 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){ + if(errno == EMFILE or errno == ENFILE or errno == ENOMEM){ perror("fopen"); exitstatus = EXIT_FAILURE; goto fallback; } } - + /* If there was any arguments from configuration file, + pass them to parser as command 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){ + ret = argp_parse(&argp, custom_argc, custom_argv, ARGP_IN_ORDER, + 0, NULL); + if(ret == ARGP_ERR_UNKNOWN){ fprintf(stderr, "Unknown error while parsing arguments\n"); exitstatus = EXIT_FAILURE; goto fallback; } } + /* Parse actual command line arguments, to let them override the + config file */ + ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, 0, NULL); + if(ret == ARGP_ERR_UNKNOWN){ + fprintf(stderr, "Unknown error while parsing arguments\n"); + exitstatus = EXIT_FAILURE; + goto fallback; + } + if(debug){ for(plugin *p = plugin_list; p != NULL; p=p->next){ fprintf(stderr, "Plugin: %s has %d arguments\n", @@ -530,24 +681,29 @@ 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){ + perror("setgid"); + } ret = setuid(uid); - if (ret == -1){ + if(ret == -1){ perror("setuid"); } - setgid(gid); - if (ret == -1){ - perror("setgid"); + if(plugindir == NULL){ + dir = opendir(PDIR); + } else { + dir = opendir(plugindir); } - dir = opendir(plugindir); if(dir == NULL){ perror("Could not open plugin dir"); exitstatus = EXIT_FAILURE; @@ -569,12 +725,15 @@ 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){ + if(errno == EBADF){ perror("readdir"); exitstatus = EXIT_FAILURE; goto fallback; @@ -584,7 +743,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; @@ -592,6 +751,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); @@ -605,11 +765,9 @@ 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) @@ -628,22 +786,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"); continue; } - ret = stat(filename, &st); - if (ret == -1){ + ret = (int)TEMP_FAILURE_RETRY(stat(filename, &st)); + if(ret == -1){ perror("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); @@ -651,7 +818,8 @@ free(filename); continue; } - plugin *p = getplugin(dirst->d_name, &plugin_list); + + plugin *p = getplugin(dirst->d_name); if(p == NULL){ perror("getplugin"); free(filename); @@ -667,7 +835,7 @@ } { /* 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)){ @@ -676,7 +844,7 @@ } /* Add global environment variables */ for(char **e = g->environ; *e != NULL; e++){ - if(not add_environment(p, *e)){ + if(not add_environment(p, *e, false)){ perror("add_environment"); } } @@ -687,24 +855,20 @@ 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)){ + if(not add_environment(p, *e, false)){ perror("add_environment"); } } } int pipefd[2]; - ret = pipe(pipefd); - if (ret == -1){ + ret = (int)TEMP_FAILURE_RETRY(pipe(pipefd)); + if(ret == -1){ perror("pipe"); exitstatus = EXIT_FAILURE; goto fallback; } + /* Ask OS to automatic close the pipe on exec */ ret = set_cloexec_flag(pipefd[0]); if(ret < 0){ perror("set_cloexec_flag"); @@ -718,14 +882,19 @@ 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; 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; @@ -738,12 +907,12 @@ perror("sigaction"); _exit(EXIT_FAILURE); } - 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); } - + ret = dup2(pipefd[1], STDOUT_FILENO); /* replace our stdout */ if(ret == -1){ perror("dup2"); @@ -768,121 +937,133 @@ } /* 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){ + perror("getplugin"); + ret = (int)(TEMP_FAILURE_RETRY + (sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, + NULL))); if(ret < 0){ - perror("sigprocmask"); + perror("sigprocmask"); } exitstatus = EXIT_FAILURE; 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; goto fallback; } - FD_SET(new_process->fd, &rfds_all); + FD_SET(new_plugin->fd, &rfds_all); - if (maxfd < new_process->fd){ - maxfd = new_process->fd; + if(maxfd < new_plugin->fd){ + maxfd = new_plugin->fd; } - } - free_plugin_list(plugin_list); - plugin_list = NULL; - - 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){ + if(select_ret == -1 and errno != EINTR){ perror("select"); exitstatus = EXIT_FAILURE; 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); + /* 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; 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; + exitstatus = EXIT_FAILURE; + 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); if(not bret){ @@ -891,16 +1072,18 @@ } goto fallback; } + /* This process has not completed. Does it have any output? */ if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ /* 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){ + if(proc->buffer == NULL){ perror("malloc"); exitstatus = EXIT_FAILURE; goto fallback; @@ -908,31 +1091,40 @@ 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){ + if(plugin_list == NULL or exitstatus != EXIT_SUCCESS){ /* 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; @@ -945,39 +1137,42 @@ perror("sigaction"); exitstatus = EXIT_FAILURE; } - + 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 */ + perror("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"); } + 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 2009-09-16 23:28:39 +0000 @@ -0,0 +1,102 @@ +/* -*- coding: utf-8 -*- */ +/* + * Askpass-FIFO - Read a password from a FIFO and output it + * + * 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 + * 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, EEXIST */ +#include /* perror() */ +#include /* EXIT_FAILURE, NULL, size_t, free(), + realloc(), EXIT_SUCCESS */ +#include /* open(), O_RDONLY */ +#include /* read(), close(), write(), + STDOUT_FILENO */ + + +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 and errno != EEXIST){ + perror("mkfifo"); + return EXIT_FAILURE; + } + + /* Open FIFO */ + int fifo_fd = open(passfifo, O_RDONLY); + if(fifo_fd == -1){ + perror("open"); + return EXIT_FAILURE; + } + + /* 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){ + perror("realloc"); + free(buf); + return EXIT_FAILURE; + } + buf = tmp; + buf_allocated += blocksize; + } + sret = read(fifo_fd, buf + buf_len, buf_allocated - buf_len); + if(sret == -1){ + perror("read"); + free(buf); + return EXIT_FAILURE; + } + 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){ + perror("write"); + free(buf); + return EXIT_FAILURE; + } + written += (size_t)sret; + } + free(buf); + + 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-24 10:49:09 +0000 +++ plugins.d/mandos-client.c 2009-09-17 11:22:28 +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,62 @@ */ /* 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 */ + 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() */ #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() */ + +#ifdef __linux__ +#include /* klogctl() */ +#endif /* __linux__ */ /* Avahi */ /* All Avahi types, constants and functions @@ -86,7 +104,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,10 +117,13 @@ #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 = ""; /* Used for passing in values through the Avahi callback functions */ @@ -112,18 +134,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 +163,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,33 +283,24 @@ /* 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; - if (debug){ + if(debug){ gpgme_decrypt_result_t result; - result = gpgme_op_decrypt_result(ctx); - if (result == NULL){ + 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", @@ -229,16 +312,14 @@ } gpgme_recipient_t recipient; recipient = result->recipients; - if(recipient){ - while(recipient != NULL){ - fprintf(stderr, "Public key algorithm: %s\n", - gpgme_pubkey_algo_name(recipient->pubkey_algo)); - fprintf(stderr, "Key ID: %s\n", recipient->keyid); - fprintf(stderr, "Secret key available: %s\n", - recipient->status == GPG_ERR_NO_SECKEY - ? "No" : "Yes"); - recipient = recipient->next; - } + while(recipient != NULL){ + fprintf(stderr, "Public key algorithm: %s\n", + gpgme_pubkey_algo_name(recipient->pubkey_algo)); + fprintf(stderr, "Key ID: %s\n", recipient->keyid); + fprintf(stderr, "Secret key available: %s\n", + recipient->status == GPG_ERR_NO_SECKEY + ? "No" : "Yes"); + recipient = recipient->next; } } } @@ -250,19 +331,19 @@ } /* 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; } @@ -270,7 +351,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; } @@ -281,7 +362,7 @@ } plaintext_length += ret; } - + if(debug){ fprintf(stderr, "Decrypted password is: "); for(ssize_t i = 0; i < plaintext_length; i++){ @@ -300,9 +381,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; } @@ -313,8 +395,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; @@ -323,13 +404,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 */ @@ -338,95 +419,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; } @@ -438,66 +536,124 @@ /* 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 */ + + if(quit_now){ + 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); + 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){ perror("socket"); - return -1; + 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){ + 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 ){ perror("inet_pton"); - return -1; + goto mandos_end; } if(ret == 0){ fprintf(stderr, "Bad address: %s\n", ip); - return -1; - } - to.in6.sin6_port = htons(port); /* Spurious warning */ + 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"); + 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){ + 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){ @@ -506,101 +662,136 @@ } } - ret = connect(tcp_sd, &to.in, sizeof(to)); - if (ret < 0){ + if(quit_now){ + 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){ perror("connect"); - return -1; - } - + goto mandos_end; + } + + if(quit_now){ + 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){ perror("write"); - retval = -1; 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){ + 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){ + goto mandos_end; + } + + gnutls_transport_set_ptr(session, (gnutls_transport_ptr_t) tcp_sd); + + if(quit_now){ + goto mandos_end; + } + + do { + ret = gnutls_handshake(session); + if(quit_now){ + 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; 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){ + 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){ + perror("incbuffer"); + goto mandos_end; + } + + if(quit_now){ + 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){ + 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); 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); goto mandos_end; } } else { - buffer_length += (size_t) ret; + buffer_length += (size_t) sret; } } @@ -608,49 +799,67 @@ fprintf(stderr, "Closing TLS session\n"); } - gnutls_bye (session, GNUTLS_SHUT_RDWR); - - if (buffer_length > 0){ + if(quit_now){ + goto mandos_end; + } + + do { + ret = gnutls_bye(session, GNUTLS_SHUT_RDWR); + if(quit_now){ + 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){ + goto mandos_end; + } + + ret = (int)fwrite(decrypted_buffer + written, 1, + (size_t)decrypted_buffer_size - written, + stdout); if(ret == 0 and ferror(stdout)){ if(debug){ fprintf(stderr, "Error writing encrypted data: %s\n", strerror(errno)); } - retval = -1; - break; + goto mandos_end; } written += (size_t)ret; } - free(decrypted_buffer); - } else { - retval = -1; + retval = 0; } - } else { - retval = -1; } /* Shutdown procedure */ mandos_end: + free(decrypted_buffer); free(buffer); - close(tcp_sd); - gnutls_deinit (session); + if(tcp_sd >= 0){ + ret = (int)TEMP_FAILURE_RETRY(close(tcp_sd)); + } + if(ret == -1){ + perror("close"); + } + gnutls_deinit(session); + if(quit_now){ + retval = -1; + } 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, @@ -661,19 +870,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: @@ -682,41 +894,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: @@ -725,12 +941,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: @@ -745,309 +960,657 @@ } } -/* 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; +} + +int main(int argc, char *argv[]){ + AvahiSServiceBrowser *sb = NULL; + int error; + int ret; + intmax_t tmpmax; char *tmp; - int ret = asprintf(&tmp, "%s/%s", first, second); - if(ret < 0){ - return NULL; - } - return tmp; -} - - -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"); - 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; - } 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"); - } - + int exitcode = EXIT_SUCCESS; + const char *interface = "eth0"; + struct ifreq network; + int sd = -1; + bool take_down_interface = false; + uid_t uid; + gid_t gid; + char *connect_to = NULL; + 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 }, + { .name = NULL } + }; + + error_t parse_opt(int key, char *arg, + struct argp_state *state){ + 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){ + fprintf(stderr, "Bad number of DH bits\n"); + exit(EXIT_FAILURE); + } + 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'){ + fprintf(stderr, "Bad delay\n"); + exit(EXIT_FAILURE); + } + 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 a Mandos server" }; + ret = argp_parse(&argp, argc, argv, 0, 0, NULL); + if(ret == ARGP_ERR_UNKNOWN){ + fprintf(stderr, "Unknown error while parsing arguments\n"); + exitcode = EXIT_FAILURE; + goto end; + } + } + + if(not debug){ + avahi_set_log_function(empty_log); + } + + /* 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 = EXIT_FAILURE; + goto end; + } + + sigemptyset(&sigterm_action.sa_mask); + ret = sigaddset(&sigterm_action.sa_mask, SIGINT); + if(ret == -1){ + perror("sigaddset"); + exitcode = EXIT_FAILURE; + goto end; + } + ret = sigaddset(&sigterm_action.sa_mask, SIGHUP); + if(ret == -1){ + perror("sigaddset"); + exitcode = EXIT_FAILURE; + goto end; + } + ret = sigaddset(&sigterm_action.sa_mask, SIGTERM); + if(ret == -1){ + perror("sigaddset"); + exitcode = EXIT_FAILURE; + 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 EXIT_FAILURE; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &sigterm_action, NULL); + if(ret == -1){ + perror("sigaction"); + exitcode = EXIT_FAILURE; + goto end; + } + } + ret = sigaction(SIGHUP, NULL, &old_sigterm_action); + if(ret == -1){ + perror("sigaction"); + return EXIT_FAILURE; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &sigterm_action, NULL); + if(ret == -1){ + perror("sigaction"); + exitcode = EXIT_FAILURE; + goto end; + } + } + ret = sigaction(SIGTERM, NULL, &old_sigterm_action); + if(ret == -1){ + perror("sigaction"); + return EXIT_FAILURE; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &sigterm_action, NULL); + if(ret == -1){ + perror("sigaction"); + exitcode = EXIT_FAILURE; + goto end; + } + } + + /* If the interface is down, bring it up */ + if(interface[0] != '\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 = EXIT_FAILURE; + 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 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 = EXIT_FAILURE; +#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 = EXIT_FAILURE; + /* 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"); + exitcode = EXIT_FAILURE; +#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 = EXIT_FAILURE; + 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 = EXIT_FAILURE; + 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 = EXIT_FAILURE; + 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 = EXIT_FAILURE; + 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; + } + + ret = start_mandos_communication(address, port, if_index, af); + if(ret < 0){ + exitcode = EXIT_FAILURE; + } else { + 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 = EXIT_FAILURE; + 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 = EXIT_FAILURE; + 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 &= ~IFF_UP; /* clear flag */ + ret = ioctl(sd, SIOCSIFFLAGS, &network); + if(ret == -1){ + perror("ioctl SIOCSIFFLAGS"); + } + } + 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 2009-02-09 02:01:13 +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,606 @@ &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 + eth0. + + + 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 empty string; + 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 2009-09-07 07:48:59 +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,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 @@ -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,7 +32,8 @@ #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, @@ -50,13 +51,18 @@ ARGP_KEY_ARG, ARGP_KEY_END, ARGP_ERR_UNKNOWN */ -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){ @@ -73,16 +79,14 @@ 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 }, { .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){ + switch (key){ case 'p': prefix = arg; break; @@ -90,7 +94,7 @@ debug = true; break; case ARGP_KEY_ARG: - argp_usage (state); + argp_usage(state); break; case ARGP_KEY_END: break; @@ -99,38 +103,56 @@ } return 0; } - + 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){ + .doc = "Mandos password-prompt -- Read and" + " output a password" }; + 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; } } - - 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){ + if(tcgetattr(STDIN_FILENO, &t_old) != 0){ + perror("tcgetattr"); return EXIT_FAILURE; } 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){ + perror("sigaddset"); + return EXIT_FAILURE; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + perror("sigaddset"); + return EXIT_FAILURE; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + perror("sigaddset"); + return EXIT_FAILURE; + } + /* 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; } - 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"); @@ -142,7 +164,7 @@ perror("sigaction"); return EXIT_FAILURE; } - 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"); @@ -154,7 +176,7 @@ perror("sigaction"); return EXIT_FAILURE; } - 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"); @@ -163,22 +185,25 @@ } - 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){ + if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ perror("tcsetattr-echo"); return EXIT_FAILURE; } - 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; } @@ -207,13 +232,29 @@ } } ret = getline(&buffer, &n, stdin); - if (ret > 0){ - fprintf(stdout, "%s", buffer); + if(ret > 0){ status = EXIT_SUCCESS; + /* Make n = data size instead of allocated buffer size */ + n = (size_t)ret; + /* 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){ + ret = write(STDOUT_FILENO, buffer + written, n - written); + if(ret < 0){ + perror("write"); + status = EXIT_FAILURE; + break; + } + written += (size_t)ret; + } break; } - if (ret < 0){ - if (errno != EINTR and not feof(stdin)){ + if(ret < 0){ + if(errno != EINTR and not feof(stdin)){ perror("getline"); status = EXIT_FAILURE; break; @@ -222,17 +263,38 @@ /* if(ret == 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){ + if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ perror("tcsetattr+echo"); } - if (debug){ - fprintf(stderr, "%s is exiting\n", argv[0]); + if(quit_now){ + sigemptyset(&old_action.sa_mask); + old_action.sa_handler = SIG_DFL; + ret = sigaction(signal_received, &old_action, NULL); + if(ret == -1){ + perror("sigaction"); + } + raise(signal_received); + } + + if(debug){ + fprintf(stderr, "%s is exiting with status %d\n", argv[0], + status); + } + if(status == EXIT_SUCCESS){ + 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-01-04 21:54:55 +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 - - + + + 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. + + + + 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/splashy.c' --- plugins.d/splashy.c 1970-01-01 00:00:00 +0000 +++ plugins.d/splashy.c 2009-09-16 23:28:39 +0000 @@ -0,0 +1,352 @@ +/* -*- coding: utf-8 -*- */ +/* + * Splashy - Read a password from splashy and output it + * + * 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 + * 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(), perror() */ +#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 */ +#include /* waitpid(), WIFEXITED(), + WEXITSTATUS() */ + +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; + + /* 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; + goto failure; + } + } + + /* Find splashy process */ + { + const char splashy_name[] = "/sbin/splashy"; + proc_dir = opendir("/proc"); + if(proc_dir == NULL){ + perror("opendir"); + 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){ + perror("asprintf"); + 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; + } + perror("lstat"); + free(exe_link); + 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){ + 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){ + perror("sigaddset"); + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + perror("sigaddset"); + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + perror("sigaddset"); + goto failure; + } + ret = sigaction(SIGINT, NULL, &old_action); + if(ret == -1){ + perror("sigaction"); + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &new_action, NULL); + if(ret == -1){ + perror("sigaction"); + goto failure; + } + } + ret = sigaction(SIGHUP, NULL, &old_action); + if(ret == -1){ + perror("sigaction"); + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &new_action, NULL); + if(ret == -1){ + perror("sigaction"); + goto failure; + } + } + ret = sigaction(SIGTERM, NULL, &old_action); + if(ret == -1){ + perror("sigaction"); + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &new_action, NULL); + if(ret == -1){ + perror("sigaction"); + 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){ + perror("fork"); + 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); + perror("execl"); + } + 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){ + perror("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){ + perror("setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + perror("chdir"); + } +/* if(fork() != 0){ */ +/* _exit(EXIT_SUCCESS); */ +/* } */ + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace stdout */ + if(ret == -1){ + perror("dup2"); + _exit(EXIT_FAILURE); + } + + execl("/sbin/splashy", "/sbin/splashy", "boot", (char *)NULL); + perror("execl"); + _exit(EXIT_FAILURE); + } + } + + 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){ + perror("sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + perror("raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return EXIT_FAILURE; +} === 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 2009-09-17 04:16:32 +0000 @@ -0,0 +1,642 @@ +/* -*- coding: utf-8 -*- */ +/* + * Usplash - Read a password from usplash and output it + * + * 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 + * 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 /* size_t, ssize_t, pid_t, DIR, struct + dirent */ +#include /* NULL */ +#include /* strlen(), memcmp() */ +#include /* asprintf(), perror() */ +#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() */ +#include /* getenv() */ +#include /* opendir(), readdir(), closedir() */ +#include /* intmax_t, strtoimax() */ +#include /* struct stat, lstat(), S_ISLNK */ + +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){ + perror("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){ + perror("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; + } + perror("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){ + perror("asprintf"); + goto fail_find_usplash; + } + cl_fd = open(cmdline_filename, O_RDONLY); + free(cmdline_filename); + if(cl_fd == -1){ + perror("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){ + perror("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){ + perror("read"); + close(cl_fd); + goto fail_find_usplash; + } + cmdline_len += (size_t)sret; + } while(sret != 0); + ret = close(cl_fd); + if(ret == -1){ + perror("close"); + goto fail_find_usplash; + } + } + /* Close directory */ + ret = closedir(proc_dir); + if(ret == -1){ + perror("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; + + char *prompt = makeprompt(); + if(prompt == NULL){ + goto failure; + } + + /* Find usplash process */ + char *cmdline = NULL; + size_t cmdline_len = 0; + usplash_pid = find_usplash(&cmdline, &cmdline_len); + if(usplash_pid == 0){ + 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){ + perror("sigaddset"); + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + perror("sigaddset"); + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + perror("sigaddset"); + goto failure; + } + ret = sigaction(SIGINT, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + perror("sigaction"); + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + perror("sigaction"); + } + goto failure; + } + } + ret = sigaction(SIGHUP, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + perror("sigaction"); + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + perror("sigaction"); + } + goto failure; + } + } + ret = sigaction(SIGTERM, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + perror("sigaction"); + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + perror("sigaction"); + } + goto failure; + } + } + } + + usplash_accessed = true; + /* Write command to FIFO */ + if(not usplash_write(&fifo_fd, "TIMEOUT", "0")){ + if(errno != EINTR){ + perror("usplash_write"); + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + if(not usplash_write(&fifo_fd, "INPUTQUIET", prompt)){ + if(errno != EINTR){ + perror("usplash_write"); + } + 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){ + perror("open"); + } + 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){ + perror("realloc"); + } + goto failure; + } + buf = tmp; + buf_allocated += blocksize; + } + sret = read(outfifo_fd, buf + buf_len, + buf_allocated - buf_len); + if(sret == -1){ + if(errno != EINTR){ + perror("read"); + } + 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){ + perror("close"); + } + goto failure; + } + outfifo_fd = -1; + + if(interrupted_by_signal){ + goto failure; + } + + if(not usplash_write(&fifo_fd, "TIMEOUT", "15")){ + if(errno != EINTR){ + perror("usplash_write"); + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + ret = close(fifo_fd); + if(ret == -1){ + if(errno != EINTR){ + perror("close"); + } + 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){ + perror("write"); + } + 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 EXIT_FAILURE; + } + + /* Close FIFO */ + if(fifo_fd != -1){ + ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); + if(ret == -1 and errno != EINTR){ + perror("close"); + } + fifo_fd = -1; + } + + /* Close output FIFO */ + if(outfifo_fd != -1){ + ret = (int)TEMP_FAILURE_RETRY(close(outfifo_fd)); + if(ret == -1){ + perror("close"); + } + } + + /* Create argc and argv for new usplash*/ + int cmdline_argc = 0; + char **cmdline_argv = malloc(sizeof(char *)); + { + size_t position = 0; + while(position < cmdline_len){ + char **tmp = realloc(cmdline_argv, + (sizeof(char *) + * (size_t)(cmdline_argc + 2))); + if(tmp == NULL){ + perror("realloc"); + free(cmdline_argv); + return EXIT_FAILURE; + } + cmdline_argv = tmp; + cmdline_argv[cmdline_argc] = cmdline + position; + cmdline_argc++; + position += strlen(cmdline + position) + 1; + } + cmdline_argv[cmdline_argc] = NULL; + } + /* 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){ + perror("setuid"); + } + + setsid(); + ret = chdir("/"); +/* if(fork() != 0){ */ +/* _exit(EXIT_SUCCESS); */ +/* } */ + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ + if(ret == -1){ + perror("dup2"); + _exit(EXIT_FAILURE); + } + + execv(usplash_name, cmdline_argv); + if(not interrupted_by_signal){ + perror("execv"); + } + free(cmdline); + free(cmdline_argv); + _exit(EXIT_FAILURE); + } + free(cmdline); + free(cmdline_argv); + sleep(2); + if(not usplash_write(&fifo_fd, "PULSATE", NULL)){ + if(errno != EINTR){ + perror("usplash_write"); + } + } + + /* Close FIFO (again) */ + if(fifo_fd != -1){ + ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); + if(ret == -1 and errno != EINTR){ + perror("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){ + perror("sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + perror("raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return EXIT_FAILURE; +} === 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 + + +
+ + + + +