chiark / gitweb /
Initial version. 1.0.0
authorMark Wooding <mdw@distorted.org.uk>
Wed, 16 May 2012 12:33:00 +0000 (13:33 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Wed, 16 May 2012 12:33:00 +0000 (13:33 +0100)
17 files changed:
.gitignore [new file with mode: 0644]
.links [new file with mode: 0644]
.skelrc [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
configure.ac [new file with mode: 0644]
debian/README.Debian [new file with mode: 0644]
debian/changelog [new file with mode: 0644]
debian/compat [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/rules [new file with mode: 0755]
debian/udpkey.examples [new file with mode: 0644]
debian/udpkey.init [new file with mode: 0755]
debian/udpkey.initramfs-hook [new file with mode: 0755]
debian/udpkey.keyscript [new file with mode: 0755]
udpkey.1 [new file with mode: 0644]
udpkey.c [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..2f3884d
--- /dev/null
@@ -0,0 +1,10 @@
+COPYING
+Makefile.in
+aclocal.m4
+config
+configure
+debian/*.log
+debian/*.debhelper
+debian/substvars
+debian/build
+debian/udpkey
diff --git a/.links b/.links
new file mode 100644 (file)
index 0000000..95d0804
--- /dev/null
+++ b/.links
@@ -0,0 +1,2 @@
+COPYING
+config/auto-version
diff --git a/.skelrc b/.skelrc
new file mode 100644 (file)
index 0000000..2d0ca64
--- /dev/null
+++ b/.skelrc
@@ -0,0 +1,9 @@
+;;; -*-emacs-lisp-*-
+
+(setq skel-alist
+      (append
+       '((full-title . "[[program-name]]")
+        (Program-name . "The udpkey program")
+        (program-name . "udpkey")
+        (author . "Mark Wooding"))
+       skel-alist))
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..402662d
--- /dev/null
@@ -0,0 +1,54 @@
+### -*-makefile-*-
+###
+### Build script for udpkey
+###
+### (c) 2012 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of udpkey.
+###
+### The udpkey 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 2 of the License, or
+### (at your option) any later version.
+###
+### The udpkey 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 udpkey; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+bin_PROGRAMS            =
+dist_man_MANS           =
+
+EXTRA_DIST              =
+
+###--------------------------------------------------------------------------
+### Main program.
+
+bin_PROGRAMS           += udpkey
+dist_man_MANS          += udpkey.1
+udpkey_SOURCES          = udpkey.c
+udpkey_LDADD            = $(mLib_LIBS) $(catacomb_LIBS)
+
+###--------------------------------------------------------------------------
+### Release tweaking.
+
+dist-hook::
+       echo $(VERSION) >$(distdir)/RELEASE
+
+EXTRA_DIST             += config/auto-version
+
+###--------------------------------------------------------------------------
+### Debian.
+
+EXTRA_DIST             += debian/control debian/rules
+EXTRA_DIST             += debian/copyright debian/changelog
+EXTRA_DIST             += debian/compat
+
+###----- That's all, folks --------------------------------------------------
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..0fbf6b7
--- /dev/null
@@ -0,0 +1,54 @@
+dnl -*-autoconf-*-
+dnl
+dnl Auto-configuration script for udpkey
+dnl
+dnl (c) 2012 Mark Wooding
+dnl
+
+dnl----- Licensing notice ---------------------------------------------------
+dnl
+dnl This file is part of udpkey.
+dnl
+dnl The udpkey program is free software; you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl The udpkey program is distributed in the hope that it will be useful,
+dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+dnl GNU General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with udpkey; if not, write to the Free Software Foundation,
+dnl Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+dnl--------------------------------------------------------------------------
+dnl Initialization.
+
+mdw_AUTO_VERSION
+AC_INIT([udpkey], AUTO_VERSION, [mdw@distorted.org.uk])
+AC_CONFIG_SRCDIR([udpkey.c])
+AC_CONFIG_AUX_DIR([config])
+AM_INIT_AUTOMAKE([foreign])
+mdw_SILENT_RULES
+
+dnl--------------------------------------------------------------------------
+dnl C programming environment.
+
+AC_PROG_CC
+AM_PROG_CC_C_O
+AX_CFLAGS_WARN_ALL
+AC_SUBST([AM_CFLAGS])
+
+PKG_CHECK_MODULES([mLib], [mLib >= 2.1.0])
+PKG_CHECK_MODULES([catacomb], [catacomb >= 2.1.1])
+AM_CFLAGS="$AM_CFLAGS $mLib_CFLAGS $catacomb_CFLAGS"
+
+dnl--------------------------------------------------------------------------
+dnl Output.
+
+AC_CONFIG_FILES([Makefile])
+AC_OUTPUT
+
+dnl----- That's all, folks --------------------------------------------------
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644 (file)
index 0000000..2186835
--- /dev/null
@@ -0,0 +1,50 @@
+udpkey in Debian
+
+The =udpkey= program itself is described in a traditional manual page.
+It makes few assumptions about the environment in which it's run, so it
+needs some work to integrate it with any particular system.
+
+* Running as a server
+
+To get =udpkey= to run as a server:
+
+  + Create a user to run the server, e.g., =adduser --system --group
+    udpkey=.
+
+  + Create =/etc/udpkey/keyring=, and populate it with key fragments and
+    client public keys as described in the manual.  The keyring file
+    must be readable by the user created above.
+
+  + Create =/etc/default/udpkey=.  This must at the very least set
+    =UDPKEY_DAEMON=yes= if the daemon is to be run at all.  I chose port
+    59274 arbitrarily; if you want to use a different one, set
+    =PORT=12345= or whatever.
+
+* Running as a client in initramfs
+
+Some simple scripts for integrating =udpkey= with =cryptsetup= are
+provided in =/usr/share/doc/udpkey/examples=.  See the comments in those
+files for details.  Here's the brief version.
+
+  + Copy =udpkey.initramfs-hook= into =/etc/initramfs-tools/hooks=.
+    Install =udpkey.keyscript= somewhere, say =/usr/local/sbin=.
+
+  + Create =/etc/udpkey/keyring= and generate a private key.  See the
+    manual for details of how to do this.  Extract the public key and
+    transport it to the server.
+
+  + Add a line to =/etc/crypttab= of the form
+    : cvolume    /dev/md/encrypted   keytag/192.0.2.69:59274   luks,keyscript=/usr/local/sbin/udpkey.keyscript
+    to =/etc/crypttab=.
+
+  + Generate a key fragment at your chosen server, here 192.0.2.69.
+    Import the client's public key and grant it access to the key
+    fragment.
+
+  + Generate a random string of the same length and write it to
+    =/etc/udpkey/keytag.local=.
+
+  + Run
+    : udpkey keytag 192.0.2.69:59274 /etc/udpkey/keytag.local | sha256sum
+    to make sure that everything's actually working.  Add the key to
+    your LUKS superblock.
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..31d9bb1
--- /dev/null
@@ -0,0 +1,6 @@
+udpkey (1.0.0) experimental; urgency=low
+
+  * Initial version.
+
+ -- Mark Wooding <mdw@distorted.org.uk>  Wed, 16 May 2012 12:37:26 +0100
+
diff --git a/debian/compat b/debian/compat
new file mode 100644 (file)
index 0000000..45a4fb7
--- /dev/null
@@ -0,0 +1 @@
+8
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..190af54
--- /dev/null
@@ -0,0 +1,29 @@
+Source: udpkey
+Section: utils
+Priority: extra
+Maintainer: Mark Wooding <mdw@distorted.org.uk>
+Build-Depends: catacomb-dev (>= 2.1.1), mlib-dev (>= 2.1.0), debhelper (>= 8)
+Standards-Version: 3.1.1
+
+Package: udpkey
+Architecture: any
+Depends: ${shlibs:Depends}
+Recommends: catacomb-bin
+Suggests: cryptsetup
+Description: Fetch or serve cryptographic keys over a network.
+ The udpkey program can fetch key data from remote servers using a simple
+ UDP-baed cryptographic protocol; or can can run as a server, providing key
+ material on request to authorized clients.
+ .
+ When running as a client, the program fetches key fragments from multiple
+ sources, combining them together.  It can read key fragments from local
+ files or request them from servers.  Key data can be split among many
+ servers for increased security, and individual fragments can be held on and
+ requested from multiple servers for increased availability.
+ .
+ The client can be run in early userland, e.g., in initramfs, to obtain key
+ material for decrypting a server's disks.
+ .
+ When running as a server, the program responds to requests, verifying that
+ the client is authorized, and encrypting the requested key fragment with the
+ appropriate client-specific public key.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..726a6cb
--- /dev/null
@@ -0,0 +1,16 @@
+The udpkey program is copyright (c) 2012 Mark Wooding.
+
+The udpkey 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 2 of the License, or
+(at your option) any later version.
+
+The udpkey 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 a copy of the GNU General Public License in
+/usr/share/common-licenses/GPL; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+USA.
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..0c6c61a
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/make -f
+
+%:; dh $@ -Bdebian/build
diff --git a/debian/udpkey.examples b/debian/udpkey.examples
new file mode 100644 (file)
index 0000000..3781e1e
--- /dev/null
@@ -0,0 +1,2 @@
+debian/udpkey.keyscript
+debian/udpkey.initramfs-hook
diff --git a/debian/udpkey.init b/debian/udpkey.init
new file mode 100755 (executable)
index 0000000..d56575e
--- /dev/null
@@ -0,0 +1,127 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides:          udpkey
+# Required-Start:    $remote_fs $syslog
+# Required-Stop:     $remote_fs $syslog
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Provide boot keys to remote systems
+### END INIT INFO
+
+# Author: Mark Wooding <mdw@distorted.org.uk>
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="Boot key daemon"
+NAME=udpkey
+DAEMON=/usr/bin/$NAME
+PORT=59274
+PIDFILE=/var/run/$NAME.pid
+USER=udpkey
+DAEMON_ARGS="-ld -k/etc/udpkey/keyring -p$PIDFILE"
+SCRIPTNAME=/etc/init.d/$NAME
+UDPKEY_DAEMON=no
+
+# 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
+case $UDPKEY_DAEMON in yes) ;; *) exit 0 ;; esac
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /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
+       touch $PIDFILE; chown $USER $PIDFILE
+       start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --chuid $USER -- \
+               $DAEMON_ARGS $PORT \
+               || return 2
+}
+
+#
+# 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"
+}
+
+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
+       ;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  restart|force-reload)
+       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|status|restart|force-reload}" >&2
+       exit 3
+       ;;
+esac
+
+:
diff --git a/debian/udpkey.initramfs-hook b/debian/udpkey.initramfs-hook
new file mode 100755 (executable)
index 0000000..33be1c4
--- /dev/null
@@ -0,0 +1,18 @@
+#! /bin/sh
+###
+### This file is an initramfs hook script: it copies stuff to the initramfs
+### as required by the example udpkey.keyscript.  It should be copied to
+### /etc/initramfs-tools/hooks.
+
+case "$1" in
+  prereqs)
+    echo ""
+    exit
+    ;;
+esac
+
+. /usr/share/initramfs-tools/hook-functions
+
+copy_exec /usr/bin/udpkey
+cp -r /etc/udpkey $DESTDIR/etc/
+dd if=/dev/random of=$DESTDIR/etc/udpkey/seed bs=1 count=32
diff --git a/debian/udpkey.keyscript b/debian/udpkey.keyscript
new file mode 100755 (executable)
index 0000000..bdc48a3
--- /dev/null
@@ -0,0 +1,59 @@
+#! /bin/sh
+### udpkey.keyscript KEY/SERVER:PORT[=TAG][#HASH];...
+###
+### This is an example cryptsetup key-script for fetching keys during early
+### boot.  The argument is obtained as the `key-file' field from the
+### crypttab(5) file.  The KEY is the key tag name requested from the
+### server(s); the rest of the argument is a udpkey(1) source-spec.
+###
+### A hook script or similar should arrange for /usr/bin/udpkey to be
+### installed and for the following things to be placed in /etc/udpkey in the
+### initramfs.  See udpkey.initramfs-hook for an example.
+###
+### keyring    The keyring file used by udpkey.
+###
+### KEY.local  A locally held key fragment.  (Optional.)
+###
+### seed       A key for udpkey's random-number generator.  Ideally, a hook
+###            script should write high-quality random data to this file
+###            each time the initramfs is constructed.
+###
+### The generated initramfs will contain important secrets.  It must not be
+### left readable by unprivileged users.
+
+set -e
+
+## Check the command-line argument.
+case $#,$1 in
+  1,*/*:*) tag=${1%%/*} server=${1#*/} ;;
+  *) echo >&2 "Usage: $0 KEY/SERVER:PORT[=TAG][#HASH];..."; exit 16 ;;
+esac
+
+## Some preflight checks.
+if [ ! -x /usr/bin/udpkey ]; then
+  echo >&2 "$0: can't find udpkey executable"
+  exit 8
+fi
+if [ ! -f /etc/udpkey/keyring ]; then
+  echo >&2 "$0: can't find local keyring"
+  exit 8
+fi
+
+## Make sure we have networking.
+if [ -f /scripts/functions ]; then
+  . /scripts/functions
+  configure_networking
+fi
+
+## Build a command line.
+cmd="/usr/bin/udpkey -k/etc/udpkey/keyring"
+if [ -f /etc/udpkey/seed ]; then
+  cmd="$cmd -r/etc/udpkey/seed"
+fi
+cmd="$cmd $tag $server"
+if [ -f /etc/udpkey/$tag.local ]; then
+  cmd="$cmd /etc/udpkey/$tag.local"
+fi
+
+## Ready to rock.
+exec $cmd
diff --git a/udpkey.1 b/udpkey.1
new file mode 100644 (file)
index 0000000..4c9df17
--- /dev/null
+++ b/udpkey.1
@@ -0,0 +1,380 @@
+.\" -*-nroff-*-
+.EQ
+delim $$
+.EN
+.de hP
+.IP
+\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c
+..
+.ie t .ds o \(bu
+.el .ds o o
+.ds DH Diffie\(enHellman
+.TH udpkey 1 "2012-05-08" "Mark Wooding" "distorted.org.uk tools"
+.SH NAME
+udpkey \- send or receive a cryptographic key via a simple UDP protocol
+.SH SYNOPSIS
+.B udpkey
+.RB [ \-k
+.IR keyring ]
+.RB [ \-r
+.IR seed-file ]
+.I fragment-tag
+.I source-spec
+\&...
+.br
+.B udpkey
+.B \-l
+.RB [ \-d ]
+.RB [ \-k
+.IR keyring ]
+.RB [ \-r
+.IR seed-file ]
+.RI [ address \c
+.BR : ] \c
+.I port
+.PP
+.IR source-spec :
+.br
+       
+.IB address : port \c
+.IB [ = \c
+.IR tag ] \c
+.IB [ # \c
+.IR hash ] \c
+.BR ; ...
+.br
+       
+.BI / filename
+|
+.BI ./ filename
+.SH DESCRIPTION
+The
+.B udpkey
+program can run in one of two modes: either it will request fragments of
+a key from a number of sources (e.g., local files or remote servers),
+assemble them together, and write the result to standard output; or it
+will listen on a UDP port and transmit encrypted copies of key fragments
+when requested.
+.PP
+The intended use of
+.B udpkey
+is for obtaining keys early in a system's boot process, so as to decrypt
+the main disk volumes.  See the discussion below regarding the security
+properties of this approach.
+.SS Options
+The recognized command-line options are listed below.  The synopsis
+shows two distinct invocations for clarity: in fact, all options are
+recognized all of the time, though options which are irrelevent in the
+chosen mode are silently ignored.
+.TP
+.B \-h, \-\-help
+Print a help message to standard output and exit successfully.
+.TP
+.B \-v, \-\-version
+Print the program's version number to standard output and exit
+successfully.
+.TP
+.B \-u, \-\-usage
+Print a brief usage summary to standard output and exit successfully.
+.TP
+.B \-d, \-\-daemon
+If the
+.B \-l
+option is also given,
+.B udpkey
+will detach from the terminal and run in the background after
+initialization.  Also, it will write messages using
+.BR syslog (3)
+(with facility
+.BR daemon )
+rather than to standard error.
+.TP
+.BI "\-k, \-\-keyring=" keyring
+Read keys from the
+.I keyring
+file, rather than the default, which is the file named
+.B keyring
+in the current working directory.
+.TP
+.B \-l, \-\-listen
+Listen for incoming requests for key fragments and reply to them.  The
+default is to request key fragments.
+.TP
+.BR "\-r, \-\-random=" seed-file
+Use (an initial portion of) the contents of
+.I seed-file
+to key the program's pseurorandom number generator.  Since
+.B udpkey
+is intended to run early in a system's boot procedure, it's quite
+unlikely that there's a great deal of high-quality entropy available.
+It's therefore useful to generate a key while the system is running, and
+store it somewhere where it can be found during early boot.
+.SS Client operation
+For each
+.I source-spec
+on the command line of the form
+.BI / filename
+or
+.BI ./ filename
+the contents (or the inital 64KB of the contents, if the file is longer)
+are read as a key fragment.
+.PP
+For each
+.I source-spec
+of the form
+.IP
+.IB address : port \c
+.IB [ = \c
+.IR tag ] \c
+.IB [ # \c
+.IR hash ] \c
+.BR ; ...
+.PP
+a packet is sent to each listed
+.I address
+and
+.I port
+requesting the key fragment named by
+.IR fragment-tag ;
+responses are decrypted using the key
+.I tag
+(default
+.BR udpkey-kem ).
+If a valid response is received from any of the listed servers (matching
+the given
+.I hash
+if specified) then the contents are used as the key fragment; if no
+response is forthcoming from any of them then the requests are
+retransmitted periodically.  If no acceptable reponse is received after
+a number of retransmissions,
+.B udpkey
+will give up.
+.PP
+If all of the fragments are successfully obtained then
+.B udpkey
+will check that they are the same length, XOR them together, and write
+the result to its standard output; it finally exits with status 0.
+.SS Server operaton
+If the
+.B \-l
+option was given,
+.B udpkey
+runs in sever mode.  It listens for incoming UDP packets addressed to
+the given
+.I port
+(and, if specified, the given
+.I address
+\(en by default, any local address will do).  If the
+.B \-d
+option was given, then
+.B udpkey
+will detach from its terminal (if any) and continue running in the
+background.
+.PP
+A request packet contains a key tag identifying the wanted key
+fragment.  The key fragment is located.  If the key data is not a plain
+binary string, or the key has no
+.B clients
+attribute then the request is rejected.  Otherwise the value of the
+.B clients
+attribute is expected to take the form
+.IP
+.IR address \c
+.RB [ / \c
+.IR prefix-len ] \c
+.RB [ = \c
+.IR tag ] \c
+.BR ; ...
+.PP
+The clauses of the attribute value are interpreted from left to right,
+as follows.  If the most significant
+.I prefix-len
+bits (default 32 \(en i.e., all of them) of client's IP match the
+corresponding bits of
+.I address
+then send the key fragment, encrypted using the key named
+.I tag
+(default
+.BI client- addr \fR,
+where
+.I addr
+is the client's IP address in dotted-quad form); no further clauses are
+examined.  If no clauses match then the request is refused and no reply
+is sent.
+.SS Key setup
+The
+.B udpkey
+program uses the Catacomb keyring format to store its cryptographic
+keys: see
+.BR keyring (5)
+for the technical details.  Keys maybe generated and managed using the
+.BR key (1)
+utility.
+.PP
+The security of
+.BR udpkey 's
+protocol (described below, for those who care about such things) is
+based on the difficulty of the \*(DH in cyclic groups.  The client need
+to know the private key; the server need only know the public part.
+Both ends must agree on the attributes associated with the key.
+.PP
+Two types of \*(DH groups are supported.  The group type is determined
+from the appropriate key's
+.B group
+attribute, if present.  The possible values are as follows.
+.TP
+.B dh
+Plain old \*(DH, in a Schnorr group \(en i.e., a prime-order subgroup of
+a the multiplicative group of a prime-order finite field.  An
+appropriate key may be generated using a command such as
+.RS
+.IP
+.nf
+.BI "key add \-t" tag " \-adh \-LS \-b3072 \-B256 udpkey-kem group=dh \fR..."
+.fi
+.RE
+.TP
+.B ec
+A prime order subgroup of the group of projective points on an elliptic
+curve.  Catacomb's
+.BR key (1)
+program can't generate such groups, though it knows of a number of
+suitable examples, or you can use your own curves.  An appropriate man
+be generated using a command such as
+.RS
+.IP
+.BI "key add \-t" tag " \-aec \-Cnist-p256 udpkey-kem group=ec \fR..."
+.RE
+.PP
+Other attributes on the key determine the ancillary cryptographic
+algorithms used in the protocol, as follows.
+.TP
+.B hash
+The hash function used to derive symmetric keys from the shared secret
+group element.  The default is
+.BR sha256 .
+.TP
+.B cipher
+The symmetric encryption algorithm used to encrypt the key fragment.
+The default is
+.BR rijndael-counter .
+.TP
+.B mac
+The message authentication code used to ensure the integrity of the
+ciphertext, in the form
+.IB name / tagbits \fR.
+The default is to use HMAC with the chosen hash function, and truncate
+the tag to half of its original length.
+.PP
+Key fragments must contain only plain binary data: you can generate one
+using a command such as
+.IP
+.BI "key add \-t" tag " \-abinary \-b256 udpkey-frag clients=" client-spec " \fR..."
+.PP
+The
+.B client
+attribute is mandatory; its syntax and semantics are described above.
+.SS Protocol description
+Let $G$ be a cyclic group with prime order $q$; we consider this as a
+one-dimensional vector space over the finite field ${roman GF}(q)$.  Let
+$P$ be any nonzero element of the group.
+.PP
+The client's private key is a scalar $x$; its public key is the
+corresponding vector $X = x P$.  When constructing a request, a client
+selects a random scalar $u$; let $U = u P$ be the corresponding vector.
+The request packet consists of the key tag of the wanted key fragment
+followed by a representation of the vector $U$.
+.PP
+When constructing a response, a server selects random scalars $v$ and
+$r$, and computes $U = u P$ and $R = r P$.  It then determines $Y = v U$
+and $Z = r X$, and hashes $R$ and $Z$ to obtain keys for a symmetric
+cipher and MAC.  It encrypts the key fragment and authenticates the
+resulting ciphertext.  Finally, the response consists of the vectors $V$
+and $W = R - Y$, the MAC tag on the ciphertext, and the ciphertext
+itself.
+.PP
+The client can determine $Y = u V$, $R = W + Y$, and compute $Z = x R$,
+and thereby recover the cipher and MAC keys.
+.SS Security discussion
+We assume that the client can securely
+.I erase
+the key, and the ephemeral secret scalar $r$, from its memory once it
+has finished using them.  If we detect that the client has compromised
+at some point when it does not know the key, we can instruct the servers
+to withhold their fragments of the key.
+.PP
+The dance with $U$ and $V$ is a standard ephemeral \*(DH key exchange.
+The other dance with $R$ and $X$, and the symmetric encryption, is
+basically DLIES.  The only trick is that $R$ is masked in the reply
+using the ephemeral \*(DH key $Y$.  (Subtracting rather than adding $Y$
+is more efficient.  For the server, it makes no difference, since it can
+compute $-Y = (-v) U$ and add; but for the client, subtraction might be
+rather slower than addition.)
+.PP
+We have the following properties.
+.hP \*o
+Passively collecting requests and responses before compromising the
+client does not assist an adversary in determining the value of a key
+fragment, since the ephemeral scalars $u$ and $v$ are random and
+independent.  Assuming that Decisional \*(DH is hard in $G$, the
+ephemeral secret $Y$ appears random to the adversary, so $W$ leaks
+nothing about $R$.  The symmetric keys are therefore independent of the
+adversary's view.
+.IP
+We can do better.  Suppose that an adversary can recover the symmetric
+key given a request/reply pair.  If we assume that the symmetric
+encryption is good, then the adversary must have found the key by
+hashing $R$ and $Z$.  Therefore it can recover $Y = R - W$, which solves
+an instance of the harder
+.I Computational
+(actually, Gap) \*(DH problem in $G$.  This analysis can be made
+rigorous and quantitative.
+.hP \*o
+Neither side attempts to authenticate the other explicitly.  The server
+implicitly authenticates the client by encrypting its key fragment using
+the client's public key.  (This encryption is standard DLIES, and
+security again depends on the Gap \*(DH problem in $G$.) The client
+doesn't attempt to authenticate the server at all, though it can check
+that the response is correct by comparing its hash to a known copy; this
+confirms that the received key fragment is correct, and the client has
+no reason to care where a correct key fragment really came from.
+.hP \*o
+If multiple sources are used, and each knows a fragment chosen uniformly
+at random,, then none of the individual sources has enough information
+to construct the complete key.
+.hP \*o
+Storing a key fragment in a local file means that compromising servers
+doesn't help an adversary obtain the key: the client
+.I must
+be compromised if the adversary is to succeed.
+.hP \*o
+If the client is compromised, and none of the sources has revoked the
+client's access to its fragment, then the game is over and the adversary
+wins.  The client can obviously decrypt the fragments and assemble
+them.  If any source refuses to provide its fragment, the adversary
+learns nothing about the reassembled key.
+.hP \*o
+In practice, high quality entropy is probably in short supply during
+early boot.  If an adversary can guess the ephemeral \*(DH scalar $u$
+having compromised the client, he can potentially decrypt a previously
+captured response.  Periodically rekeying the random number generator
+\(en by rewriting the
+.I seed-file
+when high-quality entropy is available \(en serves to limit the exposure
+to responses captured since the last rekeying.
+.SH BUGS
+For some mysterious reason,
+.BR cryptsetup (8)
+initially rejects a key from
+.BR udpkey ;
+but when the relevant
+.B initramfs
+script retries, everything works.  I'm not sure what's going on here.
+.SH SEE ALSO
+.BR key (1),
+.BR crypttab (5),
+.BR keyring (5),
+.BR cryptsetup (8),
+.BR initramfs-tools (8).
+.SH AUTHOR
+Mark Wooding, <mdw@distorted.org.uk>
diff --git a/udpkey.c b/udpkey.c
new file mode 100644 (file)
index 0000000..3341ff5
--- /dev/null
+++ b/udpkey.c
@@ -0,0 +1,1191 @@
+/* -*-c-*-
+ *
+ * Request a key over UDP, or respond to such a request
+ *
+ * (c) 2012 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of udpkey.
+ *
+ * The udpkey 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * The udpkey 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 udpkey; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <sys/types.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <syslog.h>
+
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <netdb.h>
+
+#include <mLib/alloc.h>
+#include <mLib/buf.h>
+#include <mLib/daemonize.h>
+#include <mLib/dstr.h>
+#include <mLib/fdflags.h>
+#include <mLib/fwatch.h>
+#include <mLib/hex.h>
+#include <mLib/mdwopt.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+#include <mLib/sub.h>
+#include <mLib/tv.h>
+
+#include <catacomb/buf.h>
+#include <catacomb/dh.h>
+#include <catacomb/ec.h>
+#include <catacomb/ec-keys.h>
+#include <catacomb/gcipher.h>
+#include <catacomb/gmac.h>
+#include <catacomb/group.h>
+#include <catacomb/key.h>
+#include <catacomb/mp.h>
+#include <catacomb/mprand.h>
+#include <catacomb/noise.h>
+#include <catacomb/rand.h>
+
+#include <catacomb/rijndael-counter.h>
+#include <catacomb/sha256.h>
+
+#ifdef DEBUG
+#  define D(x) x
+#else
+#  define D(x)
+#endif
+
+/*---- Static variables ---------------------------------------------------*/
+
+static unsigned flags = 0;
+#define f_bogus 1u
+#define f_listen 2u
+#define f_daemon 4u
+#define f_syslog 8u
+
+#define BUFSZ 65536
+static unsigned char ibuf[BUFSZ], obuf[BUFSZ];
+
+static key_file *kf;
+static const char *kfname = "keyring";
+static const char *pidfile;
+static fwatch kfwatch;
+static unsigned nq;
+
+/*----- Miscellaneous utilities -------------------------------------------*/
+
+/* Resolve NAME, storing the address in *ADDR.  Exit on error. */
+static void resolve(const char *name, struct in_addr *addr)
+{
+  struct hostent *h;
+
+  if ((h = gethostbyname(name)) == 0)
+    die(1, "failed to resolve `%s': %s", name, hstrerror(h_errno));
+  if (h->h_addrtype != AF_INET)
+    die(1, "unexpected address type %d", h->h_addrtype);
+  memcpy(addr, h->h_addr, sizeof(struct in_addr));
+}
+
+/* Convert PORT to a port number (in host byte order).  Exit on error. */
+static unsigned short getport(const char *port)
+{
+  unsigned long i = 0;
+  char *q;
+  int e = errno;
+
+  errno = 0;
+  if (!isdigit(*port) ||
+      (i = strtoul(port, &q, 0)) == 0 ||
+      i >= 65536 || *q || errno)
+    die(1, "invalid port number `%s'", port);
+  errno = e;
+  return ((unsigned short)i);
+}
+
+/* Read the file named by NAME into a buffer -- or at least an initial
+ * portion of it; set *P to the start and *SZ to the length.  Return -1 if it
+ * didn't work.  The buffer doesn't need to be freed: the data is stashed in
+ * ibuf.
+ */
+static int snarf(const char *name, void **p, size_t *sz)
+{
+  ssize_t n;
+  int fd;
+
+  if ((fd = open(name, O_RDONLY)) < 0) return (-1);
+  n = read(fd, ibuf, sizeof(ibuf));
+  close(fd);
+  if (n < 0) return (-1);
+  *p = ibuf; *sz = n;
+  return (0);
+}
+
+/* Complain about something.  If f_syslog is set then complain to that;
+ * otherwise write to stderr.  Don't use `%m' because that won't work when
+ * writing to stderr.
+ */
+static void complain(int sev, const char *msg, ...)
+{
+  va_list ap;
+
+  va_start(ap, msg);
+  if (flags & f_syslog)
+    vsyslog(sev, msg, ap);
+  else {
+    fprintf(stderr, "%s: ", QUIS);
+    vfprintf(stderr, msg, ap);
+    fputc('\n', stderr);
+  }
+}
+
+/*----- Reading key data --------------------------------------------------*/
+
+struct kinfo {
+  group *g;
+  ge *X;
+  mp *x;
+  const gccipher *cc;
+  const gcmac *mc; size_t tagsz;
+  const gchash *hc;
+};
+
+/* Clear a kinfo structure so it can be freed without trouble. */
+static void k_init(struct kinfo *k) { k->g = 0; k->x = 0; k->X = 0; }
+
+/* Free a kinfo structure. This is safe on any initialized kinfo
+ * structure.
+ */
+static void k_free(struct kinfo *k)
+{
+  if (k->X) { G_DESTROY(k->g, k->X); k->X = 0; }
+  if (k->x) { MP_DROP(k->x); k->x = 0; }
+  if (k->g) { G_DESTROYGROUP(k->g); k->g = 0; }
+}
+
+/* Empty macro arguments are forbidden.  But arguments are expended during
+ * replacement, not while the call is being processed, so this hack is OK.
+ * Unfortunately, if a potentially empty argument is passed on to another
+ * macro then it needs to be guarded with a use of EMPTY too...
+ */
+#define EMPTY
+
+/* Table of key types.  Entries have the form
+ *
+ *     _(name, NAME, SETGROUP, SETPRIV, SETPUB)
+ *
+ * The name and NAME are lower- and uppercase names for the type used for
+ * constructing various type name constant names.  The code fragment SETGROUP
+ * initializes k->g given the name_{pub,priv} structure in p; SETPRIV and
+ * SETPUB set up k->x and k->X respectively.  (In this last case, k->X will
+ * have been created as a group element already.)
+ */
+#define KEYTYPES(_)                                                    \
+                                                                       \
+  _(dh, DH,                                                            \
+    { k->g = group_prime(&p.dp); },                                    \
+    { k->x = MP_COPY(p.x); },                                          \
+    { if (G_FROMINT(k->g, k->X, p.y)) {                                        \
+       complain(LOG_ERR, "bad public key in `%s'", t->buf);            \
+       goto fail;                                                      \
+      }                                                                        \
+    })                                                                 \
+                                                                       \
+  _(ec, EC,                                                            \
+    { ec_info ei; const char *e;                                       \
+      if ((e = ec_getinfo(&ei, p.cstr)) != 0) {                                \
+       complain(LOG_ERR, "bad elliptic curve in `%s': %s", t->buf, e); \
+       goto fail;                                                      \
+      }                                                                        \
+      k->g = group_ec(&ei);                                            \
+    },                                                                 \
+    { k->x = MP_COPY(p.x); },                                          \
+    { if (G_FROMEC(k->g, k->X, &p.p)) {                                        \
+       complain(LOG_ERR, "bad public point in `%s'", t->buf);          \
+       goto fail;                                                      \
+      }                                                                        \
+    })
+
+/* Define load_tywhich, where which is `pub' or `priv', to load a public or
+ * private key.  Other parameters are as for the KEYTYPES list above.
+ */
+#define KLOAD(ty, TY, which, WHICH, setgroup, setpriv, setpub)         \
+static int load_##ty##which(key_data *kd, struct kinfo *k, dstr *t)    \
+{                                                                      \
+  key_packstruct kps[TY##_##WHICH##FETCHSZ];                           \
+  key_packdef *kp;                                                     \
+  ty##_##which p;                                                      \
+  int rc;                                                              \
+                                                                       \
+  /* Extract the key data from the keydata. */                         \
+  kp = key_fetchinit(ty##_##which##fetch, kps, &p);                    \
+  if ((rc = key_unpack(kp, kd, t)) != 0) {                             \
+    complain(LOG_ERR, "failed to unpack key `%s': %s",                 \
+            t->buf, key_strerror(rc));                                 \
+    goto fail;                                                         \
+  }                                                                    \
+                                                                       \
+  /* Extract the components as abstract group elements. */             \
+  setgroup;                                                            \
+  setpriv;                                                             \
+  k->X = G_CREATE(k->g);                                               \
+  setpub;                                                              \
+                                                                       \
+  /* Dispose of stuff we don't need. */                                        \
+  key_fetchdone(kp);                                                   \
+  return (0);                                                          \
+                                                                       \
+  /* Tidy up after mishaps. */                                         \
+fail:                                                                  \
+  k_free(k);                                                           \
+  key_fetchdone(kp);                                                   \
+  return (-1);                                                         \
+}
+
+/* Map over the KEYTYPES to declare the load_tywhich functions using KLOAD
+ * above.
+ */
+#define KEYTYPE_KLOAD(ty, TY, setgroup, setpriv, setpub)               \
+  KLOAD(ty, TY, priv, PRIV, setgroup, setpriv,                         \
+       { G_EXP(k->g, k->X, k->g->g, k->x); })                          \
+  KLOAD(ty, TY, pub, PUB, setgroup, { }, setpub)
+KEYTYPES(KEYTYPE_KLOAD)
+
+/* Define a table of group key-loading operations. */
+struct kload_ops {
+  const char *name;
+  int (*loadpriv)(key_data *, struct kinfo *, dstr *);
+  int (*loadpub)(key_data *, struct kinfo *, dstr *);
+};
+
+static const struct kload_ops kload_ops[] = {
+#define KEYTYPE_OPS(ty, TY, setgroup, setpriv, setpub)                 \
+  { #ty, load_##ty##priv, load_##ty##pub },
+KEYTYPES(KEYTYPE_OPS)
+  { 0 }
+};
+
+/* Load a private or public (indicated by PRIVP) key named TAG into a kinfo
+ * structure K.  Also fill in the cipher suite selections extracted from the
+ * key attributes.
+ */
+static int loadkey(const char *tag, struct kinfo *k, int privp)
+{
+  const struct kload_ops *ops;
+  dstr d = DSTR_INIT, dd = DSTR_INIT;
+  key *ky;
+  key_data **kd;
+  const char *ty, *p;
+  char *q;
+  int tsz;
+  int rc;
+
+  /* Find the key data. */
+  if (key_qtag(kf, tag, &d, &ky, &kd)) {
+    complain(LOG_ERR, "unknown key tag `%s'", tag);
+    goto fail;
+  }
+
+  /* Find the key's group type and locate the group operations. */
+  ty = key_getattr(kf, ky, "group");
+  if (!ty && strncmp(ky->type, "udpkey-", 7) == 0) ty = ky->type + 7;
+  if (!ty) {
+    complain(LOG_ERR, "no group type for key %s", d.buf);
+    goto fail;
+  }
+  for (ops = kload_ops; ops->name; ops++) {
+    if (strcmp(ty, ops->name) == 0)
+      goto found;
+  }
+  complain(LOG_ERR, "unknown group type `%s' in key %s", ty, d.buf);
+  goto fail;
+
+found:
+  /* Extract the key data into an appropriately abstract form. */
+  k->g = 0; k->x = 0; k->X = 0;
+  if ((rc = (privp ? ops->loadpriv : ops->loadpub)(*kd, k, &d)) != 0)
+    goto fail;
+
+  /* Extract the chosen symmetric cipher. */
+  if ((p = key_getattr(kf, ky, "cipher")) == 0)
+    k->cc = &rijndael_counter;
+  else if ((k->cc = gcipher_byname(p)) == 0) {
+    complain(LOG_ERR, "unknown cipher `%s' in key %s", p, d.buf);
+    goto fail;
+  }
+
+  /* And the chosen hash function. */
+  if ((p = key_getattr(kf, ky, "hash")) == 0)
+    k->hc = &sha256;
+  else if ((k->hc = ghash_byname(p)) == 0) {
+    complain(LOG_ERR, "unknown hash `%s' in key %s", p, d.buf);
+    goto fail;
+  }
+
+  /* And finally a MAC.  This is more fiddly because we must handle (a)
+   * truncation and (b) defaulting based on the hash.
+   */
+  if ((p = key_getattr(kf, ky, "mac")) == 0)
+    dstr_putf(&dd, "%s-hmac", k->hc->name);
+  else
+    dstr_puts(&dd, p);
+  if ((q = strchr(dd.buf, '/')) != 0) *q++ = 0;
+  else q = 0;
+  if ((k->mc = gmac_byname(dd.buf)) == 0) {
+    complain(LOG_ERR, "unknown mac `%s' in key %s", dd.buf, d.buf);
+    goto fail;
+  }
+  if (!q)
+    k->tagsz = k->mc->hashsz/2;
+  else {
+    tsz = atoi(q);
+    if (tsz <= 0 || tsz%8 || tsz/8 > k->mc->hashsz) {
+      complain(LOG_ERR, "bad tag size for mac `%s' in key %s",
+              q, k->mc->name, d.buf);
+      goto fail;
+    }
+    k->tagsz = tsz/8;
+  }
+
+  /* Done. */
+  rc = 0;
+  goto done;
+
+fail:
+  rc = -1;
+done:
+  dstr_destroy(&d);
+  dstr_destroy(&dd);
+  return (rc);
+}
+
+static void keymoan(const char *file, int line, const char *err, void *p)
+  { complain(LOG_ERR, "%s:%d: %s", file, line, err); }
+
+/* Update the keyring `kf' if the file has been changed since we last looked.
+ */
+static void kfupdate(void)
+{
+  key_file *kfnew;
+
+  if (!fwatch_update(&kfwatch, kfname)) return;
+  kfnew = CREATE(key_file);
+  if (key_open(kfnew, kfname, KOPEN_READ, keymoan, 0)) {
+    DESTROY(kfnew);
+    return;
+  }
+  key_close(kf);
+  DESTROY(kf);
+  kf = kfnew;
+}
+
+/*----- Low-level crypto operations ---------------------------------------*/
+
+/* Derive a key, writing its address to *KK and size to *N.  The size is
+ * compatible with the keysz rules KSZ.  It is generated for the purpose of
+ * keying a WHAT (used for key separation and in error messages), and NAME is
+ * the name of the specific instance (e.g., `twofish-counter') from the class
+ * name.  The kinfo structure K tells us which algorithms to use for the
+ * derivation.  The group elements U and Z are the cryptographic inputs
+ * for the derivation.
+ *
+ * Basically all we do is compute H(what || U || Z).
+ */
+static int derive(struct kinfo *k, ge *U, ge *Z,
+                 const char *what, const char *name, const octet *ksz,
+                 octet **kk, size_t *n)
+{
+  buf b;
+  ghash *h;
+  octet *p;
+
+  /* Find a suitable key size. */
+  if ((*n = keysz(k->hc->hashsz, ksz)) == 0) {
+    complain(LOG_ERR,
+            "failed to find suitable key size for %s `%s' and hash `%s'",
+            what, name, k->hc->name);
+    return (-1);
+  }
+
+  /* Build the hash preimage. */
+  buf_init(&b, obuf, sizeof(obuf));
+  buf_put(&b, "udpkey-", 7);
+  buf_putstrz(&b, what);
+  G_TORAW(k->g, &b, U);
+  G_TORAW(k->g, &b, Z);
+  if (BBAD(&b)) {
+    complain(LOG_ERR, "overflow while deriving key (prepare preimage)!");
+    return (-1);
+  }
+
+  /* Derive the output key. */
+  h = GH_INIT(k->hc);
+  GH_HASH(h, BBASE(&b), BLEN(&b));
+  buf_init(&b, obuf, sizeof(obuf));
+  if ((p = buf_get(&b, h->ops->c->hashsz)) == 0) {
+    complain(LOG_ERR, "overflow while deriving key (output hash)!");
+    GH_DESTROY(h);
+    return (-1);
+  }
+  GH_DONE(h, p);
+  GH_DESTROY(h);
+  *kk = p;
+  return (0);
+}
+
+#ifdef DEBUG
+static void debug_mp(const char *what, mp *x)
+  { fprintf(stderr, "%s: *** ", QUIS); MP_EPRINT(what, x); }
+static void debug_ge(const char *what, group *g, ge *X)
+{
+  fprintf(stderr, "%s: *** %s = ", QUIS, what);
+  group_writefile(g, X, stderr);
+  fputc('\n', stderr);
+}
+#endif
+
+/*----- Listening for requests --------------------------------------------*/
+
+/* Rate limiting parameters.
+ *
+ * There's a probabilistic rate-limiting mechanism.  A counter starts at 0.
+ * Every time we oricess a request, we increment the counter.  The counter
+ * drops by RATE_REFILL every second.  If the counter is below RATE_CREDIT
+ * then the request is processed; otherwise it is processed with probability
+ * 1/(counter - RATE_CREDIT).
+ */
+#define RATE_REFILL 10                 /* Credits per second. */
+#define RATE_CREDIT 1000               /* Initial credit. */
+
+static int dolisten(int argc, char *argv[])
+{
+  int sk;
+  char *p, *q, ch;
+  const char *pp;
+  char *aspec;
+  ssize_t n;
+  size_t sz;
+  fd_set fdin;
+  struct sockaddr_in sin;
+  struct in_addr in;
+  int mlen;
+  socklen_t len;
+  buf bin, bout;
+  dstr d = DSTR_INIT, dd = DSTR_INIT;
+  FILE *fp = 0;
+  key *ky;
+  key_data **kkd;
+  mp *r = MP_NEW, *v = MP_NEW;
+  ge *R = 0, *U = 0, *V = 0, *W = 0, *Y = 0, *Z = 0;
+  ghash *h = 0;
+  gmac *m = 0;
+  gcipher *c = 0;
+  octet *kk, *t, *tt;
+  size_t ksz;
+  struct kinfo k;
+  unsigned bucket = 0, toks;
+  time_t last = 0, now;
+
+  /* Set up the socket address. */
+  sin.sin_family = AF_INET;
+  aspec = xstrdup(argv[0]);
+  if ((p = strchr(aspec, ':')) == 0) {
+    p = aspec;
+    sin.sin_addr.s_addr = INADDR_ANY;
+  } else {
+    *p++ = 0;
+    resolve(aspec, &sin.sin_addr);
+  }
+  sin.sin_port = htons(getport(p));
+
+  /* Create and set up the socket itself. */
+  if ((sk = socket(PF_INET, SOCK_DGRAM, 0)) < 0 ||
+      fdflags(sk, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC) ||
+      bind(sk, (struct sockaddr *)&sin, sizeof(sin)))
+    die(1, "failed to create socket: %s", strerror(errno));
+
+  /* That's enough initialization.  If we should fork, then do that. */
+  if (flags & f_daemon) {
+    if (pidfile && (fp = fopen(pidfile, "w")) == 0)
+      die(1, "failed to open pidfile `%s': %s", pidfile, strerror(errno));
+    openlog(QUIS, LOG_PID, LOG_DAEMON);
+    if (daemonize())
+      die(1, "failed to become background process: %s", strerror(errno));
+    if (pidfile) { fprintf(fp, "%ld\n", (long)getpid()); fclose(fp); }
+    flags |= f_syslog;
+  }
+
+  for (;;) {
+
+    /* Clear out the key state. */
+    k_init(&k);
+
+    /* Wait for something to happen. */
+    FD_ZERO(&fdin);
+    FD_SET(sk, &fdin);
+    if (select(sk + 1, &fdin, 0, 0, 0) < 0)
+      die(1, "select failed: %s", strerror(errno));
+    noise_timer(RAND_GLOBAL);
+
+    /* Fetch a packet. */
+    len = sizeof(sin);
+    n = recvfrom(sk, ibuf, sizeof(ibuf), 0, (struct sockaddr *)&sin, &len);
+    if (n < 0) {
+      if (errno != EAGAIN && errno != EINTR)
+       complain(LOG_ERR, "unexpected receive error: %s", strerror(errno));
+      goto again;
+    }
+
+    /* Refill the bucket, and see whether we should reject this packet. */
+    now = time(0);
+    if (bucket && now != last) {
+      toks = (now - last)*RATE_REFILL;
+      bucket = bucket < toks ? 0 : bucket - toks;
+    }
+    last = now;
+    if (bucket > RATE_CREDIT &&
+       grand_range(&rand_global, bucket - RATE_CREDIT))
+      goto again;
+    bucket++;
+
+    /* Set up the input buffer for parsing the request. */
+    buf_init(&bin, ibuf, n);
+
+    /* Extract the key tag name. */
+    if ((p = buf_getmemz(&bin, &sz)) == 0) {
+      complain(LOG_WARNING, "invalid key tag from %s:%d",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+      goto again;
+    }
+
+    /* Find the key. */
+    kfupdate();
+    if (key_qtag(kf, p, &d, &ky, &kkd)) {
+      complain(LOG_WARNING, "unknown key tag `%s' from %s:%d",
+              p, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+      goto again;
+    }
+
+    /* And make sure that it has the right shape. */
+    if ((ky->k->e & KF_ENCMASK) != KENC_BINARY) {
+      complain(LOG_ERR, "key %s is not plain binary data", d.buf);
+      goto again;
+    }
+
+    /* Find the list of clients, and look up the caller's address in the
+     * list.  Entries have the form ADDRESS[/LEN][=TAG] and are separated by
+     * `;'.
+     */
+    if ((pp = key_getattr(kf, ky, "clients")) == 0) {
+      complain(LOG_WARNING,
+              "key %s requested from %s:%d has no `clients' attribute",
+              d.buf, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+      goto again;
+    }
+    dstr_puts(&dd, pp);
+    p = dd.buf;
+    while (*p) {
+      q = p;
+      while (isdigit((unsigned char)*q) || *q == '.') q++;
+      ch = *q; *q++ = 0;
+      if (!inet_aton(p, &in)) goto skip;
+      if (ch != '/')
+       mlen = 32;
+      else {
+       p = q;
+       while (isdigit((unsigned char)*q)) q++;
+       ch = *q; *q++ = 0;
+       mlen = atoi(p);
+      }
+      if (((sin.sin_addr.s_addr ^ in.s_addr) &
+          (0xffffffff << (32 - mlen))) == 0)
+       goto match;
+    skip:
+      if (!ch) break;
+      p = q;
+      while (*p && *p != ';') p++;
+      if (*p) p++;
+    }
+    complain(LOG_WARNING, "access to key %s denied to %s:%d",
+            d.buf, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+    goto again;
+
+  match:
+    /* Build a tag name for the caller's KEM key, either from the client
+     * match or the source address.
+     */
+    if (ch != '=') {
+      DRESET(&dd);
+      dstr_puts(&dd, "client-");
+      dstr_puts(&dd, inet_ntoa(sin.sin_addr));
+      p = dd.buf;
+    } else {
+      p = q;
+      while (*q && *q != ';') q++;
+      if (*q == ';') *q++ = 0;
+    }
+
+    /* Report the match. */
+    complain(LOG_NOTICE, "client %s:%d (`%s') requests key %s",
+            inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), p, d.buf);
+
+    /* Load the KEM key. */
+    if (loadkey(p, &k, 0)) goto again;
+    D( debug_ge("X", k.g, k.X); )
+
+    /* Read the caller's ephemeral key. */
+    R = G_CREATE(k.g); W = G_CREATE(k.g);
+    U = G_CREATE(k.g); V = G_CREATE(k.g);
+    Y = G_CREATE(k.g); Z = G_CREATE(k.g);
+    if (G_FROMBUF(k.g, &bin, U)) {
+      complain(LOG_WARNING, "failed to read ephemeral vector from %s:%d",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+      goto again;
+    }
+    D( debug_ge("U", k.g, U); )
+    if (BLEFT(&bin)) {
+      complain(LOG_WARNING, "trailing junk in request from %s:%d",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+      goto again;
+    }
+
+    /* Ephemeral Diffie--Hellman.  Choose v in GF(q) at random; compute
+     * V = v P and -Y = (-v) U.
+     */
+    v = mprand_range(v, k.g->r, &rand_global, 0);
+    G_EXP(k.g, V, k.g->g, v);
+    D( debug_mp("v", v); debug_ge("V", k.g, V); )
+    v = mp_sub(v, k.g->r, v);
+    G_EXP(k.g, Y, U, v);
+    D( debug_ge("-Y", k.g, Y); )
+
+    /* DLIES.  Choose r in GF(q) at random; compute R = r P and Z = r X.
+     * Mask the clue R as W = R - Y.  (Doing the subtraction here makes life
+     * easier at the other end, since we can determine -Y by negating v
+     * whereas the recipient must subtract vectors which may be less
+     * efficient.)
+     */
+    r = mprand_range(r, k.g->r, &rand_global, 0);
+    G_EXP(k.g, R, k.g->g, r);
+    D( debug_mp("r", r); debug_ge("R", k.g, R); )
+    G_EXP(k.g, Z, k.X, r);
+    G_MUL(k.g, W, R, Y);
+    D( debug_ge("Z", k.g, Z); debug_ge("W", k.g, W); )
+
+    /* Derive encryption and integrity keys. */
+    derive(&k, R, Z, "cipher", k.cc->name, k.cc->keysz, &kk, &ksz);
+    c = GC_INIT(k.cc, kk, ksz);
+    derive(&k, R, Z, "mac", k.mc->name, k.mc->keysz, &kk, &ksz);
+    m = GM_KEY(k.mc, kk, ksz);
+
+    /* Build the ciphertext and compute a MAC tag over it. */
+    buf_init(&bout, obuf, sizeof(obuf));
+    if (G_TOBUF(k.g, &bout, V) ||
+       G_TOBUF(k.g, &bout, W))
+      goto bad;
+    if ((t = buf_get(&bout, k.tagsz)) == 0) goto bad;
+    sz = ky->k->u.k.sz;
+    if (BENSURE(&bout, sz)) goto bad;
+    GC_ENCRYPT(c, ky->k->u.k.k, BCUR(&bout), sz);
+    h = GM_INIT(m);
+    GH_HASH(h, BCUR(&bout), sz);
+    tt = GH_DONE(h, 0); memcpy(t, tt, k.tagsz);
+    BSTEP(&bout, sz);
+
+    /* Send the reply packet back to the caller. */
+    if (sendto(sk, BBASE(&bout), BLEN(&bout), 0,
+              (struct sockaddr *)&sin, len) < 0) {
+      complain(LOG_ERR, "failed to send response to %s:%d: %s",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port),
+              strerror(errno));
+      goto again;
+    }
+
+    goto again;
+
+  bad:
+    /* Report a problem building the reply. */
+    complain(LOG_ERR, "failed to construct response to %s:%d",
+            inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+
+  again:
+    /* Free stuff for the next iteration. */
+    DRESET(&d); DRESET(&dd);
+    if (R) { G_DESTROY(k.g, R); R = 0; }
+    if (U) { G_DESTROY(k.g, U); U = 0; }
+    if (V) { G_DESTROY(k.g, V); V = 0; }
+    if (W) { G_DESTROY(k.g, W); W = 0; }
+    if (Y) { G_DESTROY(k.g, Y); Y = 0; }
+    if (Z) { G_DESTROY(k.g, Z); Z = 0; }
+    if (c) { GC_DESTROY(c); c = 0; }
+    if (m) { GM_DESTROY(m); m = 0; }
+    if (h) { GH_DESTROY(h); h = 0; }
+    k_free(&k);
+  }
+
+  return (-1);
+}
+
+/*----- Sending requests and processing responses -------------------------*/
+
+struct query {
+  struct query *next;
+  octet *k;
+  size_t sz;
+  struct server *s;
+};
+
+struct server {
+  struct server *next;
+  struct sockaddr_in sin;
+  struct kinfo k;
+  mp *u;
+  ge *U;
+  octet *h;
+};
+
+/* Record a successful fetch of key material for a query Q.  The data starts
+ * at K and is SZ bytes long.  The data is copied: it's safe to overwrite it.
+ */
+static void donequery(struct query *q, const void *k, size_t sz)
+  { q->k = xmalloc(sz); memcpy(q->k, k, sz); q->sz = sz; nq--; }
+
+/* Initialize a query to a remote server. */
+static struct query *qinit_net(const char *tag, const char *spec)
+{
+  struct query *q;
+  struct server *s, **stail;
+  dstr d = DSTR_INIT, dd = DSTR_INIT;
+  hex_ctx hc;
+  char *p, *pp, ch;
+
+  /* Allocate the query block. */
+  q = CREATE(struct query);
+  stail = &q->s;
+
+  /* Put the spec somewhere we can hack at it. */
+  dstr_puts(&d, spec);
+  p = d.buf;
+
+  /* Parse the query spec.  Entries have the form ADDRESS:PORT[=TAG][#HASH]
+   * and are separated by `;'.
+   */
+  while (*p) {
+
+    /* Allocate a new server node. */
+    s = CREATE(struct server);
+    s->sin.sin_family = AF_INET;
+
+    /* Extract the server address. */
+    if ((pp = strchr(p, ':')) == 0)
+      die(1, "invalid syntax: missing `:PORT'");
+    *pp++ = 0;
+    resolve(p, &s->sin.sin_addr);
+
+    /* Extract the port number. */
+    p = pp;
+    while (isdigit((unsigned char)*pp)) pp++;
+    ch = *pp; *pp++ = 0;
+    s->sin.sin_port = htons(getport(p));
+
+    /* If there's a key tag then extract that; otherwise use a default. */
+    if (ch != '=')
+      p = "udpkey-kem";
+    else {
+      p = pp;
+      pp += strcspn(pp, ";#");
+      ch = *pp; *pp++ = 0;
+    }
+    if (loadkey(p, &s->k, 1)) exit(1);
+    D( debug_mp("x", s->k.x); debug_ge("X", s->k.g, s->k.X); )
+
+    /* Choose an ephemeral private key u.  Let x be our private key.  We
+     * compute U = u P and transmit this.
+     */
+    s->u = mprand_range(MP_NEW, s->k.g->r, &rand_global, 0);
+    s->U = G_CREATE(s->k.g);
+    G_EXP(s->k.g, s->U, s->k.g->g, s->u);
+    D( debug_mp("u", s->u); debug_ge("U", s->k.g, s->U); )
+
+    /* Link the server on. */
+    *stail = s; stail = &s->next;
+
+    /* If there's a trailing hash then extract it. */
+    if (ch != '#')
+      s->h = 0;
+    else {
+      p = pp;
+      while (*pp == '-' || isxdigit((unsigned char)*pp)) pp++;
+      hex_init(&hc);
+      DRESET(&dd);
+      hex_decode(&hc, p, pp - p, &dd);
+      if (dd.len != s->k.hc->hashsz) die(1, "incorrect hash length");
+      s->h = xmalloc(dd.len);
+      memcpy(s->h, dd.buf, dd.len);
+      ch = *pp++;
+    }
+
+    /* If there are more servers, then continue parsing. */
+    if (!ch) break;
+    else if (ch != ';') die(1, "invalid syntax: expected `;'");
+    p = pp;
+  }
+
+  /* Terminate the server list and return. */
+  *stail = 0;
+  q->k = 0;
+  dstr_destroy(&d);
+  dstr_destroy(&dd);
+  return (q);
+}
+
+/* Handle a `query' to a local file. */
+static struct query *qinit_file(const char *tag, const char *file)
+{
+  struct query *q;
+  void *k;
+  size_t sz;
+
+  /* Snarf the file. */
+  q = CREATE(struct query);
+  if (snarf(file, &k, &sz))
+    die(1, "failed to read `%s': %s", file, strerror(errno));
+  q->s = 0;
+  donequery(q, k, sz);
+  return (q);
+}
+
+/* Reransmission and timeout parameters. */
+#define TO_NEXT(t) (((t) + 2)*4/3)     /* Timeout growth function */
+#define TO_MAX 30                      /* When to give up */
+
+static int doquery(int argc, char *argv[])
+{
+  struct query *q = 0, *qq, **qtail = &qq;
+  struct server *s = 0;
+  const char *tag = argv[0];
+  octet *p;
+  int i;
+  int sk;
+  fd_set fdin;
+  struct timeval now, when, tv;
+  struct sockaddr_in sin;
+  ge *R, *V = 0, *W = 0, *Y = 0, *Z = 0;
+  octet *kk, *t, *tt;
+  gcipher *c = 0;
+  gmac *m = 0;
+  ghash *h = 0;
+  socklen_t len;
+  unsigned next = 0;
+  buf bin, bout;
+  size_t n, j, ksz;
+  ssize_t nn;
+
+  /* Create a socket.  We just use the one socket for everything.  We don't
+   * care which port we get allocated.
+   */
+  if ((sk = socket(PF_INET, SOCK_DGRAM, 0)) < 0 ||
+      fdflags(sk, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC))
+    die(1, "failed to create socket: %s", strerror(errno));
+
+  /* Parse the query target specifications.  The adjustments of `nq' aren't
+   * in the right order but that doesn't matter.
+   */
+  for (i = 1; i < argc; i++) {
+    if (*argv[i] == '.' || *argv[i] == '/') q = qinit_file(tag, argv[i]);
+    else if (strchr(argv[i], ':')) q = qinit_net(tag, argv[i]);
+    else die(1, "unrecognized query target `%s'", argv[i]);
+    *qtail = q; qtail = &q->next; nq++;
+  }
+  *qtail = 0;
+
+  /* Find the current time so we can compute retransmission times properly.
+   */
+  gettimeofday(&now, 0);
+  when = now;
+
+  /* Continue retransmitting until we have all the answers. */
+  while (nq) {
+
+    /* Work out when we next want to wake up. */
+    if (TV_CMP(&now, >=, &when)) {
+      do {
+       if (next >= TO_MAX) die(1, "no responses: giving up");
+       next = TO_NEXT(next);
+       TV_ADDL(&when, &when, next, 0);
+      } while (TV_CMP(&when, <=, &now));
+      for (q = qq; q; q = q->next) {
+       if (q->k) continue;
+       for (s = q->s; s; s = s->next) {
+         buf_init(&bout, obuf, sizeof(obuf));
+         buf_putstrz(&bout, tag);
+         G_TOBUF(s->k.g, &bout, s->U);
+         if (BBAD(&bout)) {
+           moan("overflow while constructing request!");
+           continue;
+         }
+         sendto(sk, BBASE(&bout), BLEN(&bout), 0,
+                (struct sockaddr *)&s->sin, sizeof(s->sin));
+       }
+      }
+    }
+
+    /* Wait until something interesting happens. */
+    FD_ZERO(&fdin);
+    FD_SET(sk, &fdin);
+    TV_SUB(&tv, &when, &now);
+    if (select(sk + 1, &fdin, 0, 0, &tv) < 0)
+      die(1, "select failed: %s", strerror(errno));
+    gettimeofday(&now, 0);
+
+    /* If we have an input event, process incoming packets. */
+    if (FD_ISSET(sk, &fdin)) {
+      for (;;) {
+
+       /* Read a packet and capture its address. */
+       len = sizeof(sin);
+       nn = recvfrom(sk, ibuf, sizeof(ibuf), 0,
+                     (struct sockaddr *)&sin, &len);
+       if (nn < 0) {
+         if (errno == EAGAIN) break;
+         else if (errno == EINTR) continue;
+         else {
+           moan("error receiving reply: %s", strerror(errno));
+           goto again;
+         }
+       }
+
+       /* Wee whether this corresponds to any of our servers.  Don't just
+        * check the active servers, since this may be late replies caused by
+        * retransmissions or similar.
+        */
+       for (q = qq; q; q = q->next) {
+         for (s = q->s; s; s = s->next) {
+           if (s->sin.sin_addr.s_addr == sin.sin_addr.s_addr &&
+               s->sin.sin_port == sin.sin_port)
+             goto found;
+         }
+       }
+       moan("received reply from unexpected source %s:%d",
+            inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+       goto again;
+
+      found:
+       /* If the query we found has now been satisfied, ignore this packet.
+        */
+       if (q->k) goto again;
+
+       /* Start parsing the reply. */
+       buf_init(&bin, ibuf, nn);
+       R = G_CREATE(s->k.g);
+       V = G_CREATE(s->k.g); W = G_CREATE(s->k.g);
+       Y = G_CREATE(s->k.g); Z = G_CREATE(s->k.g);
+       if (G_FROMBUF(s->k.g, &bin, V)) {
+         moan("invalid Diffie--Hellman vector from %s:%d",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+         goto again;
+       }
+       if (G_FROMBUF(s->k.g, &bin, W)) {
+         moan("invalid clue vector from %s:%d",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+         goto again;
+       }
+       D( debug_ge("V", s->k.g, V); debug_ge("W", s->k.g, W); )
+
+       /* We have V and W from the server; determine Y = u V, R = W + Y and
+        * Z = x R, and then derive the symmetric keys.
+        */
+       G_EXP(s->k.g, Y, V, s->u);
+       G_MUL(s->k.g, R, W, Y);
+       G_EXP(s->k.g, Z, R, s->k.x);
+       D( debug_ge("R", s->k.g, R);
+          debug_ge("Y", s->k.g, Y);
+          debug_ge("Z", s->k.g, Z); )
+       derive(&s->k, R, Z, "cipher", s->k.cc->name, s->k.cc->keysz,
+              &kk, &ksz);
+       c = GC_INIT(s->k.cc, kk, ksz);
+       derive(&s->k, R, Z, "mac", s->k.cc->name, s->k.cc->keysz,
+              &kk, &ksz);
+       m = GM_KEY(s->k.mc, kk, ksz);
+
+       /* Find where the MAC tag is. */
+       if ((t = buf_get(&bin, s->k.tagsz)) == 0) {
+         moan("missing tag from %s:%d",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+         goto again;
+       }
+
+       /* Check the integrity of the ciphertext against the tag. */
+       p = BCUR(&bin); n = BLEFT(&bin);
+       h = GM_INIT(m);
+       GH_HASH(h, p, n);
+       tt = GH_DONE(h, 0);
+       if (memcmp(t, tt, s->k.tagsz) != 0) {
+         moan("incorrect tag from %s:%d",
+              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+         goto again;
+       }
+
+       /* Decrypt the result and declare this server done. */
+       GC_DECRYPT(c, p, p, n);
+       if (s->h) {
+         GH_DESTROY(h);
+         h = GH_INIT(s->k.hc);
+         GH_HASH(h, p, n);
+         tt = GH_DONE(h, 0);
+         if (memcmp(tt, s->h, h->ops->c->hashsz) != 0) {
+           moan("response from %s:%d doesn't match hash",
+                inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+           goto again;
+         }
+       }
+       donequery(q, p, n);
+
+      again:
+       /* Tidy things up for the next run through. */
+       if (R) { G_DESTROY(s->k.g, R); R = 0; }
+       if (V) { G_DESTROY(s->k.g, V); V = 0; }
+       if (W) { G_DESTROY(s->k.g, W); W = 0; }
+       if (Y) { G_DESTROY(s->k.g, Y); Y = 0; }
+       if (Z) { G_DESTROY(s->k.g, Z); Z = 0; }
+       if (c) { GC_DESTROY(c); c = 0; }
+       if (m) { GM_DESTROY(m); m = 0; }
+       if (h) { GH_DESTROY(h); h = 0; }
+      }
+    }
+  }
+
+  /* Check that all of the responses match up and XOR them together. */
+  n = qq->sz;
+  if (n > BUFSZ) die(1, "response too large");
+  memset(obuf, 0, n);
+  for (q = qq; q; q = q->next) {
+    if (!q->k) die(1, "INTERNAL: query not complete");
+    if (q->sz != n) die(1, "inconsistent response sizes");
+    for (j = 0; j < n; j++) obuf[j] ^= q->k[j];
+  }
+
+  /* Write out the completed answer. */
+  p = obuf;
+  while (n) {
+    if ((nn = write(STDOUT_FILENO, p, n)) < 0)
+      die(1, "error writing response: %s", strerror(errno));
+    p += nn; n -= nn;
+  }
+  return (0);
+}
+
+/*----- Main program ------------------------------------------------------*/
+
+static void usage(FILE *fp)
+{
+  pquis(fp, "Usage: \n\
+       $ [-OPTS] LABEL {ADDR:PORT | FILE} ...\n\
+       $ [-OPTS] -l [ADDR:]PORT\n\
+");
+}
+
+static void version(FILE *fp)
+  { pquis(fp, "$, version " VERSION); }
+
+static void help(FILE *fp)
+{
+  version(fp);
+  putc('\n', fp);
+  usage(fp);
+  fputs("\n\
+Options:\n\
+\n\
+  -d, --daemon         Run in the background while listening.\n\
+  -k, --keyring=FILE   Read keys from FILE. [default = `keyring']\n\
+  -l, --listen         Listen for incoming requests and serve keys.\n\
+  -p, --pidfile=FILE   Write process id to FILE if in daemon mode.\n\
+  -r, --random=FILE    Key random number generator with contents of FILE.\n\
+", fp);
+}
+
+int main(int argc, char *argv[])
+{
+  int argmin, argmax;
+  void *k;
+  size_t sz;
+
+  ego(argv[0]);
+  for (;;) {
+    static const struct option opts[] = {
+      { "help",                        0,              0,      'h' },
+      { "version",             0,              0,      'v' },
+      { "usage",               0,              0,      'u' },
+      { "daemon",              0,              0,      'd' },
+      { "keyfile",             OPTF_ARGREQ,    0,      'k' },
+      { "listen",              0,              0,      'l' },
+      { "pidfile",             OPTF_ARGREQ,    0,      'p' },
+      { "random",              OPTF_ARGREQ,    0,      'r' },
+      { 0 }
+    };
+
+    int i = mdwopt(argc, argv, "hvu" "dk:lp:r:", opts, 0, 0, 0);
+    if (i < 0) break;
+
+    switch (i) {
+      case 'h': help(stdout); exit(0);
+      case 'v': version(stdout); exit(0);
+      case 'u': usage(stdout); exit(0);
+
+      case 'd': flags |= f_daemon; break;
+      case 'k': kfname = optarg; break;
+      case 'l': flags |= f_listen; break;
+      case 'p': pidfile = optarg; break;
+      case 'r':
+       if (snarf(optarg, &k, &sz))
+         die(1, "failed to read `%s': %s", optarg, strerror(errno));
+       rand_key(RAND_GLOBAL, k, sz);
+       break;
+
+      default: flags |= f_bogus; break;
+    }
+  }
+
+  argv += optind; argc -= optind;
+  if (flags & f_listen) argmin = argmax = 1;
+  else argmin = 2, argmax = -1;
+  if ((flags & f_bogus) || argc < argmin || (argmax >= 0 && argc > argmax))
+    { usage(stderr); exit(1); }
+
+  fwatch_init(&kfwatch, kfname);
+  kf = CREATE(key_file);
+  if (key_open(kf, kfname, KOPEN_READ, keymoan, 0))
+    die(1, "failed to open keyring file `%s'", kfname);
+
+  rand_noisesrc(RAND_GLOBAL, &noise_source);
+  rand_seed(RAND_GLOBAL, 512);
+
+  if (flags & f_listen) return dolisten(argc, argv);
+  else return doquery(argc, argv);
+}
+
+/*----- That's all, folks -------------------------------------------------*/