From d2fafc423d87680437284c933d15d9edefd207a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 28 Nov 2020 22:59:21 +0100 Subject: [PATCH] cryptenroll: support listing and wiping tokens --- meson.build | 5 + src/cryptenroll/cryptenroll-list.c | 126 ++++++++ src/cryptenroll/cryptenroll-list.h | 6 + src/cryptenroll/cryptenroll-wipe.c | 445 +++++++++++++++++++++++++++++ src/cryptenroll/cryptenroll-wipe.h | 12 + src/cryptenroll/cryptenroll.c | 142 +++++++-- src/cryptenroll/cryptenroll.h | 26 ++ 7 files changed, 740 insertions(+), 22 deletions(-) create mode 100644 src/cryptenroll/cryptenroll-list.c create mode 100644 src/cryptenroll/cryptenroll-list.h create mode 100644 src/cryptenroll/cryptenroll-wipe.c create mode 100644 src/cryptenroll/cryptenroll-wipe.h create mode 100644 src/cryptenroll/cryptenroll.h diff --git a/meson.build b/meson.build index 70696a724b..21ab85518f 100644 --- a/meson.build +++ b/meson.build @@ -2434,13 +2434,18 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptenroll_sources = files(''' src/cryptenroll/cryptenroll-fido2.h + src/cryptenroll/cryptenroll-list.c + src/cryptenroll/cryptenroll-list.h src/cryptenroll/cryptenroll-password.c src/cryptenroll/cryptenroll-password.h src/cryptenroll/cryptenroll-pkcs11.h src/cryptenroll/cryptenroll-recovery.c src/cryptenroll/cryptenroll-recovery.h src/cryptenroll/cryptenroll-tpm2.h + src/cryptenroll/cryptenroll-wipe.c + src/cryptenroll/cryptenroll-wipe.h src/cryptenroll/cryptenroll.c + src/cryptenroll/cryptenroll.h '''.split()) if conf.get('HAVE_P11KIT') == 1 and conf.get('HAVE_OPENSSL') == 1 diff --git a/src/cryptenroll/cryptenroll-list.c b/src/cryptenroll/cryptenroll-list.c new file mode 100644 index 0000000000..3171973395 --- /dev/null +++ b/src/cryptenroll/cryptenroll-list.c @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cryptenroll-list.h" +#include "cryptenroll.h" +#include "format-table.h" +#include "parse-util.h" + +int list_enrolled(struct crypt_device *cd) { + + struct keyslot_metadata { + int slot; + const char *type; + } *keyslot_metadata = NULL; + size_t n_keyslot_metadata = 0, n_keyslot_metadata_allocated = 0; + _cleanup_(table_unrefp) Table *t = NULL; + int slot_max, r; + TableCell *cell; + + assert(cd); + + /* First step, find out all currently used slots */ + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + if (!GREEDY_REALLOC(keyslot_metadata, n_keyslot_metadata_allocated, n_keyslot_metadata+1)) + return log_oom(); + + keyslot_metadata[n_keyslot_metadata++] = (struct keyslot_metadata) { + .slot = slot, + }; + } + + /* Second step, enumerate through all tokens, and update the slot table, indicating what kind of + * token they are assigned to */ + for (int token = 0; token < LUKS2_TOKENS_MAX; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + const char *type; + JsonVariant *w, *z; + EnrollType et; + + r = cryptsetup_get_token_as_json(cd, token, NULL, &v); + if (IN_SET(r, -ENOENT, -EINVAL)) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m"); + continue; + } + + w = json_variant_by_key(v, "type"); + if (!w || !json_variant_is_string(w)) { + log_warning("Token JSON data lacks type field, ignoring."); + continue; + } + + et = luks2_token_type_from_string(json_variant_string(w)); + if (et < 0) + type = "other"; + else + type = enroll_type_to_string(et); + + w = json_variant_by_key(v, "keyslots"); + if (!w || !json_variant_is_array(w)) { + log_warning("Token JSON data lacks keyslots field, ignoring."); + continue; + } + + JSON_VARIANT_ARRAY_FOREACH(z, w) { + unsigned u; + + if (!json_variant_is_string(z)) { + log_warning("Token JSON data's keyslot field is not an array of strings, ignoring."); + continue; + } + + r = safe_atou(json_variant_string(z), &u); + if (r < 0) { + log_warning_errno(r, "Token JSON data's keyslot filed is not an integer formatted as string, ignoring."); + continue; + } + + for (size_t i = 0; i < n_keyslot_metadata; i++) { + if ((unsigned) keyslot_metadata[i].slot != u) + continue; + + if (keyslot_metadata[i].type) /* Slot claimed multiple times? */ + keyslot_metadata[i].type = POINTER_MAX; + else + keyslot_metadata[i].type = type; + } + } + } + + /* Finally, create a table out of it all */ + t = table_new("slot", "type"); + if (!t) + return log_oom(); + + assert_se(cell = table_get_cell(t, 0, 0)); + (void) table_set_align_percent(t, cell, 100); + + for (size_t i = 0; i < n_keyslot_metadata; i++) { + r = table_add_many( + t, + TABLE_INT, keyslot_metadata[i].slot, + TABLE_STRING, keyslot_metadata[i].type == POINTER_MAX ? "conflict" : + keyslot_metadata[i].type ?: "password"); + if (r < 0) + return table_log_add_error(r); + } + + if (table_get_rows(t) <= 1) { + log_info("No slots found."); + return 0; + } + + r = table_print(t, stdout); + if (r < 0) + return log_error_errno(r, "Failed to show slot table: %m"); + + return 0; +} diff --git a/src/cryptenroll/cryptenroll-list.h b/src/cryptenroll/cryptenroll-list.h new file mode 100644 index 0000000000..d322988f79 --- /dev/null +++ b/src/cryptenroll/cryptenroll-list.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptsetup-util.h" + +int list_enrolled(struct crypt_device *cd); diff --git a/src/cryptenroll/cryptenroll-wipe.c b/src/cryptenroll/cryptenroll-wipe.c new file mode 100644 index 0000000000..3c4a4e6acb --- /dev/null +++ b/src/cryptenroll/cryptenroll-wipe.c @@ -0,0 +1,445 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cryptenroll-wipe.h" +#include "cryptenroll.h" +#include "json.h" +#include "memory-util.h" +#include "parse-util.h" +#include "set.h" +#include "sort-util.h" + +static int find_all_slots(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) { + int slot_max; + + assert(cd); + assert(wipe_slots); + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + /* Finds all currently assigned slots, and adds them to 'wipe_slots', except if listed already in 'keep_slots' */ + + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + /* No need to check this slot if we already know we want to wipe it or definitely keep it. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + set_contains(wipe_slots, INT_TO_PTR(slot))) + continue; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + + return 0; +} + +static int find_empty_passphrase_slots(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) { + size_t vks; + int r, slot_max; + + assert(cd); + assert(wipe_slots); + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + /* Finds all slots with an empty passphrase assigned (i.e. "") and adds them to 'wipe_slots', except + * if listed already in 'keep_slots' */ + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + vks = (size_t) r; + + for (int slot = 0; slot < slot_max; slot++) { + _cleanup_(erase_and_freep) char *vk = NULL; + crypt_keyslot_info status; + + /* No need to check this slot if we already know we want to wipe it or definitely keep it. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + set_contains(wipe_slots, INT_TO_PTR(slot))) + continue; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + vk = malloc(vks); + if (!vk) + return log_oom(); + + r = crypt_volume_key_get(cd, slot, vk, &vks, "", 0); + if (r < 0) { + log_debug_errno(r, "Failed to acquire volume key from slot %i with empty password, ignoring: %m", slot); + continue; + } + + if (set_put(wipe_slots, INT_TO_PTR(r)) < 0) + return log_oom(); + } + + return 0; +} + +static int find_slots_by_mask( + struct crypt_device *cd, + Set *wipe_slots, + Set *keep_slots, + unsigned by_mask) { + + _cleanup_(set_freep) Set *listed_slots = NULL; + int r; + + assert(cd); + assert(wipe_slots); + + if (by_mask == 0) + return 0; + + /* Find all slots that are associated with a token of a type in the specified token type mask */ + + for (int token = 0; token < LUKS2_TOKENS_MAX; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + JsonVariant *w, *z; + EnrollType t; + + r = cryptsetup_get_token_as_json(cd, token, NULL, &v); + if (IN_SET(r, -ENOENT, -EINVAL)) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m"); + continue; + } + + w = json_variant_by_key(v, "type"); + if (!w || !json_variant_is_string(w)) { + log_warning("Token JSON data lacks type field, ignoring."); + continue; + } + + t = luks2_token_type_from_string(json_variant_string(w)); + + w = json_variant_by_key(v, "keyslots"); + if (!w || !json_variant_is_array(w)) { + log_warning("Token JSON data lacks keyslots field, ignoring."); + continue; + } + + JSON_VARIANT_ARRAY_FOREACH(z, w) { + int slot; + + if (!json_variant_is_string(z)) { + log_warning("Token JSON data's keyslot field is not an array of strings, ignoring."); + continue; + } + + r = safe_atoi(json_variant_string(z), &slot); + if (r < 0) { + log_warning_errno(r, "Token JSON data's keyslot filed is not an integer formatted as string, ignoring."); + continue; + } + + if (t >= 0 && (by_mask & (1U << t)) != 0) { + /* Selected by token type */ + if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } else if ((by_mask & (1U << ENROLL_PASSWORD)) != 0) { + /* If we shall remove all plain password slots, let's maintain a list of + * slots that are listed in any tokens, since those are *NOT* plain + * passwords */ + if (set_ensure_allocated(&listed_slots, NULL) < 0) + return log_oom(); + + if (set_put(listed_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + } + } + + /* "password" slots are those which have no token assigned. If we shall remove those, iterate through + * all slots and mark those for wiping that weren't listed in any token */ + if ((by_mask & (1U << ENROLL_PASSWORD)) != 0) { + int slot_max; + + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + /* No need to check this slot if we already know we want to wipe it or definitely keep it. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + set_contains(wipe_slots, INT_TO_PTR(slot))) + continue; + + if (set_contains(listed_slots, INT_TO_PTR(slot))) /* This has a token, hence is not a password. */ + continue; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) /* Not actually assigned? */ + continue; + + /* Finally, we found a password, add it to the list of slots to wipe */ + if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + } + + return 0; +} + +static int find_slot_tokens(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots, Set *wipe_tokens) { + int r; + + assert(cd); + assert(wipe_slots); + assert(keep_slots); + assert(wipe_tokens); + + /* Find all tokens matching the slots we want to wipe, so that we can wipe them too. Also, for update + * the slots sets according to the token data: add any other slots listed in the tokens we act on. */ + + for (int token = 0; token < LUKS2_TOKENS_MAX; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + bool shall_wipe = false; + JsonVariant *w, *z; + + r = cryptsetup_get_token_as_json(cd, token, NULL, &v); + if (IN_SET(r, -ENOENT, -EINVAL)) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m"); + continue; + } + + w = json_variant_by_key(v, "keyslots"); + if (!w || !json_variant_is_array(w)) { + log_warning("Token JSON data lacks keyslots field, ignoring."); + continue; + } + + /* Go through the slots associated with this token: if we shall keep any slot of them, the token shall stay too. */ + JSON_VARIANT_ARRAY_FOREACH(z, w) { + int slot; + + if (!json_variant_is_string(z)) { + log_warning("Token JSON data's keyslot field is not an array of strings, ignoring."); + continue; + } + + r = safe_atoi(json_variant_string(z), &slot); + if (r < 0) { + log_warning_errno(r, "Token JSON data's keyslot filed is not an integer formatted as string, ignoring."); + continue; + } + + if (set_contains(keep_slots, INT_TO_PTR(slot))) { + shall_wipe = false; + break; /* If we shall keep this slot, then this is definite: we will keep its token too */ + } + + /* If there's a slot associated with this token that we shall wipe, then remove the + * token too. But we are careful here: let's continue iterating, maybe there's a slot + * that we need to keep, in which case we can reverse the decision again. */ + if (set_contains(wipe_slots, INT_TO_PTR(slot))) + shall_wipe = true; + } + + /* Go through the slots again, and this time add them to the list of slots to keep/remove */ + JSON_VARIANT_ARRAY_FOREACH(z, w) { + int slot; + + if (!json_variant_is_string(z)) + continue; + if (safe_atoi(json_variant_string(z), &slot) < 0) + continue; + + if (set_put(shall_wipe ? wipe_slots : keep_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + + /* And of course, als remember the tokens to remove. */ + if (shall_wipe) + if (set_put(wipe_tokens, INT_TO_PTR(token)) < 0) + return log_oom(); + } + + return 0; +} + +static bool slots_remain(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) { + int slot_max; + + assert(cd); + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + /* Checks if any slots remaining in the LUKS2 header if we remove all slots listed in 'wipe_slots' + * (keeping those listed in 'keep_slots') */ + + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + /* The "keep" set wins if a slot is listed in both sets. This is important so that we can + * safely add a new slot and remove all others of the same type, which in a naive + * implementation might mean we remove what we just added — which we of course don't want. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + !set_contains(wipe_slots, INT_TO_PTR(slot))) + return true; + } + + return false; +} + +int wipe_slots(struct crypt_device *cd, + const int explicit_slots[], + size_t n_explicit_slots, + WipeScope by_scope, + unsigned by_mask, + int except_slot) { + + _cleanup_(set_freep) Set *wipe_slots = NULL, *wipe_tokens = NULL, *keep_slots = NULL; + _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL; + size_t n_ordered_slots = 0, n_ordered_tokens = 0; + int r, slot_max, ret; + void *e; + + assert_se(cd); + + /* Shortcut if nothing to wipe. */ + if (n_explicit_slots == 0 && by_mask == 0 && by_scope == WIPE_EXPLICIT) + return 0; + + /* So this is a bit more complicated than I'd wish, but we want support three different axis for wiping slots: + * + * 1. Wiping by slot indexes + * 2. Wiping slots of specified token types + * 3. Wiping "all" entries, or entries with an empty password (i.e. "") + * + * (or any combination of the above) + * + * Plus: We always want to remove tokens matching the slots. + * Plus: We always want to exclude the slots/tokens we just added. + */ + + wipe_slots = set_new(NULL); + keep_slots = set_new(NULL); + wipe_tokens = set_new(NULL); + if (!wipe_slots || !keep_slots || !wipe_tokens) + return log_oom(); + + /* Let's maintain one set of slots for the slots we definitely want to keep */ + if (except_slot >= 0) + if (set_put(keep_slots, INT_TO_PTR(except_slot)) < 0) + return log_oom(); + + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + /* Maintain another set of the slots we intend to wipe */ + for (size_t i = 0; i < n_explicit_slots; i++) { + if (explicit_slots[i] >= slot_max) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Slot index %i out of range.", explicit_slots[i]); + + if (set_put(wipe_slots, INT_TO_PTR(explicit_slots[i])) < 0) + return log_oom(); + } + + /* Now, handle the "all" and "empty passphrase" cases. */ + switch (by_scope) { + + case WIPE_EXPLICIT: + break; /* Nothing to do here */ + + case WIPE_ALL: + r = find_all_slots(cd, wipe_slots, keep_slots); + if (r < 0) + return r; + + break; + + case WIPE_EMPTY_PASSPHRASE: + r = find_empty_passphrase_slots(cd, wipe_slots, keep_slots); + if (r < 0) + return r; + + break; + default: + assert_not_reached("Unexpected wipe scope"); + } + + /* Then add all slots that match a token type */ + r = find_slots_by_mask(cd, wipe_slots, keep_slots, by_mask); + if (r < 0) + return r; + + /* And determine tokens that we shall remove */ + r = find_slot_tokens(cd, wipe_slots, keep_slots, wipe_tokens); + if (r < 0) + return r; + + /* Safety check: let's make sure that after we are done there's at least one slot remaining */ + if (!slots_remain(cd, wipe_slots, keep_slots)) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Wipe operation would leave no valid slots around, can't allow that, sorry."); + + /* Generated ordered lists of the slots and the tokens to remove */ + ordered_slots = new(int, set_size(wipe_slots)); + if (!ordered_slots) + return log_oom(); + SET_FOREACH(e, wipe_slots) { + int slot = PTR_TO_INT(e); + + if (set_contains(keep_slots, INT_TO_PTR(slot))) + continue; + + ordered_slots[n_ordered_slots++] = slot; + } + typesafe_qsort(ordered_slots, n_ordered_slots, cmp_int); + + ordered_tokens = new(int, set_size(wipe_tokens)); + if (!ordered_tokens) + return log_oom(); + SET_FOREACH(e, wipe_tokens) + ordered_tokens[n_ordered_tokens++] = PTR_TO_INT(e); + typesafe_qsort(ordered_tokens, n_ordered_tokens, cmp_int); + + if (n_ordered_slots == 0 && n_ordered_tokens == 0) { + log_full(except_slot < 0 ? LOG_NOTICE : LOG_DEBUG, + "No slots to remove selected."); + return 0; + } + + if (DEBUG_LOGGING) { + for (size_t i = 0; i < n_ordered_slots; i++) + log_debug("Going to wipe slot %i.", ordered_slots[i]); + for (size_t i = 0; i < n_ordered_tokens; i++) + log_debug("Going to wipe token %i.", ordered_tokens[i]); + } + + /* Now, let's actually start wiping things. (We go from back to front, to make space at the end + * first.) */ + ret = 0; + for (size_t i = n_ordered_slots; i > 0; i--) { + r = crypt_keyslot_destroy(cd, ordered_slots[i - 1]); + if (r < 0) { + log_warning_errno(r, "Failed to wipe slot %i, continuing: %m", ordered_slots[i - 1]); + if (ret == 0) + ret = r; + } else + log_info("Wiped slot %i.", ordered_slots[i - 1]); + } + + for (size_t i = n_ordered_tokens; i > 0; i--) { + r = crypt_token_json_set(cd, ordered_tokens[i - 1], NULL); + if (r < 0) { + log_warning_errno(r, "Failed to wipe token %i, continuing: %m", ordered_tokens[i - 1]); + if (ret == 0) + ret = r; + } + } + + return ret; +} diff --git a/src/cryptenroll/cryptenroll-wipe.h b/src/cryptenroll/cryptenroll-wipe.h new file mode 100644 index 0000000000..5bcd78391a --- /dev/null +++ b/src/cryptenroll/cryptenroll-wipe.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptenroll.h" +#include "cryptsetup-util.h" + +int wipe_slots(struct crypt_device *cd, + const int explicit_slots[], + size_t n_explicit_slots, + WipeScope by_scope, + unsigned by_mask, + int except_slot); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index e3a660d021..fb41bbe4aa 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -4,10 +4,13 @@ #include "ask-password-api.h" #include "cryptenroll-fido2.h" +#include "cryptenroll-list.h" #include "cryptenroll-password.h" #include "cryptenroll-pkcs11.h" #include "cryptenroll-recovery.h" #include "cryptenroll-tpm2.h" +#include "cryptenroll-wipe.h" +#include "cryptenroll.h" #include "cryptsetup-util.h" #include "escape.h" #include "libfido2-util.h" @@ -17,32 +20,55 @@ #include "path-util.h" #include "pkcs11-util.h" #include "pretty-print.h" +#include "string-table.h" #include "strv.h" #include "terminal-util.h" #include "tpm2-util.h" -typedef enum EnrollType { - ENROLL_PASSWORD, - ENROLL_RECOVERY, - ENROLL_PKCS11, - ENROLL_FIDO2, - ENROLL_TPM2, - _ENROLL_TYPE_MAX, - _ENROLL_TYPE_INVALID = -1, -} EnrollType; - static EnrollType arg_enroll_type = _ENROLL_TYPE_INVALID; static char *arg_pkcs11_token_uri = NULL; static char *arg_fido2_device = NULL; static char *arg_tpm2_device = NULL; static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; static char *arg_node = NULL; +static int *arg_wipe_slots = NULL; +static size_t arg_n_wipe_slots = 0; +static WipeScope arg_wipe_slots_scope = WIPE_EXPLICIT; +static unsigned arg_wipe_slots_mask = 0; /* Bitmask of (1U << EnrollType), for wiping all slots of specific types */ + +assert_cc(sizeof(arg_wipe_slots_mask) * 8 >= _ENROLL_TYPE_MAX); STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, freep); STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_node, freep); +static bool wipe_requested(void) { + return arg_n_wipe_slots > 0 || + arg_wipe_slots_scope != WIPE_EXPLICIT || + arg_wipe_slots_mask != 0; +} + +static const char* const enroll_type_table[_ENROLL_TYPE_MAX] = { + [ENROLL_PASSWORD] = "password", + [ENROLL_RECOVERY] = "recovery", + [ENROLL_PKCS11] = "pkcs11", + [ENROLL_FIDO2] = "fido2", + [ENROLL_TPM2] = "tpm2", +}; + +DEFINE_STRING_TABLE_LOOKUP(enroll_type, EnrollType); + +static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = { + /* ENROLL_PASSWORD has no entry here, as slots of this type do not have a token in the LUKS2 header */ + [ENROLL_RECOVERY] = "systemd-recovery", + [ENROLL_PKCS11] = "systemd-pkcs11", + [ENROLL_FIDO2] = "systemd-fido2", + [ENROLL_TPM2] = "systemd-tpm2", +}; + +DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); + static int help(void) { _cleanup_free_ char *link = NULL; int r; @@ -65,6 +91,8 @@ static int help(void) { " Enroll a TPM2 device\n" " --tpm2-pcrs=PCR1,PCR2,PCR3,…\n" " Specifiy TPM2 PCRs to seal against\n" + " --wipe-slot=SLOT1,SLOT2,…\n" + " Wipe specified slots\n" "\nSee the %s for details.\n" , program_invocation_short_name , ansi_highlight(), ansi_normal() @@ -84,6 +112,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_FIDO2_DEVICE, ARG_TPM2_DEVICE, ARG_TPM2_PCRS, + ARG_WIPE_SLOT, }; static const struct option options[] = { @@ -95,6 +124,7 @@ static int parse_argv(int argc, char *argv[]) { { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, + { "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT }, {} }; @@ -223,6 +253,60 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_WIPE_SLOT: { + const char *p = optarg; + + if (isempty(optarg)) { + arg_wipe_slots_mask = 0; + arg_wipe_slots_scope = WIPE_EXPLICIT; + break; + } + + for (;;) { + _cleanup_free_ char *slot = NULL; + unsigned n; + + r = extract_first_word(&p, &slot, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == 0) + break; + if (r < 0) + return log_error_errno(r, "Failed to parse slot list: %s", optarg); + + if (streq(slot, "all")) + arg_wipe_slots_scope = WIPE_ALL; + else if (streq(slot, "empty")) { + if (arg_wipe_slots_scope != WIPE_ALL) /* if "all" was specified before, that wins */ + arg_wipe_slots_scope = WIPE_EMPTY_PASSPHRASE; + } else if (streq(slot, "password")) + arg_wipe_slots_mask = 1U << ENROLL_PASSWORD; + else if (streq(slot, "recovery")) + arg_wipe_slots_mask = 1U << ENROLL_RECOVERY; + else if (streq(slot, "pkcs11")) + arg_wipe_slots_mask = 1U << ENROLL_PKCS11; + else if (streq(slot, "fido2")) + arg_wipe_slots_mask = 1U << ENROLL_FIDO2; + else if (streq(slot, "tpm2")) + arg_wipe_slots_mask = 1U << ENROLL_TPM2; + else { + int *a; + + r = safe_atou(slot, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse slot index: %s", slot); + if (n > INT_MAX) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Slot index out of range: %u", n); + + a = reallocarray(arg_wipe_slots, sizeof(int), arg_n_wipe_slots + 1); + if (!a) + return log_oom(); + + arg_wipe_slots = a; + arg_wipe_slots[arg_n_wipe_slots++] = (int) n; + } + } + break; + } + case '?': return -EINVAL; @@ -231,10 +315,6 @@ static int parse_argv(int argc, char *argv[]) { } } - if (arg_enroll_type < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "No operation specified, refusing."); - if (optind >= argc) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No block device node specified, refusing."); @@ -373,7 +453,7 @@ static int run(int argc, char *argv[]) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_(erase_and_freep) void *vk = NULL; size_t vks; - int r; + int slot, r; log_show_color(true); log_parse_environment(); @@ -383,37 +463,55 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - r = prepare_luks(&cd, &vk, &vks); + if (arg_enroll_type < 0) + r = prepare_luks(&cd, NULL, NULL); /* No need to unlock device if we don't need the volume key because we don't need to enroll anything */ + else + r = prepare_luks(&cd, &vk, &vks); if (r < 0) return r; switch (arg_enroll_type) { case ENROLL_PASSWORD: - r = enroll_password(cd, vk, vks); + slot = enroll_password(cd, vk, vks); break; case ENROLL_RECOVERY: - r = enroll_recovery(cd, vk, vks); + slot = enroll_recovery(cd, vk, vks); break; case ENROLL_PKCS11: - r = enroll_pkcs11(cd, vk, vks, arg_pkcs11_token_uri); + slot = enroll_pkcs11(cd, vk, vks, arg_pkcs11_token_uri); break; case ENROLL_FIDO2: - r = enroll_fido2(cd, vk, vks, arg_fido2_device); + slot = enroll_fido2(cd, vk, vks, arg_fido2_device); break; case ENROLL_TPM2: - r = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask); + slot = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask); break; + case _ENROLL_TYPE_INVALID: + /* List enrolled slots if we are called without anything to enroll or wipe */ + if (!wipe_requested()) + return list_enrolled(cd); + + /* Only slot wiping selected */ + return wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, -1); + default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet."); } + if (slot < 0) + return slot; - return r; + /* After we completed enrolling, remove user selected slots */ + r = wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, slot); + if (r < 0) + return r; + + return 0; } DEFINE_MAIN_FUNCTION(run); diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h new file mode 100644 index 0000000000..f8b81f17a5 --- /dev/null +++ b/src/cryptenroll/cryptenroll.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef enum EnrollType { + ENROLL_PASSWORD, + ENROLL_RECOVERY, + ENROLL_PKCS11, + ENROLL_FIDO2, + ENROLL_TPM2, + _ENROLL_TYPE_MAX, + _ENROLL_TYPE_INVALID = -1, +} EnrollType; + +typedef enum WipeScope { + WIPE_EXPLICIT, /* only wipe the listed slots */ + WIPE_ALL, /* wipe all slots */ + WIPE_EMPTY_PASSPHRASE, /* wipe slots with empty passphrases plus listed slots */ + _WIPE_SCOPE_MAX, + _WIPE_SCOPE_INVALID = -1, +} WipeScope; + +const char* enroll_type_to_string(EnrollType t); +EnrollType enroll_type_from_string(const char *s); + +const char* luks2_token_type_to_string(EnrollType t); +EnrollType luks2_token_type_from_string(const char *s);