- # -*-shell-script-*-
- # Shared sh functions for the monkeysphere
- #
- # Written by
- # Jameson Rollins <jrollins@fifthhorseman.net>
- #
- # 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
- ########################################################################
- ### COMMON VARIABLES
- # managed directories
- ETC="/etc/monkeysphere"
- export ETC
- ########################################################################
- ### UTILITY FUNCTIONS
- # failure function. exits with code 255, unless specified otherwise.
- failure() {
- echo "$1" >&2
- exit ${2:-'255'}
- }
- # write output to stderr
- log() {
- echo -n "ms: " >&2
- echo "$@" >&2
- }
- loge() {
- echo "$@" >&2
- }
- # cut out all comments(#) and blank lines from standard input
- meat() {
- grep -v -e "^[[:space:]]*#" -e '^$' "$1"
- }
- # cut a specified line from standard input
- cutline() {
- head --line="$1" "$2" | tail -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 usage
- local capcheck
- usage="$1"
- shift 1
- for capcheck ; do
- if echo "$usage" | grep -q -v "$capcheck" ; then
- return 1
- fi
- done
- return 0
- }
- # hash of a file
- file_hash() {
- md5sum "$1" 2> /dev/null
- }
- # 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/:/g'
- }
- # remove all lines with specified string from specified file
- remove_line() {
- local file
- local string
- file="$1"
- string="$2"
- if [ -z "$file" -o -z "$string" ] ; then
- return 1
- fi
- if [ ! -e "$file" ] ; then
- return 1
- fi
- # if the string is in the file...
- if grep -q -F "$string" "$file" 2> /dev/null ; then
- # remove the line with the string, and return 0
- grep -v -F "$string" "$file" | sponge "$file"
- return 0
- # otherwise return 1
- else
- return 1
- fi
- }
- # remove all lines with MonkeySphere strings in file
- remove_monkeysphere_lines() {
- local file
- file="$1"
- if [ -z "$file" ] ; then
- return 1
- fi
- if [ ! -e "$file" ] ; then
- return 1
- fi
- egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
- "$file" | sponge "$file"
- }
- # translate ssh-style path variables %h and %u
- translate_ssh_variables() {
- local uname
- local home
- uname="$1"
- path="$2"
- # get the user's home directory
- userHome=$(getent passwd "$uname" | cut -d: -f6)
- # translate '%u' to user name
- path=${path/\%u/"$uname"}
- # translate '%h' to user home directory
- path=${path/\%h/"$userHome"}
- echo "$path"
- }
- # test that a string to conforms to GPG's expiration format
- test_gpg_expire() {
- echo "$1" | egrep -q "^[0-9]+[mwy]?$"
- }
- # check that a file is properly owned, and that all it's parent
- # directories are not group/other writable
- check_key_file_permissions() {
- local user
- local path
- local access
- local gAccess
- local oAccess
- # function to check that an octal corresponds to writability
- is_write() {
- [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
- }
- user="$1"
- path="$2"
- # return 0 is path does not exist
- [ -e "$path" ] || return 0
- owner=$(stat --format '%U' "$path")
- access=$(stat --format '%a' "$path")
- gAccess=$(echo "$access" | cut -c2)
- oAccess=$(echo "$access" | cut -c3)
- # check owner
- if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
- return 1
- fi
- # check group/other writability
- if is_write "$gAccess" || is_write "$oAccess" ; then
- return 2
- fi
- if [ "$path" = '/' ] ; then
- return 0
- else
- check_key_file_permissions $(dirname "$path")
- fi
- }
- ### CONVERSION UTILITIES
- # output the ssh key for a given key ID
- gpg2ssh() {
- local keyID
-
- keyID="$1"
- gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
- }
- # output known_hosts line from ssh key
- ssh2known_hosts() {
- local host
- local key
- host="$1"
- key="$2"
- echo -n "$host "
- echo -n "$key" | tr -d '\n'
- echo " MonkeySphere${DATE}"
- }
- # output authorized_keys line from ssh key
- ssh2authorized_keys() {
- local userID
- local key
-
- userID="$1"
- key="$2"
- echo -n "$key" | tr -d '\n'
- echo " MonkeySphere${DATE} ${userID}"
- }
- # convert key from gpg to ssh known_hosts format
- gpg2known_hosts() {
- local host
- local keyID
- host="$1"
- keyID="$2"
- # 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}$'
- echo -n "$host "
- gpg2ssh "$keyID" | tr -d '\n'
- echo " MonkeySphere${DATE}"
- }
- # convert key from gpg to ssh authorized_keys format
- gpg2authorized_keys() {
- local userID
- local keyID
- userID="$1"
- keyID="$2"
- # 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}$'
- gpg2ssh "$keyID" | tr -d '\n'
- echo " MonkeySphere${DATE} ${userID}"
- }
- ### GPG UTILITIES
- # retrieve all keys with given user id from keyserver
- # FIXME: need to figure out how to retrieve all matching keys
- # (not just first N (5 in this case))
- gpg_fetch_userid() {
- local userID
- local returnCode
- if [ "$CHECK_KEYSERVER" != 'true' ] ; then
- return 0
- fi
- userID="$1"
- log -n " checking keyserver $KEYSERVER... "
- echo 1,2,3,4,5 | \
- gpg --quiet --batch --with-colons \
- --command-fd 0 --keyserver "$KEYSERVER" \
- --search ="$userID" > /dev/null 2>&1
- returnCode="$?"
- loge "done."
- # if the user is the monkeysphere user, then update the
- # monkeysphere user's trustdb
- if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
- gpg_authentication "--check-trustdb" > /dev/null 2>&1
- fi
- return "$returnCode"
- }
- ########################################################################
- ### PROCESSING FUNCTIONS
- # 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 requested user ID has appropriate validity
- # (see /usr/share/doc/gnupg/DETAILS.gz)
- # output is one line for every found key, in the following format:
- #
- # flag:fingerprint
- #
- # "flag" is an acceptability flag, 0 = ok, 1 = bad
- # "fingerprint" is the fingerprint of the key
- #
- # expects global variable: "MODE"
- process_user_id() {
- local userID
- local requiredCapability
- local requiredPubCapability
- local gpgOut
- local type
- local validity
- local keyid
- local uidfpr
- local usage
- local keyOK
- local uidOK
- local lastKey
- local lastKeyOK
- local fingerprint
- userID="$1"
- # set the required key capability based on the mode
- if [ "$MODE" = 'known_hosts' ] ; then
- requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
- elif [ "$MODE" = 'authorized_keys' ] ; then
- requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
- fi
- requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
- # fetch the user ID if necessary/requested
- gpg_fetch_userid "$userID"
- # output gpg info for (exact) userid and store
- gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
- --with-fingerprint --with-fingerprint \
- ="$userID" 2>/dev/null)
- # if the gpg query return code is not 0, return 1
- if [ "$?" -ne 0 ] ; then
- log " no primary keys found."
- return 1
- fi
- # loop over all lines in the gpg output and process.
- echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
- while IFS=: read -r type validity keyid uidfpr usage ; do
- # process based on record type
- case $type in
- 'pub') # primary keys
- # new key, wipe the slate
- keyOK=
- uidOK=
- lastKey=pub
- lastKeyOK=
- fingerprint=
- log " primary key found: $keyid"
- # if overall key is not valid, skip
- if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
- log " - unacceptable primary key validity ($validity)."
- continue
- fi
- # if overall key is disabled, skip
- if check_capability "$usage" 'D' ; then
- log " - key disabled."
- continue
- fi
- # if overall key capability is not ok, skip
- if ! check_capability "$usage" $requiredPubCapability ; then
- log " - unacceptable primary key capability ($usage)."
- continue
- fi
- # mark overall key as ok
- keyOK=true
- # mark primary key as ok if capability is ok
- if check_capability "$usage" $requiredCapability ; then
- lastKeyOK=true
- fi
- ;;
- 'uid') # user ids
- if [ "$lastKey" != pub ] ; then
- log " - got a user ID after a sub key! user IDs should only follow primary keys!"
- continue
- fi
- # don't bother with a uid if there is no valid or reasonable primary key.
- if [ "$keyOK" != true ] ; then
- continue
- fi
- # if an acceptable user ID was already found, skip
- if [ "$uidOK" ] ; then
- continue
- fi
- # if the user ID does not match, skip
- if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
- continue
- fi
- # if the user ID validity is not ok, skip
- if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
- continue
- fi
- # mark user ID acceptable
- uidOK=true
- # output a line for the primary key
- # 0 = ok, 1 = bad
- if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
- log " * acceptable primary key."
- if [ -z "$sshKey" ] ; then
- log " ! primary key could not be translated (not RSA or DSA?)."
- else
- echo "0:${sshKey}"
- fi
- else
- log " - unacceptable primary key."
- if [ -z "$sshKey" ] ; then
- log " ! primary key could not be translated (not RSA or DSA?)."
- else
- echo "1:${sshKey}"
- fi
- fi
- ;;
- 'sub') # sub keys
- # unset acceptability of last key
- lastKey=sub
- lastKeyOK=
- fingerprint=
-
- # don't bother with sub keys if the primary key is not valid
- if [ "$keyOK" != true ] ; then
- continue
- fi
- # don't bother with sub keys if no user ID is acceptable:
- if [ "$uidOK" != true ] ; then
- continue
- fi
-
- # if sub key validity is not ok, skip
- if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
- continue
- fi
- # if sub key capability is not ok, skip
- if ! check_capability "$usage" $requiredCapability ; then
- continue
- fi
- # mark sub key as ok
- lastKeyOK=true
- ;;
- 'fpr') # key fingerprint
- fingerprint="$uidfpr"
- sshKey=$(gpg2ssh "$fingerprint")
- # if the last key was the pub key, skip
- if [ "$lastKey" = pub ] ; then
- continue
- fi
- # output a line for the sub key
- # 0 = ok, 1 = bad
- if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
- log " * acceptable sub key."
- if [ -z "$sshKey" ] ; then
- log " ! sub key could not be translated (not RSA or DSA?)."
- else
- echo "0:${sshKey}"
- fi
- else
- log " - unacceptable sub key."
- if [ -z "$sshKey" ] ; then
- log " ! sub key could not be translated (not RSA or DSA?)."
- else
- echo "1:${sshKey}"
- fi
- fi
- ;;
- esac
- done | sort -t: -k1 -n -r
- # NOTE: this last sort is important so that the "good" keys (key
- # flag '0') come last. This is so that they take precedence when
- # being processed in the key files over "bad" keys (key flag '1')
- }
- # process a single host in the known_host file
- process_host_known_hosts() {
- local host
- local userID
- local nKeys
- local nKeysOK
- local ok
- local sshKey
- local tmpfile
- host="$1"
- userID="ssh://${host}"
- log "processing: $host"
- nKeys=0
- nKeysOK=0
- IFS=$'\n'
- for line in $(process_user_id "${userID}") ; do
- # note that key was found
- nKeys=$((nKeys+1))
- ok=$(echo "$line" | cut -d: -f1)
- sshKey=$(echo "$line" | cut -d: -f2)
- if [ -z "$sshKey" ] ; then
- continue
- fi
- # remove the old host key line, and note if removed
- remove_line "$KNOWN_HOSTS" "$sshKey"
- # if key OK, add new host line
- if [ "$ok" -eq '0' ] ; then
- # note that key was found ok
- nKeysOK=$((nKeysOK+1))
- # hash if specified
- if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
- # FIXME: this is really hackish cause ssh-keygen won't
- # hash from stdin to stdout
- tmpfile=$(mktemp)
- ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
- ssh-keygen -H -f "$tmpfile" 2> /dev/null
- cat "$tmpfile" >> "$KNOWN_HOSTS"
- rm -f "$tmpfile" "${tmpfile}.old"
- else
- ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
- fi
- fi
- done
- # if at least one key was found...
- if [ "$nKeys" -gt 0 ] ; then
- # if ok keys were found, return 0
- if [ "$nKeysOK" -gt 0 ] ; then
- return 0
- # else return 2
- else
- return 2
- fi
- # if no keys were found, return 1
- else
- return 1
- fi
- }
- # update the known_hosts file for a set of hosts listed on command
- # line
- update_known_hosts() {
- local nHosts
- local nHostsOK
- local nHostsBAD
- local fileCheck
- local host
- # the number of hosts specified on command line
- nHosts="$#"
- nHostsOK=0
- nHostsBAD=0
- # set the trap to remove any lockfiles on exit
- trap "lockfile-remove $KNOWN_HOSTS" EXIT
- # create a lockfile on known_hosts
- lockfile-create "$KNOWN_HOSTS"
- # note pre update file checksum
- fileCheck="$(file_hash "$KNOWN_HOSTS")"
- for host ; do
- # process the host
- process_host_known_hosts "$host"
- # note the result
- case "$?" in
- 0)
- nHostsOK=$((nHostsOK+1))
- ;;
- 2)
- nHostsBAD=$((nHostsBAD+1))
- ;;
- esac
- # touch the lockfile, for good measure.
- lockfile-touch --oneshot "$KNOWN_HOSTS"
- done
- # remove the lockfile
- lockfile-remove "$KNOWN_HOSTS"
- # note if the known_hosts file was updated
- if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
- log "known_hosts file updated."
- fi
- # if an acceptable host was found, return 0
- if [ "$nHostsOK" -gt 0 ] ; then
- return 0
- # else if no ok hosts were found...
- else
- # if no bad host were found then no hosts were found at all,
- # and return 1
- if [ "$nHostsBAD" -eq 0 ] ; then
- return 1
- # else if at least one bad host was found, return 2
- else
- return 2
- fi
- fi
- }
- # process hosts from a known_hosts file
- process_known_hosts() {
- local hosts
- log "processing known_hosts file..."
- hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
- if [ -z "$hosts" ] ; then
- log "no hosts to process."
- return
- fi
- # take all the hosts from the known_hosts file (first
- # field), grep out all the hashed hosts (lines starting
- # with '|')...
- update_known_hosts $hosts
- }
- # process uids for the authorized_keys file
- process_uid_authorized_keys() {
- local userID
- local nKeys
- local nKeysOK
- local ok
- local sshKey
- userID="$1"
- log "processing: $userID"
- nKeys=0
- nKeysOK=0
- IFS=$'\n'
- for line in $(process_user_id "$userID") ; do
- # note that key was found
- nKeys=$((nKeys+1))
- ok=$(echo "$line" | cut -d: -f1)
- sshKey=$(echo "$line" | cut -d: -f2)
- if [ -z "$sshKey" ] ; then
- continue
- fi
- # remove the old host key line
- remove_line "$AUTHORIZED_KEYS" "$sshKey"
- # if key OK, add new host line
- if [ "$ok" -eq '0' ] ; then
- # note that key was found ok
- nKeysOK=$((nKeysOK+1))
- ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
- fi
- done
- # if at least one key was found...
- if [ "$nKeys" -gt 0 ] ; then
- # if ok keys were found, return 0
- if [ "$nKeysOK" -gt 0 ] ; then
- return 0
- # else return 2
- else
- return 2
- fi
- # if no keys were found, return 1
- else
- return 1
- fi
- }
- # update the authorized_keys files from a list of user IDs on command
- # line
- update_authorized_keys() {
- local userID
- local nIDs
- local nIDsOK
- local nIDsBAD
- local fileCheck
- # the number of ids specified on command line
- nIDs="$#"
- nIDsOK=0
- nIDsBAD=0
- # set the trap to remove any lockfiles on exit
- trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
- # create a lockfile on authorized_keys
- lockfile-create "$AUTHORIZED_KEYS"
- # note pre update file checksum
- fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
- # remove any monkeysphere lines from authorized_keys file
- remove_monkeysphere_lines "$AUTHORIZED_KEYS"
- for userID ; do
- # process the user ID, change return code if key not found for
- # user ID
- process_uid_authorized_keys "$userID"
- # note the result
- case "$?" in
- 0)
- nIDsOK=$((nIDsOK+1))
- ;;
- 2)
- nIDsBAD=$((nIDsBAD+1))
- ;;
- esac
- # touch the lockfile, for good measure.
- lockfile-touch --oneshot "$AUTHORIZED_KEYS"
- done
- # remove the lockfile
- lockfile-remove "$AUTHORIZED_KEYS"
- # note if the authorized_keys file was updated
- if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
- log "authorized_keys file updated."
- fi
- # if an acceptable id was found, return 0
- if [ "$nIDsOK" -gt 0 ] ; then
- return 0
- # else if no ok ids were found...
- else
- # if no bad ids were found then no ids were found at all, and
- # return 1
- if [ "$nIDsBAD" -eq 0 ] ; then
- return 1
- # else if at least one bad id was found, return 2
- else
- return 2
- fi
- fi
- }
- # process an authorized_user_ids file for authorized_keys
- process_authorized_user_ids() {
- local line
- local nline
- local userIDs
- authorizedUserIDs="$1"
- log "processing authorized_user_ids file..."
- if ! meat "$authorizedUserIDs" > /dev/null ; then
- log "no user IDs to process."
- return
- fi
- nline=0
- # extract user IDs from authorized_user_ids file
- IFS=$'\n'
- for line in $(meat "$authorizedUserIDs") ; do
- userIDs["$nline"]="$line"
- nline=$((nline+1))
- done
- update_authorized_keys "${userIDs[@]}"
- }
|