summaryrefslogtreecommitdiffstats
path: root/src/pcrextend
diff options
context:
space:
mode:
Diffstat (limited to 'src/pcrextend')
-rw-r--r--src/pcrextend/meson.build19
-rw-r--r--src/pcrextend/pcrextend.c391
2 files changed, 410 insertions, 0 deletions
diff --git a/src/pcrextend/meson.build b/src/pcrextend/meson.build
new file mode 100644
index 0000000..05c5350
--- /dev/null
+++ b/src/pcrextend/meson.build
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+executables += [
+ libexec_template + {
+ 'name' : 'systemd-pcrextend',
+ 'conditions' : [
+ 'HAVE_BLKID',
+ 'ENABLE_BOOTLOADER',
+ 'HAVE_OPENSSL',
+ 'HAVE_TPM2',
+ ],
+ 'sources' : files('pcrextend.c'),
+ 'dependencies' : [
+ libblkid,
+ libopenssl,
+ tpm2,
+ ],
+ },
+]
diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c
new file mode 100644
index 0000000..1295949
--- /dev/null
+++ b/src/pcrextend/pcrextend.c
@@ -0,0 +1,391 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include <sd-messages.h>
+
+#include "build.h"
+#include "efi-loader.h"
+#include "escape.h"
+#include "main-func.h"
+#include "openssl-util.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "pcrextend-util.h"
+#include "pretty-print.h"
+#include "strv.h"
+#include "tpm2-pcr.h"
+#include "tpm2-util.h"
+#include "varlink.h"
+#include "varlink-io.systemd.PCRExtend.h"
+
+static bool arg_graceful = false;
+static char *arg_tpm2_device = NULL;
+static char **arg_banks = NULL;
+static char *arg_file_system = NULL;
+static bool arg_machine_id = false;
+static unsigned arg_pcr_index = UINT_MAX;
+static bool arg_varlink = false;
+
+STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_file_system, freep);
+
+#define EXTENSION_STRING_SAFE_LIMIT 1024
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-pcrextend", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] WORD\n"
+ "%1$s [OPTIONS...] --file-system=PATH\n"
+ "%1$s [OPTIONS...] --machine-id\n"
+ "\n%5$sExtend a TPM2 PCR with boot phase, machine ID, or file system ID.%6$s\n"
+ "\n%3$sOptions:%4$s\n"
+ " -h --help Show this help\n"
+ " --version Print version\n"
+ " --bank=DIGEST Select TPM PCR bank (SHA1, SHA256)\n"
+ " --pcr=INDEX Select TPM PCR index (0…23)\n"
+ " --tpm2-device=PATH Use specified TPM2 device\n"
+ " --graceful Exit gracefully if no TPM2 device is found\n"
+ " --file-system=PATH Measure UUID/labels of file system into PCR 15\n"
+ " --machine-id Measure machine ID into PCR 15\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_PCR,
+ ARG_TPM2_DEVICE,
+ ARG_GRACEFUL,
+ ARG_FILE_SYSTEM,
+ ARG_MACHINE_ID,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "bank", required_argument, NULL, ARG_BANK },
+ { "pcr", required_argument, NULL, ARG_PCR },
+ { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+ { "graceful", no_argument, NULL, ARG_GRACEFUL },
+ { "file-system", required_argument, NULL, ARG_FILE_SYSTEM },
+ { "machine-id", no_argument, NULL, ARG_MACHINE_ID },
+ {}
+ };
+
+ int c, r;
+
+ 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_PCR:
+ r = tpm2_pcr_index_from_string(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse PCR index: %s", optarg);
+
+ arg_pcr_index = r;
+ 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 ARG_GRACEFUL:
+ arg_graceful = true;
+ break;
+
+ case ARG_FILE_SYSTEM:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_file_system);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_MACHINE_ID:
+ arg_machine_id = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (arg_file_system && arg_machine_id)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system= and --machine-id may not be combined.");
+
+ r = varlink_invocation(VARLINK_ALLOW_ACCEPT);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
+ if (r > 0)
+ arg_varlink = true;
+ else if (arg_pcr_index == UINT_MAX)
+ arg_pcr_index = (arg_file_system || arg_machine_id) ?
+ TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */
+ TPM2_PCR_KERNEL_BOOT; /* → PCR 11 */
+
+ return 1;
+}
+
+static int determine_banks(Tpm2Context *c, unsigned target_pcr_nr) {
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ assert(c);
+
+ if (!strv_isempty(arg_banks)) /* Explicitly configured? Then use that */
+ return 0;
+
+ r = tpm2_get_good_pcr_banks_strv(c, UINT32_C(1) << target_pcr_nr, &l);
+ if (r < 0)
+ return log_error_errno(r, "Could not verify pcr banks: %m");
+
+ strv_free_and_replace(arg_banks, l);
+ return 0;
+}
+
+static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2UserspaceEventType event) {
+ _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL;
+ int r;
+
+ r = tpm2_context_new(arg_tpm2_device, &c);
+ if (r < 0)
+ return r;
+
+ r = determine_banks(c, pcr);
+ 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.");
+
+ _cleanup_free_ char *joined_banks = NULL;
+ joined_banks = strv_join(arg_banks, ", ");
+ if (!joined_banks)
+ return log_oom();
+
+ _cleanup_free_ char *safe = NULL;
+ if (size > EXTENSION_STRING_SAFE_LIMIT) {
+ safe = cescape_length(data, EXTENSION_STRING_SAFE_LIMIT);
+ if (!safe)
+ return log_oom();
+
+ if (!strextend(&safe, "..."))
+ return log_oom();
+ } else {
+ safe = cescape_length(data, size);
+ if (!safe)
+ return log_oom();
+ }
+
+ log_debug("Measuring '%s' into PCR index %u, banks %s.", safe, pcr, joined_banks);
+
+ r = tpm2_extend_bytes(c, arg_banks, pcr, data, size, /* secret= */ NULL, /* secret_size= */ 0, event, safe);
+ if (r < 0)
+ return log_error_errno(r, "Could not extend PCR: %m");
+
+ log_struct(LOG_INFO,
+ "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR,
+ LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", pcr, safe, joined_banks),
+ "MEASURING=%s", safe,
+ "PCR=%u", pcr,
+ "BANKS=%s", joined_banks);
+
+ return 0;
+}
+
+typedef struct MethodExtendParameters {
+ unsigned pcr;
+ const char *text;
+ struct iovec data;
+} MethodExtendParameters;
+
+static void method_extend_parameters_done(MethodExtendParameters *p) {
+ assert(p);
+
+ iovec_done(&p->data);
+}
+
+static int vl_method_extend(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "pcr", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(MethodExtendParameters, pcr), JSON_MANDATORY },
+ { "text", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 },
+ { "data", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodExtendParameters, data), 0 },
+ {}
+ };
+ _cleanup_(method_extend_parameters_done) MethodExtendParameters p = {
+ .pcr = UINT_MAX,
+ };
+ int r;
+
+ assert(link);
+
+ r = varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ if (!TPM2_PCR_INDEX_VALID(p.pcr))
+ return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "pcr")));
+
+ if (p.text) {
+ /* Specifying both the text string and the binary data is not allowed */
+ if (p.data.iov_base)
+ return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "data")));
+
+ r = extend_now(p.pcr, p.text, strlen(p.text), _TPM2_USERSPACE_EVENT_TYPE_INVALID);
+ } else if (p.data.iov_base)
+ r = extend_now(p.pcr, p.data.iov_base, p.data.iov_len, _TPM2_USERSPACE_EVENT_TYPE_INVALID);
+ else
+ return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "text")));
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_free_ char *word = NULL;
+ Tpm2UserspaceEventType event;
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (arg_varlink) {
+ _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL;
+
+ /* Invocation as Varlink service */
+
+ r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate Varlink server: %m");
+
+ r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_PCRExtend);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add Varlink interface: %m");
+
+ r = varlink_server_bind_method(varlink_server, "io.systemd.PCRExtend.Extend", vl_method_extend);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind Varlink method: %m");
+
+ r = varlink_server_loop_auto(varlink_server);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run Varlink event loop: %m");
+
+ return EXIT_SUCCESS;
+ }
+
+ if (arg_file_system) {
+ if (optind != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument.");
+
+ r = pcrextend_file_system_word(arg_file_system, &word, NULL);
+ if (r < 0)
+ return r;
+
+ event = TPM2_EVENT_FILESYSTEM;
+
+ } else if (arg_machine_id) {
+
+ if (optind != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument.");
+
+ r = pcrextend_machine_id_word(&word);
+ if (r < 0)
+ return r;
+
+ event = TPM2_EVENT_MACHINE_ID;
+
+ } else {
+ if (optind+1 != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument.");
+
+ word = strdup(argv[optind]);
+ if (!word)
+ return log_oom();
+
+ /* 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.");
+
+ event = TPM2_EVENT_PHASE;
+ }
+
+ if (arg_graceful && tpm2_support() != TPM2_SUPPORT_FULL) {
+ log_notice("No complete TPM2 support detected, exiting gracefully.");
+ return EXIT_SUCCESS;
+ }
+
+ /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */
+ r = efi_measured_uki(LOG_ERR);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ log_info("Kernel stub did not measure kernel image into PCR %i, skipping userspace measurement, too.", TPM2_PCR_KERNEL_BOOT);
+ return EXIT_SUCCESS;
+ }
+
+ r = extend_now(arg_pcr_index, word, strlen(word), event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create TPM2 context: %m");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_MAIN_FUNCTION(run);