</variablelist>
</refsect1>
+ <refsect1>
+ <title>Interactive Enrollment</title>
+
+ <para>The following options are understood that drive an interactive enrollment wizard, intended to be
+ run on first boot (similar in spirit to
+ <citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>):</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><option>--firstboot</option></term>
+
+ <listitem><para>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 <option>--wipe-slot=</option>. As with the other commands, the volume to
+ operate on is the one backing <filename>/var/</filename> if none is specified. Honours the
+ <varname>systemd.firstboot=</varname> kernel command line option.</para>
+
+ <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--prompt-suppress=<replaceable>TYPE</replaceable>…</option></term>
+
+ <listitem><para>Takes a comma-separated list of slot types, out of <literal>password</literal>,
+ <literal>recovery</literal>, <literal>pkcs11</literal>, <literal>fido2</literal> and
+ <literal>tpm2</literal>. Only useful together with <option>--firstboot</option>: 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.</para>
+
+ <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--chrome=<replaceable>BOOL</replaceable></option></term>
+
+ <listitem><para>Takes a boolean argument, defaults to on. Only useful together with
+ <option>--firstboot</option>: if enabled the interactive wizard draws a coloured bar across the top and
+ bottom of the terminal to visually frame the prompt. Pass <option>--chrome=no</option> to disable this
+ decoration, for example when running on a terminal that does not render it well.</para>
+
+ <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--mute-console=<replaceable>BOOL</replaceable></option></term>
+
+ <listitem><para>Takes a boolean argument, defaults to off. Only useful together with
+ <option>--firstboot</option>: 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.</para>
+
+ <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
<refsect1>
<title>PKCS#11 Enrollment</title>
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#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;
+}
--- /dev/null
+/* 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);
#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"
#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"
#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"
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;
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;
c->link = sd_varlink_unref(c->link);
}
-static int determine_default_node(void) {
+static int resolve_default_node(const char *path, char **ret) {
int r;
- /* If no device is specified we'll default to the backing device of /var/.
- *
- * Why /var/ and not just / you ask?
- *
- * On most systems /var/ is going to be on the root fs, hence the outcome is usually the same.
- *
- * However, on systems where / and /var/ are separate it makes more sense to default to /var/ because
- * that's where the persistent and variable data is placed (i.e. where LUKS should be used) while /
- * doesn't really have to be variable and could as well be immutable or ephemeral. Hence /var/ should
- * be a better default.
- *
- * 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. */
+ /* 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("/var", &devno);
+ r = get_block_device(path, &devno);
if (r < 0)
- return log_error_errno(r, "Failed to determine block device backing /var/: %m");
+ return log_debug_errno(r, "Failed to determine block device backing %s: %m", path);
if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(ENXIO),
- "File system /var/ is on not backed by a (single) whole block device.");
+ 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_error_errno(r, "Unable to access backing block device for /var/: %m");
+ 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_error_errno(r, "Backing block device of /var/ is not a DM device: %m");
+ return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "Backing block device of %s is not a DM device.", path);
if (r < 0)
- return log_error_errno(r, "Unable to query DM_UUID udev property of backing block device for /var/: %m");
+ 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_error_errno(SYNTHETIC_ERRNO(ENXIO), "Block device backing /var/ is not a LUKS2 device.");
+ 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_error_errno(r, "Failed to get originating device of LUKS2 device backing /var/: %m");
+ 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_error_errno(r, "Failed to get device path for LUKS2 device backing /var/: %m");
+ return log_debug_errno(r, "Failed to get device path for LUKS2 device backing %s: %m", path);
- r = free_and_strdup_warn(&arg_node, dp);
- if (r < 0)
- return r;
+ return strdup_to(ret, dp);
+}
- log_info("No device specified, defaulting to '%s'.", arg_node);
- return 0;
+static int determine_default_node(void) {
+ int r;
+
+ /* If no device is specified we'll default to the backing device of /var/.
+ *
+ * Why /var/ and not just / you ask?
+ *
+ * On most systems /var/ is going to be on the root fs, hence the outcome is usually the same.
+ *
+ * However, on systems where / and /var/ are separate it makes more sense to default to /var/ because
+ * that's where the persistent and variable data is placed (i.e. where LUKS should be used) while /
+ * doesn't really have to be variable and could as well be immutable or ephemeral. Hence /var/ should
+ * be a better default.
+ *
+ * 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.
+ *
+ * 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. */
+
+ const char *candidates[2];
+ size_t n_candidates = 0;
+
+ if (in_initrd()) {
+ candidates[n_candidates++] = "/sysroot/var";
+ candidates[n_candidates++] = "/sysroot";
+ } else
+ candidates[n_candidates++] = "/var";
+
+ 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;
+ }
+ }
+
+ 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) {
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;
}
}
-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,
(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;
}
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"):
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);
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;
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);
systemd_cryptenroll_sources = files(
'cryptenroll.c',
'cryptenroll-fido2.c',
+ 'cryptenroll-interactive.c',
'cryptenroll-list.c',
'cryptenroll-password.c',
'cryptenroll-pkcs11.c',
#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;
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);
'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' },
{
--- /dev/null
+# 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