From 4793624c65673268128fb0146cd9bd1b3cfeb6c4 Mon Sep 17 00:00:00 2001 From: Jameson Graef Rollins Date: Tue, 10 Jun 2008 17:17:51 -0400 Subject: New client/server components: - broke out all common functions to "common" file - put all client commands into "monkeysphere" script - put all server commands into "monkeysphere-server" script - moved all code into src directory to clean things up a bit - this effectively makes obsolete rhesus and howler - added proposed monkeysphere-ssh-proxycommand script that can be called to update known_hosts from ssh ProxyCommand - updated monkeysphere.conf to work as global client config - added monkeysphere-server.conf for server config --- src/rhesus/rhesus | 466 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100755 src/rhesus/rhesus (limited to 'src/rhesus/rhesus') diff --git a/src/rhesus/rhesus b/src/rhesus/rhesus new file mode 100755 index 0000000..f607f0b --- /dev/null +++ b/src/rhesus/rhesus @@ -0,0 +1,466 @@ +#!/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 -- cgit v1.2.3