diff --git a/build.sh b/build.sh index 0c2fdd9..cc5a258 100755 --- a/build.sh +++ b/build.sh @@ -98,6 +98,12 @@ if [ $ret -eq 0 ]; then grep "skipped" tests/sign_verify.log | wc -l fi tail -20 tests/boot_aggregate.log + + if [ -f tests/fsverity.log ]; then + tail -4 tests/fsverity.log + grep "skipped" tests/fsverity.log && \ + grep "skipped" tests/fsverity.log | wc -l + fi exit 0 fi diff --git a/ci/alpine.sh b/ci/alpine.sh index 0e4ba0d..6b17942 100755 --- a/ci/alpine.sh +++ b/ci/alpine.sh @@ -30,6 +30,7 @@ apk add \ diffutils \ docbook-xml \ docbook-xsl \ + e2fsprogs-extra \ keyutils-dev \ libtool \ libxslt \ @@ -41,6 +42,7 @@ apk add \ pkgconfig \ procps \ sudo \ + util-linux \ wget \ which \ xxd diff --git a/ci/alt.sh b/ci/alt.sh index 65389be..36ff657 100755 --- a/ci/alt.sh +++ b/ci/alt.sh @@ -11,7 +11,8 @@ apt-get install -y \ $TSS \ asciidoc \ attr \ - docbook-style-xsl \ + e2fsprogs \ + fsverity-utils-devel \ gnutls-utils \ libattr-devel \ libkeyutils-devel \ @@ -21,6 +22,7 @@ apt-get install -y \ openssl-gost-engine \ rpm-build \ softhsm \ + util-linux \ wget \ xsltproc \ xxd \ diff --git a/ci/debian.sh b/ci/debian.sh index 005b1f6..431203a 100755 --- a/ci/debian.sh +++ b/ci/debian.sh @@ -40,6 +40,7 @@ $apt \ debianutils \ docbook-xml \ docbook-xsl \ + e2fsprogs \ gzip \ libattr1-dev$ARCH \ libkeyutils-dev$ARCH \ @@ -50,6 +51,7 @@ $apt \ pkg-config \ procps \ sudo \ + util-linux \ wget \ xsltproc diff --git a/ci/fedora.sh b/ci/fedora.sh index 0993607..2272bbc 100755 --- a/ci/fedora.sh +++ b/ci/fedora.sh @@ -25,9 +25,12 @@ yum -y install \ automake \ diffutils \ docbook-xsl \ + e2fsprogs \ + git-core \ gnutls-utils \ gzip \ keyutils-libs-devel \ + kmod \ libattr-devel \ libtool \ libxslt \ @@ -38,6 +41,7 @@ yum -y install \ pkg-config \ procps \ sudo \ + util-linux \ vim-common \ wget \ which @@ -49,4 +53,6 @@ yum -y install swtpm || true if [ -f /etc/centos-release ]; then yum -y install epel-release fi -yum -y install softhsm || true \ No newline at end of file +yum -y install softhsm || true + +./tests/install-fsverity.sh diff --git a/ci/tumbleweed.sh b/ci/tumbleweed.sh index 4e3da0c..6f70b0f 100755 --- a/ci/tumbleweed.sh +++ b/ci/tumbleweed.sh @@ -26,6 +26,7 @@ zypper --non-interactive install --force-resolution --no-recommends \ diffutils \ docbook_5 \ docbook5-xsl-stylesheets \ + e2fsprogs \ gzip \ ibmswtpm2 \ keyutils-devel \ @@ -37,6 +38,7 @@ zypper --non-interactive install --force-resolution --no-recommends \ pkg-config \ procps \ sudo \ + util-linux \ vim \ wget \ which \ diff --git a/tests/Makefile.am b/tests/Makefile.am index ff928e1..3050824 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,7 +1,8 @@ check_SCRIPTS = TESTS = $(check_SCRIPTS) -check_SCRIPTS += ima_hash.test sign_verify.test boot_aggregate.test +check_SCRIPTS += ima_hash.test sign_verify.test boot_aggregate.test \ + fsverity.test clean-local: -rm -f *.txt *.out *.sig *.sig2 diff --git a/tests/fsverity.test b/tests/fsverity.test new file mode 100755 index 0000000..20e09fa --- /dev/null +++ b/tests/fsverity.test @@ -0,0 +1,377 @@ +#!/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 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 + _report_exit_and_cleanup +} + +# 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 -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: + # "evmctl sign_hash" output: + [ "$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: <2 spaces> + # 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 diff --git a/tests/install-fsverity.sh b/tests/install-fsverity.sh new file mode 100755 index 0000000..418fc42 --- /dev/null +++ b/tests/install-fsverity.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +git clone https://git.kernel.org/pub/scm/linux/kernel/git/ebiggers/fsverity-utils.git +cd fsverity-utils +CC=gcc make -j$(nproc) && sudo make install +cd .. +rm -rf fsverity-utils