#!/bin/sh # rhesus: monkeysphere authorized_keys/known_hosts generating script # # Written by # Jameson Rollins # # Copyright 2008, released under the GPL, version 3 or later # all caps variables are meant to be user supplied (ie. from config # file) and are considered global PGRM=$(basename $0) # date in UTF format if needed DATE=$(date -u '+%FT%T') # unset some environment variables that could screw things up GREP_OPTIONS= ######################################################################## # FUNCTIONS ######################################################################## usage() { cat <&2 exit ${2:-'1'} } # write output to stdout log() { echo -n "ms: " echo "$@" } # write output to stderr loge() { echo -n "ms: " 1>&2 echo "$@" 1>&2 } # cut out all comments(#) and blank lines from standard input meat() { grep -v -e "^[[:space:]]*#" -e '^$' } # cut a specified line from standard input cutline() { head --line="$1" | tail -1 } # retrieve all keys with given user id from keyserver # FIXME: need to figure out how to retrieve all matching keys # (not just first 5) gpg_fetch_keys() { local id id="$1" echo 1,2,3,4,5 | \ gpg --quiet --batch --command-fd 0 --with-colons \ --keyserver "$KEYSERVER" \ --search ="$id" >/dev/null 2>&1 } # check that characters are in a string (in an AND fashion). # used for checking key capability # check_capability capability a [b...] check_capability() { local capability local capcheck capability="$1" shift 1 for capcheck ; do if echo "$capability" | grep -q -v "$capcheck" ; then return 1 fi done return 0 } # convert escaped characters from gpg output back into original # character # FIXME: undo all escape character translation in with-colons gpg output unescape() { echo "$1" | sed 's/\\x3a/:/' } # stand in until we get dkg's gpg2ssh program gpg2ssh_tmp() { local mode local keyID local userID local host mode="$1" keyID="$2" userID="$3" if [ "$mode" = 'authorized_keys' ] ; then gpgkey2ssh "$keyID" | sed -e "s/COMMENT/${userID}/" # 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}$' elif [ "$mode" = 'known_hosts' ] ; then host=$(echo "$userID" | sed -e "s|ssh://||") echo -n "$host "; gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere${DATE}/" fi } # 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 that particular desired user id has appropriate validity # see /usr/share/doc/gnupg/DETAILS.gz # expects global variable: "mode" process_user_id() { local userID local cacheDir local requiredPubCapability local gpgOut local line local type local validity local keyid local uidfpr local capability local keyOK local pubKeyID local uidOK local keyIDs local userIDHash local keyID userID="$1" cacheDir="$2" requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]") # fetch keys from keyserver, return 1 if none found gpg_fetch_keys "$userID" || return 1 # output gpg info for (exact) userid and store gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \ ="$userID" 2> /dev/null) # return 1 if there only "tru" lines are output from gpg if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then return 1 fi # loop over all lines in the gpg output and process. # need to do it this way (as opposed to "while read...") so that # variables set in loop will be visible outside of loop for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do # read the contents of the line type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1) validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2) keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5) uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10) capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12) # process based on record type case $type in 'pub') # primary keys # new key, wipe the slate keyOK= pubKeyID= uidOK= keyIDs= pubKeyID="$keyid" # check primary key validity if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then loge " unacceptable primary key validity ($validity)." continue fi # check capability is not Disabled... if check_capability "$capability" 'D' ; then loge " key disabled." continue fi # check overall key capability # must be Encryption and Authentication if ! check_capability "$capability" $requiredPubCapability ; then loge " unacceptable primary key capability ($capability)." continue fi # mark if primary key is acceptable keyOK=true # add primary key ID to key list if it has required capability if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then keyIDs[${#keyIDs[*]}]="$keyid" fi ;; 'uid') # user ids # check key ok and we have key fingerprint if [ -z "$keyOK" ] ; then continue fi # check key validity if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then continue fi # check the uid matches if [ "$(unescape "$uidfpr")" != "$userID" ] ; then continue fi # mark if uid acceptable uidOK=true ;; 'sub') # sub keys # add sub key ID to key list if it has required capability if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then keyIDs[${#keyIDs[*]}]="$keyid" fi ;; esac done # hash userid for cache file name userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }') # touch/clear key cache file # (will be left empty if there are noacceptable keys) > "$cacheDir"/"$userIDHash"."$pubKeyID" # for each acceptable key, write an ssh key line to the # key cache file if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then for keyID in ${keyIDs[@]} ; do # export the key with gpg2ssh # FIXME: needs to apply extra options for authorized_keys # lines if specified gpg2ssh_tmp "$mode" "$keyID" "$userID" >> "$cacheDir"/"$userIDHash"."$pubKeyID" # hash the cache file if specified if [ "$mode" = 'known_hosts' -a "$HASH_KNOWN_HOSTS" ] ; then ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1 rm "$cacheDir"/"$userIDHash"."$pubKeyID".old fi done fi # echo the path to the key cache file echo "$cacheDir"/"$userIDHash"."$pubKeyID" } # process a host for addition to a known_host file process_host() { local host local cacheDir local hostKeyCachePath host="$1" cacheDir="$2" log "processing host: '$host'" hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir") if [ $? = 0 ] ; then ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS" cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS" fi } # process known_hosts file # go through line-by-line, extract each host, and process with the # host processing function process_known_hosts() { local cacheDir local userID cacheDir="$1" # take all the hosts from the known_hosts file (first field), # grep out all the hashed hosts (lines starting with '|') cut -d ' ' -f 1 "$USER_KNOWN_HOSTS" | \ grep -v '^|.*$' | \ while IFS=, read -r -a hosts ; do # process each host for host in ${hosts[*]} ; do process_host "$host" "$cacheDir" done done } # process an authorized_*_ids file # go through line-by-line, extract each userid, and process process_authorized_ids() { local authorizedIDsFile local cacheDir local userID local userKeyCachePath authorizedIDsFile="$1" cacheDir="$2" # clean out keys file and remake keys directory rm -rf "$cacheDir" mkdir -p "$cacheDir" # loop through all user ids in file # FIXME: needs to handle extra options if necessary cat "$authorizedIDsFile" | meat | \ while read -r userID ; do # process the userid log "processing userid: '$userID'" userKeyCachePath=$(process_user_id "$userID" "$cacheDir") if [ -s "$userKeyCachePath" ] ; then loge " acceptable key/uid found." fi done } ######################################################################## # MAIN ######################################################################## if [ -z "$1" ] ; then usage exit 1 fi # mode given in first variable mode="$1" shift 1 # check user if ! id -u "$USER" > /dev/null 2>&1 ; then failure "invalid user '$USER'." fi # set user home directory HOME=$(getent passwd "$USER" | cut -d: -f6) # set ms home directory MS_HOME=${MS_HOME:-"$HOME"/.config/monkeysphere} # load configuration file MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} [ -e "$MS_CONF" ] && . "$MS_CONF" # set config variable defaults STAGING_AREA=${STAGING_AREA:-"$MS_HOME"} AUTHORIZED_USER_IDS=${AUTHORIZED_USER_IDS:-"$MS_HOME"/authorized_user_ids} GNUPGHOME=${GNUPGHOME:-"$HOME"/.gnupg} KEYSERVER=${KEYSERVER:-subkeys.pgp.net} REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-"e a"} USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-"$HOME"/.ssh/authorized_keys} USER_KNOWN_HOSTS=${USER_KNOWN_HOSTS:-"$HOME"/.ssh/known_hosts} HASH_KNOWN_HOSTS=${HASH_KNOWN_HOSTS:-} # export USER and GNUPGHOME variables, since they are used by gpg export USER export GNUPGHOME # stagging locations hostKeysCacheDir="$STAGING_AREA"/host_keys userKeysCacheDir="$STAGING_AREA"/user_keys msKnownHosts="$STAGING_AREA"/known_hosts msAuthorizedKeys="$STAGING_AREA"/authorized_keys # make sure gpg home exists with proper permissions mkdir -p -m 0700 "$GNUPGHOME" ## KNOWN_HOST MODE if [ "$mode" = 'known_hosts' -o "$mode" = 'k' ] ; then mode='known_hosts' cacheDir="$hostKeysCacheDir" log "user '$USER': monkeysphere known_hosts processing" # touch the known_hosts file to make sure it exists touch "$USER_KNOWN_HOSTS" # if hosts are specified on the command line, process just # those hosts if [ "$1" ] ; then for host ; do process_host "$host" "$cacheDir" done # otherwise, if no hosts are specified, process the user # known_hosts file else if [ ! -s "$USER_KNOWN_HOSTS" ] ; then failure "known_hosts file '$USER_KNOWN_HOSTS' is empty." fi process_known_hosts "$cacheDir" fi ## AUTHORIZED_KEYS MODE elif [ "$mode" = 'authorized_keys' -o "$mode" = 'a' ] ; then mode='authorized_keys' cacheDir="$userKeysCacheDir" # check auth ids file if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then log "authorized_user_ids file is empty or does not exist." exit fi log "user '$USER': monkeysphere authorized_keys processing" # if userids are specified on the command line, process just # those userids if [ "$1" ] ; then for userID ; do if ! grep -q "$userID" "$AUTHORIZED_USER_IDS" ; then log "userid '$userID' not in authorized_user_ids file." continue fi log "processing user id: '$userID'" process_user_id "$userID" "$cacheDir" > /dev/null done # otherwise, if no userids are specified, process the entire # authorized_user_ids file else process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir" fi # write output key file log "writing monkeysphere authorized_keys file... " touch "$msAuthorizedKeys" if [ "$(ls "$cacheDir")" ] ; then log -n "adding gpg keys... " cat "$cacheDir"/* > "$msAuthorizedKeys" echo "done." else log "no gpg keys to add." fi if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then if [ -s "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then log -n "adding user authorized_keys file... " cat "$USER_CONTROLLED_AUTHORIZED_KEYS" >> "$msAuthorizedKeys" echo "done." fi fi log "monkeysphere authorized_keys file generated:" log "$msAuthorizedKeys" else failure "unknown command '$mode'." fi