#!/usr/bin/bash

KEYS_PATH=/usr/share/amazon-linux-sb-keys
CERT="${KEYS_PATH}/signing-CA.crt"

VERBOSE=false
NOASK=false
FORCE=false
EX_CODE=false
EXCLUDE_DBX=false
COMMAND=

log() {
    if "$VERBOSE" = true; then
	echo "$@"
    fi
}

err() {
    echo "$@" >&2
    exit 1
}

ask() {
    if [ "$NOASK" = "false" ]; then
	while true; do
	    read -r -p "$@ (y/N) " yn
	    case $yn in
		[Yy]* ) break;;
		[Nn]* | "") exit 1;;
	    esac
	done
    fi
}

cmd_status() {
    if [ "$secure_boot" -eq 1 ]; then
	echo "Enabled"
	if [ "$EX_CODE" = "true" ] ; then
	    exit 0
	fi
    elif [ "$secure_boot" -eq 0 ]; then
	if [ "$setup_mode" -eq 1 ]; then
	    echo "Disabled (Setup)"
	else
	    echo "Disabled"
	fi
    else
	echo "Unsupported"
    fi
    if [ "$EX_CODE" = "true" ] ; then
	exit 1
    fi
}

check_error() {
    result=$("${@}" 2>&1)
    if [[ $? != 0 ]]; then
        err "Failed running $*: ${result}"
    else
        log "${result}"
    fi
}

COLOR_OFF='\033[0m'
COLOR_RED='\033[0;31m'
COLOR_GREEN='\033[0;32m'

verify() {
    local _msg _file

    _msg="${1}"
    _file="${2}"

    if sbverify --cert "$CERT" "$_file" >/dev/null 2>&1; then
	printf "%-40s: ${COLOR_GREEN}OK${COLOR_OFF}\n" "$_msg"
    else
	printf "%-40s: ${COLOR_RED}FAIL${COLOR_OFF}\n" "$_msg"
	enroll_verif=0
    fi
}

cmd_enroll() {
    local _pkg _grub _shim _kernel _tmp_kernel

    if [ "$(id -u)" -ne 0 ]; then
	err "This command can only be run as root";
    fi

    if [ "$secure_boot" -eq 1 ]; then
	echo "Secure Boot is already enabled"
	exit 0
    fi
    if [ "$secure_boot" -eq -1 ]; then
	echo "Secure Boot isn't supported"
	exit 1
    fi

    if [ "$setup_mode" -ne 1 ]; then
	echo "Secure Boot cannot be enabled (not in Setup mode)"
	exit 1
    fi

    if [ ! -e "$CERT" ] ; then
	err "Can't find certificate file $CERT, check package installation"
    fi

    echo "Checking bootloader and kernel signatures..."
    echo
    echo "WARNING: This only checks they have been signed by an Amazon Linux"
    echo "         key. This does NOT check that the key hasn't been revoked."
    echo "         Ensure your kernels and bootloaders are up to date with"
    echo "         the version of the installed amazon-linux-sb-keys package."
    echo
    echo "Checking signatures:"
    echo
    enroll_verif=1

    if [ -f /boot/efi/EFI/BOOT/BOOTX64.EFI ] ; then
	_grub=/boot/efi/EFI/BOOT/BOOTX64.EFI
    elif [ -f /boot/efi/EFI/BOOT/BOOTAA64.EFI ] ; then
	_grub=/boot/efi/EFI/BOOT/BOOTAA64.EFI
    else
        ask "Grub2 bootloader or shim no found, ignore ?"
	_grub=""
    fi

    _shim=""
    if [ -n "$_grub" ] ; then	
	# Check if it's a grub or a shim
	_pkg=$(rpm -qf "$_grub")
	if echo $_pkg | grep -q grub ; then
	    log "$_grub bootloader appears to be grub"
	elif echo $_pkg | grep -q shim ; then
	    log "$_grub bootloader appears to be a shim"

	    _shim="$_grub"

	    if [ -f /boot/efi/EFI/amzn/grubx64.efi ] ; then
		_grub=/boot/efi/EFI/amzn/grubx64.efi
	    elif [ -f /boot/efi/EFI/amzn/grubaa64.efi ] ; then
		_grub=/boot/efi/EFI/amzn/grubaa64.efi
	    else
		ask "Grub2 bootloader unrecognized, ignore ?"
		_grub=
	    fi
	else
            echo "WARNING: Bootloader of an unknown type"
	fi
    fi
    _tmp_kernel=""
    _kernel=$(grubby --default-kernel)
    _kernel=$(readlink -e "$_kernel")
    if [ $? -ne 0 ] ; then
	err "Can't locate default kernel"
    fi

    # Check for gzip'ed kernel
    if [[ $(file -b --mime-type "$_kernel") =~ "application/gzip" ]] ; then
	echo "arm64 style gzipped kernel detected, decompressing..."
	_tmp_kernel=$(mktemp)
	if [ $? -ne 0 ] ; then
	    err "Failed to create temp file for kernel"
	fi
	gunzip -c "$_kernel" > "$_tmp_kernel"
	if [ $? -ne 0 ] ; then
	    err "Failed to decompress kernel"
	fi
	_kernel="$_tmp_kernel"
    fi

    # Verify that the various images are signed by our CA
    if [ -n "$_shim" ] ; then	
	verify "Shim bootloader" "$_shim"
    fi
    if [ -n "$_grub" ] ; then	
	verify "Grub2 bootloader" "$_grub"
    fi
    if [ -n "$_kernel" ] ; then	
	verify "Linux Kernel" "$_kernel"
    fi

    # Cleanup
    if [ -n "$_tmp_kernel" ] ; then
	rm "$_tmp_kernel"
    fi

    echo
    if [ $enroll_verif -ne 1 ] ; then
        if [ $FORCE != "false" ] ; then
            echo "Signature verification failed, but enrolling anyway (--force in effect)..."
        else
            err "Signature verification failed."
        fi
	echo
    fi

    echo "Enrolling the Secure Boot keys in instance cannot be undone."
    echo
    ask "Please confirm you want to proceed"
    echo
    echo "Enrolling ..."

    # Enroll PK first, the other ones need to be signed by it"
    log "Enrolling PK"
    check_error efivar -A 39 -n 8be4df61-93ca-11d2-aa0d-00e098032b8c-PK -w -f "${KEYS_PATH}/PK.esl.auth"
    log "Enrolling KEK"
    check_error efivar -A 39 -n 8be4df61-93ca-11d2-aa0d-00e098032b8c-KEK -w -f "${KEYS_PATH}/KEK.esl.auth"
    log "Enrolling db"
    check_error efivar -A 39 -n d719b2cb-3d3a-4596-a3bc-dad00e67656f-db -w -f "${KEYS_PATH}/db.esl.auth"
    if [ "$EXCLUDE_DBX" = "true" ] ; then
	echo "Skipping dbx enrollment. Enroll manually if necessary using as root:"
	echo
	echo " efivar -A 0x27 -n d719b2cb-3d3a-4596-a3bc-dad00e67656f-dbx \\"
	echo "   -a -f \"${KEYS_PATH}/dbx.esl.auth\" dbx"
	echo
    else
	# Don't try to enroll an empty dbx
	if [ -s "${KEYS_PATH}/dbx.esl" ] ; then
	    log "Enrolling dbx"
	    # Note: efivar -a doesn't work when the variable doesn't exist so
	    #       we use -w with the append flag set in the attributes
	    check_error efivar -A 103 -n d719b2cb-3d3a-4596-a3bc-dad00e67656f-dbx -w -f "${KEYS_PATH}/dbx.esl.auth"
	fi
    fi

    echo "Done. Reboot for Secure Boot to be fully enabled"
}

usage() {
    local me=$(basename "${0}")
    echo
    echo "Usage: $me [options] command"
    echo
    echo " This tool us used to querty the state of Secure Boot and enable it"
    echo " by enrolling Amazon Linux keys in an EC2 instance."
    echo
    echo " This tool must be run as root."
    echo
    echo "Options:"
    echo "  -v, --verbose       Verbose log messages"
    echo "  -y, --yes           Assume Y to all questions"
    echo "  -f, --force         Continue even if signature verification fails, implies --yes."
    echo "  -e, --exit-code     Set exit code for status command"
    echo "  -X, --exclude-dbx   Exclude enrolling dbx for enroll command"
    echo "  -h, --help          Print this"
    echo
    echo "Commands:"
    echo "  status              Display secure boot status. Can be either of:"
    echo "                         - Enabled          : Secure boot is enabled"
    echo "                         - Disabled (Setup) : Secure boot is not enabled (can enroll)"
    echo "                         - Disabled         : Secure boot is not enabled (cannot enroll)"
    echo "                         - Unsupported : Secure boot is not supported (not in UEFI mode)"
    echo "                      If -e/--exit-code is specified, the command will have a success"
    echo "                      status if secure boot is enabled, and failure otherwise"
    echo
    echo "  enroll              Enroll Amazon Linux secure boot keys. This will cause Secure Boot"
    echo "                      to be enable on this instance. For this to work the instance must:"
    echo "                         - Support secure boot (be booted in UEFI mode)"
    echo "                         - Be in \"Setup Mode\" (secure boot is Disabled)"
    echo "                      The command will have a success status if secure boot is already"
    echo "                      enabled or has been successfully enrolled, failure otherwise."
    echo "                      If -X/--exclude-dbx is specified, the revocation database (dbx)"
    echo "                      will not be enrolled. This is useful for certain testing cases."
    echo
    exit 0
}

set_command() {
    if [ -n "$COMMAND" ]; then
	err "Only one command at a time !"
    fi
    COMMAND=$1
}

# Parse arguments. TODO: Use getopt
set +u
while [ -n "$1" ]; do
    case "$1" in
	--verbose | -v)
	    VERBOSE=true
	    ;;
	--help | -h)
	    usage
	    ;;
	--force | -f)
	    FORCE=true
	    NOASK=true
	    ;;
    --yes | -y)
	    NOASK=true
	    ;;
	--exit-code | -e)
	    EX_CODE=true
	    ;;
	--exclude-dbx | -X)
	    EXCLUDE_DBX=true
	    ;;
	status|enroll)
	    set_command "$1"
	    ;;
	*)
	    err "Unknown command/option $1"	    
    esac
    shift
done
set -u

if [ -z "$COMMAND" ] ; then
    COMMAND=status
fi

# Get current SB status
setup_mode=-1
secure_boot=$(efivar -p -d -n 8be4df61-93ca-11d2-aa0d-00e098032b8c-SecureBoot 2>/dev/null)
if [ $? -ne 0 ] ; then
    secure_boot=-1
else
    setup_mode=$(efivar -p -d -n 8be4df61-93ca-11d2-aa0d-00e098032b8c-SetupMode 2>/dev/null)
fi
log "secure_boot = $secure_boot"
log " setup_mode = $setup_mode"

# Run command
cmd_"$COMMAND"

exit 0
