cryptenroll: add interactive --firstboot enrollment wizard

Add a --firstboot mode that interactively walks the user through enrolling a
passphrase, a recovery key, or a FIDO2 token, with one menu entry per suitable
token currently plugged in (driven by a new fido2_enumerate_devices() helper).
Pressing enter at the top-level menu leaves the volume unchanged; for each
already-enrolled credential type the wizard offers to wipe it as part of the
operation. It populates the same EnrollContext the command line and Varlink
paths use, so the actual enrollment goes through the shared enroll_now() path.

A companion --prompt-suppress= option takes a list of slot types: if a slot of
any listed type already exists, the wizard does nothing and exits successfully.
This lets it be hooked into the boot process while staying quiet once the
system has been set up.

The accompanying systemd-cryptenroll-firstboot.service runs this from the
initrd, after systemd-repart has created the encrypted volume but before we
transition to the host, suppressing itself once a password, recovery key or
FIDO2 token is enrolled. To make that work, determine_default_node() now looks
below /sysroot/ when running in the initrd, since the host file systems aren't
at their final location yet.

While the wizard is active it draws the same installer-style chrome (blue bars
at the top and bottom of the terminal) as systemd-sysinstall, using the shared
prompt_loop_yes_no() helper for its wipe confirmations.

Honours the systemd.firstboot= kernel command line option.

Fixes: #36298
This commit is contained in:
Lennart Poettering
2026-06-27 17:28:39 +02:00
parent e298331876
commit 0089a46b6b
9 changed files with 674 additions and 68 deletions
+59
View File
@@ -362,6 +362,65 @@
</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>
+243
View File
@@ -0,0 +1,243 @@
/* 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;
}
+12
View File
@@ -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);
+195 -66
View File
@@ -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");
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);
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) {
@@ -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);
/* 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_oom();
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;
_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. */
if (c.enroll_type < 0) {
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);
+1
View File
@@ -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',
+103
View File
@@ -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;
+13
View File
@@ -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);
+5
View File
@@ -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' },
{
@@ -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