From d84d17853e120d2074d7f1a973fc965c6f7200f9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 16:29:19 +0100 Subject: [PATCH 01/11] cryptsetup: move default choice of nvpcr for keyslots from generator into cryptsetup Let's pick the default NvPCR name to use inside of cryptsetup itself, instead of in the generator. I think this is the better choice, since it means the default can also be used if the regular verittab generator is used instead of the gpt-auto generator. --- man/crypttab.xml | 12 ++++++------ src/cryptsetup/cryptsetup.c | 8 +++++--- src/gpt-auto-generator/gpt-auto-generator.c | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/man/crypttab.xml b/man/crypttab.xml index f0d0ead947..d57823f88a 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -945,12 +945,12 @@ Controls whether to measure information about the used LUKS unlock keyslot to a TPM2 - non-volatile index (nvindex in PCR mode). If set to to an empty string (which is the default) no TPM2 - nvindex extension is done, otherwise keyslot information is measured to an nvindex of the specified - name, which is allocated if needed. It is recommended to set this to cryptsetup to - enable this logic. The slot index and the used unlock mechanism (i.e. tpm2, - fido2, pkcs11) is measured along with the activated volume name - and its UUID. + non-volatile index (nvindex in PCR mode). Takes a boolean argument, or an NvPCR name. If set to false + or an empty string (which is the default) no TPM2 nvindex extension is done, otherwise keyslot + information is measured to an nvindex of the specified name, which is allocated if needed. If set to + true the recommended default of cryptsetup is selected as NvPCR. The slot index + and the used unlock mechanism (i.e. tpm2, fido2, + pkcs11) are measured along with the activated volume name and its UUID. diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index f715494418..d3e4ce3747 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -557,12 +557,14 @@ static int parse_one_option(const char *option) { } else if ((val = startswith(option, "tpm2-measure-keyslot-nvpcr="))) { - if (isempty(val)) { + r = isempty(val) ? false : parse_boolean(val); + if (r == 0) { arg_tpm2_measure_keyslot_nvpcr = mfree(arg_tpm2_measure_keyslot_nvpcr); return 0; } - - if (!tpm2_nvpcr_name_is_valid(val)) { + if (r > 0) + val = "cryptsetup"; + else if (!tpm2_nvpcr_name_is_valid(val)) { log_warning("Invalid NvPCR name, ignoring: %s", option); return 0; } diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 5a4e5d101e..9e20e23abf 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -127,7 +127,7 @@ static int add_cryptsetup( * assignment, under the assumption that people who are fine to use sd-stub with its PCR * assignments are also OK with our PCR 15 use here. */ if (r > 0) - if (!strextend_with_separator(&options, ",", "tpm2-measure-pcr=yes,tpm2-measure-keyslot-nvpcr=cryptsetup")) + if (!strextend_with_separator(&options, ",", "tpm2-measure-pcr=yes,tpm2-measure-keyslot-nvpcr=yes")) return log_oom(); if (r == 0) log_debug("Will not measure volume key of volume '%s', not booted via systemd-stub with measurements enabled.", id); From be36f69c90d530ea12173eaa17be87685a7f799d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 14:28:02 +0100 Subject: [PATCH 02/11] pcrextend: allow access to the userspace event type field when measuring something It think we should move most measurements out of the individual tools requesting them and into the pcrextend service via Varlink, so that fewer components require access to the TPM. This only works however, if we can actually write full-blown event log records via this mechanism, and for that we still were missing access to the userspace event type we insert into the event log. Add that. --- src/pcrextend/pcrextend.c | 17 +++++++++++------ src/shared/varlink-io.systemd.PCRExtend.c | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index d887da924f..c80bf376fd 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -369,6 +369,7 @@ typedef struct MethodExtendParameters { const char *nvpcr; const char *text; struct iovec data; + Tpm2UserspaceEventType event_type; } MethodExtendParameters; static void method_extend_parameters_done(MethodExtendParameters *p) { @@ -377,17 +378,21 @@ static void method_extend_parameters_done(MethodExtendParameters *p) { iovec_done(&p->data); } +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_tpm2_userspace_event_type, Tpm2UserspaceEventType, tpm2_userspace_event_type_from_string); + static int vl_method_extend(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { static const sd_json_dispatch_field dispatch_table[] = { - { "pcr", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(MethodExtendParameters, pcr), 0 }, - { "nvpcr", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodExtendParameters, nvpcr), 0 }, - { "text", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 }, - { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodExtendParameters, data), 0 }, + { "pcr", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(MethodExtendParameters, pcr), 0 }, + { "nvpcr", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodExtendParameters, nvpcr), 0 }, + { "text", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 }, + { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodExtendParameters, data), 0 }, + { "eventType", SD_JSON_VARIANT_STRING, json_dispatch_tpm2_userspace_event_type, offsetof(MethodExtendParameters, event_type), 0 }, {} }; _cleanup_(method_extend_parameters_done) MethodExtendParameters p = { .pcr = UINT_MAX, + .event_type = _TPM2_USERSPACE_EVENT_TYPE_INVALID, }; int r; @@ -424,11 +429,11 @@ static int vl_method_extend(sd_varlink *link, sd_json_variant *parameters, sd_va return sd_varlink_error_invalid_parameter_name(link, "text"); if (p.nvpcr) { - r = extend_nvpcr_now(p.nvpcr, extend_iovec->iov_base, extend_iovec->iov_len, _TPM2_USERSPACE_EVENT_TYPE_INVALID); + r = extend_nvpcr_now(p.nvpcr, extend_iovec->iov_base, extend_iovec->iov_len, p.event_type); if (r == -ENOENT) return sd_varlink_error(link, "io.systemd.PCRExtend.NoSuchNvPCR", NULL); } else - r = extend_pcr_now(p.pcr, extend_iovec->iov_base, extend_iovec->iov_len, _TPM2_USERSPACE_EVENT_TYPE_INVALID); + r = extend_pcr_now(p.pcr, extend_iovec->iov_base, extend_iovec->iov_len, p.event_type); if (r < 0) return r; diff --git a/src/shared/varlink-io.systemd.PCRExtend.c b/src/shared/varlink-io.systemd.PCRExtend.c index 87783f1ee9..9179668d7d 100644 --- a/src/shared/varlink-io.systemd.PCRExtend.c +++ b/src/shared/varlink-io.systemd.PCRExtend.c @@ -2,6 +2,17 @@ #include "varlink-io.systemd.PCRExtend.h" +static SD_VARLINK_DEFINE_ENUM_TYPE( + EventType, + SD_VARLINK_DEFINE_ENUM_VALUE(phase), + SD_VARLINK_DEFINE_ENUM_VALUE(filesystem), + SD_VARLINK_DEFINE_ENUM_VALUE(volume_key), + SD_VARLINK_DEFINE_ENUM_VALUE(machine_id), + SD_VARLINK_DEFINE_ENUM_VALUE(product_id), + SD_VARLINK_DEFINE_ENUM_VALUE(keyslot), + SD_VARLINK_DEFINE_ENUM_VALUE(nvpcr_init), + SD_VARLINK_DEFINE_ENUM_VALUE(nvpcr_separator)); + static SD_VARLINK_DEFINE_METHOD( Extend, SD_VARLINK_FIELD_COMMENT("PCR number to extend, in range of 0…23. Either this or 'nvpcr' must be specified, not both, not neither."), @@ -11,7 +22,9 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_FIELD_COMMENT("Text string to measure. (Specify either this, or the 'data' field below, not both)"), SD_VARLINK_DEFINE_INPUT(text, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Binary data to measure, encoded in Base64. (Specify either this, or the 'text' field above, not both)"), - SD_VARLINK_DEFINE_INPUT(data, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); + SD_VARLINK_DEFINE_INPUT(data, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Event type to include in the (userspace) event log). This is optional, and mostly for debugging."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(eventType, EventType, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_ERROR(NoSuchNvPCR); @@ -21,4 +34,6 @@ SD_VARLINK_DEFINE_INTERFACE( SD_VARLINK_INTERFACE_COMMENT("TPM PCR Extension APIs"), SD_VARLINK_SYMBOL_COMMENT("Measure some text or binary data into a PCR"), &vl_method_Extend, + SD_VARLINK_SYMBOL_COMMENT("Event type to store in event log"), + &vl_type_EventType, &vl_error_NoSuchNvPCR); From 0a8f393c64f3716883cf57a7a28bf7dae03e06b3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 13:10:05 +0100 Subject: [PATCH 03/11] cryptsetup: extend HAVE_TPM2 conditioning to cover more If TPM2 support is off, any check for TPM2 support will fail, hence we can just suppress it. --- src/cryptsetup/cryptsetup.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index d3e4ce3747..dbd0b14d7b 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -1097,8 +1097,9 @@ static int measure_keyslot( const char *mechanism, int keyslot) { +#if HAVE_TPM2 int r; - +#endif assert(cd); assert(name); @@ -1107,6 +1108,7 @@ static int measure_keyslot( return 0; } +#if HAVE_TPM2 r = efi_measured_uki(LOG_WARNING); if (r < 0) return r; @@ -1115,7 +1117,6 @@ static int measure_keyslot( 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) From 8485bba53d82a0852318fa4ce3bd76ac291cd418 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 13:13:00 +0100 Subject: [PATCH 04/11] tpm2-setup: introduce nvpcr for measuring Verity images I thnk it's crucial we start to measure Verity images as we activate them, so that the event log has a full trace of the compisition of the system. hence let's introduce a new NvPCR for this purpse, under the name "verity". --- src/tpm2-setup/meson.build | 4 ++-- src/tpm2-setup/nvpcr/verity.nvpcr.in | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 src/tpm2-setup/nvpcr/verity.nvpcr.in diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index 9814fe1005..a862e7239c 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -31,12 +31,12 @@ executables += [ 'HAVE_TPM2', ], }, - ] if conf.get('ENABLE_BOOTLOADER') == 1 and conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_TPM2') == 1 nvpcrs = [ 'cryptsetup', - 'hardware' ] + 'hardware', + 'verity'] foreach n : nvpcrs custom_target( input : 'nvpcr/' + n + '.nvpcr.in', diff --git a/src/tpm2-setup/nvpcr/verity.nvpcr.in b/src/tpm2-setup/nvpcr/verity.nvpcr.in new file mode 100644 index 0000000000..b4fb62bd76 --- /dev/null +++ b/src/tpm2-setup/nvpcr/verity.nvpcr.in @@ -0,0 +1,5 @@ +{ + "name" : "verity", + "algorithm" : "sha256", + "nvIndex" : {{TPM2_NVPCR_BASE + 2}} +} From 3f31c8ff46537b5193a7289d1b40406263260806 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 13:10:47 +0100 Subject: [PATCH 05/11] pkcs7-util: add helpers for extracting signer info from PKCS7 signatures Once we start measuring Verity volumes as we activate them we want to include information about the signature keys used, so that we can have distinct ones for confext and for sysext and ther purposes and thus have a cryptograpically protected hint about the kind of image we have activated in the event log. Ideally we'd measure a fingerprint of the signing certificate here, but we don't have that here typically (as PKCS7 signatures used here typically do not embed that), hence use the next best thing: the issuer name and the serial number. --- src/shared/meson.build | 1 + src/shared/pkcs7-util.c | 84 ++++++++++++++++++++++++++++++++++++++ src/shared/pkcs7-util.h | 16 ++++++++ src/test/meson.build | 5 +++ src/test/test-pkcs7-util.c | 55 +++++++++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 src/shared/pkcs7-util.c create mode 100644 src/shared/pkcs7-util.h create mode 100644 src/test/test-pkcs7-util.c diff --git a/src/shared/meson.build b/src/shared/meson.build index b9630bef65..8b49ad9a52 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -152,6 +152,7 @@ shared_sources = files( 'pcre2-util.c', 'pcrextend-util.c', 'pe-binary.c', + 'pkcs7-util.c', 'pkcs11-util.c', 'plymouth-util.c', 'polkit-agent.c', diff --git a/src/shared/pkcs7-util.c b/src/shared/pkcs7-util.c new file mode 100644 index 0000000000..0a0e102adf --- /dev/null +++ b/src/shared/pkcs7-util.c @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "openssl-util.h" +#include "pkcs7-util.h" +#include "log.h" + +#define SIGNERS_MAX 32 + +static void signer_done(Signer *signer) { + assert(signer); + + iovec_done(&signer->issuer); + iovec_done(&signer->serial); +} + +void signer_free_many(Signer *signers, size_t n) { + assert(signers || n == 0); + + FOREACH_ARRAY(i, signers, n) + signer_done(i); + + free(signers); +} + +int pkcs7_extract_signers( + const struct iovec *sig, + Signer **ret_signers, + size_t *ret_n_signers) { + + assert(ret_signers); + assert(ret_n_signers); + + if (!iovec_is_set(sig)) + return -EBADMSG; + +#if HAVE_OPENSSL + const unsigned char *d = sig->iov_base; + _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL; + p7 = d2i_PKCS7(/* a= */ NULL, &d, (long) sig->iov_len); + if (!p7) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Failed to parse PKCS7 DER signature data."); + + STACK_OF(PKCS7_SIGNER_INFO) *sinfos = PKCS7_get_signer_info(p7); + if (!sinfos) + return log_debug_errno(SYNTHETIC_ERRNO(ENODATA), "No signature information in PKCS7 signature?"); + int n = sk_PKCS7_SIGNER_INFO_num(sinfos); + if (n == 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENODATA), "No signatures in PKCS7 signature, refusing."); + if (n > SIGNERS_MAX) /* safety net, in case people send us weirdly complex signatures */ + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Too many signatures, refusing."); + assert(n > 0); + + size_t n_signers = 0; + Signer *signers = new(Signer, n); + if (!signers) + return log_oom_debug(); + + CLEANUP_ARRAY(signers, n_signers, signer_free_many); + + for (int i = 0; i < n; i++) { + PKCS7_SIGNER_INFO *si = sk_PKCS7_SIGNER_INFO_value(PKCS7_get_signer_info(p7), i); + if (!si) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get signer information."); + + _cleanup_(signer_done) Signer signer = {}; + + _cleanup_free_ unsigned char *p = NULL; + int len = i2d_X509_NAME(si->issuer_and_serial->issuer, &p); + signer.issuer = IOVEC_MAKE(TAKE_PTR(p), len); + + len = i2d_ASN1_INTEGER(si->issuer_and_serial->serial, &p); + signer.serial = IOVEC_MAKE(TAKE_PTR(p), len); + + signers[n_signers++] = TAKE_STRUCT(signer); + } + + *ret_signers = TAKE_PTR(signers); + *ret_n_signers = n_signers; + return n; +#else + return -EOPNOTSUPP; +#endif +} diff --git a/src/shared/pkcs7-util.h b/src/shared/pkcs7-util.h new file mode 100644 index 0000000000..11a93cd857 --- /dev/null +++ b/src/shared/pkcs7-util.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +typedef struct Signer { + struct iovec issuer; + struct iovec serial; +} Signer; + +void signer_free_many(Signer *signers, size_t n); + +int pkcs7_extract_signers( + const struct iovec *sig, + Signer **ret_signers, + size_t *ret_n_signers); diff --git a/src/test/meson.build b/src/test/meson.build index cd563654df..3ca614db36 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -405,6 +405,11 @@ executables += [ 'dependencies' : libopenssl, 'conditions' : ['HAVE_OPENSSL'], }, + test_template + { + 'sources' : files('test-pkcs7-util.c'), + 'dependencies' : libopenssl, + 'conditions' : ['HAVE_OPENSSL'], + }, test_template + { 'sources' : files('test-parse-util.c'), 'dependencies' : libm, diff --git a/src/test/test-pkcs7-util.c b/src/test/test-pkcs7-util.c new file mode 100644 index 0000000000..ca1d38a4de --- /dev/null +++ b/src/test/test-pkcs7-util.c @@ -0,0 +1,55 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "hexdecoct.h" +#include "iovec-util.h" +#include "pkcs7-util.h" +#include "tests.h" + +TEST(pkcs7_extract_signers) { + + const char tsig[] = + "MIIEgwYJKoZIhvcNAQcCoIIEdDCCBHACAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGgggLp" + "MIIC5TCCAc2gAwIBAgIURlvlj5ak0ZhvNS8hENNKwVv60x0wDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UE" + "AwwQbWtvc2kgb2YgbGVubmFydDAeFw0yNTAyMDMxMTAwMjNaFw0yNzAyMDMxMTAwMjNaMBsxGTAXBgNV" + "BAMMEG1rb3NpIG9mIGxlbm5hcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCldQdmHVgU" + "m4saaSqEpF1EVRf9pcIxpVShROBGAbxpxs4BQ7vV7zCg5bXwEVaqaENlIyzqPAj+YQifQS3Dj6LQfH3i" + "War+ciKAv4PYcESG+pcdBb2kJOS8cVD6abZlO9rInvdhXK6PhYF7VohMwMPj/yJYdO50skwA5OQsCHO6" + "amowh0tVzNzpbaJZg6wWIyTf3+ZzZdnOl5EvBCaUqeUhbaxRV9SrAw51rzAOnndUhW1we/vVKEGAPk3c" + "SCb0LLPBG20XX2C00UXgnCWBkU5rkq6BnWSuInhFKCa48avstIet1ZFBA5T5J83ncU9UOVFksEYBXFoc" + "lzcmcbx/2/oFAgMBAAGjITAfMB0GA1UdDgQWBBQewILFCixwq2rejOvJqmZSug2BBDANBgkqhkiG9w0B" + "AQsFAAOCAQEAIvaeNPaJUoIUN5lQC/kcCiKeys96WNRGL2wbTp5PqdnRw14sbWY5iC2z13ih3dmTI9NF" + "TBa7C/ji+5BaAfiJF17LOV00Y8eP5V94fHz4isb0sv5RzLsE4h8X7QFk4JBdV5GiCDzPXjxQAx9kM2so" + "9RGtL8EhHpNygYDgyZ18YeiwcUPkCXT+xG2rM6s/Xlsji0s/18ycI4G8AC8dj5HycyS9BiZHgKrkgqTb" + "VPo4zHYzhZdh0Qrd0J4YpoaotzQ35bkH9PtIkF6C7mE1Z7uMSGFkGQASgJ0BDTpM8QPAf2HIR2xxEtJR" + "ZXkwxxdC+W9AJAzqJldmCHYGSrSR54J0rDGCAV4wggFaAgEBMDMwGzEZMBcGA1UEAwwQbWtvc2kgb2Yg" + "bGVubmFydAIURlvlj5ak0ZhvNS8hENNKwVv60x0wDQYJYIZIAWUDBAIBBQAwDQYJKoZIhvcNAQEBBQAE" + "ggEAXccqvpiEWsz/xvuLhINVZKIOznVdqjkERbZSqCBK94BYESSd+cijaB4XbYaFUZ45Bb3uUDQ56Ojq" + "WoY1elEfqPyCb4vc887QoHmxI0BtdIaHhIDfCGBxhX8fwMknxqjgFa9YvONmDtv4QG4syTw+U3SEqBaa" + "Avftqaa4v4eLk4uZ0nMIgMkx4qOlaxknpP404/nyZPANkOIwDxviNtRBCN9zSiPSqo1zre1vqzaM57Ww" + "8zJASsPEzNR7OsPoLaIZv2OHXpowsRB78TuXGkQnm74T6xdG6DNs24jTYJuCPfGuYLHbrytdhXpFBS6m" + "Orz9715jK2NU5VvGhNVXX4chcw=="; + + _cleanup_free_ void *sig = NULL; + size_t siglen; + ASSERT_OK(unbase64mem(tsig, &sig, &siglen)); + + size_t n_signers = 0; + Signer *signers = NULL; + CLEANUP_ARRAY(signers, n_signers, signer_free_many); + + ASSERT_OK_EQ(pkcs7_extract_signers(&IOVEC_MAKE(sig, siglen), &signers, &n_signers), 1); + ASSERT_EQ(n_signers, 1U); + ASSERT_EQ(signers[0].issuer.iov_len, 29U); + ASSERT_EQ(signers[0].serial.iov_len, 22U); + + _cleanup_free_ char *issuer = NULL; + ASSERT_OK(base64mem(signers[0].issuer.iov_base, signers[0].issuer.iov_len, &issuer)); + + _cleanup_free_ char *serial = NULL; + ASSERT_OK(base64mem(signers[0].serial.iov_base, signers[0].serial.iov_len, &serial)); + + ASSERT_STREQ(issuer, "MBsxGTAXBgNVBAMMEG1rb3NpIG9mIGxlbm5hcnQ="); + ASSERT_STREQ(serial, "AhRGW+WPlqTRmG81LyEQ00rBW/rTHQ=="); +} + +DEFINE_TEST_MAIN(LOG_INFO); From 71ca7532de1cf56ed3c1360c8db3ea2bee0f9ace Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 13:11:38 +0100 Subject: [PATCH 06/11] pcrextend-util: add helpers for measuring roothash/signature of Verity volumes This adds infrastructure for measuring Verity root hashes from userspace, along with he issuer/serial of the signatures used to unlock them. We measure the triplet of volume name, root hash and issuer/serial. if confext/sysext use different signing keys then this ensures the event log carry information about the type of image measures. --- src/shared/pcrextend-util.c | 111 ++++++++++++++++++++++ src/shared/pcrextend-util.h | 3 + src/shared/tpm2-util.c | 3 +- src/shared/tpm2-util.h | 1 + src/shared/varlink-io.systemd.PCRExtend.c | 3 +- 5 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/shared/pcrextend-util.c b/src/shared/pcrextend-util.c index 15e0da822a..e4d3b958a8 100644 --- a/src/shared/pcrextend-util.c +++ b/src/shared/pcrextend-util.c @@ -2,6 +2,7 @@ #include "sd-device.h" #include "sd-id128.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "blkid-util.h" @@ -10,10 +11,13 @@ #include "errno-util.h" #include "escape.h" #include "fd-util.h" +#include "hexdecoct.h" #include "id128-util.h" +#include "iovec-util.h" #include "log.h" #include "mountpoint-util.h" #include "pcrextend-util.h" +#include "pkcs7-util.h" #include "string-util.h" #include "strv.h" @@ -180,3 +184,110 @@ int pcrextend_product_id_word(char **ret) { *ret = TAKE_PTR(word); return 0; } + +int pcrextend_verity_word( + const char *name, + const struct iovec *root_hash, + const struct iovec *root_hash_sig, + char **ret) { + + int r; + + assert(name); + assert(iovec_is_set(root_hash)); + + _cleanup_free_ char *name_escaped = xescape(name, ":"); /* Avoid ambiguity around ":" */ + if (!name_escaped) + return log_oom(); + + _cleanup_free_ char *h = hexmem(root_hash->iov_base, root_hash->iov_len); + if (!h) + return log_oom(); + + _cleanup_free_ char *sigs = NULL; + if (iovec_is_set(root_hash_sig)) { + size_t n_signers = 0; + Signer *signers = NULL; + + /* Let's extract the X.509 issuer + serial number from the PKCS#7 signature and include that + * in the measurement record. This is useful since it allows us to have different signing + * keys for confext + sysext + other types of DDIs, and by means of this information we can + * discern which kind it was. Ideally, we'd measure the fingerprint of the X.509 certificate, + * but typically that's not available in a PKCS#7 signature. */ + + CLEANUP_ARRAY(signers, n_signers, signer_free_many); + + r = pkcs7_extract_signers(root_hash_sig, &signers, &n_signers); + if (r < 0) + return r; + + FOREACH_ARRAY(i, signers, n_signers) { + _cleanup_free_ char *serial = hexmem(i->serial.iov_base, i->serial.iov_len); + if (!serial) + return log_oom(); + + _cleanup_free_ char *issuer = NULL; + if (base64mem(i->issuer.iov_base, i->issuer.iov_len, &issuer) < 0) + return log_oom(); + + if (strextendf_with_separator(&sigs, ",", "%s/%s", serial, issuer) < 0) + return log_oom(); + } + } + + _cleanup_free_ char *word = strjoin("verity:", name_escaped, ":", h, ":", strempty(sigs)); + if (!word) + return log_oom(); + + *ret = TAKE_PTR(word); + return 0; +} + +int pcrextend_verity_now( + const char *name, + const struct iovec *root_hash, + const struct iovec *root_hash_sig) { + +#if HAVE_TPM2 + int r; + + _cleanup_free_ char *word = NULL; + r = pcrextend_verity_word( + name, + root_hash, + root_hash_sig, + &word); + if (r < 0) + return r; + + _cleanup_free_ sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.PCRExtend"); + if (r < 0) + return r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = sd_varlink_callbo( + vl, + "io.systemd.PCRExtend.Extend", + /* ret_reply= */ NULL, + &error_id, + SD_JSON_BUILD_PAIR_STRING("nvpcr", "verity"), + SD_JSON_BUILD_PAIR_STRING("text", word), + SD_JSON_BUILD_PAIR_STRING("eventType", "dm_verity")); + if (r < 0) + return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %m"); + if (error_id) { + r = sd_varlink_error_to_errno(error_id, reply); + if (r != -EBADR) + return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %m"); + + return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %s", error_id); + } + + log_debug("Measurement of '%s' into 'images' NvPCR completed.", word); + return 1; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring Verity root hashes and signatures."); +#endif +} diff --git a/src/shared/pcrextend-util.h b/src/shared/pcrextend-util.h index 88bd23c10d..00bc5b9b48 100644 --- a/src/shared/pcrextend-util.h +++ b/src/shared/pcrextend-util.h @@ -4,3 +4,6 @@ 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); +int pcrextend_verity_word(const char *name, const struct iovec *root_hash, const struct iovec *root_hash_sig, char **ret); + +int pcrextend_verity_now(const char *name, const struct iovec *root_hash,const struct iovec *root_hash_sig); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index c751afb06b..3b559bf84c 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -5441,7 +5441,7 @@ int tpm2_seal(Tpm2Context *c, seal_key_handle); primary_alg = primary_public->publicArea.type; - + /* Propagate fixedTPM/fixedParent flags from sealing key to hmac key */ hmac_template.objectAttributes = (hmac_template.objectAttributes & ~(TPMA_OBJECT_FIXEDTPM|TPMA_OBJECT_FIXEDPARENT)) | (primary_public->publicArea.objectAttributes & (TPMA_OBJECT_FIXEDTPM|TPMA_OBJECT_FIXEDPARENT)); @@ -6478,6 +6478,7 @@ static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MA [TPM2_EVENT_KEYSLOT] = "keyslot", [TPM2_EVENT_NVPCR_INIT] = "nvpcr-init", [TPM2_EVENT_NVPCR_SEPARATOR] = "nvpcr-separator", + [TPM2_EVENT_DM_VERITY] = "dm-verity", }; DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 6827719e86..68289bec48 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -145,6 +145,7 @@ typedef enum Tpm2UserspaceEventType { TPM2_EVENT_KEYSLOT, TPM2_EVENT_NVPCR_INIT, TPM2_EVENT_NVPCR_SEPARATOR, + TPM2_EVENT_DM_VERITY, _TPM2_USERSPACE_EVENT_TYPE_MAX, _TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL, } Tpm2UserspaceEventType; diff --git a/src/shared/varlink-io.systemd.PCRExtend.c b/src/shared/varlink-io.systemd.PCRExtend.c index 9179668d7d..87edec349e 100644 --- a/src/shared/varlink-io.systemd.PCRExtend.c +++ b/src/shared/varlink-io.systemd.PCRExtend.c @@ -11,7 +11,8 @@ static SD_VARLINK_DEFINE_ENUM_TYPE( SD_VARLINK_DEFINE_ENUM_VALUE(product_id), SD_VARLINK_DEFINE_ENUM_VALUE(keyslot), SD_VARLINK_DEFINE_ENUM_VALUE(nvpcr_init), - SD_VARLINK_DEFINE_ENUM_VALUE(nvpcr_separator)); + SD_VARLINK_DEFINE_ENUM_VALUE(nvpcr_separator), + SD_VARLINK_DEFINE_ENUM_VALUE(dm_verity)); static SD_VARLINK_DEFINE_METHOD( Extend, From 32f405074a3aa221982ad92a7f61560b9f6a2b03 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 13:12:26 +0100 Subject: [PATCH 07/11] dissect-image: measure Verity before making use of them Let's hook up the dissection logic with the new measurement infra, and issue the measurement after successfully unlock an image, but before returning to the caller. Note that ideally we'd do this measurement in the kernel, so that we can place it after authenticating the root hash, but before activating the medium. One day we should be able to do that via eBPF based on userspace policies, but for now, this would require too much kernel rework. Let's however make sure our measurements only contain data that the kernel could know too, so that we hopefully can move these measurements to the kernel without changing their formatting. --- src/shared/dissect-image.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index ec9247b048..4156903774 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -60,6 +60,7 @@ #include "openssl-util.h" #include "os-util.h" #include "path-util.h" +#include "pcrextend-util.h" #include "pidref.h" #include "proc-cmdline.h" #include "process-util.h" @@ -3219,13 +3220,13 @@ static int do_crypt_activate_verity( DissectImageFlags flags, PartitionPolicyFlags policy_flags) { - bool check_signature; - int r, k; + int r; assert(cd); assert(name); assert(verity); + bool check_signature; if (iovec_is_set(&verity->root_hash_sig) && FLAGS_SET(policy_flags, PARTITION_POLICY_SIGNED)) { r = secure_getenv_bool("SYSTEMD_DISSECT_VERITY_SIGNATURE"); if (r < 0 && r != -ENXIO) @@ -3235,7 +3236,10 @@ static int do_crypt_activate_verity( } else check_signature = false; + bool measure_signature; if (check_signature) { + int k; + /* First, if we have support for signed keys in the kernel, then try that first. */ r = sym_crypt_activate_by_signed_key( cd, @@ -3247,7 +3251,8 @@ static int do_crypt_activate_verity( CRYPT_ACTIVATE_READONLY); if (r >= 0) { log_debug("Verity activation via kernel signature logic worked."); - return 0; + measure_signature = true; + goto done; } log_debug_errno(r, "Validation of dm-verity signature failed via the kernel, trying userspace validation instead: %m"); @@ -3280,9 +3285,13 @@ static int do_crypt_activate_verity( /* Otherwise let's see what signature-less activation results in. */ + measure_signature = true; + } else if (!FLAGS_SET(policy_flags, PARTITION_POLICY_VERITY)) return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), "No-signature activation of Verity volume not allowed by policy, refusing."); + else + measure_signature = false; r = sym_crypt_activate_by_volume_key( cd, @@ -3294,6 +3303,12 @@ static int do_crypt_activate_verity( return log_debug_errno(r, "Activation of Verity via root hash failed: %m"); log_debug("Activation of Verity via root hash succeeded."); + +done: + (void) pcrextend_verity_now( + name, + &verity->root_hash, + measure_signature ? &verity->root_hash_sig : NULL); return 0; } From 85d7fb22470e5a4b17045d9288d4e2c06ff7b2e8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 13:13:21 +0100 Subject: [PATCH 08/11] veritysetup: optionally measure Verity once we activated it As in the previous commit, also enable the measurement logic when activating Verity via veritsetup rather than the dissection logic. The logic stays close to the interface of cryptsetup for measuring the volume key. --- man/veritytab.xml | 12 ++++++++++++ src/veritysetup/veritysetup.c | 26 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/man/veritytab.xml b/man/veritytab.xml index 9b131e350c..913acad9a2 100644 --- a/man/veritytab.xml +++ b/man/veritytab.xml @@ -298,6 +298,18 @@ This is based on crypttab(5). + + + + Takes a boolean argument, or an NvPCR name as argument. May be used to enable + automatic measurement of the volume name and Verity root hash, as well as the serials and issuers of + the certificates used to generate the provided signatures (if any) will be measured. Passing false + disables the mechanism. Passing true enables measurement into the verity NvPCR. If + any other string is specified this selects the NvPCR to measure into by name. + + + + At early boot and when the system manager configuration is diff --git a/src/veritysetup/veritysetup.c b/src/veritysetup/veritysetup.c index 91b5bcaaa2..e2135562c1 100644 --- a/src/veritysetup/veritysetup.c +++ b/src/veritysetup/veritysetup.c @@ -16,9 +16,11 @@ #include "main-func.h" #include "parse-util.h" #include "path-util.h" +#include "pcrextend-util.h" #include "pretty-print.h" #include "string-util.h" #include "strv.h" +#include "tpm2-util.h" #include "verbs.h" static char *arg_hash = NULL; /* the hash algorithm */ @@ -38,12 +40,14 @@ static uint64_t arg_fec_roots = 2; static void *arg_root_hash_signature = NULL; static size_t arg_root_hash_signature_size = 0; static bool arg_root_hash_signature_auto = false; +static char *arg_tpm2_measure_nvpcr = NULL; STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_salt, freep); STATIC_DESTRUCTOR_REGISTER(arg_uuid, freep); STATIC_DESTRUCTOR_REGISTER(arg_fec_what, freep); STATIC_DESTRUCTOR_REGISTER(arg_root_hash_signature, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_nvpcr, freep); static int help(void) { _cleanup_free_ char *link = NULL; @@ -142,7 +146,7 @@ static int parse_options(const char *options) { for (;;) { _cleanup_free_ char *word = NULL; - char *val; + const char *val; r = extract_first_word(&options, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS | EXTRACT_UNESCAPE_SEPARATORS); if (r < 0) @@ -290,6 +294,21 @@ static int parse_options(const char *options) { if (r < 0) return r; + } else if ((val = startswith(word, "tpm2-measure-nvpcr="))) { + r = isempty(val) ? 0 : parse_boolean(val); + if (r == 0) { + arg_tpm2_measure_nvpcr = mfree(arg_tpm2_measure_nvpcr); + return 0; + } + if (r > 0) + val = "verity"; + else if (!tpm2_nvpcr_name_is_valid(val)) { + log_warning("Invalid NvPCR name, ignoring: %s", word); + return 0; + } + + if (free_and_strdup(&arg_tpm2_measure_nvpcr, val) < 0) + return log_oom(); } else log_warning("Encountered unknown option '%s', ignoring.", word); } @@ -423,6 +442,11 @@ static int verb_attach(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Failed to set up verity device '%s': %m", volume); + (void) pcrextend_verity_now( + volume, + &IOVEC_MAKE(rh, rh_size), + &IOVEC_MAKE(arg_root_hash_signature, arg_root_hash_signature_size)); + return 0; } From a450fab9c6c625739e7f5c8f5c4dbb88677a1fb3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 11 Feb 2026 16:29:59 +0100 Subject: [PATCH 09/11] gpt-auto-generator: enable nvpcr logic by default Let's enable this kind of measurement by default if people buy into UKIs and stuff, just like volume key measurement is now enabled by default. --- man/systemd-gpt-auto-generator.xml | 10 ++++++--- src/gpt-auto-generator/gpt-auto-generator.c | 24 +++++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/man/systemd-gpt-auto-generator.xml b/man/systemd-gpt-auto-generator.xml index 398076abc7..e267fc9528 100644 --- a/man/systemd-gpt-auto-generator.xml +++ b/man/systemd-gpt-auto-generator.xml @@ -272,9 +272,13 @@ If the system was booted via systemd-stub7 and the stub reported to userspace that the kernel image was measured to a TPM2 PCR, then any discovered root and - /var/ volume identifiers (and volume encryption key in case it is encrypted) will be - automatically measured into PCR 15 on activation, via - systemd-pcrfs@.service8. + /var/ volume identifiers (and volume encryption keys, in case they are encrypted) + will be automatically measured into PCR 15 on activation, via + systemd-pcrfs@.service8. Moreover, + information about the LUKS key slot used to unlock the volume is measured into NvPCR + cryptsetup. Finally, if the root or /usr/ partition is protected + via Verity its root hash and the serial/issuer of the key used for the provided root hash signature (if + any) are measured into the NvPCR verity. Mount constraint metadata contained in the file systems is validated by pulling in systemd-validatefs@.service8 diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 9e20e23abf..4fd92a6057 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -190,7 +190,8 @@ static int add_veritysetup( const char *id, const char *data_what, const char *hash_what, - const char *mount_opts) { + const char *mount_opts, + MountPointFlags flags) { #if HAVE_LIBCRYPTSETUP int r; @@ -233,13 +234,26 @@ static int add_veritysetup( "After=%1$s %2$s\n", dd, dh); + _cleanup_free_ char *options = + strdup("root-hash-signature=auto"); /* auto means: derive signature from udev property ID_DISSECT_PART_ROOTHASH_SIG */ + if (!options) + return log_oom(); + + if (FLAGS_SET(flags, MOUNT_MEASURE)) { + r = efi_measured_uki(LOG_WARNING); + if (r > 0 && !strextend_with_separator(&options, ",", "tpm2-measure-nvpcr=yes")) + return log_oom(); + if (r == 0) + log_debug("Will not measure root hash/signature of volume '%s', not booted via systemd-stub with measurements enabled.", id); + } + r = generator_write_veritysetup_service_section( f, id, data_what, hash_what, /* roothash= */ NULL, /* NULL means: derive root hash from udev property ID_DISSECT_PART_ROOTHASH */ - "root-hash-signature=auto"); /* auto means: derive signature from udev property ID_DISSECT_PART_ROOTHASH_SIG */ + options); if (r < 0) return r; @@ -871,7 +885,8 @@ static int add_root_mount(void) { "root", "/dev/disk/by-designator/root-verity-data", "/dev/disk/by-designator/root-verity", - arg_root_options); + arg_root_options, + MOUNT_MEASURE); if (r < 0) return r; } @@ -952,7 +967,8 @@ static int add_usr_mount(void) { "usr", "/dev/disk/by-designator/usr-verity-data", "/dev/disk/by-designator/usr-verity", - arg_usr_options); + arg_usr_options, + MOUNT_MEASURE); if (r < 0) return r; } From 521a523ce0cdcf0d529bd566f3d64ae93f10419d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 17 Feb 2026 21:59:30 +0100 Subject: [PATCH 10/11] ci: add simple test that ensures the verity nvpcr measurements are made --- test/units/TEST-70-TPM2.nvpcr.sh | 46 +++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/test/units/TEST-70-TPM2.nvpcr.sh b/test/units/TEST-70-TPM2.nvpcr.sh index ce5bdfc651..ba5d6c9b7f 100755 --- a/test/units/TEST-70-TPM2.nvpcr.sh +++ b/test/units/TEST-70-TPM2.nvpcr.sh @@ -20,7 +20,8 @@ at_exit() { jq --seq --slurp "${OPENSSL_CONFIG:?}" < Date: Wed, 11 Feb 2026 18:27:42 +0100 Subject: [PATCH 11/11] update TODO --- TODO | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/TODO b/TODO index b9200b9ea4..cf346e0509 100644 --- a/TODO +++ b/TODO @@ -720,10 +720,6 @@ Features: deleting entries for rotation, place an event that declares how many items have been dropped, and what the hash before and after that. -* measure information about all DDIs as we activate them to an NvPCR. We - probably should measure the dm-verity root hash from the kernel side, but - DDI meta info from userspace. - * use name_to_handle_at() with AT_HANDLE_FID instead of .st_ino (inode number) for identifying inodes, for example in copy.c when finding hard links, or loop-util.c for tracking backing files, and other places. @@ -1299,9 +1295,9 @@ Features: - If run on every boot, should it use the sysupdate config from the host on subsequent boots? -* To mimic the new tpm2-measure-pcr= crypttab option add the same to veritytab - (measuring the root hash) and integritytab (measuring the HMAC key if one is - used) +* To mimic the new tpm2-measure-pcr= crypttab option and tpm2-measure-nvpcr= + veritytab option, add the same to integritytab (measuring the HMAC key if one + is used) * We should start measuring all services, containers, and system extensions we activate. probably into PCR 13. i.e. add --tpm2-measure-pcr= or so to @@ -1720,18 +1716,6 @@ Features: keys of /etc/crypttab. That way people can store/provide the roothash externally and provide to us on demand only. -* we probably should extend the root verity hash of the root fs into some PCR - on boot. (i.e. maybe add a veritytab option tpm2-measure=12 or so to measure - it into PCR 12); Similar: we probably should extend the LUKS volume key of - the root fs into some PCR on boot. (i.e. maybe add a crypttab option - tpm2-measure=15 or so to measure it into PCR 15); once both are in place - update gpt-auto-discovery to generate these by default for the partitions it - discovers. Static vendor stuff should probably end up in PCR 12 (i.e. the - verity hash), with local keys in PCR 15 (i.e. the encryption volume - key). That way, we nicely distinguish resources supplied by the OS vendor - (i.e. sysext, root verity) from those inherently local (i.e. encryption key), - which is useful if they shall be signed separately. - * rework recursive read-only remount to use new mount API * when mounting disk images: if IMAGE_ID/IMAGE_VERSION is set in os-release