#!/usr/bin/env bash # monkeysphere: MonkeySphere client tool # # The monkeysphere scripts are written by: # Jameson Rollins <jrollins@fifthhorseman.net> # Jamie McClelland <jm@mayfirst.org> # Daniel Kahn Gillmor <dkg@fifthhorseman.net> # # They are Copyright 2008, and are all released under the GPL, version 3 # or later. ######################################################################## PGRM=$(basename $0) SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"} export SYSSHAREDIR . "${SYSSHAREDIR}/common" || exit 1 # UTC date in ISO 8601 format if needed DATE=$(date -u '+%FT%T') # unset some environment variables that could screw things up unset GREP_OPTIONS # default return code RETURN=0 # set the file creation mask to be only owner rw umask 077 ######################################################################## # FUNCTIONS ######################################################################## usage() { cat <<EOF >&2 usage: $PGRM <subcommand> [options] [args] Monkeysphere client tool. subcommands: update-known_hosts (k) [HOST]... update known_hosts file update-authorized_keys (a) update authorized_keys file gen-subkey (g) [KEYID] generate an authentication subkey --length (-l) BITS key length in bits (2048) --expire (-e) EXPIRE date to expire subkey-to-ssh-agent (s) store authentication subkey in ssh-agent help (h,?) this help EOF } # generate a subkey with the 'a' usage flags set gen_subkey(){ local keyLength local keyExpire local keyID local gpgOut local userID # set default key parameter values keyLength= keyExpire= # get options TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o l:e: -l length:,expire: -n "$PGRM" -- "$@") || failure "getopt failed! Does your getopt support GNU-style long options?" if [ $? != 0 ] ; then exit 1 fi # Note the quotes around `$TEMP': they are essential! eval set -- "$TEMP" while true ; do case "$1" in -l|--length) keyLength="$2" shift 2 ;; -e|--expire) keyExpire="$2" shift 2 ;; --) shift ;; *) break ;; esac done if [ -z "$1" ] ; then # find all secret keys keyID=$(gpg --with-colons --list-secret-keys | grep ^sec | cut -f5 -d: | sort -u) # if multiple sec keys exist, fail if (( $(echo "$keyID" | wc -l) > 1 )) ; then echo "Multiple secret keys found:" echo "$keyID" failure "Please specify which primary key to use." fi else keyID="$1" fi if [ -z "$keyID" ] ; then failure "You have no secret key available. You should create an OpenPGP key before joining the monkeysphere. You can do this with: gpg --gen-key" fi # get key output, and fail if not found gpgOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons \ "$keyID") || failure # fail if multiple sec lines are returned, which means the id # given is not unique if [ $(echo "$gpgOut" | grep -c '^sec:') -gt '1' ] ; then failure "Key ID '$keyID' is not unique." fi # prompt if an authentication subkey already exists if echo "$gpgOut" | egrep "^(sec|ssb):" | cut -d: -f 12 | grep -q a ; then echo "An authentication subkey already exists for key '$keyID'." read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N} if [ "${OK/y/Y}" != 'Y' ] ; then failure "aborting." fi fi # set subkey defaults # prompt about key expiration if not specified keyExpire=$(get_gpg_expiration "$keyExpire") # generate the list of commands that will be passed to edit-key editCommands=$(cat <<EOF addkey 7 S E A Q $keyLength $keyExpire save EOF ) log verbose "generating subkey..." fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX) (umask 077 && mkfifo "$fifoDir/pass") echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" & passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass" rm -rf "$fifoDir" wait log verbose "done." } function subkey_to_ssh_agent() { # try to add all authentication subkeys to the agent: local sshaddresponse local secretkeys local authsubkeys local workingdir local keysuccess local subkey local publine local kname if ! test_gnu_dummy_s2k_extension ; then failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys. You may want to consider patching or upgrading to GnuTLS 2.6 or later. For more details, see: http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html" fi # if there's no agent running, don't bother: if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then failure "No ssh-agent available." fi # and if it looks like it's running, but we can't actually talk to # it, bail out: ssh-add -l >/dev/null sshaddresponse="$?" if [ "$sshaddresponse" = "2" ]; then failure "Could not connect to ssh-agent" fi # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945): secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \ grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }') if [ -z "$secretkeys" ]; then failure "You have no secret keys in your keyring! You might want to run 'gpg --gen-key'." fi authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \ --fingerprint --fingerprint $secretkeys | \ cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \ grep '^fpr::' | cut -f3 -d: | sort -u) if [ -z "$authsubkeys" ]; then failure "no authentication-capable subkeys available. You might want to 'monkeysphere gen-subkey'" fi workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX) umask 077 mkfifo "$workingdir/passphrase" keysuccess=1 # FIXME: we're currently allowing any other options to get passed # through to ssh-add. should we limit it to known ones? For # example: -d or -c and/or -t <lifetime> for subkey in $authsubkeys; do # choose a label by which this key will be known in the agent: # we are labelling the key by User ID instead of by # fingerprint, but filtering out all / characters to make sure # the filename is legit. primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /) #kname="[monkeysphere] $primaryuid" kname="$primaryuid" if [ "$1" = '-d' ]; then # we're removing the subkey: gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" (cd "$workingdir" && ssh-add -d "$kname") else # we're adding the subkey: mkfifo "$workingdir/$kname" gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \ --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \ --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" & (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )& passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase" wait %2 fi keysuccess="$?" rm -f "$workingdir/$kname" done rm -rf "$workingdir" # FIXME: sort out the return values: we're just returning the # success or failure of the final authentication subkey in this # case. What if earlier ones failed? exit "$keysuccess" } ######################################################################## # MAIN ######################################################################## # unset variables that should be defined only in config file unset KEYSERVER unset CHECK_KEYSERVER unset KNOWN_HOSTS unset HASH_KNOWN_HOSTS unset AUTHORIZED_KEYS # load global config [ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf" # set monkeysphere home directory MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"} mkdir -p -m 0700 "$MONKEYSPHERE_HOME" # load local config [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG" # set empty config variables with ones from the environment, or from # config file, or with defaults LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}} GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}} KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"} # if keyserver not specified in env or monkeysphere.conf, # look in gpg.conf if [ -z "$KEYSERVER" ] ; then if [ -f "${GNUPGHOME}/gpg.conf" ] ; then KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }') fi fi # if it's still not specified, use the default KEYSERVER=${KEYSERVER:="subkeys.pgp.net"} CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}} KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}} HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}} AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}} # other variables not in config file AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"} REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"} REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"} # export GNUPGHOME and make sure gpg home exists with proper # permissions export GNUPGHOME mkdir -p -m 0700 "$GNUPGHOME" export LOG_LEVEL # get subcommand COMMAND="$1" [ "$COMMAND" ] || failure "Type '$PGRM help' for usage." shift case $COMMAND in 'update-known_hosts'|'update-known-hosts'|'k') MODE='known_hosts' # check permissions on the known_hosts file path if ! check_key_file_permissions "$USER" "$KNOWN_HOSTS" ; then failure "Improper permissions on known_hosts file path." fi # if hosts are specified on the command line, process just # those hosts if [ "$1" ] ; then update_known_hosts "$@" RETURN="$?" # otherwise, if no hosts are specified, process every host # in the user's known_hosts file else # exit if the known_hosts file does not exist if [ ! -e "$KNOWN_HOSTS" ] ; then log error "known_hosts file '$KNOWN_HOSTS' does not exist." exit fi process_known_hosts RETURN="$?" fi ;; 'update-authorized_keys'|'update-authorized-keys'|'a') MODE='authorized_keys' # check permissions on the authorized_user_ids file path if ! check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" ; then failure "Improper permissions on authorized_user_ids file path." fi # check permissions on the authorized_keys file path if ! check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" ; then failure "Improper permissions on authorized_keys file path." fi # exit if the authorized_user_ids file is empty if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist." exit fi # process authorized_user_ids file process_authorized_user_ids "$AUTHORIZED_USER_IDS" RETURN="$?" ;; 'gen-subkey'|'g') gen_subkey "$@" ;; 'subkey-to-ssh-agent'|'s') subkey_to_ssh_agent "$@" ;; '--help'|'help'|'-h'|'h'|'?') usage ;; *) failure "Unknown command: '$COMMAND' Type '$PGRM help' for usage." ;; esac exit "$RETURN"