['systemd-pcrextend',
'systemd-pcrfs-root.service',
'systemd-pcrfs@.service',
+ 'systemd-pcrlogin@.service',
'systemd-pcrmachine.service',
'systemd-pcrnvdone.service',
'systemd-pcrosseparator.service',
<refname>systemd-pcrmachine.service</refname>
<refname>systemd-pcrosseparator.service</refname>
<refname>systemd-pcrproduct.service</refname>
+ <refname>systemd-pcrlogin@.service</refname>
<refname>systemd-pcrfs-root.service</refname>
<refname>systemd-pcrfs@.service</refname>
<refname>systemd-pcrnvdone.service</refname>
<refname>systemd-pcrextend</refname>
- <refpurpose>Measure boot phases, machine ID, product UUID and file system identity into TPM PCRs and NvPCRs</refpurpose>
+ <refpurpose>Measure boot phases, machine ID, product UUID, user records and file system identity into TPM PCRs and NvPCRs</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-pcrmachine.service</filename></para>
<para><filename>systemd-pcrosseparator.service</filename></para>
<para><filename>systemd-pcrproduct.service</filename></para>
+ <para><filename>systemd-pcrlogin@.service</filename></para>
<para><filename>systemd-pcrfs-root.service</filename></para>
<para><filename>systemd-pcrfs@.service</filename></para>
<para><filename>systemd-pcrnvdone.service</filename></para>
product UUID (as provided by one of SMBIOS, Devicetree, …) into a NvPCR named
<literal>hardware</literal>.</para>
+ <para><filename>systemd-pcrlogin@.service</filename> 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
+ <literal>login</literal>. It is started by
+ <citerefentry><refentrytitle>systemd-logind.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ on the user's first login of the current boot, much like
+ <citerefentry><refentrytitle>user-runtime-dir@.service</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+ Unlike the latter it is intentionally never stopped again: it is a <varname>Type=oneshot</varname> service
+ with <varname>RemainAfterExit=yes</varname> in <filename>system.slice</filename>, 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
+ <literal>login</literal> NvPCR (together with the
+ <filename>/run/log/systemd/tpm2-measure.log</filename> event log) thus provides a TPM-backed,
+ append-only record of which user identities were activated during the current boot.</para>
+
<para><filename>systemd-pcrnvdone.service</filename> is a system service that measures a separator event
into PCR 9 once all NvPCRs have completed initialization.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--login=</option></term>
+
+ <listitem><para>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 <literal>login</literal>. The parameter must be a
+ user name or numeric UID. The record is reduced to its <literal>regular</literal>,
+ <literal>perMachine</literal> and <literal>binding</literal> 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
+ <filename>systemd-pcrlogin@.service</filename>.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>--file-system=</option></term>
<listitem><para>Set the event log event type for this measurement. Pass <literal>help</literal> for a
list of currently defined identifiers. Defaults to an appropriate value for
- <option>--machine-id</option>, <option>--product-id</option>, <option>--file-system=</option>, and
- otherwise to <literal>phase</literal>.</para>
+ <option>--machine-id</option>, <option>--product-id</option>, <option>--file-system=</option>,
+ <option>--login=</option>, and otherwise to <literal>phase</literal>.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
#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"
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;
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
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);
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;
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.");
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;
}
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.");
#include "sd-device.h"
#include "sd-id128.h"
+#include "sd-json.h"
#include "sd-varlink.h"
#include "alloc-util.h"
#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,
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,
#include <sys/uio.h>
+#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);
[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);
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;
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(
--- /dev/null
+{
+ "name" : "login",
+ "algorithm" : "sha256",
+ "nvIndex" : {{TPM2_NVPCR_BASE + 3}},
+ "priority" : 800
+}
'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'],
--- /dev/null
+# 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