From: Lennart Poettering Date: Thu, 21 May 2026 12:47:59 +0000 (+0200) Subject: pcrextend: add support for measuring a user record, to be executed on first login... X-Git-Tag: v261-rc1~10^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e32209c69c64c26dc35e27ceed463bb3b1169dcb;p=thirdparty%2Fsystemd.git pcrextend: add support for measuring a user record, to be executed on first login of the user This is supposed to be useful to mark an interactive user login as a "break glass" event in the measurement logs, i.e. as in many typically headless scenerios this indicates debug access or similar. --- diff --git a/man/rules/meson.build b/man/rules/meson.build index e0a99b1a21c..6fd43ad4c33 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1147,6 +1147,7 @@ manpages = [ ['systemd-pcrextend', 'systemd-pcrfs-root.service', 'systemd-pcrfs@.service', + 'systemd-pcrlogin@.service', 'systemd-pcrmachine.service', 'systemd-pcrnvdone.service', 'systemd-pcrosseparator.service', diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index 3e9afe7caa3..8dbe8e45312 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -23,11 +23,12 @@ systemd-pcrmachine.service systemd-pcrosseparator.service systemd-pcrproduct.service + systemd-pcrlogin@.service systemd-pcrfs-root.service systemd-pcrfs@.service systemd-pcrnvdone.service systemd-pcrextend - Measure boot phases, machine ID, product UUID and file system identity into TPM PCRs and NvPCRs + Measure boot phases, machine ID, product UUID, user records and file system identity into TPM PCRs and NvPCRs @@ -37,6 +38,7 @@ systemd-pcrmachine.service systemd-pcrosseparator.service systemd-pcrproduct.service + systemd-pcrlogin@.service systemd-pcrfs-root.service systemd-pcrfs@.service systemd-pcrnvdone.service @@ -64,6 +66,20 @@ product UUID (as provided by one of SMBIOS, Devicetree, …) into a NvPCR named hardware. + systemd-pcrlogin@.service is a template service that measures the (reduced, + canonical JSON) user record of the user indicated by its instance identifier (a UID) into a NvPCR named + login. It is started by + systemd-logind.service8 + on the user's first login of the current boot, much like + user-runtime-dir@.service5. + Unlike the latter it is intentionally never stopped again: it is a Type=oneshot service + with RemainAfterExit=yes in system.slice, so each user is measured + exactly once per boot regardless of how often they log in and out (NvPCR extensions form a hash chain, so + measuring the same record twice would otherwise make the NvPCR value unpredictable). The resulting + login NvPCR (together with the + /run/log/systemd/tpm2-measure.log event log) thus provides a TPM-backed, + append-only record of which user identities were activated during the current boot. + systemd-pcrnvdone.service is a system service that measures a separator event into PCR 9 once all NvPCRs have completed initialization. @@ -244,6 +260,20 @@ + + + + Instead of measuring a word specified on the command line into PCR 11, measure the + user record of the specified user into an NvPCR named login. The parameter must be a + user name or numeric UID. The record is reduced to its regular, + perMachine and binding sections (i.e. the privileged, secret, + status and signature sections are stripped, so that for example password hash changes do not perturb + the NvPCR), normalized and serialized to canonical JSON before measurement. This is primarily used by + systemd-pcrlogin@.service. + + + + @@ -259,8 +289,8 @@ Set the event log event type for this measurement. Pass help for a list of currently defined identifiers. Defaults to an appropriate value for - , , , and - otherwise to phase. + , , , + , and otherwise to phase. diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index 5109ebb897e..ac432aaac57 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -21,6 +21,8 @@ #include "strv.h" #include "tpm2-pcr.h" #include "tpm2-util.h" +#include "user-record.h" +#include "userdb.h" #include "varlink-io.systemd.PCRExtend.h" #include "varlink-util.h" @@ -30,6 +32,7 @@ static char **arg_banks = NULL; static char *arg_file_system = NULL; static bool arg_machine_id = false; static bool arg_product_id = false; +static UserRecord *arg_login = NULL; static uint32_t arg_pcr_mask = 0; static char *arg_nvpcr_name = NULL; static bool arg_varlink = false; @@ -40,6 +43,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_file_system, freep); STATIC_DESTRUCTOR_REGISTER(arg_nvpcr_name, freep); +STATIC_DESTRUCTOR_REGISTER(arg_login, user_record_unrefp); #define EXTENSION_STRING_SAFE_LIMIT 1024 @@ -55,7 +59,8 @@ static int help(void) { help_cmdline("[OPTIONS...] --file-system=PATH"); help_cmdline("[OPTIONS...] --machine-id"); help_cmdline("[OPTIONS...] --product-id"); - help_abstract("Extend a TPM2 PCR with boot phase, machine ID, or file system ID."); + help_cmdline("[OPTIONS...] --login=UID|USER"); + help_abstract("Extend a TPM2 PCR with boot phase, machine ID, file system ID or user record."); help_section("Options"); r = table_print_or_warn(options); @@ -158,6 +163,19 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { arg_product_id = true; break; + OPTION_LONG("login", "UID|USER", + "Measure a user's record into NvPCR 'login'"): { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + + r = userdb_by_name(opts.arg, /* match= */ NULL, USERDB_PARSE_NUMERIC|USERDB_SUPPRESS_SHADOW, &ur); + if (r < 0) + return log_error_errno(r, "Failed to look up user '%s': %m", opts.arg); + + user_record_unref(arg_login); + arg_login = TAKE_PTR(ur); + break; + } + OPTION_LONG("early", NULL, "Run in early boot mode, without access to /var/"): arg_early = true; @@ -174,8 +192,8 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { break; } - if (!!arg_file_system + arg_machine_id + arg_product_id > 1) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system=, --machine-id, --product-id may not be combined."); + if (!!arg_file_system + arg_machine_id + arg_product_id + !!arg_login > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system=, --machine-id, --product-id, --login= may not be combined."); if (arg_pcr_mask != 0 && arg_nvpcr_name) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--pcr= and --nvpcr= may not be combined."); @@ -188,10 +206,13 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { else if (arg_pcr_mask == 0 && !arg_nvpcr_name) { arg_pcr_mask = (arg_file_system || arg_machine_id) ? INDEX_TO_MASK(uint32_t, TPM2_PCR_SYSTEM_IDENTITY) : /* → PCR 15 */ - !arg_product_id ? INDEX_TO_MASK(uint32_t, TPM2_PCR_KERNEL_BOOT) : /* → PCR 11 */ - 0; + (arg_product_id || arg_login) ? 0 : /* → NvPCR */ + INDEX_TO_MASK(uint32_t, TPM2_PCR_KERNEL_BOOT); /* → PCR 11 */ - r = free_and_strdup_warn(&arg_nvpcr_name, arg_product_id ? "hardware" : NULL); + r = free_and_strdup_warn(&arg_nvpcr_name, + arg_product_id ? "hardware" : + arg_login ? "login" : + NULL); if (r < 0) return r; } @@ -485,6 +506,17 @@ static int run(int argc, char *argv[]) { return r; event = TPM2_EVENT_PRODUCT_ID; + + } else if (arg_login) { + + if (n_args != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument."); + + r = pcrextend_login_word(arg_login, &word); + if (r < 0) + return r; + + event = TPM2_EVENT_LOGIN; } else { if (n_args != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument."); diff --git a/src/shared/pcrextend-util.c b/src/shared/pcrextend-util.c index 79efdc4574e..cf6eb7d1a56 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-json.h" #include "sd-varlink.h" #include "alloc-util.h" @@ -22,6 +23,7 @@ #include "string-util.h" #include "strv.h" #include "tpm2-pcr.h" +#include "user-record.h" static int device_get_file_system_word( sd_device *d, @@ -187,6 +189,58 @@ int pcrextend_product_id_word(char **ret) { return 0; } +int pcrextend_login_word(UserRecord *ur, char **ret) { + int r; + + assert(ur); + assert(ret); + + /* Reduce the user record to the sections that make up its stable, host-specific identity, and turn + * that into a word to measure into the 'login' NvPCR. We deliberately keep the 'regular', + * 'perMachine' and 'binding' sections (the portable identity plus how it is realized on *this* + * host), but drop 'privileged' (so password hash churn doesn't perturb the NvPCR), 'secret' and + * 'status' (transient) and 'signature' (so the scheme is identical for signed and unsigned records). + * Only 'regular' is required, the others are merely allowed, so plain NSS users reduce cleanly. */ + UserRecordLoadFlags mask = + USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_BINDING | + USER_RECORD_STRIP_PRIVILEGED | + USER_RECORD_STRIP_SECRET | + USER_RECORD_STRIP_STATUS | + USER_RECORD_STRIP_SIGNATURE | + USER_RECORD_PERMISSIVE; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = user_group_record_mangle(ur->json, mask, &v, /* ret_mask= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to reduce user record of '%s': %m", ur->user_name); + + /* Normalize so the serialization is canonical (stable key order) regardless of how the record was + * assembled by the various userdb backends. */ + r = sd_json_variant_normalize(&v); + if (r < 0) + return log_error_errno(r, "Failed to normalize user record of '%s': %m", ur->user_name); + + /* Format to compact, single-line JSON (no SD_JSON_FORMAT_NEWLINE), so the measured word stays on one + * line and the colon-separated word structure is unambiguous. */ + _cleanup_free_ char *text = NULL; + r = sd_json_variant_format(v, /* flags= */ 0, &text); + if (r < 0) + return log_error_errno(r, "Failed to format user record of '%s': %m", ur->user_name); + + _cleanup_free_ char *name_escaped = xescape(ur->user_name, ":"); /* Avoid ambiguity around ":" */ + if (!name_escaped) + return log_oom(); + + _cleanup_free_ char *word = strjoin("login:", name_escaped, ":", text); + if (!word) + return log_oom(); + + *ret = TAKE_PTR(word); + return 0; +} + int pcrextend_verity_word( const char *name, const struct iovec *root_hash, diff --git a/src/shared/pcrextend-util.h b/src/shared/pcrextend-util.h index eadc2d5cffc..c753c265683 100644 --- a/src/shared/pcrextend-util.h +++ b/src/shared/pcrextend-util.h @@ -3,11 +3,14 @@ #include +#include "shared-forward.h" + 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_imds_userdata_word(const struct iovec *data, char **ret); +int pcrextend_login_word(UserRecord *ur, char **ret); int pcrextend_verity_now(const char *name, const struct iovec *root_hash, const struct iovec *root_hash_sig); int pcrextend_imds_userdata_now(const struct iovec *data); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 26ce8d72464..3f29730c7f8 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6702,6 +6702,7 @@ static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MA [TPM2_EVENT_DM_VERITY] = "dm-verity", [TPM2_EVENT_IMDS_USERDATA] = "imds-userdata", [TPM2_EVENT_OS_SEPARATOR] = "os-separator", + [TPM2_EVENT_LOGIN] = "login", }; DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index bf2321a514a..bc26a16947a 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -151,6 +151,7 @@ typedef enum Tpm2UserspaceEventType { TPM2_EVENT_DM_VERITY, TPM2_EVENT_IMDS_USERDATA, TPM2_EVENT_OS_SEPARATOR, + TPM2_EVENT_LOGIN, _TPM2_USERSPACE_EVENT_TYPE_MAX, _TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL, } Tpm2UserspaceEventType; diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index e87a13d8e66..55f0ce26d31 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -45,6 +45,7 @@ executables += [ if conf.get('ENABLE_BOOTLOADER') == 1 and conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_TPM2') == 1 nvpcrs = [ 'cryptsetup', 'hardware', + 'login', 'verity'] foreach n : nvpcrs custom_target( diff --git a/src/tpm2-setup/nvpcr/login.nvpcr.in b/src/tpm2-setup/nvpcr/login.nvpcr.in new file mode 100644 index 00000000000..babad7aaf89 --- /dev/null +++ b/src/tpm2-setup/nvpcr/login.nvpcr.in @@ -0,0 +1,6 @@ +{ + "name" : "login", + "algorithm" : "sha256", + "nvIndex" : {{TPM2_NVPCR_BASE + 3}}, + "priority" : 800 +} diff --git a/units/meson.build b/units/meson.build index c78835aa70e..de7a796a1e4 100644 --- a/units/meson.build +++ b/units/meson.build @@ -604,6 +604,10 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['sysinit.target.wants/'], }, + { + 'file' : 'systemd-pcrlogin@.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, { 'file' : 'systemd-pcrphase-initrd.service.in', 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2', 'ENABLE_INITRD'], diff --git a/units/systemd-pcrlogin@.service.in b/units/systemd-pcrlogin@.service.in new file mode 100644 index 00000000000..dbdf488550d --- /dev/null +++ b/units/systemd-pcrlogin@.service.in @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=TPM NvPCR Measurement of User Record for UID %i +Documentation=man:systemd-pcrlogin@.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=tpm2.target systemd-pcrnvdone.service +Before=shutdown.target +RequiresMountsFor=/var/lib/systemd/nvpcr +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-os + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrextend --graceful --login=%i