#!/bin/sh -eu
#
# 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) 2024 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}"

OPT_LIST=
OPT_STDIN=
OPT_VERIFY=
OPT_FILE_LOG=

verbose=

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

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

init_kmk_user()
{
    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)" >/etc/keys/kmk-user.blob
}

init_evm_key()
{
    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)" > /etc/keys/evm-key.blob
}

del_evm_key()
{
    #shellcheck disable=SC2086
    rm -f $verbose /etc/keys/evm-key.blob
}

__load_cert()
{
    local keyring="$1"; shift
    local s="${keyring#[._]}"
    local cert="/etc/keys/x509_$s.der"
    
    [ -n "$keyring" ] || return 1

    # search for keyring
    kr_id="$(keyctl search @u keyring "$keyring" 2>/dev/null)" ||:
    if [ -z "$kr_id" ]; then
        kr_id="$(keyctl newring "$keyring" @u)" || return $?
    fi

    [ -n "$kr_id" ] || \
	fatal "Unable to get keyring ID!"

    # import IMA X509 certificate
    if [ -r "$cert" ]; then
        evmctl import "$cert" "$kr_id" >/dev/null || \
	    fatal "Failed to load the cert $cert!"
    else
        fatal "Unable to read $cert!"
    fi
}

init_cert()
{
    message "Generating certificates..."
    [ -n "$workdir" ] || init_workdir || return $?

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

    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 >self_signed.conf <<EOF
[ 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
EOF

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

    case "$HASH_ALGO" in
	streebog*)
	    message "Enabling OpenSSL GOST engine..."
	    control openssl-gost enabled || \
		fatal "Failed to enable OpenSSL GOST engine"
	    ;;
    esac

    #shellcheck disable=SC2086,SC2015
    openssl req -x509 -new $keyopts -nodes -utf8 -days 60 -batch \
	        -config self_signed.conf \
		-out cert.pem -keyout priv.pem >/dev/null 2>&1 || \
	fatal "Failed to generate the cert!"

    openssl x509 -in cert.pem -out /etc/keys/x509_ima.der -outform DER || \
	fatal "Failed to convert the cert!"

    cat cert.pem priv.pem >certandkey.pem || \
	fatal "Failed to combine cert and the key!"

    case "$HASH_ALGO" in
	streebog*)
	    message "Inserting GOST kernel modules..."
	    modprobe ecrdsa_generic && \
	    modprobe streebog_generic || \
		fatal "Failed to insert GOST kernel modules!"
	    ;;
    esac

    __load_cert _ima || return $?

    if [ -n "$WITH_EVM" ]; then
	ln -sf x509_ima.der /etc/keys/x509_evm.der && \
	__load_cert _evm || return $?
    fi
}

sign_one_file()
{
    local f="$1"; shift
    [ -d "$workdir/cert" ] || return 1
    [ -f "$f" ] || return 1

    message "Signing file $f:"

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

sign_files()
{
    message "Signing system files..."

    (
	if [ -n "$OPT_FILE_LOG" ]; then
	    if [ "$OPT_FILE_LOG" = "-" ]; then
		exec 3>&2
	    else
		exec 3>"$OPT_FILE_LOG"
		truncate -s0 "$OPT_FILE_LOG"
	    fi
	else
	    exec 3>/dev/null
	fi

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

verify_files()
{
    local action=

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

    while read -r file; do
	if evmctl $action --key /etc/keys/x509_ima.der "$file" >/dev/null 2>&1; then
            verbose "$file: OK"
        else
            message "$file: BAD"
        fi
    done
}

list_all_files()
{
    local kconf=
    # 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
    # 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 ] [ -v | --verbose ]"
    echo "       $PROG [ --sign ] [ -a HASH | --hash=HASH ] [ -E | --with-evm ] [ --without-evm ] [ --log FILE ]"
    echo "       $PROG -l | --list"
    echo "       $PROG --verify"
    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:ElvhV -l sign,hash:,with-evm,without-evm,log:,list,stdin,verify,verbose,help,version -- "$@")" || usage 1
eval set -- "$TEMP"
while :; do
    case "$1" in
	--sign)
	    ;;
	--log)
	    shift; OPT_FILE_LOG="$1"
	    ;;
        -a|--hash)
	    shift; HASH_ALGO="$1"
            ;;
        -l|--list)
	    OPT_LIST=1
            ;;
        --stdin)
	    OPT_STDIN=1
            ;;
        --verify)
	    OPT_VERIFY=1
            ;;
	-E|--with-evm)
	    WITH_EVM=1
            ;;
	--without-evm)
	    WITH_EVM=
            ;;
        -h|--help)
	    usage 0
            ;;
        -v|--verbose)
	    verbose=-v
            ;;
	-V|--version)
	    cat <<EOF
$VERSION 2024
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


__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
    grep -qs 'ima_appraise=fix' /proc/cmdline  || \
        fatal "ima_appraise=fix  not found in the /proc/cmdline"

    mount | grep -qs '[[:blank:]]/sys/kernel/security[[:blank:]]' ||
        fatal "/sys/kernel/security is not mounted"

    init_cert || fatal "Can't initialize certificates"
    init_kmk_user || fatal "Can't initialize kmk-user"

    if [ "$WITH_EVM" ]; then
	init_evm_key || fatal "Can't initialize EVM key"
    else
	del_evm_key
    fi
fi

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