For TPM 2.0 TSS stack, the TCG2 command sending function is the only difference between the a QEMU instance and grub-emu. To test TPM key unsealing with a QEMU instance, it requires an extra OS image to invoke grub-protect to seal the LUKS key, not only a simple grub-shell rescue CD image. On the other hand, grub-emu can share the emulated TPM 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 TPM device is created by swtpm_cuse and PCR 0 and 1 are extended. The LUKS password is sealed by grub-protect against PCR 0 and 1. The last step is to launch grub-emu 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> Signed-off-by: Gary Lin <g...@suse.com> --- Makefile.util.def | 6 ++ tests/tpm2_test.in | 179 +++++++++++++++++++++++++++++++++++++++ tests/util/grub-shell.in | 6 +- 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tests/tpm2_test.in diff --git a/Makefile.util.def b/Makefile.util.def index a0a3e2cd5..77bbdd453 100644 --- a/Makefile.util.def +++ b/Makefile.util.def @@ -1279,6 +1279,12 @@ script = { common = tests/asn1_test.in; }; +script = { + testcase = native; + name = tpm2_test; + common = tests/tpm2_test.in; +}; + program = { testcase = native; name = example_unit_test; diff --git a/tests/tpm2_test.in b/tests/tpm2_test.in new file mode 100644 index 000000000..c874888d6 --- /dev/null +++ b/tests/tpm2_test.in @@ -0,0 +1,179 @@ +#! @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 ! which cryptsetup >/dev/null 2>&1; then + echo "cryptsetup not installed; cannot test tpm2." + exit 99 +fi + +if ! which swtpm >/dev/null 2>&1; then + echo "swtpm not installed; cannot test tpm2." + exit 99 +fi + +if ! which 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 20 + +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" + +tpm2devname="vtpm-test0" +tpm2statedir=${tpm2testdir}/tpm +tpm2dev="/dev/${tpm2devname}" + +sealedkey=${tpm2testdir}/sealed.tpm + +timeout=20 + +testoutput=$tpm2testdir/testoutput + +vtext="TEST VERIFIED" + +# Create the password file +echo -n "top secret" > ${lukskeyfile} + +# Setup LUKS2 image +truncate -s ${disksize} ${luksfile} || exit 21 +cryptsetup luksFormat -q ${csopt} ${luksfile} ${lukskeyfile} || exit 22 + +# Shutdown the swtpm instance on exit +cleanup() { + if [ -e "$tpm2dev" ]; then + swtpm_ioctl -s ${tpm2dev} + fi + if [ "${RET:-1}" -eq 0 ]; then + rm -rf "$tpm2testdir" || : + fi +} +trap cleanup EXIT INT TERM KILL QUIT + +# Shutdown the previous swtpm instance if exists +if [ -c "${tpm2dev}" ]; then + swtpm_ioctl -s ${tpm2dev} +fi + +# Create the swtpm cuse instannce +swtpm_cuse -n ${tpm2devname} --tpm2 --tpmstate dir=${tpm2statedir} +ret=$? +if [ "$ret" -ne 0 ]; then + exit $ret +fi + +# Initialize swtpm device +swtpm_ioctl -i ${tpm2dev} +ret=$? +if [ "$ret" -ne 0 ]; then + exit $ret +fi + +# Export the TCTI variable for tpm2-tools +export TPM2TOOLS_TCTI="device:${tpm2dev}" + +# Send the startup command +tpm2_startup -c +ret=$? +if [ "$ret" -ne 0 ]; then + exit $ret +fi + +# Extend PCR 0 +tpm2_pcrextend 0:sha256=$(sha256sum <<< "test0" | cut -d ' ' -f 1) +ret=$? +if [ "$ret" -ne 0 ]; then + exit $ret +fi + +# Extend PCR 1 +tpm2_pcrextend 1:sha256=$(sha256sum <<< "test1" | cut -d ' ' -f 1) +ret=$? +if [ "$ret" -ne 0 ]; then + exit $ret +fi + +# Seal the password with grub-protect +grub-protect \ + --tpm2-device=${tpm2dev} \ + --action=add \ + --protector=tpm2 \ + --tpm2key \ + --tpm2-asymmetric=RSA2048 \ + --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" + exit 1 +fi + +# Write the TPM unsealing script +cat > ${tpm2testdir}/testcase.cfg <<EOF +loopback luks (host)${luksfile} +tpm2_key_protector_init -T (host)${sealedkey} +if cryptomount -a --protector tpm2; then + echo "${vtext}" +fi +EOF + +# Test TPM unsealing with the same PCR +${grubshell} --timeout=$timeout --grub-emu-opts="-t ${tpm2dev}" < ${tpm2testdir}/testcase.cfg > ${testoutput} || ret=$? + +if [ "$ret" -eq 0 ]; then + if ! grep -q "^${vtext}$" "$testoutput"; then + echo "error: test not verified [`cat $testoutput`]" >&2 + exit 1 + fi +else + echo "grub-emu exited with error: $ret" >&2 + exit $ret +fi + +exit $ret diff --git a/tests/util/grub-shell.in b/tests/util/grub-shell.in index 496e1bab3..f8642543d 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= +grubemuopts= serial_port=com0 serial_null= halt_cmd=halt @@ -281,6 +282,9 @@ for option in "$@"; do --qemu-opts=*) qs=`echo "$option" | sed -e 's/--qemu-opts=//'` qemuopts="$qemuopts $qs" ;; + --grub-emu-opts=*) + qs=`echo "$option" | sed -e 's/--grub-emu-opts=//'` + grubemuopts="$grubemuopts $qs" ;; --disk=*) dsk=`echo "$option" | sed -e 's/--disk=//'` if [ ${grub_modinfo_platform} = emu ]; then @@ -576,7 +580,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" ${grubemuopts} 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