diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml index c88d38fa43..9f090ec94b 100644 --- a/man/systemd-cryptenroll.xml +++ b/man/systemd-cryptenroll.xml @@ -362,6 +362,65 @@ + + Interactive Enrollment + + The following options are understood that drive an interactive enrollment wizard, intended to be + run on first boot (similar in spirit to + systemd-firstboot1): + + + + + + Run an interactive wizard that lets the user enroll a passphrase, a recovery key, or a + FIDO2 security token (one menu entry per suitable token currently plugged in). If credentials of any + of these types are already enrolled, the wizard additionally offers to wipe them. Pressing enter at + the top-level menu leaves the volume unchanged. This option may not be combined with an explicit + enrollment type or with . As with the other commands, the volume to + operate on is the one backing /var/ if none is specified. Honours the + systemd.firstboot= kernel command line option. + + + + + + + + Takes a comma-separated list of slot types, out of password, + recovery, pkcs11, fido2 and + tpm2. Only useful together with : if a slot of any of + the listed types is already enrolled on the volume, the wizard does nothing and exits successfully. + This is useful to hook the wizard into the boot process while ensuring it stays quiet once suitable + credentials have been set up. + + + + + + + + Takes a boolean argument, defaults to on. Only useful together with + : if enabled the interactive wizard draws a coloured bar across the top and + bottom of the terminal to visually frame the prompt. Pass to disable this + decoration, for example when running on a terminal that does not render it well. + + + + + + + + Takes a boolean argument, defaults to off. Only useful together with + : if enabled the kernel and PID 1 are asked to refrain from writing log + messages to the console while the interactive wizard is shown, so that such output does not interfere + with the prompt. The console is unmuted again once the wizard exits. + + + + + + PKCS#11 Enrollment diff --git a/src/cryptenroll/cryptenroll-interactive.c b/src/cryptenroll/cryptenroll-interactive.c new file mode 100644 index 0000000000..6dc584b18b --- /dev/null +++ b/src/cryptenroll/cryptenroll-interactive.c @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "ansi-color.h" +#include "cryptenroll.h" +#include "cryptenroll-interactive.h" +#include "cryptenroll-list.h" +#include "cryptsetup-util.h" +#include "glyph-util.h" +#include "libfido2-util.h" +#include "log.h" +#include "proc-cmdline.h" +#include "prompt-util.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" + +static int collect_existing_types(struct crypt_device *cd, unsigned *ret_mask) { + _cleanup_free_ EnrolledSlot *slots = NULL; + size_t n_slots; + unsigned mask = 0; + int r; + + assert(cd); + assert(ret_mask); + + r = collect_enrolled_slots(cd, &slots, &n_slots); + if (r < 0) + return r; + + FOREACH_ARRAY(s, slots, n_slots) + if (!s->conflict && s->type >= 0) + mask |= 1U << s->type; + + *ret_mask = mask; + return 0; +} + +static int choose_mechanism(EnrollContext *c) { + int r; + + assert(c); + + /* Top-level menu. Each iteration re-enumerates the FIDO2 tokens, so a token plugged in after the + * menu was first shown appears once the user picks "Rescan". An empty answer leaves the volume + * untouched. */ + + for (;;) { + Fido2DeviceInfo *devices = NULL; + size_t n_devices = 0; + CLEANUP_ARRAY(devices, n_devices, fido2_device_info_free_many); + + /* Best effort: if FIDO2 isn't available we simply offer no token rows. */ + (void) fido2_enumerate_devices(&devices, &n_devices); + + _cleanup_strv_free_ char **menu = strv_new("Enroll a recovery key", "Enroll a passphrase"); + if (!menu) + return log_oom(); + + FOREACH_ARRAY(d, devices, n_devices) { + _cleanup_free_ char *label = NULL; + + label = strjoin("Enroll FIDO2 security token: ", + d->manufacturer ?: "Security Token", + d->product ? " " : "", strempty(d->product), + " (", d->path, ")"); + if (!label) + return log_oom(); + + if (strv_consume(&menu, TAKE_PTR(label)) < 0) + return log_oom(); + } + + if (strv_extend(&menu, "Rescan for FIDO2 security tokens") < 0) + return log_oom(); + + _cleanup_free_ char *choice = NULL; + r = prompt_loop( + "Select enrollment option", + GLYPH_LOCK_AND_KEY, + /* prefill= */ NULL, + menu, + /* accepted= */ menu, /* only accept exact menu entries (or their numbers) */ + /* ellipsize_percentage= */ 60, + /* n_columns= */ 1, + /* column_width= */ SIZE_MAX, /* auto-size to the widest entry */ + /* is_valid= */ NULL, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_MAY_SKIP|PROMPT_SHOW_MENU|PROMPT_SHOW_MENU_NOW|PROMPT_HIDE_MENU_HINT, + &choice); + if (r < 0) + return r; + if (!choice) { + /* Empty answer: do nothing. */ + log_info("No selection made, leaving volume unchanged."); + return 0; + } + + /* Map the chosen label back to its index. */ + size_t idx = SIZE_MAX; + STRV_FOREACH(m, menu) + if (streq(*m, choice)) { + idx = m - menu; + break; + } + + assert(idx != SIZE_MAX); + + if (idx == 0) + c->enroll_type = ENROLL_RECOVERY; + else if (idx == 1) + c->enroll_type = ENROLL_PASSWORD; + else if (idx + 1 == strv_length(menu)) /* "Rescan" */ + continue; + else { + c->enroll_type = ENROLL_FIDO2; + assert(idx > 1); + assert(idx + 1 < strv_length(menu)); + + if (strdup_to(&c->fido2_device, devices[idx - 2].path) < 0) + return log_oom(); + } + + return 1; + } +} + +static int ask_wipe(EnrollContext *c, unsigned existing_mask) { + static const EnrollType candidates[] = { + ENROLL_PASSWORD, + ENROLL_RECOVERY, + ENROLL_FIDO2, + }; + int r; + + assert(c); + + /* For each already-enrolled type the wizard understands, offer to wipe it alongside the new + * enrollment. */ + + FOREACH_ELEMENT(t, candidates) { + if (!FLAGS_SET(existing_mask, 1U << *t)) + continue; + + _cleanup_free_ char *question = NULL; + question = strjoin("A ", enroll_type_to_string(*t), " slot is already enrolled. Wipe it as part of this enrollment?"); + if (!question) + return log_oom(); + + bool wipe; + r = prompt_loop_yes_no(question, /* def= */ false, &wipe); + if (r < 0) + return r; + + if (wipe) + c->wipe_slots_mask |= 1U << *t; + } + + return 0; +} + +int cryptenroll_run_interactive( + EnrollContext *c, + unsigned prompt_suppress_mask, + bool chrome, + sd_varlink **mute_console_link) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + unsigned existing_mask = 0; + int r; + + assert(c); + assert(c->node); + + /* Honour the systemd.firstboot= kernel command line option, just like systemd-firstboot. */ + bool enabled; + r = proc_cmdline_get_bool("systemd.firstboot", PROC_CMDLINE_TRUE_WHEN_MISSING, &enabled); + if (r < 0) + log_warning_errno(r, "Failed to parse systemd.firstboot= kernel command line option, ignoring: %m"); + else if (!enabled) { + log_debug("systemd.firstboot=no set, skipping interactive enrollment."); + return 0; + } + + /* Open the volume just to inspect its header (no unlocking needed yet). */ + r = prepare_luks(c, &cd, /* ret_volume_key= */ NULL); + if (r < 0) + return r; + + r = collect_existing_types(cd, &existing_mask); + if (r < 0) + return r; + + /* If a credential of a suppressed type is already enrolled, do nothing. This lets the wizard be + * wired into first boot but stay quiet once the system has been set up. */ + if ((existing_mask & prompt_suppress_mask) != 0) { + log_debug("A credential of a suppressed type is already enrolled, skipping interactive setup."); + return 0; + } + + if (existing_mask == 0) { + log_debug("No recognized LUKS slots, we're unlikely able to unlock, skipping interactive setup."); + return 0; + } + + (void) mute_console(mute_console_link); + + /* Draw the installer-style chrome (blue bars at the top and bottom) around the wizard, matching + * systemd-sysinstall. The caller hides it again via a deferred chrome_hide() once enrollment is + * complete. */ + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); + + if (chrome) + (void) chrome_show("Additional Disk Encryption Key Enrollment", /* bottom= */ NULL); + + printf("%s%s%sLet's enroll additional disk encryption mechanisms for recovering access to the system.%s\n\n", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "", + ansi_highlight(), ansi_normal()); + + _cleanup_free_ char *s = NULL; + for (EnrollType t = 0; t < _ENROLL_TYPE_MAX; t++) { + if (!FLAGS_SET(existing_mask, 1 << t)) + continue; + + if (!strextend_with_separator(&s, ", ", enroll_type_to_string(t))) + return log_oom(); + } + + printf("Currently enrolled mechanisms: %s\n\n", s); + + r = choose_mechanism(c); + if (r <= 0) + return r; + + r = ask_wipe(c, existing_mask); + if (r < 0) + return r; + + return 1; +} diff --git a/src/cryptenroll/cryptenroll-interactive.h b/src/cryptenroll/cryptenroll-interactive.h new file mode 100644 index 0000000000..413e0d8b93 --- /dev/null +++ b/src/cryptenroll/cryptenroll-interactive.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptenroll.h" + +/* Runs the interactive first-boot enrollment wizard, populating *c (c->enroll_type, c->fido2_device, + * c->wipe_slots_mask). On return c->enroll_type is set to the chosen mechanism, or left + * _ENROLL_TYPE_INVALID if the user skipped or the wizard was suppressed. + * + * prompt_suppress_mask is a bitmask of (1U << EnrollType): if a slot of any such type already exists on the + * volume, the wizard does nothing (so it can be hooked into first boot but stay quiet on later boots). */ +int cryptenroll_run_interactive(EnrollContext *c, unsigned prompt_suppress_mask, bool chrome, sd_varlink **mute_console_link); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index 8c488e554f..67386f31d8 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -9,8 +9,10 @@ #include "blockdev-list.h" #include "blockdev-util.h" #include "build.h" +#include "cleanup-util.h" #include "cryptenroll.h" #include "cryptenroll-fido2.h" +#include "cryptenroll-interactive.h" #include "cryptenroll-list.h" #include "cryptenroll-password.h" #include "cryptenroll-pkcs11.h" @@ -21,6 +23,8 @@ #include "cryptsetup-util.h" #include "extract-word.h" #include "format-table.h" +#include "help-util.h" +#include "initrd-util.h" #include "libfido2-util.h" #include "log.h" #include "main-func.h" @@ -30,10 +34,11 @@ #include "parse-argument.h" #include "parse-util.h" #include "pkcs11-util.h" -#include "pretty-print.h" #include "process-util.h" +#include "prompt-util.h" #include "string-table.h" #include "string-util.h" +#include "terminal-util.h" #include "tpm2-pcr.h" #include "tpm2-util.h" @@ -63,6 +68,10 @@ 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 */ +static bool arg_firstboot = false; +static bool arg_chrome = true; +static bool arg_mute_console = false; +static unsigned arg_prompt_suppress_mask = 0; /* Bitmask of (1U << EnrollType): if any such slot exists, --firstboot does nothing */ static Fido2EnrollFlags arg_fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP; #if HAVE_LIBFIDO2 static int arg_fido2_cred_alg = COSE_ES256; @@ -113,6 +122,20 @@ static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = { DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); +static int enroll_type_mask_from_string(const char *name) { + assert(name); + + /* Maps an enroll type name (command line spelling) to its (1U << EnrollType) bitmask, or returns a + * negative errno if the name is not a known type. Callers merge the returned mask into their own + * accumulator. Shared by the various places that parse type lists into a bitmask. */ + + EnrollType t = enroll_type_from_string(name); + if (t < 0) + return -EINVAL; + + return 1 << t; +} + void enroll_context_done(EnrollContext *c) { if (!c) return; @@ -137,6 +160,53 @@ void enroll_context_done(EnrollContext *c) { c->link = sd_varlink_unref(c->link); } +static int resolve_default_node(const char *path, char **ret) { + int r; + + /* Resolves the path of the underlying LUKS2 block device of the file system at the given mount + * point, i.e. the raw partition behind the dm-crypt mapping that file system sits on. Returns + * -ENXIO if the file system is not backed by a (single) LUKS2 device. Logs only at debug level, so + * the caller can try the next candidate path. */ + + assert(path); + assert(ret); + + dev_t devno; + r = get_block_device(path, &devno); + if (r < 0) + return log_debug_errno(r, "Failed to determine block device backing %s: %m", path); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), + "File system %s is not backed by a (single) whole block device.", path); + + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + r = sd_device_new_from_devnum(&dev, 'b', devno); + if (r < 0) + return log_debug_errno(r, "Unable to access backing block device for %s: %m", path); + + const char *dm_uuid; + r = sd_device_get_property_value(dev, "DM_UUID", &dm_uuid); + if (r == -ENOENT) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "Backing block device of %s is not a DM device.", path); + if (r < 0) + return log_debug_errno(r, "Unable to query DM_UUID udev property of backing block device for %s: %m", path); + + if (!startswith(dm_uuid, "CRYPT-LUKS2-")) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "Block device backing %s is not a LUKS2 device.", path); + + _cleanup_(sd_device_unrefp) sd_device *origin = NULL; + r = block_device_get_originating(dev, &origin, /* recursive= */ false); + if (r < 0) + return log_debug_errno(r, "Failed to get originating device of LUKS2 device backing %s: %m", path); + + const char *dp; + r = sd_device_get_devname(origin, &dp); + if (r < 0) + return log_debug_errno(r, "Failed to get device path for LUKS2 device backing %s: %m", path); + + return strdup_to(ret, dp); +} + static int determine_default_node(void) { int r; @@ -154,47 +224,30 @@ static int determine_default_node(void) { * Or to say this differently: it makes sense to support well systems with /var/ being on /. It also * makes sense to support well systems with them being separate, and /var/ being variable and * persistent. But any other kind of system appears much less interesting to support, and in that - * case people should just specify the device name explicitly. */ + * case people should just specify the device name explicitly. + * + * When invoked from the initrd the host's file systems are not mounted at their final location yet, + * but below /sysroot/, hence look there instead. */ - dev_t devno; - r = get_block_device("/var", &devno); - if (r < 0) - return log_error_errno(r, "Failed to determine block device backing /var/: %m"); - if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), - "File system /var/ is on not backed by a (single) whole block device."); + const char *candidates[2]; + size_t n_candidates = 0; - _cleanup_(sd_device_unrefp) sd_device *dev = NULL; - r = sd_device_new_from_devnum(&dev, 'b', devno); - if (r < 0) - return log_error_errno(r, "Unable to access backing block device for /var/: %m"); + if (in_initrd()) { + candidates[n_candidates++] = "/sysroot/var"; + candidates[n_candidates++] = "/sysroot"; + } else + candidates[n_candidates++] = "/var"; - const char *dm_uuid; - r = sd_device_get_property_value(dev, "DM_UUID", &dm_uuid); - if (r == -ENOENT) - return log_error_errno(r, "Backing block device of /var/ is not a DM device: %m"); - if (r < 0) - return log_error_errno(r, "Unable to query DM_UUID udev property of backing block device for /var/: %m"); + FOREACH_ARRAY(path, candidates, n_candidates) { + r = resolve_default_node(*path, &arg_node); + if (r >= 0) { + log_info("No device specified, defaulting to '%s' (backing %s).", arg_node, *path); + return 0; + } + } - if (!startswith(dm_uuid, "CRYPT-LUKS2-")) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Block device backing /var/ is not a LUKS2 device."); - - _cleanup_(sd_device_unrefp) sd_device *origin = NULL; - r = block_device_get_originating(dev, &origin, /* recursive= */ false); - if (r < 0) - return log_error_errno(r, "Failed to get originating device of LUKS2 device backing /var/: %m"); - - const char *dp; - r = sd_device_get_devname(origin, &dp); - if (r < 0) - return log_error_errno(r, "Failed to get device path for LUKS2 device backing /var/: %m"); - - r = free_and_strdup_warn(&arg_node, dp); - if (r < 0) - return r; - - log_info("No device specified, defaulting to '%s'.", arg_node); - return 0; + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "Failed to automatically determine a LUKS2 block device to operate on, please specify one explicitly."); } static int parse_wipe_slot(const char *arg) { @@ -217,21 +270,15 @@ static int parse_wipe_slot(const char *arg) { if (r < 0) return log_error_errno(r, "Failed to parse slot list: %s", arg); + int mask; + 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 if ((mask = enroll_type_mask_from_string(slot)) >= 0) + arg_wipe_slots_mask |= mask; else { unsigned n; @@ -249,15 +296,34 @@ static int parse_wipe_slot(const char *arg) { } } -static int help(void) { - _cleanup_free_ char *link = NULL; +static int parse_prompt_suppress(const char *arg) { int r; - pager_open(arg_pager_flags); + assert(arg); - r = terminal_urlify_man("systemd-cryptenroll", "1", &link); - if (r < 0) - return log_oom(); + /* Parses a comma-separated list of the slot types the --firstboot wizard knows how to enroll. If a + * slot of any listed type already exists on the volume, the wizard does nothing. */ + + for (const char *p = arg;;) { + _cleanup_free_ char *type = NULL; + int mask; + + r = extract_first_word(&p, &type, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == 0) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to parse type list: %s", arg); + + mask = enroll_type_mask_from_string(type); + if (mask < 0) + return log_error_errno(mask, "Unknown slot type: %s", type); + + arg_prompt_suppress_mask |= mask; + } +} + +static int help(void) { + int r; static const char* const groups[] = { NULL, @@ -279,21 +345,20 @@ static int help(void) { (void) table_sync_column_widths(0, tables[0], tables[1], tables[2], tables[3], tables[4], tables[5]); - printf("%s [OPTIONS...] [BLOCK-DEVICE]\n\n" - "%sEnroll a security token or authentication credential to a LUKS volume.%s\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal()); + pager_open(arg_pager_flags); + + help_cmdline("[OPTIONS...] [BLOCK-DEVICE]"); + help_abstract("Enroll a security token or authentication credential to a LUKS volume."); for (size_t i = 0; i < ELEMENTSOF(groups); i++) { - printf("\n%s%s:%s\n", ansi_underline(), groups[i] ?: "Options", ansi_normal()); + help_section(groups[i] ?: "Options"); r = table_print_or_warn(tables[i]); if (r < 0) return r; } - printf("\nSee the %s for details.\n", link); + help_man_page_reference("systemd-cryptenroll", "1"); return 0; } @@ -332,6 +397,32 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + OPTION_LONG("firstboot", NULL, + "Interactively enroll a credential (first-boot wizard)"): + arg_firstboot = true; + break; + + OPTION_LONG("prompt-suppress", "TYPE1,TYPE2,…", + "Skip the --firstboot wizard if a slot of any listed type exists"): + r = parse_prompt_suppress(opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("chrome", "BOOL", + "In first-boot mode: if false don't show colour bar at top and bottom of terminal"): + r = parse_boolean_argument("--chrome=", opts.arg, &arg_chrome); + if (r < 0) + return r; + break; + + OPTION_LONG("mute-console", "BOOL", + "In first-boot mode, tell kernel/PID 1 to not write to the console while running"): + r = parse_boolean_argument("--mute-console=", opts.arg, &arg_mute_console); + if (r < 0) + return r; + break; + OPTION_GROUP("Unlocking"): {} OPTION_LONG("unlock-empty", NULL, "Use an empty password to unlock the volume"): @@ -625,6 +716,17 @@ static int parse_argv(int argc, char *argv[]) { if (option_parser_get_n_args(&opts) > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments, refusing."); + if (arg_firstboot) { + if (arg_enroll_type >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--firstboot may not be combined with an explicit enrollment type, refusing."); + if (wipe_requested()) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--firstboot may not be combined with --wipe-slot=, refusing."); + } else if (arg_prompt_suppress_mask != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--prompt-suppress= is only useful together with --firstboot, refusing."); + const char *arg = option_parser_get_arg(&opts, 0); if (arg) r = parse_path_argument(arg, false, &arg_node); @@ -932,8 +1034,27 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - /* If we are called without anything to enroll, we just need the LUKS device, not the volume key. */ - if (c.enroll_type < 0) { + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL; + + /* Ensure the interactive chrome (drawn by cryptenroll_run_interactive() in --firstboot mode) is + * always torn down on exit; chrome_hide() is a no-op if no chrome was shown. */ + DEFER_VOID_CALL(chrome_hide); + + if (arg_firstboot) { + assert(c.enroll_type < 0); + + r = cryptenroll_run_interactive( + &c, + arg_prompt_suppress_mask, + arg_chrome, + arg_mute_console ? &mute_console_link : NULL); + if (r <= 0) + return r; + + assert(c.enroll_type >= 0); + + } else if (c.enroll_type < 0) { + /* If we are called without anything to enroll, we just need the LUKS device, not the volume key. */ r = prepare_luks(&c, &cd, /* ret_volume_key= */ NULL); if (r < 0) return r; @@ -948,19 +1069,27 @@ static int run(int argc, char *argv[]) { r = prepare_luks(&c, &cd, &vk); if (r < 0) - return r; + goto finish; slot = enroll_now(&c, cd, &vk, /* ret_recovery_key= */ NULL); - if (slot < 0) - return slot; + if (slot < 0) { + r = slot; + goto finish; + } /* After we completed enrolling, remove user selected slots (keeping the one we just added) */ c.wipe_except_slot = slot; r = wipe_slots(&c, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL); if (r < 0) - return r; + goto finish; - return 0; + r = 0; + +finish: + if (arg_firstboot) + (void) any_key_to_proceed(); + + return r; } DEFINE_MAIN_FUNCTION(run); diff --git a/src/cryptenroll/meson.build b/src/cryptenroll/meson.build index 990b3dbc17..3fbb5bf080 100644 --- a/src/cryptenroll/meson.build +++ b/src/cryptenroll/meson.build @@ -7,6 +7,7 @@ endif systemd_cryptenroll_sources = files( 'cryptenroll.c', 'cryptenroll-fido2.c', + 'cryptenroll-interactive.c', 'cryptenroll-list.c', 'cryptenroll-password.c', 'cryptenroll-pkcs11.c', diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index a7c776d0ce..4a0d7e6d64 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -1319,6 +1319,109 @@ finish: #endif } +void fido2_device_info_done(Fido2DeviceInfo *d) { + assert(d); + + d->path = mfree(d->path); + d->manufacturer = mfree(d->manufacturer); + d->product = mfree(d->product); +} + +void fido2_device_info_free_many(Fido2DeviceInfo *a, size_t n) { + FOREACH_ARRAY(i, a, n) + fido2_device_info_done(i); + + free(a); +} + +int fido2_enumerate_devices(Fido2DeviceInfo **ret, size_t *ret_n) { +#if HAVE_LIBFIDO2 + Fido2DeviceInfo *devices = NULL; + size_t n_devices = 0, allocated = 64, found = 0; + fido_dev_info_t *di = NULL; + int r; + + CLEANUP_ARRAY(devices, n_devices, fido2_device_info_free_many); + + assert(ret); + assert(ret_n); + + r = dlopen_libfido2(LOG_ERR); + if (r < 0) + return r; + + di = sym_fido_dev_info_new(allocated); + if (!di) + return log_oom(); + + r = sym_fido_dev_info_manifest(di, allocated, &found); + if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { + /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ + r = 0; + goto finish; + } + if (r != FIDO_OK) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); + goto finish; + } + + for (size_t i = 0; i < found; i++) { + bool has_rk, has_client_pin, has_up, has_uv, has_always_uv; + const fido_dev_info_t *entry; + const char *path; + + entry = sym_fido_dev_info_ptr(di, i); + if (!entry) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get device information for FIDO device %zu.", i); + goto finish; + } + + path = sym_fido_dev_info_path(entry); + + r = check_device_is_fido2_with_hmac_secret(path, &has_rk, &has_client_pin, &has_up, &has_uv, &has_always_uv); + if (r < 0) + goto finish; + if (r == 0) /* Not a FIDO2 device suitable for enrollment, skip it */ + continue; + + if (!GREEDY_REALLOC(devices, n_devices + 1)) { + r = log_oom(); + goto finish; + } + + Fido2DeviceInfo *d = devices + n_devices; + *d = (Fido2DeviceInfo) {}; + + /* The manufacturer / product strings are optional (NULL when the device doesn't report + * them), but a failure to duplicate any string is a genuine OOM and thus fatal. */ + if (strdup_to(&d->path, path) < 0 || + strdup_to(&d->manufacturer, empty_to_null(sym_fido_dev_info_manufacturer_string(entry))) < 0 || + strdup_to(&d->product, empty_to_null(sym_fido_dev_info_product_string(entry))) < 0) { + fido2_device_info_done(d); + r = log_oom(); + goto finish; + } + + n_devices++; + } + + r = 0; + +finish: + sym_fido_dev_info_free(&di, allocated); + if (r < 0) + return r; + + *ret = TAKE_PTR(devices); + *ret_n = n_devices; + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 tokens not supported on this build."); +#endif +} + int fido2_find_device_auto(char **ret) { #if HAVE_LIBFIDO2 _cleanup_free_ char *copy = NULL; diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index 6b23b62688..12a0201332 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -137,4 +137,17 @@ static inline int parse_fido2_algorithm(const char *s, int *ret) { int fido2_list_devices(void); int fido2_find_device_auto(char **ret); +typedef struct Fido2DeviceInfo { + char *path; + char *manufacturer; /* may be NULL if the device doesn't report it */ + char *product; /* may be NULL if the device doesn't report it */ +} Fido2DeviceInfo; + +void fido2_device_info_done(Fido2DeviceInfo *d); +void fido2_device_info_free_many(Fido2DeviceInfo *a, size_t n); + +/* Enumerates the FIDO2 tokens that implement the 'hmac-secret' extension (i.e. are suitable for + * enrollment), returning a newly allocated array. */ +int fido2_enumerate_devices(Fido2DeviceInfo **ret, size_t *ret_n); + int fido2_have_device(const char *device); diff --git a/units/meson.build b/units/meson.build index f813426216..60ce3bf0ca 100644 --- a/units/meson.build +++ b/units/meson.build @@ -347,6 +347,11 @@ units = [ 'file' : 'systemd-cryptenroll@.service', 'conditions' : ['HAVE_LIBCRYPTSETUP'], }, + { + 'file' : 'systemd-cryptenroll-firstboot.service', + 'conditions' : ['HAVE_LIBCRYPTSETUP', 'ENABLE_INITRD'], + 'symlinks' : ['initrd.target.wants/'], + }, { 'file' : 'systemd-exit.service' }, { 'file' : 'systemd-factory-reset@.service.in' }, { diff --git a/units/systemd-cryptenroll-firstboot.service b/units/systemd-cryptenroll-firstboot.service new file mode 100644 index 0000000000..ccd69ef60f --- /dev/null +++ b/units/systemd-cryptenroll-firstboot.service @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +# Runs from the initrd: once systemd-repart has created/grown the partitions +# (including any encrypted ones) on first boot, but before we transition into +# the host, give the user a chance to interactively enroll a disk encryption +# credential. + +[Unit] +Description=Interactive Disk Encryption Setup +Documentation=man:systemd-cryptenroll(1) + +ConditionPathExists=/etc/initrd-release + +# We want a first boot check, but in the initrd that's not a defined +# state. Hence let's look into the mounted image directly for the primary +# indicator for the first boot state +ConditionFileNotEmpty=!/sysroot/etc/machine-id +ConditionPathIsEncrypted=/sysroot/var + +DefaultDependencies=no +After=systemd-repart.service systemd-vconsole-setup.service systemd-mute-console.socket initrd-fs.target +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target initrd.target + +[Service] +Type=oneshot +RemainAfterExit=yes +# Ignore failures: there might not be an encrypted volume to enroll into, or the +# user might decline, neither of which should hold up the boot. +ExecStart=-systemd-cryptenroll --firstboot --prompt-suppress=password,recovery,fido2 --unlock-headless --mute-console=yes +StandardOutput=tty +StandardInput=tty +StandardError=tty +TTYReset=yes