#!/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 version (v) show version number 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 while true ; do case "$1" in -l|--length) keyLength="$2" shift 2 ;; -e|--expire) keyExpire="$2" shift 2 ;; *) if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then failure "Unknown option '$1'. Type '$PGRM help' for usage." fi break ;; esac done case "$#" in 0) gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:') ;; 1) gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure ;; *) failure "You must specify only a single primary key ID." ;; esac # check that only a single secret key was found case $(echo "$gpgSecOut" | grep -c '^sec:') in 0) failure "No secret keys found. Create an OpenPGP key with the following command: gpg --gen-key" ;; 1) keyID=$(echo "$gpgSecOut" | cut -d: -f5) ;; *) echo "Multiple primary secret keys found:" echo "$gpgSecOut" | cut -d: -f5 failure "Please specify which primary key to use." ;; esac # check that a valid authentication key does not already exist IFS=$'\n' for line in $(gpg --quiet --fixed-list-mode --list-keys --with-colons "$keyID") ; do type=$(echo "$line" | cut -d: -f1) validity=$(echo "$line" | cut -d: -f2) usage=$(echo "$line" | cut -d: -f12) # look at keys only if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then continue fi # check for authentication capability if ! check_capability "$usage" 'a' ; then continue fi # if authentication key is valid, prompt to continue if [ "$validity" = 'u' ] ; then echo "A valid authentication key already exists for primary 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 break fi done # 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" & # FIXME: this needs to fail more gracefully if the passphrase is incorrect passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass" rm -rf "$fifoDir" wait log verbose "done." } 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' # 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") # check permissions on the known_hosts file path check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure # 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 check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure # check permissions on the authorized_keys file path check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure # 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 "$@" ;; 'version'|'v') echo "$VERSION" ;; '--help'|'help'|'-h'|'h'|'?') usage ;; *) failure "Unknown command: '$COMMAND' Type '$PGRM help' for usage." ;; esac exit "$RETURN"