#!/bin/bash

# monkeysphere-server: MonkeySphere server admin tool
#
# The monkeysphere scripts are written by:
# Jameson Rollins <jrollins@fifthhorseman.net>
#
# They are Copyright 2008, and are all released under the GPL, version 3
# or later.

########################################################################
PGRM=$(basename $0)

SHARE=${MONKEYSPHERE_SHARE:="/usr/share/monkeysphere"}
export SHARE
. "${SHARE}/common" || exit 1

VARLIB="/var/lib/monkeysphere"
export VARLIB

# date in UTF 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
usage: $PGRM <subcommand> [options] [args]
MonkeySphere server admin tool.

subcommands:
 update-users (u) [USER]...          update user authorized_keys files

 gen-key (g) [HOSTNAME]              generate gpg key for the server
   -l|--length BITS                    key length in bits (2048)
   -e|--expire EXPIRE                  date to expire
   -r|--revoker FINGERPRINT            add a revoker
 show-fingerprint (f)                show server's host key fingerprint
 publish-key (p)                     publish server's host key to keyserver
 diagnostics (d)                     report on the server's monkeysphere status

 add-identity-certifier (a) KEYID    import and tsign a certification key
   -n|--domain DOMAIN                  limit ID certifications to IDs in DOMAIN
   -t|--trust TRUST                    trust level of certifier (full)
   -d|--depth DEPTH                    trust depth for certifier (1)
 remove-identity-certifier (r) KEYID remove a certification key
 list-identity-certifiers (l)        list certification keys

 gpg-authentication-cmd CMD          gnupg-authentication command

 help (h,?)                          this help
EOF
}

su_monkeysphere_user() {
    su --preserve-environment "$MONKEYSPHERE_USER" -- -c "$@"
}

# 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"
}

# function to interact with the authentication gnupg keyring
# FIXME: this function requires basically accepts only a single
# argument because of problems with quote expansion.  this needs to be
# fixed/improved.
gpg_authentication() {
    GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
    export GNUPGHOME

    su_monkeysphere_user "gpg $@"
}

# update authorized_keys for users
update_users() {
    if [ "$1" ] ; then
	# get users from command line
	unames="$@"
    else
	# or just look at all users if none specified
	unames=$(getent passwd | cut -d: -f1)
    fi

    # set mode
    MODE="authorized_keys"

    # set gnupg home
    GNUPGHOME="$GNUPGHOME_AUTHENTICATION"

    # check to see if the gpg trust database has been initialized
    if [ ! -s "${GNUPGHOME}/trustdb.gpg" ] ; then
	failure "GNUPG trust database uninitialized.  Please see MONKEYSPHERE-SERVER(8)."
    fi

    # make sure the authorized_keys directory exists
    mkdir -p "${VARLIB}/authorized_keys"

    # loop over users
    for uname in $unames ; do
	# check all specified users exist
	if ! getent passwd "$uname" >/dev/null ; then
	    log "----- unknown user '$uname' -----"
	    continue
	fi

	# set authorized_user_ids and raw authorized_keys variables,
	# translating ssh-style path variables
	authorizedUserIDs=$(translate_ssh_variables "$uname" "$AUTHORIZED_USER_IDS")
	rawAuthorizedKeys=$(translate_ssh_variables "$uname" "$RAW_AUTHORIZED_KEYS")

	# if neither is found, skip user
	if [ ! -s "$authorizedUserIDs" ] ; then
	    if [ "$rawAuthorizedKeys" = '-' -o ! -s "$rawAuthorizedKeys" ] ; then
		continue
	    fi
	fi

	log "----- user: $uname -----"

        # exit if the authorized_user_ids file is empty
	if ! check_key_file_permissions "$uname" "$AUTHORIZED_USER_IDS" ; then
	    log "Improper permissions on authorized_user_ids file path."
	    continue
	fi

	# check permissions on the authorized_keys file path
	if ! check_key_file_permissions "$uname" "$RAW_AUTHORIZED_KEYS" ; then
	    log "Improper permissions on authorized_keys file path path."
	    continue
	fi

        # make temporary directory
        TMPDIR=$(mktemp -d)

	# trap to delete temporary directory on exit
	trap "rm -rf $TMPDIR" EXIT

        # create temporary authorized_user_ids file
        TMP_AUTHORIZED_USER_IDS="${TMPDIR}/authorized_user_ids"
        touch "$TMP_AUTHORIZED_USER_IDS"

        # create temporary authorized_keys file
        AUTHORIZED_KEYS="${TMPDIR}/authorized_keys"
        touch "$AUTHORIZED_KEYS"

        # set restrictive permissions on the temporary files
	# FIXME: is there a better way to do this?
        chmod 0700 "$TMPDIR"
        chmod 0600 "$AUTHORIZED_KEYS"
        chmod 0600 "$TMP_AUTHORIZED_USER_IDS"
        chown -R "$MONKEYSPHERE_USER" "$TMPDIR"

	# if the authorized_user_ids file exists...
	if [ -s "$authorizedUserIDs" ] ; then
            # copy user authorized_user_ids file to temporary
            # location
	    cat "$authorizedUserIDs" > "$TMP_AUTHORIZED_USER_IDS"

	    # export needed variables
	    export AUTHORIZED_KEYS
	    export TMP_AUTHORIZED_USER_IDS

	    # process authorized_user_ids file, as monkeysphere
	    # user
	    su_monkeysphere_user \
		". ${SHARE}/common; process_authorized_user_ids $TMP_AUTHORIZED_USER_IDS"
	    RETURN="$?"
	fi

	# add user-controlled authorized_keys file path if specified
	if [ "$rawAuthorizedKeys" != '-' -a -s "$rawAuthorizedKeys" ] ; then
	    log -n "adding raw authorized_keys file... "
	    cat "$rawAuthorizedKeys" >> "$AUTHORIZED_KEYS"
	    loge "done."
	fi

	# openssh appears to check the contents of the
        # authorized_keys file as the user in question, so the
        # file must be readable by that user at least.
	# FIXME: is there a better way to do this?
	chown root "$AUTHORIZED_KEYS"
	chgrp $(getent passwd "$uname" | cut -f4 -d:) "$AUTHORIZED_KEYS"
	chmod g+r "$AUTHORIZED_KEYS"

	# move the resulting authorized_keys file into place
	mv -f "$AUTHORIZED_KEYS" "${VARLIB}/authorized_keys/${uname}"

	# destroy temporary directory
	rm -rf "$TMPDIR"
    done
}

# generate server gpg key
gen_key() {
    local keyType
    local keyLength
    local keyUsage
    local keyExpire
    local revoker
    local hostName
    local userID
    local keyParameters
    local fingerprint

    # set default key parameter values
    keyType="RSA"
    keyLength="2048"
    keyUsage="auth"
    keyExpire=
    revoker=

    # get options
    TEMP=$(getopt -o l:e:r: -l length:,expire:,revoker: -n "$PGRM" -- "$@")

    if [ $? != 0 ] ; then
	exit 1
    fi

    # Note the quotes around `$TEMP': they are essential!
    eval set -- "$TEMP"

    while true ; do
	case "$1" in
	    -l|--length)
		keyLength="$2"
		shift 2
		;;
	    -e|--expire)
		keyExpire="$2"
		shift 2
		;;
	    -r|--revoker)
		revoker="$2"
		shift 2
		;;
	    --)
		shift
		;;
            *)
		break
		;;
	esac
    done

    hostName=${1:-$(hostname --fqdn)}
    userID="ssh://${hostName}"

    # check for presense of key with user ID
    if gpg_host --list-key ="$userID" > /dev/null 2>&1 ; then
	failure "Key for '$userID' already exists"
    fi

    # prompt about key expiration if not specified
    if [ -z "$keyExpire" ] ; then
	cat <<EOF
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
EOF
	while [ -z "$keyExpire" ] ; do
	    read -p "Key is valid for? (0) " keyExpire
	    if ! test_gpg_expire ${keyExpire:=0} ; then
		echo "invalid value"
		unset keyExpire
	    fi
	done
    elif ! test_gpg_expire "$keyExpire" ; then
	failure "invalid key expiration value '$keyExpire'."
    fi

    # set key parameters
    keyParameters=$(cat <<EOF
Key-Type: $keyType
Key-Length: $keyLength
Key-Usage: $keyUsage
Name-Real: $userID
Expire-Date: $keyExpire
EOF
)

    # add the revoker field if specified
    # FIXME: the "1:" below assumes that $REVOKER's key is an RSA key.
    # FIXME: key is marked "sensitive"?  is this appropriate?
    if [ "$revoker" ] ; then
	keyParameters="${keyParameters}"$(cat <<EOF
Revoker: 1:$revoker sensitive
EOF
)
    fi

    echo "The following key parameters will be used for the host private key:"
    echo "$keyParameters"

    read -p "Generate key? (Y/n) " OK; OK=${OK:=Y}
    if [ ${OK/y/Y} != 'Y' ] ; then
	failure "aborting."
    fi

    # add commit command
    keyParameters="${keyParameters}"$(cat <<EOF

%commit
%echo done
EOF
)

    log "generating server key..."
    echo "$keyParameters" | gpg_host --batch --gen-key

    # output the server fingerprint
    fingerprint_server_key "=${userID}"

    # find the key fingerprint of the server primary key
    fingerprint=$(gpg_host --list-key --with-colons --with-fingerprint "=${userID}" | \
	grep '^fpr:' | head -1 | cut -d: -f10)

    # export host ownertrust to authentication keyring
    log "setting ultimate owner trust for server key..."
    echo "${fingerprint}:6:" | gpg_authentication "--import-ownertrust"

    # translate the private key to ssh format, and export to a file
    # for sshs usage.
    # NOTE: assumes that the primary key is the proper key to use
    (umask 077 && \
	gpg_host --export-secret-key "$fingerprint" | \
	openpgp2ssh "$fingerprint" > "${VARLIB}/ssh_host_rsa_key")
    log "Private SSH host key output to file: ${VARLIB}/ssh_host_rsa_key"
}

# gpg output key fingerprint
fingerprint_server_key() {
    gpg_host --fingerprint --list-secret-keys
}

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

    # publish host key
    # FIXME: need to figure out better way to identify host key
    # dummy command so as not to publish fakes keys during testing
    # eventually:
    #gpg_authentication "--keyserver $KEYSERVER --send-keys $(hostname -f)"
    echo "NOT PUBLISHED (to avoid permanent publication errors during monkeysphere development)."
    echo "The following command should publish the key:"
    echo "monkeysphere-server gpg-authentication-cmd '--keyserver $KEYSERVER --send-keys $(hostname -f)'"
    exit 255
}

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

    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=$(date +%s -d "$warnwindow")

    echo "Checking host GPG key..."
    if (( "$keysfound" < 1 )); then
	echo "! No host key found."
	echo " - Recommendation: run 'monkeysphere-server gen-key'"
    elif (( "$keysfound" > 1 )); then
	echo "! More than one host key found?"
	# FIXME: recommend a way to resolve this
    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."
		# FIXME: recommend a way to resolve this other than re-keying?
	    elif (( "$expire" < "$warndate" )); then
		echo "! Host key expires in less than $warnwindow:" $(date -d "$(( $expire - $curdate )) seconds" +%F)
		# FIXME: recommend a way to resolve this?
	    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?"
	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?"
	    fi
	    if [ "$expire" ] ; then
		if (( "$expire" < "$curdate" )); then
		    echo "! User ID '$uid' is expired."
			# FIXME: recommend a way to resolve this
		elif (( "$expire" < "$warndate" )); then
		    echo "! User ID '$uid' expires in less than $warnwindow:" $(date -d "$(( $expire - $curdate )) seconds" +%F)		
		    # FIXME: recommend a way to resolve this
		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 "Checking host SSH key..."
	if [ ! -s "${VARLIB}/ssh_host_rsa_key" ] ; then
	    echo "! The host key as prepared for SSH (${VARLIB}/ssh_host_rsa_key) is missing or empty."
	else
	    if [ $(stat -c '%a' "${VARLIB}/ssh_host_rsa_key") != 600 ] ; then
		echo "! Permissions seem wrong for ${VARLIB}/ssh_host_rsa_key -- should be 0600."
	    fi

	    # propose changes needed for sshd_config (if any)
	    if ! grep -q "^HostKey ${VARLIB}/ssh_host_rsa_key$" /etc/ssh/sshd_config; then
		echo "! /etc/ssh/sshd_config does not point to the monkeysphere host key (${VARLIB}/ssh_host_rsa_key)."
		echo " - Recommendation: add a line to /etc/ssh/sshd_config: 'HostKey ${VARLIB}/ssh_host_rsa_key'"
	    fi
	    if badhostkeys=$(grep '^HostKey' | grep -q -v "^HostKey ${VARLIB}/ssh_host_rsa_key$") ; then
		echo "! /etc/sshd_config refers to some non-monkeysphere host keys:"
		echo "$badhostkeys"
		echo " - Recommendation: remove the above HostKey lines from /etc/ssh/sshd_config"
	    fi
	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

}

# retrieve key from web of trust, import it into the host keyring, and
# ltsign the key in the host keyring so that it may certify other keys
add_certifier() {
    local domain
    local trust
    local depth
    local keyID
    local fingerprint
    local ltsignCommand
    local trustval

    # set default values for trust depth and domain
    domain=
    trust=full
    depth=1

    # get options
    TEMP=$(getopt -o n:t:d: -l domain:,trust:,depth: -n "$PGRM" -- "$@")

    if [ $? != 0 ] ; then
	exit 1
    fi

    # Note the quotes around `$TEMP': they are essential!
    eval set -- "$TEMP"

    while true ; do
	case "$1" in
	    -n|--domain)
		domain="$2"
		shift 2
		;;
	    -t|--trust)
		trust="$2"
		shift 2
		;;
	    -d|--depth)
		depth="$2"
		shift 2
		;;
	    --)
		shift
		;;
            *)
		break
		;;
	esac
    done

    keyID="$1"
    if [ -z "$keyID" ] ; then
	failure "You must specify the key ID of a key to add."
    fi
    export keyID

    # get the key from the key server
    gpg_authentication "--keyserver $KEYSERVER --recv-key '$keyID'"

    # get the full fingerprint of a key ID
    fingerprint=$(gpg_authentication "--list-key --with-colons --with-fingerprint $keyID" | \
	grep '^fpr:' | grep "$keyID" | cut -d: -f10)

    echo "key found:"
    gpg_authentication "--fingerprint $fingerprint"

    echo "Are you sure you want to add this key as a certifier of"
    read -p "users on this system? (y/N) " OK; OK=${OK:-N}
    if [ "${OK/y/Y}" != 'Y' ] ; then
	failure "aborting."
    fi

    # export the key to the host keyring
    gpg_authentication "--export $keyID" | gpg_host --import

    if [ "$trust" == marginal ]; then
	trustval=1
    elif [ "$trust" == full ]; then
	trustval=2
    else
	failure "trust value requested ('$trust') was unclear (only 'marginal' or 'full' are supported)"
    fi

    # ltsign command
    # NOTE: *all* user IDs will be ltsigned
    ltsignCommand=$(cat <<EOF
ltsign
y
$trustval
$depth
$domain
y
save
EOF
	)

    # ltsign the key
    echo "$ltsignCommand" | gpg_host --quiet --command-fd 0 --edit-key "$fingerprint"

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

# delete a certifiers key from the host keyring
remove_certifier() {
    local keyID
    local fingerprint

    keyID="$1"
    if [ -z "$keyID" ] ; then
	failure "You must specify the key ID of a key to remove."
    fi

    # delete the requested key (with prompting)
    gpg_host --delete-key "$keyID"

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

# list the host certifiers
list_certifiers() {
    gpg_host --list-keys
}

# issue command to gpg-authentication keyring
gpg_authentication_cmd() {
    gpg_authentication "$@"
}

########################################################################
# 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:="${ETC}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"

# set empty config variable with ones from the environment, or with
# defaults
KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="subkeys.pgp.net"}}
AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.config/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:="${VARLIB}/gnupg-host"}
GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${VARLIB}/gnupg-authentication"}

# export variables needed in su invocation
export DATE
export MODE
export MONKEYSPHERE_USER
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
    'update-users'|'update-user'|'u')
	update_users "$@"
	;;

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

    'show-fingerprint'|'f')
	fingerprint_server_key
	;;

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

    'diagnostics'|'d')
	diagnostics
	;;

    'add-identity-certifier'|'add-certifier'|'a')
	add_certifier "$1"
	;;

    'remove-identity-certifier'|'remove-certifier'|'r')
	remove_certifier "$1"
	;;

    'list-identity-certifiers'|'list-certifiers'|'list-certifier'|'l')
	list_certifiers "$@"
	;;

    'gpg-authentication-cmd')
	gpg_authentication_cmd "$@"
	;;

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

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

exit "$RETURN"