mirror of
https://git.code.sf.net/p/linux-ima/ima-evm-utils
synced 2025-04-28 14:43:37 +02:00

If an error occurs before any test is executed, _report_exit_and_cleanup() returns 77 ($SKIP) as exit code, which might not reflect the real exit code at the time the script terminated its execution. If the function registered in the shell trap() is a cleanup function calling _report_exit_and_cleanup() inside, the latter will not have access to the exit code at the time of the trap but instead to the exit code of the cleanup function. To solve this issue, pass the cleanup function and its arguments to _report_exit_and_cleanup(), so that the latter can first get the script exit code and then can execute the cleanup function. Finally, if no test was executed, return the exit code at the time of the trap() instead of 77. Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com> Reviewed-by: Stefan Berger <stefanb@linux.ibm.com> Signed-off-by: Mimi Zohar <zohar@linux.ibm.com>
377 lines
9.8 KiB
Bash
Executable File
377 lines
9.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
#
|
|
# Test IMA support for including fs-verity enabled files measurements
|
|
# in the IMA measurement list.
|
|
#
|
|
# Define policy rules showing the different types of IMA and fs-verity
|
|
# records in the IMA measurement list. Include examples of files that
|
|
# are suppose to be fs-verity enabled, but aren't.
|
|
#
|
|
# test 1: IMA policy rule using the new ima-ngv2 template
|
|
# - Hash prefixed with "ima:"
|
|
#
|
|
# test 2: fs-verity IMA policy rule using the new ima-ngv2 template
|
|
# - fs-verity hash prefixed with "verity:"
|
|
# - Non fs-verity enabled file, zeros prefixed with "verity:"
|
|
#
|
|
# test 3: IMA policy rule using the new ima-sigv2 template
|
|
# - Hash prefixed with "ima:"
|
|
# - Appended signature, when available.
|
|
#
|
|
# test 4: fs-verity IMA policy rule using the new ima-sigv2 template
|
|
# - fs-verity hash prefixed with "verity:"
|
|
# - Non fs-verity enabled file, zeros prefixed with "verity:"
|
|
# - Appended IMA signature of fs-verity file hash, when available.
|
|
|
|
# To avoid affecting the system's IMA custom policy or requiring a
|
|
# reboot between tests, define policy rules based on UUID. However,
|
|
# since the policy rules are walked sequentially, the system's IMA
|
|
# custom policy rules might take precedence.
|
|
|
|
cd "$(dirname "$0")" || exit 1
|
|
PATH=../src:$PATH
|
|
source ./functions.sh
|
|
|
|
# Base VERBOSE on the environment variable, if set.
|
|
VERBOSE="${VERBOSE:-0}"
|
|
|
|
IMA_POLICY_FILE="/sys/kernel/security/integrity/ima/policy"
|
|
IMA_MEASUREMENT_LIST="/sys/kernel/security/integrity/ima/ascii_runtime_measurements"
|
|
TST_MNT="/tmp/fsverity-test"
|
|
TST_IMG="/tmp/test.img"
|
|
|
|
LOOPBACK_MOUNTED=0
|
|
FSVERITY="$(which fsverity)"
|
|
|
|
_require dd mkfs blkid e2fsck tune2fs evmctl setfattr
|
|
./gen-keys.sh >/dev/null 2>&1
|
|
|
|
trap '_report_exit_and_cleanup cleanup' SIGINT SIGTERM EXIT
|
|
|
|
cleanup() {
|
|
if [ -e $TST_MNT ]; then
|
|
if [ $LOOPBACK_MOUNTED -eq 1 ]; then
|
|
umount $TST_MNT
|
|
fi
|
|
if [ -f "$TST_IMG" ]; then
|
|
rm "$TST_IMG"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Loopback mount a file
|
|
mount_loopback_file() {
|
|
local ret
|
|
|
|
if [ ! -d $TST_MNT ]; then
|
|
mkdir $TST_MNT
|
|
fi
|
|
|
|
# if modprobe loop; then
|
|
# echo "${CYAN}INFO: modprobe loop failed${NORM}"
|
|
# fi
|
|
|
|
if ! losetup -f &> /dev/null; then
|
|
echo "${RED}FAILURE: losetup${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
|
|
mount -v -o loop ${TST_IMG} $TST_MNT
|
|
ret=$?
|
|
|
|
if [ "${ret}" -eq 0 ]; then
|
|
LOOPBACK_MOUNTED=1
|
|
fi
|
|
|
|
return "$ret"
|
|
}
|
|
|
|
# Change the loopback mounted filesystem's UUID in between tests
|
|
change_loopback_file_uuid() {
|
|
echo " "
|
|
[ "$VERBOSE" -ge 1 ] && echo "INFO: Changing loopback file uuid"
|
|
|
|
umount $TST_MNT
|
|
if ! e2fsck -y -f ${TST_IMG} &> /dev/null; then
|
|
echo "${RED}FAILURE: e2fsck${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
|
|
if ! tune2fs -f ${TST_IMG} -U random &> /dev/null; then
|
|
echo "${RED}FAILURE: change UUID${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
|
|
[ "$VERBOSE" -ge 1 ] && echo "INFO: Remounting loopback filesystem"
|
|
if ! mount_loopback_file; then
|
|
echo "${RED}FAILURE: re-mounting loopback filesystem${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Create a file to be loopback mounted
|
|
create_loopback_file() {
|
|
local fs_type=$1
|
|
local options=""
|
|
|
|
echo "INFO: Creating loopback filesystem"
|
|
case $fs_type in
|
|
ext4|f2fs)
|
|
options="-O verity"
|
|
;;
|
|
btrfs)
|
|
;;
|
|
*)
|
|
echo "${RED}FAILURE: unsupported fs-verity filesystem${NORM}"
|
|
exit "${FAIL}"
|
|
;;
|
|
esac
|
|
|
|
[ "$VERBOSE" -ge 2 ] && echo "INFO: Creating a file to be loopback mounted with options: $options"
|
|
if ! dd if=/dev/zero of="${TST_IMG}" bs=100M count=6 &> /dev/null; then
|
|
echo "${RED}FAILURE: creating ${TST_IMG}${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
|
|
echo "INFO: Building an $fs_type filesystem"
|
|
if ! mkfs -t "$fs_type" -q "${TST_IMG}" "$options"; then
|
|
echo "${RED}FAILURE: Creating $fs_type filesystem${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
|
|
echo "INFO: Mounting loopback filesystem"
|
|
if ! mount_loopback_file; then
|
|
echo "${RED}FAILURE: mounting loopback filesystem${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
get_current_uuid() {
|
|
[ "$VERBOSE" -ge 2 ] && echo "INFO: Getting loopback file uuid"
|
|
if ! UUID=$(blkid -s UUID -o value ${TST_IMG}); then
|
|
echo "${RED}FAILURE: to get UUID${NORM}"
|
|
return "$FAIL"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
unqualified_bprm_rule() {
|
|
local test=$1
|
|
local rule=$2
|
|
local rule_match="measure func=BPRM_CHECK"
|
|
local rule_dontmatch="fsuuid"
|
|
|
|
if [ -z "${rule##*$digest_type=verity*}" ]; then
|
|
if grep "$rule_match" $IMA_POLICY_FILE | grep -v "$rule_dontmatch" &> /dev/null; then
|
|
return "$SKIP"
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
load_policy_rule() {
|
|
local test=$1
|
|
local rule=$2
|
|
|
|
if ! get_current_uuid; then
|
|
echo "${RED}FAILURE:FAILED getting uuid${NORM}"
|
|
exit "$FAIL"
|
|
fi
|
|
|
|
unqualified_bprm_rule "${test}" "${rule}"
|
|
if [ $? -eq "${SKIP}" ]; then
|
|
echo "${CYAN}SKIP: fsuuid unqualified \"BPRM_CHECK\" rule exists${NORM}"
|
|
return "$SKIP"
|
|
fi
|
|
|
|
echo "$test: rule: $rule fsuuid=$UUID"
|
|
if ! echo "$rule fsuuid=$UUID" > $IMA_POLICY_FILE; then
|
|
echo "${CYAN}SKIP: Loading policy rule failed, skipping test${NORM}"
|
|
return "$SKIP"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
create_file() {
|
|
local test=$1
|
|
local type=$2
|
|
|
|
TST_FILE=$(mktemp -p $TST_MNT -t "${type}".XXXXXX)
|
|
[ "$VERBOSE" -ge 1 ] && echo "INFO: creating $TST_FILE"
|
|
|
|
# heredoc to create a script
|
|
cat <<-EOF > "$TST_FILE"
|
|
#!/bin/bash
|
|
echo "Hello" &> /dev/null
|
|
EOF
|
|
|
|
chmod a+x "$TST_FILE"
|
|
}
|
|
|
|
measure-verity() {
|
|
local test=$1
|
|
local verity="${2:-disabled}"
|
|
local digest_filename
|
|
local error="$OK"
|
|
local KEY=$PWD/test-rsa2048.key
|
|
|
|
create_file "$test" verity-hash
|
|
if [ "$verity" = "enabled" ]; then
|
|
msg="Measuring fs-verity enabled file $TST_FILE"
|
|
if ! "$FSVERITY" enable "$TST_FILE" &> /dev/null; then
|
|
echo "${CYAN}SKIP: Failed enabling fs-verity on $TST_FILE${NORM}"
|
|
return "$SKIP"
|
|
fi
|
|
else
|
|
msg="Measuring non fs-verity enabled file $TST_FILE"
|
|
fi
|
|
|
|
# Sign the fsverity digest and write it as security.ima xattr.
|
|
# "evmctl sign_hash" input: <digest> <filename>
|
|
# "evmctl sign_hash" output: <digest> <filename> <signature>
|
|
[ "$VERBOSE" -ge 2 ] && echo "INFO: Signing the fsverity digest"
|
|
xattr=$("$FSVERITY" digest "$TST_FILE" | evmctl sign_hash --veritysig --key "$KEY" 2> /dev/null)
|
|
sig=$(echo "$xattr" | cut -d' ' -f3)
|
|
|
|
# On failure to write security.ima xattr, the signature will simply
|
|
# not be appended to the measurement list record.
|
|
if ! setfattr -n security.ima -v "0x$sig" "$TST_FILE"; then
|
|
echo "${CYAN}INFO: Failed to write security.ima xattr${NORM}"
|
|
fi
|
|
"$TST_FILE"
|
|
|
|
# "fsverity digest" calculates the fsverity hash, even for
|
|
# non fs-verity enabled files.
|
|
digest_filename=$("$FSVERITY" digest "$TST_FILE")
|
|
[ "$VERBOSE" -ge 2 ] && echo "INFO: verity:$digest_filename"
|
|
|
|
grep "verity:$digest_filename" $IMA_MEASUREMENT_LIST &> /dev/null
|
|
ret=$?
|
|
|
|
# Not finding the "fsverity digest" result in the IMA measurement
|
|
# list is expected for non fs-verity enabled files. The measurement
|
|
# list will contain zeros for the file hash.
|
|
if [ $ret -eq 1 ]; then
|
|
error="$FAIL"
|
|
if [ "$verity" = "enabled" ]; then
|
|
echo "${RED}FAILURE: ${msg} ${NORM}"
|
|
else
|
|
echo "${GREEN}SUCCESS: ${msg}, fsverity digest not found${NORM}"
|
|
fi
|
|
else
|
|
if [ "$verity" = "enabled" ]; then
|
|
echo "${GREEN}SUCCESS: ${msg} ${NORM}"
|
|
else
|
|
error="$FAIL"
|
|
echo "${RED}FAILURE: ${msg} ${NORM}"
|
|
fi
|
|
fi
|
|
return "$error"
|
|
}
|
|
|
|
measure-ima() {
|
|
local test=$1
|
|
local digest_filename
|
|
local error="$OK"
|
|
local hashalg
|
|
local digestsum
|
|
|
|
create_file "$test" ima-hash
|
|
"$TST_FILE"
|
|
|
|
hashalg=$(grep "${TST_FILE}" $IMA_MEASUREMENT_LIST | cut -d':' -f2)
|
|
if [ -z "${hashalg}" ]; then
|
|
echo "${CYAN}SKIP: Measurement record with algorithm not found${NORM}"
|
|
return "$SKIP"
|
|
fi
|
|
|
|
digestsum=$(which "${hashalg}"sum)
|
|
if [ -z "${digestsum}" ]; then
|
|
echo "${CYAN}SKIP: ${hashalg}sum is not installed${NORM}"
|
|
return "$SKIP"
|
|
fi
|
|
|
|
# sha1sum,sha256sum return: <digest> <2 spaces> <filename>
|
|
# Remove the extra space before the filename
|
|
digest_filename=$(${digestsum} "$TST_FILE" | sed "s/\ \ /\ /")
|
|
[ "$VERBOSE" -ge 2 ] && echo "$test: $digest_filename"
|
|
if grep "$digest_filename" $IMA_MEASUREMENT_LIST &> /dev/null; then
|
|
echo "${GREEN}SUCCESS: Measuring $TST_FILE ${NORM}"
|
|
else
|
|
error="$FAIL"
|
|
echo "${RED}FAILURE: Measuring $TST_FILE ${NORM}"
|
|
fi
|
|
|
|
return "$error"
|
|
}
|
|
|
|
# Dependency on being able to read and write the IMA policy file.
|
|
# Requires both CONFIG_IMA_WRITE_POLICY, CONFIG_IMA_READ_POLICY be
|
|
# enabled.
|
|
if [ -e "$IMA_POLICY_FILE" ]; then
|
|
mode=$(stat -c "%a" $IMA_POLICY_FILE)
|
|
if [ "$mode" != "600" ]; then
|
|
echo "${CYAN}SKIP: IMA policy file must be read-write${NORM}"
|
|
exit "$SKIP"
|
|
fi
|
|
else
|
|
echo "${CYAN}SKIP: $IMA_POLICY_FILE does not exist${NORM}"
|
|
exit "$SKIP"
|
|
fi
|
|
|
|
# Skip the test if fsverity is not found; using _require fails the test.
|
|
if [ -z "$FSVERITY" ]; then
|
|
echo "${CYAN}SKIP: fsverity is not installed${NORM}"
|
|
exit "$SKIP"
|
|
fi
|
|
|
|
if [ "x$(id -u)" != "x0" ]; then
|
|
echo "${CYAN}SKIP: Must be root to execute this test${NORM}"
|
|
exit "$SKIP"
|
|
fi
|
|
|
|
create_loopback_file ext4
|
|
|
|
# Commit 989dc72511f7 ("ima: define a new template field named 'd-ngv2' and
|
|
# templates") introduced ima-ngv2 and ima-sigv2 in linux-5.19.
|
|
__skip() { return "$SKIP"; }
|
|
|
|
# IMA policy rule using the ima-ngv2 template
|
|
if load_policy_rule test1 "measure func=BPRM_CHECK template=ima-ngv2"; then
|
|
expect_pass measure-ima test1
|
|
else
|
|
expect_pass __skip
|
|
fi
|
|
|
|
# fsverity IMA policy rule using the ima-ngv2 template
|
|
change_loopback_file_uuid
|
|
if load_policy_rule test2 "measure func=BPRM_CHECK template=ima-ngv2 digest_type=verity"; then
|
|
expect_fail measure-verity test2
|
|
expect_pass measure-verity test2 enabled
|
|
else
|
|
expect_pass __skip
|
|
expect_pass __skip
|
|
fi
|
|
|
|
# IMA policy rule using the ima-sigv2 template
|
|
change_loopback_file_uuid
|
|
if load_policy_rule test3 "measure func=BPRM_CHECK template=ima-sigv2"; then
|
|
expect_pass measure-ima test3
|
|
else
|
|
expect_pass __skip
|
|
fi
|
|
|
|
# fsverity IMA policy rule using the ima-sigv2 template
|
|
change_loopback_file_uuid
|
|
if load_policy_rule test4 "measure func=BPRM_CHECK template=ima-sigv2 digest_type=verity"; then
|
|
expect_fail measure-verity test4
|
|
expect_pass measure-verity test4 enabled
|
|
else
|
|
expect_pass __skip
|
|
expect_pass __skip
|
|
fi
|
|
exit
|