#!/bin/bash
# Sign a valid directory of kexec params
set -e -o pipefail
. /tmp/config
. /etc/functions.sh

TRACE_FUNC

rollback="n"
update="n"
while getopts "p:c:ur" arg; do
	case $arg in
	p) paramsdir="$OPTARG" ;;
	c)
		counter="$OPTARG"
		rollback="y"
		;;
	u) update="y" ;;
	r) rollback="y" ;;
	esac
done

if [ -z "$paramsdir" ]; then
	DIE "Usage: $0 -p /boot [ -u | -c counter ]"
fi

paramsdir="${paramsdir%%/}"

assert_signable
TRACE_FUNC

# remount /boot as rw
mount -o remount,rw /boot

DEBUG "Signing kexec parameters in $paramsdir, rollback=$rollback, update=$update, counter=$counter"

# All writes go to a staging directory first; files are moved to their final
# locations only after signing succeeds.  This prevents a failed signing
# attempt (wrong PIN, connection error, etc.) from leaving /boot with updated
# hash files but a stale or missing signature.
stagedir=$(mktemp -d /tmp/kexec-sign-XXXXXX)
cleanup_stagedir() { rm -rf "$stagedir"; }
trap cleanup_stagedir EXIT

# Seed staging with any existing kexec*.txt from paramsdir so unchanged files
# are included when building param_files for signing.
for f in "$paramsdir"/kexec*.txt; do
	[ -e "$f" ] && cp "$f" "$stagedir/"
done

# update hashes in staging before signing
if [ "$update" = "y" ]; then
	(
		hash_pipeline_exit=0
		TRACE_FUNC
		DEBUG "update=y: Updating kexec hashes in staging dir $stagedir"
		cd /boot
		find ./ -type f ! -path './kexec*' -print0 | xargs -0 sha256sum >"$stagedir/kexec_hashes.txt" ||
			hash_pipeline_exit=$?
		#also save the file & directory structure to detect added files
		print_tree >"$stagedir/kexec_tree.txt"
		exit $hash_pipeline_exit
	)
	[ $? -eq 0 ] || DIE "$paramsdir: Failed to update hashes."
fi

if [ "$rollback" = "y" ]; then
	rollback_file="$paramsdir/kexec_rollback.txt"

	DEBUG "rollback=y, counter=$counter, paramsdir=$paramsdir, rollback_file=$rollback_file"
	TRACE_FUNC

	if [ -n "$counter" ]; then
		DEBUG "rollback=y: provided counter=$counter, will read tpm counter next"
		TRACE_FUNC

		# use existing tpm counter
		DO_WITH_DEBUG read_tpm_counter "$counter" >/dev/null 2>&1 ||
			DIE "$paramsdir: Unable to read tpm counter '$counter'"
	else
		DEBUG "rollback=y: counter was not provided: checking for existing TPM counter from TPM rollback_file=$rollback_file"
		TRACE_FUNC

		if [ -e "$rollback_file" ]; then
			# Extract TPM_COUNTER from rollback file
			TPM_COUNTER=$(grep -o 'counter-[0-9a-f]*' "$rollback_file" | cut -d- -f2)
			DEBUG "rollback=y: Found TPM counter $TPM_COUNTER in rollback file $rollback_file"
		else
			DEBUG "Rollback file $rollback_file does not exist. Creating new TPM counter."
			DO_WITH_DEBUG check_tpm_counter $rollback_file ||
				DIE "$paramsdir: Unable to find/create tpm counter"

			TRACE_FUNC
			TPM_COUNTER=$(cut -d: -f1 </tmp/counter | tr -d '\n')
			DEBUG "rollback=y: Created new TPM counter $TPM_COUNTER"
		fi
	fi

	TRACE_FUNC

	# Increment the TPM counter
	DEBUG "rollback=y: Incrementing counter $TPM_COUNTER."
	increment_tpm_counter $TPM_COUNTER ||
		DIE "$paramsdir: Unable to increment tpm counter"

	# Ensure the incremented counter file exists
	incremented_counter_file="/tmp/counter-$TPM_COUNTER"
	if [ ! -e "$incremented_counter_file" ]; then
		DEBUG "TPM counter file '$incremented_counter_file' not found. Attempting to read it again."
		# read_tpm_counter doesn't prompt; silence its normal output if called in DEBUG context
		DO_WITH_DEBUG read_tpm_counter "$TPM_COUNTER" >/dev/null 2>&1 ||
			DIE "$paramsdir: TPM counter file '$incremented_counter_file' not found after incrementing."
	fi

	DEBUG "TPM counter file '$incremented_counter_file' found."

	# Write rollback file to staging; moved to paramsdir only on signing success
	sha256sum "$incremented_counter_file" >"$stagedir/kexec_rollback.txt" ||
		DIE "$paramsdir: Unable to create rollback file in staging"
fi

TRACE_FUNC

# Collect the list of kexec*.txt files present in staging (relative names
# only, so the sha256sum output is path-independent and check_config can
# reproduce the same signed data from $paramsdir after the move).
param_files=()
for f in "$stagedir"/kexec*.txt; do
	[ -e "$f" ] || continue
	param_files+=( "$(basename "$f")" )
done
if [ ${#param_files[@]} -eq 0 ]; then
	DIE "$paramsdir: No kexec parameter files to sign"
fi
DEBUG "kexec-sign-config.sh: ${#param_files[@]} file(s) to sign from $stagedir: ${param_files[*]}"

# before we even attempt to sign, make sure there is at least one public
# key available so the user gets a clear error instead of a mysterious gpg
# failure.
if [ "$(gpg -k 2>/dev/null | wc -l)" -eq 0 ]; then
	DIE "$paramsdir: no public GPG keys in keyring; add one via Options --> GPG Options --> Add GPG key to running BIOS and reflash, or perform OEM Factory Reset / Re-Ownership."
fi

for tries in 1 2 3; do
	confirm_gpg_card
	TRACE_FUNC
	DEBUG "kexec-sign-config.sh: signing attempt ${tries}/3 begins after GPG card confirmation"

	# Public keys are not sufficient for signing. After cache_gpg_signing_pin,
	# force discovery of a usable secret key identity and pass it explicitly
	# to gpg, instead of relying on implicit default-key selection.
	card_status_output=$(gpg --card-status 2>/dev/null || true)
	SIGNING_KEY_ID=$(gpg --with-colons --list-secret-keys 2>/dev/null | awk -F: '$1=="sec"||$1=="ssb" {print $5; exit}')
	if [ -z "$SIGNING_KEY_ID" ]; then
		CARD_SIGNING_KEY_ID=$(echo "$card_status_output" | awk -F: '/Signature key/ {gsub(/[[:space:]]/,"",$2); print $2; exit}')
		if [ -n "$CARD_SIGNING_KEY_ID" ]; then
			SIGNING_KEY_ID="$CARD_SIGNING_KEY_ID"
			DEBUG "kexec-sign-config.sh: using card-reported signing key id ${SIGNING_KEY_ID}"
		fi
	fi
	if [ -z "$SIGNING_KEY_ID" ]; then
		DIE "$paramsdir: no private signing key is available. A public key in keyring is not enough to sign. Insert/unlock the signing smartcard (or import private key backup material), then retry. If smartcard is inserted, ensure it contains a signing key and that GPG card status shows a non-empty 'Signature key'."
	fi
	DEBUG "kexec-sign-config.sh: using explicit signing key id ${SIGNING_KEY_ID}"

	# Sign using relative filenames (cd into stagedir) so the sha256sum
	# output contains bare names like "kexec_hashes.txt" rather than the
	# full staging path.  check_config reproduces the same output by doing
	# the same cd+sha256sum from $paramsdir after the files are moved there.
	DEBUG "kexec-sign-config.sh: running sha256sum (relative) | gpg in $stagedir"
	if (cd "$stagedir" && sha256sum "${param_files[@]}") | \
		gpg --local-user "$SIGNING_KEY_ID" \
			--pinentry-mode=loopback \
			--passphrase-file /tmp/secret/gpg_pin \
			--detach-sign -a >"$stagedir/kexec.sig" 2>/tmp/kexec-sign.log; then
		# Signing succeeded — move all staged files to their final locations
		DEBUG "kexec-sign-config.sh: signing succeeded; moving staged files to $paramsdir"
		for f in "$stagedir"/*; do
			[ -e "$f" ] || continue
			DEBUG "kexec-sign-config.sh: mv $f -> $paramsdir/$(basename "$f")"
			mv "$f" "$paramsdir/$(basename "$f")"
		done
		DEBUG "kexec-sign-config.sh: all staged files moved to $paramsdir"

		# Remove any package trigger log files now that the user has signed
		rm -f /boot/kexec_package_trigger*

		# Validate the final config in paramsdir
		DEBUG "kexec-sign-config.sh: calling check_config $paramsdir"
		check_config "$paramsdir"

		# remount /boot as ro
		mount -o remount,ro /boot

		STATUS_OK "Boot hashes signed successfully"
		exit 0
	fi

	DEBUG "kexec-sign-config.sh: signing attempt ${tries}/3 failed"
	if [ -r /tmp/kexec-sign.log ]; then
		DEBUG "kexec-sign-config.sh: gpg signing stderr/stdout excerpt follows"
		DEBUG "$(sed -n '1,40p' /tmp/kexec-sign.log)"
	fi

	if grep -Eiq 'no default secret key|no secret key|secret key not available|signing failed: no secret key' /tmp/kexec-sign.log 2>/dev/null; then
		DIE "$paramsdir: GPG signing failed because no private signing key is available to gpg. Confirm the expected signing key is present/unlocked on your smartcard or imported backup key material, then retry (Options --> GPG Options --> Add GPG key to running BIOS and reflash, or OEM Factory Reset / Re-Ownership)."
	fi

	# Bad PIN after cache_gpg_signing_pin pre-validation is unexpected; clear
	# and retry so the user can re-enter the correct PIN on the next attempt.
	if grep -Eiq 'bad pin|wrong pin|incorrect pin|pin incorrect|pinentry.*cancel' /tmp/kexec-sign.log 2>/dev/null; then
		if [ "$tries" -lt 3 ]; then
			WARN "$paramsdir: GPG signing failed due to incorrect PIN (attempt $tries/3) - re-enter the correct PIN at the next prompt"
			rm -f /tmp/secret/gpg_pin
			continue
		else
			DIE "$paramsdir: GPG signing failed due to incorrect PIN after 3 attempts. Check remaining retries in the GPG card status menu; if retries are exhausted, unblock/reset with Admin PIN and try again."
		fi
	fi

	if grep -Eiq 'pin blocked|card is blocked|authentication failed' /tmp/kexec-sign.log 2>/dev/null; then
		DIE "$paramsdir: GPG signing failed because smartcard PIN is blocked or authentication is denied. Unblock/reset the card PIN with Admin PIN (or use valid backup key material) and retry signing."
	fi
done

# remount /boot as ro
mount -o remount,ro /boot

DIE "$paramsdir: Unable to sign kexec hashes"
