From c47f2aba7d705252c660ba1ad0931fbb93122d80 Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Sat, 24 Dec 2011 02:29:11 +0000 Subject: [PATCH] Multiple key types, key profiles, and user key storage. Organization: Straylight/Edgeware From: Mark Wooding * Introduce multiple key types (currently GnuPG and Seccure, but maybe more later, e.g., OpenSSL). * Parameters are provided via time-varying profiles. * Profiles can be chosen for keeper and recovery keys. * Allow users to generate and use keys. --- Makefile.am | 52 ++- cryptop.decrypt | 43 +++ cryptop.delkey | 43 +++ cryptop.encrypt | 43 +++ cryptop.genkey | 100 +++++ cryptop.in | 61 +++ cryptop.info | 43 +++ cryptop.public | 47 +++ cryptop.recover | 68 ++++ cryptop.sign | 43 +++ cryptop.verify | 43 +++ extract-profile.in | 465 ++++++++++++++++++++++ keyfunc.sh.in | 617 ++++++++++++++++++++++++++---- keys.conceal | 51 +++ keys.in | 78 +--- keeper-cards => keys.keeper-cards | 13 +- new-keeper => keys.new-keeper | 29 +- new-recov => keys.new-recov | 52 ++- recover => keys.recover | 17 +- reveal => keys.reveal | 93 +++-- stash => keys.stash | 10 +- ktype.gnupg | 150 ++++++++ ktype.seccure | 99 +++++ 23 files changed, 2007 insertions(+), 253 deletions(-) create mode 100644 cryptop.decrypt create mode 100644 cryptop.delkey create mode 100644 cryptop.encrypt create mode 100644 cryptop.genkey create mode 100755 cryptop.in create mode 100644 cryptop.info create mode 100644 cryptop.public create mode 100644 cryptop.recover create mode 100644 cryptop.sign create mode 100644 cryptop.verify create mode 100644 extract-profile.in create mode 100755 keys.conceal rename keeper-cards => keys.keeper-cards (97%) rename new-keeper => keys.new-keeper (77%) rename new-recov => keys.new-recov (79%) rename recover => keys.recover (77%) mode change 100644 => 100755 rename reveal => keys.reveal (61%) rename stash => keys.stash (88%) mode change 100644 => 100755 create mode 100644 ktype.gnupg create mode 100644 ktype.seccure diff --git a/Makefile.am b/Makefile.am index 12168f6..0cd0854 100644 --- a/Makefile.am +++ b/Makefile.am @@ -24,7 +24,9 @@ ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. bin_SCRIPTS = +sbin_SCRIPTS = dist_pkglib_SCRIPTS = +dist_pkglib_DATA = pkglib_DATA = noinst_SCRIPTS = @@ -43,6 +45,7 @@ SUBSTVARS = \ PYTHON="$(PYTHON)" \ bindir="$(bindir)" \ pkgconfdir="$(sysconfdir)/$(PACKAGE)" \ + pkgstatedir="$(localstatedir)/$(PACKAGE)" \ pkglibdir="$(pkglibdir)" SUBST = $(AM_V_GEN)$(confsubst) @@ -58,6 +61,42 @@ shamir: shamir.in Makefile $(SUBST) $(srcdir)/shamir.in $(SUBSTVARS) >shamir.new && \ chmod +x shamir.new && mv shamir.new shamir +## Property expansion. +bin_SCRIPTS += extract-profile +EXTRA_DIST += extract-profile.in +CLEANFILES += extract-profile +extract-profile: extract-profile.in Makefile + $(SUBST) $(srcdir)/extract-profile.in $(SUBSTVARS) \ + >extract-profile.new && \ + chmod +x extract-profile.new && \ + mv extract-profile.new extract-profile + +###-------------------------------------------------------------------------- +### Crypto operations. + +## Main driver program. +sbin_SCRIPTS += cryptop +EXTRA_DIST += cryptop.in +CLEANFILES += cryptop +cryptop: cryptop.in Makefile + $(SUBST) $(srcdir)/cryptop.in $(SUBSTVARS) >cryptop.new && \ + chmod +x cryptop.new && mv cryptop.new cryptop + +## Key type libraries. +dist_pkglib_DATA += ktype.gnupg +dist_pkglib_DATA += ktype.seccure + +## Commands. +dist_pkglib_SCRIPTS += cryptop.genkey +dist_pkglib_SCRIPTS += cryptop.delkey +dist_pkglib_SCRIPTS += cryptop.recover +dist_pkglib_SCRIPTS += cryptop.info +dist_pkglib_SCRIPTS += cryptop.public +dist_pkglib_SCRIPTS += cryptop.encrypt +dist_pkglib_SCRIPTS += cryptop.decrypt +dist_pkglib_SCRIPTS += cryptop.sign +dist_pkglib_SCRIPTS += cryptop.verify + ###-------------------------------------------------------------------------- ### Main driver program and commands. @@ -78,11 +117,12 @@ keyfunc.sh: keyfunc.sh.in Makefile mv keyfunc.sh.new keyfunc.sh ## Commands. -dist_pkglib_SCRIPTS += keeper-cards -dist_pkglib_SCRIPTS += new-keeper -dist_pkglib_SCRIPTS += new-recov -dist_pkglib_SCRIPTS += recover -dist_pkglib_SCRIPTS += reveal -dist_pkglib_SCRIPTS += stash +dist_pkglib_SCRIPTS += keys.conceal +dist_pkglib_SCRIPTS += keys.keeper-cards +dist_pkglib_SCRIPTS += keys.new-keeper +dist_pkglib_SCRIPTS += keys.new-recov +dist_pkglib_SCRIPTS += keys.recover +dist_pkglib_SCRIPTS += keys.reveal +dist_pkglib_SCRIPTS += keys.stash ###----- That's all, folks -------------------------------------------------- diff --git a/cryptop.decrypt b/cryptop.decrypt new file mode 100644 index 0000000..ccc42a8 --- /dev/null +++ b/cryptop.decrypt @@ -0,0 +1,43 @@ +#! /bin/sh +### +### Decrypt data using a user key +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +set -e +case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$quis: invalid owner \`$kowner' for key"; exit 1 ;; +esac +case $forcep in + nil) + if [ -d $kdir ]; then + echo >&2 "$quis: key \`$key' already exists" + exit 1 + fi + ;; +esac + +## Check the profile name. We shouldn't allow users to refer to each +## other's profiles. +case "$profile" in + :*) + label=${profile#:} + ;; + *:*) + powner=${profile%%:*} label=${profile#*:} + case "$powner" in + "$USERV_USER") ;; + *) echo >&2 "$quis: invalid owner \`$powner' for profile"; exit 1 ;; + esac + ;; + *) + label=$profile + ;; +esac +checkword "profile label" "$label" +read_profile $profile + +## Generate the key. +c_genkey $profile $kdir $knub genhook_recov "$@" + +###----- That's all, folks -------------------------------------------------- diff --git a/cryptop.in b/cryptop.in new file mode 100755 index 0000000..3efb554 --- /dev/null +++ b/cryptop.in @@ -0,0 +1,61 @@ +#! /bin/sh +### +### User cryptographic operations +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +set -e +: ${ETC=@pkgconfdir@} +: ${KEYS=@pkgstatedir@} +: ${KEYSLIB=@pkglibdir@} +export ETC KEYS KEYSLIB + +. "$KEYSLIB"/keyfunc.sh + +usage="usage: $quis COMMAND [ARGUMENTS ...]" +prefix=cryptop + +## Fake up caller credentials if not called via userv. +case "${USERV_USER+t}" in + t) ;; + *) USERV_USER=${LOGNAME-${USER-$(id -un)}} USERV_UID=$(id -u) ;; +esac +case "${USERV_GROUP+t}" in + t) ;; + *) USERV_GROUP=$(id -Gn) USERV_GID=$(id -gn) ;; +esac +export USERV_USER USERV_UID USERV_GROUP USERV_GID + +## Parse options. +while getopts "hv" opt; do + case "$opt" in + h) cmd_help; exit ;; + v) version; exit ;; + *) usage_err ;; + esac +done +shift $(( $OPTIND - 1 )) + +## Dispatch. +dispatch "$@" + +###----- That's all, folks -------------------------------------------------- diff --git a/cryptop.info b/cryptop.info new file mode 100644 index 0000000..1d0ff21 --- /dev/null +++ b/cryptop.info @@ -0,0 +1,43 @@ +#! /bin/sh +### +### Show information about a key +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +set -e +case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$quis: \`$key' has no public part" + exit 1 +fi + +###----- That's all, folks -------------------------------------------------- diff --git a/cryptop.recover b/cryptop.recover new file mode 100644 index 0000000..f661569 --- /dev/null +++ b/cryptop.recover @@ -0,0 +1,68 @@ +#! /bin/sh +### +### Recover a user key nub +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +set -e +case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$quis: unknown key \`$key'"; exit 1; fi +checkword "recovery key label" "$recov" + +mktmp +nubid=$(cat $kdir/nubid) +readmeta $kdir +read_profile "$profile" +if [ -f $knub ]; then + nubbin=$(nubid <$knub) + case "$nubbin" in + "$nubid") + echo >&2 "$quis: key \`$key' doesn't need recovery" + exit 1 + ;; + esac +fi + +umask 077 +recover $recov $kowner/$klabel >$knub.new +nubbin=$(nubid <$knub.new) +case "$nubbin" in + "$nubid") ;; + *) + echo >&2 "$quis: recovery produced incorrect nub" + exit 1 + ;; +esac +mv $knub.new $knub + +###----- That's all, folks -------------------------------------------------- diff --git a/cryptop.sign b/cryptop.sign new file mode 100644 index 0000000..38d4e78 --- /dev/null +++ b/cryptop.sign @@ -0,0 +1,43 @@ +#! /bin/sh +### +### Sign data using a user key +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +set -e +case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp < '.join(["`%s'" % s.name for s in path[seen[me]:]]) + + ## Not done this one yet: process my included Sections, and compute the + ## union of their inferiors. + else: + seen[me] = len(path) - 1 + me._visited = me.V_GREY + me.inferiors = set([me]) + for s in me.includes: + s.transit(seen, path) + me.inferiors.update(s.inferiors) + me._visited = me.V_BLACK + + ## Remove myself from the path. + path.pop() + + def inherit(me): + """ + Compute the inherited properties for this Section. + + A Section has an inherited property named P if any inferior has a direct + property named P. The value of the property is determined as follows. + Firstly, determine the set A of all inferiors which have a direct + property P. Secondly, determine a /reduced/ set containing only the + maximal elements of A: if R contains a pair of distinct inferiors I and J + such that I is an inferior of J, then R does not contain I; R contains + all elements A not so excluded. If all inferiors in R define the same + value for the property, then that is the value of the inherited property; + if two inferiors disagree, then the situation is erroneous. + + Note that if a Section defines a direct property then it has an inherited + property with the same value: in this case, the reduced set is a + singleton. + """ + + ## First pass: for each property name, determine the reduced set of + ## inferiors defining that property, and the values they have for it. + ## Here, D maps property names to lists of `prop' records. + d = {} + for s in me.inferiors: + + ## Work through the direct properties of inferior S. + for k, v in s.iteritems(): + + ## Ignore `special' properties. + if k.startswith('@'): + continue + + ## Work through the current reduced set. Discard entries from + ## sections inferior to S. If an entry exists for a section T to + ## which S is inferior, then don't add S itself. + addp = True + pp = [] + try: + for q in d[k]: + if s in q.source.inferiors: + addp = False + if q.source not in s.inferiors: + pp.append(q) + except KeyError: + pass + if addp: + pp.append(prop(value = v, source = s)) + d[k] = pp + + ## Second pass: check that the reduced set defines a unique value for + ## each inherited property. + for k, vv in d.iteritems(): + c = {} + + ## Build in C a dictionary mapping candidate values to lists of + ## inferiors asserting those values. + for p in vv: + c.setdefault(p.value, []).append(p.source) + + ## Now C should have only one key. If not, C records enough + ## information that we can give a useful error report. + if len(c) != 1: + raise UserError, \ + "inconsistent values for property `%s' in section `%s': %s" % \ + (k, me.name, + ''.join(["\n\t`%s' via %s" % + (v, ', '.join(["`%s'" % s.name for s in ss])) + for v, ss in c.iteritems()])) + + ## Insert the computed property value. + me.inherited[k] = c.keys()[0] + + def expand(me, string, seen = None, path = None): + """ + Expand placeholders in STRING and return the result. + + A placeholder has the form $PROP or ${PROP} (the latter syntax identifies + the property name unambiguously), and is replaced by the value of the + (inherited) property named PROP. A token $$ is replaced with a single $. + + The SEEN and PATH parameters work the same way as in the `transit' + method. + """ + + if seen is None: seen = {} + if path is None: path = [] + + ## Prepare stuff for the loop. + out = StringIO() + left = 0 + n = len(string) + + ## Pick out placeholders and expand them. + while True: + + ## Find a placeholder. + dol = string.find('$', left) + + ## None: commit the rest of the string and we're done. + if dol < 0: + out.write(string[left:]) + break + + ## Commit the portion before the placeholder. + out.write(string[left:dol]) + + ## Check for a trailing `$'. After this, we can be sure of at least + ## one more character. + if dol + 1 >= n: + prop = '' + + ## If there's a left brace, find a right brace: the property name is + ## between them. + elif string[dol + 1] == '{': + ace = string.find('}', dol + 2) + if ace < 0: + raise UserError, \ + "invalid placeholder (missing `}') in `%s'" % string + prop = string[dol + 2:ace] + left = ace + 1 + + ## If there's a dollar, just commit it and go round again. + elif string[dol + 1] == '$': + left = dol + 2 + out.write('$') + continue + + ## Otherwise take as many constituent characters as we can. + else: + left = dol + 1 + while left < n and (string[left].isalnum() or string[left] in '-_'): + left += 1 + prop = string[dol + 1:left].replace('-', '_') + + ## If we came up empty, report an error. + if prop == '': + raise UserError, \ + "invalid placeholder (empty name) in `%s'" % string + + ## Extend the path: we're going to do a recursive expansion. + path.append(prop) + + ## Report a cycle if we found one. + if prop in seen: + raise UserError, 'substitution cycle:\n\t%s' % \ + (' -> '.join(["`%s'" % p for p in path[seen[prop]:]])) + + ## Look up the raw value. + try: + value = me.inherited[prop] + except KeyError: + raise UserError, "unknown property `%s'" % prop + + ## Recursively expand, and unwind the PATH and SEEN stuff. + seen[prop] = len(path) - 1 + out.write(me.expand(value, seen, path)) + path.pop() + del seen[prop] + + ## Done: return the accumulated result. + return out.getvalue() + +def link(d): + """ + Link together the Sections in D according to their inclusions. + + If a Section S has an `@include' special property, then set S's `includes' + slot to be the set of sections named in that property's value. Then + compute the inferiors and inherited properties for all of the Sections. + """ + + ## Capture the global section. + g = d['@GLOBAL'] + + ## Walk through all of the sections. + for sect in d.itervalues(): + + ## If this isn't the global section, then add the global section as an + ## implicit inclusion. + if sect is not g: + sect.includes.add(g) + + ## If there are explicit inclusions, then add them to the included set. + try: + inc = sect['@include'] + except KeyError: + pass + else: + for s in inc.split(): + try: + sect.includes.add(d[s]) + except KeyError: + raise UserError, \ + "unknown section `%s' included in `%s'" % (s, sect.name) + + ## Compute the inferiors and inherited properties. + for sect in d.itervalues(): + sect.transit() + for sect in d.itervalues(): + sect.inherit() + +###-------------------------------------------------------------------------- +### Parsing input files. + +## Names of special properties. All of these begin with an `@' sign. +SPECIALS = set(['@include']) + +def parse(filename, d): + """ + Parse a profile file FILENAME, updating dictionary D. + + Each entry in the dictionary maps a section name to the section's contents; + the contents are in turn represented as a dictionary mapping properties to + values. Inter-section references, defaults, and so on are not processed + here. + """ + + sect = '@GLOBAL' + + with open(filename) as f: + n = 0 + for line in f: + n += 1 + line = line.strip() + if not line or line[0] in ';#': + continue + if line[0] == '[' and line[-1] == ']': + sect = line[1:-1] + continue + + ## Parse an assignment. + eq = line.find('=') + colon = line.find(':') + if eq < 0 or 0 <= colon < eq: eq = colon + if eq < 0: raise UserError, '%s:%d: no assignment' % (filename, n) + name, value = line[:eq].strip(), line[eq + 1:].strip() + + ## Check that the name is well-formed. + name = name.replace('-', '_') + if not (name and + (name in SPECIALS or + all(map(lambda ch: ch == '_' or ch.isalnum(), name)))): + raise UserError, "%s:%d: bad name `%s'" % (filename, n, name) + + ## Store the assignment. + try: + d[sect][name] = value + except KeyError: + s = Section(sect) + d[sect] = s + s[name] = value + +###-------------------------------------------------------------------------- +### Main program. + +OP = O.OptionParser( + usage = '%prog FILE|DIRECTORY ... SECTION', + version = '%%prog, version %s' % VERSION, + description = '''\ +Parse the configurations FILE and DIRECTORY contents, and output the named +SECTION as a sequence of simple assignments. +''') + +def main(args): + try: + + ## Check the arguments. + opts, args = OP.parse_args(args[1:]) + if len(args) < 2: + OP.error('not enough positional parameters') + files = args[:-1] + sect = args[-1] + + ## Read in the inputs. + d = { '@GLOBAL': Section('@GLOBAL') } + for f in files: + + ## It's a directory: pick out the files contained. + if OS.path.isdir(f): + for sf in sorted(OS.listdir(f)): + if not all(map(lambda ch: ch in '_-' or ch.isalnum(), sf)): + continue + ff = OS.path.join(f, sf) + if not OS.path.isfile(ff): + continue + parse(ff, d) + + ## Not a directory: just try to parse it. + else: + parse(f, d) + + ## Print the contents. + link(d) + try: + s = d[sect] + except KeyError: + raise UserError, "unknown section `%s'" % sect + for k, v in s.inherited.iteritems(): + print '%s=%s' % (k, s.expand(v)) + + ## Report errors for expected problems. + except UserError, e: + SYS.stderr.write('%s: %s\n' % (OP.get_prog_name(), e.args[0])) + SYS.exit(1) + except OSError, e: + SYS.stderr.write('%s: %s\n' % (OP.get_prog_name(), e.args[1])) + SYS.exit(1) + except IOError, e: + SYS.stderr.write('%s: %s: %s\n' % + (OP.get_prog_name(), e.filename, e.strerror)) + SYS.exit(1) + +if __name__ == '__main__': + main(SYS.argv) + +###----- That's all, folks -------------------------------------------------- diff --git a/keyfunc.sh.in b/keyfunc.sh.in index 70da24a..ca14782 100644 --- a/keyfunc.sh.in +++ b/keyfunc.sh.in @@ -29,13 +29,11 @@ quis=${0##*/} ### Configuration variables. PACKAGE="@PACKAGE@" VERSION="@VERSION@" -pkgconfdir="@pkgconfdir@" pkglibdir="@pkglibdir@" bindir="@bindir@" case ":$PATH:" in *:"$bindir":*) ;; *) PATH=$bindir:$PATH ;; esac -if [ -f $KEYS/keys.conf ]; then . $KEYS/keys.conf; fi -: ${random=/dev/random} +if [ -f $ETC/keys.conf ]; then . $ETC/keys.conf; fi case "${KEYS_DEBUG+t}" in t) set -x ;; esac @@ -43,142 +41,534 @@ case "${KEYS_DEBUG+t}" in t) set -x ;; esac ### Cleanup handling. cleanups="" -cleanup () { cleanups="$cleanups $1"; } -trap 'rc=$?; for i in $cleanups; do $i; done; exit $rc' EXIT -trap 'exit 127' INT TERM +cleanup () { cleanups=${cleanups+$cleanups }$1; } +runcleanups () { for i in $cleanups; do $i; done; } +trap 'rc=$?; runcleanups; exit $rc' EXIT +trap 'trap "" EXIT; runcleanups; exit 127' INT TERM ###-------------------------------------------------------------------------- ### Utility functions. +reqsafe () { + ## Fail unless a safe directory is set. + + err="$quis: (CONFIGURATION ERROR)" + case ${SAFE+t} in + t) ;; + *) echo >&2 "$err: no SAFE directory"; exit 1 ;; + esac + if [ ! -d "$SAFE" ]; then + echo >&2 "$err: SAFE path \`$SAFE' isn't a directory" + exit 1 + fi + case "$SAFE" in + [!/]* | *[][[:space:]*?]*) + echo >&2 "$err: SAFE path \`$SAFE' contains bad characters" + exit 1 + ;; + esac + ls -ld "$SAFE" | { + me=$(id -un) + read perm _ user stuff + case "$perm:$user" in + d???------:"$me") ;; + *) + echo >&2 "$err: SAFE path \`$SAFE' has bad owner or permissions" + exit 1 + ;; + esac + } +} + ## Temporary directory. unset tmp -rmtmp () { cd /; rm -rf $tmp; } +rmtmp () { case ${tmp+t} in t) cd /; rm -rf $tmp ;; esac; } +cleanup rmtmp mktmp () { - ## Make and return the name of a temporary directory. + ## Make a temporary directory and store its name in `tmp'. - case "${tmp+t}" in t) echo "$tmp"; return ;; esac - mem=$(userv root claim-mem-dir &2 "$quis: bad $what \`$thing'" - exit 1 - ;; + case ${tmp+t} in + t) ;; + *) echo >&2 "$quis (INTERNAL): no tmp directory set"; exit 127 ;; esac } -checkword () { - what=$1 thing=$2 - case "$thing" in - "" | *[!-0-9a-zA-Z_!%@+=]*) - echo >&2 "$quis: bad $what: \`$thing'" - exit 1 - ;; +parse_keylabel () { + key=$1 + ## Parse the key label string KEY. Set `kdir' to the base path to use for + ## the key's storage, and `kowner' to the key owner's name. + + case "$key" in + *:*) kowner=${key%%:*} klabel=${key#*:} ;; + *) kowner=$USERV_USER klabel=$key ;; esac + checkword "key owner name" "$kowner" + checklabel "key" "$klabel" + kdir=$KEYS/store/$kowner/$klabel + knub=$KEYS/nub/$kowner/$klabel } -checklabel () { - what=$1 thing=$2 +###-------------------------------------------------------------------------- +### Input validation functions. + +nl=" +" +check () { + ckwhat=$1 ckpat=$2 thing=$3 + ## Verify that THING matches the (anchored, basic) regular expression + ## CKPAT. Since matching newlines is hard to do portably, also check that + ## THING doesn't contain any. If the checks fail, report an error and + ## exit. + + validp=t case "$thing" in - *[!-0-9a-zA-Z_!%@+=/#]* | *//* | /* | */) - echo >&2 "$quis: bad $what label \`$thing'" - exit 1 - ;; + *"$nl"*) validp=nil ;; + *) if ! expr >/dev/null "$thing" : "$ckpat\$"; then validp=nil; fi ;; + esac + case $validp in + nil) echo >&2 "$quis: bad $ckwhat \`$thing'"; exit 1 ;; esac } +## Regular expressions for validating input. +R_IDENTCHARS="A-Za-z0-9_" +R_WORDCHARS="-$R_IDENTCHARS!%@+=" +R_IDENT="[$R_IDENTCHARS][$R_IDENTCHARS]*" +R_WORD="[$R_WORDCHARS][$R_WORDCHARS]*" +R_WORDSEQ="[$R_WORDCHARS[:space:]][$R_WORDCHARS[:space:]]*" +R_NUMERIC='\(\([1-9][0-9]*\)\{0,1\}0\{0,1\}\)' +R_LABEL="\($R_WORD\(/$R_WORD\)*\)" +R_LINE=".*" + +## Various validation functions. +checknumber () { check "$1" "$R_NUMERIC" "$2"; } +checkident () { check "$1" "$R_IDENT" "$2"; } +checkword () { check "$1" "$R_WORD" "$2"; } +checklabel () { check "$1 label" "$R_LABEL" "$2"; } + ###-------------------------------------------------------------------------- -### Crypto operations. -### -### We use Seccure for this, but it's interface is Very Annoying. +### Key storage and properties. + +getsysprofile () { + profile=$1 + ## Write the named system PROFILE to standard output. + + $bindir/extract-profile $ETC/profile.d/ "$profile" +} + +setprops () { + what=$1 prefix=$2; shift 2 + ## Set variables based on the NAME=VALUE assignments in the arguments. The + ## value for property NAME is stored in the shell variable PREFIX_NAME. + + for assg in "$@"; do + goodp=t + case "$assg" in + *\=*) name=${assg%%=*} value=${assg#*=} ;; + *) goodp=nil ;; + esac + case "$goodp,$name" in t,*[!0-9A-Za-z_]*=*) goodp=nil ;; esac + case "$goodp" in + nil) echo >&2 "$quis: bad $what assignment \`$assg'"; exit 1 ;; + esac + eval "$prefix$name=\$value" + done +} -run_seccure () { - op=$1; shift - ## run_seccure OP ARG ... +checkprops () { + whatprop=$1 prefix=$2; shift 2 + ## Check that property variables are set in accordance with the remaining + ## TABLE arguments. Each row of TABLE has the form ## - ## Run a Seccure program, ensuring that its stderr is reported if it had - ## anything very interesting to say, but suppressed if it was boring. + ## NAME OMIT PAT + ## + ## A table row is satisfied if there is a variable PREFIXNAME whose value + ## matces the (basic) regular expression PAT, or if the variable is unset + ## and OMIT is `t'. + + for table in "$@"; do + case "$table" in ?*) ;; *) continue ;; esac + while read -r name omit pat; do + eval foundp=\${$prefix$name+t} + case "$foundp,$omit" in + ,t) continue ;; + ,nil) + echo >&2 "$quis: missing $whatprop \`$name' required" + exit 1 + ;; + esac + eval value=\$$prefix$name + check "value for $whatprop \`$name'" "$pat" "$value" + done <&- + checkprops "property" kprop_ "$g_props" + + ## Fetch the key-type handling library. + if [ ! -f $KEYSLIB/ktype.$kprop_type ]; then + echo >&2 "$quis: unknown key type \`$kprop_type'" + exit 1 + fi + . $KEYSLIB/ktype.$kprop_type + checkprops "property" kprop_ "$k_props" +} + +readmeta () { + kdir=$1 + ## Read key metadata from KDIR. + + { read profile; } <"$kdir"/meta +} + +makenub () { + ## Generate a key nub in the default way, and write it to standard output. + ## The properties `random', `nubsz' and `nubhash' are referred to. + + dd 2>/dev/null \ + if=/dev/${kprop_random-random} bs=1 count=${kprop_nubsz-512} | + openssl dgst -${kprop_nubhash-sha384} -binary | + openssl base64 +} + +nubid () { + ## Compute a hash of the key nub in stdin, and write it to stdout in hex. + ## The property `nubidhash' is used. + + { echo "distorted-keys nubid"; cat -; } | + openssl dgst -${kprop_nubidhash-sha256} +} + +subst () { + what=$1 templ=$2 prefix=$3 pat=$4 + ## Substitute option values into the template TEMPL. Each occurrence of + ## %{VAR} is replaced by the value of the variable PREFIXVAR. Finally, an + ## error is reported unless the final value matches the regular expression + ## PAT. + + out="" + rest=$templ + while :; do + + ## If there are no more markers to substitute, then finish. + case "$rest" in *"%{"*"}"*) ;; *) out=$out$rest; break ;; esac + + ## Split the template into three parts. + left=${rest%%\%\{*} right=${rest#*\%\{} + var=${right%%\}*} rest=${right#*\}} + case "$var" in + *-*) default=${var#*-} var=${var%%-*} defaultp=t ;; + *) defaultp=nil ;; + esac + + ## Find the variable value. + checkident "template variable name" "$var" + eval foundp=\${$prefix$var+t} + case $foundp,$defaultp in + t,*) eval value=\$$prefix$var ;; + ,t) value=$default ;; + *) + echo >&2 "$quis: option \`$var' unset, used in template \`$templ'" + exit 1 + ;; + esac + + ## Do the substitution. + out=$out$left$value + done + + ## Check the final result. + check "$what" "$pat" "$out" + + ## Done. + echo "$out" +} + +read_profile () { + profile=$1 + ## Read property settings from a profile. The PROFILE name has the form + ## [USER:]LABEL. Properties are set using `setprops' with prefix `kprop_'. + + reqtmp + case "$profile" in + :*) + label=${profile#:} uservp=nil + ;; *) - echo >&2 "$quis (INTERNAL): run_seccure called without tmpdir" - exit 127 + user=$USERV_USER label=$profile uservp=t + ;; + *:*) + user=${profile%%:*} label=${profile#*:} uservp=t + ;; + esac + checkword "profile label" "$label" + + ## Fetch the profile settings from the user. + reqtmp + case $uservp in + t) + checkword "profile user" "$user" + userv "$user" cryptop-profile "$label" >$tmp/profile + ;; + nil) + $bindir/extract-profile $ETC/profile.d/ "$label" >$tmp/profile ;; esac - ## Run the program. - set +e; seccure-$op "$@" 2>$tmp/seccure.out; rc=$?; set -e - grep -v '^WARNING: Cannot obtain memory lock' $tmp/seccure.out >&2 || : - return $rc + ## Read the file. + readprops $tmp/profile } -ec_public () { - private=$1 - ## Write the public key corresponding to PRIVATE to stdout. +###-------------------------------------------------------------------------- +### General crypto operations. + +c_genkey () { + profile=$1 kdir=$2 knub=$3 hook=$4; shift 4 + ## Generate a key, and associate it with the named PROFILE (which is + ## assumed already to have been read!); store the main data in KDIR, and + ## the nub separately in the file KNUB; run HOOK after generation, passing + ## it the working key directory and nub file. Remaining arguments are + ## options to the key type. + + ## Set options and check them. + setprops "option" kopt_ "$@" + checkprops "option" kopt_ "$k_genopts" + + ## Create directory structure and start writing metadata. + rm -rf "$kdir.new" + mkdir -m755 -p "$kdir.new" + case "$knub" in */*) mkdir -m700 -p "${knub%/*}" ;; esac + cat >"$kdir.new/meta" <"$knub.new"; umask $umask + k_generate "$kdir.new" "$knub.new" + $hook "$kdir.new" "$knub.new" + + ## Hash the nub. + nubid <"$knub.new" >"$kdir.new/nubid" + + ## Juggle everything into place. Doing this atomically is very difficult, + ## and requires more machinery than I can really justify here. If + ## something goes wrong halfway, it should always be possible to fix it, + ## either by backing out (if $kdir.new still exists) or pressing on + ## forwards (if not). + rm -rf "$kdir.old" + if [ -e "$kdir" ]; then mv "$kdir" "$kdir.old"; fi + mv "$kdir.new" "$kdir" + mv "$knub.new" "$knub" + rm -rf "$kdir.old" } -ec_keygen () { - private=$1 public=$2 - ## Make a new key, write private key to PRIVATE and public key to PUBLIC. - - dd if=$random bs=1 count=512 2>/dev/null | - openssl dgst -sha384 -binary | - (umask 077 && openssl base64 >"$private") - ec_public "$private" >"$public" +c_encrypt () { k_encrypt "$@"; } +c_decrypt () { + if k_decrypt "$@" >$tmp/plain; then cat $tmp/plain + else return $? + fi +} +c_sign () { k_sign "$@"; } +c_verify () { k_verify "$@"; } + +## Stub implementations. +notsupp () { op=$1; echo >&2 "$quis: operation \`$op' not supported"; } +k_info () { :; } +k_encrypt () { notsupp encrypt; } +k_decrypt () { notsupp decrypt; } +k_sign () { notsupp sign; } +k_verify () { notsupp verify; } + +prepare () { + key=$1 op=$2 + ## Prepare for a crypto operation OP, using the KEY. This validates the + ## key label, reads the profile, and checks the access-control list. + + ## Find the key properties. + parse_keylabel "$key" + if [ ! -d $kdir ]; then echo >&2 "$quis: unknown key \`$key'"; exit 1; fi + readmeta $kdir + read_profile "$profile" + + ## Check whether we're allowed to do this thing. This is annoyingly + ## fiddly. + eval acl=\${kprop_acl_$op-!owner} + verdict=forbid + while :; do + + ## Remove leading whitespace. + while :; do + case "$acl" in + [[:space:]]*) acl=${acl#?} ;; + *) break ;; + esac + done + + ## If there's nothing left, leave. + case "$acl" in ?*) ;; *) break ;; esac + + ## Split off the leading word. + case "$acl" in + *[[:space:]]*) word=${acl%%[[:space:]]*} acl=${acl#*[[:space:]]} ;; + *) word=$acl acl="" ;; + esac + + ## See what sense it has if it matches. + case "$word" in + -*) sense=forbid rest=${word#-} ;; + *) sense=allow rest=$word ;; + esac + + ## See whether the calling user matches. + case "$rest" in + !owner) pat=$kowner list=$USERV_USER ;; + !*) echo >&2 "$quis: unknown ACL token \`$word'" ;; + %*) pat=${rest#%} list="$USERV_GROUP $USERV_GID" ;; + *) pat=$rest list="$USERV_USER $USERV_UID" ;; + esac + matchp=nil + for i in $list; do case "$i" in $pat) matchp=t; break ;; esac; done + case $matchp in t) verdict=$sense; break ;; esac + done + + case $verdict in + forbid) echo >&2 "$quis: $op access to key \`$key' forbidden"; exit ;; + esac } -ec_encrypt () { - public=$1; shift - ## Encrypt stuff using the PUBLIC key. Use -i/-o or redirection. +###-------------------------------------------------------------------------- +### Crypto operations for infrastructure purposes. + +c_sysprofile () { + profile=$1 + ## Select the profile in FILE for future crypto operations. - run_seccure encrypt -q -cp256 -m128 "$@" -- $(cat "$public") + unset $(set | sed -n '/^kprop_/s/=.*$//p') + reqtmp + getsysprofile "$profile" >$tmp/profile + readprops $tmp/profile } -ec_decrypt () { - private=$1; shift - ## Decrypt stuff using the PRIVATE key. Use -i/-o or redirection. +c_gensyskey () { + profile=$1 kdir=$2 knub=$3; shift 3 + ## Generate a system key using PROFILE; store the data in KDIR and the nub + ## in KNUB. Remaining arguments are options. - run_seccure decrypt -q -cp256 -m128 -F"$private" "$@" + c_sysprofile "$profile" + c_genkey "$profile" "$kdir" "$knub" : "$@" } -ec_sign () { - private=$1; shift - ## Sign stuff using the PRIVATE key. Use -i/-o or redirection. +c_sysprepare () { + kdir=$1 + readmeta "$kdir" + c_sysprofile "$profile" +} - run_seccure sign -q -cp256 -F"$private" "$@" +c_sysop () { + op=$1 kdir=$2; shift 1 + c_sysprepare "$kdir" + c_$op "$@" } -ec_verify () { - public=$1 signature=$2; shift - ## Verify a SIGNATURE using the PUBLIC key; use -i or redirection for the - ## input. +c_sysencrypt () { c_sysop encrypt "$1" /dev/null; } +c_sysdecrypt () { c_sysop decrypt "$1" "$2"; } +c_syssign () { c_sysop sign "$1" "$2"; } +c_sysverify () { c_sysop verify "$1" /dev/null; } + +###-------------------------------------------------------------------------- +### Recovery operations. + +stash () { + recov=$1 label=$2 + ## Stash a copy of stdin encrypted under the recovery key RECOV, with a + ## given LABEL. + checkword "recovery key label" "$recov" + checklabel "secret" "$label" + + rdir=$KEYS/recov/$recov/current + if [ ! -d $rdir/store ]; then + echo >&2 "$quis: unknown recovery key \`$recov'" + exit 1 + fi + case $label in */*) mkdir -m755 -p $rdir/${label%/*} ;; esac + (c_sysencrypt $rdir/store >$rdir/$label.new) + mv $rdir/$label.new $rdir/$label.recov +} - run_seccure verify -q -cp256 "$@" -- $(cat "$public") "$signature" +recover () { + recov=$1 label=$2 + ## Recover a stashed secret, protected by RECOV and stored as LABEL, and + ## write it to stdout. + checkword "recovery key label" "$recov" + checklabel "secret" "$label" + + rdir=$KEYS/recov/$recov/current + if [ ! -f $rdir/$label.recov ]; then + echo >&2 "$quis: no blob for \`$label' under recovery key \`$recov'" + exit 1 + fi + reqsafe + nub=$SAFE/keys.reveal/$recov.current/nub + if [ ! -f $nub ]; then + echo >&2 "$quis: current recovery key \`$recov' not revealed" + exit 1; + fi + mktmp + c_sysdecrypt $rdir/store $nub <$rdir/$label.recov } ###-------------------------------------------------------------------------- ### Help text. -dohelp () { - case "$KEYS_HELP" in t) ;; *) return ;; esac - help; exit +defhelp () { + read umsg + usage="usage: $quis${umsg+ }$umsg" + help=$(cat) + case "$KEYS_HELP" in t) help; exit ;; esac } -defhelp () { read umsg; usage="usage: $quis${umsg+ }$umsg"; help=$(cat); } help () { showhelp; } showhelp () { cat <&2 "$usage"; exit 1; } + +###-------------------------------------------------------------------------- +### Subcommand handling. + +version () { + echo "$PACKAGE version $VERSION" +} + +cmd_help () { + rc=0 + version + case $# in + 0) + cat <&2 "$quis: unrecognized command \`$i'" + rc=1 + continue + elif ! KEYS_HELP=t "$KEYSLIB/$prefix.$i"; then + rc=1 + fi + done + ;; + esac + return $rc +} + +dispatch () { + case $# in 0) echo >&2 "$usage"; exit 1 ;; esac + cmd=$1; shift + case "$cmd" in help) cmd_help "$@"; exit ;; esac + if [ ! -x "$KEYSLIB/$prefix.$cmd" ]; then + echo >&2 "$quis: unrecognized command \`$cmd'" + exit 1 + fi + + unset KEYS_HELP + exec "$KEYSLIB/$prefix.$cmd" "$@" +} + ###----- That's all, folks -------------------------------------------------- diff --git a/keys.conceal b/keys.conceal new file mode 100755 index 0000000..db34fec --- /dev/null +++ b/keys.conceal @@ -0,0 +1,51 @@ +#! /bin/sh +### +### Unreveal a revealed recovery key +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +set -e +case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac +. "$KEYSLIB"/keyfunc.sh + +defhelp <&2 "$usage"; exit 1 ;; esac +recov=$1 +checklabel "recovery key" "$recov" +case "$recov" in + */*) ;; + *) recov=$recov/current ;; +esac + +reqsafe +tag=$(echo $recov | tr / .) +if [ ! -d $SAFE/keys.reveal/$tag ]; then + echo >&2 "$quis: recovery key \`$recov' is not revealed" + exit 1 +fi +rm -rf $SAFE/keys.reveal/$tag + +###----- That's all, folks -------------------------------------------------- diff --git a/keys.in b/keys.in index 7915a4f..2676d56 100755 --- a/keys.in +++ b/keys.in @@ -24,83 +24,27 @@ ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. set -e - -quis=${0##*/} -PACKAGE=@PACKAGE@ -VERSION=@VERSION@ - -: ${KEYS=@pkgconfdir@} +: ${ETC=@pkgconfdir@} +: ${KEYS=@pkgstatedir@} : ${KEYSLIB=@pkglibdir@} -export KEYS KEYSLIB +export ETC KEYS KEYSLIB -###-------------------------------------------------------------------------- -### Help. +. "$KEYSLIB"/keyfunc.sh usage="usage: $quis COMMAND [ARGUMENTS ...]" +prefix=keys -version () { - echo "$PACKAGE version $VERSION" -} - -help () { - rc=0 - version - case $# in - 0) - cat <&2 "$quis: unrecognized command \`$i'" - rc=1 - continue - elif ! KEYS_HELP=t "$KEYSLIB"/"$i"; then - rc=1 - fi - done - ;; - esac - return $rc -} - -###-------------------------------------------------------------------------- -### Command dispatch. - +## Parse options. while getopts "hv" opt; do case "$opt" in - h) help; exit ;; + h) cmd_help; exit ;; v) version; exit ;; - *) echo >&2 "$usage"; exit 1 ;; + *) usage_err ;; esac done -shift $((OPTIND - 1)) - -case $# in 0) echo >&2 "$usage"; exit 1 ;; esac -cmd=$1; shift -case "$cmd" in help) help "$@"; exit ;; esac -if [ ! -x "$KEYSLIB"/"$cmd" ]; then - echo >&2 "$quis: unrecognized command \`$cmd'" - exit 1 -fi +shift $(( $OPTIND - 1 )) -unset KEYS_HELP -exec "$KEYSLIB"/"$cmd" "$@" +## Dispatch. +dispatch "$@" ###----- That's all, folks -------------------------------------------------- diff --git a/keeper-cards b/keys.keeper-cards similarity index 97% rename from keeper-cards rename to keys.keeper-cards index 359c5a2..73f2411 100755 --- a/keeper-cards +++ b/keys.keeper-cards @@ -40,10 +40,9 @@ index. If no INDICES are given then all secret keys are written. The public keys are found in $KEYS/keeper/KEEPER/I.pub; private keys are read from KEEPER/I in the current directory. HELP -dohelp ## Parse the command line. -case $# in 0) echo >&2 "$usage"; exit 1 ;; esac +case $# in 0) usage_err ;; esac keeper=$1; shift checkword "keeper set label" "$keeper" read n hunoz <$KEYS/keeper/$keeper/meta @@ -65,21 +64,21 @@ for range in "$@"; do ;; esac case "$low" in ?*) ;; *) low=0 ;; esac - case "$high" in ?*) ;; *) high=$((n - 1)) ;; esac + case "$high" in ?*) ;; *) high=$(( $n - 1 )) ;; esac if [ 0 -gt $low -o $low -gt $high -o $high -ge $n ]; then echo >&2 "$quis: invalid index range \`$range'" exit 1 fi - i=$((low + 0)) + i=$(( $low + 0 )) while [ $i -le $high ]; do case $want in *:"$i":*) ;; *) want=$want$i: ;; esac - i=$((i + 1)) + i=$(( $i + 1 )) done done ## Start working on the output file. This will contain deep secrets, so ## don't leave stuff easily readable. -tmp=$(mktmp); cleanup rmtmp +mktmp umask 077 exec 3>$tmp/$keeper.tex cat >&3 <<'EOF' @@ -247,7 +246,7 @@ while [ $i -lt $n ]; do \card{$i}{$secret} EOF esac - i=$((i + 1)) + i=$(( $i + 1 )) done ## Wrap up and build the document. diff --git a/new-keeper b/keys.new-keeper similarity index 77% rename from new-keeper rename to keys.new-keeper index 05423f4..45764bd 100755 --- a/new-keeper +++ b/keys.new-keeper @@ -28,21 +28,29 @@ case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac . "$KEYSLIB"/keyfunc.sh defhelp <&2 "$usage"; exit 1 ;; esac -keeper=$1 n=$2 +profile=${keeper_profile-keeper} +while getopts "p:" opt; do + case "$opt" in + p) profile=$OPTARG ;; + *) usage_err ;; + esac +done +shift $(( $OPTIND - 1 )) +case $# in 0 | 1) usage_err ;; esac +keeper=$1 n=$2; shift 2 checkword "keeper set label" "$keeper" checknumber "set size" "$n" +checkword "profile label" "$profile" ## Preflight checking. if [ -e $KEYS/keeper/$keeper ]; then @@ -55,15 +63,18 @@ if [ -e $keeper ]; then fi ## Generate the private keys, one per file, and compute the public keys. -tmp=$(mktmp); cleanup rmtmp +mktmp rm -rf $keeper.new mkdir -m700 $keeper.new mkdir -p -m755 $KEYS/keeper/$keeper.new echo $n >$KEYS/keeper/$keeper.new/meta i=0 while [ $i -lt $n ]; do - ec_keygen $keeper.new/$i $KEYS/keeper/$keeper.new/$i.pub - i=$(( i + 1 )) + c_gensyskey $profile \ + $KEYS/keeper/$keeper.new/$i \ + $keeper.new/$i \ + keeper="$keeper" seq="$i" tot="$n" "$@" + i=$(( $i + 1 )) done mv $keeper.new $keeper mv $KEYS/keeper/$keeper.new $KEYS/keeper/$keeper diff --git a/new-recov b/keys.new-recov similarity index 79% rename from new-recov rename to keys.new-recov index d221764..c5d7fce 100755 --- a/new-recov +++ b/keys.new-recov @@ -28,7 +28,7 @@ case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac . "$KEYSLIB"/keyfunc.sh defhelp <&2 "$usage"; exit 1 ;; esac +profile=${recov_profile-recovery} +while getopts "p:" opt; do + case "$opt" in + p) profile=$OPTARG ;; + *) usage_err ;; + esac +done +shift $(( $OPTIND - 1 )) +case $# in 0) usage_err ;; esac recov=$1; shift checkword "recovery key label" "$recov" +checkword "profile label" "$profile" +nkeep=0 for k in "$@"; do case "$k" in + --) break ;; *:*) ;; *) echo >&2 "$quis: bad keeper spec \`$k'"; exit 1 ;; esac - keeper=${k%:*} t=${k#*:} + keeper=${k%:*} t=${k#*:} nkeep=$(( $nkeep + 1 )) checkword "keeper set label" "$keeper" checknumber "keeper quorum" "$t" done @@ -62,12 +72,13 @@ done ## Establish the keeper parameters. rdir=$KEYS/recov/$recov if [ ! -d $rdir ]; then mkdir -m755 -p $rdir; fi -case $# in +case $nkeep in 0) kparam=$rdir/keepers ;; *) for k in "$@"; do + case "$k" in --) break ;; esac keeper=${k%:*} t=${k#*:} echo $keeper $t done >$rdir/keepers.new @@ -77,11 +88,18 @@ esac if [ ! -f $kparam ]; then echo >&2 "$quis: no keepers specified"; exit 1; fi ## Make the new key and issue the shares. -tmp=$(mktmp); cleanup rmtmp +mktmp rm -rf $rdir/new mkdir -m755 $rdir/new cd $tmp -ec_keygen secret $rdir/new/pub +while :; do case "$#,$1" in + 0,) break ;; + *,*,*) ;; + *,--) break ;; + esac + shift +done +c_gensyskey $profile $rdir/new/store secret recov="$recov" while read keeper k; do read n hunoz <$KEYS/keeper/$keeper/meta shamir issue $k/$n secret | { @@ -89,9 +107,10 @@ while read keeper k; do echo "$param" >$rdir/new/$keeper.param i=0 while read share; do - echo $share | - ec_encrypt $KEYS/keeper/$keeper/$i.pub -o$rdir/new/$keeper.$i.share - i=$(( i + 1 )) + c_sysencrypt $KEYS/keeper/$keeper/$i >$rdir/new/$keeper.$i.share <&2 "$quis: current $recov key not revealed" exit 1 @@ -112,10 +131,11 @@ else find $rdir/current/ -type f -name '*.recov' -print | while read name; do name=${name#$rdir/current/} case "$name" in */*) mkdir -p -m755 $rdir/new/${name%/*} ;; esac - ec_decrypt $reveal -i$rdir/current/$name | - ec_encrypt $rdir/new/pub -o$rdir/new/$name + c_sysdecrypt $rdir/current/store $reveal <$rdir/current/$name >tmp + c_sysencrypt $rdir/new/store $rdir/new/$name + rm tmp done - rm -r $mem/keys.reveal/$recov.current + rm -r $SAFE/keys.reveal/$recov.current fi ## Tidy up and commit. Repointing the symlink is grim because, according to @@ -123,7 +143,7 @@ fi ## symlink to a directory -- and there's no way of turning this behaviour ## off. The subterfuge here is due to Colin Watson. cd $rdir -while [ -d $seq ]; do seq=$(( seq + 1 )); done +while [ -d $seq ]; do seq=$(( $seq + 1 )); done case $kparam in *.new) mv keepers.new keepers ;; esac rm -f next ln -s $seq next diff --git a/recover b/keys.recover old mode 100644 new mode 100755 similarity index 77% rename from recover rename to keys.recover index b4f64d2..2c48be8 --- a/recover +++ b/keys.recover @@ -33,27 +33,14 @@ Recover the secret LABEL using recovery key RECOV. The recovery key must be revealed. The secret is written to stdout. HELP -dohelp ## Parse the command line. -case $# in 2) ;; *) echo >&2 "$usage"; exit 1 ;; esac +case $# in 2) ;; *) usage_err ;; esac recov=$1 label=$2 checklabel "recovery key label" "$recov" checklabel "secret" "$label" ## Do the recovery. -blob=$KEYS/recov/$recov/current/$label.recov -if [ ! -f $blob ]; then - echo >&2 "$quis: no recovery blob for secret \`$label'" - exit 1 -fi -mem=$(userv root claim-mem-dir &2 "$quis: current $recov key not revealed" - exit 1 -fi -tmp=$(mktmp); cleanup rmtmp -ec_decrypt $reveal -i$blob +recover $recov $label ###----- That's all, folks -------------------------------------------------- diff --git a/reveal b/keys.reveal similarity index 61% rename from reveal rename to keys.reveal index 8f8e347..a93ec90 100755 --- a/reveal +++ b/keys.reveal @@ -28,19 +28,18 @@ case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac . "$KEYSLIB"/keyfunc.sh defhelp <&2 "$quis: stdin is a terminal"; exit 1; fi ;; 3) ;; - *) echo >&2 "$usage"; exit 1 ;; + *) usage_err ;; esac recov=$1 keeper=$2; shift 2 checklabel "recovery key" "$recov" @@ -50,13 +49,23 @@ case "$recov" in esac checkword "keeper set label" "$keeper" +## Check that this is a sensible thing to do. +if [ ! -f $KEYS/keeper/$keeper/meta ]; then + echo >&2 "$quis: unknown keeper set \`$keeper'" + exit 1 +fi +if [ ! -d $KEYS/recov/$recov ]; then + echo >&2 "$quis: unknown recovery key \`$recov'" + exit 1 +fi +if [ ! -f $KEYS/recov/$recov/$keeper.param ]; then + echo >&2 "$quis: recovery key \`$recov' not kept by keeper set \`$keeper'" + exit 1 +fi + ## Grab the key, because we'll need to read it several times. -tmp=$(mktmp); cleanup rmtmp -secret=$(cat -- "$@") -pub=$(ec_public /dev/stdin <$tmp/secret ## Read the threshold from the recovery metadata. read param <$KEYS/recov/$recov/$keeper.param @@ -82,62 +91,62 @@ t=${t%%;*} read n hunoz <$KEYS/keeper/$keeper/meta i=0 foundp=nil -: "$pub" while [ $i -lt $n ]; do - read cand <$KEYS/keeper/$keeper/$i.pub - : "$cand" - case "$pub" in "$cand") foundp=t; break ;; esac - i=$(( i + 1 )) + c_sysprepare $KEYS/keeper/$keeper/$i + nubbin=$(nubid <$tmp/secret) + nubid=$(cat $KEYS/keeper/$keeper/$i/nubid) + case "$nubbin" in "$nubid") foundp=t; break ;; esac + i=$(( $i + 1 )) done case $foundp in - nil) echo >&2 "$quis: key doesn't match keeper \`$keeper'"; exit 1 ;; + nil) echo >&2 "$quis: nub doesn't match keeper \`$keeper'"; exit 1 ;; esac ## Establish the recovery staging area. See whether we've done enough ## already. -mem=$(userv root claim-mem-dir &2 "$quis: secret $recov already revealed" - exit 1 -fi -if [ -f $keeper.$i ]; then - echo >&2 "$quis: share $i already revealed" +if [ -f nub ]; then + echo >&2 "$quis: recovery key \`$recov' already revealed" exit 1 fi ## Decrypt the share. umask 077 -ec_decrypt /dev/stdin \ - -i$KEYS/recov/$recov/$keeper.$i.share \ - -o$keeper.$i.new <&2 "$quis: share $i already revealed" +else + c_sysdecrypt $KEYS/keeper/$keeper/$i $tmp/secret \ + <$KEYS/recov/$recov/$keeper.$i.share \ + >$keeper.$i.new + mv $keeper.$i.new $keeper.$i.share +fi ## See if there's enough for a recovery. n=0 -for j in $keeper.*; do if [ -f "$j" ]; then n=$(( n + 1 )); fi; done +for j in $keeper.*.share; do if [ -f "$j" ]; then n=$(( $n + 1 )); fi; done if [ $n -lt $t ]; then - echo >&2 "$quis: share $i revealed; $(( t - n )) more required" + echo >&2 "$quis: share $i revealed; $(( $t - $n )) more required" else - cat $KEYS/recov/$recov/$keeper.param $keeper.* >$keeper.shares - shamir recover <$keeper.shares >secret.new - pubx=$(ec_public secret.new) - puby=$(cat $KEYS/recov/$recov/pub) - case "$pubx" in - "$puby") ;; + cat $KEYS/recov/$recov/$keeper.param $keeper.*.share >$keeper.shares + shamir recover <$keeper.shares >nub.new + c_sysprepare $KEYS/recov/$recov/store + nubbin=$(nubid &2 "quis: recovered secret key doesn't match public key" + echo >&2 "$quis: recovered nub doesn't match stored hash" exit 1 ;; esac - mv secret.new secret - echo >&2 "$quis: secret $recov revealed" + mv nub.new nub + rm -f $keeper.* + echo >&2 "$quis: recovery key \`$recov' revealed" fi ###----- That's all, folks -------------------------------------------------- diff --git a/stash b/keys.stash old mode 100644 new mode 100755 similarity index 88% rename from stash rename to keys.stash index 98116b5..59eeabf --- a/stash +++ b/keys.stash @@ -35,24 +35,20 @@ The LABEL is used to identify the encrypted secret later to the \`recover' command. The secret is read from SECRET, or stdin if SECRET is omitted or \`-'. HELP -dohelp ## Parse the command line. case $# in 2) if [ -t 0 ]; then echo >&2 "$quis: stdin is a terminal"; exit 1; fi ;; 3) ;; - *) echo >&2 "$usage"; exit 1 ;; + *) usage_err ;; esac recov=$1 label=$2; shift 2 checkword "recovery key label" "$recov" checklabel "secret" "$label" ## Do the thing. -tmp=$(mktmp); cleanup rmtmp +mktmp cat -- "$@" >$tmp/secret -cd $KEYS/recov/$recov/current -case $label in */*) mkdir -m755 -p ${label%/*} ;; esac -ec_encrypt pub -i$tmp/secret -o$label.new -mv $label.new $label.recov +stash $recov $label <$tmp/secret ###----- That's all, folks -------------------------------------------------- diff --git a/ktype.gnupg b/ktype.gnupg new file mode 100644 index 0000000..8a8e764 --- /dev/null +++ b/ktype.gnupg @@ -0,0 +1,150 @@ +### -*-sh-*- +### +### Key type for GNU Privacy Guard +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +run_gnupg () { + base=$1; shift + ## Run GnuPG with some standard options. + + gpg --homedir="$base" --no-permission-warning -q --batch \ + --always-trust \ + "$@" +} + +defprops k_props <"$nub" + prefs="$kprop_cipher_prefs $kprop_digest_prefs $kprop_compress_prefs" + + case ${kprop_s2k_cipher+t} in + t) ;; + *) set -- $kprop_cipher_prefs; kprop_s2k_cipher=$1 ;; + esac + case ${kprop_s2k_digest+t} in + t) ;; + *) set -- $kprop_digest_prefs; kprop_s2k_digest=$1 ;; + esac + + cat >"$base/gpg.conf" <"$base/fpr" + run_gnupg "$base" --export --armor --output="$base/pub" +} + +k_encrypt () { + base=$1 + run_gnupg "$base" --encrypt --armor --recipient=$(cat "$base/fpr") +} + +k_decrypt () { + base=$1 nub=$2 + run_gnupg "$base" --passphrase-file "$nub" --decrypt +} + +k_sign () { + base=$1 nub=$2 + run_gnupg "$base" --passphrase-file "$nub" --detach-sign --armor +} + +k_verify () { + base=$1 sig=$3 + echo "$sig" >$tmp/sig + if run_gnupg "$base" --verify $tmp/sig - >/dev/null 2>$tmp/err + then :; else + rc=$? + cat >&2 $tmp/err + return $rc + fi +} + +###----- That's all, folks -------------------------------------------------- diff --git a/ktype.seccure b/ktype.seccure new file mode 100644 index 0000000..3b716d2 --- /dev/null +++ b/ktype.seccure @@ -0,0 +1,99 @@ +### -*-sh-*- +### +### Key type for B. Poettering's `Seccure' suite +### +### (c) 2011 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the distorted.org.uk key management suite. +### +### distorted-keys 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. +### +### distorted-keys 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 distorted-keys; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +###-------------------------------------------------------------------------- +### Utility functions. + +run_seccure () { + op=$1; shift + ## run_seccure OP ARG ... + ## + ## Run a Seccure program, ensuring that its stderr is reported if it had + ## anything very interesting to say, but suppressed if it was boring. + + set +e; seccure-$op "$@" 2>$tmp/seccure.out; rc=$?; set -e + grep -v '^WARNING: Cannot obtain memory lock' $tmp/seccure.out >&2 || : + return $rc +} + +###-------------------------------------------------------------------------- +### Key type definition. + +defprops k_props <"$nub" + k_public "$base" "$nub" >"$base/pub" +} + +k_check () { + base=$1 nub=$2 + this=$(k_public "$base" "$nub") + orig=$(cat "$base/pub") + case "$orig" in "$this") return 0 ;; *) return 1 ;; esac +} + +k_encrypt () { + base=$1 + run_seccure encrypt -q -c$kprop_curve -m$kprop_tagsz -- $(cat "$base/pub") +} + +k_decrypt () { + nub=$2 + if ! run_seccure decrypt -q -c$kprop_curve -m$kprop_tagsz -F"$nub"; then + echo >&2 "$quis: decryption failed" + return 1 + fi +} + +k_sign () { + nub=$2 + run_seccure sign -q -c$kprop_curve -F"$nub" -s/dev/stdout +} + +k_verify () { + base=$1 sig=$3 + if run_seccure verify -q -c$kprop_curve -- \ + $(cat "$base/pub") "$sig" + then :; else + rc=$? + echo >&2 "$quis: signature verification failed" + return $rc + fi +} + +###----- That's all, folks -------------------------------------------------- -- [mdw]