diff options
Diffstat (limited to 'src/pcrextend')
-rw-r--r-- | src/pcrextend/meson.build | 19 | ||||
-rw-r--r-- | src/pcrextend/pcrextend.c | 391 |
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); |