diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2afdfe..8b1eda8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,79 @@ name: "distros" on: [push, pull_request] jobs: + build: + runs-on: ubuntu-latest + outputs: + LINUX_SHA: ${{ steps.last-commit.outputs.LINUX_SHA }} + name: build + timeout-minutes: 100 + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v3 + + - name: Determine last kernel commit + id: last-commit + shell: bash + run: | + mkdir linux-integrity + pushd linux-integrity + git init + LINUX_URL=${{ vars.LINUX_URL }} + if [ -z "$LINUX_URL" ]; then + LINUX_URL=https://git.kernel.org/pub/scm/linux/kernel/git/zohar/linux-integrity.git + fi + LINUX_BRANCH=${{ vars.LINUX_BRANCH }} + if [ -z "$LINUX_BRANCH" ]; then + LINUX_BRANCH=next-integrity + fi + git remote add origin $LINUX_URL + LINUX_SHA=$(git ls-remote origin $GITHUB_REF_NAME | awk '{print $1}') + [ -z "$LINUX_SHA" ] && LINUX_SHA=$(git ls-remote origin $LINUX_BRANCH | awk '{print $1}') + echo "LINUX_SHA=$LINUX_SHA" >> $GITHUB_OUTPUT + popd + + - name: Cache UML kernel + id: cache-linux + uses: actions/cache@v3 + with: + path: linux + key: linux-${{ steps.last-commit.outputs.LINUX_SHA }}-${{ hashFiles('**/kernel-configs/*') }} + + - name: Cache signing key + id: cache-key + uses: actions/cache@v3 + with: + path: signing_key.pem + key: signing_key.pem-${{ steps.last-commit.outputs.LINUX_SHA }}-${{ hashFiles('**/kernel-configs/*') }} + + - name: Compile UML kernel + if: steps.cache-linux.outputs.cache-hit != 'true' || steps.cache-key.outputs.cache-hit != 'true' + shell: bash + run: | + if [ "$DEVTOOLSET" = "yes" ]; then + source /opt/rh/devtoolset-10/enable + fi + if [ "$ARCH" = "i386" ]; then + CROSS_COMPILE_OPT="CROSS_COMPILE=i686-linux-gnu-" + fi + pushd linux-integrity + git pull --depth 1 origin ${{ steps.last-commit.outputs.LINUX_SHA }} + make ARCH=um defconfig + ./scripts/kconfig/merge_config.sh -m .config $(ls ../kernel-configs/*) + # Update manually, to specify ARCH=um + make ARCH=um olddefconfig + # Make everything built-in + make ARCH=um localyesconfig + make ARCH=um $CROSS_COMPILE_OPT -j$(nproc) + chmod +x linux + cp linux .. + cp certs/signing_key.pem .. + popd + job: + needs: build runs-on: ubuntu-latest strategy: @@ -75,6 +147,13 @@ jobs: CC: clang TSS: ibmtss + - container: "fedora:latest" + env: + CC: clang + TSS: ibmtss + TST_ENV: um + TST_KERNEL: ../linux + - container: "centos:7" env: CC: gcc @@ -98,7 +177,7 @@ jobs: container: image: ${{ matrix.container }} env: ${{ matrix.env }} - options: --privileged --device /dev/loop-control + options: --privileged --device /dev/loop-control -v /dev/shm:/dev/shm steps: - name: Show OS @@ -125,8 +204,24 @@ jobs: fi fi + - name: Retrieve UML kernel + if: ${{ matrix.env.TST_ENV }} + uses: actions/cache@v3 + continue-on-error: false + with: + path: linux + key: linux-${{ needs.build.outputs.LINUX_SHA }}-${{ hashFiles('**/kernel-configs/*') }} + + - name: Retrieve signing key + if: ${{ matrix.env.TST_ENV }} + continue-on-error: false + uses: actions/cache@v3 + with: + path: signing_key.pem + key: signing_key.pem-${{ needs.build.outputs.LINUX_SHA }}-${{ hashFiles('**/kernel-configs/*') }} + - name: Compiler version run: $CC --version - name: Compile - run: CC="$CC" VARIANT="$VARIANT" COMPILE_SSL="$COMPILE_SSL" ./build.sh + run: CC="$CC" VARIANT="$VARIANT" COMPILE_SSL="$COMPILE_SSL" TST_ENV="$TST_ENV" TST_KERNEL="$TST_KERNEL" ./build.sh diff --git a/build.sh b/build.sh index 4e2f1bb..0920599 100755 --- a/build.sh +++ b/build.sh @@ -114,6 +114,11 @@ if [ $ret -eq 0 ]; then grep "skipped" tests/fsverity.log && \ grep "skipped" tests/fsverity.log | wc -l fi + if [ -f tests/portable_signatures.log ]; then + [ -n "$CI" ] && cat tests/portable_signatures.log || tail tests/portable_signatures.log + grep "skipped" tests/portable_signatures.log && \ + grep "skipped" tests/portable_signatures.log | wc -l + fi exit 0 fi diff --git a/ci/fedora.sh b/ci/fedora.sh index 2272bbc..1d17c6b 100755 --- a/ci/fedora.sh +++ b/ci/fedora.sh @@ -44,7 +44,13 @@ yum -y install \ util-linux \ vim-common \ wget \ - which + which \ + zstd \ + systemd \ + keyutils \ + e2fsprogs \ + acl \ + libcap yum -y install docbook5-style-xsl || true yum -y install swtpm || true @@ -55,4 +61,8 @@ if [ -f /etc/centos-release ]; then fi yum -y install softhsm || true +# haveged is available via EPEL on CentOS stream8. +yum -y install haveged || true + ./tests/install-fsverity.sh +./tests/install-mount-idmapped.sh diff --git a/kernel-configs/base b/kernel-configs/base new file mode 100644 index 0000000..7acbd5b --- /dev/null +++ b/kernel-configs/base @@ -0,0 +1,213 @@ +CONFIG_LOCALVERSION="-dont-use" +CONFIG_WATCH_QUEUE=y +CONFIG_AUDIT=y +CONFIG_AUDITSYSCALL=y +CONFIG_HZ_PERIODIC=y +CONFIG_LOG_BUF_SHIFT=17 +CONFIG_USER_NS=y +CONFIG_PID_NS=y +CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE=y +CONFIG_KALLSYMS_ALL=y +CONFIG_SYSTEM_DATA_VERIFICATION=y +CONFIG_TRACEPOINTS=y +CONFIG_CON_CHAN="xterm" +CONFIG_SSL_CHAN="pty" +CONFIG_MODULE_SIG_FORMAT=y +CONFIG_MODULE_SIG=y +CONFIG_MODULE_SIG_FORCE=y +CONFIG_MODULE_SIG_ALL=y +CONFIG_MODULE_SIG_SHA1=y +CONFIG_MODULE_SIG_HASH="sha1" +CONFIG_MODULES_TREE_LOOKUP=y +CONFIG_BLK_DEBUG_FS=y +CONFIG_ASN1=y +CONFIG_UNINLINE_SPIN_UNLOCK=y +CONFIG_SLUB=y +CONFIG_COMPACTION=y +CONFIG_COMPACT_UNEVICTABLE_DEFAULT=1 +CONFIG_MIGRATION=y +CONFIG_BLK_DEV_LOOP=y +CONFIG_LEGACY_PTY_COUNT=256 +CONFIG_NULL_TTY=y +CONFIG_SERIAL_DEV_BUS=y +CONFIG_SERIAL_DEV_CTRL_TTYPORT=y +CONFIG_VALIDATE_FS_PARSER=y +CONFIG_EXT4_FS_POSIX_ACL=y +CONFIG_EXT4_FS_SECURITY=y +CONFIG_EXT4_DEBUG=y +CONFIG_REISERFS_FS_XATTR=y +CONFIG_REISERFS_FS_POSIX_ACL=y +CONFIG_REISERFS_FS_SECURITY=y +CONFIG_FS_POSIX_ACL=y +CONFIG_FS_VERITY=y +CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_TMPFS_XATTR=y +CONFIG_CONFIGFS_FS=y +CONFIG_KEYS=y +CONFIG_ENCRYPTED_KEYS=y +CONFIG_SECURITY=y +CONFIG_SECURITYFS=y +CONFIG_SECURITY_NETWORK=y +CONFIG_SECURITY_PATH=y +CONFIG_LSM="lockdown,yama,loadpin,safesetid,integrity,bpf" +CONFIG_CRYPTO_AEAD2=y +CONFIG_CRYPTO_SKCIPHER=y +CONFIG_CRYPTO_SKCIPHER2=y +CONFIG_CRYPTO_RNG=y +CONFIG_CRYPTO_RNG2=y +CONFIG_CRYPTO_RNG_DEFAULT=y +CONFIG_CRYPTO_AKCIPHER2=y +CONFIG_CRYPTO_AKCIPHER=y +CONFIG_CRYPTO_KPP2=y +CONFIG_CRYPTO_ACOMP2=y +CONFIG_CRYPTO_MANAGER=y +CONFIG_CRYPTO_MANAGER2=y +CONFIG_CRYPTO_NULL2=y +CONFIG_CRYPTO_RSA=y +CONFIG_CRYPTO_ECC=y +CONFIG_CRYPTO_ECDSA=y +CONFIG_CRYPTO_AES=y +CONFIG_CRYPTO_CBC=y +CONFIG_CRYPTO_HMAC=y +CONFIG_CRYPTO_MD5=y +CONFIG_CRYPTO_SHA1=y +CONFIG_CRYPTO_SHA256=y +CONFIG_CRYPTO_SHA512=y +CONFIG_CRYPTO_WP512=y +CONFIG_CRYPTO_LZO=y +CONFIG_CRYPTO_ZSTD=y +CONFIG_CRYPTO_DRBG_MENU=y +CONFIG_CRYPTO_DRBG_HMAC=y +CONFIG_CRYPTO_DRBG=y +CONFIG_CRYPTO_JITTERENTROPY=y +CONFIG_CRYPTO_HASH_INFO=y +CONFIG_ASYMMETRIC_KEY_TYPE=y +CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE=y +CONFIG_X509_CERTIFICATE_PARSER=y +CONFIG_PKCS8_PRIVATE_KEY_PARSER=y +CONFIG_PKCS7_MESSAGE_PARSER=y +CONFIG_PKCS7_TEST_KEY=y +CONFIG_SIGNED_PE_FILE_VERIFICATION=y +CONFIG_MODULE_SIG_KEY="certs/signing_key.pem" +CONFIG_MODULE_SIG_KEY_TYPE_RSA=y +CONFIG_SYSTEM_TRUSTED_KEYRING=y +CONFIG_SYSTEM_TRUSTED_KEYS="" +CONFIG_SYSTEM_EXTRA_CERTIFICATE=y +CONFIG_SYSTEM_EXTRA_CERTIFICATE_SIZE=4096 +CONFIG_SECONDARY_TRUSTED_KEYRING=y +CONFIG_SYSTEM_BLACKLIST_KEYRING=y +CONFIG_SYSTEM_BLACKLIST_HASH_LIST="" +CONFIG_SYSTEM_REVOCATION_LIST=y +CONFIG_SYSTEM_REVOCATION_KEYS="" +CONFIG_SYSTEM_BLACKLIST_AUTH_UPDATE=y +CONFIG_BINARY_PRINTF=y +CONFIG_CRYPTO_LIB_AES=y +CONFIG_CRYPTO_LIB_SHA256=y +CONFIG_CRC_CCITT=y +CONFIG_XXHASH=y +CONFIG_AUDIT_GENERIC=y +CONFIG_LZO_COMPRESS=y +CONFIG_LZO_DECOMPRESS=y +CONFIG_ZSTD_COMMON=y +CONFIG_ZSTD_COMPRESS=y +CONFIG_ZSTD_DECOMPRESS=y +CONFIG_ASSOCIATIVE_ARRAY=y +CONFIG_SGL_ALLOC=y +CONFIG_GLOB=y +CONFIG_CLZ_TAB=y +CONFIG_MPILIB=y +CONFIG_SIGNATURE=y +CONFIG_OID_REGISTRY=y +CONFIG_STACKDEPOT=y +CONFIG_STACKDEPOT_ALWAYS_INIT=y +CONFIG_PRINTK_TIME=y +CONFIG_PRINTK_CALLER=y +CONFIG_DYNAMIC_DEBUG=y +CONFIG_DYNAMIC_DEBUG_CORE=y +CONFIG_DEBUG_INFO_DWARF5=y +CONFIG_GDB_SCRIPTS=y +CONFIG_FRAME_WARN=2048 +CONFIG_READABLE_ASM=y +CONFIG_DEBUG_SECTION_MISMATCH=y +CONFIG_DEBUG_FS=y +CONFIG_DEBUG_FS_ALLOW_ALL=y +CONFIG_UBSAN=y +CONFIG_CC_HAS_UBSAN_BOUNDS=y +CONFIG_UBSAN_BOUNDS=y +CONFIG_UBSAN_ONLY_BOUNDS=y +CONFIG_UBSAN_SHIFT=y +CONFIG_UBSAN_DIV_ZERO=y +CONFIG_UBSAN_BOOL=y +CONFIG_UBSAN_ENUM=y +CONFIG_UBSAN_ALIGNMENT=y +CONFIG_PAGE_EXTENSION=y +CONFIG_DEBUG_PAGEALLOC=y +CONFIG_DEBUG_PAGEALLOC_ENABLE_DEFAULT=y +CONFIG_SLUB_DEBUG=y +CONFIG_SLUB_DEBUG_ON=y +CONFIG_PAGE_OWNER=y +CONFIG_PAGE_POISONING=y +CONFIG_DEBUG_OBJECTS=y +CONFIG_DEBUG_OBJECTS_FREE=y +CONFIG_DEBUG_OBJECTS_TIMERS=y +CONFIG_DEBUG_OBJECTS_WORK=y +CONFIG_DEBUG_OBJECTS_RCU_HEAD=y +CONFIG_DEBUG_OBJECTS_PERCPU_COUNTER=y +CONFIG_DEBUG_OBJECTS_ENABLE_DEFAULT=1 +CONFIG_DEBUG_KMEMLEAK=y +CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE=16000 +CONFIG_DEBUG_KMEMLEAK_AUTO_SCAN=y +CONFIG_DEBUG_STACK_USAGE=y +CONFIG_SCHED_STACK_END_CHECK=y +CONFIG_DEBUG_SHIRQ=y +CONFIG_PANIC_ON_OOPS=y +CONFIG_PANIC_ON_OOPS_VALUE=1 +CONFIG_LOCKUP_DETECTOR=y +CONFIG_SOFTLOCKUP_DETECTOR=y +CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y +CONFIG_DETECT_HUNG_TASK=y +CONFIG_DEFAULT_HUNG_TASK_TIMEOUT=120 +CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y +CONFIG_WQ_WATCHDOG=y +CONFIG_DEBUG_TIMEKEEPING=y +CONFIG_PROVE_LOCKING=y +CONFIG_PROVE_RAW_LOCK_NESTING=y +CONFIG_LOCK_STAT=y +CONFIG_DEBUG_RT_MUTEXES=y +CONFIG_DEBUG_SPINLOCK=y +CONFIG_DEBUG_MUTEXES=y +CONFIG_DEBUG_WW_MUTEX_SLOWPATH=y +CONFIG_DEBUG_RWSEMS=y +CONFIG_DEBUG_LOCK_ALLOC=y +CONFIG_LOCKDEP=y +CONFIG_LOCKDEP_BITS=15 +CONFIG_LOCKDEP_CHAINS_BITS=16 +CONFIG_LOCKDEP_STACK_TRACE_BITS=19 +CONFIG_LOCKDEP_STACK_TRACE_HASH_BITS=14 +CONFIG_LOCKDEP_CIRCULAR_QUEUE_BITS=12 +CONFIG_WW_MUTEX_SELFTEST=y +CONFIG_CSD_LOCK_WAIT_DEBUG=y +CONFIG_TRACE_IRQFLAGS=y +CONFIG_DEBUG_IRQFLAGS=y +CONFIG_DEBUG_LIST=y +CONFIG_DEBUG_PLIST=y +CONFIG_DEBUG_NOTIFIERS=y +CONFIG_BUG_ON_DATA_CORRUPTION=y +CONFIG_PROVE_RCU=y +CONFIG_RCU_TRACE=y +CONFIG_NOP_TRACER=y +CONFIG_TRACE_CLOCK=y +CONFIG_RING_BUFFER=y +CONFIG_EVENT_TRACING=y +CONFIG_CONTEXT_SWITCH_TRACER=y +CONFIG_PREEMPTIRQ_TRACEPOINTS=y +CONFIG_TRACING=y +CONFIG_DRM=n +CONFIG_USB=n +CONFIG_SOUND=n +CONFIG_9P_FS=y +CONFIG_9P_FS_POSIX_ACL=y +CONFIG_9P_FS_SECURITY=y +CONFIG_ETHERNET=n +CONFIG_WLAN=n diff --git a/kernel-configs/integrity b/kernel-configs/integrity new file mode 100644 index 0000000..a7e01e1 --- /dev/null +++ b/kernel-configs/integrity @@ -0,0 +1,29 @@ +CONFIG_INTEGRITY=y +CONFIG_INTEGRITY_SIGNATURE=y +CONFIG_INTEGRITY_ASYMMETRIC_KEYS=y +CONFIG_INTEGRITY_TRUSTED_KEYRING=y +CONFIG_INTEGRITY_AUDIT=y +CONFIG_IMA=y +CONFIG_IMA_MEASURE_PCR_IDX=10 +CONFIG_IMA_NG_TEMPLATE=y +CONFIG_IMA_DEFAULT_TEMPLATE="ima-ng" +CONFIG_IMA_DEFAULT_HASH_SHA256=y +CONFIG_IMA_DEFAULT_HASH="sha256" +CONFIG_IMA_WRITE_POLICY=y +CONFIG_IMA_READ_POLICY=y +CONFIG_IMA_APPRAISE=y +CONFIG_IMA_ARCH_POLICY=y +CONFIG_IMA_APPRAISE_BUILD_POLICY=y +CONFIG_IMA_APPRAISE_BOOTPARAM=y +CONFIG_IMA_APPRAISE_MODSIG=y +CONFIG_IMA_TRUSTED_KEYRING=y +CONFIG_IMA_BLACKLIST_KEYRING=y +CONFIG_IMA_LOAD_X509=y +CONFIG_IMA_X509_PATH="/etc/keys/x509_ima.der" +CONFIG_IMA_MEASURE_ASYMMETRIC_KEYS=y +CONFIG_IMA_QUEUE_EARLY_BOOT_KEYS=y +CONFIG_EVM=y +CONFIG_EVM_ATTR_FSUUID=y +CONFIG_EVM_ADD_XATTRS=y +CONFIG_EVM_LOAD_X509=y +CONFIG_EVM_X509_PATH="/etc/keys/x509_evm.der" diff --git a/src/evmctl.c b/src/evmctl.c index 0ac7930..91b531c 100644 --- a/src/evmctl.c +++ b/src/evmctl.c @@ -1184,9 +1184,9 @@ static int cmd_setxattr_ima(struct command *cmd) #define MAX_KEY_SIZE 128 -static int calc_evm_hmac(const char *file, const char *keyfile, unsigned char *hash) +static int calc_evm_hmac(const char *file, const char *keyfile, unsigned char *sig) { - size_t mdlen; + size_t siglen = MAX_DIGEST_SIZE; EVP_MD_CTX *pctx; EVP_PKEY *pkey = NULL; struct stat st; @@ -1260,7 +1260,7 @@ static int calc_evm_hmac(const char *file, const char *keyfile, unsigned char *h pkey = EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, NULL, evmkey, sizeof(evmkey)); if (!pkey) { - log_err("HMAC_Init() failed\n"); + log_err("EVP_PKEY_new_mac_key() failed\n"); goto out; } @@ -1326,12 +1326,12 @@ static int calc_evm_hmac(const char *file, const char *keyfile, unsigned char *h err = EVP_DigestSignUpdate(pctx, &hmac_misc, hmac_size); if (err != 1) { - log_err("HMAC_Update() failed\n"); + log_err("EVP_DigestSignUpdate() failed\n"); goto out_ctx_cleanup; } - err = EVP_DigestSignFinal(pctx, hash, &mdlen); + err = EVP_DigestSignFinal(pctx, sig, &siglen); if (err != 1) - log_err("HMAC_Final() failed\n"); + log_err("EVP_DigestSignFinal() failed\n"); out_ctx_cleanup: EVP_PKEY_free(pkey); #if OPENSSL_VERSION_NUMBER >= 0x10100000 @@ -1340,7 +1340,7 @@ out_ctx_cleanup: out: free(key); if (err == 1) - return mdlen; + return siglen; return err; } diff --git a/tests/Makefile.am b/tests/Makefile.am index 3050824..421fac5 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -2,7 +2,7 @@ check_SCRIPTS = TESTS = $(check_SCRIPTS) check_SCRIPTS += ima_hash.test sign_verify.test boot_aggregate.test \ - fsverity.test + fsverity.test portable_signatures.test clean-local: -rm -f *.txt *.out *.sig *.sig2 diff --git a/tests/boot_aggregate.test b/tests/boot_aggregate.test index d711566..ca5faf9 100755 --- a/tests/boot_aggregate.test +++ b/tests/boot_aggregate.test @@ -12,7 +12,7 @@ # for verifying the calculated boot_aggregate is included in this # directory as well. -trap cleanup SIGINT SIGTERM EXIT +trap '_report_exit_and_cleanup cleanup' SIGINT SIGTERM EXIT # Base VERBOSE on the environment variable, if set. VERBOSE="${VERBOSE:-0}" diff --git a/tests/fsverity.test b/tests/fsverity.test index 549c42a..01d5c35 100755 --- a/tests/fsverity.test +++ b/tests/fsverity.test @@ -30,7 +30,7 @@ # custom policy rules might take precedence. cd "$(dirname "$0")" || exit 1 -PATH=../src:$PATH +PATH=../src:../fsverity-utils:$PATH source ./functions.sh # Base VERBOSE on the environment variable, if set. @@ -47,7 +47,7 @@ FSVERITY="$(which fsverity)" _require dd mkfs blkid e2fsck tune2fs evmctl setfattr ./gen-keys.sh >/dev/null 2>&1 -trap cleanup SIGINT SIGTERM EXIT +trap '_report_exit_and_cleanup _cleanup_env cleanup' SIGINT SIGTERM EXIT cleanup() { if [ -e $TST_MNT ]; then @@ -58,7 +58,6 @@ cleanup() { rm "$TST_IMG" fi fi - _report_exit_and_cleanup } # Loopback mount a file @@ -309,6 +308,15 @@ measure-ima() { return "$error" } +# Run in the new environment if TST_ENV is set. +_run_env "$TST_KERNEL" "$PWD/$(basename "$0")" "TST_ENV=$TST_ENV TST_KERNEL=$TST_KERNEL PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH VERBOSE=$VERBOSE" + +# Exit from the creator of the new environment. +_exit_env "$TST_KERNEL" + +# Mount filesystems in the new environment. +_init_env + # Dependency on being able to read and write the IMA policy file. # Requires both CONFIG_IMA_WRITE_POLICY, CONFIG_IMA_READ_POLICY be # enabled. diff --git a/tests/functions.sh b/tests/functions.sh index 8f6f02d..2c4d205 100755 --- a/tests/functions.sh +++ b/tests/functions.sh @@ -72,6 +72,12 @@ declare -i TNESTED=0 # just for sanity checking expect_pass() { local -i ret + if [ -n "$TST_LIST" ] && [ "${TST_LIST/$1/}" = "$TST_LIST" ]; then + [ "$VERBOSE" -gt 1 ] && echo "____ SKIP test: $*" + testsskip+=1 + return "$SKIP" + fi + if [ $TNESTED -gt 0 ]; then echo $RED"expect_pass should not be run nested"$NORM testsfail+=1 @@ -98,6 +104,12 @@ expect_pass() { expect_fail() { local ret + if [ -n "$TST_LIST" ] && [ "${TST_LIST/$1/}" = "$TST_LIST" ]; then + [ "$VERBOSE" -gt 1 ] && echo "____ SKIP test: $*" + testsskip+=1 + return "$SKIP" + fi + if [ $TNESTED -gt 0 ]; then echo $RED"expect_fail should not be run nested"$NORM testsfail+=1 @@ -250,10 +262,14 @@ _enable_gost_engine() { # Show test stats and exit into automake test system # with proper exit code (same as ours). Do cleanups. _report_exit_and_cleanup() { + local exit_code=$? + if [ -n "${WORKDIR}" ]; then rm -rf "${WORKDIR}" fi + "$@" + if [ $testsfail -gt 0 ]; then echo "=================================" echo " Run with FAILEARLY=1 $0 $*" @@ -267,12 +283,33 @@ _report_exit_and_cleanup() { [ $testsfail -gt 0 ] && echo -n "$RED" || echo -n "$NORM" echo " FAIL: $testsfail" echo "$NORM" + # Signal failure to the testing environment creator with an unclean shutdown. + if [ -n "$TST_ENV" ] && [ $$ -eq 1 ]; then + if [ -z "$(command -v poweroff)" ]; then + echo "Warning: cannot properly shutdown system" + fi + + # If no test was executed and the script was successful, + # do a clean shutdown. + if [ $testsfail -eq 0 ] && [ $testspass -eq 0 ] && [ $testsskip -eq 0 ] && + [ $exit_code -ne "$FAIL" ] && [ $exit_code -ne "$HARDFAIL" ]; then + poweroff -f + fi + + # If tests were executed and no test failed, do a clean shutdown. + if { [ $testspass -gt 0 ] || [ $testsskip -gt 0 ]; } && + [ $testsfail -eq 0 ]; then + poweroff -f + fi + fi if [ $testsfail -gt 0 ]; then exit "$FAIL" elif [ $testspass -gt 0 ]; then exit "$OK" - else + elif [ $testsskip -gt 0 ]; then exit "$SKIP" + else + exit "$exit_code" fi } @@ -312,4 +349,76 @@ _softhsm_teardown() { rm -rf "${SOFTHSM_SETUP_CONFIGDIR}" unset SOFTHSM_SETUP_CONFIGDIR SOFTHSM2_CONF PKCS11_KEYURI \ EVMCTL_ENGINE OPENSSL_ENGINE OPENSSL_KEYFORM -} \ No newline at end of file +} + +# Syntax: _run_env +_run_env() { + if [ -z "$TST_ENV" ]; then + return + fi + + if [ $$ -eq 1 ]; then + return + fi + + if [ "$TST_ENV" = "um" ]; then + expect_pass "$1" rootfstype=hostfs rw init="$2" quiet mem=2048M "$3" + else + echo $RED"Testing environment $TST_ENV not supported"$NORM + exit "$FAIL" + fi +} + +# Syntax: _exit_env +_exit_env() { + if [ -z "$TST_ENV" ]; then + return + fi + + if [ $$ -eq 1 ]; then + return + fi + + exit "$OK" +} + +# Syntax: _init_env +_init_env() { + if [ -z "$TST_ENV" ]; then + return + fi + + if [ $$ -ne 1 ]; then + return + fi + + mount -t tmpfs tmpfs /tmp + mount -t proc proc /proc + mount -t sysfs sysfs /sys + mount -t securityfs securityfs /sys/kernel/security + + if [ -n "$(command -v haveged 2> /dev/null)" ]; then + $(command -v haveged) -w 1024 &> /dev/null + fi + + pushd "$PWD" > /dev/null || exit "$FAIL" +} + +# Syntax: _cleanup_env +_cleanup_env() { + if [ -z "$TST_ENV" ]; then + $1 + return + fi + + if [ $$ -ne 1 ]; then + return + fi + + $1 + + umount /sys/kernel/security + umount /sys + umount /proc + umount /tmp +} diff --git a/tests/install-fsverity.sh b/tests/install-fsverity.sh index 418fc42..8311bc0 100755 --- a/tests/install-fsverity.sh +++ b/tests/install-fsverity.sh @@ -2,6 +2,5 @@ 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 +CC=gcc make -j$(nproc) cd .. -rm -rf fsverity-utils diff --git a/tests/install-mount-idmapped.sh b/tests/install-mount-idmapped.sh new file mode 100755 index 0000000..c954006 --- /dev/null +++ b/tests/install-mount-idmapped.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +git clone https://github.com/brauner/mount-idmapped.git +cd mount-idmapped +gcc -o mount-idmapped mount-idmapped.c +cd .. diff --git a/tests/portable_signatures.test b/tests/portable_signatures.test new file mode 100755 index 0000000..9f3339b --- /dev/null +++ b/tests/portable_signatures.test @@ -0,0 +1,1122 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (C) 2022-2023 Roberto Sassu +# +# Check if operations on files with EVM portable signatures succeed. + +trap '_report_exit_and_cleanup _cleanup_env cleanup' SIGINT SIGTERM SIGSEGV EXIT + +# Base VERBOSE on the environment variable, if set. +VERBOSE="${VERBOSE:-0}" +TST_EVM_CHANGE_MODE="${TST_EVM_CHANGE_MODE:-0}" + +# From security/integrity/evm/evm.h in kernel source directory. +(( EVM_INIT_HMAC=0x0001 )) +(( EVM_INIT_X509=0x0002 )) +(( EVM_ALLOW_METADATA_WRITES=0x0004 )) +(( EVM_SETUP_COMPLETE=0x80000000 )) + +cd "$(dirname "$0")" || exit "$FAIL" +export PATH=$PWD/../src:$PWD/../mount-idmapped:$PATH +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH +. ./functions.sh +_require evmctl + +cleanup() { + if [ "$g_loop_mounted" = "1" ]; then + popd > /dev/null || exit "$FAIL" + + if [ -n "$g_mountpoint_idmapped" ]; then + umount "$g_mountpoint_idmapped" + fi + + umount "$g_mountpoint" + fi + + if [ -n "$g_dev" ]; then + losetup -d "$g_dev" + fi + + if [ -n "$g_image" ]; then + rm -f "$g_image" + fi + + if [ -n "$key_path_der" ]; then + rm -f "$key_path_der" + fi + + if [ -n "$g_mountpoint" ]; then + rm -Rf "$g_mountpoint" + fi + + if [ -n "$g_mountpoint_idmapped" ]; then + rm -Rf "$g_mountpoint_idmapped" + fi +} + +get_xattr() { + local format="hex" + + if [ "$1" = "security.selinux" ]; then + format="text" + fi + + getfattr -n "$1" -e $format -d "$2" 2> /dev/null | awk -F "=" '$1 == "'"$1"'" {if ("'$format'" == "hex") v=substr($2, 3); else { split($2, temp, "\""); v=temp[2] }; print v}' +} + +# Use the fsuuid= IMA policy keyword to select only files created/used by the +# tests below. Also use fowner= to differentiate between files created/used by +# individual tests. +IMA_UUID="28b23254-9467-44c0-b6ba-34b12e85a26d" +APPRAISE_DIGSIG_FOWNER=2000 +APPRAISE_DIGSIG_RULE="appraise fsuuid=$IMA_UUID fowner=$APPRAISE_DIGSIG_FOWNER appraise_type=imasig" +MEASURE_FOWNER=2001 +MEASURE_RULE="measure fsuuid=$IMA_UUID fowner=$MEASURE_FOWNER template=ima-sig" +APPRAISE_FOWNER=2002 +APPRAISE_RULE="appraise fsuuid=$IMA_UUID fowner=$APPRAISE_FOWNER" +METADATA_CHANGE_FOWNER=3001 +METADATA_CHANGE_FOWNER_2=3002 + +check_load_ima_rule() { + local rule_loaded + local result + local new_policy + + rule_loaded=$(grep "$1" /sys/kernel/security/ima/policy) + if [ -z "$rule_loaded" ]; then + new_policy=$(mktemp -p "$g_mountpoint") + echo "$1" > "$new_policy" + evmctl sign -o -a sha256 --imasig --key "$key_path" "$new_policy" &> /dev/null + echo "$new_policy" > /sys/kernel/security/ima/policy + result=$? + rm -f "$new_policy" + + if [ "$result" -ne 0 ]; then + echo "${RED}Failed to set IMA policy${NORM}" + return "$FAIL" + fi + fi + + return "$OK" +} + +# The purpose of this test is to verify that the patch 'ima: Allow imasig +# requirement to be satisfied by EVM portable signatures' didn't break the +# current behavior (IMA signatures still satisfy the imasig requirement). +check_ima_sig_appraisal() { + local result + + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if [ $((evm_value & (EVM_INIT_X509 | EVM_INIT_HMAC))) -ne 0 ]; then + echo "${CYAN}EVM mode 0 required${NORM}" + return "$SKIP" + fi + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! evmctl ima_sign -a sha256 --key "$key_path" test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + if ! chown "$APPRAISE_DIGSIG_FOWNER" test-file; then + echo "${RED}Cannot change owner of test-file${NORM}" + return "$FAIL" + fi + + check_load_ima_rule "$APPRAISE_DIGSIG_RULE" + result=$? + if [ $result -ne "$OK" ]; then + return $result + fi + + # Check if appraisal works. + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file${NORM}" + return "$FAIL" + fi + + # Ensure that files with IMA signature cannot be updated (immutable). + if echo "test" 2> /dev/null >> test-file; then + echo "${RED}Write to test-file should not succeed (immutable file)${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_ima_sig_appraisal() { + rm -f test-file +} + +# Requires: +# - ima: Don't remove security.ima if file must not be appraised +# +# The purpose of this test is to verify that the patch 'ima: Introduce template +# field evmsig and write to field sig as fallback' still allows IMA signatures +# to be displayed in the measurement list. +check_ima_sig_ima_measurement_list() { + local result + local ima_sig_fs + local ima_sig_list + + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! evmctl ima_sign -a sha256 --imasig --key "$key_path" test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + if ! chown "$MEASURE_FOWNER" test-file; then + echo "${RED}Cannot change owner of test-file${NORM}" + return "$FAIL" + fi + + check_load_ima_rule "$MEASURE_RULE" + result=$? + if [ $result -ne "$OK" ]; then + return $result + fi + + # Read the file to add it to the measurement list. + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file${NORM}" + return "$FAIL" + fi + + ima_sig_fs=$(get_xattr security.ima test-file) + if [ -z "$ima_sig_fs" ]; then + echo "${RED}security.ima not found${NORM}" + return "$FAIL" + fi + + # Search security.ima in the measurement list. + ima_sig_list=$(awk '$6 == "'"$ima_sig_fs"'"' < /sys/kernel/security/ima/ascii_runtime_measurements) + if [ -z "$ima_sig_list" ]; then + echo "${RED}security.ima mismatch (xattr != measurement list)${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_ima_sig_ima_measurement_list() { + rm -f test-file +} + +# Requires: +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# +# The purpose of this test is to verify that new files can be created when EVM +# is initialized only with a public key. +check_create_file() { + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + # To trigger the bug we need to enable public key verification without HMAC key loaded. + if [ $((evm_value & EVM_INIT_X509)) -ne "$EVM_INIT_X509" ]; then + echo "${CYAN}EVM mode $EVM_INIT_X509 required${NORM}" + return "$SKIP" + fi + + if [ $((evm_value & EVM_INIT_HMAC)) -eq "$EVM_INIT_HMAC" ]; then + echo "${CYAN}EVM mode $EVM_INIT_HMAC must be disabled${NORM}" + return "$SKIP" + fi + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_create_file() { + rm -f test-file +} + +# Requires: +# - evm: Introduce evm_hmac_disabled() to safely ignore verification errors +# - evm: Allow xattr/attr operations for portable signatures +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# +# The purpose of this test is to verify that EVM with the patches above allows +# metadata to copied one by one, even if the portable signature verification +# temporarily fails until the copy is completed. +check_cp_preserve_xattrs() { + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if [ "$evm_value" -ne "$EVM_INIT_X509" ]; then + echo "${CYAN}EVM mode $EVM_INIT_X509 required${NORM}" + return "$SKIP" + fi + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! evmctl sign -o -a sha256 --imahash --key "$key_path" test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + # Check if cp is allowed to set metadata for the new file. + if ! cp -a test-file test-file.copy; then + echo "${RED}Cannot copy test-file with attrs/xattrs preserved${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_cp_preserve_xattrs() { + rm -f test-file test-file.copy +} + +# Requires: +# - evm: Introduce evm_hmac_disabled() to safely ignore verification errors +# - evm: Allow xattr/attr operations for portable signatures +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# - ima: Don't remove security.ima if file must not be appraised +# +# The purpose of this test is similar to that of the previous test, with the +# difference that tar is used instead of cp. One remark is that the owner is +# intentionally different (or it should be) from the current owner, to +# incrementally test the patches without 'evm: Allow setxattr() and setattr() +# for unmodified metadata'. +check_tar_extract_xattrs_different_owner() { + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if [ "$evm_value" -ne "$EVM_INIT_X509" ]; then + echo "${CYAN}EVM mode $EVM_INIT_X509 required${NORM}" + return "$SKIP" + fi + + if ! mkdir in out; then + echo "${RED}Cannot create directories${NORM}" + return "$FAIL" + fi + + if ! echo "test" > in/test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! chown 3000 in/test-file; then + echo "${RED}Cannot change owner of test-file${NORM}" + return "$FAIL" + fi + + if ! chmod 600 in/test-file; then + echo "${RED}Cannot change mode of test-file${NORM}" + return "$FAIL" + fi + + if ! evmctl sign -o -a sha256 --imahash --key "$key_path" in/test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + if ! tar --xattrs-include=* -cf test-archive.tar in/test-file; then + echo "${RED}Cannot create archive with xattrs${NORM}" + return "$FAIL" + fi + + # Check if tar is allowed to set metadata for the extracted file. + # Ensure that the owner from the archive is different from the + # owner of the extracted file to avoid that portable signature + # verification succeeds before restoring original metadata + # (a patch allows modification of immutable metadata if portable + # signature verification fails). + if ! tar --xattrs-include=* -xf test-archive.tar -C out; then + echo "${RED}Cannot extract archive with xattrs${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_tar_extract_xattrs_different_owner() { + rm -Rf in out test-archive.tar +} + +# Requires: +# - evm: Introduce evm_hmac_disabled() to safely ignore verification errors +# - evm: Allow xattr/attr operations for portable signatures +# - evm: Pass user namespace to set/remove xattr hooks +# - evm: Allow setxattr() and setattr() for unmodified metadata +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# - ima: Don't remove security.ima if file must not be appraised +# +# The purpose of this test is similar to that of the previous two tests. The +# difference is that tar is used instead of cp, and the extracted files have +# the same owner as the current one. Thus, this test requires 'evm: Allow +# setxattr() and setattr() for unmodified metadata'. +check_tar_extract_xattrs_same_owner() { + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if [ "$evm_value" -ne "$EVM_INIT_X509" ]; then + echo "${CYAN}EVM mode $EVM_INIT_X509 required${NORM}" + return "$SKIP" + fi + + if ! mkdir in out; then + echo "${RED}Cannot create directories${NORM}" + return "$FAIL" + fi + + if ! echo "test" > in/test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! evmctl sign -o -a sha256 --imahash --key "$key_path" in/test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + if ! tar --xattrs-include=* -cf test-archive.tar in/test-file; then + echo "${RED}Cannot create archive with xattrs${NORM}" + return "$FAIL" + fi + + # Check if tar is allowed to set metadata for the extracted file. + # This test is different from the previous one, as the owner + # from the archive is the same of the owner of the extracted + # file. tar will attempt anyway to restore the original owner but + # unlike the previous test, portable signature verification already + # succeeds at the time the owner is set (another patch allows + # metadata operations if those operations don't modify current + # values). + if ! tar --xattrs-include=* -xf test-archive.tar -C out; then + echo "${RED}Cannot extract archive with xattrs${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_tar_extract_xattrs_same_owner() { + rm -Rf in out test-archive.tar +} + +# Requires: +# - evm: Introduce evm_hmac_disabled() to safely ignore verification errors +# - evm: Allow xattr/attr operations for portable signatures +# - evm: Pass user namespace to set/remove xattr hooks +# - evm: Allow setxattr() and setattr() for unmodified metadata +# - ima: Don't remove security.ima if file must not be appraised +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# +# The purpose of this test is to further verify the patches above, by executing +# commands to set the same or different metadata. Setting the same metadata +# should be allowed, setting different metadata should be denied. +check_metadata_change() { + local ima_xattr + local label + local last_char + local msg + + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if [ "$evm_value" -ne "$EVM_INIT_X509" ]; then + echo "${CYAN}EVM mode $EVM_INIT_X509 required${NORM}" + return "$SKIP" + fi + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! chown "$METADATA_CHANGE_FOWNER" test-file; then + echo "${RED}Cannot change owner of test-file${NORM}" + return "$FAIL" + fi + + if ! chgrp "$METADATA_CHANGE_FOWNER" test-file; then + echo "${RED}Cannot change group of test-file${NORM}" + return "$FAIL" + fi + + if ! chmod 2644 test-file; then + echo "${RED}Cannot change mode of test-file${NORM}" + return "$FAIL" + fi + + if ! evmctl sign -o -a sha256 --imahash --key "$key_path" test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + # If metadata modification is not allowed, EVM should deny any + # operation that modifies metadata. Check if setting the same + # value is allowed. + if ! chown "$METADATA_CHANGE_FOWNER" test-file; then + echo "${RED}Cannot set same owner for test-file${NORM}" + return "$FAIL" + fi + + # Setting a different value should not be allowed. + if chown "$METADATA_CHANGE_FOWNER_2" test-file 2> /dev/null; then + echo "${RED}Owner change for test-file should not be allowed (immutable metadata)${NORM}" + return "$FAIL" + fi + + # Repeat the test for the file mode. + if ! chmod 2644 test-file; then + echo "${RED}Cannot set same mode for test-file${NORM}" + return "$FAIL" + fi + + if chmod 2666 test-file 2> /dev/null; then + echo "${RED}Mode change for test-file should not be allowed (immutable metadata)${NORM}" + return "$FAIL" + fi + + if [ -n "$(command -v chcon 2> /dev/null)" ] && [ -n "$(command -v getenforce 2> /dev/null)" ] && [ "$(getenforce 2> /dev/null)" != "Disabled" ]; then + # Repeat the test for the SELinux label. + label=$(get_xattr security.selinux test-file) + + if [ -n "$label" ]; then + if ! chcon "$label" test-file; then + echo "${RED}Cannot set same security.selinux for test-file${NORM}" + return "$FAIL" + fi + fi + + if chcon unconfined_u:object_r:null_device_t:s0 test-file 2> /dev/null; then + echo "${RED}security.selinux change for test file should not be allowed (immutable metadata)${NORM}" + return "$FAIL" + fi + fi + + # Repeat the test for the IMA signature. + ima_xattr=$(get_xattr security.ima test-file) + if [ -z "$ima_xattr" ]; then + echo "${RED}security.ima not found${NORM}" + return "$FAIL" + fi + + if ! setfattr -n security.ima -v 0x"$ima_xattr" test-file; then + echo "${RED}Cannot set same security.ima for test-file${NORM}" + return "$FAIL" + fi + + last_char=${ima_xattr: -1} + ((last_char += 1)) + ((last_char %= 10)) + ima_xattr=${ima_xattr:0:-1}$last_char + + if setfattr -n security.ima -v 0x"$ima_xattr" test-file 2> /dev/null; then + echo "${RED}Change of security.ima for test-file should not be allowed (immutable metadata)${NORM}" + return "$FAIL" + fi + + # Repeat the test for ACLs. + if ! msg=$(exec 2>&1 && setfacl --set u::rw,g::r,o::r,m:r test-file); then + if [ "${msg%not supported}" != "$msg" ]; then + return "$OK" + fi + + echo "${RED}Cannot preserve system.posix_acl_access for test-file${NORM}" + return "$FAIL" + fi + + if setfacl --set u::rw,g::r,o::r,m:rw test-file 2> /dev/null; then + echo "${RED}Change of system.posix_acl_access for test-file should not be allowed (immutable metadata)${NORM}" + return "$FAIL" + fi + + if [ -n "$g_mountpoint_idmapped" ]; then + pushd "$g_mountpoint_idmapped" > /dev/null || exit "$FAIL" + + # Repeat the test for ACLs on an idmapped mount. + # + # This test relies on the fact that the caller of this script (root) is in + # the same owning group of test-file (in the idmapped mount the group is + # root, not $METADATA_CHANGE_FOWNER and, for this reason, the S_ISGID bit + # is not cleared. If EVM was not aware of the mapping, it would have + # determined that root is not in the owning group of test-file and given + # that also CAP_FSETID is cleared, the S_ISGID bit would have been cleared + # and thus the operation would fail (file metadata changed). + if ! capsh --drop='cap_fsetid' -- -c 'setfacl --set u::rw,g::r,o::r test-file'; then + echo "${RED}Cannot preserve system.posix_acl_access for test-file${NORM}" + popd || exit "$FAIL" + return "$FAIL" + fi + + popd > /dev/null || exit "$FAIL" + fi + + return "$OK" +} + +cleanup_metadata_change() { + rm -f test-file +} + +# Requires: +# - evm: Introduce evm_revalidate_status() +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# +# Note: +# This test can be run if EVM_ALLOW_METADATA_WRITES is set in advance +# before running this script. If it is not set before, this script sets +# EVM_SETUP_COMPLETE, disabling further EVM mode modifications until reboot. +# +# Without EVM_ALLOW_METADATA_WRITES, EVM_SETUP_COMPLETE is necessary to ignore +# the INTEGRITY_NOLABEL and INTEGRITY_NOXATTRS errors. +# +# The purpose of this test is to verify that IMA detected a metadata change +# when EVM_ALLOW_METADATA_WRITES is set (metadata operations are always +# allowed). After the first successful appraisal, the test intentionally changes +# metadata and verifies that IMA revoked access to the file. The test also +# verifies that IMA grants access again to the file after restoring the correct +# metadata. +check_evm_revalidate() { + local result + local ima_xattr + local ima_xattr_new + local evm_xattr + local evm_xattr_new + local label + local last_char + local msg + + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if [ "$evm_value" -ne $((EVM_INIT_X509 | EVM_ALLOW_METADATA_WRITES)) ]; then + echo "${CYAN}EVM mode $((EVM_INIT_X509 | EVM_ALLOW_METADATA_WRITES)) required, execute echo 4 > /sys/kernel/security/evm before running this test${NORM}" + return "$SKIP" + fi + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! chmod 600 test-file; then + echo "${RED}Cannot change mode of test-file${NORM}" + return "$FAIL" + fi + + # We need to defer setting the correct owner, as there could be + # already an IMA policy rule preventing evmctl from reading the + # file to calculate the digest. + if ! evmctl sign -o -a sha256 --imahash --uid "$APPRAISE_FOWNER" --key "$key_path" test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + if ! chown "$APPRAISE_FOWNER" test-file; then + echo "${RED}Cannot change owner of test-file${NORM}" + return "$FAIL" + fi + + check_load_ima_rule "$APPRAISE_RULE" + result=$? + if [ $result -ne "$OK" ]; then + return $result + fi + + # Read the file so that IMA would not re-appraise it next time. + if ! cat test-file &> /dev/null; then + echo "${RED}Cannot read test-file${NORM}" + return "$FAIL" + fi + + # After enabling metadata modification, operations should succeed even + # if the file has a portable signature. However, the previously cached + # appraisal status should be invalidated. + if ! chmod 644 test-file; then + echo "${RED}Cannot change mode of test-file${NORM}" + return "$FAIL" + fi + + # Here check if IMA re-appraised the file. The read should fail + # since now file metadata is invalid. + if cat test-file &> /dev/null; then + echo "${RED}Read of test-file should not succeed (invalid mode)${NORM}" + return "$FAIL" + fi + + # Restore metadata back to the original value. + if ! chmod 600 test-file; then + echo "${RED}Cannot restore original mode of test-file${NORM}" + return "$FAIL" + fi + + # Ensure that now IMA appraisal succeeds. + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file after restoring correct mode${NORM}" + return "$FAIL" + fi + + if [ -n "$(command -v chcon 2> /dev/null)" ] && [ -n "$(command -v getenforce 2> /dev/null)" ] && [ "$(getenforce 2> /dev/null)" != "Disabled" ]; then + # Repeat the test for the SELinux label. + label=$(get_xattr security.selinux test-file) + + if ! chcon unconfined_u:object_r:null_device_t:s0 test-file; then + echo "${RED}Cannot change security.selinux of test-file${NORM}" + return "$FAIL" + fi + + if cat test-file &> /dev/null; then + echo "${RED}Read of test-file should not succeed (invalid security.selinux)${NORM}" + return "$FAIL" + fi + + if [ -n "$label" ]; then + if ! chcon "$label" test-file; then + echo "${RED}Cannot restore original security.selinux of test-file${NORM}" + return "$FAIL" + fi + else + attr -S -r selinux test-file + fi + + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file after restoring correct security.selinux${NORM}" + return "$FAIL" + fi + fi + + # Repeat the test for the IMA signature. + ima_xattr=$(get_xattr security.ima test-file) + if [ -z "$ima_xattr" ]; then + echo "${RED}security.ima not found${NORM}" + return "$FAIL" + fi + + last_char=${ima_xattr: -1} + ((last_char += 1)) + ((last_char %= 10)) + ima_xattr_new=${ima_xattr:0:-1}$last_char + + if ! setfattr -n security.ima -v 0x"$ima_xattr_new" test-file; then + echo "${RED}Cannot set security.ima of test-file${NORM}" + return "$FAIL" + fi + + if cat test-file &> /dev/null; then + echo "${RED}Read of test-file should not succeed (invalid security.ima)${NORM}" + return "$FAIL" + fi + + if ! setfattr -n security.ima -v 0x"$ima_xattr" test-file; then + echo "${RED}Cannot restore original security.ima of test-file${NORM}" + return "$FAIL" + fi + + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file after restoring correct security.ima${NORM}" + return "$FAIL" + fi + + # Repeat the test for the EVM signature. + evm_xattr=$(get_xattr security.evm test-file) + if [ -z "$evm_xattr" ]; then + echo "${RED}security.evm not found${NORM}" + return "$FAIL" + fi + + last_char=${evm_xattr: -1} + ((last_char += 1)) + ((last_char %= 10)) + evm_xattr_new=${evm_xattr:0:-1}$last_char + + if ! setfattr -n security.evm -v 0x"$evm_xattr_new" test-file; then + echo "${RED}Cannot set security.evm of test-file${NORM}" + return "$FAIL" + fi + + if cat test-file &> /dev/null; then + echo "${RED}Read of test-file should not succeed (invalid security.evm)${NORM}" + return "$FAIL" + fi + + if ! setfattr -n security.evm -v 0x"$evm_xattr" test-file; then + echo "${RED}Cannot restore original security.evm of test-file${NORM}" + return "$FAIL" + fi + + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file after restoring correct security.evm${NORM}" + return "$FAIL" + fi + + # Repeat the test for ACLs. + if ! setfacl -m u::rwx test-file 2> /dev/null; then + echo "${RED}Cannot change system.posix_acl_access${NORM}" + return "$FAIL" + fi + + if cat test-file &> /dev/null; then + echo "${RED}Read of test-file should not succeed (invalid system.posix_acl_access)${NORM}" + return "$FAIL" + fi + + if ! setfacl -m u::rw test-file; then + echo "${RED}Cannot restore original system.posix_acl_access for test-file${NORM}" + return "$FAIL" + fi + + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file after restoring correct system.posix_acl_access${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_evm_revalidate() { + rm -f test-file +} + +# Requires: +# - evm: Introduce evm_hmac_disabled() to safely ignore verification errors +# - evm: Introduce evm_revalidate_status() +# - ima: Allow imasig requirement to be satisfied by EVM portable signatures +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# +# The purpose of this test is to verify that IMA manages files with an EVM +# portable signature similarly to those with an IMA signature: content can be +# written to new files after adding the signature and files can be accessed +# when the imasig requirement is specified in the IMA policy. +check_evm_portable_sig_ima_appraisal() { + local result + local xattr_orig + local xattr + local mode + local owner + + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if [ $((evm_value & EVM_INIT_X509)) -ne "$EVM_INIT_X509" ]; then + echo "${CYAN}EVM flag $EVM_INIT_X509 required${NORM}" + return "$SKIP" + fi + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! chmod 600 test-file; then + echo "${RED}Cannot change mode of test-file${NORM}" + return "$FAIL" + fi + + # We need to defer setting the correct owner, as there could be + # already an IMA policy rule preventing evmctl from reading the + # file to calculate the digest. + if ! evmctl sign -o -a sha256 --imahash --uid "$APPRAISE_DIGSIG_FOWNER" --key "$key_path" test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + if ! chown "$APPRAISE_DIGSIG_FOWNER" test-file; then + echo "${RED}Cannot change owner of test-file${NORM}" + return "$FAIL" + fi + + check_load_ima_rule "$APPRAISE_DIGSIG_RULE" + result=$? + if [ "$result" -ne "$OK" ]; then + return "$result" + fi + + # Ensure that a file with a portable signature satisfies the + # appraise_type=imasig requirement specified in the IMA policy. + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file${NORM}" + return "$FAIL" + fi + + # Even files with a portable signature should be considered as + # immutable by IMA. Write should fail. + if echo "test" 2> /dev/null >> test-file; then + echo "${RED}Write to test-file should not succeed (immutable metadata)${NORM}" + return "$FAIL" + fi + + if ! tar --xattrs-include=* -cf test-archive.tar test-file; then + echo "${RED}Cannot create archive with xattrs${NORM}" + return "$FAIL" + fi + + mkdir out + + # Appraisal of the new file, extracted by tar, should succeed + # not only if the new file has an IMA signature but also if + # it has a portable signature. + if ! tar --xattrs-include=* -xf test-archive.tar -C out; then + echo "${RED}Cannot extract archive with xattrs${NORM}" + return "$FAIL" + fi + + # Check if xattrs have been correctly set. + xattr_orig=$(get_xattr security.selinux test-file) + xattr=$(get_xattr security.selinux out/test-file) + if [ "$xattr" != "$xattr_orig" ]; then + echo "${RED}security.selinux mismatch between original and extracted file${NORM}" + return "$FAIL" + fi + + xattr_orig=$(get_xattr security.ima test-file) + xattr=$(get_xattr security.ima out/test-file) + if [ "$xattr" != "$xattr_orig" ]; then + echo "${RED}security.ima mismatch between original and extracted file${NORM}" + return "$FAIL" + fi + + xattr_orig=$(get_xattr security.evm test-file) + xattr=$(get_xattr security.evm out/test-file) + if [ "$xattr" != "$xattr_orig" ]; then + echo "${RED}security.evm mismatch between original and extracted file${NORM}" + return "$FAIL" + fi + + # Check if attrs have been correctly set. + owner=$(stat -c "%u" out/test-file) + if [ "$owner" != "$APPRAISE_DIGSIG_FOWNER" ]; then + echo "${RED}owner mismatch between original and extracted file${NORM}" + return "$FAIL" + fi + + mode=$(stat -c "%a" out/test-file) + if [ "$mode" != "600" ]; then + echo "${RED}mode mismatch between original and extracted file${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_evm_portable_sig_ima_appraisal() { + rm -f test-file test-archive.tar + rm -Rf out +} + +# Requires: +# - ima: Introduce template field evmsig and write to field sig as fallback +# - evm: Execute evm_inode_init_security() only when an HMAC key is loaded +# - ima: Don't remove security.ima if file must not be appraised +# +# The purpose of this test is to verify that the EVM portable signature is +# displayed in the measurement list. +check_evm_portable_sig_ima_measurement_list() { + local result + local evm_sig_fs + local evm_sig_list + + echo "Test: ${FUNCNAME[0]} (evm_value: $evm_value)" + + if ! echo "test" > test-file; then + echo "${RED}Cannot write test-file${NORM}" + return "$FAIL" + fi + + if ! chown "$MEASURE_FOWNER" test-file; then + echo "${RED}Cannot change owner of test-file${NORM}" + return "$FAIL" + fi + + if ! evmctl sign -o -a sha256 --imahash --key "$key_path" test-file &> /dev/null; then + echo "${RED}Cannot sign test-file${NORM}" + return "$FAIL" + fi + + check_load_ima_rule "$MEASURE_RULE" + result=$? + if [ "$result" -ne "$OK" ]; then + return "$result" + fi + + # Invalidate previous measurement to add new entry + touch test-file + + # Read the file to add it to the measurement list. + if ! cat test-file > /dev/null; then + echo "${RED}Cannot read test-file${NORM}" + return "$FAIL" + fi + + evm_sig_fs=$(get_xattr security.evm test-file) + if [ -z "$evm_sig_fs" ]; then + echo "${RED}security.evm not found${NORM}" + return "$FAIL" + fi + + # Search security.evm in the measurement list. + evm_sig_list=$(awk '$6 == "'"$evm_sig_fs"'"' < /sys/kernel/security/ima/ascii_runtime_measurements) + if [ -z "$evm_sig_list" ]; then + echo "${RED}security.evm mismatch (xattr != measurement list)${NORM}" + return "$FAIL" + fi + + return "$OK" +} + +cleanup_evm_portable_sig_ima_measurement_list() { + rm -f test-file +} + +# Run in the new environment if TST_ENV is set. +_run_env "$TST_KERNEL" "$PWD/$(basename "$0")" "TST_ENV=$TST_ENV TST_KERNEL=$TST_KERNEL PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH VERBOSE=$VERBOSE TST_EVM_CHANGE_MODE=$TST_EVM_CHANGE_MODE TST_KEY_PATH=$TST_KEY_PATH" + +# Run in the new environment if TST_ENV is set (skipped test). +_run_env "$TST_KERNEL" "$PWD/$(basename "$0")" "TST_ENV=$TST_ENV TST_KERNEL=$TST_KERNEL PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH VERBOSE=$VERBOSE TST_EVM_CHANGE_MODE=$TST_EVM_CHANGE_MODE TST_KEY_PATH=$TST_KEY_PATH TST_LIST=check_evm_revalidate" + +# Exit from the creator of the new environment. +_exit_env "$TST_KERNEL" + +# Mount filesystems in the new environment. +_init_env + +g_mountpoint=$(mktemp -d) +g_image=$(mktemp) + +if [ -z "$g_mountpoint" ]; then + echo "${RED}Mountpoint directory not created${NORM}" + exit "$FAIL" +fi + +if [ "$(whoami)" != "root" ]; then + echo "${CYAN}This script must be executed as root${NORM}" + exit "$SKIP" +fi + +if [ -n "$TST_KEY_PATH" ]; then + if [ "${TST_KEY_PATH:0:1}" != "/" ]; then + echo "${RED}Absolute path required for the signing key${NORM}" + exit "$FAIL" + fi + + if [ ! -f "$TST_KEY_PATH" ]; then + echo "${RED}Kernel signing key not found in $TST_KEY_PATH${NORM}" + exit "$FAIL" + fi + + key_path="$TST_KEY_PATH" +elif [ -f "$PWD/../signing_key.pem" ]; then + key_path="$PWD/../signing_key.pem" +elif [ -f "/lib/modules/$(uname -r)/source/certs/signing_key.pem" ]; then + key_path="/lib/modules/$(uname -r)/source/certs/signing_key.pem" +elif [ -f "/lib/modules/$(uname -r)/build/certs/signing_key.pem" ]; then + key_path="/lib/modules/$(uname -r)/build/certs/signing_key.pem" +else + echo "${CYAN}Kernel signing key not found${NORM}" + exit "$SKIP" +fi + +key_path_der=$(mktemp) + +if [ ! -f "/sys/kernel/security/evm" ]; then + echo "${CYAN}EVM support in the kernel disabled${NORM}" + exit "$SKIP" +fi + +# Assume that the EVM mode can be changed in a new environment. +if [ -n "$TST_ENV" ]; then + TST_EVM_CHANGE_MODE=1 +fi + +evm_value=$(cat /sys/kernel/security/evm) + +openssl x509 -in "$key_path" -out "$key_path_der" -outform der +if ! keyctl padd asymmetric pubkey %keyring:.ima < "$key_path_der" &> /dev/null; then + echo "${RED}Public key cannot be added to the IMA keyring${NORM}" + exit "$FAIL" +fi + +if ! dd if=/dev/zero of="$g_image" bs=1M count=20 &> /dev/null; then + echo "${RED}Cannot create test image${NORM}" + exit "$FAIL" +fi + +g_dev=$(losetup -f "$g_image" --show) +if [ -z "$g_dev" ]; then + echo "${RED}Cannot create loop device${NORM}" + exit "$FAIL" +fi + +if ! mkfs.ext4 -U "$IMA_UUID" -b 4096 "$g_dev" &> /dev/null; then + echo "${RED}Cannot format $g_dev${NORM}" + exit "$FAIL" +fi + +if ! mount -o i_version "$g_dev" "$g_mountpoint"; then + echo "${RED}Cannot mount loop device${NORM}" + exit "$FAIL" +fi + +if [ -n "$(command -v mount-idmapped 2> /dev/null)" ]; then + echo "Found mount-idmapped at $(command -v mount-idmapped), testing idmapped mounts" + g_mountpoint_idmapped=$(mktemp -d) + if ! mount-idmapped --map-mount b:"$METADATA_CHANGE_FOWNER":0:1 "$g_mountpoint" "$g_mountpoint_idmapped"; then + echo "${RED}mount-idmapped failed${NORM}" + exit "$FAIL" + fi +fi + +g_loop_mounted=1 +pushd "$g_mountpoint" > /dev/null || exit "$FAIL" + +expect_pass check_ima_sig_appraisal +cleanup_ima_sig_appraisal +expect_pass check_ima_sig_ima_measurement_list +cleanup_ima_sig_ima_measurement_list + +if [ "$(echo -e "$(uname -r)\n5.12" | sort -V | head -n 1)" != "5.12" ]; then + exit "$OK" +fi + +if [ $((evm_value & EVM_INIT_X509)) -ne "$EVM_INIT_X509" ] && [ "$TST_EVM_CHANGE_MODE" -eq 1 ]; then + if ! keyctl padd asymmetric pubkey %keyring:.evm < "$key_path_der" &> /dev/null; then + echo "${RED}Public key cannot be added to the EVM keyring${NORM}" + exit "$FAIL" + fi + + echo "$EVM_INIT_X509" > /sys/kernel/security/evm 2> /dev/null +fi + +if [ "$(expr index "$TST_LIST" "check_evm_revalidate")" -gt 0 ] && [ "$TST_EVM_CHANGE_MODE" -eq 1 ]; then + echo "$EVM_ALLOW_METADATA_WRITES" > /sys/kernel/security/evm 2> /dev/null +fi + +# We cannot determine from securityfs if EVM_SETUP_COMPLETE is set, so we set it unless EVM_ALLOW_METADATA_WRITES is set. +if [ $((evm_value & EVM_ALLOW_METADATA_WRITES)) -ne "$EVM_ALLOW_METADATA_WRITES" ] && [ "$TST_EVM_CHANGE_MODE" -eq 1 ]; then + echo "$EVM_SETUP_COMPLETE" > /sys/kernel/security/evm 2> /dev/null +fi + +evm_value=$(cat /sys/kernel/security/evm) + +expect_pass check_create_file +cleanup_create_file +expect_pass check_cp_preserve_xattrs +cleanup_cp_preserve_xattrs +expect_pass check_tar_extract_xattrs_different_owner +cleanup_tar_extract_xattrs_different_owner +expect_pass check_tar_extract_xattrs_same_owner +cleanup_tar_extract_xattrs_same_owner +expect_pass check_metadata_change +cleanup_metadata_change +expect_pass check_evm_revalidate +cleanup_evm_revalidate +expect_pass check_evm_portable_sig_ima_appraisal +cleanup_evm_portable_sig_ima_appraisal +expect_pass check_evm_portable_sig_ima_measurement_list +cleanup_evm_portable_sig_ima_measurement_list