On Mon, May 06, 2024 at 02:09:12PM -0500, Glenn Washburn wrote: > On Fri, 3 May 2024 14:48:56 +0800 > Gary Lin <g...@suse.com> wrote: > > > For the tpm2 module, the TCG2 command submission 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, rather than 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. > > > > 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 > > against 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> > > --- > > Makefile.util.def | 6 + > > tests/tpm2_test.in | 306 +++++++++++++++++++++++++++++++++++++++ > > tests/util/grub-shell.in | 6 +- > > 3 files changed, 317 insertions(+), 1 deletion(-) > > create mode 100644 tests/tpm2_test.in > > > > diff --git a/Makefile.util.def b/Makefile.util.def > > index 40bfe713d..8d4c53a03 100644 > > --- a/Makefile.util.def > > +++ b/Makefile.util.def > > @@ -1281,6 +1281,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..75c042274 > > --- /dev/null > > +++ b/tests/tpm2_test.in > > @@ -0,0 +1,306 @@ > > +#! @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 > > Braces in quoted variables at the end of the quoted string don't need > braces. But no need to change it back. > > > + 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 > > Its a good idea to have variables that can be user provided and thus > have spaces to be quoted. tpm2testdir and any variable contructed from > it fall in that category. Note that putting a variable in braces > unquoted, does not make implicit quoting. > Ok, will quote those variables.
> > +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 21 > > +cryptsetup luksFormat -q ${csopt} ${luksfile} ${lukskeyfile} || exit 22 > > You should never be exiting with an explicit value other than 77 or 99. > Everything else indicates a real failure in the test. I think you got > confused by reading grub-shell-luks-tester and seeing that it does not > follow that rule. The reason for that is because it is expected that > scripts using it will convert those exit codes to 99. Take a look at > grub_cmd_cryptomount and you will see that in the _testcase function. > So the big difference in this script and grub-shell-luks-tester is that > this script is run directly by the test harness, but > grub-shell-luks-tester is not. Please change the rest of the exit > codes to 99. > Thanks for the explanation. I'll change the exit codes to 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 23 > > +echo "${vtext}" | dd of=${luksdev} conv=notrunc status=none > > I would think `echo "${vtext}" | cat > "${luksdev}"` would be sufficient > here. I'm curious if I'm wrong about that. Certainly "conv=notrunc" > seems superfluous as ${luksdev} is a device so no truncation happens > anyway. If true, then as is it may be confusing to readers. In general, > I think its better to use simpler commands when available (in this case > no need for arg parsing). > Oh, I copied the code from my test script which works on a blank file and forgot to change it. The dd command is certainly unnecessary here. > > +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" > > + exit ${ret} > > This should also exit with 99, as this is not an actual failure of the > test, but a failure to setup the test. You could put the $ret in the > message echo'd, which would be helpful to someone debugging this > failure. > Copy that. > > +fi > > + > > +# Wait for tpm2 chardev > > +wait_count=3 > > +for count in `seq 1 ${wait_count}`; do > > Considering that this may run in a virtual machine where things will be > going much slower, perhaps instead use: > > `seq 1 ${tpm2timeout}` > > And above after setting tpm2log, have a line like: > > tpm2timeout=${GRUB_TEST_SWTPM_DEFAULT_TIMEOUT:-3} > > This way this timeout can be overridden if needed. > Good idea. I'll change the timeout variable. > > + sleep 1 > > + > > + tpm2dev=$(grep "New TPM device" ${tpm2log} | cut -d' ' -f 4) > > + if [ -c "${tpm2dev}" ]; then > > + wait_count=0 > > + break > > + fi > > +done > > +if [ "${wait_count}" -ne 0 ]; then > > I thought we agreed that it would be better to use [ -z "$tmp2log" ] > here. In which case, the wait_count variable seem unnecessary. > Ah, I forgot to mention that I ran into a situation that tpm2dev was fetched but the char device was actually not ready, so I'd like to make sure that tpm2dev is really available. > > + echo "TPM device did not appear." > > + exit QUIT > > I believe this invalid usage of the exit command (I get errors using > this in both bash and sh). Regardless we should be exiting with 99 here. > Will fix it. > > +fi > > + > > +# 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) > > + > > +# Extend PCR 1 > > +tpm2_pcrextend 1:sha256=$(echo "test1" | sha256sum | cut -d ' ' -f 1) > > Both of the above commands should have " || exit 99" at the end to make > these hard errors. > Got it > > + > > +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" > > + > > + 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" > > + exit 1 > > The error code should be 99 as this is a hard error (ie a failure in > the setup). Perhaps this should be a "return" instead of "exit" > statement. This allows the caller to handle this. Since currently the > caller does nothing, using either one is effectively the same, but I > think return is better. Same for other uses of exit in functions. > Originally the return status was handled outside this function and it ended up with several duplicate code. To improve the readability, I moved the error handling code inside the function and made it exit directly. I'll flip 'exit' back to 'return' and handle the return status with a shorter code. > > + 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 > ${tpm2testdir}/testcase.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}" < > > ${tpm2testdir}/testcase.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 > > Probably should " || :" here to ignore a potential failure? > Got it. > > + fi > > + > > + if [ "${ret}" -eq 0 ]; then > > + if ! grep -q "^${vtext}$" "${testoutput}"; then > > + echo "error: test not verified [`cat ${testoutput}`]" >&2 > > + exit 1 > > return as mentioned above. > > > + fi > > + else > > + echo "grub-emu exited with error: ${ret}" >&2 > > + exit ${ret} > > ditto > > > + fi > > +} > > + > > +tpm2_seal_unseal_nv() { > > + persistent_handle="0x81000000" > > + 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 -C o -g sha256 -G ecc -c ${primary_file} -Q > > + tpm2_flushcontext -t > > + > > + # Create the policy object > > + tpm2_startauthsession -S ${session_file} > > + tpm2_policypcr -S ${session_file} -l sha256:0,1 -L ${policy_file} -Q > > + tpm2_flushcontext ${session_file} > > + > > + # Seal the key into TPM > > + tpm2_create -C ${primary_file} -u ${keypub_file} -r ${keypriv_file} -L > > ${policy_file} -i ${lukskeyfile} -Q > > + tpm2_flushcontext -t > > + tpm2_load -C ${primary_file} -u ${keypub_file} -r ${keypriv_file} -n > > ${name_file} -c ${sealing_ctx_file} > > + tpm2_flushcontext -t > > + tpm2_evictcontrol -C o -c ${sealing_ctx_file} ${persistent_handle} -Q > > All of these can fail and their failure should cause a 99 error code. > Perhaps wrap them in a subshell and do " || return 99". > Will move them into another shell script. > > + > > + # Write the TPM unsealing script > > + cat > ${tpm2testdir}/testcase.cfg <<EOF > > +loopback luks (host)${luksfile} > > +tpm2_key_protector_init --mode=nv --nvindex=${persistent_handle} --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}" < > > ${tpm2testdir}/testcase.cfg > ${testoutput} || ret=$? > > + > > + # Remove the persistent handle > > + grub-protect \ > > + --tpm2-device=${tpm2dev} \ > > + --protector=tpm2 \ > > + --action=remove \ > > + --tpm2-srk=${persistent_handle} \ > > + --tpm2-evict > > ||:, as mentioned above > > > + > > + if [ "${ret}" -eq 0 ]; then > > + if ! grep -q "^${vtext}$" "${testoutput}"; then > > + echo "error: test not verified [`cat ${testoutput}`]" >&2 > > + exit 1 > > return > > > + fi > > + else > > + echo "grub-emu exited with error: ${ret}" >&2 > > + exit ${ret} > > return > Will fix the return code. Gary Lin > Glenn > > > + fi > > +} > > + > > +tpm2_seal_unseal default transient no_fallback_srk > > + > > +tpm2_seal_unseal RSA transient no_fallback_srk > > + > > +tpm2_seal_unseal ECC transient no_fallback_srk > > + > > +tpm2_seal_unseal RSA persistent no_fallback_srk > > + > > +tpm2_seal_unseal ECC persistent no_fallback_srk > > + > > +tpm2_seal_unseal RSA transient fallback_srk > > + > > +tpm2_seal_unseal ECC transient fallback_srk > > + > > +tpm2_seal_unseal_nv > > + > > +exit 0 > > diff --git a/tests/util/grub-shell.in b/tests/util/grub-shell.in > > index 496e1bab3..6847d869e 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 > > @@ -281,6 +282,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 > > @@ -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" ${emuopts} > > EOF > > else > > cat >"$work_directory/run.sh" <<EOF _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel