From: Mark Wooding Date: Sat, 24 Dec 2011 02:29:11 +0000 (+0000) Subject: Multiple key types, key profiles, and user key storage. X-Git-Tag: 0.99.1~17 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/distorted-keys/commitdiff_plain/c47f2aba7d705252c660ba1ad0931fbb93122d80 Multiple key types, key profiles, and user key storage. * 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. --- 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 --------------------------------------------------