#!/bin/sh # # vim: set ts=8 sw=2 sts=2 et ai ft=sh: # Shell script to install your identity.pub on a remote machine # Takes the remote machine name as an argument. # Obviously, the remote machine must accept password authentication, # or one of the other keys in your ssh-agent, for this to work. ME="`basename \"$0\"`" SSH_DIR="${SSH_DIR:-${HOME}/.ssh}" ID_RSA="id_rsa.pub" ID_DSA="id_dsa.pub" ID_RSA1="identity.pub" SKIP_AGENT="no" SSH_COPY_ID_DEBUG="${SSH_COPY_ID_DEBUG}" VERBOSE="yes" ID_FILE="" IDENTITY="" #################### # Utility functions Say() { echo "--> $*" } Progress() { [ x"${VERBOSE}" = x"yes" ] && Say "$* ..." } Finish() { [ x"${VERBOSE}" = x"yes" ] && Say "Done." } Debug() { if [ x"${SSH_COPY_ID_DEBUG}" = x"yes" ]; then for i in ${1:+"$@"}; do if [ x"${i}" = x"" ]; then continue fi echo "${ME}: debug: ${i}" done fi } Msg () { local msglvl="$1"; shift local msgcount=0 for i in ${1:+"$@"}; do if [ x"${i}" = x"" ]; then continue fi msgcount=`expr ${msgcount} + 1` if [ ${msgcount} -eq 1 ]; then echo "${ME}: ${msglvl}: ${i}" >&2 else echo " -- ${i}" >&2 fi done if [ x"${msglvl}" = x"fatal" ]; then exit 1 fi } Notice() { Msg notice ${1:+"$@"}; } Warning() { Msg warning ${1:+"$@"}; } Error() { Msg error ${1:+"$@"}; } Fatal() { Msg fatal ${1:+"$@"}; } #################### # Locate a suitable AWK implementation # # BEWARE: If you make changes to this, you may also need to make the same # or equivalent changes to the definition in the ssh command # executed on the remote system near the end of this script! FindAwk() { local path_to_awk if [ x"${AWK}" = x"" ]; then for i in gawk mawk nawk awk; do path_to_awk="`type \"${i}\" 2>/dev/null `" case "${path_to_awk}" in "${i} is "*) AWK="`echo \"${path_to_awk}\" |sed -e \"s/${i} is *//\" -e \"s/a tracked alias for *//\"`" break ;; *) ;; esac done if [ x"${AWK}" = x"" ]; then return 1 else return 0 fi fi } #################### # Identity functions ValidatePubkeyFormat() { local pubkey="$1" local ssh2_pubkey_pattern='[ ][ ]*AAAA[+/0-9=A-Za-z][+/0-9=A-Za-z]*\([ ].*\)\?$' echo "${pubkey}" |grep '^ssh-rsa'"${ssh2_pubkey_pattern}" >/dev/null 2>&1 if [ $? -eq 0 ]; then echo "rsa" return 0 fi echo "${pubkey}" |grep '^ssh-dss'"${ssh2_pubkey_pattern}" >/dev/null 2>&1 if [ $? -eq 0 ]; then echo "dsa" return 0 fi echo "${pubkey}" |grep '^[1-9][0-9]\?[0-9]\?[0-9]\?[0-9]\?[ ][ ]*[1-9][0-9]*[ ][ ]*[1-9][0-9]*\([ ].*\)\?$' >/dev/null 2>&1 if [ $? -eq 0 ]; then echo "rsa1" return 0 fi return 1 } GetIdentityFromAgent() { local agent_identity local id_type if [ x"${SSH_AUTH_SOCK}" != x"" ] ; then agent_identity="`ssh-add -L |grep -v '^The agent has no identities\.$' 2>/dev/null`" if [ x"${agent_identity}" = x"" ]; then Debug "An ssh-agent appears to be running, but no identities are added." else # TODO: ssh-agent could have more than one identity added; # TODO: this should be handled here, and probably later on as well. id_type="`ValidatePubkeyFormat \"${agent_identity}\"`" if [ x"${id_type}" = x"" ]; then Warning "The identity loaded in ssh-agent appears to be invalid." return 1 else Progress "Using ${id_type} identity from ssh-agent" IDENTITY="${agent_identity}" return 0 fi fi fi return 1 } GetIdentityFromFile() { local id_file="$1" local file_identity local id_type if [ -f "${id_file}" ]; then file_identity="`cat \"${id_file}\"`" id_type="`ValidatePubkeyFormat \"${file_identity}\"`" if [ x"${id_type}" = x"" ]; then Warning "The identity in '${id_file}' appears to be invalid." return 1 else Progress "Using ${id_type} identity from '${id_file}'" IDENTITY="${file_identity}" return 0 fi fi return 1 } #################### # Option-processing functions NeedArgs() { local option="$1" local have="$2" local need="$3" local arg_label if [ ${have} -lt ${need} ]; then need="`expr ${need} - 1`" if [ ${need} -eq 1 ]; then arg_label="parameter" else arg_label="parameters" fi Fatal "Option '${option}' requires ${need} ${arg_label}." fi } #################### # Printing help/usage messages PrintShortHelp() { echo "${ME}: Use the '--help' option for information on how to use this program." exit 1 } PrintHelp() { cat <] [--] [@] Copy the SSH identity in to 's ~/.ssh/authorized_keys file on . If neither '-f' nor '-i' is supplied, copy the identities loaded in a running ssh-agent. If '-f' is used, or if no agent is running, or if no identities are loaded in the running agent, look for identities in the following order and copy the first one found: ~/.ssh/${ID_RSA} ~/.ssh/${ID_DSA} ~/.ssh/${ID_RSA1} Options: -i or --identity= Copy the SSH identity from . -A or --skip-agent Bypass ssh-agent in search for identities. -q or --quiet Only print warnings or error messages. Environment: SSH_DIR If set to a directory path, identity files are searched for there instead of in ~/.ssh. EOF exit 1 } #################### # Main program while [ $# -gt 0 ]; do case "$1" in -h|--help|-\?) PrintHelp ;; -q|--quiet) VERBOSE="no" shift ;; -A|--skip-agent|--no-agent) SKIP_AGENT="yes" shift ;; -i|--identity) NeedArgs "$1" $# 2 shift ID_FILE="$1" shift ;; --identity=*) ID_FILE="`echo \"$1\" |sed -e 's/^--identity=//'`" shift case "${ID_FILE}" in *.pub) : ;; *) ID_FILE="${ID_FILE}.pub" ;; esac ;; --) shift break ;; -*) Fatal "Unrecognized option '$1'." ;; *) break ;; esac done if [ $# -lt 1 ]; then Error "Please specify a host to copy your SSH identity to." PrintShortHelp elif [ $# -gt 1 ]; then Error "Too many hosts specified on command line." PrintShortHelp fi FindAwk if [ $? -ne 0 ]; then Fatal "Unable to find a suitable \"awk\" interpreter." \ "Please set the AWK environment variable to the location of your" \ "system's \"gawk\", \"mawk\", \"nawk\", or \"awk\" command. For example:" \ "\"env AWK=/usr/local/bin/gawk ${ME}\"" fi if [ x"${ID_FILE}" = x"" ]; then if [ x"${SKIP_AGENT}" = x"no" ]; then GetIdentityFromAgent fi if [ x"${IDENTITY}" = x"" ]; then for i in "${ID_RSA}" "${ID_DSA}" "${ID_RSA1}"; do GetIdentityFromFile "${SSH_DIR}/${i}" if [ $? -eq 0 ]; then break fi done fi else GetIdentityFromFile "${ID_FILE}" fi if [ x"${IDENTITY}" = x"" ]; then if [ x"${ID_FILE}" = x"" ]; then if [ x"${SKIP_AGENT}" = x"yes" ] || [ x"${SSH_AUTH_SOCK}" = x"" ]; then comma="" agent_message="" else comma="," agent_message="no identities added to ssh-agent, " fi Fatal "No identities found in '${SSH_DIR}'${comma}" \ "${agent_message}and no identity given on command line." \ "Please use the '-i' option to specify an SSH identity to copy." else Fatal "Unable to find a valid identity in '${ID_FILE}'." fi fi REMOTE_HOST="$1" Progress "Copying identity to ${REMOTE_HOST}'s authorized_keys file" echo "${IDENTITY}" |ssh "${REMOTE_HOST}" 'sh '"${SSH_COPY_ID_DEBUG:+-x}"' -c '\'' umask 077 VERBOSE="'\'"${VERBOSE}"\''" ME="'\'"${ME}"\''" REMOTE_HOST="'\'"`echo \"${REMOTE_HOST}\" |sed -e 's/^[^@]*@//'`"\''" AWK="'\'"${REMOTE_AWK}"\''" AUTHKEYS_NEW="authorized_keys.$$" AUTHKEYS_OLD="authorized_keys.old" TEMP_ID="temp_id.$$" Progress() { if [ x"${VERBOSE}" = x"yes" ]; then echo "--> $* ..."; fi; } Msg () { local msglvl="$1"; shift local msgcount=0 for i in ${1:+"$@"}; do if [ x"${i}" = x"" ]; then continue fi msgcount=`expr ${msgcount} + 1` if [ ${msgcount} -eq 1 ]; then echo "${ME}: ${msglvl}: ${i}" >&2 else echo " -- ${i}" >&2 fi done if [ x"${msglvl}" = x"fatal" ]; then exit 1 fi } Notice() { Msg notice ${1:+"$@"}; } Warning() { Msg warning ${1:+"$@"}; } Fatal() { Msg fatal ${1:+"$@"}; } FindAwk() { local path_to_awk if [ x"${AWK}" = x"" ]; then for i in gawk mawk nawk awk; do path_to_awk="`type \"${i}\" 2>/dev/null `" case "${path_to_awk}" in "${i} is "*) AWK="`echo \"${path_to_awk}\" |sed -e \"s/${i} is *//\" -e \"s/a tracked alias for *//\"`" break ;; *) ;; esac done [ x"${AWK}" = x"" ] && return 1 || return 0 fi } ParsePubkey() { # Now pay attention to these quotes, folks, we are already inside # a single-quoted string inside a single quoted string, # and we are creating another single-quoted string inside that. "${AWK}" '\'\\\'\''{ Prefix = "" Key = "" Comment = "" if ($0 ~ /^[ ]*(#.*)?$/) { Type = "blank" } else if ($1 ~ /^ssh-(dss|rsa)$/) { Type = $1 sub(/^ssh-/, "", Type) Prefix = $1 Key = $2 if (NF > 2) { Comment = $3 for (i = 4; i <= NF; i++) { Comment = Comment " " $i } } } else if ((NF >= 3) && ($1 ~ /^[1-9][0-9]?[0-9]?[0-9]?[0-9]?/) && ($2 ~ /^[1-9][0-9]*/) && ($3 ~ /^[0-9][0-9]*/)) { # RSA1 keys with 2^n bits, 0 < n < 17, seem adequate Type = "rsa1" Prefix = $1 " " $2 Key = $3 if (NF > 3) { Comment = $4 for (i = 5; i <= NF; i++) { Comment = Comment " " $i } } } else { Type = "unknown" } # Escape shell metacharacters that matter within double quotes gsub(/["\$\\`]/, "\\\\&", Type) gsub(/["\$\\`]/, "\\\\&", Prefix) gsub(/["\$\\`]/, "\\\\&", Key) gsub(/["\$\\`]/, "\\\\&", Comment) printf("ID_TYPE=\"%s\"; ID_PREFIX=\"%s\"; ID_PUBKEY=\"%s\"; ID_COMMENT=\"%s\"\n", Type, Prefix, Key, Comment) }'\'\\\'\'' } Cleanup() { test x"${AUTHKEYS_NEW}" = x"" || { rm -f "${AUTHKEYS_NEW}" unset AUTHKEYS_NEW } test x"${TEMP_ID}" = x"" || { rm -f "${TEMP_ID}" unset TEMP_ID } test x"$1" = x"0" || Fatal "Exiting on signal $1" } test -d .ssh || { Progress "Creating ~/.ssh directory"; mkdir .ssh; } cd .ssh || Fatal "Unable to change to ~/.ssh on ${REMOTE_HOST}." test -f authorized_keys || touch authorized_keys \ || Fatal "Unable to create authorized_keys file." for i in 0 1 2 15; do trap "Cleanup ${i}" ${i} done cat authorized_keys >"${AUTHKEYS_NEW}" \ || Fatal "Unable to write to temporary authorized_keys file" FindAwk \ || Fatal "Unable to find a suitable \"awk\" interpreter." \ "Please set the REMOTE_AWK environment variable to the location of the" \ "\"gawk\", \"mawk\", \"nawk\", or \"awk\" command on ${REMOTE_HOST}." \ "For example: \"env REMOTE_AWK=/usr/local/bin/gawk ${ME}\"" ADDED_KEYS=0 while read IDENTITY; do echo "${IDENTITY}" >"${TEMP_ID}" eval `cat "${TEMP_ID}" |ParsePubkey` case "${ID_TYPE}" in blank) Warning "Remote end received blank identity" ;; rsa|dss|rsa1) ID_FP="`ssh-keygen -l -f \"${TEMP_ID}\" |\"${AWK}\" '\'\\\'\''{ print $1, $2 }'\'\\\'\''`" grep -F "${ID_PREFIX} ${ID_PUBKEY}" authorized_keys >/dev/null 2>&1 if [ $? -eq 0 ]; then Notice "This identity is already present in authorized_keys:" \ "${ID_FP} ${ID_COMMENT}" else Progress "Adding identity to authorized_keys" cat "${TEMP_ID}" >>"${AUTHKEYS_NEW}" \ || Fatal "Unable to write to temporary authorized_keys file" ADDED_KEYS=`expr ${ADDED_KEYS} + 1` fi ;; *) Warning "Remote end received unknown key type" ;; esac done ( test ${ADDED_KEYS} -gt 0 || exit 1 ) \ && rm -f "${AUTHKEYS_OLD}" \ && ln authorized_keys "${AUTHKEYS_OLD}" \ && mv -f "${AUTHKEYS_NEW}" authorized_keys '\' || exit 1 Finish if [ x"${VERBOSE}" = x"yes" ]; then cat <