#!/bin/sh -efu
#
# Configures the system bootloader to load the kernel in
# IMA fix mode and reboots the system. It then is used to
# run the signing process and to configure the system bootloader
# to load the kernel in IMA enforce mode and to reboot the system.
#
# Copyright (C) 2023  Denis Medvedev.
# Copyright (C) 2026  Paul Wolneykien.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
. shell-error
. shell-make-config

REBOOT_DELAY=5
STATEDIR=/var/lib/integrity_update

[ ! -e /etc/integrity/config ] || . /etc/integrity/config
[ ! -e /etc/sysconfig/integrity ] || . /etc/sysconfig/integrity

PROG="${0##*/}"
VERSION="0.8.2"

HASH_ALGO="${HASH_ALGO:-sha512}"
CERT="${CERT:-}"
PRIVKEY="${PRIVKEY:-}"
CERT_BASENAME="${CERT_BASENAME:-x509}"
RESIGN="${RESIGN:-}"

usage()
{
    [ "$1" = 0 ] || exec >&2
    echo "Usage: $PROG action [ options ... ]"
    echo
    echo "# This is the 1st step:"
    echo "       $PROG -i | --init [ -G | --disable-graphics ] [ -v | --verbose ] [ --hash=HASH ]"
    echo
    echo "# This is the 2nd step:"
    echo "       $PROG -s | --sign [ -I | --skip-initrd ] [ --log FILE ] [ --cert=CERT.pem ] [ --key=KEY.pem ] [ -B BASENAME | --basename=BASENAME ] [ -r | --resign ] [ -v | --verbose ] [ -E | --with-evm ] [ --without-evm ] [ -R | --no-reboot ]"
    echo
    echo "# Other:"
    echo "       $PROG -A | --auto [ -I | --skip-initrd ] [ --log FILE ] [ --cert=CERT.pem ] [ --key=KEY.pem ] [ -B BASENAME | --basename=BASENAME ] [ -r | --resign ] [ -v | --verbose ] [ --hash=HASH ] [ -E | --with-evm ] [ --without-evm ]"
    echo "       $PROG --initrd-only"
    echo "       $PROG -h | --help"
    echo "       $PROG -V | --version"
    exit "${1:-0}"
}

TEMP="$(getopt -n "$PROG" -o a:GvsiRIAhVErB: -l hash:,disable-graphics,with-evm,without-evm,init,sign,no-reboot,skip-initrd,initrd-only,verbose,log:,log-stderr,auto,help,version,cert:,key:,resign,basename: -- "$@")" || usage 1
eval set -- "$TEMP"

mode=
disable_graphics=
no_reboot=
skip_initrd=
log_file=
auto=
override_hash=
with_evm=
without_evm=
cert=
key=
basename=
resign=

if [ "${IMA_SIGNING_SERVICE:-0}" -ne 0 ]; then
    message "Reading options from $STATEDIR..."
    with_evm="$(cat "$STATEDIR"/with_evm)" ||:
    without_evm="$(cat "$STATEDIR"/without_evm)" ||:
    cert="$(cat "$STATEDIR"/cert)" ||:
    key="$(cat "$STATEDIR"/key)" ||:
    basename="$(cat "$STATEDIR"/basename)" ||:
    verbose="$(cat "$STATEDIR"/verbose)" ||:
    resign="$(cat "$STATEDIR"/resign)" ||:
    log_file="$(cat "$STATEDIR"/log_file)" ||:

    [ -z "$log_file" ] || LOG_FILE="$log_file"

    if [ -n "$LOG_FILE" -a "$LOG_FILE" != '-' ]; then
	truncate -s0 "$LOG_FILE"
	message "Redirect messages to $LOG_FILE."
	exec 2>"$LOG_FILE"
	export NO_TRUNCATE_LOG=1
    fi
fi

while :; do
    case "$1" in
	-i|--init)
	    mode=init
	    ;;
	-s|--sign)
	    mode=sign
	    ;;
	-R|--no-reboot)
	    no_reboot=1
	    ;;
	-I|--skip-initrd)
	    skip_initrd=1
	    ;;
	--initrd-only)
	    mode=initrd_only
	    ;;
	-A|--auto)
	    auto=1
	    ;;
	-v|--verbose)
	    #shellcheck disable=SC2034
	    verbose=y
	    ;;
        -a|--hash)
	    shift; HASH_ALGO="$1"
	    override_hash=1
            ;;
	-G|--disable-graphics)
	    disable_graphics=y
	    ;;
        --log)
	    shift; log_file="$1"
            ;;
        --log-stderr)
	    log_file="-"
            ;;
	-E|--with-evm)
	    with_evm=1
	    ;;
	--without-evm)
	    without_evm=1
	    ;;
	-r|--resign)
	    resign=1
	    ;;
        --cert)
	    shift; cert="$(realpath "$1")"
            ;;
        --key)
	    shift; key="$(realpath "$1")"
            ;;
	-B|--basename)
	    shift; basename="$1"
	    ;;
        -h|--help)
	    usage 0
            ;;
	-V|--version)
	    cat <<EOF
$VERSION 2026
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
EOF
	    exit 0
	    ;;
        --)
	    shift
	    break
            ;;
        *)
	    message "$PROG: unrecognized option: $1" >&2
	    usage 1
            ;;
    esac
    shift
done

[ -z "$basename" ] || CERT_BASENAME="$basename"
[ -z "$resign" ] || RESIGN="$resign"
[ -z "$cert" ] || CERT="$cert"
[ -z "$key" ] || PRIVKEY="$key"

get_ima_hash() {
    sed -e 's/^.*[[:space:]]ima_hash=\([^[:space:]]\+\).*$/\1/' </proc/cmdline
}

get_ima_appraise() {
    sed -e 's/^.*[[:space:]]ima_appraise=\([^[:space:]]\+\).*$/\1/' </proc/cmdline
}

current_appraise="$(get_ima_appraise)"
current_hash="$(get_ima_hash)"

if [ -z "$HASH_ALGO" -a -z "$current_hash" ]; then
    fatal "Hash algo isn't set (config or cmdline)"
elif [ -z "$HASH_ALGO" ]; then
    HASH_ALGO="$current_hash"
fi

if [ -z "$mode" ]; then
    case "$current_appraise" in
	fix)
	    message "Current IMA mode is 'fix'. Select signing mode."
	    mode=sign
	    ;;
	*)
	    message "Current IMA mode is not 'fix'. Select initialization mode."
	    mode=init
	    ;;
    esac
fi

if [ "$mode" != 'init' ]; then
    if [ -z "$override_hash" ]; then
	if [ -n "$current_hash" ]; then
	    HASH_ALGO="$current_hash"
	fi
    else
	fatal "Can't override hash in Stage II."
    fi
fi

inherit_keyring()
{
    local _sd_booted=

    _sd_booted="$(which sd_booted 2>/dev/null)" ||:
    if [ -n "$_sd_booted" ]; then
	if "$_sd_booted"; then
	    systemd-run -p KeyringMode=inherit \
			-p PrivateTmp=false \
			--service-type=oneshot \
			--quiet \
			-E NO_TRUNCATE_LOG="${NO_TRUNCATE_LOG:-0}" \
			-P -- "$@"
	    return $?
	fi
    fi

    "$@"
}

case "$mode" in
    init)
	rm -rf "$STATEDIR"
	mkdir -p "$STATEDIR"

	systemctl is-enabled integalert.service \
		  >"$STATEDIR"/integalert.status ||:

	case "$(cat "$STATEDIR"/integalert.status)" in
	    enabled)
		message "Disabling the integalert.service..."
		systemctl disable integalert.service
		;;
	esac

	systemctl get-default >"$STATEDIR"/default

	if [ -z "$auto" ]; then
	    if [ -n "$disable_graphics" ]; then
		message "Disabling graphical.target..."
		systemctl set-default multi-user.target
	    fi
	else
	    message "Writing options to $STATEDIR..."
	    echo "$with_evm"      >"$STATEDIR"/with_evm
	    echo "$without_evm"   >"$STATEDIR"/without_evm
	    echo "$cert"          >"$STATEDIR"/cert
	    echo "$key"           >"$STATEDIR"/key
	    echo "$basename"      >"$STATEDIR"/basename
	    echo "$verbose"       >"$STATEDIR"/verbose
	    echo "$resign"        >"$STATEDIR"/resign
	    echo "$log_file"      >"$STATEDIR"/log_file

	    systemctl set-default ima-signing.target
	    systemctl enable ima-signing.service
	    message "The special ima-signing.target is set for the next boot."
	fi

	message "Reconfiguring the bootloader..."

	SKIP_APPLY=1 control ima_hash "$HASH_ALGO"
	control ima_appraise fix

	message "Finished stage I."

	if [ -z "$no_reboot" ]; then
	    if [ -z "$auto" ]; then
		message "Rebooting to Stage II... After reboot please run '$PROG --sign [ options ... ]'."
	    else
		message "Rebooting to Stage II... After reboot it will be run automatically."
	    fi
	    [ -z "$REBOOT_DELAY" ] || sleep "$REBOOT_DELAY"
	    reboot -fp
	else
	    message "Now please reboot. After reboot run '$PROG --sign [ options ... ]'."
	fi
	;;
    sign|initrd_only)
	if [ -z "$log_file" -a -n "$LOG_FILE" ]; then
	    log_file="$LOG_FILE"
	    [ "$log_file" = "-" ] || \
		message "Write file signing log to $LOG_FILE as configured..."
	fi

	case "$mode" in
	    sign)
		if [ -z "$PRIVKEY" ]; then
		    # Enable resign with on-the-fly generated
		    # one-time key:
		    RESIGN=1
		fi

		do_update=
		if [ -n "$CERT" -o -z "$PRIVKEY" ]; then
		    do_update=1
		fi

		ret=0
		inherit_keyring \
	        /usr/sbin/integrity-sign ${verbose:+-v} --sign \
		                         ${do_update:+-U --basename="${CERT_BASENAME}"} \
					 ${log_file:+--log "$log_file"} \
					 ${with_evm:+--with-evm} \
					 ${without_evm:+--without-evm} \
					 ${override_hash:+--hash="$HASH_ALGO"} \
					 ${CERT:+--cert="$CERT"} \
					 ${PRIVKEY:+--key="$PRIVKEY"} \
					 ${RESIGN:+--resign} \
		    || ret=$?

		# Re-open the log file for append:
		exec 2>>"$LOG_FILE"

		[ "$ret" -eq 0 ] || fatal "Error while signing the files!"
	        ;;
	esac

	if [ -z "$skip_initrd" ]; then
	    message "Regenerating initrd image..."
	    add_make_value /etc/initrd.mk FEATURES integrity

	    case "$HASH_ALGO" in
		streebog*)
		    add_make_value /etc/initrd.mk \
				   INTEGRITY_FEATURES \
				   gost
		    ;;
		*)
		    del_make_value /etc/initrd.mk \
				   INTEGRITY_FEATURES \
				   gost
		    ;;
	    esac

	    make-initrd || \
		fatal "Error regenerating initrd image!"

	    case "$mode" in
		initrd_only)
		    exit 0
		    ;;
	    esac
	fi

	if [ -e "$STATEDIR"/integalert.status ]; then
	    case "$(cat "$STATEDIR"/integalert.status)" in
		enabled)
		    message "Updating integalert data..."
		    integalert="$(which integalert 2>/dev/null)" ||:
		    if [ -n "$integalert" ]; then
			"$integalert" fix
		    fi

		    message "Re-enabling the integalert.service..."
		    systemctl enable integalert.service
		    ;;
	    esac
	fi

	if [ -e "$STATEDIR"/default ]; then
	    message "Re-enabling the default target..."
	    systemctl set-default "$(cat "$STATEDIR"/default)"
	fi

	message "Reconfiguring the bootloader..."

	SKIP_APPLY=1 control ima_hash "$HASH_ALGO"
	control ima_appraise enforce

	message "Stage II is done."

	[ -z "$skip_initrd" ] || \
	    message "WARNING! You may also need to regenerate initrd image (you can use --initrd-only option for that)."

	if [ -z "$no_reboot" ]; then
	    message "Rebooting to normal mode..."
	    [ -z "$REBOOT_DELAY" ] || sleep "$REBOOT_DELAY"
	    reboot -fp
	else
	    message "Now please reboot. After reboot the system will be ready to use${skip_initrd:+ (if initrd image is okay)}."
	fi
	;;
esac
