summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--catalog/systemd.catalog.in12
-rw-r--r--man/rules/meson.build4
-rw-r--r--man/systemd-measure.xml3
-rw-r--r--man/systemd-pcrphase.service.xml134
-rw-r--r--meson.build9
-rw-r--r--src/boot/pcrphase.c262
-rw-r--r--src/systemd/sd-messages.h3
7 files changed, 426 insertions, 1 deletions
diff --git a/catalog/systemd.catalog.in b/catalog/systemd.catalog.in
index 8cd284c195..56307003f9 100644
--- a/catalog/systemd.catalog.in
+++ b/catalog/systemd.catalog.in
@@ -527,3 +527,15 @@ Support: %SUPPORT_URL%
For the first time during the current boot an NTP synchronization has been
acquired and the local system clock adjustment has been initiated.
+
+-- 3f7d5ef3e54f4302b4f0b143bb270cab
+Subject: TPM PCR Extended
+Defined-By: systemd
+Support: %SUPPORT_URL%
+
+The string '@MEASURING@' has been extended into Trusted Platform Module's (TPM)
+Platform Configuration Register (PCR) @PCR@, on banks @BANKS@.
+
+Whenever the system transitions to a new runtime phase, a different string is
+extended into the specified PCR, to ensure that security policies for TPM-bound
+secrets and other resources are limited to specific phases of the runtime.
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 4f3fe0da7c..a250326a4d 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -966,6 +966,10 @@ manpages = [
['systemd-nspawn', '1', [], ''],
['systemd-oomd.service', '8', ['systemd-oomd'], 'ENABLE_OOMD'],
['systemd-path', '1', [], ''],
+ ['systemd-pcrphase.service',
+ '8',
+ ['systemd-pcrphase', 'systemd-pcrphase-initrd.service'],
+ 'HAVE_GNU_EFI'],
['systemd-portabled.service', '8', ['systemd-portabled'], 'ENABLE_PORTABLED'],
['systemd-pstore.service', '8', ['systemd-pstore'], 'ENABLE_PSTORE'],
['systemd-quotacheck.service',
diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml
index 69ac348184..ab5c9787d6 100644
--- a/man/systemd-measure.xml
+++ b/man/systemd-measure.xml
@@ -250,7 +250,8 @@
<citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
<citerefentry project='man-pages'><refentrytitle>objcopy</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
- <citerefentry><refentrytitle>systemd-cryptsetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ <citerefentry><refentrytitle>systemd-cryptsetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>systemd-pcrphase.service</refentrytitle><manvolnum>1</manvolnum></citerefentry>
</para>
</refsect1>
diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml
new file mode 100644
index 0000000000..61f1fe06fd
--- /dev/null
+++ b/man/systemd-pcrphase.service.xml
@@ -0,0 +1,134 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="systemd-pcrphase.service" conditional='HAVE_GNU_EFI'
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+ <refentryinfo>
+ <title>systemd-pcrphase.service</title>
+ <productname>systemd</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>systemd-pcrphase.service</refentrytitle>
+ <manvolnum>8</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>systemd-pcrphase.service</refname>
+ <refname>systemd-pcrphase-initrd.service</refname>
+ <refname>systemd-pcrphase</refname>
+ <refpurpose>Mark current boot process as successful</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para><filename>systemd-pcrphase.service</filename></para>
+ <para><filename>systemd-pcrphase-initrd.service</filename></para>
+ <para><filename>/usr/lib/systemd/system-pcrphase</filename> <replaceable>STRING</replaceable></para>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para><filename>systemd-pcrphase.service</filename> and
+ <filename>systemd-pcrphase-initrd.service</filename> are system services that measure specific strings
+ into TPM2 PCR 11 during boot.</para>
+
+ <para>These services require
+ <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry> to be
+ used in a unified kernel image (UKI) setup. They execute no operation when invoked when the stub has not
+ been used to invoke the kernel. The stub will measure the invoked kernel and associated vendor resources
+ into PCR 11 before handing control to it; once userspace is invoked these services then will extend
+ certain literal strings indicating various phases of the boot process into TPM2 PCR 11. During a regular
+ boot process the following strings are extended into PCR 11.</para>
+
+ <orderedlist>
+ <listitem><para><literal>enter-initrd</literal> is extended into PCR 11 early when the initrd
+ initializes, before activating system extension images for the initrd. It is supposed to act as barrier
+ between the time where the kernel initializes, and where the initrd starts operating and enables
+ system extension images, i.e. code shipped outside of the UKI. (This string is extended at start of
+ <filename>systemd-pcrphase-initrd.service</filename>.)</para></listitem>
+
+ <listitem><para><literal>leave-initrd</literal> is extended into PCR 11 when the initrd is about to
+ transition into the host file system, i.e. when it achieved its purpose. It is supposed to act as
+ barrier between kernel/initrd code and host OS code. (This string is extended at stop of
+ <filename>systemd-pcrphase-initrd.service</filename>.)</para></listitem>
+
+ <listitem><para><literal>ready</literal> is extended into PCR 11 during later boot-up, after remote
+ file systems have been activated (i.e. after <filename>remote-fs.target</filename>), but before users
+ are permitted to log in (i.e. before <filename>systemd-user-sessions.service</filename>). It is
+ supposed to act as barrier between the time where unprivileged regular users are still prohibited to
+ log in and where they are allowed to log in. (This string is extended at start of
+ <filename>systemd-pcrphase.service</filename>.)</para></listitem>
+
+ <listitem><para><literal>shutdown</literal> is extended into PCR 11 during system shutdown. It is
+ supposed to act as barrier between the time the system is fully up and running and where it is about to
+ shut down. (This string is extended at stop of
+ <filename>systemd-pcrphase.service</filename>.)</para></listitem>
+ </orderedlist>
+
+ <para>During a regular system lifecycle, the strings <literal>enter-initrd</literal> →
+ <literal>leave-initrd</literal> → <literal>ready</literal> → <literal>shutdown</literal> are extended into
+ PCR 11, one after the other.</para>
+
+ <para>Specific phases of the boot process may be referenced via the series of strings measured, separated
+ by colons (the "boot path"). For example, the boot path for the regular system runtime is
+ <literal>enter-initrd:leave-initrd:ready</literal>, while the one for the initrd is just
+ <literal>enter-initrd</literal>. The boot path for the the boot phase before the initrd, is an empty
+ string; because that's hard to pass around a single colon (<literal>:</literal>) may be used
+ instead. Note that the aforementioned four strings are just the default strings and individual systems
+ might measure other strings at other times, and thus implement different and more fine-grained boot
+ phases to bind policy to.</para>
+
+ <para>By binding policy of TPM2 objects to a specific boot path it is possible to restrict access to them
+ to specific phases of the boot process, for example making it impossible to access the root file system's
+ encryption key after the system transitioned from the initrd into the host root file system.</para>
+
+ <para>Use
+ <citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry> to
+ pre-calculate expected PCR 11 values for specific boot phases (via the <option>--phase=</option> switch).</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Options</title>
+
+ <para>The <filename>/usr/lib/systemd/system-pcrphase</filename> executable may also be invoked from the
+ command line, where it expects the word to extend into PCR 11, as well as the following switches:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><option>--bank=</option></term>
+
+ <listitem><para>Takes the PCR banks to extend the specified word into. If not specified the tool
+ automatically determines all enabled PCR banks and measures the word into all of
+ them.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--tpm2-device=</option><replaceable>PATH</replaceable></term>
+
+ <listitem><para>Controls which TPM2 device to use. Expects a device node path referring to the TPM2
+ chip (e.g. <filename>/dev/tpmrm0</filename>). Alternatively the special value <literal>auto</literal>
+ may be specified, in order to automatically determine the device node of a suitable TPM2 device (of
+ which there must be exactly one). The special value <literal>list</literal> may be used to enumerate
+ all suitable TPM2 devices currently discovered.</para></listitem>
+ </varlistentry>
+
+ <xi:include href="standard-options.xml" xpointer="help" />
+ <xi:include href="standard-options.xml" xpointer="version" />
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+
+</refentry>
diff --git a/meson.build b/meson.build
index a1bedc220b..6022617832 100644
--- a/meson.build
+++ b/meson.build
@@ -2566,6 +2566,15 @@ if conf.get('HAVE_BLKID') == 1 and conf.get('HAVE_GNU_EFI') == 1
install_rpath : rootpkglibdir,
install : true,
install_dir : rootlibexecdir)
+ executable(
+ 'systemd-pcrphase',
+ 'src/boot/pcrphase.c',
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [libopenssl, tpm2],
+ install_rpath : rootpkglibdir,
+ install : true,
+ install_dir : rootlibexecdir)
endif
endif
diff --git a/src/boot/pcrphase.c b/src/boot/pcrphase.c
new file mode 100644
index 0000000000..3be89bc286
--- /dev/null
+++ b/src/boot/pcrphase.c
@@ -0,0 +1,262 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include <sd-messages.h>
+
+#include "efivars.h"
+#include "main-func.h"
+#include "openssl-util.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "tpm-pcr.h"
+#include "tpm2-util.h"
+
+static char *arg_tpm2_device = NULL;
+static char **arg_banks = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-pcrphase", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] COMMAND ...\n"
+ "\n%5$sMeasure boot phase into TPM2 PCR 11.%6$s\n"
+ "\n%3$sOptions:%4$s\n"
+ " -h --help Show this help\n"
+ " --version Print version\n"
+ " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n"
+ " --tpm2-device=PATH Use specified TPM2 device\n"
+ "\nSee the %2$s for details.\n",
+ program_invocation_short_name,
+ link,
+ ansi_underline(),
+ ansi_normal(),
+ ansi_highlight(),
+ ansi_normal());
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_BANK,
+ ARG_TPM2_DEVICE,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "bank", required_argument, NULL, ARG_BANK },
+ { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ help(0, NULL, NULL);
+ return 0;
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_BANK: {
+ const EVP_MD *implementation;
+
+ implementation = EVP_get_digestbyname(optarg);
+ if (!implementation)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", optarg);
+
+ if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0)
+ return log_oom();
+
+ break;
+ }
+
+ case ARG_TPM2_DEVICE: {
+ _cleanup_free_ char *device = NULL;
+
+ if (streq(optarg, "list"))
+ return tpm2_list_devices();
+
+ if (!streq(optarg, "auto")) {
+ device = strdup(optarg);
+ if (!device)
+ return log_oom();
+ }
+
+ free_and_replace(arg_tpm2_device, device);
+ break;
+ }
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int determine_banks(struct tpm2_context *c) {
+ _cleanup_free_ TPMI_ALG_HASH *algs = NULL;
+ int n_algs, r;
+
+ assert(c);
+
+ if (!strv_isempty(arg_banks)) /* Explicitly configured? Then use that */
+ return 0;
+
+ n_algs = tpm2_get_good_pcr_banks(c->esys_context, UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &algs);
+ if (n_algs <= 0)
+ return n_algs;
+
+ for (int i = 0; i < n_algs; i++) {
+ const EVP_MD *implementation;
+ const char *salg;
+
+ salg = tpm2_pcr_bank_to_string(algs[i]);
+ if (!salg)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 operates with unknown PCR algorithm, can't measure.");
+
+ implementation = EVP_get_digestbyname(salg);
+ if (!implementation)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 operates with unsupported PCR algorithm, can't measure.");
+
+ r = strv_extend(&arg_banks, EVP_MD_name(implementation));
+ if (r < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
+ _cleanup_free_ char *joined = NULL, *pcr_string = NULL;
+ const char *word;
+ unsigned pcr_nr;
+ size_t length;
+ TSS2_RC rc;
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (optind+1 != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument.");
+
+ word = argv[optind];
+
+ /* Refuse to measure an empty word. We want to be able to write the series of measured words
+ * separated by colons, where multiple separating colons are collapsed. Thus it makes sense to
+ * disallow an empty word to avoid ambiguities. */
+ if (isempty(word))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "String to measure cannot be empty, refusing.");
+
+ length = strlen(word);
+
+ /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */
+ r = efi_get_variable_string(EFI_LOADER_VARIABLE(StubPcrKernelImage), &pcr_string);
+ if (r == -ENOENT) {
+ log_info("Kernel stub did not measure kernel image into PCR %u, skipping measurement.", TPM_PCR_INDEX_KERNEL_IMAGE);
+ return EXIT_SUCCESS;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to read StubPcrKernelImage EFI variable: %m");
+
+ /* Let's validate that the stub announced PCR 11 as we expected. */
+ r = safe_atou(pcr_string, &pcr_nr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse StubPcrKernelImage EFI variable: %s", pcr_string);
+ if (pcr_nr != TPM_PCR_INDEX_KERNEL_IMAGE)
+ return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), "Kernel stub measured kernel image into PCR %u, which is different than expected %u.", pcr_nr, TPM_PCR_INDEX_KERNEL_IMAGE);
+
+ r = dlopen_tpm2();
+ if (r < 0)
+ return log_error_errno(r, "Failed to load TPM2 libraries: %m");
+
+ r = tpm2_context_init(arg_tpm2_device, &c);
+ if (r < 0)
+ return r;
+
+ r = determine_banks(&c);
+ if (r < 0)
+ return r;
+ if (strv_isempty(arg_banks)) /* Still none? */
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate.");
+
+ TPML_DIGEST_VALUES values = {};
+ STRV_FOREACH(bank, arg_banks) {
+ const EVP_MD *implementation;
+ int id;
+
+ assert_se(implementation = EVP_get_digestbyname(*bank));
+
+ if (values.count >= ELEMENTSOF(values.digests))
+ return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many banks selected.");
+
+ if ((size_t) EVP_MD_size(implementation) > sizeof(values.digests[values.count].digest))
+ return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Hash result too large for TPM2.");
+
+ id = tpm2_pcr_bank_from_string(EVP_MD_name(implementation));
+ if (id < 0)
+ return log_error_errno(id, "Can't map hash name to TPM2.");
+
+ values.digests[values.count].hashAlg = id;
+
+ if (EVP_Digest(word, length, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word.");
+
+ values.count++;
+ }
+
+ joined = strv_join(arg_banks, ", ");
+ if (!joined)
+ return log_oom();
+
+ log_debug("Measuring '%s' into PCR index %u, banks %s.", word, TPM_PCR_INDEX_KERNEL_IMAGE, joined);
+
+ rc = sym_Esys_PCR_Extend(
+ c.esys_context,
+ ESYS_TR_PCR0 + TPM_PCR_INDEX_KERNEL_IMAGE, /* → PCR 11 */
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &values);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(
+ SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to measure '%s': %s",
+ word,
+ sym_Tss2_RC_Decode(rc));
+
+ log_struct(LOG_INFO,
+ "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR,
+ LOG_MESSAGE("Successfully extended PCR index %u with '%s' (banks %s).", TPM_PCR_INDEX_KERNEL_IMAGE, word, joined),
+ "MEASURING=%s", word,
+ "PCR=%u", TPM_PCR_INDEX_KERNEL_IMAGE,
+ "BANKS=%s", joined);
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/systemd/sd-messages.h b/src/systemd/sd-messages.h
index ffb9ba4739..51241c9426 100644
--- a/src/systemd/sd-messages.h
+++ b/src/systemd/sd-messages.h
@@ -189,6 +189,9 @@ _SD_BEGIN_DECLARATIONS;
#define SD_MESSAGE_SHUTDOWN_CANCELED SD_ID128_MAKE(24,9f,6f,b9,e6,e2,42,8c,96,f3,f0,87,56,81,ff,a3)
#define SD_MESSAGE_SHUTDOWN_CANCELED_STR SD_ID128_MAKE_STR(24,9f,6f,b9,e6,e2,42,8c,96,f3,f0,87,56,81,ff,a3)
+#define SD_MESSAGE_TPM_PCR_EXTEND SD_ID128_MAKE(3f,7d,5e,f3,e5,4f,43,02,b4,f0,b1,43,bb,27,0c,ab)
+#define SD_MESSAGE_TPM_PCR_EXTEND_STR SD_ID128_MAKE_STR(3f,7d,5e,f3,e5,4f,43,02,b4,f0,b1,43,bb,27,0c,ab)
+
_SD_END_DECLARATIONS;
#endif