For the tpm2_key_protector module, the TCG2 command submission function is the only difference between a QEMU instance and grub-emu. To test TPM2 key unsealing with a QEMU instance, it requires an extra OS image to invoke grub-protect to seal the LUKS key, rather than a simple grub-shell rescue CD image. On the other hand, grub-emu can share the emulated TPM2 device with the host, so that we can seal the LUKS key on host and test key unsealing with grub-emu.
This test script firstly creates a simple LUKS image to be loaded as a loopback device in grub-emu. Then an emulated TPM2 device is created by "swtpm chardev" and PCR 0 and 1 are extended. There are several test cases in the script to test various settings. Each test case uses grub-protect or tpm2-tools to seal the LUKS password with PCR 0 and PCR 1. Then grub-emu is launched to load the LUKS image, try to mount the image with tpm2_key_protector_init and cryptomount, and verify the result. Based on the idea from Michael Chang. Cc: Michael Chang <mch...@suse.com> Cc: Stefan Berger <stef...@linux.ibm.com> Cc: Glenn Washburn <developm...@efficientek.com> Signed-off-by: Gary Lin <g...@suse.com> Reviewed-by: Daniel Kiper <daniel.ki...@oracle.com> --- Makefile.util.def | 6 + tests/tpm2_key_protector_test.in | 389 +++++++++++++++++++++++++++++++ tests/util/grub-shell.in | 6 +- 3 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 tests/tpm2_key_protector_test.in diff --git a/Makefile.util.def b/Makefile.util.def index 074c0aff7..038253b37 100644 --- a/Makefile.util.def +++ b/Makefile.util.def @@ -1290,6 +1290,12 @@ script = { common = tests/asn1_test.in; }; +script = { + testcase = native; + name = tpm2_key_protector_test; + common = tests/tpm2_key_protector_test.in; +}; + program = { testcase = native; name = example_unit_test; diff --git a/tests/tpm2_key_protector_test.in b/tests/tpm2_key_protector_test.in new file mode 100644 index 000000000..a92e5f498 --- /dev/null +++ b/tests/tpm2_key_protector_test.in @@ -0,0 +1,389 @@ +#! @BUILD_SHEBANG@ -e + +# Test GRUBs ability to unseal a LUKS key with TPM 2.0 +# Copyright (C) 2024 Free Software Foundation, Inc. +# +# GRUB 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 3 of the License, or +# (at your option) any later version. +# +# GRUB 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 GRUB. If not, see <http://www.gnu.org/licenses/>. + +grubshell=@builddir@/grub-shell + +. "@builddir@/grub-core/modinfo.sh" + +if [ x${grub_modinfo_platform} != xemu ]; then + exit 77 +fi + +builddir="@builddir@" + +# Force build directory components +PATH="${builddir}:${PATH}" +export PATH + +if [ "x${EUID}" = "x" ] ; then + EUID=`id -u` +fi + +if [ "${EUID}" != 0 ] ; then + echo "not root; cannot test tpm2." + exit 99 +fi + +if ! command -v cryptsetup >/dev/null 2>&1; then + echo "cryptsetup not installed; cannot test tpm2." + exit 99 +fi + +if ! grep -q tpm_vtpm_proxy /proc/modules && ! modprobe tpm_vtpm_proxy; then + echo "no tpm_vtpm_proxy support; cannot test tpm2." + exit 99 +fi + +if ! command -v swtpm >/dev/null 2>&1; then + echo "swtpm not installed; cannot test tpm2." + exit 99 +fi + +if ! command -v tpm2_startup >/dev/null 2>&1; then + echo "tpm2-tools not installed; cannot test tpm2." + exit 99 +fi + +tpm2testdir="`mktemp -d "${TMPDIR:-/tmp}/$(basename "$0").XXXXXXXXXX"`" || exit 99 + +disksize=20M + +luksfile=${tpm2testdir}/luks.disk +lukskeyfile=${tpm2testdir}/password.txt + +# Choose a low iteration number to reduce the time to decrypt the disk +csopt="--type luks2 --pbkdf pbkdf2 --iter-time 1000" + +tpm2statedir=${tpm2testdir}/tpm +tpm2ctrl=${tpm2statedir}/ctrl +tpm2log=${tpm2statedir}/logfile + +sealedkey=${tpm2testdir}/sealed.tpm + +timeout=20 + +testoutput=${tpm2testdir}/testoutput + +vtext="TEST VERIFIED" + +ret=0 + +# Create the password file +echo -n "top secret" > "${lukskeyfile}" + +# Setup LUKS2 image +truncate -s ${disksize} "${luksfile}" || exit 99 +cryptsetup luksFormat -q ${csopt} "${luksfile}" "${lukskeyfile}" || exit 99 + +# Write vtext into the first block of the LUKS2 image +luksdev=/dev/mapper/`basename "${tpm2testdir}"` +cryptsetup open --key-file "${lukskeyfile}" "${luksfile}" `basename "${luksdev}"` || exit 99 +echo "${vtext}" > "${luksdev}" +cryptsetup close "${luksdev}" + +# Shutdown the swtpm instance on exit +cleanup() { + RET=$? + if [ -e "${tpm2ctrl}" ]; then + swtpm_ioctl -s --unix "${tpm2ctrl}" + fi + if [ "${RET}" -eq 0 ]; then + rm -rf "$tpm2testdir" || : + fi +} +trap cleanup EXIT INT TERM KILL QUIT + +mkdir -p "${tpm2statedir}" + +# Create the swtpm chardev instance +swtpm chardev --vtpm-proxy --tpmstate dir="${tpm2statedir}" \ + --tpm2 --ctrl type=unixio,path="${tpm2ctrl}" \ + --flags startup-clear --daemon > "${tpm2log}" || ret=$? +if [ "${ret}" -ne 0 ]; then + echo "Failed to start swtpm chardev: ${ret}" >&2 + exit 99 +fi + +# Wait for tpm2 chardev +tpm2timeout=${GRUB_TEST_SWTPM_DEFAULT_TIMEOUT:-3} +for count in `seq 1 ${tpm2timeout}`; do + sleep 1 + + tpm2dev=$(grep "New TPM device" "${tpm2log}" | cut -d' ' -f 4) + if [ -c "${tpm2dev}" ]; then + break + elif [ "${count}" -eq "${tpm2timeout}" ]; then + echo "TPM device did not appear." >&2 + exit 99 + fi +done + +# Export the TCTI variable for tpm2-tools +export TPM2TOOLS_TCTI="device:${tpm2dev}" + +# Extend PCR 0 +tpm2_pcrextend 0:sha256=$(echo "test0" | sha256sum | cut -d ' ' -f 1) || exit 99 + +# Extend PCR 1 +tpm2_pcrextend 1:sha256=$(echo "test1" | sha256sum | cut -d ' ' -f 1) || exit 99 + +tpm2_seal_unseal() { + srk_alg="$1" + handle_type="$2" + srk_test="$3" + + grub_srk_alg=${srk_alg} + + extra_opt="" + extra_grub_opt="" + + persistent_handle="0x81000000" + + grub_cfg=${tpm2testdir}/testcase.cfg + + if [ "${handle_type}" = "persistent" ]; then + extra_opt="--tpm2-srk=${persistent_handle}" + fi + + if [ "${srk_alg}" != "default" ]; then + extra_opt="${extra_opt} --tpm2-asymmetric=${srk_alg}" + fi + + # Seal the password with grub-protect + grub-protect ${extra_opt} \ + --tpm2-device="${tpm2dev}" \ + --action=add \ + --protector=tpm2 \ + --tpm2key \ + --tpm2-bank=sha256 \ + --tpm2-pcrs=0,1 \ + --tpm2-keyfile="${lukskeyfile}" \ + --tpm2-outfile="${sealedkey}" || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to seal the secret key: ${ret}" >&2 + return 99 + fi + + # Flip the asymmetric algorithm in grub.cfg to trigger fallback SRKs + if [ "${srk_test}" = "fallback_srk" ]; then + if [ -z "${srk_alg##RSA*}" ]; then + grub_srk_alg="ECC" + elif [ -z "${srk_alg##ECC*}" ]; then + grub_srk_alg="RSA" + fi + fi + + if [ "${grub_srk_alg}" != "default" ] && [ "${handle_type}" != "persistent" ]; then + extra_grub_opt="-a ${grub_srk_alg}" + fi + + # Write the TPM unsealing script + cat > "${grub_cfg}" <<EOF +loopback luks (host)${luksfile} +tpm2_key_protector_init -T (host)${sealedkey} ${extra_grub_opt} +if cryptomount -a --protector tpm2; then + cat (crypto0)+1 +fi +EOF + + # Test TPM unsealing with the same PCR + ${grubshell} --timeout=${timeout} --emu-opts="-t ${tpm2dev}" < "${grub_cfg}" > "${testoutput}" || ret=$? + + # Remove the persistent handle + if [ "${handle_type}" = "persistent" ]; then + grub-protect \ + --tpm2-device="${tpm2dev}" \ + --protector=tpm2 \ + --action=remove \ + --tpm2-srk=${persistent_handle} \ + --tpm2-evict || : + fi + + if [ "${ret}" -eq 0 ]; then + if ! grep -q "^${vtext}$" "${testoutput}"; then + echo "error: test not verified [`cat ${testoutput}`]" >&2 + return 1 + fi + else + echo "grub-emu exited with error: ${ret}" >&2 + return 99 + fi +} + +tpm2_seal_nv () { + keyfile="$1" + nv_index="$2" + pcr_list="$3" + + primary_file=${tpm2testdir}/primary.ctx + session_file=${tpm2testdir}/session.dat + policy_file=${tpm2testdir}/policy.dat + keypub_file=${tpm2testdir}/key.pub + keypriv_file=${tpm2testdir}/key.priv + name_file=${tpm2testdir}/sealing.name + sealing_ctx_file=${tpm2testdir}/sealing.ctx + + # Since we don't run a resource manager on our swtpm instance, it has + # to flush the transient handles after tpm2_createprimary, tpm2_create + # and tpm2_load to avoid the potential out-of-memory (0x902) errors. + # Ref: https://github.com/tpm2-software/tpm2-tools/issues/1338#issuecomment-469689398 + + # Create the primary object + tpm2_createprimary -Q -C o -g sha256 -G ecc -c "${primary_file}" || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to create the primary object: ${ret}" >&2 + return 1 + fi + tpm2_flushcontext -t || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to flush the transient handles: ${ret}" >&2 + return 1 + fi + + # Create the policy object + tpm2_startauthsession -S "${session_file}" || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to start auth session: ${ret}" >&2 + return 1 + fi + tpm2_policypcr -Q -S "${session_file}" -l "${pcr_list}" -L "${policy_file}" || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to create the policy object: ${ret}" >&2 + return 1 + fi + tpm2_flushcontext "${session_file}" || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to flush the transient handles: ${ret}" >&2 + return 1 + fi + + # Seal the key into TPM + tpm2_create -Q \ + -C "${primary_file}" \ + -u "${keypub_file}" \ + -r "${keypriv_file}" \ + -L "${policy_file}" \ + -i "${keyfile}" || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to seal \"${keyfile}\": ${ret}" >&2 + return 1 + fi + tpm2_flushcontext -t || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to flush the transient handles: ${ret}" >&2 + return 1 + fi + + tpm2_load -Q \ + -C "${primary_file}" \ + -u "${keypub_file}" \ + -r "${keypriv_file}" \ + -n "${name_file}" \ + -c "${sealing_ctx_file}" || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to load the sealed key into TPM: ${ret}" >&2 + return 1 + fi + tpm2_flushcontext -t || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to flush the transient handles: ${ret}" >&2 + return 1 + fi + + tpm2_evictcontrol -Q -C o -c "${sealing_ctx_file}" ${nv_index} || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to store the sealed key into ${nv_index}: ${ret}" >&2 + return 1 + fi + + return 0 +} + +tpm2_seal_unseal_nv() { + nv_index="0x81000000" + pcr_list="sha256:0,1" + + grub_cfg=${tpm2testdir}/testcase.cfg + + # Seal the key into a NV index guarded by PCR 0 and 1 + tpm2_seal_nv "${lukskeyfile}" ${nv_index} ${pcr_list} || ret=$? + if [ "${ret}" -ne 0 ]; then + echo "Failed to seal the secret key into ${nv_index}" >&2 + return 99 + fi + + # Write the TPM unsealing script + cat > ${grub_cfg} <<EOF +loopback luks (host)${luksfile} +tpm2_key_protector_init --mode=nv --nvindex=${nv_index} --pcrs=0,1 +if cryptomount -a --protector tpm2; then + cat (crypto0)+1 +fi +EOF + + # Test TPM unsealing with the same PCR + ${grubshell} --timeout=${timeout} --emu-opts="-t ${tpm2dev}" < "${grub_cfg}" > "${testoutput}" || ret=$? + + # Remove the object from the NV index + tpm2_evictcontrol -Q -C o -c "${nv_index}" || : + + if [ "${ret}" -eq 0 ]; then + if ! grep -q "^${vtext}$" "${testoutput}"; then + echo "error: test not verified [`cat ${testoutput}`]" >&2 + return 1 + fi + else + echo "grub-emu exited with error: ${ret}" >&2 + return 99 + fi +} + +# Testcases for SRK mode +declare -a srktests=() +srktests+=("default transient no_fallback_srk") +srktests+=("RSA transient no_fallback_srk") +srktests+=("ECC transient no_fallback_srk") +srktests+=("RSA persistent no_fallback_srk") +srktests+=("ECC persistent no_fallback_srk") +srktests+=("RSA transient fallback_srk") +srktests+=("ECC transient fallback_srk") + +for i in "${!srktests[@]}"; do + tpm2_seal_unseal ${srktests[$i]} || ret=$? + if [ "${ret}" -eq 0 ]; then + echo "TPM2 [${srktests[$i]}]: PASS" + elif [ "${ret}" -eq 1 ]; then + echo "TPM2 [${srktests[$i]}]: FAIL" + else + echo "Unexpected failure [${srktests[$i]}]" >&2 + exit ${ret} + fi +done + +# Testcase for NV index mode +tpm2_seal_unseal_nv || ret=$? +if [ "${ret}" -eq 0 ]; then + echo "TPM2 [NV Index]: PASS" +elif [ "${ret}" -eq 1 ]; then + echo "TPM2 [NV Index]: FAIL" +else + echo "Unexpected failure [NV index]" >&2 + exit ${ret} +fi + +exit 0 diff --git a/tests/util/grub-shell.in b/tests/util/grub-shell.in index ae5f711fe..15c5f45a5 100644 --- a/tests/util/grub-shell.in +++ b/tests/util/grub-shell.in @@ -75,6 +75,7 @@ work_directory=${WORKDIR:-`mktemp -d "${TMPDIR:-/tmp}/grub-shell.XXXXXXXXXX"`} | . "${builddir}/grub-core/modinfo.sh" qemuopts= +emuopts= serial_port=com0 serial_null= halt_cmd=halt @@ -376,6 +377,9 @@ for option in "$@"; do --qemu-opts=*) qs=`echo "$option" | sed -e 's/--qemu-opts=//'` qemuopts="$qemuopts $qs" ;; + --emu-opts=*) + qs=`echo "$option" | sed -e 's/--emu-opts=//'` + emuopts="$emuopts $qs" ;; --disk=*) dsk=`echo "$option" | sed -e 's/--disk=//'` if [ ${grub_modinfo_platform} = emu ]; then @@ -674,7 +678,7 @@ elif [ x$boot = xemu ]; then cat >"$work_directory/run.sh" <<EOF #! @BUILD_SHEBANG@ SDIR=\$(realpath -e \${0%/*}) -exec "$(realpath -e "${builddir}")/grub-core/grub-emu" -m "\$SDIR/${device_map##*/}" --memdisk "\$SDIR/${roottar##*/}" -r memdisk -d "/boot/grub" +exec "$(realpath -e "${builddir}")/grub-core/grub-emu" -m "\$SDIR/${device_map##*/}" --memdisk "\$SDIR/${roottar##*/}" -r memdisk -d "/boot/grub" ${emuopts} EOF else cat >"$work_directory/run.sh" <<EOF -- 2.35.3 _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel