On Fri, Apr 26, 2024 at 05:18:04PM -0500, Glenn Washburn wrote: > On Thu, 25 Apr 2024 16:02:06 +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. > > I'm glad we're getting a test with this feature. Its also unfortunate > that the test only works on the emu platform, which I suspect is tested > less. > Doing a full test with QEMU requires a more powerful testsuite such as openQA to manipulate the OS to seal the disk secret to the TPM of the VM. With grub-emu, at least we can test the TPM2 stack and verify the result of tpm2_key_protector_init.
> > > > 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> > > Signed-off-by: Gary Lin <g...@suse.com> > > --- > > Makefile.util.def | 6 + > > tests/tpm2_test.in | 308 +++++++++++++++++++++++++++++++++++++++ > > tests/util/grub-shell.in | 6 +- > > 3 files changed, 319 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..19cb21849 > > --- /dev/null > > +++ b/tests/tpm2_test.in > > @@ -0,0 +1,308 @@ > > +#! @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 > > Should be using "command" here (and in the other tests) as the external > command "which" may not be available. So perhaps this line instead: > > if ! command -v cryptsetup >/dev/null 2>&1; then > > Same for subsequent usages of which. > Got it. Will fix it in v14. > > + 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 ! 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 > > Why exit 20 here instead of 99 like other tests? It looks like you want > to differentiate different error conditions, but this will make the > test show as error, not hard error, which is what it is. > I followed tests/util/grub-shell-luks-tester.in to create the test directory and didn't notice the difference between 20 and 99. Will fix it in v14. > > + > > +disksize=20M > > + > > +luksfile=$tpm2testdir/luks.disk > > Use brace around variable name here as is done below, and also add > braces for unbraced uses below. > Alright, I'll go through the whole script again to make sure all variables are braced. > > +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" > > + > > +# 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 > > This test currently only tests the return code of grub's cryptmount. It > would be better to do a more end-to-end test where $vtext is written to > the LUKS device and then read back to verify encryption and decryption > works. When doing the luks tests, I made a fat filesystem and a test > file with $vtext as the contents of the file. I think that's more than > necessary here. The annoying thing is that grub has limited tools for > getting specific bytes from a device. Instead of doing the verification > check in grub, grub could just output the first block of the grub > crypto device (eg. "cat (crypto0)+1"). Then check in the grub-shell > output as is currently done. As the check is currently done. This will > mean that a new line should be added after $vtext to make > sure $vtext is the only output on the line. > I'll try to modify the script to write vtext into the block. > > + > > +# 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 > > + exit $ret > > +fi > > I don't see the purpose of the above 4 lines. The shell is run with -e, > so it will exit on any unprocessed errors. The swtpm command if it > errors is unprocessed (no || ...). So will not use the above 4 lines, > but should do the equivalent internally. If swtpm has a success return > code, the above 4 lines don't matter. Am I missing something? > Will remove those 4 lines in v14. > > + > > +# Wait for tpm2 chardev > > +wait=3 > > +while [ "${wait}" -gt 0 ]; do > > + tpm2dev=$(grep "New TPM device" ${tpm2log} | cut -d' ' -f 4) > > + if [ -n "${tpm2dev}" ] && [ -c "${tpm2dev}" ]; then > > The "[ -n "${tpm2dev}" ] &&" is superfluous as "[ -c '' ]" will never > be true. > It's added when I merged another check into this loop. But yes, it's not necessary. > > + break > > + fi > > + sleep 1 > > + ((wait--)) > > +done > > +if [ "$wait" -le 0 ]; then > > Perhaps what you really want is '[ -z "${tpm2dev}" ]'. > Thanks! This is certainly a better check. > > + echo "TPM device did not appear." > > + exit QUIT > > +fi > > + > > +# Export the TCTI variable for tpm2-tools > > +export TPM2TOOLS_TCTI="device:${tpm2dev}" > > + > > +# Extend PCR 0 > > +tpm2_pcrextend 0:sha256=$(sha256sum <<< "test0" | cut -d ' ' -f 1) > > The '<<<' only works in bash (as far as I know), not sh. Let's not use > it and instead use: echo -n "test0" | sha256sum. Same for other such > uses. > No problem. Will update in v14. > > +ret=$? > > +if [ "$ret" -ne 0 ]; then > > + exit $ret > > +fi > > Again, this seems to be superfluous. > Ditto > > + > > +# Extend PCR 1 > > +tpm2_pcrextend 1:sha256=$(sha256sum <<< "test1" | cut -d ' ' -f 1) > > +ret=$? > > +if [ "$ret" -ne 0 ]; then > > + exit $ret > > +fi > > + > > +tpm2_seal_unseal() { > > + local srk_alg="$1" > > "local" is not available in sh. If these variables clash with others, > please rename them. Either way don't use local. In principle I think > local should be used where we can, but we're more interested in > portability. > Will remove "local" and check the variable names again. > > + local handle_type="$2" > > + local srk_test="$3" > > + > > + local grub_srk_alg=${srk_alg} > > + > > + local extra_opt="" > > + local extra_grub_opt="" > > + > > + local 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 > > + fi > > This is not superfluous because of the error message, but it is > unreachable code. To do what you want, put the "ret=$?" as " || ret=$?" > on the grub-protect command. > Got it. > > + > > + # Flip the asymmetric algorithm in grub.cfg to trigger fallback SRKs > > + if [ "${srk_test}" == "fallback_srk" ]; then > > + if [[ "${srk_alg}" == RSA* ]]; then > > '[[' is another bashism, unless there is a binary for it (I think > busybox does have). So let's not use it. Instead do: > > if [ -z "${srk_alg##RSA*}" ]; then > > Likewise for other uses. > Will remove those '[[' in v14. > > + grub_srk_alg="ECC" > > + elif [[ "${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 > > + echo "${vtext}" > > +fi > > +EOF > > + > > + # Test TPM unsealing with the same PCR > > + ${grubshell} --timeout=$timeout --grub-emu-opts="-t ${tpm2dev}" < > > ${tpm2testdir}/testcase.cfg > ${testoutput} > > + ret=$? > > This ret=$? is equivalent to ret=0, and so is really not needed. I > think you want to '||' it with the grubshell command. > Will update the command. > > + > > + # 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 > > + exit 1 > > + fi > > + else > > + echo "grub-emu exited with error: $ret" >&2 > > + exit $ret > > + fi > > +} > > + > > +tpm2_seal_unseal_nv() { > > + local persistent_handle="0x81000000" > > + local primary_file=${tpm2testdir}/primary.ctx > > + local session_file=${tpm2testdir}/session.dat > > + local policy_file=${tpm2testdir}/policy.dat > > + local keypub_file=${tpm2testdir}/key.pub > > + local keypriv_file=${tpm2testdir}/key.priv > > + local name_file=${tpm2testdir}/sealing.name > > + local 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 > > + > > + # 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 > > + echo "${vtext}" > > +fi > > +EOF > > + > > + # Test TPM unsealing with the same PCR > > + ${grubshell} --timeout=$timeout --grub-emu-opts="-t ${tpm2dev}" < > > ${tpm2testdir}/testcase.cfg > ${testoutput} > > + ret=$? > > Same comment as in function above. > Ditto > > + > > + # Remove the persistent handle > > + grub-protect \ > > + --tpm2-device=${tpm2dev} \ > > + --protector=tpm2 \ > > + --action=remove \ > > + --tpm2-srk=${persistent_handle} \ > > + --tpm2-evict > > + > > + 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 > > +} > > + > > +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..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" ;; > > I'm on the fence on this. '--qemu-opts' could just be reused, although > it might be a little confusing, as it would be misnamed. It would be > nice to think of a way to combine --qemu-opts and --grub-emu-opts into > one appropriately named argument (perhaps just --emu-opts?). They are > mutually exclusive in the sense that they will both never be in effect > for a given target. Also, prefixing with 'grub' goes against the > existing naming scheme, so regardless should not be done. > Then I'd just append ${qemuopts} to the grub-emu command since renaming the '--qemu-opts' will affect other test scripts. This has to be done in a separate patch set. Gary Lin > Glenn > > > --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 _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel