chiark / gitweb /
Multiple key types, key profiles, and user key storage.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 24 Dec 2011 02:29:11 +0000 (02:29 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 24 Dec 2011 02:33:15 +0000 (02:33 +0000)
  * 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.

23 files changed:
Makefile.am
cryptop.decrypt [new file with mode: 0644]
cryptop.delkey [new file with mode: 0644]
cryptop.encrypt [new file with mode: 0644]
cryptop.genkey [new file with mode: 0644]
cryptop.in [new file with mode: 0755]
cryptop.info [new file with mode: 0644]
cryptop.public [new file with mode: 0644]
cryptop.recover [new file with mode: 0644]
cryptop.sign [new file with mode: 0644]
cryptop.verify [new file with mode: 0644]
extract-profile.in [new file with mode: 0644]
keyfunc.sh.in
keys.conceal [new file with mode: 0755]
keys.in
keys.keeper-cards [moved from keeper-cards with 97% similarity]
keys.new-keeper [moved from new-keeper with 77% similarity]
keys.new-recov [moved from new-recov with 79% similarity]
keys.recover [moved from recover with 77% similarity, mode: 0755]
keys.reveal [moved from reveal with 61% similarity]
keys.stash [moved from stash with 88% similarity, mode: 0755]
ktype.gnupg [new file with mode: 0644]
ktype.seccure [new file with mode: 0644]

index 12168f6..0cd0854 100644 (file)
@@ -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 (file)
index 0000000..ccc42a8
--- /dev/null
@@ -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 <<HELP
+KEY
+Decrypt the ciphertext from standard input using the named KEY.  The
+plaintext is written to standard output.
+HELP
+
+case $# in 1) ;; *) usage_err ;; esac
+key=$1
+
+mktmp
+prepare "$key" decrypt
+c_decrypt $kdir $knub
+
+###----- That's all, folks --------------------------------------------------
diff --git a/cryptop.delkey b/cryptop.delkey
new file mode 100644 (file)
index 0000000..15dcc7d
--- /dev/null
@@ -0,0 +1,43 @@
+#! /bin/sh
+###
+### Delete 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 <<HELP
+KEY
+Delete the named user KEY.
+HELP
+
+case $# in 1) ;; *) usage_err ;; esac
+key=$1
+
+parse_keylabel "$key"
+rm -rf $kdir
+rm -f $knub
+rm -f $KEYS/recov/*/current/$kowner/$klabel.recov
+
+###----- That's all, folks --------------------------------------------------
diff --git a/cryptop.encrypt b/cryptop.encrypt
new file mode 100644 (file)
index 0000000..16da12a
--- /dev/null
@@ -0,0 +1,43 @@
+#! /bin/sh
+###
+### Encrypt 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 <<HELP
+KEY
+Encrypt the data from standard input using the named KEY.  The ciphertext is
+written to standard output.
+HELP
+
+case $# in 1) ;; *) usage_err ;; esac
+key=$1
+
+mktmp
+prepare "$key" encrypt
+c_encrypt $kdir $knub
+
+###----- That's all, folks --------------------------------------------------
diff --git a/cryptop.genkey b/cryptop.genkey
new file mode 100644 (file)
index 0000000..b33458d
--- /dev/null
@@ -0,0 +1,100 @@
+#! /bin/sh
+###
+### Generate 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 <<HELP
+[-f] KEY PROFILE [ARGUMENTS ...]
+Generate a named KEY, according to the PROFILE.
+
+The ARGUMENTS are passed to the key-type handler selected by the profile.
+
+Options:
+  -f           Force overwriting an existing key.
+HELP
+
+genhook_recov () {
+  base=$1 nub=$2
+  for recov in $kopt_recovery; do
+    (stash $recov $kowner/$klabel <"$nub")
+  done
+}
+
+## Parse command-line arguments.
+forcep=nil
+while getopts "f" opt; do
+  case "$opt" in
+    f) forcep=t ;;
+    *) usage_err ;;
+  esac
+done
+shift $(( $OPTIND - 1 ))
+case $# in 0 | 1) usage_err ;; esac
+key=$1 profile=$2; shift 2
+
+## Check the key name carefully.  This is where we actually create files,
+## so it's reallly important to make sure that we don't make any stupid
+## mistakes.
+mktmp
+parse_keylabel "$key"
+case "$kowner" in
+  "$USERV_USER") ;;
+  *) echo >&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 (executable)
index 0000000..3efb554
--- /dev/null
@@ -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 (file)
index 0000000..1d0ff21
--- /dev/null
@@ -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 <<HELP
+KEY
+Show information about the named user KEY.
+HELP
+
+case $# in 1) ;; *) usage_err ;; esac
+key=$1
+
+mktmp
+prepare "$key" info
+set | sed -n '/^kprop_/{s///;y/_/-/;p}'
+k_info $kdir
+
+###----- That's all, folks --------------------------------------------------
diff --git a/cryptop.public b/cryptop.public
new file mode 100644 (file)
index 0000000..a0054e9
--- /dev/null
@@ -0,0 +1,47 @@
+#! /bin/sh
+###
+### Show a public 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 <<HELP
+KEY
+If the user KEY is asymmetric, write the public key to stdout.
+HELP
+
+case $# in 1) ;; *) usage_err ;; esac
+key=$1
+
+mktmp
+prepare "$key" info
+if [ -f $kdir/pub ]; then
+  cat $kdir/pub
+else
+  echo >&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 (file)
index 0000000..f661569
--- /dev/null
@@ -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 <<HELP
+KEY RECOV
+Recover the named user KEY using a blob protected using the recovery key
+RECOV; it is an error if RECOV is not currently revealed.
+HELP
+
+case $# in 2) ;; *) usage_err ;; esac
+key=$1 recov=$2
+parse_keylabel "$key"
+if [ ! -d $kdir ]; then echo >&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 (file)
index 0000000..38d4e78
--- /dev/null
@@ -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 <<HELP
+KEY
+Sign the data from standard input using the named KEY.  The signature is
+written to standard output.
+HELP
+
+case $# in 1) ;; *) usage_err ;; esac
+key=$1
+
+mktmp
+prepare "$key" sign
+c_sign $kdir $knub
+
+###----- That's all, folks --------------------------------------------------
diff --git a/cryptop.verify b/cryptop.verify
new file mode 100644 (file)
index 0000000..9e6b63c
--- /dev/null
@@ -0,0 +1,43 @@
+#! /bin/sh
+###
+### Verify signed 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 <<HELP
+KEY SIGNATURE
+Verify the message on standard input against a literla SIGNATURE using the
+named KEY.
+HELP
+
+case $# in 2) ;; *) usage_err ;; esac
+key=$1 sig=$2
+
+mktmp
+prepare "$key" sign
+c_sign $kdir $knub "$sig"
+
+###----- That's all, folks --------------------------------------------------
diff --git a/extract-profile.in b/extract-profile.in
new file mode 100644 (file)
index 0000000..1d19fc2
--- /dev/null
@@ -0,0 +1,465 @@
+#! @PYTHON@
+###
+### Parse a profile definition file and emit a particular section
+###
+### (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.
+
+from __future__ import with_statement
+
+import sys as SYS
+import os as OS
+import UserDict as UD
+import optparse as O
+from cStringIO import StringIO
+
+PACKAGE = "@PACKAGE@"
+VERSION = "@VERSION@"
+
+###--------------------------------------------------------------------------
+### Utilities.
+
+class struct (object):
+  def __init__(me, **kw):
+    me.__dict__.update(kw)
+
+class UserError (Exception): pass
+
+###--------------------------------------------------------------------------
+### Configuration section management.
+
+class prop (struct): pass
+
+class Section (object, UD.DictMixin):
+  """
+  A section of a profile configuration file.
+  """
+
+  ## States for depth-first traversal.
+  V_WHITE = 0                           # Not yet visited.
+  V_GREY = 1                            # Currently being visited.
+  V_BLACK = 2                           # Visited previously and processed.
+
+  def __init__(me, name, dict = None):
+    """
+    Initialize a Section object.
+
+    The DICT provides the initial (direct) contents of the Section.  The NAME
+    is used for presentation purposes, e.g., when reporting error conditions.
+    """
+    super(Section, me).__init__()
+    if dict is None: me._dict = {}
+    else: me._dict = dict
+    me._visited = me.V_WHITE
+    me.name = name
+    me.includes = set()
+    me.inferiors = set()
+    me.inherited = {}
+
+  ## Dictionary methods for UD.DictMixin.  The built-in `dict' class provides
+  ## equality, and is therefore not hashable.  By doing things this way, we
+  ## can retain reference equality and stay hashable, which will be useful
+  ## later.
+  def __getitem__(me, key):
+    return me._dict[key]
+  def __setitem__(me, key, value):
+    me._dict[key] = value
+  def __delitem__(me, key):
+    del me._dict[key]
+  def keys(me):
+    return me._dict.keys()
+  def __contains__(me, key):
+    return key in me._dict
+  def __iter__(me):
+    return me._dict.__iter__()
+  def iteritems(me):
+    return me._dict.iteritems()
+  def __repr__(me):
+    return 'Section(%r, %r)' % (me.name, me.inherited)
+
+  def transit(me, seen = None, path = None):
+    """
+    Visit the Section for the purposes of computing transitive inclusion.
+
+    If this completes successfully, the Section's inferiors slot is set up to
+    contain all of its (non-strict) inferiors.  A section's inferiors consist
+    of itself, together with the union of the inferiors of all of its
+    included Sections.
+
+    If the Section's visited state is black, nothing happens; if it's white
+    then it will be coloured grey temporarily, and its included Sections
+    processed recursively; if it's grey to begin with then we have
+    encountered a cycle.
+
+    The SEEN dictionary and PATH list are used for detecting and reporting
+    cycles.  The PATH contains a list of the currently grey Sections, in the
+    order in which they were encountered; SEEN maps Section names to their
+    indices in the PATH list.
+
+    It is possible to make this work in the presence of cycles, but it's more
+    effort than it's worth.
+    """
+
+    ## Extend the path to include us.  This will be useful when reporting
+    ## cycles.
+    if seen is None: seen = {}
+    if path is None: path = []
+    path.append(me)
+
+    ## Already been here: nothing to do.
+    if me._visited == me.V_BLACK:
+      pass
+
+    ## We've found a cycle: report it to the user.
+    elif me._visited == me.V_GREY:
+      raise UserError, 'detected inclusion cycle:\n\t%s' % \
+            ' -> '.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 --------------------------------------------------
index 70da24a..ca14782 100644 (file)
@@ -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 </dev/null)
-  tmp="$mem/keys.tmp.$$"
+  case "${tmp+t}" in t) return ;; esac
+  reqsafe
+  tmp="$SAFE/keys.tmp.$$"
   rm -rf "$tmp"
   mkdir -m700 "$tmp"
-  echo "$tmp"
 }
 
-###--------------------------------------------------------------------------
-### Input validation functions.
+reqtmp () {
+  ## Fail unless a temporary directory is set.
 
-checknumber () {
-  what=$1 thing=$2
-  case "$thing" in
-    "" | [!1-9]* | *[!0-9]*)
-      echo >&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 <<EOF
+$table
+EOF
+  done
+}
 
-  ## We need a temporary place for the error output.
-  case ${tmp+t} in
-    t) ;;
+defprops () {
+  name=$1
+  ## Define a properties table NAME.
+
+  table=$(cat)
+  eval $name=\$table
+}
+
+defprops g_props <<EOF
+type                   nil     $R_IDENT
+recovery               t       $R_WORDSEQ
+random                 t       $R_WORD
+nubhash                        t       $R_WORD
+nubidhash              t       $R_WORD
+nubsz                  t       $R_NUMERIC
+EOF
+
+readprops () {
+  file=$1
+  ## Read a profile from a file.  This doesn't check the form of the
+  ## filename, so it's not suitable for unchecked input.  Properties are set
+  ## using `setprops' with prefix `kprop_'.
+
+  ## Parse the settings from the file.
+  exec 3<"$file"
+  while read line; do
+    case "$line" in "" | \#*) continue ;; esac
+    setprops "property" kprop_ "$line"
+  done <&3
+  exec 3>&-
+  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" <<EOF
+$profile
+EOF
 
-  run_seccure key -q -cp256 -F"$private"
+  ## Generate the key.
+  umask=$(umask); umask 077; >"$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 <<EOF
@@ -188,4 +578,63 @@ $help
 EOF
 }
 
+usage_err () { echo >&2 "$usage"; exit 1; }
+
+###--------------------------------------------------------------------------
+### Subcommand handling.
+
+version () {
+  echo "$PACKAGE version $VERSION"
+}
+
+cmd_help () {
+  rc=0
+  version
+  case $# in
+    0)
+      cat <<EOF
+
+$usage
+
+Options:
+  -h           Show this help text.
+  -v           Show the program version number.
+
+Commands installed:
+EOF
+      cd "$KEYSLIB"
+      for i in $prefix.*; do
+       if [ ! -x "$i" ]; then continue; fi
+       sed -n "/<<HELP/{n;s/^/ ${i#$prefix.} /;p;q;}" "$i"
+      done
+      ;;
+    *)
+      for i in "$@"; do
+       echo
+       if [ ! -x "$KEYSLIB/$prefix.$i" ]; then
+         echo >&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 (executable)
index 0000000..db34fec
--- /dev/null
@@ -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 <<HELP
+RECOV
+Remove the revealed nub of the recovery key RECOV.
+HELP
+
+case $# in 1) ;; *) echo >&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 (executable)
--- a/keys.in
+++ b/keys.in
 ### 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 <<EOF
-
-$usage
-
-Options:
-  -h           Show this help text.
-  -v           Show the program version number.
-
-Commands installed:
-EOF
-      cd "$KEYSLIB"
-      for i in *; do
-       case "$i" in *.*) continue ;; esac
-       if [ ! -x "$i" ]; then continue; fi
-       sed -n "/<<HELP/{n;s/^/ $i /;p;q;}" "$i"
-      done
-      ;;
-    *)
-      for i in "$@"; do
-       echo
-       if [ ! -x "$KEYSLIB"/"$i" ]; then
-         echo >&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 --------------------------------------------------
similarity index 97%
rename from keeper-cards
rename to keys.keeper-cards
index 359c5a2..73f2411 100755 (executable)
@@ -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.
similarity index 77%
rename from new-keeper
rename to keys.new-keeper
index 05423f4..45764bd 100755 (executable)
@@ -28,21 +28,29 @@ case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac
 . "$KEYSLIB"/keyfunc.sh
 
 defhelp <<HELP
-KEEPER N
+[-p PROFILE] KEEPER N [OPTION=VALUE ...]
 Create a new set of keeper keys.
 
-The private keys are stored in KEEPER/I for each 0 <= I < N in the current
+The key nubs are stored in KEEPER/I for each 0 <= I < N in the current
 directory; presumably you'll do something sensible with them.  A new
 directory $KEYS/keeper/KEEPER is created (it is an error if it already
-exists), containing the public keys I.pub and some metadata meta.
+exists), containing the key store directories and some metadata meta.
 HELP
-dohelp
 
 ## Parse the command line.
-case $# in 2) ;; *) echo >&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
similarity index 79%
rename from new-recov
rename to keys.new-recov
index d221764..c5d7fce 100755 (executable)
--- a/new-recov
@@ -28,7 +28,7 @@ case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac
 . "$KEYSLIB"/keyfunc.sh
 
 defhelp <<HELP
-RECOV [KEEPER:K ...]
+[-p PROFILE] RECOV [KEEPER:K ...] [-- OPTION=VALUE ...]
 Generate a new recovery secret, and split it among the available keepers.
 
 The new secret will be called RECOV.  For each KEEPER set, the private key
@@ -43,18 +43,28 @@ used.
 If there is not a recovery key called RECOV then at least one keeper set must
 be specified.
 HELP
-dohelp
 
 ## Parse the command line.
-case $# in 0) echo >&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 <<EOF
+$share
+EOF
+      i=$(( $i + 1 ))
     done
   }
 done <$kparam
@@ -102,8 +121,8 @@ if [ ! -d $rdir/current ]; then
   seq=0
 else
   seq=$(readlink $rdir/current)
-  mem=$(userv root claim-mem-dir </dev/null)
-  reveal=$mem/keys.reveal/$recov.current/secret
+  reqsafe
+  reveal=$SAFE/keys.reveal/$recov.current/secret
   if [ ! -f $reveal ]; then
     echo >&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 <tmp >$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
old mode 100644 (file)
new mode 100755 (executable)
similarity index 77%
rename from recover
rename to keys.recover
index b4f64d2..2c48be8
--- a/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 </dev/null)
-reveal=$mem/keys.reveal/$recov.current/secret
-if [ ! -f $reveal ]; then
-  echo >&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 --------------------------------------------------
similarity index 61%
rename from reveal
rename to keys.reveal
index 8f8e347..a93ec90 100755 (executable)
--- a/reveal
@@ -28,19 +28,18 @@ case "${KEYSLIB+t}" in t) ;; *) echo >&2 "$0: KEYSLIB unset"; exit 1 ;; esac
 . "$KEYSLIB"/keyfunc.sh
 
 defhelp <<HELP
-RECOV KEEPER [KEY]
+RECOV KEEPER [NUB]
 Reveal a share of a recovery key distributed among keepers.
 
 If enough shares have been revealed, reconstruct the recovery private key.
-The key is read from KEY, or stdin if KEY is omitted or \`-'.
+The keeper nub is read from NUB, or stdin if NUB 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 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 <<EOF
-$secret
-EOF
-)
+mktmp
+cat -- "$@" >$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 </dev/null)
+reqsafe
 tag=$(echo $recov | tr / .)
-mkdir -p -m700 $mem/keys.reveal
-reveal=$mem/keys.reveal/$tag
+mkdir -p -m700 $SAFE/keys.reveal
+reveal=$SAFE/keys.reveal/$tag
 if [ ! -d $reveal ]; then mkdir -m700 $reveal; fi
 cd $reveal
-if [ -f secret ]; then
-  echo >&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 <<EOF
-$secret
-EOF
-mv $keeper.$i.new $keeper.$i
+if [ -f $keeper.$i.share ]; then
+  echo >&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 <nub.new)
+  nubid=$(cat $KEYS/recov/$recov/store/nubid)
+  case "$nubbin" in
+    "$nubid") ;;
     *)
-      echo >&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 --------------------------------------------------
old mode 100644 (file)
new mode 100755 (executable)
similarity index 88%
rename from stash
rename to keys.stash
index 98116b5..59eeabf
--- a/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 (file)
index 0000000..8a8e764
--- /dev/null
@@ -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 <<EOF
+main_type              t       $R_WORD
+main_length            t       $R_NUMERIC
+sub_type               t       $R_WORD
+sub_length             t       $R_NUMERIC
+s2k_cipher             t       $R_WORD
+s2k_digest             t       $R_WORD
+cipher_prefs           t       $R_WORDSEQ
+digest_prefs           t       $R_WORDSEQ
+compress_prefs         t       $R_WORDSEQ
+realname               t       $R_LINE
+comment                        t       $R_LINE
+email                  t       $R_LINE
+EOF
+
+: ${kprop_main_type=RSA} ${kprop_main_length=3072}
+: ${kprop_sub_type=ELG-E} ${kprop_sub_length=3072}
+: ${kprop_cipher_prefs=AES256 AES TWOFISH 3DES BLOWFISH CAST5}
+: ${kprop_digest_prefs=SHA256 SHA1 RIPEMD160}
+: ${kprop_compress_prefs=ZLIB ZIP}
+
+: ${kprop_realname=%{realname\}} ${kprop_email=%{email\}}
+: ${kprop_comment=%{comment-nil\}}
+
+k_generate () {
+  base=$1 nub=$2
+
+  makenub >"$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" <<EOF
+### GnuPG configuration
+
+## Annoying copyright notice and other tedious warnings.
+no-greeting
+expert
+always-trust
+
+## Algorithm selection
+s2k-cipher-algo $kprop_s2k_cipher
+s2k-digest-algo $kprop_s2k_digest
+personal-cipher-preferences $kprop_cipher_prefs
+personal-digest-preferences $kprop_digest_prefs
+personal-compress-preferences $kprop_compress_prefs
+default-preference-list $prefs
+EOF
+
+  { cat <<EOF
+Key-Type: $kprop_main_type
+Key-Length: $kprop_main_length
+Passphrase: $(cat "$nub")
+EOF
+    case ${kprop_sub_type-nil} in
+      nil) ;;
+      *) cat <<EOF
+Subkey-Type: $kprop_sub_type
+Subkey-Length: $kprop_sub_length
+EOF
+    esac
+    real=$(subst "\`realname' value" "$kprop_realname" kopt_ "$R_LINE")
+    email=$(subst "\`email' value" "$kprop_email" kopt_ "$R_LINE")
+    cat <<EOF
+Name-Real: $real
+Name-Email: $email
+EOF
+    comment=$(subst "\`comment' value" "$kprop_comment" kopt_ "$R_LINE")
+    case "$comment" in
+      ?*) cat <<EOF
+Name-Comment: $comment
+EOF
+       ;;
+    esac
+  } | run_gnupg "$base" --gen-key
+
+  ## Commit the new key.
+  run_gnupg "$base" --fingerprint --with-colons | \
+    grep '^fpr:' | cut -d: -f10 >"$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 (file)
index 0000000..3b716d2
--- /dev/null
@@ -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 <<EOF
+curve                  t       $R_LABEL
+tagsz                  t       $R_NUMERIC
+EOF
+
+: ${kprop_curve=p256}
+: ${kprop_tagsz=128}
+
+k_public () {
+  nub=$2
+  run_seccure key -q -c$kprop_curve -F"$nub"
+}
+
+k_generate () {
+  base=$1 nub=$2
+  makenub >"$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 --------------------------------------------------