#!/usr/bin/env bash

# monkeysphere-host: Monkeysphere host admin 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

SYSDATADIR=${MONKEYSPHERE_SYSDATADIR:-"/var/lib/monkeysphere/host"}
export SYSDATADIR

# monkeysphere temp directory, in sysdatadir to enable atomic moves of
# authorized_keys files
MSTMPDIR="${SYSDATADIR}/tmp"
export MSTMPDIR

# 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

########################################################################
# FUNCTIONS
########################################################################

usage() {
    cat <<EOF >&2
usage: $PGRM <subcommand> [options] [args]
Monkeysphere host admin tool.

subcommands:
 show-key (s)                        output all host key information
 extend-key (e) EXPIRE               extend host key expiration
 add-hostname (n+) NAME[:PORT]       add hostname user ID to host key
 revoke-hostname (n-) NAME[:PORT]    revoke hostname user ID
 add-revoker (o) FINGERPRINT         add a revoker to the host key
 revoke-key (r)                      revoke host key
 publish-key (p)                     publish server host key to keyserver

 expert
  import-key (i)                     import existing ssh key to gpg
   --hostname (-h) NAME[:PORT]         hostname for key user ID
   --keyfile (-f) FILE                 key file to import
   --expire (-e) EXPIRE                date to expire
  gen-key (g)                        generate gpg key for the host
   --hostname (-h) NAME[:PORT]         hostname for key user ID
   --length (-l) BITS                  key length in bits (2048)
   --expire (-e) EXPIRE                date to expire
   --revoker (-r) FINGERPRINT          add a revoker
  diagnostics (d)                    monkeysphere host status

 version (v)                         show version number
 help (h,?)                          this help

EOF
}

# function to run command as monkeysphere user
su_monkeysphere_user() {
    # if the current user is the monkeysphere user, then just eval
    # command
    if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
	eval "$@"

    # otherwise su command as monkeysphere user
    else
	su "$MONKEYSPHERE_USER" -c "$@"
    fi
}

# function to interact with the host gnupg keyring
gpg_host() {
    local returnCode

    GNUPGHOME="$GNUPGHOME_HOST"
    export GNUPGHOME

    # NOTE: we supress this warning because we need the monkeysphere
    # user to be able to read the host pubring.  we realize this might
    # be problematic, but it's the simplest solution, without too much
    # loss of security.
    gpg --no-permission-warning "$@"
    returnCode="$?"

    # always reset the permissions on the host pubring so that the
    # monkeysphere user can read the trust signatures
    chgrp "$MONKEYSPHERE_USER" "${GNUPGHOME_HOST}/pubring.gpg"
    chmod g+r "${GNUPGHOME_HOST}/pubring.gpg"
    
    return "$returnCode"
}

# check if user is root
is_root() {
    [ $(id -u 2>/dev/null) = '0' ]
}

# check that user is root, for functions that require root access
check_user() {
    is_root || failure "You must be root to run this command."
}

# output just key fingerprint
fingerprint_server_key() {
    # set the pipefail option so functions fails if can't read sec key
    set -o pipefail

    gpg_host --list-secret-keys --fingerprint \
	--with-colons --fixed-list-mode 2> /dev/null | \
	grep '^fpr:' | head -1 | cut -d: -f10 2>/dev/null
}

# function to check for host secret key
check_host_keyring() {
    fingerprint_server_key >/dev/null \
	|| failure "You don't appear to have a Monkeysphere host key on this server.  Please run 'monkeysphere-server gen-key' first."
}

# output key information
show_server_key() {
    local fingerprintPGP
    local fingerprintSSH
    local ret=0

    # FIXME: you shouldn't have to be root to see the host key fingerprint
    if is_root ; then
	check_host_keyring
	fingerprintPGP=$(fingerprint_server_key)
	gpg_authentication "--fingerprint --list-key --list-options show-unusable-uids $fingerprintPGP" 2>/dev/null
	echo "OpenPGP fingerprint: $fingerprintPGP"
    else
	log info "You must be root to see host OpenPGP fingerprint."
	ret='1'
    fi

    if [ -f "${SYSDATADIR}/ssh_host_rsa_key.pub" ] ; then
	fingerprintSSH=$(ssh-keygen -l -f "${SYSDATADIR}/ssh_host_rsa_key.pub" | \
	    awk '{ print $1, $2, $4 }')
	echo "ssh fingerprint: $fingerprintSSH"
    else
	log info "SSH host key not found."
	ret='1'
    fi

    return $ret
}

# extend the lifetime of a host key:
extend_key() {
    local fpr=$(fingerprint_server_key)
    local extendTo="$1"

    # get the new expiration date
    extendTo=$(get_gpg_expiration "$extendTo")

    gpg_host --quiet --command-fd 0 --edit-key "$fpr" <<EOF 
expire
$extendTo
save
EOF

    echo
    echo "NOTE: Host key expiration date adjusted, but not yet published."
    echo "Run '$PGRM publish-key' to publish the new expiration date."
}

# add hostname user ID to server key
add_hostname() {
    local userID
    local fingerprint
    local tmpuidMatch
    local line
    local adduidCommand

    if [ -z "$1" ] ; then
	failure "You must specify a hostname to add."
    fi

    userID="ssh://${1}"

    fingerprint=$(fingerprint_server_key)

    # match to only ultimately trusted user IDs
    tmpuidMatch="u:$(echo $userID | gpg_escape)"

    # find the index of the requsted user ID
    # NOTE: this is based on circumstantial evidence that the order of
    # this output is the appropriate index
    if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
	| egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
	failure "Host userID '$userID' already exists."
    fi

    echo "The following user ID will be added to the host key:"
    echo "  $userID"
    read -p "Are you sure you would like to add this user ID? (y/N) " OK; OK=${OK:=N}
    if [ ${OK/y/Y} != 'Y' ] ; then
	failure "User ID not added."
    fi

    # edit-key script command to add user ID
    adduidCommand=$(cat <<EOF
adduid
$userID


save
EOF
)

    # execute edit-key script
    if echo "$adduidCommand" | \
	gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then

        # update the trustdb for the authentication keyring
	gpg_authentication "--check-trustdb"

	show_server_key

	echo
	echo "NOTE: User ID added to key, but key not published."
	echo "Run '$PGRM publish-key' to publish the new user ID."
    else
	failure "Problem adding user ID."
    fi
}

# revoke hostname user ID to server key
revoke_hostname() {
    local userID
    local fingerprint
    local tmpuidMatch
    local line
    local uidIndex
    local message
    local revuidCommand

    if [ -z "$1" ] ; then
	failure "You must specify a hostname to revoke."
    fi

    echo "WARNING: There is a known bug in this function."
    echo "This function has been known to occasionally revoke the wrong user ID."
    echo "Please see the following bug report for more information:"
    echo "http://web.monkeysphere.info/bugs/revoke-hostname-revoking-wrong-userid/"
    read -p "Are you sure you would like to proceed? (y/N) " OK; OK=${OK:=N}
    if [ ${OK/y/Y} != 'Y' ] ; then
	failure "aborting."
    fi

    userID="ssh://${1}"

    fingerprint=$(fingerprint_server_key)

    # match to only ultimately trusted user IDs
    tmpuidMatch="u:$(echo $userID | gpg_escape)"

    # find the index of the requsted user ID
    # NOTE: this is based on circumstantial evidence that the order of
    # this output is the appropriate index
    if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
	| egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
	uidIndex=${line%%:*}
    else
	failure "No non-revoked user ID '$userID' is found."
    fi

    echo "The following host key user ID will be revoked:"
    echo "  $userID"
    read -p "Are you sure you would like to revoke this user ID? (y/N) " OK; OK=${OK:=N}
    if [ ${OK/y/Y} != 'Y' ] ; then
	failure "User ID not revoked."
    fi

    message="Hostname removed by monkeysphere-server $DATE"

    # edit-key script command to revoke user ID
    revuidCommand=$(cat <<EOF
$uidIndex
revuid
y
4
$message

y
save
EOF
	)	

    # execute edit-key script
    if echo "$revuidCommand" | \
	gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then

        # update the trustdb for the authentication keyring
	gpg_authentication "--check-trustdb"

	show_server_key

	echo
	echo "NOTE: User ID revoked, but revocation not published."
	echo "Run '$PGRM publish-key' to publish the revocation."
    else
	failure "Problem revoking user ID."
    fi
}

# add a revoker to the host key
add_revoker() {
    # FIXME: implement!
    failure "not implemented yet!"
}

# revoke the host key
revoke_key() {
    # FIXME: implement!
    failure "not implemented yet!"
}

# publish server key to keyserver
publish_server_key() {
    read -p "Really publish host key to $KEYSERVER? (y/N) " OK; OK=${OK:=N}
    if [ ${OK/y/Y} != 'Y' ] ; then
	failure "key not published."
    fi

    # find the key fingerprint
    fingerprint=$(fingerprint_server_key)

    # publish host key
    gpg_authentication "--keyserver $KEYSERVER --send-keys '0x${fingerprint}!'"
}

diagnostics() {
#  * check on the status and validity of the key and public certificates
    local seckey
    local keysfound
    local curdate
    local warnwindow
    local warndate
    local create
    local expire
    local uid
    local fingerprint
    local badhostkeys
    local sshd_config
    local problemsfound=0

    # FIXME: what's the correct, cross-platform answer?
    sshd_config=/etc/ssh/sshd_config
    seckey=$(gpg_host --list-secret-keys --fingerprint --with-colons --fixed-list-mode)
    keysfound=$(echo "$seckey" | grep -c ^sec:)
    curdate=$(date +%s)
    # warn when anything is 2 months away from expiration
    warnwindow='2 months'
    warndate=$(advance_date $warnwindow +%s)

    if ! id monkeysphere >/dev/null ; then
	echo "! No monkeysphere user found!  Please create a monkeysphere system user with bash as its shell."
	problemsfound=$(($problemsfound+1))
    fi

    if ! [ -d "$SYSDATADIR" ] ; then
	echo "! no $SYSDATADIR directory found.  Please create it."
	problemsfound=$(($problemsfound+1))
    fi

    echo "Checking host GPG key..."
    if (( "$keysfound" < 1 )); then
	echo "! No host key found."
	echo " - Recommendation: run 'monkeysphere-server gen-key'"
	problemsfound=$(($problemsfound+1))
    elif (( "$keysfound" > 1 )); then
	echo "! More than one host key found?"
	# FIXME: recommend a way to resolve this
	problemsfound=$(($problemsfound+1))
    else
	create=$(echo "$seckey" | grep ^sec: | cut -f6 -d:)
	expire=$(echo "$seckey" | grep ^sec: | cut -f7 -d:)
	fingerprint=$(echo "$seckey" | grep ^fpr: | head -n1 | cut -f10 -d:)
	# check for key expiration:
	if [ "$expire" ]; then
	    if (( "$expire"  < "$curdate" )); then
		echo "! Host key is expired."
		echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
		problemsfound=$(($problemsfound+1))
	    elif (( "$expire" < "$warndate" )); then
		echo "! Host key expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
		echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
		problemsfound=$(($problemsfound+1))
	    fi
	fi

        # and weirdnesses:
	if [ "$create" ] && (( "$create" > "$curdate" )); then
	    echo "! Host key was created in the future(?!). Is your clock correct?"
	    echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
	    problemsfound=$(($problemsfound+1))
	fi

        # check for UserID expiration:
	echo "$seckey" | grep ^uid: | cut -d: -f6,7,10 | \
	while IFS=: read create expire uid ; do
	    # FIXME: should we be doing any checking on the form
	    # of the User ID?  Should we be unmangling it somehow?

	    if [ "$create" ] && (( "$create" > "$curdate" )); then
		echo "! User ID '$uid' was created in the future(?!).  Is your clock correct?"
		echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
		problemsfound=$(($problemsfound+1))
	    fi
	    if [ "$expire" ] ; then
		if (( "$expire" < "$curdate" )); then
		    echo "! User ID '$uid' is expired."
		    # FIXME: recommend a way to resolve this
		    problemsfound=$(($problemsfound+1))
		elif (( "$expire" < "$warndate" )); then
		    echo "! User ID '$uid' expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)		
		    # FIXME: recommend a way to resolve this
		    problemsfound=$(($problemsfound+1))
		fi
	    fi
	done
	    
# FIXME: verify that the host key is properly published to the
#   keyservers (do this with the non-privileged user)

# FIXME: check that there are valid, non-expired certifying signatures
#   attached to the host key after fetching from the public keyserver
#   (do this with the non-privileged user as well)

# FIXME: propose adding a revoker to the host key if none exist (do we
#   have a way to do that after key generation?)

	# Ensure that the ssh_host_rsa_key file is present and non-empty:
	echo
	echo "Checking host SSH key..."
	if [ ! -s "${SYSDATADIR}/ssh_host_rsa_key" ] ; then
	    echo "! The host key as prepared for SSH (${SYSDATADIR}/ssh_host_rsa_key) is missing or empty."
	    problemsfound=$(($problemsfound+1))
	else
	    if [ $(ls -l "${SYSDATADIR}/ssh_host_rsa_key" | cut -f1 -d\ ) != '-rw-------' ] ; then
		echo "! Permissions seem wrong for ${SYSDATADIR}/ssh_host_rsa_key -- should be 0600."
		problemsfound=$(($problemsfound+1))
	    fi

	    # propose changes needed for sshd_config (if any)
	    if ! grep -q "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$" "$sshd_config"; then
		echo "! $sshd_config does not point to the monkeysphere host key (${SYSDATADIR}/ssh_host_rsa_key)."
		echo " - Recommendation: add a line to $sshd_config: 'HostKey ${SYSDATADIR}/ssh_host_rsa_key'"
		problemsfound=$(($problemsfound+1))
	    fi
	    if badhostkeys=$(grep -i '^HostKey' "$sshd_config" | grep -v "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$") ; then
		echo "! $sshd_config refers to some non-monkeysphere host keys:"
		echo "$badhostkeys"
		echo " - Recommendation: remove the above HostKey lines from $sshd_config"
		problemsfound=$(($problemsfound+1))
	    fi

        # FIXME: test (with ssh-keyscan?) that the running ssh
        # daemon is actually offering the monkeysphere host key.

	fi
    fi

# FIXME: look at the ownership/privileges of the various keyrings,
#    directories housing them, etc (what should those values be?  can
#    we make them as minimal as possible?)

# FIXME: look to see that the ownertrust rules are set properly on the
#    authentication keyring

# FIXME: make sure that at least one identity certifier exists

# FIXME: look at the timestamps on the monkeysphere-generated
# authorized_keys files -- warn if they seem out-of-date.

# FIXME: check for a cronjob that updates monkeysphere-generated
# authorized_keys?

    echo
    echo "Checking for MonkeySphere-enabled public-key authentication for users ..."
    # Ensure that User ID authentication is enabled:
    if ! grep -q "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$" "$sshd_config"; then
	echo "! $sshd_config does not point to monkeysphere authorized keys."
	echo " - Recommendation: add a line to $sshd_config: 'AuthorizedKeysFile ${SYSDATADIR}/authorized_keys/%u'"
	problemsfound=$(($problemsfound+1))
    fi
    if badauthorizedkeys=$(grep -i '^AuthorizedKeysFile' "$sshd_config" | grep -v "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$") ; then
	echo "! $sshd_config refers to non-monkeysphere authorized_keys files:"
	echo "$badauthorizedkeys"
	echo " - Recommendation: remove the above AuthorizedKeysFile lines from $sshd_config"
	problemsfound=$(($problemsfound+1))
    fi

    if [ "$problemsfound" -gt 0 ]; then
	echo "When the above $problemsfound issue"$(if [ "$problemsfound" -eq 1 ] ; then echo " is" ; else echo "s are" ; fi)" resolved, please re-run:"
	echo "  monkeysphere-server diagnostics"
    else
	echo "Everything seems to be in order!"
    fi
}

########################################################################
# MAIN
########################################################################

# unset variables that should be defined only in config file
unset KEYSERVER
unset AUTHORIZED_USER_IDS
unset RAW_AUTHORIZED_KEYS
unset MONKEYSPHERE_USER

# load configuration file
[ -e ${MONKEYSPHERE_SERVER_CONFIG:="${SYSCONFIGDIR}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"

# set empty config variable with ones from the environment, or with
# defaults
LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="pool.sks-keyservers.net"}}
AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.monkeysphere/authorized_user_ids"}}
RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}

# other variables
CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${SYSDATADIR}/gnupg-host"}
GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${SYSDATADIR}/gnupg-authentication"}

# export variables needed in su invocation
export DATE
export MODE
export MONKEYSPHERE_USER
export LOG_LEVEL
export KEYSERVER
export CHECK_KEYSERVER
export REQUIRED_USER_KEY_CAPABILITY
export GNUPGHOME_HOST
export GNUPGHOME_AUTHENTICATION
export GNUPGHOME

# get subcommand
COMMAND="$1"
[ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
shift

case $COMMAND in
    'show-key'|'show'|'s')
	show_server_key
	;;

    'extend-key'|'e')
	check_user
	check_host_keyring
	extend_key "$@"
	;;

    'add-hostname'|'add-name'|'n+')
	check_user
	check_host_keyring
	add_hostname "$@"
	;;

    'revoke-hostname'|'revoke-name'|'n-')
	check_user
	check_host_keyring
	revoke_hostname "$@"
	;;

    'add-revoker'|'o')
	check_user
	check_host_keyring
	add_revoker "$@"
	;;

    'revoke-key'|'r')
	check_user
	check_host_keyring
	revoke_key "$@"
	;;

    'publish-key'|'publish'|'p')
	check_user
	check_host_keyring
	publish_server_key
	;;

    'expert'|'e')
	check_user
	SUBCOMMAND="$1"
	shift
	case "$SUBCOMMAND" in
	    'import-key'|'i')
		import_key "$@"
		;;

	    'gen-key'|'g')
		gen_key "$@"
		;;

	    'diagnostics'|'d')
		diagnostics
		;;

	    *)
		failure "Unknown expert subcommand: '$COMMAND'
Type '$PGRM help' for usage."
		;;
	esac
	;;

    'version'|'v')
	echo "$VERSION"
	;;

    '--help'|'help'|'-h'|'h'|'?')
        usage
        ;;

    *)
        failure "Unknown command: '$COMMAND'
Type '$PGRM help' for usage."
        ;;
esac

exit "$RETURN"