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
+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);