]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
boot: add new pcrphase tool to measure barrier strings into PCR 11
authorLennart Poettering <lennart@poettering.net>
Fri, 16 Sep 2022 21:57:26 +0000 (23:57 +0200)
committerLennart Poettering <lennart@poettering.net>
Thu, 22 Sep 2022 14:52:06 +0000 (16:52 +0200)
catalog/systemd.catalog.in
man/rules/meson.build
man/systemd-measure.xml
man/systemd-pcrphase.service.xml [new file with mode: 0644]
meson.build
src/boot/pcrphase.c [new file with mode: 0644]
src/systemd/sd-messages.h

index 8cd284c195c1e1a65e90df54f9abc8f56c508dae..56307003f9b3e210b62ddc0c566046bbee067e4d 100644 (file)
@@ -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.
index 4f3fe0da7c74aa44727a11ab0704cccdcaacb716..a250326a4d32d0b5435512b85a3c13b0084ca685 100644 (file)
@@ -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',
index 69ac34818433009575c3c0392846958a842cf136..ab5c9787d64450a5e50149fd93da2e24efaf681a 100644 (file)
       <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 (file)
index 0000000..61f1fe0
--- /dev/null
@@ -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>
index a1bedc220b430a444e7515fddd38e0fa9c7fcb2d..6022617832012a39a4d9ed2cffd1f5aa5f9577b5 100644 (file)
@@ -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 (file)
index 0000000..3be89bc
--- /dev/null
@@ -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);
index ffb9ba473956a27ffa36b4c5e553527fcd97fe8d..51241c942602dbe868b7b51065c5e9acd20465a7 100644 (file)
@@ -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