#!/bin/sh -efu
#
# Runs the IMA and EVM file signing procedure. It can also be used
# to verify the file signatures.
#
# Copyright (C) 2018 Mikhail Efremov <sem@altlinux.org>
# Copyright (C) 2023 Denis Medvedev <nbr@altlinux.org>
# Copyright (C) 2026 Paul Wolneykien <manowar@altlinux.org>
#
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

. shell-error

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

HASH_ALGO="${HASH_ALGO:-sha512}"
LIBDIRS="${LIBDIRS:-/lib /usr/lib}"
EXECLIBDIRS="${EXECLIBDIRS:-/usr/libexec $LIBDIRS}"
WITH_EVM="${WITH_EVM:-}"
GOST_PARAMSET="${GOST_PARAMSET:-paramset:A}"
LIB_EXT="${LIB_EXT:-}"
CERT="${CERT:-}"
PRIVKEY="${PRIVKEY:-}"
CERT_BASENAME="${CERT_BASENAME:-x509}"

OPT_LIST=
OPT_STDIN=
OPT_VERIFY=
OPT_FILE_LOG=

verbose=

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

workdir=
init_workdir()
{
    workdir="$(mktemp --tmpdir -d "$PROG.XXXXXXXX")" || \
	fatal "Failed to create workdir!"
    [ -n "$workdir" ] || \
	fatal "BUG: Unexpectedly empty workdir path!"
}

_cleanup_handler()
{
    local rc=$?
    trap - EXIT
    [ -z "$workdir" ] || rm -rf -- "$workdir"
    exit $rc
}

trap _cleanup_handler EXIT HUP PIPE INT QUIT TERM

_check_update()
{
    [ -n "$update" ] || \
	fatal "Can't touch files under /etc/keys/ without -U | --update option. WARNING! Use it with caution and only when you know what you are doing."
}

init_kmk_user()
{
    if [ -e /etc/keys/kmk-user.blob ]; then
	verbose "Don't replace /etc/keys/kmk-user.blob."
	return 0
    fi

    _check_update

    message "Generating kmk-user key..."
    local id=
    id="$(keyctl search @u user kmk-user 2>/dev/null)" ||:
    [ -z "$id" ] || keyctl unlink "$id" @u >/dev/null || return $?
    dd if=/dev/urandom bs=1 count=32 2>/dev/null | \
	keyctl padd user kmk-user @u >/dev/null && \
	keyctl pipe "$(keyctl search @u user kmk-user)" \
	       >"$workdir"/kmk-user.blob || return $?
    mv -f "$workdir"/kmk-user.blob /etc/keys/ || return $?
}

init_evm_key()
{
    if [ -e /etc/keys/evm-key.blob ]; then
	verbose "Don't replace /etc/keys/evm-key.blob."
	return 0
    fi

    _check_update

    message "Generating evm-key..."
    local id=
    id="$(keyctl search @u encrypted evm-key 2>/dev/null)" ||:
    [ -z "$id" ] || keyctl unlink "$id" @u >/dev/null || return $?
    keyctl add encrypted evm-key "new user:kmk-user 64" @u >/dev/null && \
	keyctl pipe "$(keyctl search @u encrypted evm-key)" \
	       >"$workdir"/evm-key.blob || return $?
    mv -f "$workdir"/evm-key.blob /etc/keys/ || return $?
}

configure_openssl()
{
    [ -n "$workdir" ] || init_workdir || return $?

    [ ! -e "$workdir"/openssl.cnf ] || return 0

    local conf_hash_algo=
    case "$HASH_ALGO" in
        sha*)
            conf_hash_algo="$HASH_ALGO"
            ;;
        streebog256)
            conf_hash_algo='md_gost12_256'
            ;;
        streebog512)
            conf_hash_algo='md_gost12_512'
            ;;
        *)
            fatal "Unknown hash algo: $HASH_ALGO"
            ;;
    esac

    cat >"$workdir"/openssl.cnf <<EOF || return $?
HOME = .
openssl_conf = openssl_init

[ req ]
default_bits = 2048
default_md = $conf_hash_algo
distinguished_name = req_distinguished_name
prompt = no
string_mask = utf8only
x509_extensions = myexts

[ req_distinguished_name ]
O = IMAALT
CN = Executable Signing Key

[ myexts ]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid

[openssl_init]
EOF

    case "$HASH_ALGO" in
	streebog*)
	    cat >>"$workdir"/openssl.cnf <<EOF || return $?
engines = engine_section

[engine_section]
gost = gost_section

[ gost_section ]
engine_id = gost
default_algorithms = ALL
CRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet
EOF
	    ;;
    esac

    export OPENSSL_CONF="$workdir"/openssl.cnf || return $?
}

init_cert()
{
    configure_openssl || return $?

    mkdir -p "$workdir/cert" && \
    cd "$workdir/cert" || return 1

    local keyopts=
    case "$HASH_ALGO" in
        streebog256)
            keyopts="-newkey gost2012_256 -pkeyopt $GOST_PARAMSET"
            ;;
        streebog512)
            keyopts="-newkey gost2012_512 -pkeyopt $GOST_PARAMSET"
            ;;
    esac

    if [ -z "$PRIVKEY" ]; then
	[ -z "$CERT" ] || \
	    fatal 'Please, configure PRIVKEY or use the --key option to specify the corresponding private key!'
	[ -n "$update" ] || \
	    fatal 'Signing the files with a one-time key requires its public part to be installed in /etc/keys/. However, -U | --update is not specified. Abort.'

	message "Generating new certificate..."

	#shellcheck disable=SC2086,SC2015
	openssl req -x509 \
		-new $keyopts \
		-nodes -utf8 ${x509:+-days 60} -batch \
	        -config "$workdir"/openssl.cnf \
		-keyout 'priv.pem' \
		-out 'cert.pem' || \
	    fatal "Failed to generate the cert!"
	CERT="$workdir"/cert/cert.pem
	PRIVKEY="$workdir"/cert/priv.pem
    else
	message "Using private key: $PRIVKEY"
    fi

    if [ -n "$update" ]; then
	[ -n "$CERT" ] || fatal "Please, configure CERT or use the --cert option in order to update /etc/keys/!"
	message "Installing /etc/keys/${CERT_BASENAME}_ima.der..."
	openssl x509 -in "$CERT" -out /etc/keys/"${CERT_BASENAME}_ima.der" -outform DER || \
	    fatal "Failed to convert the public certificate!"
    else
	verbose "Skip installation of /etc/keys/${CERT_BASENAME}_ima.der: no -U | --update option is given."
    fi

    if [ -n "$WITH_EVM" ]; then
	if [ -n "$update" ]; then
	    find /etc/keys -type f -name '*_ima.der' | \
		while read -r key; do
		    ln -sf "${key##*/}" "${key%_ima.der}_evm.der" || exit $?
		done || return $?
	else
	    verbose "Skip installation of EVM keys: no -U | --update option is given."
	fi
    fi
}

sign_one_file()
{
    local f="$1"; shift

    [ -e "$f" ] || fatal "$f doesn't exist!"
    [ -f "$f" ] || fatal "$f is not a regular file!"

    if [ -z "$resign" ]; then
	if getfattr -n 'security.ima' "$f" 1>/dev/null 2>&1; then
	    verbose "Skip re-signing $f"
	    return 0
	fi
    fi

    message "Signing $f"

    if [ -z "$WITH_EVM" ]; then
	evmctl ${verbose:+-v} ima_sign -a "$HASH_ALGO" --key "$workdir"/cert/signwith.pem "$f" || return $?
    else
	evmctl ${verbose:+-v} sign --imasig -a "$HASH_ALGO" --key "$workdir"/cert/signwith.pem "$f" || return $?
    fi
}

sign_files()
{
    [ -r "$PRIVKEY" ] || fatal "Unable to access private key file $PRIVKEY!"

    if [ -n "$CERT" ]; then
	[ -r "$CERT" ] || \
	    fatal "Unable to access the specified public key file $CERT!"
    fi

    cat "$PRIVKEY" ${CERT:+"$CERT"} >"$workdir"/cert/signwith.pem

    if [ -z "$OPT_STDIN" ]; then
	message "Signing system files..."
    else
	message "Signing the given files..."
    fi

    (
	if [ -n "$OPT_FILE_LOG" ]; then
	    if [ "$OPT_FILE_LOG" = "-" ]; then
		exec 3>&2
	    else
		[ "${NO_TRUNCATE_LOG:-0}" -ne 0 ] || \
		    truncate -s0 "$OPT_FILE_LOG"
		exec 3>>"$OPT_FILE_LOG"
	    fi
	else
	    exec 3>&2
	fi

	while read -r file; do
            sign_one_file "$file" 1>&3 2>&1 || \
		fatal "Can't sign file $file"
	done
    )
}

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 \
			-P -- "$@"
	    return $?
	fi
    fi

    "$@"
}

find_certs()
{
    local keyring="${1:-$IMA_KEYRING}"
    local suf="${2:-_ima}"

    if ! grep -qs 'ima_appraise=enforce' /proc/cmdline; then
	message "WARNING! Kenrel is loaded with no IMA enforcement. The verification is done against all installed certificates."
	find /etc/keys -name "*$suf.der"
	return $?
    fi

    inherit_keyring \
        keyctl show "%keyring:$keyring" | sed -n -e '/asymmetric:/ s/^.*[[:space:]]\([0-9A-Fa-f]\+\)$/\1/p' | \
	while read -r skid; do
	    find /etc/keys -name "*$suf.der" | \
		while read -r cert; do
		    if openssl x509 -in "$cert" -inform DER -noout -ext subjectKeyIdentifier 2>/dev/null | tail -1 | sed -e 's/^[[:space:]]*//' -e 's/://g' | grep -qiFx "$skid"
		    then
			echo "$cert"
		    fi
		done
	done
}

verify_files()
{
    local action=

    configure_openssl || return $?

    if [ -n "$WITH_EVM" -o \
	 "$(cat /sys/kernel/security/evm 2>/dev/null)" != '0' ]
    then
	action=verify
    else
	action=ima_verify
    fi

    local keys="$(find_certs "$IMA_KEYRING" '_ima')"
    [ -n "$keys" ] || \
	fatal "No public keys found. Check the '$IMA_KEYRING' keyring and /etc/keys/."

    local ret=0
    local problem=
    while read -r f; do
	problem=
	if [ ! -e "$f" ]; then
	    problem="nonexistent"
	elif [ -d "$f" ]; then
	    problem="directory"
	elif [ ! -f "$f" -o -h "$f" ]; then
	    problem="irregular"
	elif [ ! -r "$f" ]; then
	    problem="inaccessible"
	fi

	if [ -n "$problem" ]; then
	    message "$f: N/A ($problem)"
	    ret=1
	    continue
	fi

	echo "$keys" | (
	    while read -r key; do
		if evmctl $action --key "$key" "$f" >/dev/null 2>&1; then
		    verbose "$f: OK ($key)"
		    exit 0
		fi
	    done
	    message "$f: BAD"
	    exit 1
	) || ret=$?
    done

    return $ret
}

list_all_files()
{
    local kconf=
    local lib_ext=
    # all system executable files
    #shellcheck disable=SC2086
    find -P /bin /sbin /usr/bin /usr/sbin /usr/share /etc /var/lib $EXECLIBDIRS -type f -executable -print
    # all libraries which don't carry an executable bit
    #shellcheck disable=SC2086
    find -P /var/lib $LIBDIRS -\! -executable -type f \( -name '*.so' -o -name '*.so.*' \) -print
    # additional libraries
    for lib_ext in $LIB_EXT; do
        #shellcheck disable=SC2086
        find -P /var/lib $LIBDIRS -\! -executable -type f \( -name "*.$lib_ext" -o -name "*.$lib_ext.*" \) -print
    done
    # kernel modules for kernels with IMA/EVM enabled
    find /boot/ -name 'config-*' -print | while read -r kconf; do
        grep -E -q '^CONFIG_IMA_APPRAISE=(y|m)' "$kconf" || continue
        local k="${kconf#/boot/config-}"
        [ -n "$k" ] || continue
        local p=/lib/modules/"$k"/
        [ -d "$p" ] || continue
        find "$p" -type f \( -name '*.ko' -o -name '*.ko.*' \) -print
    done
}

usage()
{
    [ "$1" = 0 ] || exec >&2
    echo "Usage: $PROG [ --stdin ] --sign [ -a HASH | --hash=HASH ] [ -E | --with-evm ] [ --without-evm ] [ --log FILE ] [ --cert=CERT.pem ] [ --key=KEY.pem ] [ -B BASENAME | --basename=BASENAME ] [ -U | --update ] [ --resign ] [ -v | --verbose ]"
    echo "       $PROG -l | --list"
    echo "       $PROG [ --stdin ] --verify [ -E | --with-evm ] [ --without-evm ] [ -v | --verbose ]"
    echo "       $PROG -h | --help"
    echo "       $PROG -V | --version"
    exit "${1:-0}"
}

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

ima_hash="$(get_ima_hash)"
if [ -n "$ima_hash" ]; then
    HASH_ALGO="$ima_hash"
fi

TEMP="$(getopt -n "$PROG" -o a:ElvhVURB: -l sign,hash:,with-evm,without-evm,log:,list,stdin,verify,verbose,help,version,cert:,key:,update,resign,basename: -- "$@")" || usage 1
eval set -- "$TEMP"

update=
resign=
while :; do
    case "$1" in
	--sign)
	    sign_mode_opts=1
	    ;;
	--log)
	    shift
	    OPT_FILE_LOG="$(realpath "$1")"
	    ;;
        -a|--hash)
	    shift; HASH_ALGO="$1"
	    sign_mode_opts=1
            ;;
        -l|--list)
	    OPT_LIST=1
	    list_mode_opts=1
            ;;
        --stdin)
	    OPT_STDIN=1
            ;;
        --verify)
	    OPT_VERIFY=1
	    vfy_mode_opts=1
            ;;
	-E|--with-evm)
	    WITH_EVM=1
            ;;
	--without-evm)
	    WITH_EVM=
            ;;
        --cert)
	    shift; CERT="$(realpath "$1")"
	    sign_mode_opts=1
            ;;
        --key)
	    shift; PRIVKEY="$(realpath "$1")"
	    sign_mode_opts=1
            ;;
        -U|--update)
	    update=yes
	    sign_mode_opts=1
            ;;
        -R|--resign)
	    resign=yes
	    sign_mode_opts=1
            ;;
	-B|--basename)
	    shift; CERT_BASENAME="$1"
	    sign_mode_opts=1
	    ;;
        -h|--help)
	    usage 0
            ;;
        -v|--verbose)
	    verbose=-v
            ;;
	-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"
	    usage 1
           ;;
    esac
    shift
done

[ $# -eq 0 ] || usage 1

if
    [ -n "${sign_mode_opts:-}" -a -n "${list_mode_opts:-}" ] || \
    [ -n "${list_mode_opts:-}" -a -n "${vfy_mode_opts:-}" ] || \
    [ -n "${sign_mode_opts:-}" -a -n "${vfy_mode_opts:-}" ]
then
    fatal "Incompatible set of options given."
fi

__process_files=sign_files

if [ -n "$OPT_STDIN" ] && [ -n "$OPT_LIST" ]; then
    fatal "--list and --stdin are mutually exclusive"
fi

if [ -n "$OPT_LIST" ]; then
    list_all_files
    exit 0
fi

if [ -n "$OPT_VERIFY" ]; then
    __process_files=verify_files
else
    mount | grep -qs '[[:blank:]]/sys/kernel/security[[:blank:]]' ||
        fatal "/sys/kernel/security is not mounted"

    init_cert || fatal "Can't initialize certificates"

    if [ "$WITH_EVM" ]; then
	init_kmk_user || fatal "Can't initialize kmk-user"
	init_evm_key || fatal "Can't initialize EVM key"
    fi
fi

if [ -z "$OPT_STDIN" ]; then
    list_all_files | $__process_files
else
    $__process_files
fi
