From: Lennart Poettering Date: Mon, 25 Jul 2022 22:13:16 +0000 (+0200) Subject: measure: add new tool to precalculate PCR values for a kernel image X-Git-Tag: v252-rc1~542^2~2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ca1092dc15ce49d2b677aa326836588839bb6fe1;p=thirdparty%2Fsystemd.git measure: add new tool to precalculate PCR values for a kernel image For now, this simply outputs the PCR hash values expected for a kernel image, if it's measured like sd-stub would do it. (Later on, we can extend the tool, to optionally sign these pre-calculated measurements, in order to implement signed PCR policies for disk encryption.) --- diff --git a/man/rules/meson.build b/man/rules/meson.build index 277ab5e5933..4f3fe0da7c7 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -953,6 +953,7 @@ manpages = [ 'systemd-makefs', 'systemd-mkswap@.service'], ''], + ['systemd-measure', '1', [], 'HAVE_GNU_EFI'], ['systemd-modules-load.service', '8', ['systemd-modules-load'], 'HAVE_KMOD'], ['systemd-mount', '1', ['systemd-umount'], ''], ['systemd-network-generator.service', '8', ['systemd-network-generator'], ''], diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml new file mode 100644 index 00000000000..e6b8d31a881 --- /dev/null +++ b/man/systemd-measure.xml @@ -0,0 +1,154 @@ + + + + + + + + systemd-measure + systemd + + + + systemd-measure + 1 + + + + systemd-measure + Pre-calculate expected TPM2 PCR values for booted unified kernel images + + + + + /usr/lib/systemd/systemd-measure OPTIONS + + + + + Description + + Note: this command is experimental for now. While it is likely to become a regular component of + systemd, it might still change in behaviour and interface. + + systemd-measure is a tool that may be used to pre-calculate the expected TPM2 + PCR 11 values that should be seen when a unified Linux kernel image based on + systemd-stub7 is + booted up. It accepts paths to the ELF kernel image file, initial ram disk image file, devicetree file, + kernel command line file, + os-release5 file, and + boot splash file that make up the unified kernel image, and determines the PCR values expected to be in + place after booting the image. Calculation starts with a zero-initialized PCR 11, and is executed in a + fashion compatible with what systemd-stub does at boot. + + + + Commands + + The following commands are understood: + + + + status + + This is the default command if none is specified. This queries the local system's + TPM2 PCR 11+12+13 values and displays them. The data is written in a similar format as the + calculate command below, and may be used to quickly compare expectation with + reality. + + + + calculate + + Pre-calculate the expected value seen in PCR register 11 after boot-up of a unified + kernel image consisting of the components specified with , + , , , + , , see below. Only is + mandatory. + + + + + + Options + + The following options are understood: + + + + + + + + + + + When used with the calculate verb, configures the files to read + the unified kernel image components from. Each option corresponds with the equally named section in + the unified kernel PE file. The switch expects the path to the ELF kernel + file that the unified PE kernel will wrap. All switches except are + optional. Each option may be used at most once. + + + + + + Controls the PCR banks to pre-calculate the PCR values for – in case + calculate is invoked –, or the banks to show in the status + output. May be used more then once to specify multiple banks. If not specified, defaults to the four + banks sha1, sha256, sha384, + sha512. + + + + + + + + + Examples + + + Generate a unified kernel image, and calculate the expected TPM PCR 11 value + + # objcopy \ + --add-section .linux=vmlinux --change-section-vma .linux=0x2000000 \ + --add-section .osrel=os-release.txt --change-section-vma .osrel=0x20000 \ + --add-section .cmdline=cmdline.txt --change-section-vma .cmdline=0x30000 \ + --add-section .initrd=initrd.cpio --change-section-vma .initrd=0x3000000 \ + --add-section .splash=splash.bmp --change-section-vma .splash=0x100000 \ + --add-section .dtb=devicetree.dtb --change-section-vma .dtb=0x40000 \ + /usr/lib/systemd/boot/efi/linuxx64.efi.stub \ + foo.efi +# systemd-measure calculate \ + --linux=vmlinux \ + --osrel=os-release \ + --cmdline=cmdline.txt \ + --initrd=initrd.cpio \ + --splash=splash.bmp \ + --dtb=devicetree.dtb +11:sha1=d775a7b4482450ac77e03ee19bda90bd792d6ec7 +11:sha256=bc6170f9ce28eb051ab465cd62be8cf63985276766cf9faf527ffefb66f45651 +11:sha384=1cf67dff4757e61e5a73d2a21a6694d668629bbc3761747d493f7f49ad720be02fd07263e1f93061243aec599d1ee4b4 +11:sha512=8e79acd3ddbbc8282e98091849c3530f996303c8ac8e87a3b2378b71c8b3a6e86d5c4f41ecea9e1517090c3e8ec0c714821032038f525f744960bcd082d937da + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + See Also + + systemd1, + systemd-stub7, + objcopy1 + + + + diff --git a/meson.build b/meson.build index bfe6b31b959..cf3aa2b4964 100644 --- a/meson.build +++ b/meson.build @@ -2541,6 +2541,18 @@ if conf.get('HAVE_BLKID') == 1 and conf.get('HAVE_GNU_EFI') == 1 install_rpath : rootpkglibdir, install : true, install_dir : systemgeneratordir) + + if conf.get('HAVE_OPENSSL') == 1 + executable( + 'systemd-measure', + 'src/boot/measure.c', + include_directories : includes, + link_with : [libshared], + dependencies : [libopenssl], + install_rpath : rootpkglibdir, + install : true, + install_dir : rootlibexecdir) + endif endif executable( diff --git a/src/boot/measure.c b/src/boot/measure.c new file mode 100644 index 00000000000..29568c5b0d1 --- /dev/null +++ b/src/boot/measure.c @@ -0,0 +1,533 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "alloc-util.h" +#include "efi-loader.h" +#include "fd-util.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "main-func.h" +#include "openssl-util.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "terminal-util.h" +#include "tpm-pcr.h" +#include "tpm2-util.h" +#include "verbs.h" + +/* Tool for pre-calculating expected TPM PCR values based on measured resources. This is intended to be used + * to pre-calculate suitable values for PCR 11, the way sd-stub measures into it. */ + +static char *arg_sections[_UNIFIED_SECTION_MAX] = {}; +static char **arg_banks = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); + +static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) { + for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++) + free((*sections)[c]); +} + +STATIC_DESTRUCTOR_REGISTER(arg_sections, free_sections); + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-measure", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n" + "\n%5$sPre-calculate PCR hash for kernel image.%6$s\n" + "\n%3$sCommands:%4$s\n" + " status Show current PCR values\n" + " calculate Calculate expected PCR values\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Print version\n" + " --linux=PATH Path Linux kernel ELF image\n" + " --osrel=PATH Path to os-release file\n" + " --cmdline=PATH Path to file with kernel command line\n" + " --initrd=PATH Path to initrd image\n" + " --splash=PATH Path to splash bitmap\n" + " --dtb=PATH Path to Devicetree file\n" + " --bank=DIGEST Select TPM bank (SHA1, SHA256)\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_SECTION_FIRST, + ARG_LINUX = _ARG_SECTION_FIRST, + ARG_OSREL, + ARG_CMDLINE, + ARG_INITRD, + ARG_SPLASH, + _ARG_SECTION_LAST, + ARG_DTB = _ARG_SECTION_LAST, + ARG_BANK, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "linux", required_argument, NULL, ARG_LINUX }, + { "osrel", required_argument, NULL, ARG_OSREL }, + { "cmdline", required_argument, NULL, ARG_CMDLINE }, + { "initrd", required_argument, NULL, ARG_INITRD }, + { "splash", required_argument, NULL, ARG_SPLASH }, + { "dtb", required_argument, NULL, ARG_DTB }, + { "bank", required_argument, NULL, ARG_BANK }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + /* Make sure the arguments list and the section list, stays in sync */ + assert_cc(_ARG_SECTION_FIRST + _UNIFIED_SECTION_MAX == _ARG_SECTION_LAST + 1); + + 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_SECTION_FIRST..._ARG_SECTION_LAST: { + UnifiedSection section = c - _ARG_SECTION_FIRST; + + r = parse_path_argument(optarg, /* suppress_root= */ false, arg_sections + section); + if (r < 0) + return r; + break; + } + + 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 '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (strv_isempty(arg_banks)) { + /* If no banks are specifically selected, pick all known banks */ + arg_banks = strv_new("SHA1", "SHA256", "SHA384", "SHA512"); + if (!arg_banks) + return log_oom(); + } + + strv_sort(arg_banks); + strv_uniq(arg_banks); + + return 1; +} + +typedef struct PcrState { + const EVP_MD *md; + void *value; + size_t value_size; +} PcrState; + +static void pcr_state_free_all(PcrState **pcr_state) { + assert(pcr_state); + + if (!*pcr_state) + return; + + for (size_t i = 0; (*pcr_state)[i].value; i++) + free((*pcr_state)[i].value); + + *pcr_state = mfree(*pcr_state); +} + +static void evp_md_ctx_free_all(EVP_MD_CTX **md[]) { + assert(md); + + if (!*md) + return; + + for (size_t i = 0; (*md)[i]; i++) + EVP_MD_CTX_free((*md)[i]); + + *md = mfree(*md); +} + +static int pcr_state_extend(PcrState *pcr_state, const void *data, size_t sz) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mc = NULL; + unsigned value_size; + + assert(pcr_state); + assert(data || sz == 0); + assert(pcr_state->md); + assert(pcr_state->value); + assert(pcr_state->value_size > 0); + + /* Extends a (virtual) PCR by the given data */ + + mc = EVP_MD_CTX_new(); + if (!mc) + return log_oom(); + + if (EVP_DigestInit_ex(mc, pcr_state->md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize %s context.", EVP_MD_name(pcr_state->md)); + + /* First thing we do, is hash the old PCR value */ + if (EVP_DigestUpdate(mc, pcr_state->value, pcr_state->value_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest."); + + /* Then, we hash the new data */ + if (EVP_DigestUpdate(mc, data, sz) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest."); + + if (EVP_DigestFinal_ex(mc, pcr_state->value, &value_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize hash context."); + + assert(value_size == pcr_state->value_size); + return 0; +} + +#define BUFFER_SIZE (16U * 1024U) + +static int measure_pcr(PcrState *pcr_states, size_t n) { + _cleanup_free_ void *buffer = NULL; + int r; + + assert(n > 0); + assert(pcr_states); + + buffer = malloc(BUFFER_SIZE); + if (!buffer) + return log_oom(); + + for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++) { + _cleanup_(evp_md_ctx_free_all) EVP_MD_CTX **mdctx = NULL; + _cleanup_close_ int fd = -1; + uint64_t m = 0; + + if (!arg_sections[c]) + continue; + + fd = open(arg_sections[c], O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", arg_sections[c]); + + /* Allocate one message digest context per bank (NULL terminated) */ + mdctx = new0(EVP_MD_CTX*, n + 1); + if (!mdctx) + return log_oom(); + + for (size_t i = 0; i < n; i++) { + mdctx[i] = EVP_MD_CTX_new(); + if (!mdctx[i]) + return log_oom(); + + if (EVP_DigestInit_ex(mdctx[i], pcr_states[i].md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize data %s context.", EVP_MD_name(pcr_states[i].md)); + } + + for (;;) { + ssize_t sz; + + sz = read(fd, buffer, BUFFER_SIZE); + if (sz < 0) + return log_error_errno(errno, "Failed to read '%s': %m", arg_sections[c]); + if (sz == 0) /* EOF */ + break; + + for (size_t i = 0; i < n; i++) + if (EVP_DigestUpdate(mdctx[i], buffer, sz) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest."); + + m += sz; + } + + fd = safe_close(fd); + + if (m == 0) /* We skip over empty files, the stub does so too */ + continue; + + for (size_t i = 0; i < n; i++) { + _cleanup_free_ void *data_hash = NULL; + unsigned data_hash_size; + + data_hash = malloc(pcr_states[i].value_size); + if (!data_hash) + return log_oom(); + + /* Measure name of section */ + if (EVP_Digest(unified_sections[c], strlen(unified_sections[c]) + 1, data_hash, &data_hash_size, pcr_states[i].md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to hash section name with %s.", EVP_MD_name(pcr_states[i].md)); + + assert(data_hash_size == (unsigned) pcr_states[i].value_size); + + r = pcr_state_extend(pcr_states + i, data_hash, data_hash_size); + if (r < 0) + return r; + + /* Retrieve hash of data an measure it*/ + if (EVP_DigestFinal_ex(mdctx[i], data_hash, &data_hash_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize hash context."); + + assert(data_hash_size == (unsigned) pcr_states[i].value_size); + + r = pcr_state_extend(pcr_states + i, data_hash, data_hash_size); + if (r < 0) + return r; + } + } + + return 0; +} + +static int verb_calculate(int argc, char *argv[], void *userdata) { + _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; + size_t n = 0; + int r; + + if (!arg_sections[UNIFIED_SECTION_LINUX]) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--linux= switch must be specified, refusing."); + + pcr_states = new0(PcrState, strv_length(arg_banks) + 1); + if (!pcr_states) + return log_oom(); + + /* Allocate a PCR state structure, one for each bank */ + STRV_FOREACH(d, arg_banks) { + const EVP_MD *implementation; + _cleanup_free_ void *v = NULL; + int sz; + + assert_se(implementation = EVP_get_digestbyname(*d)); /* Must work, we already checked while parsing command line */ + + sz = EVP_MD_size(implementation); + if (sz <= 0 || sz >= INT_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected digest size: %i", sz); + + v = malloc0(sz); /* initial PCR state is all zeroes */ + if (!v) + return log_oom(); + + pcr_states[n++] = (struct PcrState) { + .md = implementation, + .value = TAKE_PTR(v), + .value_size = sz, + }; + } + + r = measure_pcr(pcr_states, n); + if (r < 0) + return r; + + for (size_t i = 0; i < n; i++) { + _cleanup_free_ char *hd = NULL, *b = NULL; + + hd = hexmem(pcr_states[i].value, pcr_states[i].value_size); + if (!hd) + return log_oom(); + + b = strdup(EVP_MD_name(pcr_states[i].md)); + if (!b) + return log_oom(); + + printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, ascii_strlower(b), hd); + } + + return 0; +} + +static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) { + _cleanup_free_ char *s = NULL; + uint32_t v; + int r; + + r = efi_get_variable_string(varname, &s); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to read EFI variable '%s': %m", varname); + + r = safe_atou32(s, &v); + if (r < 0) + return log_error_errno(r, "Failed to parse EFI variable '%s': %s", varname, s); + + if (pcr != v) + log_warning("PCR number reported by stub for %s (%" PRIu32 ") different from our expectation (%" PRIu32 ").\n" + "The measurements are likely inconsistent.", description, v, pcr); + + return 0; +} + +static int validate_stub(void) { + uint64_t features; + bool found = false; + int r; + + if (tpm2_support() != TPM2_SUPPORT_FULL) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Sorry, system lacks full TPM2 support."); + + r = efi_stub_get_features(&features); + if (r < 0) + return log_error_errno(r, "Unable to get stub features: %m"); + + if (!FLAGS_SET(features, EFI_STUB_FEATURE_THREE_PCRS)) + log_warning("Warning: current kernel image does not support measuring itself, the command line or initrd system extension images.\n" + "The PCR measurements seen are unlikely to be valid."); + + r = compare_reported_pcr_nr(TPM_PCR_INDEX_KERNEL_IMAGE, EFI_LOADER_VARIABLE("StubPcrKernelImage"), "kernel image"); + if (r < 0) + return r; + + r = compare_reported_pcr_nr(TPM_PCR_INDEX_KERNEL_PARAMETERS, EFI_LOADER_VARIABLE("StubPcrKernelParameters"), "kernel parameters"); + if (r < 0) + return r; + + r = compare_reported_pcr_nr(TPM_PCR_INDEX_INITRD_SYSEXTS, EFI_LOADER_VARIABLE("StubPcrInitRDSysExts"), "initrd system extension images"); + if (r < 0) + return r; + + STRV_FOREACH(bank, arg_banks) { + _cleanup_free_ char *b = NULL, *p = NULL; + + b = strdup(*bank); + if (!b) + return log_oom(); + + if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/", ascii_strlower(b)) < 0) + return log_oom(); + + if (access(p, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to detect if '%s' exists: %m", b); + } else + found = true; + } + + if (!found) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "None of the select PCR banks appear to exist."); + + return 0; +} + +static int verb_status(int argc, char *argv[], void *userdata) { + + static const struct { + uint32_t nr; + const char *description; + } relevant_pcrs[] = { + { TPM_PCR_INDEX_KERNEL_IMAGE, "Unified Kernel Image" }, + { TPM_PCR_INDEX_KERNEL_PARAMETERS, "Kernel Parameters" }, + { TPM_PCR_INDEX_INITRD_SYSEXTS, "initrd System Extensions" }, + }; + + int r; + + r = validate_stub(); + if (r < 0) + return r; + + for (size_t i = 0; i < ELEMENTSOF(relevant_pcrs); i++) { + + STRV_FOREACH(bank, arg_banks) { + _cleanup_free_ char *b = NULL, *p = NULL, *s = NULL, *f = NULL; + _cleanup_free_ void *h = NULL; + size_t l; + + b = strdup(*bank); + if (!b) + return log_oom(); + + if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, ascii_strlower(b), relevant_pcrs[i].nr) < 0) + return log_oom(); + + r = read_virtual_file(p, 4096, &s, NULL); + if (r == -ENOENT) + continue; + if (r < 0) + return log_error_errno(r, "Failed to read '%s': %m", p); + + r = unhexmem(strstrip(s), SIZE_MAX, &h, &l); + if (r < 0) + return log_error_errno(r, "Failed to decode PCR value '%s': %m", s); + + f = hexmem(h, l); + if (!h) + return log_oom(); + + if (bank == arg_banks) { + /* before the first line for each PCR, write a short descriptive text to + * stderr, and leave the primary content on stdout */ + fflush(stdout); + fprintf(stderr, "%s# PCR[%" PRIu32 "] %s%s%s\n", + ansi_grey(), + relevant_pcrs[i].nr, + relevant_pcrs[i].description, + memeqzero(h, l) ? " (NOT SET!)" : "", + ansi_normal()); + fflush(stderr); + } + + printf("%" PRIu32 ":%s=%s\n", relevant_pcrs[i].nr, b, f); + } + } + + return 0; +} + +static int measure_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, + { "calculate", VERB_ANY, 1, 0, verb_calculate }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return measure_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run);