fix need for only single argument to gpg_sphere
[monkeysphere.git] / src / share / common
index 5e0cb6a4a00ec1d5d79eb594f43f673f4239aabf..34ac52da7f5dcfcdd2e893be3234ac400002b18c 100644 (file)
 # all-caps variables are meant to be user supplied (ie. from config
 # file) and are considered global
 
-########################################################################
-### COMMON VARIABLES
-
-# managed directories
-SYSCONFIGDIR=${MONKEYSPHERE_SYSCONFIGDIR:-"/etc/monkeysphere"}
-export SYSCONFIGDIR
-
-# monkeysphere version
-VERSION=0.23~pre
-
-# default log level
-LOG_LEVEL="INFO"
-
-# default keyserver
-KEYSERVER="pool.sks-keyservers.net"
-
-# whether or not to check keyservers by defaul
-CHECK_KEYSERVER="true"
-
-# default monkeysphere user
-MONKEYSPHERE_USER="monkeysphere"
-
-# default about whether or not to prompt
-PROMPT="true"
-
 ########################################################################
 ### UTILITY FUNCTIONS
 
+# output version info
+version() {
+    cat "${SYSSHAREDIR}/VERSION"
+}
+
 # failure function.  exits with code 255, unless specified otherwise.
 failure() {
     [ "$1" ] && echo "$1" >&2
@@ -96,11 +76,10 @@ log() {
        fi
        if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
            if [ "$1" ] ; then
-               echo -n "ms: " >&2
-               echo "$@" >&2
+               echo "$@"
            else
-               cat >&2
-           fi
+               cat
+           fi | sed 's/^/'"${LOG_PREFIX}"'/' >&2
        fi
     done
 }
@@ -124,12 +103,12 @@ su_monkeysphere_user() {
     case $(id -un) in
        # if monkeysphere user, run the command under bash
        "$MONKEYSPHERE_USER")
-           bash -c "$@"
+           bash -c "$*"
            ;;
 
          # if root, su command as monkeysphere user
        'root')
-           su "$MONKEYSPHERE_USER" -c "$@"
+           su "$MONKEYSPHERE_USER" -c "$*"
            ;;
 
        # otherwise, fail
@@ -168,8 +147,8 @@ lock() {
     local action="$1"
     local file="$2"
 
-    if ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
-       if ! ( which lockfile >/dev/null ); then
+    if ! ( type lockfile-create &>/dev/null ) ; then
+       if ! ( type lockfile &>/dev/null ); then
            failure "Neither lockfile-create nor lockfile are in the path!"
        fi
        use_lockfileprogs=
@@ -218,7 +197,7 @@ advance_date() {
     local shortunits
 
     # try things the GNU way first 
-    if date -d "$number $longunits" "$format" >/dev/null 2>&1; then
+    if date -d "$number $longunits" "$format" &>/dev/null; then
        date -d "$number $longunits" "$format"
     else
        # otherwise, convert to (a limited version of) BSD date syntax:
@@ -273,7 +252,13 @@ check_capability() {
 
 # hash of a file
 file_hash() {
-    md5sum "$1" 2> /dev/null
+    if type md5sum &>/dev/null ; then
+       md5sum "$1"
+    elif type md5 &>/dev/null ; then
+       md5 "$1"
+    else
+       failure "Neither md5sum nor md5 are in the path!"
+    fi
 }
 
 # convert escaped characters in pipeline from gpg output back into
@@ -296,7 +281,7 @@ get_gpg_expiration() {
 
     keyExpire="$1"
 
-    if [ -z "$keyExpire" -a "$PROMPT" = 'true' ]; then
+    if [ -z "$keyExpire" -a "$PROMPT" != 'false' ]; then
        cat >&2 <<EOF
 Please specify how long the key should be valid.
          0 = key does not expire
@@ -306,7 +291,8 @@ Please specify how long the key should be valid.
       <n>y = key expires in n years
 EOF
        while [ -z "$keyExpire" ] ; do
-           read -p "Key is valid for? (0) " keyExpire
+           printf "Key is valid for? (0) " >&2
+           read keyExpire
            if ! test_gpg_expire ${keyExpire:=0} ; then
                echo "invalid value" >&2
                unset keyExpire
@@ -324,7 +310,9 @@ passphrase_prompt() {
     local fifo="$2"
     local PASS
 
-    if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
+    if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null 2>/dev/null; then
+       printf 'Launching "%s"\n' "${SSH_ASKPASS:-ssh-askpass}" | log info
+       printf '(with prompt "%s")\n' "$prompt" | log debug
        "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
     else
        read -s -p "$prompt" PASS
@@ -334,52 +322,6 @@ passphrase_prompt() {
     fi
 }
 
-test_gnu_dummy_s2k_extension() {
-
-# this block contains a demonstration private key that has had the
-# primary key stripped out using the GNU S2K extension known as
-# "gnu-dummy" (see /usr/share/doc/gnupg/DETAILS.gz).  The subkey is
-# present in cleartext, however.
-
-# openpgp2ssh will be able to deal with this based on whether the
-# local copy of GnuTLS contains read_s2k support that can handle it.
-
-# read up on that here:
-
-# http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html
-
-echo "
------BEGIN PGP PRIVATE KEY BLOCK-----
-Version: GnuPG v1.4.9 (GNU/Linux)
-
-lQCVBEO3YdABBACRqqEnucag4+vyZny2M67Pai5+5suIRRvY+Ly8Ms5MvgCi3EVV
-xT05O/+0ShiRaf+QicCOFrhbU9PZzzU+seEvkeW2UCu4dQfILkmj+HBEIltGnHr3
-G0yegHj5pnqrcezERURf2e17gGFWX91cXB9Cm721FPXczuKraphKwCA9PwARAQAB
-/gNlAkdOVQG0OURlbW9uc3RyYXRpb24gS2V5IGZvciBTMksgR05VIGV4dGVuc2lv
-biAxMDAxIC0tIGdudS1kdW1teYi8BBMBAgAmBQJDt2HQAhsDBQkB4TOABgsJCAcD
-AgQVAggDBBYCAwECHgECF4AACgkQQZUwSa4UDezTOQP/TMQXUVrWzHYZGopoPZ2+
-ZS3qddiznBHsgb7MGYg1KlTiVJSroDUBCHIUJvdQKZV9zrzrFl47D07x6hGyUPHV
-aZXvuITW8t1o5MMHkCy3pmJ2KgfDvdUxrBvLfgPMICA4c6zA0mWquee43syEW9NY
-g3q61iPlQwD1J1kX1wlimLCdAdgEQ7dh0AEEANAwa63zlQbuy1Meliy8otwiOa+a
-mH6pxxUgUNggjyjO5qx+rl25mMjvGIRX4/L1QwIBXJBVi3SgvJW1COZxZqBYqj9U
-8HVT07mWKFEDf0rZLeUE2jTm16cF9fcW4DQhW+sfYm+hi2sY3HeMuwlUBK9KHfW2
-+bGeDzVZ4pqfUEudABEBAAEAA/0bemib+wxub9IyVFUp7nPobjQC83qxLSNzrGI/
-RHzgu/5CQi4tfLOnwbcQsLELfker2hYnjsLrT9PURqK4F7udrWEoZ1I1LymOtLG/
-4tNZ7Mnul3wRC2tCn7FKx8sGJwGh/3li8vZ6ALVJAyOia5TZ/buX0+QZzt6+hPKk
-7MU1WQIA4bUBjtrsqDwro94DvPj3/jBnMZbXr6WZIItLNeVDUcM8oHL807Am97K1
-ueO/f6v1sGAHG6lVPTmtekqPSTWBfwIA7CGFvEyvSALfB8NUa6jtk27NCiw0csql
-kuhCmwXGMVOiryKEfegkIahf2bAd/gnWHPrpWp7bUE20v8YoW22I4wIAhnm5Wr5Q
-Sy7EHDUxmJm5TzadFp9gq08qNzHBpXSYXXJ3JuWcL1/awUqp3tE1I6zZ0hZ38Ia6
-SdBMN88idnhDPqPoiKUEGAECAA8FAkO3YdACGyAFCQHhM4AACgkQQZUwSa4UDezm
-vQP/ZhK+2ly9oI2z7ZcNC/BJRch0/ybQ3haahII8pXXmOThpZohr/LUgoWgCZdXg
-vP6yiszNk2tIs8KphCAw7Lw/qzDC2hEORjWO4f46qk73RAgSqG/GyzI4ltWiDhqn
-vnQCFl3+QFSe4zinqykHnLwGPMXv428d/ZjkIc2ju8dRsn4=
-=CR5w
------END PGP PRIVATE KEY BLOCK-----
-" | openpgp2ssh 4129E89D17C1D591 >/dev/null 2>/dev/null
-
-}
-
 # remove all lines with specified string from specified file
 remove_line() {
     local file
@@ -398,7 +340,7 @@ remove_line() {
     fi
 
     # if the string is in the file...
-    if grep -q -F "$string" "$file" 2> /dev/null ; then
+    if grep -q -F "$string" "$file" 2>/dev/null ; then
        tempfile=$(mktemp "${file}.XXXXXXX") || \
            failure "Unable to make temp file '${file}.XXXXXXX'"
        
@@ -420,12 +362,15 @@ remove_monkeysphere_lines() {
 
     file="$1"
 
-    if [ -z "$file" ] ; then
+    # return error if file does not exist
+    if [ ! -e "$file" ] ; then
        return 1
     fi
 
-    if [ ! -e "$file" ] ; then
-       return 1
+    # just return ok if the file is empty, since there aren't any
+    # lines to remove
+    if [ ! -s "$file" ] ; then
+       return 0
     fi
 
     tempfile=$(mktemp "${file}.XXXXXXX") || \
@@ -446,7 +391,7 @@ translate_ssh_variables() {
     path="$2"
 
     # get the user's home directory
-    userHome=$(getent passwd "$uname" | cut -d: -f6)
+    userHome=$(get_homedir "$uname")
 
     # translate '%u' to user name
     path=${path/\%u/"$uname"}
@@ -466,49 +411,63 @@ test_gpg_expire() {
 check_key_file_permissions() {
     local uname
     local path
-    local stat
-    local access
-    local gAccess
-    local oAccess
-
-    # function to check that the given permission corresponds to writability
-    is_write() {
-       [ "$1" = "w" ]
-    }
 
     uname="$1"
     path="$2"
 
+    if [ "$STRICT_MODES" = 'false' ] ; then
+       log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
+       return 0
+    fi
     log debug "checking path permission '$path'..."
+    "${SYSSHAREDIR}/checkperms" "$uname" "$path"
+}
 
-    # return 255 if cannot stat file
-    if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
-        log error "could not stat path '$path'."
-       return 255
+# return a list of all users on the system
+list_users() {
+    if type getent &>/dev/null ; then
+       # for linux and FreeBSD systems
+       getent passwd | cut -d: -f1
+    elif type dscl &>/dev/null ; then
+       # for Darwin systems
+       dscl localhost -list /Search/Users
+    else
+       failure "Neither getent or dscl is in the path!  Could not determine list of users."
     fi
+}
 
-    owner=$(echo "$stat" | awk '{ print $3 }')
-    gAccess=$(echo "$stat" | cut -c6)
-    oAccess=$(echo "$stat" | cut -c9)
-
-    # return 1 if path has invalid owner
-    if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
-       log error "improper ownership on path '$path'."
-       return 1
+# take one argument, a service name.  in response, print a series of
+# lines, each with a unique numeric port number that might be
+# associated with that service name.  (e.g. in: "https", out: "443")
+# if nothing is found, print nothing, and return 0.
+# 
+# return 1 if there was an error in the search somehow
+get_port_for_service() {
+
+    [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
+        failure $(printf "This is not a valid service name: '%s'" "$1")
+    if type getent &>/dev/null ; then
+        # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
+        (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
+    elif [ -r /etc/services ] ; then
+        # fall back to /etc/services for systems that don't have getent (MacOS?)
+        # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
+        awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
+    else
+        return 1
     fi
+}
 
-    # return 2 if path has group or other writability
-    if is_write "$gAccess" || is_write "$oAccess" ; then
-       log error "improper group or other writability on path '$path'."
-       return 2
-    fi
+# return the path to the home directory of a user
+get_homedir() {
+    local uname=${1:-`whoami`}
+    eval "echo ~${uname}"
+}
 
-    # return zero if all clear, or go to next path
-    if [ "$path" = '/' ] ; then
-       return 0
-    else
-       check_key_file_permissions "$uname" $(dirname "$path")
-    fi
+# return the primary group of a user
+get_primary_group() {
+    local uname=${1:-`whoami`}
+    groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
 }
 
 ### CONVERSION UTILITIES
@@ -519,69 +478,91 @@ gpg2ssh() {
     
     keyID="$1"
 
-    gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
+    gpg --export --no-armor "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
 }
 
 # output known_hosts line from ssh key
 ssh2known_hosts() {
     local host
+    local port
     local key
 
-    host="$1"
+    # FIXME this does not properly deal with IPv6 hosts using the
+    # standard port (because it's unclear whether their final
+    # colon-delimited address section is a port number or an address
+    # string)
+    host=${1%:*}
+    port=${1##*:}
     key="$2"
 
-    echo -n "$host "
-    echo -n "$key" | tr -d '\n'
-    echo " MonkeySphere${DATE}"
+    # specify the host and port properly for new ssh known_hosts
+    # format
+    if [ "$port" != "$host" ] ; then
+       host="[${host}]:${port}"
+    fi
+    printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
 }
 
 # output authorized_keys line from ssh key
 ssh2authorized_keys() {
-    local userID
-    local key
-    
-    userID="$1"
-    key="$2"
+    local koptions="$1"
+    local userID="$2"
+    local key="$3"
 
-    echo -n "$key" | tr -d '\n'
-    echo " MonkeySphere${DATE} ${userID}"
+    if [[ -z "$koptions" ]]; then
+        printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
+    else
+        printf "%s %s MonkeySphere%s %s\n" "$koptions" "$key" "$DATE" "$userID"
+    fi
 }
 
 # convert key from gpg to ssh known_hosts format
 gpg2known_hosts() {
     local host
     local keyID
+    local key
 
     host="$1"
     keyID="$2"
 
+    key=$(gpg2ssh "$keyID")
+
     # NOTE: it seems that ssh-keygen -R removes all comment fields from
     # all lines in the known_hosts file.  why?
     # NOTE: just in case, the COMMENT can be matched with the
     # following regexp:
     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
-    echo -n "$host "
-    gpg2ssh "$keyID" | tr -d '\n'
-    echo " MonkeySphere${DATE}"
+    printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
 }
 
 # convert key from gpg to ssh authorized_keys format
 gpg2authorized_keys() {
     local userID
     local keyID
+    local key
 
     userID="$1"
     keyID="$2"
 
+    key=$(gpg2ssh "$keyID")
+
     # NOTE: just in case, the COMMENT can be matched with the
     # following regexp:
     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
-    gpg2ssh "$keyID" | tr -d '\n'
-    echo " MonkeySphere${DATE} ${userID}"
+    printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
 }
 
 ### GPG UTILITIES
 
+# script to determine if gpg version is equal to or greater than specified version
+is_gpg_version_greater_equal() {
+    local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
+    local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
+       | tr '.' ' ' | sort -g -k1 -k2 -k3 \
+       | tail -1 | tr ' ' '.')
+    [[ "$gpgVersion" == "$latest" ]]
+}
+
 # retrieve all keys with given user id from keyserver
 # FIXME: need to figure out how to retrieve all matching keys
 # (not just first N (5 in this case))
@@ -599,9 +580,13 @@ gpg_fetch_userid() {
     echo 1,2,3,4,5 | \
        gpg --quiet --batch --with-colons \
        --command-fd 0 --keyserver "$KEYSERVER" \
-       --search ="$userID" > /dev/null 2>&1
+       --search ="$userID" &>/dev/null
     returnCode="$?"
 
+    if [ "$returnCode" != 0 ] ; then
+        log error "Failure ($returnCode) searching keyserver $KEYSERVER for user id '$userID'"
+    fi
+
     return "$returnCode"
 }
 
@@ -611,7 +596,7 @@ gpg_fetch_userid() {
 # userid and key policy checking
 # the following checks policy on the returned keys
 # - checks that full key has appropriate valididy (u|f)
-# - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
+# - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
 # - checks that requested user ID has appropriate validity
 # (see /usr/share/doc/gnupg/DETAILS.gz)
 # output is one line for every found key, in the following format:
@@ -619,15 +604,13 @@ gpg_fetch_userid() {
 # flag:sshKey
 #
 # "flag" is an acceptability flag, 0 = ok, 1 = bad
-# "sshKey" is the translated gpg key
+# "sshKey" is the relevant OpenPGP key, in the form accepted by OpenSSH
 #
 # all log output must go to stderr, as stdout is used to pass the
 # flag:sshKey to the calling function.
-#
-# expects global variable: "MODE"
 process_user_id() {
     local returnCode=0
-    local userID
+    local userID="$1"
     local requiredCapability
     local requiredPubCapability
     local gpgOut
@@ -642,14 +625,8 @@ process_user_id() {
     local lastKeyOK
     local fingerprint
 
-    userID="$1"
-
     # set the required key capability based on the mode
-    if [ "$MODE" = 'known_hosts' ] ; then
-       requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
-    elif [ "$MODE" = 'authorized_keys' ] ; then
-       requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
-    fi
+    requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
 
     # fetch the user ID if necessary/requested
@@ -732,14 +709,14 @@ process_user_id() {
                if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
                    log verbose "  * acceptable primary key."
                    if [ -z "$sshKey" ] ; then
-                       log error "    ! primary key could not be translated (not RSA or DSA?)."
+                       log verbose "    ! primary key could not be translated (not RSA?)."
                    else
                        echo "0:${sshKey}"
                    fi
                else
                    log debug "  - unacceptable primary key."
                    if [ -z "$sshKey" ] ; then
-                       log debug "    ! primary key could not be translated (not RSA or DSA?)."
+                       log debug "    ! primary key could not be translated (not RSA?)."
                    else
                        echo "1:${sshKey}"
                    fi
@@ -790,14 +767,14 @@ process_user_id() {
                if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
                    log verbose "  * acceptable sub key."
                    if [ -z "$sshKey" ] ; then
-                       log error "    ! sub key could not be translated (not RSA or DSA?)."
+                       log error "    ! sub key could not be translated (not RSA?)."
                    else
                        echo "0:${sshKey}"
                    fi
                else
                    log debug "  - unacceptable sub key."
                    if [ -z "$sshKey" ] ; then
-                       log debug "    ! sub key could not be translated (not RSA or DSA?)."
+                       log debug "    ! sub key could not be translated (not RSA?)."
                    else
                        echo "1:${sshKey}"
                    fi
@@ -810,6 +787,59 @@ process_user_id() {
     # being processed in the key files over "bad" keys (key flag '1')
 }
 
+# output all valid keys for specified user ID literal
+keys_for_userid() {
+    local userID
+    local noKey=
+    local nKeys
+    local nKeysOK
+    local ok
+    local sshKey
+    local tmpfile
+
+    userID="$1"
+
+    log verbose "processing: $userID"
+
+    nKeys=0
+    nKeysOK=0
+
+    IFS=$'\n'
+    for line in $(process_user_id "${userID}") ; do
+       # note that key was found
+       nKeys=$((nKeys+1))
+
+       ok=$(echo "$line" | cut -d: -f1)
+       sshKey=$(echo "$line" | cut -d: -f2)
+
+        if [ -z "$sshKey" ] ; then
+            continue
+        fi
+
+       # if key OK, output key to stdout
+       if [ "$ok" -eq '0' ] ; then
+           # note that key was found ok
+           nKeysOK=$((nKeysOK+1))
+
+           printf '%s\n' "$sshKey"
+       fi
+    done
+
+    # if at least one key was found...
+    if [ "$nKeys" -gt 0 ] ; then
+       # if ok keys were found, return 0
+       if [ "$nKeysOK" -gt 0 ] ; then
+           return 0
+       # else return 2
+       else
+           return 2
+       fi
+    # if no keys were found, return 1
+    else
+       return 1
+    fi
+}
+
 # process a single host in the known_host file
 process_host_known_hosts() {
     local host
@@ -822,7 +852,7 @@ process_host_known_hosts() {
     local tmpfile
 
     # set the key processing mode
-    export MODE='known_hosts'
+    export REQUIRED_KEY_CAPABILITY="$REQUIRED_HOST_KEY_CAPABILITY"
 
     host="$1"
     userID="ssh://${host}"
@@ -855,13 +885,25 @@ process_host_known_hosts() {
 
            # hash if specified
            if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
+                if (type ssh-keygen >/dev/null) ; then
                # FIXME: this is really hackish cause ssh-keygen won't
                # hash from stdin to stdout
-               tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
-               ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
-               ssh-keygen -H -f "$tmpfile" 2> /dev/null
-               cat "$tmpfile" >> "$KNOWN_HOSTS"
-               rm -f "$tmpfile" "${tmpfile}.old"
+                   tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
+                   ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
+                   ssh-keygen -H -f "$tmpfile" 2>/dev/null
+                   cat "$tmpfile" >> "$KNOWN_HOSTS"
+                   rm -f "$tmpfile" "${tmpfile}.old"
+                else
+       # FIXME: we could do this without needing ssh-keygen.  hashed
+       # known_hosts looks like: |1|X|Y where 1 means SHA1 (nothing
+       # else is defined in openssh sources), X is the salt (same
+       # length as the digest output), base64-encoded, and Y is the
+       # digested hostname (also base64-encoded).
+
+       # see hostfile.{c,h} in openssh sources.
+
+                    failure "Cannot hash known_hosts as requested"
+                fi
            else
                ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
            fi
@@ -897,6 +939,7 @@ update_known_hosts() {
     local nHostsBAD
     local fileCheck
     local host
+    local newUmask
 
     # the number of hosts specified on command line
     nHosts="$#"
@@ -906,10 +949,20 @@ update_known_hosts() {
 
     # touch the known_hosts file so that the file permission check
     # below won't fail upon not finding the file
-    (umask 0022 && touch "$KNOWN_HOSTS")
+    if [ ! -f "$KNOWN_HOSTS" ]; then
+       # make sure to create any files or directories with the appropriate write bits turned off:
+       newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
+       [ -d $(dirname "$KNOWN_HOSTS") ] \
+           || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$KNOWN_HOSTS") ) \
+           || failure "Could not create path to known_hosts file '$KNOWN_HOSTS'"
+       # make sure to create this file with the appropriate bits turned off:
+       (umask "$newUmask" && touch "$KNOWN_HOSTS") \
+           || failure "Unable to create known_hosts file '$KNOWN_HOSTS'"
+    fi
 
     # check permissions on the known_hosts file path
-    check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
+    check_key_file_permissions $(whoami) "$KNOWN_HOSTS" \
+       || failure "Bad permissions governing known_hosts file '$KNOWN_HOSTS'"
 
     # create a lockfile on known_hosts:
     lock create "$KNOWN_HOSTS"
@@ -917,7 +970,7 @@ update_known_hosts() {
     trap "lock remove $KNOWN_HOSTS" EXIT
 
     # note pre update file checksum
-    fileCheck="$(file_hash "$KNOWN_HOSTS")"
+    fileCheck=$(file_hash "$KNOWN_HOSTS")
 
     for host ; do
        # process the host
@@ -970,7 +1023,8 @@ process_known_hosts() {
        failure "known_hosts file '$KNOWN_HOSTS' does not exist."
     fi
 
-    log debug "processing known_hosts file..."
+    log debug "processing known_hosts file:"
+    log debug " $KNOWN_HOSTS"
 
     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
 
@@ -988,15 +1042,17 @@ process_known_hosts() {
 # process uids for the authorized_keys file
 process_uid_authorized_keys() {
     local userID
+    local koptions
     local nKeys
     local nKeysOK
     local ok
     local sshKey
 
     # set the key processing mode
-    export MODE='authorized_keys'
+    export REQUIRED_KEY_CAPABILITY="$REQUIRED_USER_KEY_CAPABILITY"
 
-    userID="$1"
+    koptions="$1"
+    userID="$2"
 
     log verbose "processing: $userID"
 
@@ -1023,7 +1079,7 @@ process_uid_authorized_keys() {
            # note that key was found ok
            nKeysOK=$((nKeysOK+1))
 
-           ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
+           ssh2authorized_keys "$koptions" "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
        fi
     done
 
@@ -1051,15 +1107,23 @@ update_authorized_keys() {
     local nIDsOK
     local nIDsBAD
     local fileCheck
+    local x koptions
+    declare -i argtype
+
+    if (( $# % 2 )); then log error "Bad number of arguments; this should never happen."; return 1; fi
 
     # the number of ids specified on command line
-    nIDs="$#"
+    (( nIDs=$#/2 ))
+    (( argtype=0 ))
 
     nIDsOK=0
     nIDsBAD=0
 
+    log debug "updating authorized_keys file:"
+    log debug " $AUTHORIZED_KEYS"
+
     # check permissions on the authorized_keys file path
-    check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
+    check_key_file_permissions $(whoami) "$AUTHORIZED_KEYS" || failure
 
     # create a lockfile on authorized_keys
     lock create "$AUTHORIZED_KEYS"
@@ -1072,23 +1136,30 @@ update_authorized_keys() {
     # remove any monkeysphere lines from authorized_keys file
     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
 
-    for userID ; do
-       # process the user ID, change return code if key not found for
-       # user ID
-       process_uid_authorized_keys "$userID" || returnCode="$?"
-
-       # note the result
-       case "$returnCode" in
-           0)
-               nIDsOK=$((nIDsOK+1))
-               ;;
-           2)
-               nIDsBAD=$((nIDsBAD+1))
-               ;;
-       esac
-
-       # touch the lockfile, for good measure.
-       lock touch "$AUTHORIZED_KEYS"
+    for x; do
+        (( argtype++ ))
+        if (( $argtype % 2 )); then
+            koptions="$x"
+        else
+            userID="$x"
+
+           # process the user ID, change return code if key not found
+           # for user ID
+           process_uid_authorized_keys "$koptions" "$userID" || returnCode="$?"
+
+           # note the result
+           case "$returnCode" in
+               0)
+                   nIDsOK=$((nIDsOK+1))
+                   ;;
+               2)
+                   nIDsBAD=$((nIDsBAD+1))
+                   ;;
+           esac
+
+           # touch the lockfile, for good measure.
+           lock touch "$AUTHORIZED_KEYS"
+       fi
     done
 
     # remove the lockfile and the trap
@@ -1121,22 +1192,27 @@ update_authorized_keys() {
 # process an authorized_user_ids file for authorized_keys
 process_authorized_user_ids() {
     local line
-    local nline
-    local userIDs
+    declare -i nline
+    declare -a userIDs
+    declare -a koptions
+    declare -a export_array
 
     authorizedUserIDs="$1"
 
+    (( nline=0 ))
+
     # exit if the authorized_user_ids file is empty
     if [ ! -e "$authorizedUserIDs" ] ; then
        failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
     fi
 
-    # check permissions on the authorized_user_ids file path
-    check_key_file_permissions "$USER" "$authorizedUserIDs" || failure
+    log debug "processing authorized_user_ids file:"
+    log debug " $authorizedUserIDs"
 
-    log debug "processing authorized_user_ids file..."
+    # check permissions on the authorized_user_ids file path
+    check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
 
-    if ! meat "$authorizedUserIDs" > /dev/null ; then
+    if ! meat "$authorizedUserIDs" >/dev/null ; then
        log debug " no user IDs to process."
        return
     fi
@@ -1146,20 +1222,50 @@ process_authorized_user_ids() {
     # extract user IDs from authorized_user_ids file
     IFS=$'\n'
     for line in $(meat "$authorizedUserIDs") ; do
-       userIDs["$nline"]="$line"
-       nline=$((nline+1))
+       case "$line" in
+           (" "*|$'\t'*)
+               if [[ -z ${koptions[${nline}]} ]]; then
+                   koptions[${nline}]=$(echo $line | sed 's/^[         ]*//;s/[        ]$//;')
+               else
+                   koptions[${nline}]="${koptions[${nline}]},$(echo $line | sed 's/^[  ]*//;s/[        ]$//;')"
+               fi
+               ;;
+            (*)
+               ((nline++))
+               userIDs[${nline}]="$line"
+               unset koptions[${nline}] || true
+               ;;
+       esac
     done
 
-    update_authorized_keys "${userIDs[@]}"
+    for i in $(seq 1 $nline); do
+        export_array+=("${koptions[$i]}" "${userIDs[$i]}")
+    done
+
+    update_authorized_keys "${export_array[@]}"
 }
 
 # takes a gpg key or keys on stdin, and outputs a list of
 # fingerprints, one per line:
 list_primary_fingerprints() {
     local fake=$(msmktempdir)
-    GNUPGHOME="$fake" gpg --no-tty --quiet --import
+    trap "rm -rf $fake" EXIT
+    GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
        awk -F: '/^fpr:/{ print $10 }'
+    trap - EXIT
+    rm -rf "$fake"
+}
+
+# takes an OpenPGP key or set of keys on stdin, a fingerprint or other
+# key identifier as $1, and outputs the gpg-formatted information for
+# the requested keys from the material on stdin
+get_cert_info() {
+    local fake=$(msmktempdir)
+    trap "rm -rf $fake" EXIT
+    GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
+    GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
+    trap - EXIT
     rm -rf "$fake"
 }