#!/bin/sh -eu
#
# Copyright (C) 2018 Mikhail Efremov <sem@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

HASH_ALGO=sha1
OPT_NOIMMUTABLE=
OPT_LIST=
OPT_STDIN=
OPT_NOINITRD=
OPT_VERIFY=

CERT_TMP_DIR=
CHATTR_TMP_DIR=
verbose=

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

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

init_kmk_user()
{
	local id="$(keyctl search @u user kmk-user 2>/dev/null)"
	verbose "Creating kmk-user key..."
	[ -z "$id" ] || keyctl unlink "$id" @u >/dev/null
	keyctl add user kmk-user "$(dd if=/dev/urandom bs=1 count=32 2>/dev/null)" @u >/dev/null
	keyctl pipe "$(keyctl search @u user kmk-user)" >/etc/keys/kmk-user.blob
}

init_evm_key()
{
	local id="$(keyctl search @u encrypted evm-key 2>/dev/null)"
	verbose "Creating evm-key..."
	[ -z "$id" ] || keyctl unlink "$id" @u >/dev/null
	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
}

__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)"
	fi

	# import IMA X509 certificate
	if [ -r "$cert" ]; then
	    evmctl import "$cert" "$kr_id" >/dev/null
	fi
}

__chattr()
{
	LD_LIBRARY_PATH="$CHATTR_TMP_DIR" "$CHATTR_TMP_DIR"/chattr "$@"
}

load_ima_cert()
{
	__load_cert _ima
}

load_evm_cert()
{
	__load_cert _evm
}

init_cert()
{
	verbose "Creating certificates..."
	CERT_TMP_DIR="$(mktemp --tmpdir -d certdir-XXXXXXXX)"
	[ -d "$CERT_TMP_DIR" ] || return 1

	cd "$CERT_TMP_DIR" || return 1

	cat >self_signed.conf <<EOF
[ req ]
default_bits = 2048
default_md = $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

	openssl req -x509 -new -nodes -utf8 -days 60 -batch -config self_signed.conf \
		-outform DER -out cert.der -keyout priv.pem >/dev/null 2>&1 || return 1
	mv -f cert.der /etc/keys/x509_evm.der || return 1
	ln -sf x509_evm.der /etc/keys/x509_ima.der || return 1

	load_ima_cert || return 1
	load_evm_cert || return 1
}

sign_one_file()
{
	local f="$1"; shift
	[ -d "$CERT_TMP_DIR" ] || return 1
	[ -f "$f" ] || return 1
	__chattr -i "$f"
	evmctl sign --imasig -a "$HASH_ALGO" -k "$CERT_TMP_DIR/priv.pem" "$f"
	__chattr +i "$f"
}

sign_files()
{
	# Prepare our own copy of chattr and its libraries
	local chattr_exe=
	[ -d "$CHATTR_TMP_DIR" ] || return 1
	chattr_exe="$(which chattr)"
	if [ -z "$chattr_exe" ] || [ ! -x "$chattr_exe" ]; then
		return 1
	fi
	ldd "$chattr_exe" | \
		sed -n -r 's;^.+[[:blank:]]=>[[:blank:]]+(/[^\(]+)[[:blank:]]+\(0x[[:xdigit:]]+\)$;\1;p' | \
		while read ch_lib; do
			cp "$ch_lib" "$CHATTR_TMP_DIR"/
		done

	cp "$chattr_exe" "$CHATTR_TMP_DIR"/

	verbose "Signing files..."
	while read file; do
		sign_one_file "$file" || message "Can't sign file $file"
	done
}

no_immutable_files()
{
	while read file; do
		chattr -i "$file"
	done
}

verify_files()
{
	while read file; do
		if evmctl verify "$file" >/dev/null 2>&1; then
			verbose "$file: OK"
		else
			message "$file: BAD"
		fi
	done
}

list_all_files()
{
	local kconf=
	# all system executable files
	find -P /bin /sbin /usr/bin /usr/sbin /usr/share /etc /var/lib /usr/libexec /lib /usr/lib -type f -executable -print
	# all libraries which don't carry an executable bit
	find -P /var/lib /lib /usr/lib -\! -executable -type f \( -name '*.so' -o -name '*.so.*' \) -print
	# kernel modules for kernels with IMA/EVM enabled
	find /boot/ -name 'config-*' -print | while read kconf; do
		egrep -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 ] [ --verbose ] [ --verify | --hashalgo=algo ] [ --noinitrd ]"
	echo "       $PROG [ --stdin ] [ -i | --noimmutable ]"
	echo "       $PROG -l | --list"
	echo "       $PROG -h | --help"
	[ -n "$1" ] && exit "$1" || exit
}

TEMP="$(getopt -n "$PROG" -o a:ilvh -l hashalgo:,noimmutable,list,stdin,noinitrd,verify,verbose,help -- "$@")" || usage 1
eval set -- "$TEMP"
while :; do
	case "$1" in
		-a|--hashalgo) shift; HASH_ALGO="$1"; shift
			;;
		-i|--noimmutable) shift; OPT_NOIMMUTABLE=1
			;;
		-l|--list) shift; OPT_LIST=1
			;;
		--stdin) shift; OPT_STDIN=1
			;;
		--noinitrd) shift; OPT_NOINITRD=1
			;;
		--verify) shift; OPT_VERIFY=1
			;;
		-h|--help) usage 0
			;;
		-v|--verbose) shift; verbose=1
			;;
		--) shift; break
			;;
		*) echo "$PROG: unrecognized option: $1" >&2; usage 1
			;;
	esac
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
elif [ -n "$OPT_NOIMMUTABLE" ]; then
	__process_files=no_immutable_files
else
	grep -qs 'ima_appraise=fix' /proc/cmdline && grep -qs 'evm=fix' /proc/cmdline || \
		fatal "ima_appraise=fix and evm=fix not found in the /proc/cmdline"

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

	CHATTR_TMP_DIR="$(mktemp --tmpdir -d chattr-tmp-XXXXXXXX)"

	trap _cleanup_handler EXIT HUP PIPE INT QUIT TERM
	init_cert || fatal "Can't initialize certificates"
	init_kmk_user || fatal "Can't kmk-user initialize "
	init_evm_key || fatal "Can't initialize EVM key"
fi

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

if [ "$__process_files" = sign_files ] && [ -z "$OPT_NOINITRD" ]; then
	verbose "Creating new initrd..."
	_OUTPUT=
	[ -n "$verbose" ] || _OUTPUT='>/dev/null'

	eval make-initrd $_OUTPUT
fi
