| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include <getopt.h> |
| #include <unistd.h> |
| |
| #include "alloc-util.h" |
| #include "build.h" |
| #include "efi-loader.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "hexdecoct.h" |
| #include "json.h" |
| #include "main-func.h" |
| #include "openssl-util.h" |
| #include "parse-argument.h" |
| #include "parse-util.h" |
| #include "pretty-print.h" |
| #include "sha256.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 char *arg_tpm2_device = NULL; |
| static char *arg_private_key = NULL; |
| static char *arg_public_key = NULL; |
| static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_OFF; |
| static PagerFlags arg_pager_flags = 0; |
| static bool arg_current = false; |
| static char **arg_phase = NULL; |
| static char *arg_append = NULL; |
| |
| STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); |
| STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); |
| STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); |
| STATIC_DESTRUCTOR_REGISTER(arg_public_key, freep); |
| STATIC_DESTRUCTOR_REGISTER(arg_phase, strv_freep); |
| STATIC_DESTRUCTOR_REGISTER(arg_append, 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 and sign PCR hash for a unified kernel image (UKI).%6$s\n" |
| "\n%3$sCommands:%4$s\n" |
| " status Show current PCR values\n" |
| " calculate Calculate expected PCR values\n" |
| " sign Calculate and sign expected PCR values\n" |
| "\n%3$sOptions:%4$s\n" |
| " -h --help Show this help\n" |
| " --version Print version\n" |
| " --no-pager Do not pipe output into a pager\n" |
| " -c --current Use current PCR values\n" |
| " --phase=PHASE Specify a boot phase to sign for\n" |
| " --bank=DIGEST Select TPM bank (SHA1, SHA256, SHA384, SHA512)\n" |
| " --tpm2-device=PATH Use specified TPM2 device\n" |
| " --private-key=KEY Private key (PEM) to sign with\n" |
| " --public-key=KEY Public key (PEM) to validate against\n" |
| " --json=MODE Output as JSON\n" |
| " -j Same as --json=pretty on tty, --json=short otherwise\n" |
| " --append=PATH Load specified JSON signature, and append new signature to it\n" |
| "\n%3$sUKI PE Section Options:%4$s %3$sUKI PE Section%4$s\n" |
| " --linux=PATH Path to Linux kernel image file %7$s .linux\n" |
| " --osrel=PATH Path to os-release file %7$s .osrel\n" |
| " --cmdline=PATH Path to file with kernel command line %7$s .cmdline\n" |
| " --initrd=PATH Path to initrd image file %7$s .initrd\n" |
| " --splash=PATH Path to splash bitmap file %7$s .splash\n" |
| " --dtb=PATH Path to Devicetree file %7$s .dtb\n" |
| " --pcrpkey=PATH Path to public key for PCR signatures %7$s .pcrpkey\n" |
| "\nSee the %2$s for details.\n", |
| program_invocation_short_name, |
| link, |
| ansi_underline(), |
| ansi_normal(), |
| ansi_highlight(), |
| ansi_normal(), |
| special_glyph(SPECIAL_GLYPH_ARROW_RIGHT)); |
| |
| return 0; |
| } |
| |
| static char *normalize_phase(const char *s) { |
| _cleanup_strv_free_ char **l = NULL; |
| |
| /* Let's normalize phase expressions. We split the series of colon-separated words up, then remove |
| * all empty ones, and glue them back together again. In other words we remove duplicate ":", as well |
| * as leading and trailing ones. */ |
| |
| l = strv_split(s, ":"); /* Split series of words */ |
| if (!l) |
| return NULL; |
| |
| /* Remove all empty words and glue things back together */ |
| return strv_join(strv_remove(l, ""), ":"); |
| } |
| |
| static int parse_argv(int argc, char *argv[]) { |
| enum { |
| ARG_VERSION = 0x100, |
| ARG_NO_PAGER, |
| _ARG_SECTION_FIRST, |
| ARG_LINUX = _ARG_SECTION_FIRST, |
| ARG_OSREL, |
| ARG_CMDLINE, |
| ARG_INITRD, |
| ARG_SPLASH, |
| ARG_DTB, |
| _ARG_PCRSIG, /* the .pcrsig section is not input for signing, hence not actually an argument here */ |
| _ARG_SECTION_LAST, |
| ARG_PCRPKEY = _ARG_SECTION_LAST, |
| ARG_BANK, |
| ARG_PRIVATE_KEY, |
| ARG_PUBLIC_KEY, |
| ARG_TPM2_DEVICE, |
| ARG_JSON, |
| ARG_PHASE, |
| ARG_APPEND, |
| }; |
| |
| static const struct option options[] = { |
| { "help", no_argument, NULL, 'h' }, |
| { "no-pager", no_argument, NULL, ARG_NO_PAGER }, |
| { "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 }, |
| { "pcrpkey", required_argument, NULL, ARG_PCRPKEY }, |
| { "current", no_argument, NULL, 'c' }, |
| { "bank", required_argument, NULL, ARG_BANK }, |
| { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, |
| { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, |
| { "public-key", required_argument, NULL, ARG_PUBLIC_KEY }, |
| { "json", required_argument, NULL, ARG_JSON }, |
| { "phase", required_argument, NULL, ARG_PHASE }, |
| { "append", required_argument, NULL, ARG_APPEND }, |
| {} |
| }; |
| |
| 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, "hjc", options, NULL)) >= 0) |
| switch (c) { |
| |
| case 'h': |
| help(0, NULL, NULL); |
| return 0; |
| |
| case ARG_VERSION: |
| return version(); |
| |
| case ARG_NO_PAGER: |
| arg_pager_flags |= PAGER_DISABLE; |
| break; |
| |
| 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 'c': |
| arg_current = true; |
| 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 ARG_PRIVATE_KEY: |
| r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_private_key); |
| if (r < 0) |
| return r; |
| |
| break; |
| |
| case ARG_PUBLIC_KEY: |
| r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_public_key); |
| if (r < 0) |
| return 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 'j': |
| arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; |
| break; |
| |
| case ARG_JSON: |
| r = parse_json_argument(optarg, &arg_json_format_flags); |
| if (r <= 0) |
| return r; |
| |
| break; |
| |
| case ARG_PHASE: { |
| char *n; |
| |
| n = normalize_phase(optarg); |
| if (!n) |
| return log_oom(); |
| |
| r = strv_consume(&arg_phase, TAKE_PTR(n)); |
| if (r < 0) |
| return r; |
| |
| break; |
| } |
| |
| case ARG_APPEND: |
| r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_append); |
| if (r < 0) |
| return r; |
| |
| 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); |
| |
| if (arg_current) |
| for (UnifiedSection us = 0; us < _UNIFIED_SECTION_MAX; us++) |
| if (arg_sections[us]) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --current switch cannot be used in combination with --linux= and related switches."); |
| |
| if (strv_isempty(arg_phase)) { |
| /* If no phases are specifically selected, pick everything from the beginning of the initrd |
| * to the beginning of shutdown. */ |
| if (strv_extend_strv(&arg_phase, |
| STRV_MAKE("enter-initrd", |
| "enter-initrd:leave-initrd", |
| "enter-initrd:leave-initrd:sysinit", |
| "enter-initrd:leave-initrd:sysinit:ready"), |
| /* filter_duplicates= */ false) < 0) |
| return log_oom(); |
| } else { |
| strv_sort(arg_phase); |
| strv_uniq(arg_phase); |
| } |
| |
| _cleanup_free_ char *j = NULL; |
| j = strv_join(arg_phase, ", "); |
| if (!j) |
| return log_oom(); |
| |
| log_debug("Measuring boot phases: %s", j); |
| return 1; |
| } |
| |
| /* The PCR 11 state for one specific bank */ |
| typedef struct PcrState { |
| char *bank; |
| const EVP_MD *md; |
| void *value; |
| size_t value_size; |
| void *saved_value; /* A copy of the original value we calculated, used by pcr_states_save()/pcr_states_restore() to come later back to */ |
| } 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].bank); |
| free((*pcr_state)[i].value); |
| free((*pcr_state)[i].saved_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.", pcr_state->bank); |
| |
| /* 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_kernel(PcrState *pcr_states, size_t n) { |
| _cleanup_free_ void *buffer = NULL; |
| int r; |
| |
| assert(n > 0); |
| assert(pcr_states); |
| |
| /* Virtually measures the components of a unified kernel image into PCR 11 */ |
| |
| if (arg_current) { |
| /* Shortcut things, if we should just use the current PCR value */ |
| |
| for (size_t i = 0; i < n; i++) { |
| _cleanup_free_ char *p = NULL, *s = NULL; |
| _cleanup_free_ void *v = NULL; |
| size_t sz; |
| |
| if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, pcr_states[i].bank, TPM_PCR_INDEX_KERNEL_IMAGE) < 0) |
| return log_oom(); |
| |
| r = read_virtual_file(p, 4096, &s, NULL); |
| if (r == -ENOENT && access("/sys/class/tpm/tpm0/", F_OK) >= 0) |
| return log_error_errno(r, "TPM device exists, but cannot open '%s'; either the kernel is too old, or selected PCR bank is not supported: %m", p); |
| if (r < 0) |
| return log_error_errno(r, "Failed to read '%s': %m", p); |
| |
| r = unhexmem(strstrip(s), SIZE_MAX, &v, &sz); |
| if (r < 0) |
| return log_error_errno(r, "Failed to decode PCR value '%s': %m", s); |
| |
| assert(pcr_states[i].value_size == sz); |
| memcpy(pcr_states[i].value, v, sz); |
| } |
| |
| return 0; |
| } |
| |
| 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 = -EBADF; |
| 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.", pcr_states[i].bank); |
| } |
| |
| 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.", pcr_states[i].bank); |
| |
| 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 and 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 measure_phase(PcrState *pcr_states, size_t n, const char *phase) { |
| _cleanup_strv_free_ char **l = NULL; |
| int r; |
| |
| assert(pcr_states); |
| assert(n > 0); |
| |
| /* Measure a phase string into PCR 11. This splits up the "phase" expression at colons, and then |
| * virtually extends each specified word into PCR 11, to model how during boot we measure a series of |
| * words into PCR 11, one for each phase. */ |
| |
| l = strv_split(phase, ":"); |
| if (!l) |
| return log_oom(); |
| |
| STRV_FOREACH(word, l) { |
| size_t wl; |
| |
| if (isempty(*word)) |
| continue; |
| |
| wl = strlen(*word); |
| |
| for (size_t i = 0; i < n; i++) { /* For each bank */ |
| _cleanup_free_ void *b = NULL; |
| int bsz; |
| |
| bsz = EVP_MD_size(pcr_states[i].md); |
| assert(bsz > 0); |
| |
| b = malloc(bsz); |
| if (!b) |
| return log_oom(); |
| |
| /* First hash the word itself */ |
| if (EVP_Digest(*word, wl, b, NULL, pcr_states[i].md, NULL) != 1) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash word '%s'.", *word); |
| |
| /* And then extend the PCR with the resulting hash */ |
| r = pcr_state_extend(pcr_states + i, b, bsz); |
| if (r < 0) |
| return r; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int pcr_states_allocate(PcrState **ret) { |
| _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; |
| size_t n = 0; |
| |
| 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; |
| _cleanup_free_ char *b = NULL; |
| int sz; |
| |
| assert_se(implementation = EVP_get_digestbyname(*d)); /* Must work, we already checked while parsing command line */ |
| |
| b = strdup(EVP_MD_name(implementation)); |
| if (!b) |
| return log_oom(); |
| |
| 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) { |
| .bank = ascii_strlower(TAKE_PTR(b)), |
| .md = implementation, |
| .value = TAKE_PTR(v), |
| .value_size = sz, |
| }; |
| } |
| |
| *ret = TAKE_PTR(pcr_states); |
| return (int) n; |
| } |
| |
| static int pcr_states_save(PcrState *pcr_states, size_t n) { |
| assert(pcr_states); |
| assert(n > 0); |
| |
| for (size_t i = 0; i < n; i++) { |
| _cleanup_free_ void *saved = NULL; |
| |
| if (!pcr_states[i].value) |
| continue; |
| |
| saved = memdup(pcr_states[i].value, pcr_states[i].value_size); |
| if (!saved) |
| return log_oom(); |
| |
| free_and_replace(pcr_states[i].saved_value, saved); |
| } |
| |
| return 0; |
| } |
| |
| static void pcr_states_restore(PcrState *pcr_states, size_t n) { |
| assert(pcr_states); |
| assert(n > 0); |
| |
| for (size_t i = 0; i < n; i++) { |
| |
| assert(pcr_states[i].value); |
| assert(pcr_states[i].saved_value); |
| |
| memcpy(pcr_states[i].value, pcr_states[i].saved_value, pcr_states[i].value_size); |
| } |
| } |
| |
| static int verb_calculate(int argc, char *argv[], void *userdata) { |
| _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; |
| _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; |
| size_t n; |
| int r; |
| |
| if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing."); |
| if (arg_append) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --append= switch is only supported for 'sign', not 'calculate'."); |
| |
| assert(!strv_isempty(arg_banks)); |
| assert(!strv_isempty(arg_phase)); |
| |
| r = pcr_states_allocate(&pcr_states); |
| if (r < 0) |
| return r; |
| |
| n = (size_t) r; |
| |
| r = measure_kernel(pcr_states, n); |
| if (r < 0) |
| return r; |
| |
| /* Save the current state, so that we later can restore to it. This way we can measure the PCR values |
| * for multiple different boot phases without heaving to start from zero each time */ |
| r = pcr_states_save(pcr_states, n); |
| if (r < 0) |
| return r; |
| |
| STRV_FOREACH(phase, arg_phase) { |
| |
| r = measure_phase(pcr_states, n, *phase); |
| if (r < 0) |
| return r; |
| |
| for (size_t i = 0; i < n; i++) { |
| if (arg_json_format_flags & JSON_FORMAT_OFF) { |
| _cleanup_free_ char *hd = NULL; |
| |
| if (i == 0) { |
| fflush(stdout); |
| fprintf(stderr, "%s# PCR[%" PRIu32 "] Phase <%s>%s\n", |
| ansi_grey(), |
| TPM_PCR_INDEX_KERNEL_IMAGE, |
| isempty(*phase) ? ":" : *phase, |
| ansi_normal()); |
| fflush(stderr); |
| } |
| |
| hd = hexmem(pcr_states[i].value, pcr_states[i].value_size); |
| if (!hd) |
| return log_oom(); |
| |
| printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, pcr_states[i].bank, hd); |
| } else { |
| _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *array = NULL; |
| |
| array = json_variant_ref(json_variant_by_key(w, pcr_states[i].bank)); |
| |
| r = json_build(&bv, |
| JSON_BUILD_OBJECT( |
| JSON_BUILD_PAIR_CONDITION(!isempty(*phase), "phase", JSON_BUILD_STRING(*phase)), |
| JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(TPM_PCR_INDEX_KERNEL_IMAGE)), |
| JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(pcr_states[i].value, pcr_states[i].value_size)) |
| ) |
| ); |
| if (r < 0) |
| return log_error_errno(r, "Failed to build JSON object: %m"); |
| |
| r = json_variant_append_array(&array, bv); |
| if (r < 0) |
| return log_error_errno(r, "Failed to append JSON object to array: %m"); |
| |
| r = json_variant_set_field(&w, pcr_states[i].bank, array); |
| if (r < 0) |
| return log_error_errno(r, "Failed to add bank info to object: %m"); |
| } |
| } |
| |
| /* Return to the original kernel measurement for the next phase calculation */ |
| pcr_states_restore(pcr_states, n); |
| } |
| |
| if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { |
| |
| if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO)) |
| pager_open(arg_pager_flags); |
| |
| json_variant_dump(w, arg_json_format_flags, stdout, NULL); |
| } |
| |
| return 0; |
| } |
| |
| static int verb_sign(int argc, char *argv[], void *userdata) { |
| _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; |
| _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; |
| _cleanup_(EVP_PKEY_freep) EVP_PKEY *privkey = NULL, *pubkey = NULL; |
| _cleanup_fclose_ FILE *privkeyf = NULL; |
| TSS2_RC rc; |
| size_t n; |
| int r; |
| |
| if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing."); |
| |
| if (!arg_private_key) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No private key specified, use --private-key=."); |
| |
| assert(!strv_isempty(arg_banks)); |
| assert(!strv_isempty(arg_phase)); |
| |
| if (arg_append) { |
| r = json_parse_file(NULL, arg_append, 0, &v, NULL, NULL); |
| if (r < 0) |
| return log_error_errno(r, "Failed to parse '%s': %m", arg_append); |
| |
| if (!json_variant_is_object(v)) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File '%s' is not a valid JSON object, refusing.", arg_append); |
| } |
| |
| /* When signing we only support JSON output */ |
| arg_json_format_flags &= ~JSON_FORMAT_OFF; |
| |
| privkeyf = fopen(arg_private_key, "re"); |
| if (!privkeyf) |
| return log_error_errno(errno, "Failed to open private key file '%s': %m", arg_private_key); |
| |
| privkey = PEM_read_PrivateKey(privkeyf, NULL, NULL, NULL); |
| if (!privkey) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse private key '%s'.", arg_private_key); |
| |
| if (arg_public_key) { |
| _cleanup_fclose_ FILE *pubkeyf = NULL; |
| |
| pubkeyf = fopen(arg_public_key, "re"); |
| if (!pubkeyf) |
| return log_error_errno(errno, "Failed to open public key file '%s': %m", arg_public_key); |
| |
| pubkey = PEM_read_PUBKEY(pubkeyf, NULL, NULL, NULL); |
| if (!pubkey) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key '%s'.", arg_public_key); |
| } else { |
| _cleanup_free_ char *data = NULL; |
| _cleanup_fclose_ FILE *tf = NULL; |
| size_t sz; |
| |
| /* No public key was specified, let's derive it automatically, if we can */ |
| |
| tf = open_memstream_unlocked(&data, &sz); |
| if (!tf) |
| return log_oom(); |
| |
| if (i2d_PUBKEY_fp(tf, privkey) != 1) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from private key file '%s'.", arg_private_key); |
| |
| fflush(tf); |
| rewind(tf); |
| |
| if (!d2i_PUBKEY_fp(tf, &pubkey)) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse extracted public key of private key file '%s'.", arg_private_key); |
| } |
| |
| r = pcr_states_allocate(&pcr_states); |
| if (r < 0) |
| return r; |
| |
| n = (size_t) r; |
| |
| r = measure_kernel(pcr_states, n); |
| if (r < 0) |
| return r; |
| |
| r = pcr_states_save(pcr_states, n); |
| if (r < 0) |
| return r; |
| |
| r = dlopen_tpm2(); |
| if (r < 0) |
| return r; |
| |
| _cleanup_tpm2_context_ Tpm2Context *c = NULL; |
| r = tpm2_context_new(arg_tpm2_device, &c); |
| if (r < 0) |
| return r; |
| |
| STRV_FOREACH(phase, arg_phase) { |
| |
| r = measure_phase(pcr_states, n, *phase); |
| if (r < 0) |
| return r; |
| |
| for (size_t i = 0; i < n; i++) { |
| static const TPMT_SYM_DEF symmetric = { |
| .algorithm = TPM2_ALG_AES, |
| .keyBits.aes = 128, |
| .mode.aes = TPM2_ALG_CFB, |
| }; |
| PcrState *p = pcr_states + i; |
| |
| _cleanup_tpm2_handle_ Tpm2Handle *session = NULL; |
| r = tpm2_handle_new(c, &session); |
| if (r < 0) |
| return r; |
| |
| rc = sym_Esys_StartAuthSession( |
| c->esys_context, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| NULL, |
| TPM2_SE_TRIAL, |
| &symmetric, |
| TPM2_ALG_SHA256, |
| &session->esys_handle); |
| if (rc != TSS2_RC_SUCCESS) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), |
| "Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc)); |
| |
| /* Generate a single hash value from the PCRs included in our policy. Given that that's |
| * exactly one, the calculation is trivial. */ |
| TPM2B_DIGEST intermediate_digest = { |
| .size = SHA256_DIGEST_SIZE, |
| }; |
| assert(sizeof(intermediate_digest.buffer) >= SHA256_DIGEST_SIZE); |
| sha256_direct(p->value, p->value_size, intermediate_digest.buffer); |
| |
| int tpmalg = tpm2_hash_alg_from_string(EVP_MD_name(p->md)); |
| if (tpmalg < 0) |
| return log_error_errno(tpmalg, "Unsupported PCR bank"); |
| |
| TPML_PCR_SELECTION pcr_selection; |
| tpm2_pcr_mask_to_selection(1 << TPM_PCR_INDEX_KERNEL_IMAGE, tpmalg, &pcr_selection); |
| |
| rc = sym_Esys_PolicyPCR( |
| c->esys_context, |
| session->esys_handle, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| &intermediate_digest, |
| &pcr_selection); |
| if (rc != TSS2_RC_SUCCESS) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), |
| "Failed to push PCR policy into TPM: %s", sym_Tss2_RC_Decode(rc)); |
| |
| _cleanup_(Esys_Freep) TPM2B_DIGEST *pcr_policy_digest = NULL; |
| rc = sym_Esys_PolicyGetDigest( |
| c->esys_context, |
| session->esys_handle, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| ESYS_TR_NONE, |
| &pcr_policy_digest); |
| if (rc != TSS2_RC_SUCCESS) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), |
| "Failed to get policy digest from TPM: %s", sym_Tss2_RC_Decode(rc)); |
| |
| _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX* mdctx = NULL; |
| mdctx = EVP_MD_CTX_new(); |
| if (!mdctx) |
| return log_oom(); |
| |
| if (EVP_DigestSignInit(mdctx, NULL, p->md, NULL, privkey) != 1) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), |
| "Failed to initialize signature context."); |
| |
| if (EVP_DigestSignUpdate(mdctx, pcr_policy_digest->buffer, pcr_policy_digest->size) != 1) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), |
| "Failed to sign data."); |
| |
| size_t ss; |
| if (EVP_DigestSignFinal(mdctx, NULL, &ss) != 1) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), |
| "Failed to finalize signature"); |
| |
| _cleanup_free_ void *sig = malloc(ss); |
| if (!sig) |
| return log_oom(); |
| |
| if (EVP_DigestSignFinal(mdctx, sig, &ss) != 1) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), |
| "Failed to acquire signature data"); |
| |
| _cleanup_free_ void *pubkey_fp = NULL; |
| size_t pubkey_fp_size = 0; |
| r = pubkey_fingerprint(pubkey, EVP_sha256(), &pubkey_fp, &pubkey_fp_size); |
| if (r < 0) |
| return r; |
| |
| _cleanup_(json_variant_unrefp) JsonVariant *a = NULL; |
| r = tpm2_make_pcr_json_array(UINT64_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &a); |
| if (r < 0) |
| return log_error_errno(r, "Failed to build JSON PCR mask array: %m"); |
| |
| _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL; |
| r = json_build(&bv, JSON_BUILD_OBJECT( |
| JSON_BUILD_PAIR("pcrs", JSON_BUILD_VARIANT(a)), /* PCR mask */ |
| JSON_BUILD_PAIR("pkfp", JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)), /* SHA256 fingerprint of public key (DER) used for the signature */ |
| JSON_BUILD_PAIR("pol", JSON_BUILD_HEX(pcr_policy_digest->buffer, pcr_policy_digest->size)), /* TPM2 policy hash that is signed */ |
| JSON_BUILD_PAIR("sig", JSON_BUILD_BASE64(sig, ss)))); /* signature data */ |
| if (r < 0) |
| return log_error_errno(r, "Failed to build JSON object: %m"); |
| |
| _cleanup_(json_variant_unrefp) JsonVariant *av = NULL; |
| av = json_variant_ref(json_variant_by_key(v, p->bank)); |
| |
| r = json_variant_append_array_nodup(&av, bv); |
| if (r < 0) |
| return log_error_errno(r, "Failed to append JSON object: %m"); |
| |
| r = json_variant_set_field(&v, p->bank, av); |
| if (r < 0) |
| return log_error_errno(r, "Failed to add JSON field: %m"); |
| } |
| |
| /* Return to the original kernel measurement for the next phase calculation */ |
| pcr_states_restore(pcr_states, n); |
| } |
| |
| if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO)) |
| pager_open(arg_pager_flags); |
| |
| json_variant_dump(v, arg_json_format_flags, stdout, NULL); |
| |
| 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) { |
| _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; |
| |
| 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; |
| _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); |
| |
| if (arg_json_format_flags & JSON_FORMAT_OFF) { |
| _cleanup_free_ char *f = NULL; |
| |
| 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); |
| |
| } else { |
| _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *a = NULL; |
| |
| r = json_build(&bv, |
| JSON_BUILD_OBJECT( |
| JSON_BUILD_PAIR("pcr", JSON_BUILD_INTEGER(relevant_pcrs[i].nr)), |
| JSON_BUILD_PAIR("hash", JSON_BUILD_HEX(h, l)) |
| ) |
| ); |
| if (r < 0) |
| return log_error_errno(r, "Failed to build JSON object: %m"); |
| |
| a = json_variant_ref(json_variant_by_key(v, b)); |
| |
| r = json_variant_append_array(&a, bv); |
| if (r < 0) |
| return log_error_errno(r, "Failed to append PCR entry to JSON array: %m"); |
| |
| r = json_variant_set_field(&v, b, a); |
| if (r < 0) |
| return log_error_errno(r, "Failed to add bank info to object: %m"); |
| } |
| } |
| } |
| |
| if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { |
| if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO)) |
| pager_open(arg_pager_flags); |
| |
| json_variant_dump(v, arg_json_format_flags, stdout, NULL); |
| } |
| |
| 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 }, |
| { "sign", VERB_ANY, 1, 0, verb_sign }, |
| {} |
| }; |
| |
| 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); |