]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
cryptsetup: measure via the pcrextend varlink service 43109/head
authorPaul Meyer <katexochen0@gmail.com>
Wed, 22 Jul 2026 06:28:53 +0000 (08:28 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Tue, 28 Jul 2026 12:59:02 +0000 (14:59 +0200)
Measure the volume key and unlock keyslot through io.systemd.PCRExtend
instead of driving the TPM directly via tpm2-util, matching how the
verity and imds measurements already work.

Bank selection and the TPM context now live entirely in
systemd-pcrextend. As a result the tpm2-measure-bank= crypttab option
can no longer be honored per volume and is now a deprecated no-op.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
NEWS
man/crypttab.xml
src/cryptsetup/cryptsetup.c
src/shared/pcrextend-util.c
src/shared/pcrextend-util.h
test/units/TEST-70-TPM2.cryptsetup-measure.sh [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index 2c760d481614963ee7244dad22552252b263319e..380ee3f75ffbe929a38b92552a5e4034fc948f6e 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -27,6 +27,12 @@ CHANGES WITH 262:
           older systemd versions, however systemd continues to accept TPM-sealed
           credentials that were created before this change.
 
+        * The "tpm2-measure-bank=" crypttab option of systemd-cryptsetup is
+          now deprecated and has no effect. Volume key measurements are
+          performed via varlink call to systemd-pcrextend, which automatically
+          selects all suitable TPM2 PCR banks; the banks can no longer be
+          restricted per volume.
+
         * The legacy socket for controlling systemd-udevd has been removed, and
           udevadm now unconditionally uses Varlink IPC. The legacy UNIX socket
           /run/udev/control, which is sometimes used to check whether udevd is
index 38ba4ceafbe80aead3025bc0866a3573a0a36ad5..2e93e11d26ec7301c8ebaa5f117aa43e5d59b717 100644 (file)
       <varlistentry>
         <term><option>tpm2-measure-bank=</option></term>
 
-        <listitem><para>Selects one or more TPM2 PCR banks to measure the volume key into, as configured with
-        <option>tpm2-measure-pcr=</option> above. Multiple banks may be specified, separated by a colon
-        character. If not specified, automatically determines available and used banks. Expects a message
-        digest name (e.g. <literal>sha1</literal>, <literal>sha256</literal>, …) as argument, to identify the
-        bank.</para>
+        <listitem><para>This option is deprecated and has no effect since version 262. All suitable PCR
+        banks are used.</para>
 
         <xi:include href="version-info.xml" xpointer="v253"/></listitem>
       </varlistentry>
index dcd830e44341f1298de0c3ede8f22bf4cd904129..5641681861fb30badb05bb1255b985d859581383 100644 (file)
@@ -7,12 +7,10 @@
 #include "sd-device.h"
 #include "sd-event.h"
 #include "sd-json.h"
-#include "sd-messages.h"
 
 #include "alloc-util.h"
 #include "ask-password-api.h"
 #include "build.h"
-#include "crypto-util.h"
 #include "cryptsetup-fido2.h"
 #include "cryptsetup-keyfile.h"
 #include "cryptsetup-pkcs11.h"
@@ -40,6 +38,7 @@
 #include "options.h"
 #include "parse-util.h"
 #include "path-util.h"
+#include "pcrextend-util.h"
 #include "pkcs11-util.h"
 #include "pretty-print.h"
 #include "process-util.h"
@@ -123,7 +122,6 @@ static bool arg_tpm2_pin = false;
 static char *arg_tpm2_pcrlock = NULL;
 static usec_t arg_token_timeout_usec = 30*USEC_PER_SEC;
 static unsigned arg_tpm2_measure_pcr = UINT_MAX; /* This and the following field is about measuring the unlocked volume key to the local TPM */
-static char **arg_tpm2_measure_banks = NULL;
 static char *arg_tpm2_measure_keyslot_nvpcr = NULL;
 static char *arg_link_keyring = NULL;
 static char *arg_link_key_type = NULL;
@@ -140,7 +138,6 @@ STATIC_DESTRUCTOR_REGISTER(arg_fido2_cid, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_fido2_rp_id, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep);
-STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_banks, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_keyslot_nvpcr, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_link_keyring, freep);
@@ -537,30 +534,9 @@ static int parse_one_option(const char *option) {
 
         } else if ((val = startswith(option, "tpm2-measure-bank="))) {
 
-#if HAVE_OPENSSL
-                _cleanup_strv_free_ char **l = NULL;
-
-                r = dlopen_libcrypto(LOG_ERR);
-                if (r < 0)
-                        return r;
-
-                l = strv_split(val, ":");
-                if (!l)
-                        return log_oom();
-
-                STRV_FOREACH(i, l) {
-                        const EVP_MD *implementation;
-
-                        implementation = sym_EVP_get_digestbyname(*i);
-                        if (!implementation)
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", val);
-
-                        if (strv_extend(&arg_tpm2_measure_banks, sym_EVP_MD_get0_name(implementation)) < 0)
-                                return log_oom();
-                }
-#else
-                log_error("Build lacks OpenSSL support, cannot measure to PCR banks, ignoring: %s", option);
-#endif
+                /* Deprecated: the PCR banks to measure into are now chosen by systemd-pcrextend
+                 * (it extends all suitable banks). Kept for compatibility, but ignored. */
+                log_warning("The tpm2-measure-bank= option is deprecated and has no effect, ignoring.");
 
         } else if ((val = startswith(option, "tpm2-measure-keyslot-nvpcr="))) {
 
@@ -1041,57 +1017,21 @@ static int measure_volume_key(
                 return 0;
         }
 
-#if HAVE_TPM2
-        _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL;
-        r = tpm2_context_new_or_warn(arg_tpm2_device, &c);
-        if (r < 0)
-                return r;
-
-        _cleanup_strv_free_ char **l = NULL;
-        if (strv_isempty(arg_tpm2_measure_banks)) {
-                r = tpm2_get_good_pcr_banks_strv(c, UINT32_C(1) << arg_tpm2_measure_pcr, &l);
-                if (r < 0)
-                        return log_error_errno(r, "Could not verify pcr banks: %m");
-        }
-
-        _cleanup_free_ char *joined = strv_join(l ?: arg_tpm2_measure_banks, ", ");
-        if (!joined)
-                return log_oom();
-
-        /* Note: we don't directly measure the volume key, it might be a security problem to send an
-         * unprotected direct hash of the secret volume key over the wire to the TPM. Hence let's instead
-         * send a HMAC signature instead. */
-
         _cleanup_free_ char *prefix = NULL;
 
         /* Note: what is extended to the SHA256 bank here must match the expected hash of 'fixate-volume-key='
          * calculated by cryptsetup_get_volume_key_id(). */
         r = cryptsetup_get_volume_key_prefix(cd, name, &prefix);
-        if (r)
-                return log_error_errno(r, "Could not verify pcr banks: %m");
-
-        r = tpm2_pcr_extend_bytes(
-                        c,
-                        /* banks= */ l ?: arg_tpm2_measure_banks,
-                        /* pcr_index = */ arg_tpm2_measure_pcr,
-                        /* data = */ &IOVEC_MAKE_STRING(prefix),
-                        /* secret = */ &IOVEC_MAKE(volume_key, volume_key_size),
-                        /* event_type = */ TPM2_EVENT_VOLUME_KEY,
-                        /* description = */ prefix);
         if (r < 0)
-                return log_error_errno(r, "Could not extend PCR: %m");
+                return log_error_errno(r, "Failed to get volume key prefix: %m");
 
-        log_struct(LOG_INFO,
-                   LOG_MESSAGE_ID(SD_MESSAGE_TPM_PCR_EXTEND_STR),
-                   LOG_MESSAGE("Successfully extended PCR index %u with '%s' and volume key (banks %s).", arg_tpm2_measure_pcr, prefix, joined),
-                   LOG_ITEM("MEASURING=%s", prefix),
-                   LOG_ITEM("PCR=%u", arg_tpm2_measure_pcr),
-                   LOG_ITEM("BANKS=%s", joined));
+        /* Pass the volume key as HMAC secret. pcrextend extends HMAC(volume_key, prefix),
+         * never a bare hash. Matches cryptsetup_get_volume_key_id(). */
+        r = pcrextend_pcr_now(arg_tpm2_measure_pcr, prefix, &IOVEC_MAKE(volume_key, volume_key_size), "volume_key");
+        if (r < 0)
+                return log_error_errno(r, "Could not extend PCR: %m");
 
         return 0;
-#else
-        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring volume key.");
-#endif
 }
 
 static int measure_keyslot(
@@ -1100,9 +1040,8 @@ static int measure_keyslot(
                 const char *mechanism,
                 int keyslot) {
 
-#if HAVE_TPM2
         int r;
-#endif
+
         assert(cd);
         assert(name);
 
@@ -1111,7 +1050,6 @@ static int measure_keyslot(
                 return 0;
         }
 
-#if HAVE_TPM2
         r = efi_measured_os(LOG_WARNING);
         if (r < 0)
                 return r;
@@ -1120,11 +1058,6 @@ static int measure_keyslot(
                 return 0;
         }
 
-        _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL;
-        r = tpm2_context_new_or_warn(arg_tpm2_device, &c);
-        if (r < 0)
-                return r;
-
         _cleanup_free_ char *escaped = NULL;
         escaped = xescape(name, ":"); /* avoid ambiguity around ":" once we join things below */
         if (!escaped)
@@ -1139,27 +1072,11 @@ static int measure_keyslot(
         if (!s)
                 return log_oom();
 
-        r = tpm2_nvpcr_extend_bytes(
-                        c,
-                        /* session= */ NULL,
-                        arg_tpm2_measure_keyslot_nvpcr,
-                        &IOVEC_MAKE_STRING(s),
-                        /* secret= */ NULL,
-                        TPM2_EVENT_KEYSLOT,
-                        s);
+        r = pcrextend_nvpcr_now(arg_tpm2_measure_keyslot_nvpcr, s, "keyslot");
         if (r < 0)
                 return log_error_errno(r, "Could not extend NvPCR: %m");
 
-        log_struct(LOG_INFO,
-                   "MESSAGE_ID=" SD_MESSAGE_TPM_NVPCR_EXTEND_STR,
-                   LOG_MESSAGE("Successfully extended NvPCR index '%s' with '%s'.", arg_tpm2_measure_keyslot_nvpcr, s),
-                   "MEASURING=%s", s,
-                   "NVPCR=%s", arg_tpm2_measure_keyslot_nvpcr);
-
         return 0;
-#else
-        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring keyslot.");
-#endif
 }
 
 static int log_external_activation(int r, const char *volume) {
index fc1e3f2ff7020bce4789beabf6d2ec2f7d69c5e2..158699a73d456ba84899ad05f6f24b18aebd073c 100644 (file)
@@ -25,7 +25,7 @@
 #include "tpm2-pcr.h"
 #include "user-record.h"
 
-static int pcrextend_pcr_now(unsigned pcr, const char *word, const struct iovec *secret, const char *event) {
+int pcrextend_pcr_now(unsigned pcr, const char *word, const struct iovec *secret, const char *event) {
 
 #if HAVE_TPM2
         int r;
@@ -78,11 +78,14 @@ static int pcrextend_pcr_now(unsigned pcr, const char *word, const struct iovec
 #endif
 }
 
-static int pcrextend_nvpcr_now(const char *nvpcr, const char *word, const char *event) {
+int pcrextend_nvpcr_now(const char *nvpcr, const char *word, const char *event) {
 
 #if HAVE_TPM2
         int r;
 
+        assert(nvpcr);
+        assert(word);
+
         _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
         r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.PCRExtend");
         if (r < 0)
index a81d8b19700b8ce6f0e43a5a77db3b7333f8c134..de1b1d49afb5e54902421807ab3b301434eed5d7 100644 (file)
@@ -5,6 +5,9 @@
 
 #include "forward.h"
 
+int pcrextend_pcr_now(unsigned pcr, const char *word, const struct iovec *secret, const char *event);
+int pcrextend_nvpcr_now(const char *nvpcr, const char *word, const char *event);
+
 int pcrextend_file_system_word(const char *path, char **ret, char **ret_normalized_path);
 int pcrextend_machine_id_word(char **ret);
 int pcrextend_product_id_word(char **ret);
diff --git a/test/units/TEST-70-TPM2.cryptsetup-measure.sh b/test/units/TEST-70-TPM2.cryptsetup-measure.sh
new file mode 100755 (executable)
index 0000000..45d49dc
--- /dev/null
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+# Tests that systemd-cryptsetup's tpm2-measure-pcr= option measures the volume key
+# through the io.systemd.PCRExtend Varlink service.
+#
+# Note: the tpm2-measure-keyslot-nvpcr= (NvPCR) path is intentionally not covered here. NvPCRs use a
+# signed PCR policy that must be anchored first (see TEST-70-TPM2.nvpcr.sh), which is heavy setup
+# orthogonal to what this test checks; the NvPCR extend-via-pcrextend path is already covered there.
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+export SYSTEMD_LOG_LEVEL=debug
+
+if ! command -v systemd-cryptsetup >/dev/null || ! tpm_has_pcr sha256 15; then
+    echo "systemd-cryptsetup or PCR 15 (sha256) not available, skipping cryptsetup measurement test"
+    exit 0
+fi
+
+IMAGE=""
+
+at_exit() {
+    if [[ $? -ne 0 ]]; then
+        # Dump the event log on failure, to ease debugging
+        jq --seq --slurp </run/log/systemd/tpm2-measure.log || :
+    fi
+
+    set +e
+
+    systemd-cryptsetup detach test-volume
+    rm -rf /run/systemd/system/systemd-pcrextend.socket.d
+    systemctl daemon-reload
+    systemctl restart systemd-pcrextend.socket
+    rm -f "${IMAGE:-}" /tmp/passphrase /tmp/vk /tmp/vk-hmac.bin \
+          /tmp/oldpcr15 /tmp/newpcr15 /tmp/measure-bank.log
+}
+
+trap at_exit EXIT
+
+# The socket carries ConditionSecurity=measured-os, which does not hold in the
+# test VM, so the Varlink service would never activate. Drop the condition so
+# systemd-cryptsetup can reach it, exactly like TEST-70-TPM2.pcrextend.sh does.
+mkdir -p /run/systemd/system/systemd-pcrextend.socket.d
+cat >/run/systemd/system/systemd-pcrextend.socket.d/50-no-condition.conf <<EOF
+[Unit]
+ConditionSecurity=
+EOF
+systemctl daemon-reload
+systemctl restart systemd-pcrextend.socket
+
+# Prepare a fresh LUKS2 volume unlocked by a passphrase keyfile
+IMAGE="$(mktemp /tmp/systemd-cryptsetup-measure-XXX.IMAGE)"
+truncate -s 20M "$IMAGE"
+echo -n passphrase >/tmp/passphrase
+chmod 0600 /tmp/passphrase
+cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$IMAGE" /tmp/passphrase
+
+UUID="$(cryptsetup luksUUID "$IMAGE")"
+
+# Extract the raw volume key so we can recompute the expected HMAC below. The
+# volume key is measured as HMAC(volume_key, prefix) rather than a plain hash
+# (see cryptsetup_get_volume_key_prefix()/cryptsetup_get_volume_key_id()).
+cryptsetup luksDump -q --dump-volume-key --volume-key-file=/tmp/vk --key-file=/tmp/passphrase "$IMAGE"
+VK_HEX="$(od -An -v -tx1 /tmp/vk | tr -d ' \n')"
+
+# prefix = "cryptsetup:" <escaped volume name> ":" <LUKS UUID>; "test-volume"
+# contains no ':' so it is unescaped.
+PREFIX="cryptsetup:test-volume:$UUID"
+
+# Expected measured digest for the sha256 bank: HMAC-SHA256(volume_key, prefix)
+printf '%s' "$PREFIX" | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:$VK_HEX" >/tmp/vk-hmac.bin
+DIGEST_EXPECTED="$(od -An -v -tx1 /tmp/vk-hmac.bin | tr -d ' \n')"
+
+# Remember the current event log length so we can index the record we add.
+# (json-seq: each record is RS(0x1e)-prefixed, LF-suffixed.) The log is created
+# lazily on the first measurement, and this subtest may be the first thing to
+# measure (e.g. when run in isolation), so treat an absent log as zero records.
+RECORD_COUNT=0
+if [[ -f /run/log/systemd/tpm2-measure.log ]]; then
+    RECORD_COUNT="$(jq --seq --slurp '. | length' </run/log/systemd/tpm2-measure.log | tr -d '\036')"
+fi
+
+tpm2_pcrread sha256:15 -Q -o /tmp/oldpcr15
+
+# Activate with measurement enabled. Unlock is via the keyfile (no TPM unlock),
+# to prove measurement is independent of the unlock mechanism.
+SYSTEMD_FORCE_MEASURE=1 systemd-cryptsetup attach test-volume "$IMAGE" /tmp/passphrase \
+    tpm2-measure-pcr=15,headless=1
+
+tpm2_pcrread sha256:15 -Q -o /tmp/newpcr15
+
+# The TPM PCR 15 must have been extended with the HMAC:
+#   new = sha256(old || HMAC(volume_key, prefix))
+diff /tmp/newpcr15 \
+     <(cat /tmp/oldpcr15 /tmp/vk-hmac.bin | openssl dgst -binary -sha256)
+
+# The volume-key measurement is the record we just added (the first one past RECORD_COUNT).
+test "$(jq --seq --slurp ".[$RECORD_COUNT].pcr" </run/log/systemd/tpm2-measure.log)" == "$(printf '\x1e15')"
+DIGEST_CURRENT="$(jq --seq --slurp --raw-output ".[$RECORD_COUNT].digests[] | select(.hashAlg == \"sha256\").digest" </run/log/systemd/tpm2-measure.log)"
+test "$DIGEST_CURRENT" == "$DIGEST_EXPECTED"
+
+systemd-cryptsetup detach test-volume
+
+# The deprecated tpm2-measure-bank= option must warn but not fail.
+SYSTEMD_FORCE_MEASURE=1 systemd-cryptsetup attach test-volume "$IMAGE" /tmp/passphrase \
+    tpm2-measure-pcr=15,tpm2-measure-bank=sha256,headless=1 |& tee /tmp/measure-bank.log
+systemd-cryptsetup detach test-volume
+grep -i "deprecated" /tmp/measure-bank.log >/dev/null