tpm2: measure user records on first login (#42228)
This is useful so that we can "blow a fuse" in TPM policies once a user logged in.
This commit is contained in:
@@ -1737,8 +1737,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
- maybe: in PID1, when we detect we run in an initrd, make superblock read-only
|
||||
early on, but provide opt-out via kernel cmdline.
|
||||
|
||||
- measure all log-in attempts into a new nvpcr
|
||||
|
||||
- measure GPT and LUKS headers somewhere when we use them (i.e. in
|
||||
systemd-gpt-auto-generator/systemd-repart and in systemd-cryptsetup?)
|
||||
|
||||
@@ -1902,8 +1900,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
keyring, so that the kernel does this validation for us for verity and kernel
|
||||
modules
|
||||
|
||||
- on first login of a user, measure its identity to some nvpcr
|
||||
|
||||
- on shutdown: move utmp, wall, audit logic all into PID 1 (or logind?)
|
||||
|
||||
- once swtpm's sd_notify() support has landed in the distributions, remove the
|
||||
|
||||
@@ -322,6 +322,23 @@ colon-separated strings, identifying the file system type, UUID, label as well
|
||||
as the GPT partition entry UUID, entry type UUID and entry label (in UTF-8,
|
||||
without trailing NUL bytes).
|
||||
|
||||
### NvPCR `login` (base+3), user logins
|
||||
|
||||
The `systemd-pcrlogin@.service` service (a per-UID template unit started by
|
||||
`systemd-logind.service` on a user's first login of the current boot) will
|
||||
measure that user's record into the `login` NvPCR. Each user is measured exactly
|
||||
once per boot (the unit is `Type=oneshot`/`RemainAfterExit=yes` and is never
|
||||
stopped again), so the NvPCR forms an append-only record of which user
|
||||
identities were activated during the current boot. Note that its value is
|
||||
inherently dynamic: it depends on *which* users log in and *in which order*.
|
||||
|
||||
→ **Measured hash** covers the string "login:", suffixed by the (escaped)
|
||||
user name, a colon, and the user's record reduced to its `regular`, `perMachine`
|
||||
and `binding` sections (i.e. with the `privileged`, `secret`, `status` and
|
||||
`signature` sections stripped), normalized and serialized to canonical,
|
||||
single-line JSON. Example string:
|
||||
`login:lennart:{"userName":"lennart","uid":1000,…}`.
|
||||
|
||||
### PCR 9, NvPCR initialization separator
|
||||
|
||||
After completion of `systemd-tpm2-setup.service` (which initializes all NvPCRs
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -23,11 +23,12 @@
|
||||
<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>
|
||||
@@ -37,6 +38,7 @@
|
||||
<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>
|
||||
@@ -64,6 +66,20 @@
|
||||
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>
|
||||
|
||||
@@ -244,6 +260,20 @@
|
||||
<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>
|
||||
|
||||
@@ -259,8 +289,8 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -4321,7 +4321,7 @@ int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *err
|
||||
/* If the user is stopping, we're tracking stop jobs here. So don't send reply. */
|
||||
if (!user->stopping) {
|
||||
char **user_job;
|
||||
FOREACH_ARGUMENT(user_job, &user->runtime_dir_job, &user->service_manager_job)
|
||||
FOREACH_ARGUMENT(user_job, &user->runtime_dir_job, &user->service_manager_job, &user->measure_job)
|
||||
if (streq_ptr(path, *user_job)) {
|
||||
*user_job = mfree(*user_job);
|
||||
|
||||
|
||||
+60
-2
@@ -10,6 +10,7 @@
|
||||
#include "bus-locator.h"
|
||||
#include "bus-util.h"
|
||||
#include "clean-ipc.h"
|
||||
#include "efi-loader.h"
|
||||
#include "env-file.h"
|
||||
#include "errno-util.h"
|
||||
#include "escape.h"
|
||||
@@ -84,6 +85,10 @@ int user_new(Manager *m, UserRecord *ur, User **ret) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = unit_name_build("systemd-pcrlogin", lu, ".service", &u->measure_unit);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = hashmap_put(m->users, UID_TO_PTR(ur->uid), u);
|
||||
if (r < 0)
|
||||
return r;
|
||||
@@ -100,6 +105,10 @@ int user_new(Manager *m, UserRecord *ur, User **ret) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = hashmap_put(m->user_units, u->measure_unit, u);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = TAKE_PTR(u);
|
||||
return 0;
|
||||
}
|
||||
@@ -118,15 +127,21 @@ User *user_free(User *u) {
|
||||
|
||||
if (u->service_manager_unit) {
|
||||
(void) hashmap_remove_value(u->manager->user_units, u->service_manager_unit, u);
|
||||
free(u->service_manager_job);
|
||||
free(u->service_manager_unit);
|
||||
}
|
||||
free(u->service_manager_job);
|
||||
|
||||
if (u->runtime_dir_unit) {
|
||||
(void) hashmap_remove_value(u->manager->user_units, u->runtime_dir_unit, u);
|
||||
free(u->runtime_dir_job);
|
||||
free(u->runtime_dir_unit);
|
||||
}
|
||||
free(u->runtime_dir_job);
|
||||
|
||||
if (u->measure_unit) {
|
||||
(void) hashmap_remove_value(u->manager->user_units, u->measure_unit, u);
|
||||
free(u->measure_unit);
|
||||
}
|
||||
free(u->measure_job);
|
||||
|
||||
if (u->slice) {
|
||||
(void) hashmap_remove_value(u->manager->user_units, u->slice, u);
|
||||
@@ -177,6 +192,7 @@ static int user_save_internal(User *u) {
|
||||
env_file_fputs_assignment(f, "RUNTIME=", u->runtime_path);
|
||||
env_file_fputs_assignment(f, "RUNTIME_DIR_JOB=", u->runtime_dir_job);
|
||||
env_file_fputs_assignment(f, "SERVICE_JOB=", u->service_manager_job);
|
||||
env_file_fputs_assignment(f, "MEASURE_JOB=", u->measure_job);
|
||||
if (u->display)
|
||||
env_file_fputs_assignment(f, "DISPLAY=", u->display->id);
|
||||
|
||||
@@ -303,6 +319,7 @@ int user_load(User *u) {
|
||||
r = parse_env_file(NULL, u->state_file,
|
||||
"RUNTIME_DIR_JOB", &u->runtime_dir_job,
|
||||
"SERVICE_JOB", &u->service_manager_job,
|
||||
"MEASURE_JOB", &u->measure_job,
|
||||
"STOPPING", &stopping,
|
||||
"REALTIME", &realtime,
|
||||
"MONOTONIC", &monotonic,
|
||||
@@ -358,6 +375,39 @@ static int user_start_runtime_dir(User *u) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int user_start_measure(User *u) {
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
int r;
|
||||
|
||||
assert(u);
|
||||
assert(!u->stopping);
|
||||
assert(u->manager);
|
||||
assert(u->measure_unit);
|
||||
|
||||
/* Measures the user's record into the 'login' TPM2 NvPCR on the first login of this boot. This is
|
||||
* strictly best-effort: a measurement failure must never block the login (simply because NvPCRs might
|
||||
* not be available). Unlike the runtime-dir and service-manager units, this one is started but never
|
||||
* stopped (see user_stop_service()): it is a Type=oneshot/RemainAfterExit unit living in
|
||||
* system.slice that stays active until reboot, so a later re-login's StartUnit is a no-op and every
|
||||
* user is measured exactly once per boot.
|
||||
*
|
||||
* Only bother if we're actually running in measured-OS mode. The unit itself also carries
|
||||
* ConditionSecurity=measured-os, but checking here avoids queuing a no-op job on every first login
|
||||
* on systems that don't measure the OS */
|
||||
if (efi_measured_os(LOG_DEBUG) <= 0)
|
||||
return 0;
|
||||
|
||||
u->measure_job = mfree(u->measure_job);
|
||||
|
||||
r = manager_start_unit(u->manager, u->measure_unit, &error, &u->measure_job);
|
||||
if (r < 0)
|
||||
return log_full_errno(sd_bus_error_has_name(&error, BUS_ERROR_UNIT_MASKED) ? LOG_DEBUG : LOG_WARNING,
|
||||
r, "Failed to start user measurement service '%s', ignoring: %s",
|
||||
u->measure_unit, bus_error_message(&error, r));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool user_wants_service_manager(const User *u) {
|
||||
assert(u);
|
||||
|
||||
@@ -512,6 +562,14 @@ int user_start(User *u) {
|
||||
/* Set slice parameters */
|
||||
(void) user_update_slice(u);
|
||||
|
||||
/* Measure the user record into the 'login' NvPCR. We do this *before* starting the runtime dir
|
||||
* (and, further below, the service manager): manager_start_unit() is synchronous, so by the
|
||||
* time those units are enqueued the measurement's start job already exists, and their
|
||||
* After=systemd-pcrlogin@%i.service ordering then guarantees the measurement completes before
|
||||
* the user's session becomes available. Best-effort and never stopped again, so each user is
|
||||
* measured exactly once per boot. */
|
||||
(void) user_start_measure(u);
|
||||
|
||||
(void) user_start_runtime_dir(u);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@ typedef struct User {
|
||||
char *service_manager_unit;
|
||||
char *service_manager_job;
|
||||
|
||||
/* systemd-pcrlogin@UID.service — measures the user record into the 'login' NvPCR on first login.
|
||||
* Unlike the units above, logind starts this but never stops it: it must run exactly once per boot,
|
||||
* so it lives in system.slice, stays active until reboot, and is intentionally absent from
|
||||
* user_stop_service(). */
|
||||
char *measure_unit;
|
||||
char *measure_job;
|
||||
|
||||
Session *display;
|
||||
|
||||
dual_timestamp timestamp; /* When this User object was 'started' the first time */
|
||||
|
||||
+45
-24
@@ -10,17 +10,19 @@
|
||||
#include "efi-loader.h"
|
||||
#include "escape.h"
|
||||
#include "format-table.h"
|
||||
#include "help-util.h"
|
||||
#include "json-util.h"
|
||||
#include "main-func.h"
|
||||
#include "options.h"
|
||||
#include "parse-argument.h"
|
||||
#include "pcrextend-util.h"
|
||||
#include "pretty-print.h"
|
||||
#include "string-table.h"
|
||||
#include "string-util.h"
|
||||
#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,40 +43,31 @@ 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
|
||||
|
||||
static int help(void) {
|
||||
_cleanup_free_ char *link = NULL;
|
||||
_cleanup_(table_unrefp) Table *options = NULL;
|
||||
int r;
|
||||
|
||||
r = terminal_urlify_man("systemd-pcrextend", "8", &link);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
r = option_parser_get_help_table(&options);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
printf("%1$s [OPTIONS...] WORD\n"
|
||||
"%1$s [OPTIONS...] --file-system=PATH\n"
|
||||
"%1$s [OPTIONS...] --machine-id\n"
|
||||
"%1$s [OPTIONS...] --product-id\n"
|
||||
"\n%2$sExtend a TPM2 PCR with boot phase, machine ID, or file system ID.%3$s\n",
|
||||
program_invocation_short_name,
|
||||
ansi_highlight(),
|
||||
ansi_normal());
|
||||
|
||||
printf("\n%sOptions:%s\n",
|
||||
ansi_underline(),
|
||||
ansi_normal());
|
||||
help_cmdline("[OPTIONS...] WORD");
|
||||
help_cmdline("[OPTIONS...] --file-system=PATH");
|
||||
help_cmdline("[OPTIONS...] --machine-id");
|
||||
help_cmdline("[OPTIONS...] --product-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);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
printf("\nSee the %s for details.\n", link);
|
||||
help_man_page_reference("systemd-pcrextend", "8");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -169,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;
|
||||
@@ -185,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.");
|
||||
@@ -199,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;
|
||||
}
|
||||
@@ -496,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.");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
|
||||
#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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name" : "login",
|
||||
"algorithm" : "sha256",
|
||||
"nvIndex" : {{TPM2_NVPCR_BASE + 3}},
|
||||
"priority" : 800
|
||||
}
|
||||
@@ -82,6 +82,41 @@ AAA_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'aaa'" | cut -d: -f1)"
|
||||
ZZZ_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'zzz'" | cut -d: -f1)"
|
||||
test "$ZZZ_LINE" -lt "$AAA_LINE"
|
||||
|
||||
# Test the --login= mode and the 'login' NvPCR, used in production by systemd-pcrlogin@.service.
|
||||
if [[ -f /usr/lib/nvpcr/login.nvpcr ]]; then
|
||||
login_nvpcr_value() {
|
||||
systemd-analyze nvpcrs login --json=pretty | jq -r '.[] | select(.name=="login") | .value'
|
||||
}
|
||||
|
||||
# Extract the most recently measured word for the 'login' NvPCR from the event log.
|
||||
login_last_word() {
|
||||
jq --seq --slurp -r '[.[] | select(.content.nvIndexName=="login") | .content.string] | last' </run/log/systemd/tpm2-measure.log
|
||||
}
|
||||
|
||||
# Measure root's user record. This lazily initializes the 'login' NvPCR if it isn't already.
|
||||
"$SD_PCREXTEND" --login=root
|
||||
|
||||
# The 'login' NvPCR must now exist and carry a non-empty value.
|
||||
LOGIN_DIGEST1="$(login_nvpcr_value)"
|
||||
test -n "$LOGIN_DIGEST1"
|
||||
test "$LOGIN_DIGEST1" != "null"
|
||||
|
||||
# A matching event log entry must be present (the word is "login:<name>:<canonical json>").
|
||||
grep -F '"nvIndexName":"login","string":"login:root:' /run/log/systemd/tpm2-measure.log >/dev/null
|
||||
LOGIN_WORD_BY_NAME="$(login_last_word)"
|
||||
|
||||
# Looking the same user up by numeric UID must yield the identical measured word
|
||||
# (systemd-pcrextend uses USERDB_PARSE_NUMERIC, and systemd-pcrlogin@.service is instanced by UID).
|
||||
"$SD_PCREXTEND" --login=0
|
||||
LOGIN_WORD_BY_UID="$(login_last_word)"
|
||||
test "$LOGIN_WORD_BY_NAME" = "$LOGIN_WORD_BY_UID"
|
||||
|
||||
# Direct tool invocations always re-extend (the once-per-boot guarantee lives in the unit's
|
||||
# RemainAfterExit=yes, not in the tool), so the NvPCR value must have advanced.
|
||||
LOGIN_DIGEST2="$(login_nvpcr_value)"
|
||||
test "$LOGIN_DIGEST2" != "$LOGIN_DIGEST1"
|
||||
fi
|
||||
|
||||
systemd-analyze identify-tpm2
|
||||
udevadm test-builtin 'tpm2_id identify' /dev/tpmrm0
|
||||
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
@@ -10,7 +10,10 @@
|
||||
[Unit]
|
||||
Description=User Runtime Directory /run/user/%i
|
||||
Documentation=man:user@.service(5)
|
||||
After=systemd-logind.service dbus.service
|
||||
# Order after the user record measurement (started by logind before this unit), so that the user's record is
|
||||
# measured into the 'login' TPM2 NvPCR before the runtime dir — and thus the user's session — becomes
|
||||
# available. This is ordering only (no Wants=/Requires=): a measurement failure must never block login.
|
||||
After=systemd-logind.service dbus.service systemd-pcrlogin@%i.service
|
||||
IgnoreOnIsolate=yes
|
||||
|
||||
[Service]
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
Description=User Manager for UID %i
|
||||
Documentation=man:user@.service(5)
|
||||
BindsTo=user-runtime-dir@%i.service
|
||||
After=systemd-logind.service user-runtime-dir@%i.service dbus.service systemd-oomd.service
|
||||
# Order after the user record measurement (see user-runtime-dir@.service), so the user's record is measured
|
||||
# into the 'login' TPM2 NvPCR before the user manager starts. Ordering only, so login is never blocked on it.
|
||||
After=systemd-logind.service user-runtime-dir@%i.service dbus.service systemd-oomd.service systemd-pcrlogin@%i.service
|
||||
IgnoreOnIsolate=yes
|
||||
|
||||
[Service]
|
||||
|
||||
Reference in New Issue
Block a user