grub-devel.gnu.org archive mirror
 help / color / mirror / Atom feed
From: Gary Lin via Grub-devel <grub-devel@gnu.org>
To: The development of GNU GRUB <grub-devel@gnu.org>
Cc: Gary Lin <glin@suse.com>,
	Hernan Gatta <hegatta@linux.microsoft.com>,
	Daniel Axtens <dja@axtens.net>,
	Daniel Kiper <daniel.kiper@oracle.com>,
	shkhisti@microsoft.com, jaskaran.khurana@microsoft.com,
	christopher.co@microsoft.com, daniel.mihai@microsoft.com,
	jaredz@redhat.com, development@efficientek.com,
	jejb@linux.ibm.com, mchang@suse.com, patrick.colp@oracle.com,
	Stefan Berger <stefanb@linux.ibm.com>
Subject: [PATCH v16 20/20] tests: Add tpm2_test
Date: Wed, 15 May 2024 13:07:12 +0800	[thread overview]
Message-ID: <20240515050712.21495-21-glin@suse.com> (raw)
In-Reply-To: <20240515050712.21495-1-glin@suse.com>

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 <mchang@suse.com>
Cc: Stefan Berger <stefanb@linux.ibm.com>
Cc: Glenn Washburn <development@efficientek.com>
Signed-off-by: Gary Lin <glin@suse.com>
---
 Makefile.util.def        |   6 +
 tests/tpm2_test.in       | 389 +++++++++++++++++++++++++++++++++++++++
 tests/util/grub-shell.in |   6 +-
 3 files changed, 400 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..aecd1a5a3
--- /dev/null
+++ b/tests/tpm2_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 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
-- 
2.35.3


_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

      parent reply	other threads:[~2024-05-15  5:10 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-05-15  5:06 [PATCH v16 00/20] Automatic Disk Unlock with TPM2 Gary Lin via Grub-devel
2024-05-15  5:06 ` [PATCH v16 01/20] posix_wrap: tweaks in preparation for libtasn1 Gary Lin via Grub-devel
2024-05-15  5:06 ` [PATCH v16 02/20] libtasn1: import libtasn1-4.19.0 Gary Lin via Grub-devel
2024-05-15  5:06 ` [PATCH v16 03/20] libtasn1: disable code not needed in grub Gary Lin via Grub-devel
2024-05-15  5:06 ` [PATCH v16 04/20] libtasn1: changes for grub compatibility Gary Lin via Grub-devel
2024-05-15  5:06 ` [PATCH v16 05/20] libtasn1: fix the potential buffer overrun Gary Lin via Grub-devel
2024-05-15  5:06 ` [PATCH v16 06/20] libtasn1: compile into asn1 module Gary Lin via Grub-devel
2024-05-15  5:06 ` [PATCH v16 07/20] asn1_test: test module for libtasn1 Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 08/20] libtasn1: Add the documentation Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 09/20] key_protector: Add key protectors framework Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 10/20] tpm2: Add TPM Software Stack (TSS) Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 11/20] key_protector: Add TPM2 Key Protector Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 12/20] cryptodisk: Support key protectors Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 13/20] util/grub-protect: Add new tool Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 14/20] tpm2: Support authorized policy Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 15/20] tpm2: Implement NV index Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 16/20] cryptodisk: Fallback to passphrase Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 17/20] cryptodisk: wipe out the cached keys from protectors Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 18/20] diskfilter: look up cryptodisk devices first Gary Lin via Grub-devel
2024-05-15  5:07 ` [PATCH v16 19/20] tpm2: Enable tpm2 module for grub-emu Gary Lin via Grub-devel
2024-05-15  5:07 ` Gary Lin via Grub-devel [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240515050712.21495-21-glin@suse.com \
    --to=grub-devel@gnu.org \
    --cc=christopher.co@microsoft.com \
    --cc=daniel.kiper@oracle.com \
    --cc=daniel.mihai@microsoft.com \
    --cc=development@efficientek.com \
    --cc=dja@axtens.net \
    --cc=glin@suse.com \
    --cc=hegatta@linux.microsoft.com \
    --cc=jaredz@redhat.com \
    --cc=jaskaran.khurana@microsoft.com \
    --cc=jejb@linux.ibm.com \
    --cc=mchang@suse.com \
    --cc=patrick.colp@oracle.com \
    --cc=shkhisti@microsoft.com \
    --cc=stefanb@linux.ibm.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).