#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (c) 2009 IBM Corporation
# Copyright (c) 2018-2020 Petr Vorel <pvorel@suse.cz>
# Author: Mimi Zohar <zohar@linux.ibm.com>

TST_TESTFUNC="test"
TST_SETUP_CALLER="$TST_SETUP"
TST_SETUP="ima_setup"
TST_CLEANUP_CALLER="$TST_CLEANUP"
TST_CLEANUP="ima_cleanup"
TST_NEEDS_ROOT=1
TST_MOUNT_DEVICE=1

# TST_MOUNT_DEVICE can be unset, therefore specify explicitly
TST_NEEDS_TMPDIR=1

SYSFS="/sys"
UMOUNT=
TST_FS_TYPE="ext3"

# TODO: find support for rmd128 rmd256 rmd320 wp256 wp384 tgr128 tgr160
compute_digest()
{
	local algorithm="$1"
	local file="$2"
	local digest

	digest="$(${algorithm}sum $file 2>/dev/null | cut -f1 -d ' ')"
	if [ -n "$digest" ]; then
		echo "$digest"
		return 0
	fi

	digest="$(openssl $algorithm $file 2>/dev/null | cut -f2 -d ' ')"
	if [ -n "$digest" ]; then
		echo "$digest"
		return 0
	fi

	# uncommon ciphers
	local arg="$algorithm"
	case "$algorithm" in
	tgr192) arg="tiger" ;;
	wp512) arg="whirlpool" ;;
	esac

	digest="$(rdigest --$arg $file 2>/dev/null | cut -f1 -d ' ')"
	if [ -n "$digest" ]; then
		echo "$digest"
		return 0
	fi
	return 1
}

check_policy_readable()
{
	if [ ! -f $IMA_POLICY ]; then
		tst_res TINFO "missing $IMA_POLICY (reboot or CONFIG_IMA_WRITE_POLICY=y required)"
		return 1
	fi
	cat $IMA_POLICY > /dev/null 2>/dev/null
}

require_policy_readable()
{
	if [ ! -f $IMA_POLICY ]; then
		tst_brk TCONF "missing $IMA_POLICY (reboot or CONFIG_IMA_WRITE_POLICY=y required)"
	fi
	if ! check_policy_readable; then
		tst_brk TCONF "cannot read IMA policy (CONFIG_IMA_READ_POLICY=y required)"
	fi
}

require_policy_writable()
{
	local err="IMA policy already loaded and kernel not configured to enable multiple writes to it (need CONFIG_IMA_WRITE_POLICY=y)"

	[ -f $IMA_POLICY ] || tst_brk TCONF "$err"
	# CONFIG_IMA_READ_POLICY
	echo "" 2> log > $IMA_POLICY
	grep -q "Device or resource busy" log && tst_brk TCONF "$err"
}

check_ima_policy_content()
{
	local pattern="$1"
	local grep_params="${2--q}"

	check_policy_readable || return 1
	grep $grep_params "$pattern" $IMA_POLICY
}

require_ima_policy_content()
{
	local pattern="$1"
	local grep_params="${2--q}"

	require_policy_readable
	if ! grep $grep_params "$pattern" $IMA_POLICY; then
		tst_brk TCONF "IMA policy does not specify '$pattern'"
	fi
}

check_ima_policy_cmdline()
{
	local policy="$1"
	local i

	grep -q "ima_$policy" /proc/cmdline && return
	for i in $(cat /proc/cmdline); do
		if echo "$i" | grep -q '^ima_policy='; then
			echo "$i" | grep -q -e "|[ ]*$policy" -e "$policy[ ]*|" -e "=$policy" && return 0
		fi
	done
	return 1
}

require_ima_policy_cmdline()
{
	local policy="$1"

	check_ima_policy_cmdline $policy || \
		tst_brk TCONF "IMA measurement tests require builtin IMA $policy policy (e.g. ima_policy=$policy kernel parameter)"
}

mount_helper()
{
	local type="$1"
	local default_dir="$2"
	local dir

	dir="$(grep ^$type /proc/mounts | cut -d ' ' -f2 | head -1)"
	[ -n "$dir" ] && { echo "$dir"; return; }

	if ! mkdir -p $default_dir; then
		tst_brk TBROK "failed to create $default_dir"
	fi
	if ! mount -t $type $type $default_dir; then
		tst_brk TBROK "failed to mount $type"
	fi
	UMOUNT="$default_dir $UMOUNT"
	echo $default_dir
}

print_ima_config()
{
	local config="${KCONFIG_PATH:-/boot/config-$(uname -r)}"
	local i

	if [ -r "$config" ]; then
		tst_res TINFO "IMA kernel config:"
		for i in $(grep ^CONFIG_IMA $config); do
			tst_res TINFO "$i"
		done
	fi

	tst_res TINFO "/proc/cmdline: $(cat /proc/cmdline)"
}

ima_setup()
{
	SECURITYFS="$(mount_helper securityfs $SYSFS/kernel/security)"

	IMA_DIR="$SECURITYFS/ima"
	[ -d "$IMA_DIR" ] || tst_brk TCONF "IMA not enabled in kernel"
	ASCII_MEASUREMENTS="$IMA_DIR/ascii_runtime_measurements"
	BINARY_MEASUREMENTS="$IMA_DIR/binary_runtime_measurements"
	IMA_POLICY="$IMA_DIR/policy"

	# hack to support running tests locally from ima/tests directory
	if [ ! -d "$TST_DATAROOT" ]; then
		TST_DATAROOT="$LTPROOT/../datafiles/$TST_ID/"
	fi

	print_ima_config

	if [ "$TST_MOUNT_DEVICE" = 1 ]; then
		tst_res TINFO "\$TMPDIR is on tmpfs => run on loop device"
		cd "$TST_MNTPOINT"
	fi

	[ -n "$TST_SETUP_CALLER" ] && $TST_SETUP_CALLER
}

ima_cleanup()
{
	local dir

	[ -n "$TST_CLEANUP_CALLER" ] && $TST_CLEANUP_CALLER

	for dir in $UMOUNT; do
		umount $dir
	done
}

set_digest_index()
{
	DIGEST_INDEX=

	local template="$(tail -1 $ASCII_MEASUREMENTS | cut -d' ' -f 3)"
	local i word

	# parse digest index
	# https://www.kernel.org/doc/html/latest/security/IMA-templates.html#use
	case "$template" in
	ima|ima-ng|ima-sig) DIGEST_INDEX=4 ;;
	*)
		# using ima_template_fmt kernel parameter
		local IFS="|"
		i=4
		for word in $template; do
			if [ "$word" = 'd' -o "$word" = 'd-ng' ]; then
				DIGEST_INDEX=$i
				break
			fi
			i=$((i+1))
		done
	esac

	[ -z "$DIGEST_INDEX" ] && tst_brk TCONF \
		"Cannot find digest index (template: '$template')"
}

get_algorithm_digest()
{
	local line="$1"
	local delimiter=':'
	local algorithm digest

	if [ -z "$line" ]; then
		echo "measurement record not found"
		return 1
	fi

	[ -z "$DIGEST_INDEX" ] && set_digest_index
	digest=$(echo "$line" | cut -d' ' -f $DIGEST_INDEX)
	if [ -z "$digest" ]; then
		echo "digest not found (index: $DIGEST_INDEX, line: '$line')"
		return 1
	fi

	if [ "${digest#*$delimiter}" != "$digest" ]; then
		algorithm=$(echo "$digest" | cut -d $delimiter -f 1)
		digest=$(echo "$digest" | cut -d $delimiter -f 2)
	else
		case "${#digest}" in
		32) algorithm="md5" ;;
		40) algorithm="sha1" ;;
		*)
			echo "algorithm must be either md5 or sha1 (digest: '$digest')"
			return 1 ;;
		esac
	fi
	if [ -z "$algorithm" ]; then
		echo "algorithm not found"
		return 1
	fi
	if [ -z "$digest" ]; then
		echo "digest not found"
		return 1
	fi

	echo "$algorithm|$digest"
}

ima_check()
{
	local test_file="$1"
	local algorithm digest expected_digest line tmp

	# need to read file to get updated $ASCII_MEASUREMENTS
	cat $test_file > /dev/null

	line="$(grep $test_file $ASCII_MEASUREMENTS | tail -1)"

	if tmp=$(get_algorithm_digest "$line"); then
		algorithm=$(echo "$tmp" | cut -d'|' -f1)
		digest=$(echo "$tmp" | cut -d'|' -f2)
	else
		tst_res TBROK "failed to get algorithm/digest for '$test_file': $tmp"
	fi

	tst_res TINFO "computing digest for $algorithm algorithm"
	expected_digest="$(compute_digest $algorithm $test_file)" || \
		tst_brk TCONF "cannot compute digest for $algorithm algorithm"

	if [ "$digest" = "$expected_digest" ]; then
		tst_res TPASS "correct digest found"
	else
		tst_res TFAIL "digest not found"
	fi
}

# check_evmctl REQUIRED_TPM_VERSION
# return: 0: evmctl is new enough, 1: version older than required (or version < v0.9)
check_evmctl()
{
	local required="$1"

	local r1="$(echo $required | cut -d. -f1)"
	local r2="$(echo $required | cut -d. -f2)"
	local r3="$(echo $required | cut -d. -f3)"
	[ -z "$r3" ] && r3=0

	tst_is_int "$r1" || tst_brk TBROK "required major version not int ($v1)"
	tst_is_int "$r2" || tst_brk TBROK "required minor version not int ($v2)"
	tst_is_int "$r3" || tst_brk TBROK "required patch version not int ($v3)"

	tst_check_cmds evmctl || return 1

	local v="$(evmctl --version | cut -d' ' -f2)"
	[ -z "$v" ] && return 1
	tst_res TINFO "evmctl version: $v"

	local v1="$(echo $v | cut -d. -f1)"
	local v2="$(echo $v | cut -d. -f2)"
	local v3="$(echo $v | cut -d. -f3)"
	[ -z "$v3" ] && v3=0

	if [ $v1 -lt $r1 ] || [ $v1 -eq $r1 -a $v2 -lt $r2 ] || \
		[ $v1 -eq $r1 -a $v2 -eq $r2 -a $v3 -lt $r3 ]; then
		return 1
	fi
	return 0
}

# require_evmctl REQUIRED_TPM_VERSION
require_evmctl()
{
	local required="$1"

	if ! check_evmctl $required; then
		tst_brk TCONF "evmctl >= $required required"
	fi
}

. tst_test.sh

# loop device is needed to use only for tmpfs
TMPDIR="${TMPDIR:-/tmp}"
if tst_supported_fs -d $TMPDIR -s "tmpfs"; then
	unset TST_MOUNT_DEVICE
fi

. tst_test.sh
