From 247f344ab1dcb88890b92fd96258f8872f76acf8 Mon Sep 17 00:00:00 2001 From: Mark Wooding Date: Wed, 16 May 2012 13:33:00 +0100 Subject: [PATCH] Initial version. --- .gitignore | 10 + .links | 2 + .skelrc | 9 + Makefile.am | 54 ++ configure.ac | 54 ++ debian/README.Debian | 50 ++ debian/changelog | 6 + debian/compat | 1 + debian/control | 29 + debian/copyright | 16 + debian/rules | 3 + debian/udpkey.examples | 2 + debian/udpkey.init | 127 +++++ debian/udpkey.initramfs-hook | 18 + debian/udpkey.keyscript | 59 +++ udpkey.1 | 380 ++++++++++++++ udpkey.c | 1191 ++++++++++++++++++++++++++++++++++++++++++ 17 files changed, 2011 insertions(+) create mode 100644 .gitignore create mode 100644 .links create mode 100644 .skelrc create mode 100644 Makefile.am create mode 100644 configure.ac create mode 100644 debian/README.Debian create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/udpkey.examples create mode 100755 debian/udpkey.init create mode 100755 debian/udpkey.initramfs-hook create mode 100755 debian/udpkey.keyscript create mode 100644 udpkey.1 create mode 100644 udpkey.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f3884d --- /dev/null +++ b/.gitignore @@ -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 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 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 index 0000000..402662d --- /dev/null +++ b/Makefile.am @@ -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 index 0000000..0fbf6b7 --- /dev/null +++ b/configure.ac @@ -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 index 0000000..2186835 --- /dev/null +++ b/debian/README.Debian @@ -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 index 0000000..31d9bb1 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,6 @@ +udpkey (1.0.0) experimental; urgency=low + + * Initial version. + + -- Mark Wooding Wed, 16 May 2012 12:37:26 +0100 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..45a4fb7 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..190af54 --- /dev/null +++ b/debian/control @@ -0,0 +1,29 @@ +Source: udpkey +Section: utils +Priority: extra +Maintainer: Mark Wooding +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 index 0000000..726a6cb --- /dev/null +++ b/debian/copyright @@ -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 index 0000000..0c6c61a --- /dev/null +++ b/debian/rules @@ -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 index 0000000..3781e1e --- /dev/null +++ b/debian/udpkey.examples @@ -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 index 0000000..d56575e --- /dev/null +++ b/debian/udpkey.init @@ -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 + +# 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 index 0000000..33be1c4 --- /dev/null +++ b/debian/udpkey.initramfs-hook @@ -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 index 0000000..bdc48a3 --- /dev/null +++ b/debian/udpkey.keyscript @@ -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 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, diff --git a/udpkey.c b/udpkey.c new file mode 100644 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 -------------------------------------------------*/ -- 2.11.0