From: Zbigniew Jędrzejewski-Szmek Date: Thu, 16 Apr 2026 17:22:11 +0000 (+0200) Subject: pcrlock: reorder function definitions to match --help X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ac2a0355fbf9b209cec4a4b8a41d512d10c17661;p=thirdparty%2Fsystemd.git pcrlock: reorder function definitions to match --help The order was random, different in the source code, different in the verb list, different in --help. Order source code by --help to make the next patch manageable. --- diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index fd8b9e95a4f..7820774fe08 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -2968,2102 +2968,2102 @@ static int unlink_pcrlock(const char *default_pcrlock_path) { return 0; } -static int verb_lock_raw(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *records = NULL; - _cleanup_fclose_ FILE *f = NULL; - int r; - - if (arg_pcr_mask == 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No PCR specified, refusing."); +static int event_log_reduce_to_safe_pcrs(EventLog *el, uint32_t *pcrs) { + _cleanup_free_ char *dropped = NULL, *kept = NULL; - if (argc >= 2) { - f = fopen(argv[1], "re"); - if (!f) - return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); - } + assert(el); + assert(pcrs); - r = make_pcrlock_record_from_stream(arg_pcr_mask, f ?: stdin, &records); - if (r < 0) - return r; + /* When we compile a new PCR policy we don't want to bind to PCRs which are fishy for one of three + * reasons: + * + * 1. The PCR value doesn't match the event log + * 2. The event log for the PCR contains measurements we don't know responsible components for + * 3. The event log for the PCR does not contain measurements for components we know + * + * This function checks for the three conditions and drops the PCR from the mask. + */ - return write_pcrlock(records, NULL); -} + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { -static int verb_unlock_simple(int argc, char *argv[], uintptr_t _data, void *userdata) { - return unlink_pcrlock(NULL); -} + if (!BIT_SET(*pcrs, pcr)) + continue; -static int verb_lock_secureboot_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { - static const struct { - sd_id128_t id; - const char *name; - int synthesize_empty; /* 0 → fail, > 0 → synthesize empty db, < 0 → skip */ - } variables[] = { - { EFI_VENDOR_GLOBAL, "SecureBoot", 0 }, - { EFI_VENDOR_GLOBAL, "PK", 1 }, - { EFI_VENDOR_GLOBAL, "KEK", 1 }, - { EFI_VENDOR_DATABASE, "db", 1 }, - { EFI_VENDOR_DATABASE, "dbx", 1 }, - { EFI_VENDOR_DATABASE, "dbt", -1 }, - { EFI_VENDOR_DATABASE, "dbr", -1 }, - }; + if (!event_log_pcr_checks_out(el, el->registers + pcr)) { + log_notice("PCR %" PRIu32 " (%s) value does not match event log. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + goto drop; + } - _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; - int r; + if (!el->registers[pcr].fully_recognized) { + log_notice("PCR %" PRIu32 " (%s) event log contains unrecognized measurements. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + goto drop; + } - /* Generates expected records from the current SecureBoot state, as readable in the EFI variables - * right now. */ + if (BIT_SET(el->missing_component_pcrs, pcr)) { + log_notice("PCR %" PRIu32 " (%s) is touched by component we can't find in event log. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + goto drop; + } - FOREACH_ELEMENT(vv, variables) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL; + log_info("PCR %" PRIu32 " (%s) matches event log and fully consists of recognized measurements. Including in set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); - _cleanup_free_ char *name = NULL; - if (asprintf(&name, "%s-" SD_ID128_UUID_FORMAT_STR, vv->name, SD_ID128_FORMAT_VAL(vv->id)) < 0) + if (strextendf_with_separator(&kept, ", ", "%" PRIu32 " (%s)", pcr, tpm2_pcr_index_to_string(pcr)) < 0) return log_oom(); - _cleanup_free_ void *data = NULL; - size_t data_size; - r = efi_get_variable(name, NULL, &data, &data_size); - if (r < 0) { - if (r != -ENOENT || vv->synthesize_empty == 0) - return log_error_errno(r, "Failed to read EFI variable '%s': %m", name); - if (vv->synthesize_empty < 0) - continue; - - /* If the main database variables are not set we don't consider this an error, but - * measure an empty database instead. */ - log_debug("EFI variable %s is not set, synthesizing empty variable for measurement.", name); - data_size = 0; - } + continue; - _cleanup_free_ char16_t* name16 = utf8_to_utf16(vv->name, SIZE_MAX); - if (!name16) - return log_oom(); - size_t name16_bytes = char16_strlen(name16) * 2; + drop: + *pcrs &= ~(UINT32_C(1) << pcr); - size_t vdata_size = offsetof(UEFI_VARIABLE_DATA, unicodeName) + name16_bytes + data_size; - _cleanup_free_ UEFI_VARIABLE_DATA *vdata = malloc(vdata_size); - if (!vdata) + if (strextendf_with_separator(&dropped, ", ", "%" PRIu32 " (%s)", pcr, tpm2_pcr_index_to_string(pcr)) < 0) return log_oom(); + } - *vdata = (UEFI_VARIABLE_DATA) { - .unicodeNameLength = name16_bytes / 2, - .variableDataLength = data_size, - }; - - efi_id128_to_guid(vv->id, vdata->variableName); - memcpy(mempcpy(vdata->unicodeName, name16, name16_bytes), data, data_size); - - r = make_pcrlock_record(TPM2_PCR_SECURE_BOOT_POLICY /* =7 */, vdata, vdata_size, &record); - if (r < 0) - return r; + if (dropped) + log_notice("PCRs dropped from protection mask: %s", dropped); + else + log_debug("No PCRs dropped from protection mask."); - r = sd_json_variant_append_array(&array, record); - if (r < 0) - return log_error_errno(r, "Failed to append to JSON array: %m"); - } + if (kept) + log_notice("PCRs in protection mask: %s", kept); + else + log_notice("No PCRs kept in protection mask."); - return write_pcrlock(array, PCRLOCK_SECUREBOOT_POLICY_PATH); + return 0; } -static int verb_unlock_secureboot_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { - return unlink_pcrlock(PCRLOCK_SECUREBOOT_POLICY_PATH); -} +static int pcr_prediction_add_result( + Tpm2PCRPrediction *context, + Tpm2PCRPredictionResult *result, + uint32_t pcr, + const char *path) { -static int event_log_record_is_secureboot_variable(EventLogRecord *rec, sd_id128_t uuid, const char *name) { - _cleanup_free_ char *found_name = NULL; - sd_id128_t found_uuid; + _cleanup_free_ Tpm2PCRPredictionResult *copy = NULL; int r; - assert(rec); - assert(name); - - if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) - return false; - - if (rec->pcr != TPM2_PCR_SECURE_BOOT_POLICY) - return false; - - if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) - return false; + assert(context); + assert(result); - if (rec->firmware_event_type != EV_EFI_VARIABLE_DRIVER_CONFIG) - return false; + copy = newdup(Tpm2PCRPredictionResult, result, 1); + if (!copy) + return log_oom(); - r = event_log_record_parse_variable_data(rec, &found_uuid, &found_name); - if (r == -EBADMSG) - return false; + r = ordered_set_ensure_put(context->results + pcr, &tpm2_pcr_prediction_result_hash_ops, copy); + if (r == -EEXIST) /* Multiple identical results for the same PCR are totally expected */ + return 0; if (r < 0) - return r; + return log_error_errno(r, "Failed to insert result into set: %m"); - if (!sd_id128_equal(found_uuid, uuid)) - return false; + log_debug("Added prediction result %u for PCR %" PRIu32 " (path: %s)", ordered_set_size(context->results[pcr]), pcr, strempty(path)); - return streq(found_name, name); + TAKE_PTR(copy); + return 0; } -static bool event_log_record_is_secureboot_authority(EventLogRecord *rec) { - assert(rec); - - if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) - return false; - - if (rec->pcr != TPM2_PCR_SECURE_BOOT_POLICY) - return false; +static const EVP_MD* evp_from_tpm2_alg(uint16_t alg) { + const char *name; - if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) - return false; + name = tpm2_hash_alg_to_string(alg); + if (!name) + return NULL; - return rec->firmware_event_type == EV_EFI_VARIABLE_AUTHORITY; + return EVP_get_digestbyname(name); } -static int event_log_ensure_secureboot_consistency(EventLog *el) { - static const struct { - sd_id128_t id; - const char *name; - bool required; - } table[] = { - { EFI_VENDOR_GLOBAL, "SecureBoot", true }, - { EFI_VENDOR_GLOBAL, "PK", true }, - { EFI_VENDOR_GLOBAL, "KEK", true }, - { EFI_VENDOR_DATABASE, "db", true }, - { EFI_VENDOR_DATABASE, "dbx", true }, - { EFI_VENDOR_DATABASE, "dbt", false }, - { EFI_VENDOR_DATABASE, "dbr", false }, - // FIXME: ensure we also find the separator here - }; - - EventLogRecord *records[ELEMENTSOF(table)] = {}; - EventLogRecord *first_authority = NULL; - - assert(el); +static int event_log_component_variant_calculate( + Tpm2PCRPredictionResult *result, + EventLogComponentVariant *variant, + uint32_t pcr) { - /* Ensures that the PCR 7 records are complete and in order. Before we lock down PCR 7 we want to - * ensure its state is actually consistent. */ + assert(result); + assert(variant); - FOREACH_ARRAY(rr, el->records, el->n_records) { + FOREACH_ARRAY(rr, variant->records, variant->n_records) { EventLogRecord *rec = *rr; - size_t found = SIZE_MAX; - - if (event_log_record_is_secureboot_authority(rec)) { - if (first_authority) - continue; - first_authority = rec; - // FIXME: also check that each authority record's data is also listed in 'db' + if (!EVENT_LOG_RECORD_IS_PCR(rec)) continue; - } - for (size_t i = 0; i < ELEMENTSOF(table); i++) - if (event_log_record_is_secureboot_variable(rec, table[i].id, table[i].name)) { - found = i; - break; - } - if (found == SIZE_MAX) + if (rec->pcr != pcr) continue; - /* Require the authority records always come *after* database measurements */ - if (first_authority) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "SecureBoot authority before variable, refusing."); + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + EventLogRecordBank *b; - /* Check for duplicates */ - if (records[found]) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate '%s' record, refusing.", rec->description); + if (result->hash[i].size <= 0) /* already invalidated */ + continue; - /* Check for order */ - for (size_t j = found + 1; j < ELEMENTSOF(table); j++) - if (records[j]) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'%s' record before '%s' record, refusing.", records[j]->description, rec->description); + b = event_log_record_find_bank(rec, tpm2_hash_algorithms[i]); + if (!b) { + /* Can't calculate, hence invalidate */ + result->hash[i] = (TPM2B_DIGEST) {}; + continue; + } - records[found] = rec; - } + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return log_oom(); - /* Check for existence */ - for (size_t i = 0; i < ELEMENTSOF(table); i++) - if (table[i].required && !records[i]) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Required record '%s' not found, refusing.", table[i].name); - - /* At this point we know that all required variables have been measured, in the right order. */ - return 0; -} - -static int verb_lock_secureboot_authority(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; - _cleanup_(event_log_freep) EventLog *el = NULL; - int r; + const EVP_MD *md = ASSERT_PTR(evp_from_tpm2_alg(tpm2_hash_algorithms[i])); - /* Lock down the EV_EFI_VARIABLE_AUTHORITY records from the existing log. Note that there's not too - * much value in locking this down too much, since it stores only the result of the primary database - * checks, and that's what we should bind policy to. Moreover it's hard to predict, since extension - * card firmware validation will result in additional records here. */ + int sz = EVP_MD_size(md); + assert(sz > 0); + assert((size_t) sz <= sizeof_field(TPM2B_DIGEST, buffer)); - if (!is_efi_secure_boot()) { - log_info("SecureBoot disabled, not generating authority .pcrlock file."); - return unlink_pcrlock(PCRLOCK_SECUREBOOT_AUTHORITY_PATH); - } + assert(sz == tpm2_hash_alg_to_size(tpm2_hash_algorithms[i])); - el = event_log_new(); - if (!el) - return log_oom(); + assert(result->hash[i].size == (size_t) sz); + assert(b->hash.size == (size_t) sz); - r = event_log_add_algorithms_from_environment(el); - if (r < 0) - return r; + if (EVP_DigestInit_ex(md_ctx, md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to initialize message digest."); - r = event_log_load(el); - if (r < 0) - return r; + if (EVP_DigestUpdate(md_ctx, result->hash[i].buffer, sz) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash bank value."); - r = event_log_read_pcrs(el); - if (r < 0) - return r; + if (EVP_DigestUpdate(md_ctx, b->hash.buffer, sz) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data value."); - r = event_log_calculate_pcrs(el); - if (r < 0) - return r; + unsigned l = (unsigned) sz; + if (EVP_DigestFinal_ex(md_ctx, result->hash[i].buffer, &l) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize message digest."); - /* Before we base anything on the event log records, let's check that the event log state checks - * out. */ + assert(l == (unsigned) sz); + } + } - r = event_log_pcr_mask_checks_out(el, UINT32_C(1) << TPM2_PCR_SECURE_BOOT_POLICY); - if (r < 0) - return r; + return 0; +} - r = event_log_validate_record_hashes(el); - if (r < 0) - return r; +static int event_log_predict_pcrs( + EventLog *el, + Tpm2PCRPrediction *context, + Tpm2PCRPredictionResult *parent_result, + size_t component_index, + uint32_t pcr, + const char *path) { - r = event_log_ensure_secureboot_consistency(el); - if (r < 0) - return r; + EventLogComponent *component; + int count = 0, r; - FOREACH_ARRAY(rr, el->records, el->n_records) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *digests = NULL; - EventLogRecord *rec = *rr; + assert(el); + assert(context); + assert(parent_result); - if (!event_log_record_is_secureboot_authority(rec)) - continue; + /* Check if we reached the end of the components, generate a result, and backtrack */ + if (component_index >= el->n_components || + (arg_location_end && strcmp(el->components[component_index]->id, arg_location_end) > 0)) { + r = pcr_prediction_add_result(context, parent_result, pcr, path); + if (r < 0) + return r; - log_debug("Locking down authority '%s'.", strna(rec->description)); + return 1; + } - LIST_FOREACH(banks, bank, rec->banks) { - r = sd_json_variant_append_arraybo( - &digests, - SD_JSON_BUILD_PAIR_STRING("hashAlg", tpm2_hash_alg_to_string(bank->algorithm)), - SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(bank->hash.buffer, bank->hash.size))); - if (r < 0) - return log_error_errno(r, "Failed to build digests array: %m"); - } + component = ASSERT_PTR(el->components[component_index]); - r = sd_json_variant_append_arraybo( - &array, - SD_JSON_BUILD_PAIR_UNSIGNED("pcr", rec->pcr), - SD_JSON_BUILD_PAIR_VARIANT("digests", digests)); + /* Check if we are just about to process a component after start, if so record a result and continue. */ + if (arg_location_start && strcmp(component->id, arg_location_start) > 0) { + r = pcr_prediction_add_result(context, parent_result, pcr, path); if (r < 0) - return log_error_errno(r, "Failed to build record array: %m"); + return r; } - return write_pcrlock(array, PCRLOCK_SECUREBOOT_AUTHORITY_PATH); -} - -static int verb_unlock_secureboot_authority(int argc, char *argv[], uintptr_t _data, void *userdata) { - return unlink_pcrlock(PCRLOCK_SECUREBOOT_AUTHORITY_PATH); -} - -static int verb_lock_gpt(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL, *record = NULL; - _cleanup_(sd_device_unrefp) sd_device *d = NULL; - uint8_t h[2 * 4096]; /* space for at least two 4K sectors. GPT header should definitely be in here */ - uint64_t start, n_members, member_size; - _cleanup_close_ int fd = -EBADF; - const GptHeader *p; - size_t found = 0; - ssize_t n; - int r; + FOREACH_ARRAY(ii, component->variants, component->n_variants) { + _cleanup_free_ Tpm2PCRPredictionResult *result = NULL; + EventLogComponentVariant *variant = *ii; + _cleanup_free_ char *subpath = NULL; - r = block_device_new_from_path( - argc >= 2 ? argv[1] : "/", - BLOCK_DEVICE_LOOKUP_WHOLE_DISK|BLOCK_DEVICE_LOOKUP_BACKING|BLOCK_DEVICE_LOOKUP_ORIGINATING, - &d); - if (r < 0) - return log_error_errno(r, "Failed to determine root block device: %m"); + /* Operate on a copy of the result */ - fd = sd_device_open(d, O_CLOEXEC|O_RDONLY|O_NOCTTY); - if (fd < 0) - return log_error_errno(fd, "Failed to open root block device: %m"); + if (path) + subpath = strjoin(path, ":", component->id); + else + subpath = strdup(component->id); + if (!subpath) + return log_oom(); - n = pread(fd, &h, sizeof(h), 0); - if (n < 0) - return log_error_errno(errno, "Failed to read GPT header of block device: %m"); - if ((size_t) n != sizeof(h)) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read trying to read GPT header."); + if (!streq(component->id, variant->id)) + if (!strextend(&subpath, "@", variant->id)) + return log_oom(); - /* Try a couple of sector sizes */ - for (size_t sz = 512; sz <= 4096; sz <<= 1) { - assert(sizeof(h) >= sz * 2); - p = (const GptHeader*) (h + sz); /* 2nd sector */ + result = newdup(Tpm2PCRPredictionResult, parent_result, 1); + if (!result) + return log_oom(); - if (!gpt_header_has_signature(p)) - continue; + r = event_log_component_variant_calculate( + result, + variant, + pcr); + if (r < 0) + return r; - if (found != 0) - return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), - "Disk has partition table for multiple sector sizes, refusing."); + r = event_log_predict_pcrs( + el, + context, + result, + component_index + 1, /* Next component */ + pcr, + subpath); + if (r < 0) + return r; - found = sz; + count += r; } - if (found == 0) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), - "Disk does not have GPT partition table, refusing."); + return count; +} - p = (const GptHeader*) (h + found); +static ssize_t event_log_calculate_component_combinations(EventLog *el) { + ssize_t count = 1; + assert(el); - if (le32toh(p->header_size) > found) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), - "GPT header size over long (%" PRIu32 "), refusing.", le32toh(p->header_size)); + FOREACH_ARRAY(cc, el->components, el->n_components) { + EventLogComponent *c = *cc; - start = le64toh(p->partition_entry_lba); - if (start > UINT64_MAX / found) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), - "Partition table start offset overflow, refusing."); + assert(c->n_variants > 0); - member_size = le32toh(p->size_of_partition_entry); - if (member_size < sizeof(GptPartitionEntry)) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), - "Partition entry size too short, refusing."); + /* Overflow check */ + if (c->n_variants > (size_t) (SSIZE_MAX/count)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many component combinations."); + count *= c->n_variants; + } - n_members = le32toh(p->number_of_partition_entries); - uint64_t member_bufsz = n_members * member_size; - if (member_bufsz > 1U*1024U*1024U) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), - "Partition table size too large, refusing."); + return count; +} - member_bufsz = ROUND_UP(member_bufsz, found); +static int event_log_show_predictions(Tpm2PCRPrediction *context, uint16_t alg) { + int r; - _cleanup_free_ void *members = malloc(member_bufsz); - if (!members) - return log_oom(); + assert(context); - n = pread(fd, members, member_bufsz, start * found); - if (n < 0) - return log_error_errno(errno, "Failed to read GPT partition table entries: %m"); - if ((size_t) n != member_bufsz) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading GPT partition table entries."); + pager_open(arg_pager_flags); - size_t vdata_size = le32toh(p->header_size) + sizeof(le64_t) + member_size * n_members; - _cleanup_free_ void *vdata = malloc0(vdata_size); - if (!vdata) - return log_oom(); + if (sd_json_format_enabled(arg_json_format_flags)) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL; - void *n_measured_entries = mempcpy(vdata, p, sizeof(GptHeader)); /* n_measured_entries is a 64bit value */ + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *aj = NULL; - void *qq = (uint8_t*) n_measured_entries + sizeof(le64_t); + r = tpm2_pcr_prediction_to_json( + context, + tpm2_hash_algorithms[i], + &aj); + if (r < 0) + return r; - for (uint64_t i = 0; i < n_members; i++) { - const GptPartitionEntry *entry = (const GptPartitionEntry*) ((const uint8_t*) members + (member_size * i)); + if (sd_json_variant_elements(aj) == 0) + continue; - if (memeqzero(entry->partition_type_guid, sizeof(entry->partition_type_guid))) - continue; + r = sd_json_variant_set_field( + &j, + tpm2_hash_alg_to_string(tpm2_hash_algorithms[i]), + aj); + if (r < 0) + return log_error_errno(r, "Failed to add prediction bank to object: %m"); + } - qq = mempcpy(qq, entry, member_size); - unaligned_write_le64(n_measured_entries, unaligned_read_le64(n_measured_entries) + 1); + if (!j) { + r = sd_json_variant_new_object(&j, NULL, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocated empty object: %m"); + } + + sd_json_variant_dump(j, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL); + return 0; } - vdata_size = (uint8_t*) qq - (uint8_t*) vdata; + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + Tpm2PCRPredictionResult *result; + if (!BIT_SET(context->pcrs, pcr)) + continue; - r = make_pcrlock_record(TPM2_PCR_BOOT_LOADER_CONFIG /* =5 */, vdata, vdata_size, &record); - if (r < 0) - return r; + if (ordered_set_isempty(context->results[pcr])) { + printf("No results for PCR %u (%s).\n", pcr, tpm2_pcr_index_to_string(pcr)); + continue; + } - r = sd_json_variant_new_array(&array, &record, 1); - if (r < 0) - return log_error_errno(r, "Failed to append to JSON array: %m"); + printf("%sResults for PCR %u (%s):%s\n", ansi_underline(), pcr, tpm2_pcr_index_to_string(pcr), ansi_normal()); - return write_pcrlock(array, PCRLOCK_GPT_PATH); -} + ORDERED_SET_FOREACH(result, context->results[pcr]) { -static int verb_unlock_gpt(int argc, char *argv[], uintptr_t _data, void *userdata) { - return unlink_pcrlock(PCRLOCK_GPT_PATH); -} + _cleanup_free_ char *aa = NULL, *h = NULL; + const char *a; -static bool event_log_record_is_separator(const EventLogRecord *rec) { - assert(rec); + TPM2B_DIGEST *hash = tpm2_pcr_prediction_result_get_hash(result, alg); + if (!hash) + continue; - /* Recognizes EV_SEPARATOR events */ + a = ASSERT_PTR(tpm2_hash_alg_to_string(alg)); + aa = strdup(a); + if (!aa) + return log_oom(); - if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) - return false; + ascii_strlower(aa); - if (rec->firmware_event_type != EV_SEPARATOR) - return false; + h = hexmem(hash->buffer, hash->size); + if (!h) + return log_oom(); - return rec->event_payload_valid == EVENT_PAYLOAD_VALID_YES; /* Insist the record is consistent */ + printf(" %s%-6s:%s %s\n", ansi_grey(), aa, ansi_normal(), h); + } + } + + return 0; } -static int event_log_record_is_action_calling_efi_app(const EventLogRecord *rec) { - _cleanup_free_ char *d = NULL; - int r; +static int tpm2_pcr_prediction_run( + EventLog *el, + Tpm2PCRPrediction *context) { - assert(rec); + int r; - /* Recognizes the special EV_EFI_ACTION that is issues when the firmware passes control to the boot loader. */ + assert(el); + assert(context); - if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) - return false; + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + _cleanup_free_ Tpm2PCRPredictionResult *result = NULL; - if (rec->pcr != TPM2_PCR_BOOT_LOADER_CODE) - return false; + if (!BIT_SET(context->pcrs, pcr)) + continue; - if (rec->firmware_event_type != EV_EFI_ACTION) - return false; + result = new0(Tpm2PCRPredictionResult, 1); + if (!result) + return log_oom(); - if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) /* Insist the record is consistent */ - return false; + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) + event_log_initial_pcr_state(el, pcr, tpm2_hash_alg_to_size(tpm2_hash_algorithms[i]), result->hash + i); - r = make_cstring(rec->firmware_payload, rec->firmware_payload_size, MAKE_CSTRING_ALLOW_TRAILING_NUL, &d); - if (r < 0) - return r; + r = event_log_predict_pcrs( + el, + context, + result, + /* component_index= */ 0, + pcr, + /* path= */ NULL); + if (r < 0) + return r; + } - return streq(d, "Calling EFI Application from Boot Option"); + return 0; } -static void enable_json_sse(void) { - /* We shall write this to a single output stream? We have to output two files, hence try to be smart - * and enable JSON SSE */ - - if (!arg_pcrlock_path && arg_pcrlock_auto) - return; +static int verb_predict(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction context = { + arg_pcr_mask != 0 ? arg_pcr_mask : DEFAULT_PCR_MASK, + }; + _cleanup_(event_log_freep) EventLog *el = NULL; + ssize_t count; + int r; - if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_SSE)) - return; + r = event_log_load_and_process(&el); + if (r < 0) + return r; - log_notice("Enabling JSON_SEQ mode, since writing two .pcrlock files to single output."); - arg_json_format_flags |= SD_JSON_FORMAT_SSE; -} + count = event_log_calculate_component_combinations(el); + if (count < 0) + return count; -static int verb_lock_firmware(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *array_early = NULL, *array_late = NULL; - _cleanup_(event_log_freep) EventLog *el = NULL; - uint32_t always_mask, separator_mask, separator_seen_mask = 0, action_seen_mask = 0; - const char *default_pcrlock_early_path, *default_pcrlock_late_path; - int r; + log_info("%zi combinations of components.", count); - enable_json_sse(); + r = event_log_reduce_to_safe_pcrs(el, &context.pcrs); + if (r < 0) + return r; - /* The PCRs we intend to cover. Note that we measure firmware, external *and* boot loader code/config - * here – but the latter only until the "separator" events are seen, which tell us where transition - * into OS boot loader happens. This reflects the fact that on some systems the firmware already - * measures some firmware-supplied apps into PCR 4. (e.g. Thinkpad X1 Gen9) */ - if (endswith(argv[0], "firmware-code")) { - always_mask = (UINT32_C(1) << TPM2_PCR_PLATFORM_CODE) | /* → 0 */ - (UINT32_C(1) << TPM2_PCR_EXTERNAL_CODE); /* → 2 */ + r = tpm2_pcr_prediction_run(el, &context); + if (r < 0) + return r; - separator_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE; /* → 4 */ + return event_log_show_predictions(&context, el->primary_algorithm); +} - default_pcrlock_early_path = PCRLOCK_FIRMWARE_CODE_EARLY_PATH; - default_pcrlock_late_path = PCRLOCK_FIRMWARE_CODE_LATE_PATH; - } else { - assert(endswith(argv[0], "firmware-config")); - always_mask = (UINT32_C(1) << TPM2_PCR_PLATFORM_CONFIG) | /* → 1 */ - (UINT32_C(1) << TPM2_PCR_EXTERNAL_CONFIG); /* → 3 */ +static int remove_policy_file(const char *path) { + assert(path); - separator_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CONFIG; /* → 5 */ + if (unlink(path) < 0) { + if (errno == ENOENT) + return 0; - default_pcrlock_early_path = PCRLOCK_FIRMWARE_CONFIG_EARLY_PATH; - default_pcrlock_late_path = PCRLOCK_FIRMWARE_CONFIG_LATE_PATH; + return log_error_errno(errno, "Failed to remove policy file '%s': %m", path); } - el = event_log_new(); - if (!el) - return log_oom(); + log_info("Removed policy file '%s'.", path); + return 1; +} - r = event_log_add_algorithms_from_environment(el); - if (r < 0) - return r; +static int determine_boot_policy_file(char **ret_path, char **ret_credential_name) { + int r; - r = event_log_load(el); + _cleanup_free_ char *path = NULL; + r = get_global_boot_credentials_path(&path); if (r < 0) return r; + if (r == 0) { + if (ret_path) + *ret_path = NULL; + if (ret_credential_name) + *ret_credential_name = NULL; + return 0; /* not found! */ + } - r = event_log_read_pcrs(el); + sd_id128_t machine_id; + r = sd_id128_get_machine(&machine_id); if (r < 0) - return r; + return log_error_errno(r, "Failed to read machine ID: %m"); - r = event_log_calculate_pcrs(el); + r = boot_entry_token_ensure( + /* root= */ NULL, + /* conf_root= */ NULL, + machine_id, + /* machine_id_is_random= */ false, + &arg_entry_token_type, + &arg_entry_token); if (r < 0) return r; - r = event_log_validate_record_hashes(el); - if (r < 0) - return r; + _cleanup_free_ char *fn = strjoin("pcrlock.", arg_entry_token, ".cred"); + if (!fn) + return log_oom(); - /* Before we base anything on the event log records for any of the selected PCRs, let's check that - * the event log state checks out for them. */ + if (!filename_is_valid(fn)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name '%s' would not be a valid file name, refusing.", fn); - r = event_log_pcr_mask_checks_out(el, always_mask|separator_mask); - if (r < 0) - return r; + _cleanup_free_ char *joined = NULL; + if (ret_path) { + joined = path_join(path, fn); + if (!joined) + return log_oom(); + } - // FIXME: before doing this, validate ahead-of-time that EV_SEPARATOR records exist for all entries, - // and exactly once + _cleanup_free_ char *cn = NULL; + if (ret_credential_name) { + /* The .cred suffix of the file is stripped when PID 1 imports the credential, hence exclude it from + * the embedded credential name. */ + cn = strjoin("pcrlock.", arg_entry_token); + if (!cn) + return log_oom(); - FOREACH_ARRAY(rr, el->records, el->n_records) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *digests = NULL; - EventLogRecord *rec = *rr; - uint32_t bit = UINT32_C(1) << rec->pcr; + ascii_strlower(cn); /* lowercase this file, no matter what, since stored on VFAT, and we don't want + * to run into case change incompatibilities */ + } - if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) - continue; + if (ret_path) + *ret_path = TAKE_PTR(joined); - if (!FLAGS_SET(always_mask, bit) && - !(FLAGS_SET(separator_mask, bit) && !FLAGS_SET(separator_seen_mask|action_seen_mask, bit))) - continue; + if (ret_credential_name) + *ret_credential_name = TAKE_PTR(cn); - /* If we hit the separator record, we stop processing the PCRs listed in `separator_mask` */ - if (event_log_record_is_separator(rec)) { - separator_seen_mask |= bit; - continue; - } + return 1; /* found! */ +} - /* If we hit the special "Calling EFI Application from Boot Option" action we treat this the - * same as a separator here, as that's where firmware passes control to boot loader. Note - * that some EFI implementations forget to generate one of them. */ - r = event_log_record_is_action_calling_efi_app(rec); - if (r < 0) - return log_error_errno(r, "Failed to check if event is 'Calling EFI Application from Boot Option' action: %m"); - if (r > 0) { - action_seen_mask |= bit; - continue; - } - - LIST_FOREACH(banks, bank, rec->banks) { - r = sd_json_variant_append_arraybo( - &digests, - SD_JSON_BUILD_PAIR_STRING("hashAlg", tpm2_hash_alg_to_string(bank->algorithm)), - SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(bank->hash.buffer, bank->hash.size))); - if (r < 0) - return log_error_errno(r, "Failed to build digests array: %m"); - } +static int write_boot_policy_file(const char *json_text) { + _cleanup_free_ char *boot_policy_file = NULL, *credential_name = NULL; + int r; - r = sd_json_variant_append_arraybo( - FLAGS_SET(separator_seen_mask, bit) ? &array_late : &array_early, - SD_JSON_BUILD_PAIR_UNSIGNED("pcr", rec->pcr), - SD_JSON_BUILD_PAIR_VARIANT("digests", digests)); - if (r < 0) - return log_error_errno(r, "Failed to build record array: %m"); - } + assert(json_text); - r = write_pcrlock(array_early, default_pcrlock_early_path); + r = determine_boot_policy_file(&boot_policy_file, &credential_name); if (r < 0) return r; - - return write_pcrlock(array_late, default_pcrlock_late_path); -} - -static int verb_unlock_firmware(int argc, char *argv[], uintptr_t _data, void *userdata) { - const char *default_pcrlock_early_path, *default_pcrlock_late_path; - int r; - - if (endswith(argv[0], "firmware-code")) { - default_pcrlock_early_path = PCRLOCK_FIRMWARE_CODE_EARLY_PATH; - default_pcrlock_late_path = PCRLOCK_FIRMWARE_CODE_LATE_PATH; - } else { - default_pcrlock_early_path = PCRLOCK_FIRMWARE_CONFIG_EARLY_PATH; - default_pcrlock_late_path = PCRLOCK_FIRMWARE_CONFIG_LATE_PATH; + if (r == 0) { + log_info("Did not find XBOOTLDR/ESP partition, not writing boot policy file."); + return 0; } - r = unlink_pcrlock(default_pcrlock_early_path); + _cleanup_(iovec_done) struct iovec encoded = {}; + r = encrypt_credential_and_warn( + CRED_AES256_GCM_BY_NULL, + credential_name, + now(CLOCK_REALTIME), + /* not_after= */ USEC_INFINITY, + /* tpm2_device= */ NULL, + /* tpm2_hash_pcr_mask= */ 0, + /* tpm2_pubkey_path= */ NULL, + /* tpm2_pubkey_pcr_mask= */ 0, + UID_INVALID, + &IOVEC_MAKE_STRING(json_text), + CREDENTIAL_ALLOW_NULL, + &encoded); if (r < 0) - return r; - - if (arg_pcrlock_path) /* if the path is specified don't delete the same thing twice */ - return 0; + return log_error_errno(r, "Failed to encode policy as credential: %m"); - r = unlink_pcrlock(default_pcrlock_late_path); + r = write_base64_file_at( + AT_FDCWD, + boot_policy_file, + &encoded, + WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_SYNC|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_LABEL); if (r < 0) - return r; + return log_error_errno(r, "Failed to write boot policy file to '%s': %m", boot_policy_file); - return 0; + log_info("Written new boot policy to '%s'.", boot_policy_file); + return 1; } -static int verb_lock_machine_id(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL, *array = NULL; - _cleanup_free_ char *word = NULL; +static int make_policy(bool force, RecoveryPinMode recovery_pin_mode) { int r; - r = pcrextend_machine_id_word(&word); + /* Here's how this all works: after predicting all possible PCR values for next boot (with + * alternatives) we'll calculate a policy from it as a combination of PolicyPCR + PolicyOR + * expressions. This is then stored in an NV index. When a component of the boot process is changed a + * new prediction is made and the NV index updated (which automatically invalidates any older + * policies). + * + * Whenever we want to lock an encrypted object (for example FDE) against this policy, we'll use a + * PolicyAuthorizeNV expression that pins the NV index in the policy, and permits access to any + * policies matching the current NV index contents. + * + * We grant world-readable read access to the NV index. Write access is controlled by a PIN (which we + * either generate locally or which the user can provide us with) which can also be used for + * recovery. This PIN is sealed to the TPM and is locked via PolicyAuthorizeNV to the NV index it + * protects (i.e. we dogfood 🌭 🐶 hard here). This means in order to update such a policy we need + * the policy to pass. + * + * Information about the used NV Index, the SRK of the TPM, the sealed PIN and the current PCR + * prediction data are stored in a JSON file in /var/lib/. In order to be able to unlock root disks + * this data must be also copied to the ESP so that it is available to the initrd. The data is not + * sensitive, as SRK and NV index are pinned by it, and the prediction data must match the NV index + * to be useful. */ + + usec_t start_usec = now(CLOCK_MONOTONIC); + + _cleanup_(event_log_freep) EventLog *el = NULL; + r = event_log_load_and_process(&el); if (r < 0) return r; - r = make_pcrlock_record(TPM2_PCR_SYSTEM_IDENTITY /* = 15 */, word, SIZE_MAX, &record); + _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction new_prediction = { + arg_pcr_mask != 0 ? arg_pcr_mask : DEFAULT_PCR_MASK, + }; + r = event_log_reduce_to_safe_pcrs(el, &new_prediction.pcrs); if (r < 0) return r; - r = sd_json_variant_new_array(&array, &record, 1); - if (r < 0) - return log_error_errno(r, "Failed to create record array: %m"); + if (!force && new_prediction.pcrs == 0) + log_notice("Set of PCRs to use for policy is empty. Generated policy will not provide any protection in its current form. Proceeding."); - return write_pcrlock(array, PCRLOCK_MACHINE_ID_PATH); -} + usec_t predict_start_usec = now(CLOCK_MONOTONIC); -static int verb_unlock_machine_id(int argc, char *argv[], uintptr_t _data, void *userdata) { - return unlink_pcrlock(PCRLOCK_MACHINE_ID_PATH); -} + r = tpm2_pcr_prediction_run(el, &new_prediction); + if (r < 0) + return r; -static int pcrlock_file_system_path(const char *normalized_path, char **ret) { - _cleanup_free_ char *s = NULL; + log_info("Predicted future PCRs in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), predict_start_usec), 1)); - assert(normalized_path); - assert(ret); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *new_prediction_json = NULL; + r = tpm2_pcr_prediction_to_json(&new_prediction, el->primary_algorithm, &new_prediction_json); + if (r < 0) + return r; - if (path_equal(normalized_path, "/")) - s = strdup(PCRLOCK_ROOT_FILE_SYSTEM_PATH); - else { - /* We reuse the escaping we use for turning paths into unit names */ - _cleanup_free_ char *escaped = NULL; + if (DEBUG_LOGGING) + (void) sd_json_variant_dump(new_prediction_json, SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO, stderr, NULL); - assert(normalized_path[0] == '/'); - assert(normalized_path[1] != '/'); + /* v257 and older mistakenly used --pcrlock= for the path. To keep backward compatibility, let's fallback to it when + * --policy= is unspecified but --pcrlock is specified. */ + if (!arg_policy_path && arg_pcrlock_path) { + log_notice("Specified --pcrlock= option for make-policy command. Please use --policy= instead."); - escaped = unit_name_escape(normalized_path + 1); - if (!escaped) + arg_policy_path = strdup(arg_pcrlock_path); + if (!arg_policy_path) return log_oom(); - - s = strjoin(PCRLOCK_FILE_SYSTEM_PATH_PREFIX, escaped, ".pcrlock"); } - if (!s) - return log_oom(); - *ret = TAKE_PTR(s); - return 0; -} - -static int verb_lock_file_system(int argc, char *argv[], uintptr_t _data, void *userdata) { - const char* paths[3] = {}; - int r; - - if (argc > 1) - paths[0] = argv[1]; - else { - dev_t a, b; - paths[0] = "/"; + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy old_policy = {}; + r = tpm2_pcrlock_policy_load(arg_policy_path, &old_policy); + if (r < 0) + return r; - r = get_block_device("/", &a); - if (r < 0) - return log_error_errno(r, "Failed to get device of root file system: %m"); + bool have_old_policy = r > 0; - r = get_block_device("/var", &b); - if (r < 0) - return log_error_errno(r, "Failed to get device of /var/ file system: %m"); + /* When we update the policy the old serializations for NV, SRK, PIN remain the same */ + _cleanup_(iovec_done) struct iovec + nv_blob = TAKE_STRUCT(old_policy.nv_handle), + nv_public_blob = TAKE_STRUCT(old_policy.nv_public), + srk_blob = TAKE_STRUCT(old_policy.srk_handle), + pin_public = TAKE_STRUCT(old_policy.pin_public), + pin_private = TAKE_STRUCT(old_policy.pin_private); - /* if backing device is distinct, then measure /var/ too */ - if (a != b) - paths[1] = "/var"; + if (have_old_policy) { + if (arg_nv_index != 0 && old_policy.nv_index != arg_nv_index) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Stored policy references different NV index (0x%x) than specified (0x%x), refusing.", old_policy.nv_index, arg_nv_index); - enable_json_sse(); + if (!force && + old_policy.algorithm == el->primary_algorithm && + tpm2_pcr_prediction_equal(&old_policy.prediction, &new_prediction, el->primary_algorithm)) { + log_info("Prediction is identical to current policy, skipping update."); + return 0; /* NOP */ + } } - STRV_FOREACH(p, paths) { - _cleanup_free_ char *word = NULL, *normalized_path = NULL, *pcrlock_file = NULL; - _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL, *array = NULL; + _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; + r = tpm2_context_new_or_warn(/* device= */ NULL, &tc); + if (r < 0) + return r; - r = pcrextend_file_system_word(*p, &word, &normalized_path); - if (r < 0) - return r; + if (!tpm2_supports_command(tc, TPM2_CC_PolicyAuthorizeNV)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 does not support PolicyAuthorizeNV command, refusing."); + if (!tpm2_supports_alg(tc, TPM2_ALG_SHA256)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 does not support SHA-256 hash algorithm, refusing."); - r = pcrlock_file_system_path(normalized_path, &pcrlock_file); - if (r < 0) - return r; + _cleanup_(tpm2_handle_freep) Tpm2Handle *srk_handle = NULL; - r = make_pcrlock_record(TPM2_PCR_SYSTEM_IDENTITY /* = 15 */, word, SIZE_MAX, &record); + r = tpm2_deserialize( + tc, + &srk_blob, + &srk_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize SRK TR: %m"); + if (r == 0) { + r = tpm2_get_or_create_srk( + tc, + /* session= */ NULL, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &srk_handle); if (r < 0) - return r; + return log_error_errno(r, "Failed to install SRK: %m"); + } - r = sd_json_variant_new_array(&array, &record, 1); - if (r < 0) - return log_error_errno(r, "Failed to create record array: %m"); + _cleanup_(tpm2_handle_freep) Tpm2Handle *encryption_session = NULL; + r = tpm2_make_encryption_session( + tc, + srk_handle, + /* bind_key= */ &TPM2_HANDLE_NONE, + &encryption_session); + if (r < 0) + return log_error_errno(r, "Failed to allocate encryption session: %m"); - r = write_pcrlock(array, pcrlock_file); + /* Acquire a recovery PIN, either from the user, or create a randomized one */ + _cleanup_(erase_and_freep) char *pin = NULL; + if (recovery_pin_mode == RECOVERY_PIN_QUERY) { + r = getenv_steal_erase("PIN", &pin); if (r < 0) - return r; - } - - return 0; -} + return log_error_errno(r, "Failed to acquire PIN from environment: %m"); + if (r == 0) { + _cleanup_strv_free_erase_ char **l = NULL; -static int verb_unlock_file_system(int argc, char *argv[], uintptr_t _data, void *userdata) { - const char* paths[3] = {}; - int r; + AskPasswordRequest req = { + .tty_fd = -EBADF, + .message = "Recovery PIN", + .id = "pcrlock-recovery-pin", + .credential = "pcrlock.recovery-pin", + .until = USEC_INFINITY, + .hup_fd = -EBADF, + }; - if (argc > 1) - paths[0] = argv[1]; - else { - paths[0] = "/"; - paths[1] = "/var"; - } + r = ask_password_auto( + &req, + /* flags= */ 0, + &l); + if (r < 0) + return log_error_errno(r, "Failed to query for recovery PIN: %m"); - STRV_FOREACH(p, paths) { - _cleanup_free_ char *normalized_path = NULL, *pcrlock_file = NULL; + if (strv_length(l) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single PIN only."); - r = chase(*p, NULL, 0, &normalized_path, NULL); - if (r < 0) - return log_error_errno(r, "Failed to normal path '%s': %m", argv[1]); + pin = TAKE_PTR(l[0]); + l = mfree(l); + } - r = pcrlock_file_system_path(normalized_path, &pcrlock_file); + } else if (!have_old_policy) { + r = make_recovery_key(&pin); if (r < 0) - return r; + return log_error_errno(r, "Failed to generate a randomized recovery PIN: %m"); - r = unlink_pcrlock(pcrlock_file); - if (r < 0) - return r; + if (recovery_pin_mode == RECOVERY_PIN_SHOW) + printf("%s Selected recovery PIN is: %s%s%s\n", + glyph(GLYPH_LOCK_AND_KEY), + ansi_highlight_cyan(), + pin, + ansi_normal()); } - return 0; -} - -static int verb_lock_pe(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; - _cleanup_close_ int fd = -EBADF; - int r; - - // FIXME: Maybe also generate a matching EV_EFI_VARIABLE_AUTHORITY records here for each signature that - // covers this PE plus its hash, as alternatives under the same component name + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + TPM2_HANDLE nv_index = 0; - if (argc >= 2) { - fd = open(argv[1], O_RDONLY|O_CLOEXEC); - if (fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); - } + r = tpm2_deserialize(tc, &nv_blob, &nv_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize NV index TR: %m"); + if (r > 0) + nv_index = old_policy.nv_index; - if (arg_pcr_mask == 0) - arg_pcr_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE; + TPM2B_AUTH auth = {}; + CLEANUP_ERASE(auth); - for (uint32_t i = 0; i < TPM2_PCRS_MAX; i++) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *digests = NULL; + if (pin) { + r = tpm2_auth_value_from_pin(TPM2_ALG_SHA256, pin, &auth); + if (r < 0) + return log_error_errno(r, "Failed to hash PIN: %m"); + } else { + assert(iovec_is_set(&pin_public)); + assert(iovec_is_set(&pin_private)); - if (!BIT_SET(arg_pcr_mask, i)) - continue; + log_debug("Retrieving PIN from sealed data."); - FOREACH_ARRAY(pa, tpm2_hash_algorithms, TPM2_N_HASH_ALGORITHMS) { - _cleanup_free_ void *hash = NULL; - size_t hash_size; - const EVP_MD *md; - const char *a; + usec_t pin_start_usec = now(CLOCK_MONOTONIC); - assert_se(a = tpm2_hash_alg_to_string(*pa)); - assert_se(md = EVP_get_digestbyname(a)); + _cleanup_(iovec_done_erase) struct iovec secret = {}; + for (unsigned attempt = 0;; attempt++) { + _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL; - r = pe_hash(fd < 0 ? STDIN_FILENO : fd, md, &hash, &hash_size); + r = tpm2_make_policy_session( + tc, + srk_handle, + encryption_session, + &policy_session); if (r < 0) - return log_error_errno(r, "Failed to hash PE binary: %m"); + return log_error_errno(r, "Failed to allocate policy session: %m"); - r = sd_json_variant_append_arraybo( - &digests, - SD_JSON_BUILD_PAIR_STRING("hashAlg", a), - SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(hash, hash_size))); + r = tpm2_policy_super_pcr( + tc, + policy_session, + &old_policy.prediction, + old_policy.algorithm); if (r < 0) - return log_error_errno(r, "Failed to build JSON digest object: %m"); - } - - r = sd_json_variant_append_arraybo( - &array, - SD_JSON_BUILD_PAIR_UNSIGNED("pcr", i), - SD_JSON_BUILD_PAIR_VARIANT("digests", digests)); - if (r < 0) - return log_error_errno(r, "Failed to append record object: %m"); - } + return r; - return write_pcrlock(array, NULL); -} + r = tpm2_policy_authorize_nv( + tc, + policy_session, + nv_handle, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to submit AuthorizeNV policy: %m"); -typedef void* SectionHashArray[_UNIFIED_SECTION_MAX * TPM2_N_HASH_ALGORITHMS]; + r = tpm2_unseal_data( + tc, + &pin_public, + &pin_private, + srk_handle, + policy_session, + encryption_session, + &secret); + if (r < 0 && (r != -ESTALE || attempt >= 16)) + return log_error_errno(r, "Failed to unseal PIN: %m"); + if (r == 0) + break; -static void section_hashes_array_done(SectionHashArray *array) { - assert(array); + log_debug("Trying again (attempt %u), as PCR values changed during unlock attempt.", attempt+1); + } - for (size_t i = 0; i < _UNIFIED_SECTION_MAX * TPM2_N_HASH_ALGORITHMS; i++) - free((*array)[i]); -} + if (secret.iov_len > sizeof_field(TPM2B_AUTH, buffer)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Decrypted PIN too large."); -static int verb_lock_uki(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL, *pe_digests = NULL; - _cleanup_(section_hashes_array_done) SectionHashArray section_hashes = {}; - size_t hash_sizes[TPM2_N_HASH_ALGORITHMS]; - _cleanup_close_ int fd = -EBADF; - int r; + auth = (TPM2B_AUTH) { + .size = secret.iov_len, + }; - if (arg_pcr_mask != 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PCR not configurable for UKI lock down."); + memcpy_safe(auth.buffer, secret.iov_base, secret.iov_len); - if (argc >= 2) { - fd = open(argv[1], O_RDONLY|O_CLOEXEC); - if (fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); + log_info("Retrieved PIN from TPM2 in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pin_start_usec), 1)); } - for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { - _cleanup_free_ void *peh = NULL; - const EVP_MD *md; - const char *a; + /* Now convert the PIN into an HMAC-SHA256 key that we can use in PolicySigned to protect access to the nvindex with */ + _cleanup_(tpm2_handle_freep) Tpm2Handle *pin_handle = NULL; + r = tpm2_hmac_key_from_pin(tc, encryption_session, &auth, &pin_handle); + if (r < 0) + return r; - assert_se(a = tpm2_hash_alg_to_string(tpm2_hash_algorithms[i])); - assert_se(md = EVP_get_digestbyname(a)); + TPM2B_NV_PUBLIC nv_public = {}; + usec_t nv_index_start_usec = now(CLOCK_MONOTONIC); - r = pe_hash(fd < 0 ? STDIN_FILENO : fd, md, &peh, hash_sizes + i); + if (!iovec_is_set(&nv_blob)) { + _cleanup_(Esys_Freep) TPM2B_NAME *pin_name = NULL; + r = tpm2_get_name( + tc, + pin_handle, + &pin_name); if (r < 0) - return log_error_errno(r, "Failed to hash PE binary: %m"); + return log_error_errno(r, "Failed to get name of PIN from TPM2: %m"); - r = sd_json_variant_append_arraybo( - &pe_digests, - SD_JSON_BUILD_PAIR_STRING("hashAlg", a), - SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(peh, hash_sizes[i]))); + TPM2B_DIGEST recovery_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + r = tpm2_calculate_policy_signed(&recovery_policy_digest, pin_name); if (r < 0) - return log_error_errno(r, "Failed to build JSON digest object: %m"); + return log_error_errno(r, "Failed to calculate PolicySigned policy: %m"); - r = uki_hash(fd < 0 ? STDIN_FILENO : fd, md, section_hashes + (i * _UNIFIED_SECTION_MAX), hash_sizes + i); + log_debug("Allocating NV index to write PCR policy to..."); + r = tpm2_define_policy_nv_index( + tc, + encryption_session, + arg_nv_index, + &recovery_policy_digest, + &nv_index, + &nv_handle, + &nv_public); + if (r == -EEXIST) + return log_error_errno(r, "NV index 0x%" PRIx32 " already allocated.", arg_nv_index); if (r < 0) - return log_error_errno(r, "Failed to UKI hash PE binary: %m"); + return log_error_errno(r, "Failed to allocate NV index: %m"); } - r = sd_json_variant_append_arraybo( - &array, - SD_JSON_BUILD_PAIR_UNSIGNED("pcr", TPM2_PCR_BOOT_LOADER_CODE), - SD_JSON_BUILD_PAIR_VARIANT("digests", pe_digests)); + _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL; + r = tpm2_make_policy_session( + tc, + srk_handle, + encryption_session, + &policy_session); if (r < 0) - return log_error_errno(r, "Failed to append record object: %m"); + return log_error_errno(r, "Failed to allocate policy session: %m"); - for (UnifiedSection section = 0; section < _UNIFIED_SECTION_MAX; section++) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *section_digests = NULL, *record = NULL; + r = tpm2_policy_signed_hmac_sha256( + tc, + policy_session, + pin_handle, + &IOVEC_MAKE(auth.buffer, auth.size), + /* ret_policy_digest= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to submit authentication value policy: %m"); - if (!unified_section_measure(section)) - continue; + log_debug("Calculating new PCR policy to write..."); + TPM2B_DIGEST new_super_pcr_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); - for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { - const char *a; - void *hash; + usec_t pcr_policy_start_usec = now(CLOCK_MONOTONIC); - hash = section_hashes[i * _UNIFIED_SECTION_MAX + section]; - if (!hash) - continue; + r = tpm2_calculate_policy_super_pcr( + &new_prediction, + el->primary_algorithm, + &new_super_pcr_policy_digest); + if (r < 0) + return log_error_errno(r, "Failed to calculate super PCR policy: %m"); - assert_se(a = tpm2_hash_alg_to_string(tpm2_hash_algorithms[i])); + log_info("Calculated new PCR policy in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pcr_policy_start_usec), 1)); - r = sd_json_variant_append_arraybo( - §ion_digests, - SD_JSON_BUILD_PAIR_STRING("hashAlg", a), - SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(hash, hash_sizes[i]))); - if (r < 0) - return log_error_errno(r, "Failed to build JSON digest object: %m"); - } + log_debug("Writing new PCR policy to NV index..."); + r = tpm2_write_policy_nv_index( + tc, + policy_session, + nv_index, + nv_handle, + &new_super_pcr_policy_digest); + if (r < 0) + return log_error_errno(r, "Failed to write to NV index: %m"); - if (!section_digests) - continue; + log_info("Updated NV index in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), nv_index_start_usec), 1)); - /* So we have digests for this section, hence generate a record for the section name first. */ - r = make_pcrlock_record(TPM2_PCR_KERNEL_BOOT /* =11 */, unified_sections[section], strlen(unified_sections[section]) + 1, &record); + assert(iovec_is_set(&pin_public) == iovec_is_set(&pin_private)); + if (!iovec_is_set(&pin_public)) { + TPM2B_DIGEST authnv_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + + r = tpm2_calculate_policy_authorize_nv(&nv_public, &authnv_policy_digest); if (r < 0) - return r; + return log_error_errno(r, "Failed to calculate AuthorizeNV policy: %m"); - r = sd_json_variant_append_array(&array, record); + struct iovec data = { + .iov_base = auth.buffer, + .iov_len = auth.size, + }; + + usec_t pin_seal_start_usec = now(CLOCK_MONOTONIC); + + log_debug("Sealing PIN to NV index policy..."); + r = tpm2_seal_data( + tc, + &data, + srk_handle, + encryption_session, + &authnv_policy_digest, + &pin_public, + &pin_private); if (r < 0) - return log_error_errno(r, "Failed to append JSON record array: %m"); + return log_error_errno(r, "Failed to seal PIN to NV auth policy: %m"); - /* And then append a record for the section contents digests as well */ - r = sd_json_variant_append_arraybo( - &array, - SD_JSON_BUILD_PAIR_UNSIGNED("pcr", TPM2_PCR_KERNEL_BOOT), - SD_JSON_BUILD_PAIR_VARIANT("digests", section_digests)); + log_info("Sealed PIN in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pin_seal_start_usec), 1)); + } + + if (!iovec_is_set(&nv_blob)) { + r = tpm2_serialize(tc, nv_handle, &nv_blob); if (r < 0) - return log_error_errno(r, "Failed to append record object: %m"); + return log_error_errno(r, "Failed to serialize NV index TR: %m"); } - return write_pcrlock(array, NULL); -} + if (!iovec_is_set(&srk_blob)) { + r = tpm2_serialize(tc, srk_handle, &srk_blob); + if (r < 0) + return log_error_errno(r, "Failed to serialize SRK index TR: %m"); + } -static int verb_lock_kernel_cmdline(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL, *array = NULL; - _cleanup_free_ char *cmdline = NULL; - int r; + if (!iovec_is_set(&nv_public_blob)) { + r = tpm2_marshal_nv_public(&nv_public, &nv_public_blob.iov_base, &nv_public_blob.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to marshal NV public area: %m"); + } - if (argc > 1) { - if (empty_or_dash(argv[1])) - r = read_full_stream(stdin, &cmdline, NULL); - else - r = read_full_file(argv[1], &cmdline, NULL); - } else - r = proc_cmdline(&cmdline); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *new_configuration_json = NULL; + r = sd_json_buildo( + &new_configuration_json, + SD_JSON_BUILD_PAIR_STRING("pcrBank", tpm2_hash_alg_to_string(el->primary_algorithm)), + SD_JSON_BUILD_PAIR_VARIANT("pcrValues", new_prediction_json), + SD_JSON_BUILD_PAIR_INTEGER("nvIndex", nv_index), + JSON_BUILD_PAIR_IOVEC_BASE64("nvHandle", &nv_blob), + JSON_BUILD_PAIR_IOVEC_BASE64("nvPublic", &nv_public_blob), + JSON_BUILD_PAIR_IOVEC_BASE64("srkHandle", &srk_blob), + JSON_BUILD_PAIR_IOVEC_BASE64("pinPublic", &pin_public), + JSON_BUILD_PAIR_IOVEC_BASE64("pinPrivate", &pin_private)); if (r < 0) - return log_error_errno(r, "Failed to read cmdline: %m"); - - delete_trailing_chars(cmdline, "\n"); - - _cleanup_free_ char16_t *u = NULL; - u = utf8_to_utf16(cmdline, SIZE_MAX); - if (!u) - return log_oom(); + return log_error_errno(r, "Failed to generate JSON: %m"); - r = make_pcrlock_record(TPM2_PCR_KERNEL_INITRD /* = 9 */, u, char16_strlen(u)*2+2, &record); + _cleanup_free_ char *text = NULL; + r = sd_json_variant_format(new_configuration_json, 0, &text); if (r < 0) - return r; + return log_error_errno(r, "Failed to format new configuration to JSON: %m"); - r = sd_json_variant_new_array(&array, &record, 1); + const char *path = arg_policy_path ?: (in_initrd() ? "/run/systemd/pcrlock.json" : "/var/lib/systemd/pcrlock.json"); + r = write_string_file(path, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_SYNC|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_LABEL); if (r < 0) - return log_error_errno(r, "Failed to create record array: %m"); + return log_error_errno(r, "Failed to write new configuration to '%s': %m", path); - r = write_pcrlock(array, PCRLOCK_KERNEL_CMDLINE_PATH); - if (r < 0) - return r; + if (!arg_policy_path && !in_initrd()) { + r = remove_policy_file("/run/systemd/pcrlock.json"); + if (r < 0) + return r; + } - return 0; -} + log_info("Written new policy to '%s' and digest to TPM2 NV index 0x%x.", path, nv_index); -static int verb_unlock_kernel_cmdline(int argc, char *argv[], uintptr_t _data, void *userdata) { - return unlink_pcrlock(PCRLOCK_KERNEL_CMDLINE_PATH); -} + (void) write_boot_policy_file(text); -static int verb_lock_kernel_initrd(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *records = NULL; - _cleanup_fclose_ FILE *f = NULL; - uint32_t pcr_mask = UINT32_C(1) << TPM2_PCR_KERNEL_INITRD; - int r; + log_info("Overall time spent: %s", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), start_usec), 1)); - if (argc >= 2) { - f = fopen(argv[1], "re"); - if (!f) - return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); - } + return 1; /* installed new policy */ +} - r = make_pcrlock_record_from_stream(pcr_mask, f ?: stdin, &records); - if (r < 0) - return r; +static int verb_make_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; - r = write_pcrlock(records, PCRLOCK_KERNEL_INITRD_PATH); + r = make_policy(arg_force, arg_recovery_pin); if (r < 0) return r; return 0; } -static int verb_unlock_kernel_initrd(int argc, char *argv[], uintptr_t _data, void *userdata) { - return unlink_pcrlock(PCRLOCK_KERNEL_INITRD_PATH); -} - -static int event_log_reduce_to_safe_pcrs(EventLog *el, uint32_t *pcrs) { - _cleanup_free_ char *dropped = NULL, *kept = NULL; - - assert(el); - assert(pcrs); +static int undefine_policy_nv_index( + uint32_t nv_index, + const struct iovec *nv_blob, + const struct iovec *srk_blob) { + int r; - /* When we compile a new PCR policy we don't want to bind to PCRs which are fishy for one of three - * reasons: - * - * 1. The PCR value doesn't match the event log - * 2. The event log for the PCR contains measurements we don't know responsible components for - * 3. The event log for the PCR does not contain measurements for components we know - * - * This function checks for the three conditions and drops the PCR from the mask. - */ + assert(nv_blob); + assert(srk_blob); - for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; + r = tpm2_context_new_or_warn(/* device= */ NULL, &tc); + if (r < 0) + return r; - if (!BIT_SET(*pcrs, pcr)) - continue; + _cleanup_(tpm2_handle_freep) Tpm2Handle *srk_handle = NULL; + r = tpm2_deserialize( + tc, + srk_blob, + &srk_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize SRK TR: %m"); - if (!event_log_pcr_checks_out(el, el->registers + pcr)) { - log_notice("PCR %" PRIu32 " (%s) value does not match event log. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); - goto drop; - } + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_deserialize( + tc, + nv_blob, + &nv_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize NV TR: %m"); - if (!el->registers[pcr].fully_recognized) { - log_notice("PCR %" PRIu32 " (%s) event log contains unrecognized measurements. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); - goto drop; - } + _cleanup_(tpm2_handle_freep) Tpm2Handle *encryption_session = NULL; + r = tpm2_make_encryption_session( + tc, + srk_handle, + /* bind_key= */ &TPM2_HANDLE_NONE, + &encryption_session); + if (r < 0) + return r; - if (BIT_SET(el->missing_component_pcrs, pcr)) { - log_notice("PCR %" PRIu32 " (%s) is touched by component we can't find in event log. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); - goto drop; - } + r = tpm2_undefine_nv_index( + tc, + encryption_session, + nv_index, + nv_handle); + if (r < 0) + return r; - log_info("PCR %" PRIu32 " (%s) matches event log and fully consists of recognized measurements. Including in set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + log_info("Removed NV index 0x%x", nv_index); + return 0; +} - if (strextendf_with_separator(&kept, ", ", "%" PRIu32 " (%s)", pcr, tpm2_pcr_index_to_string(pcr)) < 0) - return log_oom(); +static int remove_policy(void) { + int ret = 0, r; - continue; + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy policy = {}; + r = tpm2_pcrlock_policy_load(arg_policy_path, &policy); + if (r == 0) { + log_info("No policy found."); + return 0; + } - drop: - *pcrs &= ~(UINT32_C(1) << pcr); + if (r < 0) + log_notice("Failed to load old policy file, assuming it is corrupted, removing."); + else { + r = undefine_policy_nv_index(policy.nv_index, &policy.nv_handle, &policy.srk_handle); + if (r < 0) + log_notice("Failed to remove NV index, assuming data out of date, removing policy file."); - if (strextendf_with_separator(&dropped, ", ", "%" PRIu32 " (%s)", pcr, tpm2_pcr_index_to_string(pcr)) < 0) - return log_oom(); + RET_GATHER(ret, r); } - if (dropped) - log_notice("PCRs dropped from protection mask: %s", dropped); - else - log_debug("No PCRs dropped from protection mask."); + if (arg_policy_path) + RET_GATHER(ret, remove_policy_file(arg_policy_path)); + else { + RET_GATHER(ret, remove_policy_file("/var/lib/systemd/pcrlock.json")); + RET_GATHER(ret, remove_policy_file("/run/systemd/pcrlock.json")); + } - if (kept) - log_notice("PCRs in protection mask: %s", kept); - else - log_notice("No PCRs kept in protection mask."); + _cleanup_free_ char *boot_policy_file = NULL; + r = determine_boot_policy_file(&boot_policy_file, /* ret_credential_name= */ NULL); + if (r == 0) + log_info("Did not find XBOOTLDR/ESP partition, not removing boot policy file."); + else if (r > 0) { + RET_GATHER(ret, remove_policy_file(boot_policy_file)); + } else + RET_GATHER(ret, r); - return 0; + return ret; } -static int pcr_prediction_add_result( - Tpm2PCRPrediction *context, - Tpm2PCRPredictionResult *result, - uint32_t pcr, - const char *path) { +static int verb_remove_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { + return remove_policy(); +} - _cleanup_free_ Tpm2PCRPredictionResult *copy = NULL; +static int test_tpm2_support_pcrlock(Tpm2Support *ret) { int r; - assert(context); - assert(result); + assert(ret); - copy = newdup(Tpm2PCRPredictionResult, result, 1); - if (!copy) - return log_oom(); + /* First check basic support */ + Tpm2Support s = tpm2_support(); - r = ordered_set_ensure_put(context->results + pcr, &tpm2_pcr_prediction_result_hash_ops, copy); - if (r == -EEXIST) /* Multiple identical results for the same PCR are totally expected */ - return 0; - if (r < 0) - return log_error_errno(r, "Failed to insert result into set: %m"); + /* If basic support is available, let's also check the things we need for systemd-pcrlock */ + if (s == TPM2_SUPPORT_FULL) { + _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; + r = tpm2_context_new_or_warn(/* device= */ NULL, &tc); + if (r < 0) + return r; - log_debug("Added prediction result %u for PCR %" PRIu32 " (path: %s)", ordered_set_size(context->results[pcr]), pcr, strempty(path)); + /* We strictly need TPM2_CC_PolicyAuthorizeNV for systemd-pcrlock to work */ + SET_FLAG(s, TPM2_SUPPORT_AUTHORIZE_NV, tpm2_supports_command(tc, TPM2_CC_PolicyAuthorizeNV)); - TAKE_PTR(copy); + log_debug("PolicyAuthorizeNV supported: %s", yes_no(FLAGS_SET(s, TPM2_SUPPORT_AUTHORIZE_NV))); + + /* We also strictly need SHA-256 to work */ + SET_FLAG(s, TPM2_SUPPORT_SHA256, tpm2_supports_alg(tc, TPM2_ALG_SHA256)); + + log_debug("SHA-256 supported: %s", yes_no(FLAGS_SET(s, TPM2_SUPPORT_SHA256))); + } + + *ret = s; return 0; } -static const EVP_MD* evp_from_tpm2_alg(uint16_t alg) { - const char *name; +static int verb_is_supported(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; - name = tpm2_hash_alg_to_string(alg); - if (!name) - return NULL; + Tpm2Support s; + r = test_tpm2_support_pcrlock(&s); + if (r < 0) + return r; - return EVP_get_digestbyname(name); -} + if (!arg_quiet) { + if (s == (TPM2_SUPPORT_FULL|TPM2_SUPPORT_API_PCRLOCK)) + printf("%syes%s\n", ansi_green(), ansi_normal()); + else if (FLAGS_SET(s, TPM2_SUPPORT_FULL)) + printf("%sobsolete%s\n", ansi_red(), ansi_normal()); + else if (s == TPM2_SUPPORT_NONE) + printf("%sno%s\n", ansi_red(), ansi_normal()); + else + printf("%spartial%s\n", ansi_yellow(), ansi_normal()); + } -static int event_log_component_variant_calculate( - Tpm2PCRPredictionResult *result, - EventLogComponentVariant *variant, - uint32_t pcr) { + assert_cc((TPM2_SUPPORT_API|TPM2_SUPPORT_API_PCRLOCK) <= 255); /* make sure this is safe to use as process exit status */ - assert(result); - assert(variant); + return ~s & (TPM2_SUPPORT_API|TPM2_SUPPORT_API_PCRLOCK); +} - FOREACH_ARRAY(rr, variant->records, variant->n_records) { - EventLogRecord *rec = *rr; +static int event_log_record_is_action_calling_efi_app(const EventLogRecord *rec) { + _cleanup_free_ char *d = NULL; + int r; - if (!EVENT_LOG_RECORD_IS_PCR(rec)) - continue; + assert(rec); - if (rec->pcr != pcr) - continue; + /* Recognizes the special EV_EFI_ACTION that is issues when the firmware passes control to the boot loader. */ - for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { - _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; - EventLogRecordBank *b; + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; - if (result->hash[i].size <= 0) /* already invalidated */ - continue; + if (rec->pcr != TPM2_PCR_BOOT_LOADER_CODE) + return false; - b = event_log_record_find_bank(rec, tpm2_hash_algorithms[i]); - if (!b) { - /* Can't calculate, hence invalidate */ - result->hash[i] = (TPM2B_DIGEST) {}; - continue; - } + if (rec->firmware_event_type != EV_EFI_ACTION) + return false; - md_ctx = EVP_MD_CTX_new(); - if (!md_ctx) - return log_oom(); + if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) /* Insist the record is consistent */ + return false; - const EVP_MD *md = ASSERT_PTR(evp_from_tpm2_alg(tpm2_hash_algorithms[i])); + r = make_cstring(rec->firmware_payload, rec->firmware_payload_size, MAKE_CSTRING_ALLOW_TRAILING_NUL, &d); + if (r < 0) + return r; - int sz = EVP_MD_size(md); - assert(sz > 0); - assert((size_t) sz <= sizeof_field(TPM2B_DIGEST, buffer)); + return streq(d, "Calling EFI Application from Boot Option"); +} - assert(sz == tpm2_hash_alg_to_size(tpm2_hash_algorithms[i])); +static void enable_json_sse(void) { + /* We shall write this to a single output stream? We have to output two files, hence try to be smart + * and enable JSON SSE */ - assert(result->hash[i].size == (size_t) sz); - assert(b->hash.size == (size_t) sz); + if (!arg_pcrlock_path && arg_pcrlock_auto) + return; - if (EVP_DigestInit_ex(md_ctx, md, NULL) != 1) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to initialize message digest."); + if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_SSE)) + return; - if (EVP_DigestUpdate(md_ctx, result->hash[i].buffer, sz) != 1) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash bank value."); + log_notice("Enabling JSON_SEQ mode, since writing two .pcrlock files to single output."); + arg_json_format_flags |= SD_JSON_FORMAT_SSE; +} - if (EVP_DigestUpdate(md_ctx, b->hash.buffer, sz) != 1) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data value."); +static bool event_log_record_is_separator(const EventLogRecord *rec) { + assert(rec); - unsigned l = (unsigned) sz; - if (EVP_DigestFinal_ex(md_ctx, result->hash[i].buffer, &l) != 1) - return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize message digest."); + /* Recognizes EV_SEPARATOR events */ - assert(l == (unsigned) sz); - } - } + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; - return 0; + if (rec->firmware_event_type != EV_SEPARATOR) + return false; + + return rec->event_payload_valid == EVENT_PAYLOAD_VALID_YES; /* Insist the record is consistent */ } -static int event_log_predict_pcrs( - EventLog *el, - Tpm2PCRPrediction *context, - Tpm2PCRPredictionResult *parent_result, - size_t component_index, - uint32_t pcr, - const char *path) { +static int verb_lock_firmware(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array_early = NULL, *array_late = NULL; + _cleanup_(event_log_freep) EventLog *el = NULL; + uint32_t always_mask, separator_mask, separator_seen_mask = 0, action_seen_mask = 0; + const char *default_pcrlock_early_path, *default_pcrlock_late_path; + int r; - EventLogComponent *component; - int count = 0, r; + enable_json_sse(); - assert(el); - assert(context); - assert(parent_result); + /* The PCRs we intend to cover. Note that we measure firmware, external *and* boot loader code/config + * here – but the latter only until the "separator" events are seen, which tell us where transition + * into OS boot loader happens. This reflects the fact that on some systems the firmware already + * measures some firmware-supplied apps into PCR 4. (e.g. Thinkpad X1 Gen9) */ + if (endswith(argv[0], "firmware-code")) { + always_mask = (UINT32_C(1) << TPM2_PCR_PLATFORM_CODE) | /* → 0 */ + (UINT32_C(1) << TPM2_PCR_EXTERNAL_CODE); /* → 2 */ - /* Check if we reached the end of the components, generate a result, and backtrack */ - if (component_index >= el->n_components || - (arg_location_end && strcmp(el->components[component_index]->id, arg_location_end) > 0)) { - r = pcr_prediction_add_result(context, parent_result, pcr, path); - if (r < 0) - return r; + separator_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE; /* → 4 */ - return 1; - } + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CODE_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CODE_LATE_PATH; + } else { + assert(endswith(argv[0], "firmware-config")); + always_mask = (UINT32_C(1) << TPM2_PCR_PLATFORM_CONFIG) | /* → 1 */ + (UINT32_C(1) << TPM2_PCR_EXTERNAL_CONFIG); /* → 3 */ - component = ASSERT_PTR(el->components[component_index]); + separator_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CONFIG; /* → 5 */ - /* Check if we are just about to process a component after start, if so record a result and continue. */ - if (arg_location_start && strcmp(component->id, arg_location_start) > 0) { - r = pcr_prediction_add_result(context, parent_result, pcr, path); - if (r < 0) - return r; + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CONFIG_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CONFIG_LATE_PATH; } - FOREACH_ARRAY(ii, component->variants, component->n_variants) { - _cleanup_free_ Tpm2PCRPredictionResult *result = NULL; - EventLogComponentVariant *variant = *ii; - _cleanup_free_ char *subpath = NULL; + el = event_log_new(); + if (!el) + return log_oom(); - /* Operate on a copy of the result */ + r = event_log_add_algorithms_from_environment(el); + if (r < 0) + return r; - if (path) - subpath = strjoin(path, ":", component->id); - else - subpath = strdup(component->id); - if (!subpath) - return log_oom(); + r = event_log_load(el); + if (r < 0) + return r; - if (!streq(component->id, variant->id)) - if (!strextend(&subpath, "@", variant->id)) - return log_oom(); + r = event_log_read_pcrs(el); + if (r < 0) + return r; - result = newdup(Tpm2PCRPredictionResult, parent_result, 1); - if (!result) - return log_oom(); + r = event_log_calculate_pcrs(el); + if (r < 0) + return r; - r = event_log_component_variant_calculate( - result, - variant, - pcr); - if (r < 0) - return r; - - r = event_log_predict_pcrs( - el, - context, - result, - component_index + 1, /* Next component */ - pcr, - subpath); - if (r < 0) - return r; - - count += r; - } - - return count; -} - -static ssize_t event_log_calculate_component_combinations(EventLog *el) { - ssize_t count = 1; - assert(el); - - FOREACH_ARRAY(cc, el->components, el->n_components) { - EventLogComponent *c = *cc; - - assert(c->n_variants > 0); - - /* Overflow check */ - if (c->n_variants > (size_t) (SSIZE_MAX/count)) - return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many component combinations."); - count *= c->n_variants; - } - - return count; -} + r = event_log_validate_record_hashes(el); + if (r < 0) + return r; -static int event_log_show_predictions(Tpm2PCRPrediction *context, uint16_t alg) { - int r; + /* Before we base anything on the event log records for any of the selected PCRs, let's check that + * the event log state checks out for them. */ - assert(context); + r = event_log_pcr_mask_checks_out(el, always_mask|separator_mask); + if (r < 0) + return r; - pager_open(arg_pager_flags); + // FIXME: before doing this, validate ahead-of-time that EV_SEPARATOR records exist for all entries, + // and exactly once - if (sd_json_format_enabled(arg_json_format_flags)) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL; + FOREACH_ARRAY(rr, el->records, el->n_records) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *digests = NULL; + EventLogRecord *rec = *rr; + uint32_t bit = UINT32_C(1) << rec->pcr; - for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *aj = NULL; + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + continue; - r = tpm2_pcr_prediction_to_json( - context, - tpm2_hash_algorithms[i], - &aj); - if (r < 0) - return r; + if (!FLAGS_SET(always_mask, bit) && + !(FLAGS_SET(separator_mask, bit) && !FLAGS_SET(separator_seen_mask|action_seen_mask, bit))) + continue; - if (sd_json_variant_elements(aj) == 0) - continue; + /* If we hit the separator record, we stop processing the PCRs listed in `separator_mask` */ + if (event_log_record_is_separator(rec)) { + separator_seen_mask |= bit; + continue; + } - r = sd_json_variant_set_field( - &j, - tpm2_hash_alg_to_string(tpm2_hash_algorithms[i]), - aj); - if (r < 0) - return log_error_errno(r, "Failed to add prediction bank to object: %m"); + /* If we hit the special "Calling EFI Application from Boot Option" action we treat this the + * same as a separator here, as that's where firmware passes control to boot loader. Note + * that some EFI implementations forget to generate one of them. */ + r = event_log_record_is_action_calling_efi_app(rec); + if (r < 0) + return log_error_errno(r, "Failed to check if event is 'Calling EFI Application from Boot Option' action: %m"); + if (r > 0) { + action_seen_mask |= bit; + continue; } - if (!j) { - r = sd_json_variant_new_object(&j, NULL, 0); + LIST_FOREACH(banks, bank, rec->banks) { + r = sd_json_variant_append_arraybo( + &digests, + SD_JSON_BUILD_PAIR_STRING("hashAlg", tpm2_hash_alg_to_string(bank->algorithm)), + SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(bank->hash.buffer, bank->hash.size))); if (r < 0) - return log_error_errno(r, "Failed to allocated empty object: %m"); + return log_error_errno(r, "Failed to build digests array: %m"); } - sd_json_variant_dump(j, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL); - return 0; + r = sd_json_variant_append_arraybo( + FLAGS_SET(separator_seen_mask, bit) ? &array_late : &array_early, + SD_JSON_BUILD_PAIR_UNSIGNED("pcr", rec->pcr), + SD_JSON_BUILD_PAIR_VARIANT("digests", digests)); + if (r < 0) + return log_error_errno(r, "Failed to build record array: %m"); } - for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { - Tpm2PCRPredictionResult *result; - if (!BIT_SET(context->pcrs, pcr)) - continue; - - if (ordered_set_isempty(context->results[pcr])) { - printf("No results for PCR %u (%s).\n", pcr, tpm2_pcr_index_to_string(pcr)); - continue; - } - - printf("%sResults for PCR %u (%s):%s\n", ansi_underline(), pcr, tpm2_pcr_index_to_string(pcr), ansi_normal()); - - ORDERED_SET_FOREACH(result, context->results[pcr]) { + r = write_pcrlock(array_early, default_pcrlock_early_path); + if (r < 0) + return r; - _cleanup_free_ char *aa = NULL, *h = NULL; - const char *a; + return write_pcrlock(array_late, default_pcrlock_late_path); +} - TPM2B_DIGEST *hash = tpm2_pcr_prediction_result_get_hash(result, alg); - if (!hash) - continue; +static int verb_unlock_firmware(int argc, char *argv[], uintptr_t _data, void *userdata) { + const char *default_pcrlock_early_path, *default_pcrlock_late_path; + int r; - a = ASSERT_PTR(tpm2_hash_alg_to_string(alg)); - aa = strdup(a); - if (!aa) - return log_oom(); + if (endswith(argv[0], "firmware-code")) { + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CODE_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CODE_LATE_PATH; + } else { + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CONFIG_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CONFIG_LATE_PATH; + } - ascii_strlower(aa); + r = unlink_pcrlock(default_pcrlock_early_path); + if (r < 0) + return r; - h = hexmem(hash->buffer, hash->size); - if (!h) - return log_oom(); + if (arg_pcrlock_path) /* if the path is specified don't delete the same thing twice */ + return 0; - printf(" %s%-6s:%s %s\n", ansi_grey(), aa, ansi_normal(), h); - } - } + r = unlink_pcrlock(default_pcrlock_late_path); + if (r < 0) + return r; return 0; } -static int tpm2_pcr_prediction_run( - EventLog *el, - Tpm2PCRPrediction *context) { +static int verb_lock_secureboot_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { + static const struct { + sd_id128_t id; + const char *name; + int synthesize_empty; /* 0 → fail, > 0 → synthesize empty db, < 0 → skip */ + } variables[] = { + { EFI_VENDOR_GLOBAL, "SecureBoot", 0 }, + { EFI_VENDOR_GLOBAL, "PK", 1 }, + { EFI_VENDOR_GLOBAL, "KEK", 1 }, + { EFI_VENDOR_DATABASE, "db", 1 }, + { EFI_VENDOR_DATABASE, "dbx", 1 }, + { EFI_VENDOR_DATABASE, "dbt", -1 }, + { EFI_VENDOR_DATABASE, "dbr", -1 }, + }; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; int r; - assert(el); - assert(context); + /* Generates expected records from the current SecureBoot state, as readable in the EFI variables + * right now. */ - for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { - _cleanup_free_ Tpm2PCRPredictionResult *result = NULL; + FOREACH_ELEMENT(vv, variables) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL; - if (!BIT_SET(context->pcrs, pcr)) - continue; + _cleanup_free_ char *name = NULL; + if (asprintf(&name, "%s-" SD_ID128_UUID_FORMAT_STR, vv->name, SD_ID128_FORMAT_VAL(vv->id)) < 0) + return log_oom(); - result = new0(Tpm2PCRPredictionResult, 1); - if (!result) + _cleanup_free_ void *data = NULL; + size_t data_size; + r = efi_get_variable(name, NULL, &data, &data_size); + if (r < 0) { + if (r != -ENOENT || vv->synthesize_empty == 0) + return log_error_errno(r, "Failed to read EFI variable '%s': %m", name); + if (vv->synthesize_empty < 0) + continue; + + /* If the main database variables are not set we don't consider this an error, but + * measure an empty database instead. */ + log_debug("EFI variable %s is not set, synthesizing empty variable for measurement.", name); + data_size = 0; + } + + _cleanup_free_ char16_t* name16 = utf8_to_utf16(vv->name, SIZE_MAX); + if (!name16) return log_oom(); + size_t name16_bytes = char16_strlen(name16) * 2; - for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) - event_log_initial_pcr_state(el, pcr, tpm2_hash_alg_to_size(tpm2_hash_algorithms[i]), result->hash + i); + size_t vdata_size = offsetof(UEFI_VARIABLE_DATA, unicodeName) + name16_bytes + data_size; + _cleanup_free_ UEFI_VARIABLE_DATA *vdata = malloc(vdata_size); + if (!vdata) + return log_oom(); - r = event_log_predict_pcrs( - el, - context, - result, - /* component_index= */ 0, - pcr, - /* path= */ NULL); + *vdata = (UEFI_VARIABLE_DATA) { + .unicodeNameLength = name16_bytes / 2, + .variableDataLength = data_size, + }; + + efi_id128_to_guid(vv->id, vdata->variableName); + memcpy(mempcpy(vdata->unicodeName, name16, name16_bytes), data, data_size); + + r = make_pcrlock_record(TPM2_PCR_SECURE_BOOT_POLICY /* =7 */, vdata, vdata_size, &record); if (r < 0) return r; + + r = sd_json_variant_append_array(&array, record); + if (r < 0) + return log_error_errno(r, "Failed to append to JSON array: %m"); } - return 0; + return write_pcrlock(array, PCRLOCK_SECUREBOOT_POLICY_PATH); } -static int verb_predict(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction context = { - arg_pcr_mask != 0 ? arg_pcr_mask : DEFAULT_PCR_MASK, - }; - _cleanup_(event_log_freep) EventLog *el = NULL; - ssize_t count; +static int verb_unlock_secureboot_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { + return unlink_pcrlock(PCRLOCK_SECUREBOOT_POLICY_PATH); +} + +static int event_log_record_is_secureboot_variable(EventLogRecord *rec, sd_id128_t uuid, const char *name) { + _cleanup_free_ char *found_name = NULL; + sd_id128_t found_uuid; int r; - r = event_log_load_and_process(&el); - if (r < 0) - return r; + assert(rec); + assert(name); - count = event_log_calculate_component_combinations(el); - if (count < 0) - return count; + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; - log_info("%zi combinations of components.", count); + if (rec->pcr != TPM2_PCR_SECURE_BOOT_POLICY) + return false; - r = event_log_reduce_to_safe_pcrs(el, &context.pcrs); - if (r < 0) - return r; + if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) + return false; - r = tpm2_pcr_prediction_run(el, &context); + if (rec->firmware_event_type != EV_EFI_VARIABLE_DRIVER_CONFIG) + return false; + + r = event_log_record_parse_variable_data(rec, &found_uuid, &found_name); + if (r == -EBADMSG) + return false; if (r < 0) return r; - return event_log_show_predictions(&context, el->primary_algorithm); -} - -static int remove_policy_file(const char *path) { - assert(path); + if (!sd_id128_equal(found_uuid, uuid)) + return false; - if (unlink(path) < 0) { - if (errno == ENOENT) - return 0; + return streq(found_name, name); +} - return log_error_errno(errno, "Failed to remove policy file '%s': %m", path); - } +static bool event_log_record_is_secureboot_authority(EventLogRecord *rec) { + assert(rec); - log_info("Removed policy file '%s'.", path); - return 1; -} + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; -static int determine_boot_policy_file(char **ret_path, char **ret_credential_name) { - int r; + if (rec->pcr != TPM2_PCR_SECURE_BOOT_POLICY) + return false; - _cleanup_free_ char *path = NULL; - r = get_global_boot_credentials_path(&path); - if (r < 0) - return r; - if (r == 0) { - if (ret_path) - *ret_path = NULL; - if (ret_credential_name) - *ret_credential_name = NULL; - return 0; /* not found! */ - } + if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) + return false; - sd_id128_t machine_id; - r = sd_id128_get_machine(&machine_id); - if (r < 0) - return log_error_errno(r, "Failed to read machine ID: %m"); + return rec->firmware_event_type == EV_EFI_VARIABLE_AUTHORITY; +} - r = boot_entry_token_ensure( - /* root= */ NULL, - /* conf_root= */ NULL, - machine_id, - /* machine_id_is_random= */ false, - &arg_entry_token_type, - &arg_entry_token); - if (r < 0) - return r; +static int event_log_ensure_secureboot_consistency(EventLog *el) { + static const struct { + sd_id128_t id; + const char *name; + bool required; + } table[] = { + { EFI_VENDOR_GLOBAL, "SecureBoot", true }, + { EFI_VENDOR_GLOBAL, "PK", true }, + { EFI_VENDOR_GLOBAL, "KEK", true }, + { EFI_VENDOR_DATABASE, "db", true }, + { EFI_VENDOR_DATABASE, "dbx", true }, + { EFI_VENDOR_DATABASE, "dbt", false }, + { EFI_VENDOR_DATABASE, "dbr", false }, + // FIXME: ensure we also find the separator here + }; - _cleanup_free_ char *fn = strjoin("pcrlock.", arg_entry_token, ".cred"); - if (!fn) - return log_oom(); + EventLogRecord *records[ELEMENTSOF(table)] = {}; + EventLogRecord *first_authority = NULL; - if (!filename_is_valid(fn)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name '%s' would not be a valid file name, refusing.", fn); + assert(el); - _cleanup_free_ char *joined = NULL; - if (ret_path) { - joined = path_join(path, fn); - if (!joined) - return log_oom(); - } + /* Ensures that the PCR 7 records are complete and in order. Before we lock down PCR 7 we want to + * ensure its state is actually consistent. */ - _cleanup_free_ char *cn = NULL; - if (ret_credential_name) { - /* The .cred suffix of the file is stripped when PID 1 imports the credential, hence exclude it from - * the embedded credential name. */ - cn = strjoin("pcrlock.", arg_entry_token); - if (!cn) - return log_oom(); + FOREACH_ARRAY(rr, el->records, el->n_records) { + EventLogRecord *rec = *rr; + size_t found = SIZE_MAX; - ascii_strlower(cn); /* lowercase this file, no matter what, since stored on VFAT, and we don't want - * to run into case change incompatibilities */ - } + if (event_log_record_is_secureboot_authority(rec)) { + if (first_authority) + continue; - if (ret_path) - *ret_path = TAKE_PTR(joined); + first_authority = rec; + // FIXME: also check that each authority record's data is also listed in 'db' + continue; + } - if (ret_credential_name) - *ret_credential_name = TAKE_PTR(cn); + for (size_t i = 0; i < ELEMENTSOF(table); i++) + if (event_log_record_is_secureboot_variable(rec, table[i].id, table[i].name)) { + found = i; + break; + } + if (found == SIZE_MAX) + continue; - return 1; /* found! */ -} + /* Require the authority records always come *after* database measurements */ + if (first_authority) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "SecureBoot authority before variable, refusing."); -static int write_boot_policy_file(const char *json_text) { - _cleanup_free_ char *boot_policy_file = NULL, *credential_name = NULL; - int r; + /* Check for duplicates */ + if (records[found]) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate '%s' record, refusing.", rec->description); - assert(json_text); + /* Check for order */ + for (size_t j = found + 1; j < ELEMENTSOF(table); j++) + if (records[j]) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'%s' record before '%s' record, refusing.", records[j]->description, rec->description); - r = determine_boot_policy_file(&boot_policy_file, &credential_name); - if (r < 0) - return r; - if (r == 0) { - log_info("Did not find XBOOTLDR/ESP partition, not writing boot policy file."); - return 0; + records[found] = rec; } - _cleanup_(iovec_done) struct iovec encoded = {}; - r = encrypt_credential_and_warn( - CRED_AES256_GCM_BY_NULL, - credential_name, - now(CLOCK_REALTIME), - /* not_after= */ USEC_INFINITY, - /* tpm2_device= */ NULL, - /* tpm2_hash_pcr_mask= */ 0, - /* tpm2_pubkey_path= */ NULL, - /* tpm2_pubkey_pcr_mask= */ 0, - UID_INVALID, - &IOVEC_MAKE_STRING(json_text), - CREDENTIAL_ALLOW_NULL, - &encoded); - if (r < 0) - return log_error_errno(r, "Failed to encode policy as credential: %m"); - - r = write_base64_file_at( - AT_FDCWD, - boot_policy_file, - &encoded, - WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_SYNC|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_LABEL); - if (r < 0) - return log_error_errno(r, "Failed to write boot policy file to '%s': %m", boot_policy_file); + /* Check for existence */ + for (size_t i = 0; i < ELEMENTSOF(table); i++) + if (table[i].required && !records[i]) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Required record '%s' not found, refusing.", table[i].name); - log_info("Written new boot policy to '%s'.", boot_policy_file); - return 1; + /* At this point we know that all required variables have been measured, in the right order. */ + return 0; } -static int make_policy(bool force, RecoveryPinMode recovery_pin_mode) { +static int verb_lock_secureboot_authority(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + _cleanup_(event_log_freep) EventLog *el = NULL; int r; - /* Here's how this all works: after predicting all possible PCR values for next boot (with - * alternatives) we'll calculate a policy from it as a combination of PolicyPCR + PolicyOR - * expressions. This is then stored in an NV index. When a component of the boot process is changed a - * new prediction is made and the NV index updated (which automatically invalidates any older - * policies). - * - * Whenever we want to lock an encrypted object (for example FDE) against this policy, we'll use a - * PolicyAuthorizeNV expression that pins the NV index in the policy, and permits access to any - * policies matching the current NV index contents. - * - * We grant world-readable read access to the NV index. Write access is controlled by a PIN (which we - * either generate locally or which the user can provide us with) which can also be used for - * recovery. This PIN is sealed to the TPM and is locked via PolicyAuthorizeNV to the NV index it - * protects (i.e. we dogfood 🌭 🐶 hard here). This means in order to update such a policy we need - * the policy to pass. - * - * Information about the used NV Index, the SRK of the TPM, the sealed PIN and the current PCR - * prediction data are stored in a JSON file in /var/lib/. In order to be able to unlock root disks - * this data must be also copied to the ESP so that it is available to the initrd. The data is not - * sensitive, as SRK and NV index are pinned by it, and the prediction data must match the NV index - * to be useful. */ + /* Lock down the EV_EFI_VARIABLE_AUTHORITY records from the existing log. Note that there's not too + * much value in locking this down too much, since it stores only the result of the primary database + * checks, and that's what we should bind policy to. Moreover it's hard to predict, since extension + * card firmware validation will result in additional records here. */ - usec_t start_usec = now(CLOCK_MONOTONIC); + if (!is_efi_secure_boot()) { + log_info("SecureBoot disabled, not generating authority .pcrlock file."); + return unlink_pcrlock(PCRLOCK_SECUREBOOT_AUTHORITY_PATH); + } - _cleanup_(event_log_freep) EventLog *el = NULL; - r = event_log_load_and_process(&el); + el = event_log_new(); + if (!el) + return log_oom(); + + r = event_log_add_algorithms_from_environment(el); if (r < 0) return r; - _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction new_prediction = { - arg_pcr_mask != 0 ? arg_pcr_mask : DEFAULT_PCR_MASK, - }; - r = event_log_reduce_to_safe_pcrs(el, &new_prediction.pcrs); + r = event_log_load(el); if (r < 0) return r; - if (!force && new_prediction.pcrs == 0) - log_notice("Set of PCRs to use for policy is empty. Generated policy will not provide any protection in its current form. Proceeding."); - - usec_t predict_start_usec = now(CLOCK_MONOTONIC); + r = event_log_read_pcrs(el); + if (r < 0) + return r; - r = tpm2_pcr_prediction_run(el, &new_prediction); + r = event_log_calculate_pcrs(el); if (r < 0) return r; - log_info("Predicted future PCRs in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), predict_start_usec), 1)); + /* Before we base anything on the event log records, let's check that the event log state checks + * out. */ - _cleanup_(sd_json_variant_unrefp) sd_json_variant *new_prediction_json = NULL; - r = tpm2_pcr_prediction_to_json(&new_prediction, el->primary_algorithm, &new_prediction_json); + r = event_log_pcr_mask_checks_out(el, UINT32_C(1) << TPM2_PCR_SECURE_BOOT_POLICY); if (r < 0) return r; - if (DEBUG_LOGGING) - (void) sd_json_variant_dump(new_prediction_json, SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO, stderr, NULL); + r = event_log_validate_record_hashes(el); + if (r < 0) + return r; - /* v257 and older mistakenly used --pcrlock= for the path. To keep backward compatibility, let's fallback to it when - * --policy= is unspecified but --pcrlock is specified. */ - if (!arg_policy_path && arg_pcrlock_path) { - log_notice("Specified --pcrlock= option for make-policy command. Please use --policy= instead."); + r = event_log_ensure_secureboot_consistency(el); + if (r < 0) + return r; - arg_policy_path = strdup(arg_pcrlock_path); - if (!arg_policy_path) - return log_oom(); + FOREACH_ARRAY(rr, el->records, el->n_records) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *digests = NULL; + EventLogRecord *rec = *rr; + + if (!event_log_record_is_secureboot_authority(rec)) + continue; + + log_debug("Locking down authority '%s'.", strna(rec->description)); + + LIST_FOREACH(banks, bank, rec->banks) { + r = sd_json_variant_append_arraybo( + &digests, + SD_JSON_BUILD_PAIR_STRING("hashAlg", tpm2_hash_alg_to_string(bank->algorithm)), + SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(bank->hash.buffer, bank->hash.size))); + if (r < 0) + return log_error_errno(r, "Failed to build digests array: %m"); + } + + r = sd_json_variant_append_arraybo( + &array, + SD_JSON_BUILD_PAIR_UNSIGNED("pcr", rec->pcr), + SD_JSON_BUILD_PAIR_VARIANT("digests", digests)); + if (r < 0) + return log_error_errno(r, "Failed to build record array: %m"); } - _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy old_policy = {}; - r = tpm2_pcrlock_policy_load(arg_policy_path, &old_policy); + return write_pcrlock(array, PCRLOCK_SECUREBOOT_AUTHORITY_PATH); +} + +static int verb_unlock_secureboot_authority(int argc, char *argv[], uintptr_t _data, void *userdata) { + return unlink_pcrlock(PCRLOCK_SECUREBOOT_AUTHORITY_PATH); +} + +static int verb_lock_gpt(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL, *record = NULL; + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + uint8_t h[2 * 4096]; /* space for at least two 4K sectors. GPT header should definitely be in here */ + uint64_t start, n_members, member_size; + _cleanup_close_ int fd = -EBADF; + const GptHeader *p; + size_t found = 0; + ssize_t n; + int r; + + r = block_device_new_from_path( + argc >= 2 ? argv[1] : "/", + BLOCK_DEVICE_LOOKUP_WHOLE_DISK|BLOCK_DEVICE_LOOKUP_BACKING|BLOCK_DEVICE_LOOKUP_ORIGINATING, + &d); if (r < 0) - return r; + return log_error_errno(r, "Failed to determine root block device: %m"); - bool have_old_policy = r > 0; + fd = sd_device_open(d, O_CLOEXEC|O_RDONLY|O_NOCTTY); + if (fd < 0) + return log_error_errno(fd, "Failed to open root block device: %m"); - /* When we update the policy the old serializations for NV, SRK, PIN remain the same */ - _cleanup_(iovec_done) struct iovec - nv_blob = TAKE_STRUCT(old_policy.nv_handle), - nv_public_blob = TAKE_STRUCT(old_policy.nv_public), - srk_blob = TAKE_STRUCT(old_policy.srk_handle), - pin_public = TAKE_STRUCT(old_policy.pin_public), - pin_private = TAKE_STRUCT(old_policy.pin_private); + n = pread(fd, &h, sizeof(h), 0); + if (n < 0) + return log_error_errno(errno, "Failed to read GPT header of block device: %m"); + if ((size_t) n != sizeof(h)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read trying to read GPT header."); - if (have_old_policy) { - if (arg_nv_index != 0 && old_policy.nv_index != arg_nv_index) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Stored policy references different NV index (0x%x) than specified (0x%x), refusing.", old_policy.nv_index, arg_nv_index); + /* Try a couple of sector sizes */ + for (size_t sz = 512; sz <= 4096; sz <<= 1) { + assert(sizeof(h) >= sz * 2); + p = (const GptHeader*) (h + sz); /* 2nd sector */ - if (!force && - old_policy.algorithm == el->primary_algorithm && - tpm2_pcr_prediction_equal(&old_policy.prediction, &new_prediction, el->primary_algorithm)) { - log_info("Prediction is identical to current policy, skipping update."); - return 0; /* NOP */ - } + if (!gpt_header_has_signature(p)) + continue; + + if (found != 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Disk has partition table for multiple sector sizes, refusing."); + + found = sz; } - _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; - r = tpm2_context_new_or_warn(/* device= */ NULL, &tc); + if (found == 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Disk does not have GPT partition table, refusing."); + + p = (const GptHeader*) (h + found); + + if (le32toh(p->header_size) > found) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "GPT header size over long (%" PRIu32 "), refusing.", le32toh(p->header_size)); + + start = le64toh(p->partition_entry_lba); + if (start > UINT64_MAX / found) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Partition table start offset overflow, refusing."); + + member_size = le32toh(p->size_of_partition_entry); + if (member_size < sizeof(GptPartitionEntry)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Partition entry size too short, refusing."); + + n_members = le32toh(p->number_of_partition_entries); + uint64_t member_bufsz = n_members * member_size; + if (member_bufsz > 1U*1024U*1024U) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Partition table size too large, refusing."); + + member_bufsz = ROUND_UP(member_bufsz, found); + + _cleanup_free_ void *members = malloc(member_bufsz); + if (!members) + return log_oom(); + + n = pread(fd, members, member_bufsz, start * found); + if (n < 0) + return log_error_errno(errno, "Failed to read GPT partition table entries: %m"); + if ((size_t) n != member_bufsz) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading GPT partition table entries."); + + size_t vdata_size = le32toh(p->header_size) + sizeof(le64_t) + member_size * n_members; + _cleanup_free_ void *vdata = malloc0(vdata_size); + if (!vdata) + return log_oom(); + + void *n_measured_entries = mempcpy(vdata, p, sizeof(GptHeader)); /* n_measured_entries is a 64bit value */ + + void *qq = (uint8_t*) n_measured_entries + sizeof(le64_t); + + for (uint64_t i = 0; i < n_members; i++) { + const GptPartitionEntry *entry = (const GptPartitionEntry*) ((const uint8_t*) members + (member_size * i)); + + if (memeqzero(entry->partition_type_guid, sizeof(entry->partition_type_guid))) + continue; + + qq = mempcpy(qq, entry, member_size); + unaligned_write_le64(n_measured_entries, unaligned_read_le64(n_measured_entries) + 1); + } + + vdata_size = (uint8_t*) qq - (uint8_t*) vdata; + + r = make_pcrlock_record(TPM2_PCR_BOOT_LOADER_CONFIG /* =5 */, vdata, vdata_size, &record); if (r < 0) return r; - if (!tpm2_supports_command(tc, TPM2_CC_PolicyAuthorizeNV)) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 does not support PolicyAuthorizeNV command, refusing."); - if (!tpm2_supports_alg(tc, TPM2_ALG_SHA256)) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 does not support SHA-256 hash algorithm, refusing."); + r = sd_json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to append to JSON array: %m"); - _cleanup_(tpm2_handle_freep) Tpm2Handle *srk_handle = NULL; + return write_pcrlock(array, PCRLOCK_GPT_PATH); +} - r = tpm2_deserialize( - tc, - &srk_blob, - &srk_handle); - if (r < 0) - return log_error_errno(r, "Failed to deserialize SRK TR: %m"); - if (r == 0) { - r = tpm2_get_or_create_srk( - tc, - /* session= */ NULL, - /* ret_public= */ NULL, - /* ret_name= */ NULL, - /* ret_qname= */ NULL, - &srk_handle); - if (r < 0) - return log_error_errno(r, "Failed to install SRK: %m"); +static int verb_unlock_gpt(int argc, char *argv[], uintptr_t _data, void *userdata) { + return unlink_pcrlock(PCRLOCK_GPT_PATH); +} + +static int verb_lock_pe(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + _cleanup_close_ int fd = -EBADF; + int r; + + // FIXME: Maybe also generate a matching EV_EFI_VARIABLE_AUTHORITY records here for each signature that + // covers this PE plus its hash, as alternatives under the same component name + + if (argc >= 2) { + fd = open(argv[1], O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); } - _cleanup_(tpm2_handle_freep) Tpm2Handle *encryption_session = NULL; - r = tpm2_make_encryption_session( - tc, - srk_handle, - /* bind_key= */ &TPM2_HANDLE_NONE, - &encryption_session); - if (r < 0) - return log_error_errno(r, "Failed to allocate encryption session: %m"); + if (arg_pcr_mask == 0) + arg_pcr_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE; - /* Acquire a recovery PIN, either from the user, or create a randomized one */ - _cleanup_(erase_and_freep) char *pin = NULL; - if (recovery_pin_mode == RECOVERY_PIN_QUERY) { - r = getenv_steal_erase("PIN", &pin); - if (r < 0) - return log_error_errno(r, "Failed to acquire PIN from environment: %m"); - if (r == 0) { - _cleanup_strv_free_erase_ char **l = NULL; + for (uint32_t i = 0; i < TPM2_PCRS_MAX; i++) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *digests = NULL; - AskPasswordRequest req = { - .tty_fd = -EBADF, - .message = "Recovery PIN", - .id = "pcrlock-recovery-pin", - .credential = "pcrlock.recovery-pin", - .until = USEC_INFINITY, - .hup_fd = -EBADF, - }; + if (!BIT_SET(arg_pcr_mask, i)) + continue; - r = ask_password_auto( - &req, - /* flags= */ 0, - &l); - if (r < 0) - return log_error_errno(r, "Failed to query for recovery PIN: %m"); + FOREACH_ARRAY(pa, tpm2_hash_algorithms, TPM2_N_HASH_ALGORITHMS) { + _cleanup_free_ void *hash = NULL; + size_t hash_size; + const EVP_MD *md; + const char *a; - if (strv_length(l) != 1) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single PIN only."); + assert_se(a = tpm2_hash_alg_to_string(*pa)); + assert_se(md = EVP_get_digestbyname(a)); - pin = TAKE_PTR(l[0]); - l = mfree(l); + r = pe_hash(fd < 0 ? STDIN_FILENO : fd, md, &hash, &hash_size); + if (r < 0) + return log_error_errno(r, "Failed to hash PE binary: %m"); + + r = sd_json_variant_append_arraybo( + &digests, + SD_JSON_BUILD_PAIR_STRING("hashAlg", a), + SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(hash, hash_size))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON digest object: %m"); } - } else if (!have_old_policy) { - r = make_recovery_key(&pin); + r = sd_json_variant_append_arraybo( + &array, + SD_JSON_BUILD_PAIR_UNSIGNED("pcr", i), + SD_JSON_BUILD_PAIR_VARIANT("digests", digests)); if (r < 0) - return log_error_errno(r, "Failed to generate a randomized recovery PIN: %m"); + return log_error_errno(r, "Failed to append record object: %m"); + } - if (recovery_pin_mode == RECOVERY_PIN_SHOW) - printf("%s Selected recovery PIN is: %s%s%s\n", - glyph(GLYPH_LOCK_AND_KEY), - ansi_highlight_cyan(), - pin, - ansi_normal()); + return write_pcrlock(array, NULL); +} + +static int verb_unlock_simple(int argc, char *argv[], uintptr_t _data, void *userdata) { + return unlink_pcrlock(NULL); +} + +typedef void* SectionHashArray[_UNIFIED_SECTION_MAX * TPM2_N_HASH_ALGORITHMS]; + +static void section_hashes_array_done(SectionHashArray *array) { + assert(array); + + for (size_t i = 0; i < _UNIFIED_SECTION_MAX * TPM2_N_HASH_ALGORITHMS; i++) + free((*array)[i]); +} + +static int verb_lock_uki(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL, *pe_digests = NULL; + _cleanup_(section_hashes_array_done) SectionHashArray section_hashes = {}; + size_t hash_sizes[TPM2_N_HASH_ALGORITHMS]; + _cleanup_close_ int fd = -EBADF; + int r; + + if (arg_pcr_mask != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PCR not configurable for UKI lock down."); + + if (argc >= 2) { + fd = open(argv[1], O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); } - _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; - TPM2_HANDLE nv_index = 0; + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + _cleanup_free_ void *peh = NULL; + const EVP_MD *md; + const char *a; - r = tpm2_deserialize(tc, &nv_blob, &nv_handle); - if (r < 0) - return log_error_errno(r, "Failed to deserialize NV index TR: %m"); - if (r > 0) - nv_index = old_policy.nv_index; + assert_se(a = tpm2_hash_alg_to_string(tpm2_hash_algorithms[i])); + assert_se(md = EVP_get_digestbyname(a)); - TPM2B_AUTH auth = {}; - CLEANUP_ERASE(auth); + r = pe_hash(fd < 0 ? STDIN_FILENO : fd, md, &peh, hash_sizes + i); + if (r < 0) + return log_error_errno(r, "Failed to hash PE binary: %m"); - if (pin) { - r = tpm2_auth_value_from_pin(TPM2_ALG_SHA256, pin, &auth); + r = sd_json_variant_append_arraybo( + &pe_digests, + SD_JSON_BUILD_PAIR_STRING("hashAlg", a), + SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(peh, hash_sizes[i]))); if (r < 0) - return log_error_errno(r, "Failed to hash PIN: %m"); - } else { - assert(iovec_is_set(&pin_public)); - assert(iovec_is_set(&pin_private)); + return log_error_errno(r, "Failed to build JSON digest object: %m"); - log_debug("Retrieving PIN from sealed data."); + r = uki_hash(fd < 0 ? STDIN_FILENO : fd, md, section_hashes + (i * _UNIFIED_SECTION_MAX), hash_sizes + i); + if (r < 0) + return log_error_errno(r, "Failed to UKI hash PE binary: %m"); + } - usec_t pin_start_usec = now(CLOCK_MONOTONIC); + r = sd_json_variant_append_arraybo( + &array, + SD_JSON_BUILD_PAIR_UNSIGNED("pcr", TPM2_PCR_BOOT_LOADER_CODE), + SD_JSON_BUILD_PAIR_VARIANT("digests", pe_digests)); + if (r < 0) + return log_error_errno(r, "Failed to append record object: %m"); - _cleanup_(iovec_done_erase) struct iovec secret = {}; - for (unsigned attempt = 0;; attempt++) { - _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL; + for (UnifiedSection section = 0; section < _UNIFIED_SECTION_MAX; section++) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *section_digests = NULL, *record = NULL; - r = tpm2_make_policy_session( - tc, - srk_handle, - encryption_session, - &policy_session); - if (r < 0) - return log_error_errno(r, "Failed to allocate policy session: %m"); + if (!unified_section_measure(section)) + continue; - r = tpm2_policy_super_pcr( - tc, - policy_session, - &old_policy.prediction, - old_policy.algorithm); - if (r < 0) - return r; + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + const char *a; + void *hash; - r = tpm2_policy_authorize_nv( - tc, - policy_session, - nv_handle, - NULL); - if (r < 0) - return log_error_errno(r, "Failed to submit AuthorizeNV policy: %m"); + hash = section_hashes[i * _UNIFIED_SECTION_MAX + section]; + if (!hash) + continue; - r = tpm2_unseal_data( - tc, - &pin_public, - &pin_private, - srk_handle, - policy_session, - encryption_session, - &secret); - if (r < 0 && (r != -ESTALE || attempt >= 16)) - return log_error_errno(r, "Failed to unseal PIN: %m"); - if (r == 0) - break; + assert_se(a = tpm2_hash_alg_to_string(tpm2_hash_algorithms[i])); - log_debug("Trying again (attempt %u), as PCR values changed during unlock attempt.", attempt+1); + r = sd_json_variant_append_arraybo( + §ion_digests, + SD_JSON_BUILD_PAIR_STRING("hashAlg", a), + SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(hash, hash_sizes[i]))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON digest object: %m"); } - if (secret.iov_len > sizeof_field(TPM2B_AUTH, buffer)) - return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Decrypted PIN too large."); + if (!section_digests) + continue; - auth = (TPM2B_AUTH) { - .size = secret.iov_len, - }; + /* So we have digests for this section, hence generate a record for the section name first. */ + r = make_pcrlock_record(TPM2_PCR_KERNEL_BOOT /* =11 */, unified_sections[section], strlen(unified_sections[section]) + 1, &record); + if (r < 0) + return r; - memcpy_safe(auth.buffer, secret.iov_base, secret.iov_len); + r = sd_json_variant_append_array(&array, record); + if (r < 0) + return log_error_errno(r, "Failed to append JSON record array: %m"); - log_info("Retrieved PIN from TPM2 in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pin_start_usec), 1)); + /* And then append a record for the section contents digests as well */ + r = sd_json_variant_append_arraybo( + &array, + SD_JSON_BUILD_PAIR_UNSIGNED("pcr", TPM2_PCR_KERNEL_BOOT), + SD_JSON_BUILD_PAIR_VARIANT("digests", section_digests)); + if (r < 0) + return log_error_errno(r, "Failed to append record object: %m"); } - /* Now convert the PIN into an HMAC-SHA256 key that we can use in PolicySigned to protect access to the nvindex with */ - _cleanup_(tpm2_handle_freep) Tpm2Handle *pin_handle = NULL; - r = tpm2_hmac_key_from_pin(tc, encryption_session, &auth, &pin_handle); + return write_pcrlock(array, NULL); +} + +static int verb_lock_machine_id(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL, *array = NULL; + _cleanup_free_ char *word = NULL; + int r; + + r = pcrextend_machine_id_word(&word); if (r < 0) return r; - TPM2B_NV_PUBLIC nv_public = {}; - usec_t nv_index_start_usec = now(CLOCK_MONOTONIC); + r = make_pcrlock_record(TPM2_PCR_SYSTEM_IDENTITY /* = 15 */, word, SIZE_MAX, &record); + if (r < 0) + return r; - if (!iovec_is_set(&nv_blob)) { - _cleanup_(Esys_Freep) TPM2B_NAME *pin_name = NULL; - r = tpm2_get_name( - tc, - pin_handle, - &pin_name); - if (r < 0) - return log_error_errno(r, "Failed to get name of PIN from TPM2: %m"); + r = sd_json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to create record array: %m"); - TPM2B_DIGEST recovery_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); - r = tpm2_calculate_policy_signed(&recovery_policy_digest, pin_name); - if (r < 0) - return log_error_errno(r, "Failed to calculate PolicySigned policy: %m"); + return write_pcrlock(array, PCRLOCK_MACHINE_ID_PATH); +} - log_debug("Allocating NV index to write PCR policy to..."); - r = tpm2_define_policy_nv_index( - tc, - encryption_session, - arg_nv_index, - &recovery_policy_digest, - &nv_index, - &nv_handle, - &nv_public); - if (r == -EEXIST) - return log_error_errno(r, "NV index 0x%" PRIx32 " already allocated.", arg_nv_index); - if (r < 0) - return log_error_errno(r, "Failed to allocate NV index: %m"); - } +static int verb_unlock_machine_id(int argc, char *argv[], uintptr_t _data, void *userdata) { + return unlink_pcrlock(PCRLOCK_MACHINE_ID_PATH); +} - _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL; - r = tpm2_make_policy_session( - tc, - srk_handle, - encryption_session, - &policy_session); - if (r < 0) - return log_error_errno(r, "Failed to allocate policy session: %m"); +static int pcrlock_file_system_path(const char *normalized_path, char **ret) { + _cleanup_free_ char *s = NULL; - r = tpm2_policy_signed_hmac_sha256( - tc, - policy_session, - pin_handle, - &IOVEC_MAKE(auth.buffer, auth.size), - /* ret_policy_digest= */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to submit authentication value policy: %m"); + assert(normalized_path); + assert(ret); - log_debug("Calculating new PCR policy to write..."); - TPM2B_DIGEST new_super_pcr_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + if (path_equal(normalized_path, "/")) + s = strdup(PCRLOCK_ROOT_FILE_SYSTEM_PATH); + else { + /* We reuse the escaping we use for turning paths into unit names */ + _cleanup_free_ char *escaped = NULL; - usec_t pcr_policy_start_usec = now(CLOCK_MONOTONIC); + assert(normalized_path[0] == '/'); + assert(normalized_path[1] != '/'); - r = tpm2_calculate_policy_super_pcr( - &new_prediction, - el->primary_algorithm, - &new_super_pcr_policy_digest); - if (r < 0) - return log_error_errno(r, "Failed to calculate super PCR policy: %m"); + escaped = unit_name_escape(normalized_path + 1); + if (!escaped) + return log_oom(); - log_info("Calculated new PCR policy in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pcr_policy_start_usec), 1)); + s = strjoin(PCRLOCK_FILE_SYSTEM_PATH_PREFIX, escaped, ".pcrlock"); + } + if (!s) + return log_oom(); - log_debug("Writing new PCR policy to NV index..."); - r = tpm2_write_policy_nv_index( - tc, - policy_session, - nv_index, - nv_handle, - &new_super_pcr_policy_digest); - if (r < 0) - return log_error_errno(r, "Failed to write to NV index: %m"); + *ret = TAKE_PTR(s); + return 0; +} - log_info("Updated NV index in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), nv_index_start_usec), 1)); +static int verb_lock_file_system(int argc, char *argv[], uintptr_t _data, void *userdata) { + const char* paths[3] = {}; + int r; - assert(iovec_is_set(&pin_public) == iovec_is_set(&pin_private)); - if (!iovec_is_set(&pin_public)) { - TPM2B_DIGEST authnv_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + if (argc > 1) + paths[0] = argv[1]; + else { + dev_t a, b; + paths[0] = "/"; - r = tpm2_calculate_policy_authorize_nv(&nv_public, &authnv_policy_digest); + r = get_block_device("/", &a); if (r < 0) - return log_error_errno(r, "Failed to calculate AuthorizeNV policy: %m"); - - struct iovec data = { - .iov_base = auth.buffer, - .iov_len = auth.size, - }; - - usec_t pin_seal_start_usec = now(CLOCK_MONOTONIC); + return log_error_errno(r, "Failed to get device of root file system: %m"); - log_debug("Sealing PIN to NV index policy..."); - r = tpm2_seal_data( - tc, - &data, - srk_handle, - encryption_session, - &authnv_policy_digest, - &pin_public, - &pin_private); + r = get_block_device("/var", &b); if (r < 0) - return log_error_errno(r, "Failed to seal PIN to NV auth policy: %m"); + return log_error_errno(r, "Failed to get device of /var/ file system: %m"); + + /* if backing device is distinct, then measure /var/ too */ + if (a != b) + paths[1] = "/var"; - log_info("Sealed PIN in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pin_seal_start_usec), 1)); + enable_json_sse(); } - if (!iovec_is_set(&nv_blob)) { - r = tpm2_serialize(tc, nv_handle, &nv_blob); - if (r < 0) - return log_error_errno(r, "Failed to serialize NV index TR: %m"); - } + STRV_FOREACH(p, paths) { + _cleanup_free_ char *word = NULL, *normalized_path = NULL, *pcrlock_file = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL, *array = NULL; - if (!iovec_is_set(&srk_blob)) { - r = tpm2_serialize(tc, srk_handle, &srk_blob); + r = pcrextend_file_system_word(*p, &word, &normalized_path); if (r < 0) - return log_error_errno(r, "Failed to serialize SRK index TR: %m"); - } + return r; - if (!iovec_is_set(&nv_public_blob)) { - r = tpm2_marshal_nv_public(&nv_public, &nv_public_blob.iov_base, &nv_public_blob.iov_len); + r = pcrlock_file_system_path(normalized_path, &pcrlock_file); if (r < 0) - return log_error_errno(r, "Failed to marshal NV public area: %m"); - } - - _cleanup_(sd_json_variant_unrefp) sd_json_variant *new_configuration_json = NULL; - r = sd_json_buildo( - &new_configuration_json, - SD_JSON_BUILD_PAIR_STRING("pcrBank", tpm2_hash_alg_to_string(el->primary_algorithm)), - SD_JSON_BUILD_PAIR_VARIANT("pcrValues", new_prediction_json), - SD_JSON_BUILD_PAIR_INTEGER("nvIndex", nv_index), - JSON_BUILD_PAIR_IOVEC_BASE64("nvHandle", &nv_blob), - JSON_BUILD_PAIR_IOVEC_BASE64("nvPublic", &nv_public_blob), - JSON_BUILD_PAIR_IOVEC_BASE64("srkHandle", &srk_blob), - JSON_BUILD_PAIR_IOVEC_BASE64("pinPublic", &pin_public), - JSON_BUILD_PAIR_IOVEC_BASE64("pinPrivate", &pin_private)); - if (r < 0) - return log_error_errno(r, "Failed to generate JSON: %m"); + return r; - _cleanup_free_ char *text = NULL; - r = sd_json_variant_format(new_configuration_json, 0, &text); - if (r < 0) - return log_error_errno(r, "Failed to format new configuration to JSON: %m"); + r = make_pcrlock_record(TPM2_PCR_SYSTEM_IDENTITY /* = 15 */, word, SIZE_MAX, &record); + if (r < 0) + return r; - const char *path = arg_policy_path ?: (in_initrd() ? "/run/systemd/pcrlock.json" : "/var/lib/systemd/pcrlock.json"); - r = write_string_file(path, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_SYNC|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_LABEL); - if (r < 0) - return log_error_errno(r, "Failed to write new configuration to '%s': %m", path); + r = sd_json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to create record array: %m"); - if (!arg_policy_path && !in_initrd()) { - r = remove_policy_file("/run/systemd/pcrlock.json"); + r = write_pcrlock(array, pcrlock_file); if (r < 0) return r; } - log_info("Written new policy to '%s' and digest to TPM2 NV index 0x%x.", path, nv_index); + return 0; +} - (void) write_boot_policy_file(text); +static int verb_unlock_file_system(int argc, char *argv[], uintptr_t _data, void *userdata) { + const char* paths[3] = {}; + int r; - log_info("Overall time spent: %s", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), start_usec), 1)); + if (argc > 1) + paths[0] = argv[1]; + else { + paths[0] = "/"; + paths[1] = "/var"; + } - return 1; /* installed new policy */ -} + STRV_FOREACH(p, paths) { + _cleanup_free_ char *normalized_path = NULL, *pcrlock_file = NULL; -static int verb_make_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; + r = chase(*p, NULL, 0, &normalized_path, NULL); + if (r < 0) + return log_error_errno(r, "Failed to normal path '%s': %m", argv[1]); - r = make_policy(arg_force, arg_recovery_pin); - if (r < 0) - return r; + r = pcrlock_file_system_path(normalized_path, &pcrlock_file); + if (r < 0) + return r; + + r = unlink_pcrlock(pcrlock_file); + if (r < 0) + return r; + } return 0; } -static int undefine_policy_nv_index( - uint32_t nv_index, - const struct iovec *nv_blob, - const struct iovec *srk_blob) { +static int verb_lock_kernel_cmdline(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *record = NULL, *array = NULL; + _cleanup_free_ char *cmdline = NULL; int r; - assert(nv_blob); - assert(srk_blob); - - _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; - r = tpm2_context_new_or_warn(/* device= */ NULL, &tc); + if (argc > 1) { + if (empty_or_dash(argv[1])) + r = read_full_stream(stdin, &cmdline, NULL); + else + r = read_full_file(argv[1], &cmdline, NULL); + } else + r = proc_cmdline(&cmdline); if (r < 0) - return r; + return log_error_errno(r, "Failed to read cmdline: %m"); - _cleanup_(tpm2_handle_freep) Tpm2Handle *srk_handle = NULL; - r = tpm2_deserialize( - tc, - srk_blob, - &srk_handle); - if (r < 0) - return log_error_errno(r, "Failed to deserialize SRK TR: %m"); + delete_trailing_chars(cmdline, "\n"); - _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; - r = tpm2_deserialize( - tc, - nv_blob, - &nv_handle); - if (r < 0) - return log_error_errno(r, "Failed to deserialize NV TR: %m"); + _cleanup_free_ char16_t *u = NULL; + u = utf8_to_utf16(cmdline, SIZE_MAX); + if (!u) + return log_oom(); - _cleanup_(tpm2_handle_freep) Tpm2Handle *encryption_session = NULL; - r = tpm2_make_encryption_session( - tc, - srk_handle, - /* bind_key= */ &TPM2_HANDLE_NONE, - &encryption_session); + r = make_pcrlock_record(TPM2_PCR_KERNEL_INITRD /* = 9 */, u, char16_strlen(u)*2+2, &record); if (r < 0) return r; - r = tpm2_undefine_nv_index( - tc, - encryption_session, - nv_index, - nv_handle); + r = sd_json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to create record array: %m"); + + r = write_pcrlock(array, PCRLOCK_KERNEL_CMDLINE_PATH); if (r < 0) return r; - log_info("Removed NV index 0x%x", nv_index); return 0; } -static int remove_policy(void) { - int ret = 0, r; - - _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy policy = {}; - r = tpm2_pcrlock_policy_load(arg_policy_path, &policy); - if (r == 0) { - log_info("No policy found."); - return 0; - } +static int verb_unlock_kernel_cmdline(int argc, char *argv[], uintptr_t _data, void *userdata) { + return unlink_pcrlock(PCRLOCK_KERNEL_CMDLINE_PATH); +} - if (r < 0) - log_notice("Failed to load old policy file, assuming it is corrupted, removing."); - else { - r = undefine_policy_nv_index(policy.nv_index, &policy.nv_handle, &policy.srk_handle); - if (r < 0) - log_notice("Failed to remove NV index, assuming data out of date, removing policy file."); +static int verb_lock_kernel_initrd(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *records = NULL; + _cleanup_fclose_ FILE *f = NULL; + uint32_t pcr_mask = UINT32_C(1) << TPM2_PCR_KERNEL_INITRD; + int r; - RET_GATHER(ret, r); + if (argc >= 2) { + f = fopen(argv[1], "re"); + if (!f) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); } - if (arg_policy_path) - RET_GATHER(ret, remove_policy_file(arg_policy_path)); - else { - RET_GATHER(ret, remove_policy_file("/var/lib/systemd/pcrlock.json")); - RET_GATHER(ret, remove_policy_file("/run/systemd/pcrlock.json")); - } + r = make_pcrlock_record_from_stream(pcr_mask, f ?: stdin, &records); + if (r < 0) + return r; - _cleanup_free_ char *boot_policy_file = NULL; - r = determine_boot_policy_file(&boot_policy_file, /* ret_credential_name= */ NULL); - if (r == 0) - log_info("Did not find XBOOTLDR/ESP partition, not removing boot policy file."); - else if (r > 0) { - RET_GATHER(ret, remove_policy_file(boot_policy_file)); - } else - RET_GATHER(ret, r); + r = write_pcrlock(records, PCRLOCK_KERNEL_INITRD_PATH); + if (r < 0) + return r; - return ret; + return 0; } -static int verb_remove_policy(int argc, char *argv[], uintptr_t _data, void *userdata) { - return remove_policy(); +static int verb_unlock_kernel_initrd(int argc, char *argv[], uintptr_t _data, void *userdata) { + return unlink_pcrlock(PCRLOCK_KERNEL_INITRD_PATH); } -static int test_tpm2_support_pcrlock(Tpm2Support *ret) { +static int verb_lock_raw(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *records = NULL; + _cleanup_fclose_ FILE *f = NULL; int r; - assert(ret); - - /* First check basic support */ - Tpm2Support s = tpm2_support(); - - /* If basic support is available, let's also check the things we need for systemd-pcrlock */ - if (s == TPM2_SUPPORT_FULL) { - _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; - r = tpm2_context_new_or_warn(/* device= */ NULL, &tc); - if (r < 0) - return r; - - /* We strictly need TPM2_CC_PolicyAuthorizeNV for systemd-pcrlock to work */ - SET_FLAG(s, TPM2_SUPPORT_AUTHORIZE_NV, tpm2_supports_command(tc, TPM2_CC_PolicyAuthorizeNV)); - - log_debug("PolicyAuthorizeNV supported: %s", yes_no(FLAGS_SET(s, TPM2_SUPPORT_AUTHORIZE_NV))); - - /* We also strictly need SHA-256 to work */ - SET_FLAG(s, TPM2_SUPPORT_SHA256, tpm2_supports_alg(tc, TPM2_ALG_SHA256)); + if (arg_pcr_mask == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No PCR specified, refusing."); - log_debug("SHA-256 supported: %s", yes_no(FLAGS_SET(s, TPM2_SUPPORT_SHA256))); + if (argc >= 2) { + f = fopen(argv[1], "re"); + if (!f) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); } - *ret = s; - return 0; -} - -static int verb_is_supported(int argc, char *argv[], uintptr_t _data, void *userdata) { - int r; - - Tpm2Support s; - r = test_tpm2_support_pcrlock(&s); + r = make_pcrlock_record_from_stream(arg_pcr_mask, f ?: stdin, &records); if (r < 0) return r; - if (!arg_quiet) { - if (s == (TPM2_SUPPORT_FULL|TPM2_SUPPORT_API_PCRLOCK)) - printf("%syes%s\n", ansi_green(), ansi_normal()); - else if (FLAGS_SET(s, TPM2_SUPPORT_FULL)) - printf("%sobsolete%s\n", ansi_red(), ansi_normal()); - else if (s == TPM2_SUPPORT_NONE) - printf("%sno%s\n", ansi_red(), ansi_normal()); - else - printf("%spartial%s\n", ansi_yellow(), ansi_normal()); - } - - assert_cc((TPM2_SUPPORT_API|TPM2_SUPPORT_API_PCRLOCK) <= 255); /* make sure this is safe to use as process exit status */ - - return ~s & (TPM2_SUPPORT_API|TPM2_SUPPORT_API_PCRLOCK); + return write_pcrlock(records, NULL); } static int help(void) {