diff --git a/TODO.md b/TODO.md
index d8badaf5ab..d82371bbc9 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2527,9 +2527,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
the dropped in certs and encrypted with machine pubkey, and pass to machine.
Machine is then able to authenticate you, and confidentiality is guaranteed.
-- systemd-cryptenroll: add --firstboot or so, that will interactively ask user
- whether recovery key shall be enrolled and do so
-
- systemd-dissect: add --cat switch for dumping files such as /etc/os-release
- systemd-dissect: show available versions inside of a disk image, i.e. if
@@ -2893,7 +2890,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- coredumpctl
- systemd-bless-boot
- systemd-measure
- - systemd-cryptenroll (to allow UIs to enroll FIDO2 keys and such)
- systemd-dissect
- systemd-sysupdate
- systemd-analyze
diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml
index 707a8d25e8..9f090ec94b 100644
--- a/man/systemd-cryptenroll.xml
+++ b/man/systemd-cryptenroll.xml
@@ -270,6 +270,16 @@
The following options are understood that may be used to unlock the device in preparation of the enrollment operations:
+
+
+
+ Use an empty password/passphrase to unlock the volume, instead of reading one from
+ stdin. This is useful to unlock volumes that are currently protected by an empty password, in order to
+ enroll a different, stronger unlock mechanism.
+
+
+
+
@@ -307,6 +317,18 @@
+
+
+
+
+ Try the unlock mechanisms suitable for headless operation in turn, without requiring
+ any interactive input. If a TPM2 device is available the volume is unlocked via TPM2, and otherwise
+ (or if TPM2 unlocking fails) an empty password is attempted. This is primarily useful in automated
+ provisioning scenarios where a volume is first set up with a TPM2 enrollment or an empty password and
+ a stronger unlock mechanism shall be enrolled subsequently.
+
+
+
@@ -340,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/shell-completion/bash/systemd-cryptenroll b/shell-completion/bash/systemd-cryptenroll
index 6ae9bb3840..a24d997802 100644
--- a/shell-completion/bash/systemd-cryptenroll
+++ b/shell-completion/bash/systemd-cryptenroll
@@ -43,10 +43,14 @@ _systemd_cryptenroll() {
local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword
local -A OPTS=(
[STANDALONE]='-h --help --version
- --password --recovery-key --list-devices'
+ --password --recovery-key --list-devices
+ --unlock-empty --unlock-headless --firstboot'
[ARG]='--unlock-key-file
--unlock-fido2-device
--unlock-tpm2-device
+ --prompt-suppress
+ --chrome
+ --mute-console
--pkcs11-token-uri
--fido2-credential-algorithm
--fido2-device
@@ -99,6 +103,12 @@ _systemd_cryptenroll() {
--wipe-slot)
comps='all empty password recovery pkcs11 fido2 tpm2'
;;
+ --prompt-suppress)
+ comps='password recovery pkcs11 fido2 tpm2'
+ ;;
+ --chrome|--mute-console)
+ comps='yes no'
+ ;;
esac
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
diff --git a/shell-completion/zsh/_systemd-cryptenroll b/shell-completion/zsh/_systemd-cryptenroll
new file mode 100644
index 0000000000..e41b67ffdc
--- /dev/null
+++ b/shell-completion/zsh/_systemd-cryptenroll
@@ -0,0 +1,74 @@
+#compdef systemd-cryptenroll
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+(( $+functions[_systemd-cryptenroll_devices] )) ||
+_systemd-cryptenroll_devices() {
+ local -a devices
+ devices=( ${(f)"$(_call_program devices systemd-cryptenroll --list-devices 2>/dev/null)"} )
+ _describe -t devices 'block device' devices
+}
+
+(( $+functions[_systemd-cryptenroll_fido2_device] )) ||
+_systemd-cryptenroll_fido2_device() {
+ _alternative \
+ "special:special:($*)" \
+ 'devices:FIDO2 device:_files -g "/dev/hidraw*(-c)"'
+}
+
+(( $+functions[_systemd-cryptenroll_tpm2_device] )) ||
+_systemd-cryptenroll_tpm2_device() {
+ _alternative \
+ "special:special:($*)" \
+ 'devices:TPM2 device:_files -g "/dev/tpmrm*(-c)"'
+}
+
+(( $+functions[_systemd-cryptenroll_wipe_slot] )) ||
+_systemd-cryptenroll_wipe_slot() {
+ _values -s , 'slot' all empty password recovery pkcs11 fido2 tpm2
+}
+
+(( $+functions[_systemd-cryptenroll_prompt_suppress] )) ||
+_systemd-cryptenroll_prompt_suppress() {
+ _values -s , 'type' password recovery pkcs11 fido2 tpm2
+}
+
+# Unlock methods are mutually exclusive with each other
+local unlock='--unlock-empty --unlock-key-file --unlock-fido2-device --unlock-tpm2-device --unlock-headless'
+# Enrollment operations are mutually exclusive with each other
+local enroll='--password --recovery-key --pkcs11-token-uri --fido2-device --tpm2-device --tpm2-device-key --firstboot'
+
+_arguments -s \
+ '(- *)'{-h,--help}'[Show this help]' \
+ '(- *)--version[Show package version]' \
+ '--no-pager[Do not pipe output into a pager]' \
+ '(- *)--list-devices[List candidate block devices to operate on]' \
+ '--wipe-slot=[Wipe specified slots]:slot:_systemd-cryptenroll_wipe_slot' \
+ "($enroll)--firstboot[Interactively enroll a credential (first-boot wizard)]" \
+ '--prompt-suppress=[Skip the --firstboot wizard if a slot of any listed type exists]:type:_systemd-cryptenroll_prompt_suppress' \
+ '--chrome=[In first-boot mode, do not show colour bar at top and bottom of terminal]:boolean:(yes no)' \
+ '--mute-console=[In first-boot mode, tell kernel/PID 1 to not write to the console while running]:boolean:(yes no)' \
+ "($unlock)--unlock-empty[Use an empty password to unlock the volume]" \
+ "($unlock)--unlock-key-file=[Use a file to unlock the volume]:key file:_files" \
+ "($unlock)--unlock-fido2-device=[Use a FIDO2 device to unlock the volume]:FIDO2 device:_systemd-cryptenroll_fido2_device auto" \
+ "($unlock)--unlock-tpm2-device=[Use a TPM2 device to unlock the volume]:TPM2 device:_systemd-cryptenroll_tpm2_device auto" \
+ "($unlock)--unlock-headless[Try the 'headless' unlock mechanisms in turn]" \
+ "($enroll)--password[Enroll a user-supplied password]" \
+ "($enroll)--recovery-key[Enroll a recovery key]" \
+ "($enroll)--pkcs11-token-uri=[Enroll a PKCS#11 security token or list them]:PKCS#11 token URI:(auto list pkcs11:)" \
+ "($enroll)--fido2-device=[Enroll a FIDO2-HMAC security token or list them]:FIDO2 device:_systemd-cryptenroll_fido2_device auto list" \
+ '--fido2-salt-file=[Use salt from a file instead of generating one]:salt file:_files' \
+ '--fido2-parameters-in-header=[Whether to store FIDO2 parameters in the LUKS2 header]:boolean:(yes no)' \
+ '--fido2-credential-algorithm=[Specify COSE algorithm for FIDO2 credential]:algorithm:(es256 rs256 eddsa)' \
+ '--fido2-with-client-pin=[Whether to require entering a PIN to unlock the volume]:boolean:(yes no)' \
+ '--fido2-with-user-presence=[Whether to require user presence to unlock the volume]:boolean:(yes no)' \
+ '--fido2-with-user-verification=[Whether to require user verification to unlock the volume]:boolean:(yes no)' \
+ "($enroll)--tpm2-device=[Enroll a TPM2 device or list them]:TPM2 device:_systemd-cryptenroll_tpm2_device auto list" \
+ "($enroll)--tpm2-device-key=[Enroll a TPM2 device using its public key]:public key file:_files" \
+ '--tpm2-seal-key-handle=[Specify handle of key to use for sealing]:handle:' \
+ '--tpm2-pcrs=[Specify TPM2 PCRs to seal against]:PCRs:' \
+ '--tpm2-public-key=[Enroll signed TPM2 PCR policy against PEM public key]:public key file:_files' \
+ '--tpm2-public-key-pcrs=[Enroll signed TPM2 PCR policy for specified TPM2 PCRs]:PCRs:' \
+ '--tpm2-signature=[Validate public key enrollment works with JSON signature file]:signature file:_files' \
+ '--tpm2-pcrlock=[Specify pcrlock policy to lock against]:pcrlock file:_files' \
+ '--tpm2-with-pin=[Whether to require entering a PIN to unlock the volume]:boolean:(yes no)' \
+ '*::block device:_systemd-cryptenroll_devices'
diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build
index f10ba7be61..edad8bcb32 100644
--- a/shell-completion/zsh/meson.build
+++ b/shell-completion/zsh/meson.build
@@ -36,6 +36,7 @@ foreach item : [
['_storagectl', ''],
['_systemd', ''],
['_systemd-analyze', ''],
+ ['_systemd-cryptenroll', 'HAVE_LIBCRYPTSETUP'],
['_systemd-delta', ''],
['_systemd-hwdb', 'ENABLE_HWDB'],
['_systemd-id128', ''],
diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c
index 600207e947..b315282df1 100644
--- a/src/cryptenroll/cryptenroll-fido2.c
+++ b/src/cryptenroll/cryptenroll-fido2.c
@@ -3,6 +3,7 @@
#include "alloc-util.h"
#include "ask-password-api.h"
#include "cryptenroll-fido2.h"
+#include "cryptenroll-varlink.h"
#include "cryptsetup-fido2.h"
#include "cryptsetup-util.h"
#include "fido2-util.h"
@@ -15,11 +16,9 @@
#include "string-util.h"
int load_volume_key_fido2(
+ const EnrollContext *c,
struct crypt_device *cd,
- const char *cd_node,
- const char *device,
- void *ret_vk,
- size_t *ret_vks) {
+ struct iovec *ret_vk) {
#if HAVE_LIBFIDO2
_cleanup_(erase_and_freep) void *decrypted_key = NULL;
@@ -28,19 +27,19 @@ int load_volume_key_fido2(
ssize_t passphrase_size;
int r;
+ assert_se(c);
+ assert_se(c->node);
assert_se(cd);
- assert_se(cd_node);
assert_se(ret_vk);
- assert_se(ret_vks);
r = acquire_fido2_key_auto(
cd,
- cd_node,
- cd_node,
- device,
+ c->node,
+ c->node,
+ c->unlock_fido2_device,
/* until= */ 0,
"cryptenroll.fido2-pin",
- ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED,
+ ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED|(c->interactive ? 0 : ASK_PASSWORD_HEADLESS),
&decrypted_key,
&decrypted_key_size);
if (r == -EAGAIN)
@@ -57,8 +56,8 @@ int load_volume_key_fido2(
r = sym_crypt_volume_key_get(
cd,
CRYPT_ANY_SLOT,
- ret_vk,
- ret_vks,
+ ret_vk->iov_base,
+ &ret_vk->iov_len,
passphrase,
passphrase_size);
if (r < 0)
@@ -71,13 +70,9 @@ int load_volume_key_fido2(
}
int enroll_fido2(
+ const EnrollContext *c,
struct crypt_device *cd,
- const struct iovec *volume_key,
- const char *device,
- Fido2EnrollFlags lock_with,
- int cred_alg,
- const char *salt_file,
- bool parameters_in_header) {
+ const struct iovec *volume_key) {
#if HAVE_LIBFIDO2
_cleanup_(iovec_done_erase) struct iovec salt = {};
@@ -88,20 +83,22 @@ int enroll_fido2(
size_t cid_size, secret_size;
_cleanup_free_ void *cid = NULL;
ssize_t base64_encoded_size;
+ Fido2EnrollFlags lock_with; /* receives the flags actually locked with, see below */
const char *node, *un;
int r, keyslot;
+ assert_se(c);
assert_se(cd);
assert_se(iovec_is_set(volume_key));
- assert_se(device);
+ assert_se(c->fido2_device);
assert_se(node = sym_crypt_get_device_name(cd));
un = strempty(sym_crypt_get_uuid(cd));
- if (salt_file)
+ if (c->fido2_salt_file)
r = fido2_read_salt_file(
- salt_file,
+ c->fido2_salt_file,
/* offset= */ UINT64_MAX,
/* client= */ "cryptenroll",
/* node= */ un,
@@ -111,8 +108,14 @@ int enroll_fido2(
if (r < 0)
return r;
+ /* If we are operating over a Varlink connection that requested progress reports, let the client
+ * know a token touch is imminent before we enter the (blocking) FIDO2 operations below. */
+ r = enroll_context_notify_state(c, "touch");
+ if (r < 0)
+ return r;
+
r = fido2_generate_hmac_hash(
- device,
+ c->fido2_device,
/* rp_id= */ "io.systemd.cryptsetup",
/* rp_name= */ "Encrypted Volume",
/* user_id= */ un, strlen(un), /* We pass the user ID and name as the same: the disk's UUID if we have it */
@@ -121,8 +124,10 @@ int enroll_fido2(
/* user_icon= */ NULL,
/* askpw_icon= */ "drive-harddisk",
/* askpw_credential= */ "cryptenroll.fido2-pin",
- lock_with,
- cred_alg,
+ c->interactive ? 0 : ASK_PASSWORD_HEADLESS,
+ c->fido2_pin,
+ c->fido2_lock_with,
+ c->fido2_cred_alg != 0 ? c->fido2_cred_alg : COSE_ES256,
&salt,
&cid, &cid_size,
&secret, &secret_size,
@@ -150,7 +155,7 @@ int enroll_fido2(
if (keyslot < 0)
return log_error_errno(keyslot, "Failed to add new FIDO2 key to %s: %m", node);
- if (parameters_in_header) {
+ if (c->fido2_parameters_in_header) {
if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
return log_oom();
diff --git a/src/cryptenroll/cryptenroll-fido2.h b/src/cryptenroll/cryptenroll-fido2.h
index 7d0c25c02a..c5d57f12db 100644
--- a/src/cryptenroll/cryptenroll-fido2.h
+++ b/src/cryptenroll/cryptenroll-fido2.h
@@ -1,7 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "cryptenroll.h"
#include "shared-forward.h"
-int load_volume_key_fido2(struct crypt_device *cd, const char *cd_node, const char *device, void *ret_vk, size_t *ret_vks);
-int enroll_fido2(struct crypt_device *cd, const struct iovec *volume_key, const char *device, Fido2EnrollFlags lock_with, int cred_alg, const char *salt_file, bool parameters_in_header);
+int load_volume_key_fido2(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk);
+int enroll_fido2(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key);
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-list.c b/src/cryptenroll/cryptenroll-list.c
index ab7637e616..21d768b1a8 100644
--- a/src/cryptenroll/cryptenroll-list.c
+++ b/src/cryptenroll/cryptenroll-list.c
@@ -11,21 +11,17 @@
#include "log.h"
#include "parse-util.h"
-struct keyslot_metadata {
- int slot;
- const char *type;
-};
-
-int list_enrolled(struct crypt_device *cd) {
- _cleanup_free_ struct keyslot_metadata *keyslot_metadata = NULL;
- _cleanup_(table_unrefp) Table *t = NULL;
- size_t n_keyslot_metadata = 0;
+int collect_enrolled_slots(struct crypt_device *cd, EnrolledSlot **ret, size_t *ret_n) {
+ _cleanup_free_ EnrolledSlot *slots = NULL;
+ size_t n_slots = 0;
int slot_max, r;
- TableCell *cell;
assert(cd);
+ assert(ret);
+ assert(ret_n);
- /* First step, find out all currently used slots */
+ /* First step, find out all currently used slots. A slot without an associated token is a bare
+ * passphrase slot, hence default to ENROLL_PASSWORD. */
assert_se((slot_max = sym_crypt_keyslot_max(CRYPT_LUKS2)) > 0);
for (int slot = 0; slot < slot_max; slot++) {
crypt_keyslot_info status;
@@ -34,11 +30,12 @@ int list_enrolled(struct crypt_device *cd) {
if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST))
continue;
- if (!GREEDY_REALLOC(keyslot_metadata, n_keyslot_metadata+1))
+ if (!GREEDY_REALLOC(slots, n_slots + 1))
return log_oom();
- keyslot_metadata[n_keyslot_metadata++] = (struct keyslot_metadata) {
+ slots[n_slots++] = (EnrolledSlot) {
.slot = slot,
+ .type = ENROLL_PASSWORD,
};
}
@@ -46,7 +43,6 @@ int list_enrolled(struct crypt_device *cd) {
* token they are assigned to */
for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
- const char *type;
sd_json_variant *w, *z;
EnrollType et;
@@ -64,11 +60,7 @@ int list_enrolled(struct crypt_device *cd) {
continue;
}
- et = luks2_token_type_from_string(sd_json_variant_string(w));
- if (et < 0)
- type = "other";
- else
- type = enroll_type_to_string(et);
+ et = luks2_token_type_from_string(sd_json_variant_string(w)); /* _ENROLL_TYPE_INVALID for unrecognized type */
w = sd_json_variant_by_key(v, "keyslots");
if (!w || !sd_json_variant_is_array(w)) {
@@ -90,19 +82,40 @@ int list_enrolled(struct crypt_device *cd) {
continue;
}
- for (size_t i = 0; i < n_keyslot_metadata; i++) {
- if ((unsigned) keyslot_metadata[i].slot != u)
+ FOREACH_ARRAY(s, slots, n_slots) {
+ if ((unsigned) s->slot != u)
continue;
- if (keyslot_metadata[i].type) /* Slot claimed multiple times? */
- keyslot_metadata[i].type = POINTER_MAX;
+ if (s->conflict) /* Already marked as claimed multiple times. */
+ break;
+
+ if (s->type != ENROLL_PASSWORD) /* Slot already claimed by another token? */
+ s->conflict = true;
else
- keyslot_metadata[i].type = type;
+ s->type = et;
}
}
}
- /* Finally, create a table out of it all */
+ *ret = TAKE_PTR(slots);
+ *ret_n = n_slots;
+ return 0;
+}
+
+int list_enrolled(struct crypt_device *cd) {
+ _cleanup_free_ EnrolledSlot *slots = NULL;
+ _cleanup_(table_unrefp) Table *t = NULL;
+ size_t n_slots;
+ int r;
+ TableCell *cell;
+
+ assert(cd);
+
+ r = collect_enrolled_slots(cd, &slots, &n_slots);
+ if (r < 0)
+ return r;
+
+ /* Create a table out of it all */
t = table_new("slot", "type");
if (!t)
return log_oom();
@@ -110,12 +123,20 @@ int list_enrolled(struct crypt_device *cd) {
assert_se(cell = table_get_cell(t, 0, 0));
(void) table_set_align_percent(t, cell, 100);
- for (size_t i = 0; i < n_keyslot_metadata; i++) {
+ FOREACH_ARRAY(s, slots, n_slots) {
+ const char *type;
+
+ if (s->conflict)
+ type = "conflict";
+ else if (s->type < 0)
+ type = "other"; /* token of unrecognized type */
+ else
+ type = enroll_type_to_string(s->type);
+
r = table_add_many(
t,
- TABLE_INT, keyslot_metadata[i].slot,
- TABLE_STRING, keyslot_metadata[i].type == POINTER_MAX ? "conflict" :
- keyslot_metadata[i].type ?: "password");
+ TABLE_INT, s->slot,
+ TABLE_STRING, type);
if (r < 0)
return table_log_add_error(r);
}
diff --git a/src/cryptenroll/cryptenroll-list.h b/src/cryptenroll/cryptenroll-list.h
index 9097bc42c1..3366336713 100644
--- a/src/cryptenroll/cryptenroll-list.h
+++ b/src/cryptenroll/cryptenroll-list.h
@@ -1,6 +1,19 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "cryptenroll.h"
#include "shared-forward.h"
+typedef struct EnrolledSlot {
+ int slot;
+ /* The token type associated with the slot. ENROLL_PASSWORD means a bare passphrase slot without
+ * any token. _ENROLL_TYPE_INVALID means either a token of an unrecognized type, or a slot claimed
+ * by more than one token (see 'conflict'). */
+ EnrollType type;
+ bool conflict;
+} EnrolledSlot;
+
+/* Enumerates the active keyslots and classifies each by token type. Returns a newly allocated array. */
+int collect_enrolled_slots(struct crypt_device *cd, EnrolledSlot **ret, size_t *ret_n);
+
int list_enrolled(struct crypt_device *cd);
diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c
index e189cce23a..22b64e3229 100644
--- a/src/cryptenroll/cryptenroll-password.c
+++ b/src/cryptenroll/cryptenroll-password.c
@@ -7,25 +7,101 @@
#include "env-util.h"
#include "errno-util.h"
#include "escape.h"
+#include "fileio.h"
#include "iovec-util.h"
#include "log.h"
#include "password-quality-util.h"
#include "string-util.h"
#include "strv.h"
-int load_volume_key_password(
+int load_volume_key_empty(
+ const EnrollContext *c,
struct crypt_device *cd,
- const char *cd_node,
- void *ret_vk,
- size_t *ret_vks) {
+ struct iovec *ret_vk) {
+
+ int r;
+
+ assert_se(c);
+ assert_se(cd);
+ assert_se(ret_vk);
+
+ r = sym_crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ ret_vk->iov_base,
+ &ret_vk->iov_len,
+ "",
+ 0);
+ if (r < 0)
+ return log_error_errno(r, "Provided empty password did not work: %m");
+
+ return r;
+}
+
+int load_volume_key_keyfile(
+ const EnrollContext *c,
+ struct crypt_device *cd,
+ struct iovec *ret_vk) {
+
+ _cleanup_(erase_and_freep) char *password = NULL;
+ size_t password_len;
+ int r;
+
+ assert_se(c);
+ assert_se(cd);
+ assert_se(ret_vk);
+
+ r = read_full_file_full(
+ AT_FDCWD,
+ c->unlock_keyfile,
+ UINT64_MAX,
+ 4U * U64_MB, /* safety net */
+ READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET|READ_FULL_FILE_FAIL_WHEN_LARGER,
+ NULL,
+ &password,
+ &password_len);
+ if (r < 0)
+ return log_error_errno(r, "Reading keyfile %s failed: %m", c->unlock_keyfile);
+
+ r = sym_crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ ret_vk->iov_base,
+ &ret_vk->iov_len,
+ password,
+ password_len);
+ if (r < 0)
+ return log_error_errno(r, "Unlocking via keyfile failed: %m");
+
+ return r;
+}
+
+int load_volume_key_password(
+ const EnrollContext *c,
+ struct crypt_device *cd,
+ struct iovec *ret_vk) {
_cleanup_(erase_and_freep) char *envpw = NULL;
int r;
+ assert_se(c);
+ assert_se(c->node);
assert_se(cd);
- assert_se(cd_node);
assert_se(ret_vk);
- assert_se(ret_vks);
+
+ if (c->unlock_password) {
+ r = sym_crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ ret_vk->iov_base,
+ &ret_vk->iov_len,
+ c->unlock_password,
+ strlen(c->unlock_password));
+ if (r < 0)
+ return log_error_errno(r, "Provided unlock password did not work: %m");
+
+ return r;
+ }
r = getenv_steal_erase("PASSWORD", &envpw);
if (r < 0)
@@ -34,22 +110,27 @@ int load_volume_key_password(
r = sym_crypt_volume_key_get(
cd,
CRYPT_ANY_SLOT,
- ret_vk,
- ret_vks,
+ ret_vk->iov_base,
+ &ret_vk->iov_len,
envpw,
strlen(envpw));
if (r < 0)
return log_error_errno(r, "Password from environment variable $PASSWORD did not work: %m");
} else {
AskPasswordFlags ask_password_flags = ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED;
+
+ if (!c->interactive)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOPKG),
+ "Password querying disabled via 'headless' option, but no password provided for disk %s.",
+ c->node);
_cleanup_free_ char *question = NULL, *id = NULL, *disk_path = NULL;
unsigned i = 5;
- question = strjoin("Please enter current passphrase for disk ", cd_node, ":");
+ question = strjoin("Please enter current passphrase for disk ", c->node, ":");
if (!question)
return log_oom();
- disk_path = cescape(cd_node);
+ disk_path = cescape(c->node);
if (!disk_path)
return log_oom();
@@ -84,8 +165,8 @@ int load_volume_key_password(
r = sym_crypt_volume_key_get(
cd,
CRYPT_ANY_SLOT,
- ret_vk,
- ret_vks,
+ ret_vk->iov_base,
+ &ret_vk->iov_len,
*p,
strlen(*p));
if (r >= 0)
@@ -103,6 +184,7 @@ int load_volume_key_password(
}
int enroll_password(
+ const EnrollContext *c,
struct crypt_device *cd,
const struct iovec *volume_key) {
@@ -111,18 +193,30 @@ int enroll_password(
const char *node;
int r, keyslot;
+ assert(c);
assert(cd);
assert(iovec_is_set(volume_key));
assert_se(node = sym_crypt_get_device_name(cd));
- r = getenv_steal_erase("NEWPASSWORD", &new_password);
- if (r < 0)
- return log_error_errno(r, "Failed to acquire password from environment: %m");
- if (r == 0) {
+ if (c->passphrase) {
+ new_password = memdup_suffix0(c->passphrase, c->passphrase_size);
+ if (!new_password)
+ return log_oom();
+ } else {
+ r = getenv_steal_erase("NEWPASSWORD", &new_password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password from environment: %m");
+ }
+
+ if (!new_password) {
_cleanup_free_ char *disk_path = NULL, *id = NULL;
unsigned i = 5;
+ if (!c->interactive)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOPKG),
+ "Password querying disabled via 'headless' option, but no new password provided.");
+
assert_se(node = sym_crypt_get_device_name(cd));
(void) suggest_passwords();
diff --git a/src/cryptenroll/cryptenroll-password.h b/src/cryptenroll/cryptenroll-password.h
index f0c97ced29..035a1becc5 100644
--- a/src/cryptenroll/cryptenroll-password.h
+++ b/src/cryptenroll/cryptenroll-password.h
@@ -1,7 +1,11 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "cryptenroll.h"
#include "shared-forward.h"
-int load_volume_key_password(struct crypt_device *cd, const char* cd_node, void *ret_vk, size_t *ret_vks);
-int enroll_password(struct crypt_device *cd, const struct iovec *volume_key);
+int load_volume_key_empty(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk);
+int load_volume_key_keyfile(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk);
+
+int load_volume_key_password(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk);
+int enroll_password(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key);
diff --git a/src/cryptenroll/cryptenroll-pkcs11.c b/src/cryptenroll/cryptenroll-pkcs11.c
index 13feda47c4..c46fcca10a 100644
--- a/src/cryptenroll/cryptenroll-pkcs11.c
+++ b/src/cryptenroll/cryptenroll-pkcs11.c
@@ -36,7 +36,7 @@ static int uri_set_private_class(const char *uri, char **ret_uri) {
}
#endif
-int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key,const char *uri) {
+int enroll_pkcs11(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key) {
#if HAVE_P11KIT && HAVE_OPENSSL
_cleanup_(erase_and_freep) void *decrypted_key = NULL;
_cleanup_(erase_and_freep) char *base64_encoded = NULL;
@@ -50,18 +50,19 @@ int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key,const
const char *node;
int r;
+ assert_se(c);
assert_se(cd);
assert_se(iovec_is_set(volume_key));
- assert_se(uri);
+ assert_se(c->pkcs11_token_uri);
assert_se(node = sym_crypt_get_device_name(cd));
r = pkcs11_acquire_public_key(
- uri,
+ c->pkcs11_token_uri,
"volume enrollment operation",
"drive-harddisk",
"cryptenroll.pkcs11-pin",
- /* askpw_flags= */ 0,
+ c->interactive ? 0 : ASK_PASSWORD_HEADLESS,
&pkey,
&rsa_padding,
/* ret_pin_used= */ NULL);
@@ -100,14 +101,14 @@ int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key,const
/* Change 'type=cert' or 'type=public' in the provided URI to 'type=private' before storing in
a LUKS2 header. This allows users to use output of some PKCS#11 tools directly without
modifications. */
- r = uri_set_private_class(uri, &private_uri);
+ r = uri_set_private_class(c->pkcs11_token_uri, &private_uri);
if (r < 0)
return r;
r = sd_json_buildo(&v,
SD_JSON_BUILD_PAIR("type", JSON_BUILD_CONST_STRING("systemd-pkcs11")),
SD_JSON_BUILD_PAIR("keyslots", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_STRING(keyslot_as_string))),
- SD_JSON_BUILD_PAIR_STRING("pkcs11-uri", private_uri ?: uri),
+ SD_JSON_BUILD_PAIR_STRING("pkcs11-uri", private_uri ?: c->pkcs11_token_uri),
SD_JSON_BUILD_PAIR_BASE64("pkcs11-key", saved_key, saved_key_size),
SD_JSON_BUILD_PAIR_CONDITION(rsa_padding > PKCS11_RSA_PADDING_PKCS1V15,
"pkcs11-padding", SD_JSON_BUILD_STRING(pkcs11_rsa_padding_to_string(rsa_padding))));
diff --git a/src/cryptenroll/cryptenroll-pkcs11.h b/src/cryptenroll/cryptenroll-pkcs11.h
index b3e6e4584a..614c6d466c 100644
--- a/src/cryptenroll/cryptenroll-pkcs11.h
+++ b/src/cryptenroll/cryptenroll-pkcs11.h
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "cryptenroll.h"
#include "shared-forward.h"
-int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key, const char *uri);
+int enroll_pkcs11(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key);
diff --git a/src/cryptenroll/cryptenroll-recovery.c b/src/cryptenroll/cryptenroll-recovery.c
index 85ec13f128..da7b7a1018 100644
--- a/src/cryptenroll/cryptenroll-recovery.c
+++ b/src/cryptenroll/cryptenroll-recovery.c
@@ -12,8 +12,10 @@
#include "recovery-key.h"
int enroll_recovery(
+ const EnrollContext *c,
struct crypt_device *cd,
- const struct iovec *volume_key) {
+ const struct iovec *volume_key,
+ char **ret_recovery_key) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_(erase_and_freep) char *password = NULL;
@@ -21,6 +23,7 @@ int enroll_recovery(
int keyslot, r, q;
const char *node;
+ assert_se(c);
assert_se(cd);
assert_se(iovec_is_set(volume_key));
@@ -44,31 +47,33 @@ int enroll_recovery(
if (keyslot < 0)
return log_error_errno(keyslot, "Failed to add new recovery key to %s: %m", node);
- fflush(stdout);
- fprintf(stderr,
- "A secret recovery key has been generated for this volume:\n\n"
- " %s%s%s",
- emoji_enabled() ? glyph(GLYPH_LOCK_AND_KEY) : "",
- emoji_enabled() ? " " : "",
- ansi_highlight());
- fflush(stderr);
+ if (c->interactive) {
+ fflush(stdout);
+ fprintf(stderr,
+ "A secret recovery key has been generated for this volume:\n\n"
+ " %s%s%s",
+ emoji_enabled() ? glyph(GLYPH_LOCK_AND_KEY) : "",
+ emoji_enabled() ? " " : "",
+ ansi_highlight());
+ fflush(stderr);
- fputs(password, stdout);
- fflush(stdout);
+ fputs(password, stdout);
+ fflush(stdout);
- fputs(ansi_normal(), stderr);
- fflush(stderr);
+ fputs(ansi_normal(), stderr);
+ fflush(stderr);
- fputc('\n', stdout);
- fflush(stdout);
+ fputc('\n', stdout);
+ fflush(stdout);
- fputs("\nPlease save this secret recovery key at a secure location. It may be used to\n"
- "regain access to the volume if the other configured access credentials have\n"
- "been lost or forgotten. The recovery key may be entered in place of a password\n"
- "whenever authentication is requested.\n", stderr);
- fflush(stderr);
+ fputs("\nPlease save this secret recovery key at a secure location. It may be used to\n"
+ "regain access to the volume if the other configured access credentials have\n"
+ "been lost or forgotten. The recovery key may be entered in place of a password\n"
+ "whenever authentication is requested.\n", stderr);
+ fflush(stderr);
- (void) print_qrcode(stderr, "Optionally scan the recovery key for safekeeping", password);
+ (void) print_qrcode(stderr, "Optionally scan the recovery key for safekeeping", password);
+ }
if (asprintf(&keyslot_as_string, "%i", keyslot) < 0) {
r = log_oom();
@@ -89,7 +94,12 @@ int enroll_recovery(
goto rollback;
}
- log_info("New recovery key enrolled as key slot %i.", keyslot);
+ if (c->interactive)
+ log_info("New recovery key enrolled as key slot %i.", keyslot);
+
+ if (ret_recovery_key)
+ *ret_recovery_key = TAKE_PTR(password);
+
return keyslot;
rollback:
diff --git a/src/cryptenroll/cryptenroll-recovery.h b/src/cryptenroll/cryptenroll-recovery.h
index 0c76d86108..1655f71445 100644
--- a/src/cryptenroll/cryptenroll-recovery.h
+++ b/src/cryptenroll/cryptenroll-recovery.h
@@ -1,6 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "cryptenroll.h"
#include "shared-forward.h"
-int enroll_recovery(struct crypt_device *cd, const struct iovec *volume_key);
+/* When c->interactive is unset, the generated recovery key is returned via ret_recovery_key
+ * instead of being printed to stdout/stderr + rendered as a QR code. */
+int enroll_recovery(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key, char **ret_recovery_key);
diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c
index 2f6c7ecd72..eb08abdec6 100644
--- a/src/cryptenroll/cryptenroll-tpm2.c
+++ b/src/cryptenroll/cryptenroll-tpm2.c
@@ -169,11 +169,9 @@ static int get_pin(char **ret_pin_str, TPM2Flags *ret_flags) {
#endif
int load_volume_key_tpm2(
+ const EnrollContext *c,
struct crypt_device *cd,
- const char *cd_node,
- const char *device,
- void *ret_vk,
- size_t *ret_vks) {
+ struct iovec *ret_vk) {
#if HAVE_TPM2
_cleanup_(iovec_done_erase) struct iovec decrypted_key = {};
@@ -181,10 +179,10 @@ int load_volume_key_tpm2(
ssize_t passphrase_size;
int r;
+ assert_se(c);
+ assert_se(c->node);
assert_se(cd);
- assert_se(cd_node);
assert_se(ret_vk);
- assert_se(ret_vks);
bool found_some = false;
int token = 0; /* first token to look at */
@@ -235,8 +233,8 @@ int load_volume_key_tpm2(
found_some = true;
r = acquire_tpm2_key(
- cd_node,
- device,
+ c->node,
+ c->unlock_tpm2_device,
hash_pcr_mask,
pcr_bank,
&pubkey,
@@ -255,7 +253,7 @@ int load_volume_key_tpm2(
tpm2_flags,
/* until= */ 0,
"cryptenroll.tpm2-pin",
- /* askpw_flags= */ 0,
+ c->interactive ? 0 : ASK_PASSWORD_HEADLESS,
&decrypted_key);
if (IN_SET(r, -EACCES, -ENOLCK))
return log_notice_errno(SYNTHETIC_ERRNO(EAGAIN), "TPM2 PIN unlock failed");
@@ -275,8 +273,8 @@ int load_volume_key_tpm2(
r = sym_crypt_volume_key_get(
cd,
CRYPT_ANY_SLOT,
- ret_vk,
- ret_vks,
+ ret_vk->iov_base,
+ &ret_vk->iov_len,
passphrase,
passphrase_size);
if (r < 0)
@@ -288,19 +286,9 @@ int load_volume_key_tpm2(
#endif
}
-int enroll_tpm2(struct crypt_device *cd,
+int enroll_tpm2(const EnrollContext *c,
+ struct crypt_device *cd,
const struct iovec *volume_key,
- const char *device,
- uint32_t seal_key_handle,
- const char *device_key,
- Tpm2PCRValue *hash_pcr_values,
- size_t n_hash_pcr_values,
- const char *pcr_pubkey_path,
- bool load_pcr_pubkey,
- uint32_t pubkey_pcr_mask,
- const char *signature_path,
- bool use_pin,
- const char *pcrlock_path,
int *ret_slot_to_wipe) {
#if HAVE_TPM2
@@ -314,6 +302,8 @@ int enroll_tpm2(struct crypt_device *cd,
int r, keyslot, slot_to_wipe = -1;
TPM2Flags flags = 0;
uint16_t primary_alg = 0;
+ /* Mutable copy: cleared on the no-public-key fallback paths below. */
+ uint32_t pubkey_pcr_mask = c->tpm2_public_key_pcr_mask;
uint8_t binary_salt[SHA256_DIGEST_SIZE] = {};
/*
* erase the salt, we'd rather attempt to not have this in a coredump
@@ -323,15 +313,16 @@ int enroll_tpm2(struct crypt_device *cd,
*/
CLEANUP_ERASE(binary_salt);
+ assert(c);
assert(cd);
assert(iovec_is_set(volume_key));
- assert(tpm2_pcr_values_valid(hash_pcr_values, n_hash_pcr_values));
- assert(TPM2_PCR_MASK_VALID(pubkey_pcr_mask));
+ assert(tpm2_pcr_values_valid(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values));
+ assert(TPM2_PCR_MASK_VALID(c->tpm2_public_key_pcr_mask));
assert(ret_slot_to_wipe);
assert_se(node = sym_crypt_get_device_name(cd));
- if (use_pin) {
+ if (c->tpm2_pin) {
r = get_pin(&pin_str, &flags);
if (r < 0)
return r;
@@ -354,10 +345,10 @@ int enroll_tpm2(struct crypt_device *cd,
}
TPM2B_PUBLIC public = {};
- if (pcr_pubkey_path || load_pcr_pubkey) {
- r = tpm2_load_pcr_public_key(pcr_pubkey_path, &pubkey.iov_base, &pubkey.iov_len);
+ if (c->tpm2_public_key || c->tpm2_load_public_key) {
+ r = tpm2_load_pcr_public_key(c->tpm2_public_key, &pubkey.iov_base, &pubkey.iov_len);
if (r < 0) {
- if (pcr_pubkey_path || signature_path || r != -ENOENT)
+ if (c->tpm2_public_key || c->tpm2_signature || r != -ENOENT)
return log_error_errno(r, "Failed to read TPM PCR public key: %m");
log_debug_errno(r, "Failed to read TPM2 PCR public key, proceeding without: %m");
@@ -367,11 +358,11 @@ int enroll_tpm2(struct crypt_device *cd,
if (r < 0)
return log_error_errno(r, "Could not convert public key to TPM2B_PUBLIC: %m");
- if (signature_path) {
+ if (c->tpm2_signature) {
/* Also try to load the signature JSON object, to verify that our enrollment will work.
* This is optional however, skip it if it's not explicitly provided. */
- r = tpm2_load_pcr_signature(signature_path, &signature_json);
+ r = tpm2_load_pcr_signature(c->tpm2_signature, &signature_json);
if (r < 0)
return log_error_errno(r, "Failed to read TPM PCR signature: %m");
}
@@ -379,15 +370,15 @@ int enroll_tpm2(struct crypt_device *cd,
} else
pubkey_pcr_mask = 0;
- bool any_pcr_value_specified = tpm2_pcr_values_has_any_values(hash_pcr_values, n_hash_pcr_values);
+ bool any_pcr_value_specified = tpm2_pcr_values_has_any_values(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values);
_cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy pcrlock_policy = {};
- if (pcrlock_path) {
- r = tpm2_pcrlock_policy_load(pcrlock_path, &pcrlock_policy);
+ if (c->tpm2_pcrlock) {
+ r = tpm2_pcrlock_policy_load(c->tpm2_pcrlock, &pcrlock_policy);
if (r < 0)
return r;
if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Couldn't find pcrlock policy %s.", pcrlock_path);
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Couldn't find pcrlock policy %s.", c->tpm2_pcrlock);
any_pcr_value_specified = true;
flags |= TPM2_FLAGS_USE_PCRLOCK;
@@ -395,23 +386,23 @@ int enroll_tpm2(struct crypt_device *cd,
_cleanup_(tpm2_context_unrefp) Tpm2Context *tpm2_context = NULL;
TPM2B_PUBLIC device_key_public = {};
- if (device_key) {
- r = tpm2_load_public_key_file(device_key, &device_key_public);
+ if (c->tpm2_device_key) {
+ r = tpm2_load_public_key_file(c->tpm2_device_key, &device_key_public);
if (r < 0)
return r;
- if (!tpm2_pcr_values_has_all_values(hash_pcr_values, n_hash_pcr_values))
+ if (!tpm2_pcr_values_has_all_values(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Must provide all PCR values when using TPM2 device key.");
primary_alg = device_key_public.publicArea.type;
} else {
- r = tpm2_context_new_or_warn(device, &tpm2_context);
+ r = tpm2_context_new_or_warn(c->tpm2_device, &tpm2_context);
if (r < 0)
return r;
- if (!tpm2_pcr_values_has_all_values(hash_pcr_values, n_hash_pcr_values)) {
- r = tpm2_pcr_read_missing_values(tpm2_context, hash_pcr_values, n_hash_pcr_values);
+ if (!tpm2_pcr_values_has_all_values(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values)) {
+ r = tpm2_pcr_read_missing_values(tpm2_context, c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values);
if (r < 0)
return log_error_errno(r, "Could not read pcr values: %m");
}
@@ -420,10 +411,10 @@ int enroll_tpm2(struct crypt_device *cd,
uint16_t hash_pcr_bank = 0;
uint32_t hash_pcr_mask = 0;
- if (n_hash_pcr_values > 0) {
+ if (c->tpm2_n_hash_pcr_values > 0) {
size_t hash_count;
- r = tpm2_pcr_values_hash_count(hash_pcr_values, n_hash_pcr_values, &hash_count);
+ r = tpm2_pcr_values_hash_count(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values, &hash_count);
if (r < 0)
return log_error_errno(r, "Could not get hash count: %m");
@@ -431,12 +422,12 @@ int enroll_tpm2(struct crypt_device *cd,
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple PCR banks selected.");
/* If we use a literal PCR value policy, derive the bank to use from the algorithm specified on the hash values */
- hash_pcr_bank = hash_pcr_values[0].hash;
- r = tpm2_pcr_values_to_mask(hash_pcr_values, n_hash_pcr_values, hash_pcr_bank, &hash_pcr_mask);
+ hash_pcr_bank = c->tpm2_hash_pcr_values[0].hash;
+ r = tpm2_pcr_values_to_mask(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values, hash_pcr_bank, &hash_pcr_mask);
if (r < 0)
return log_error_errno(r, "Could not get hash mask: %m");
- } else if (pubkey_pcr_mask != 0 && !device_key) {
+ } else if (pubkey_pcr_mask != 0 && !c->tpm2_device_key) {
/* If no literal PCR value policy is used, then let's determine the mask to use automatically
* from the measurements of the TPM. */
@@ -461,21 +452,21 @@ int enroll_tpm2(struct crypt_device *cd,
/* If both PCR public key unlock and pcrlock unlock is selected, then we create the one for PCR public key unlock first. */
r = tpm2_calculate_sealing_policy(
- hash_pcr_values,
- n_hash_pcr_values,
+ c->tpm2_hash_pcr_values,
+ c->tpm2_n_hash_pcr_values,
iovec_is_set(&pubkey) ? &public : NULL,
- use_pin,
- pcrlock_path && !iovec_is_set(&pubkey) ? &pcrlock_policy : NULL,
+ c->tpm2_pin,
+ c->tpm2_pcrlock && !iovec_is_set(&pubkey) ? &pcrlock_policy : NULL,
policy_hash + 0);
if (r < 0)
return r;
- if (pcrlock_path && iovec_is_set(&pubkey)) {
+ if (c->tpm2_pcrlock && iovec_is_set(&pubkey)) {
r = tpm2_calculate_sealing_policy(
- hash_pcr_values,
- n_hash_pcr_values,
+ c->tpm2_hash_pcr_values,
+ c->tpm2_n_hash_pcr_values,
/* public= */ NULL, /* This one is off now */
- use_pin,
+ c->tpm2_pin,
&pcrlock_policy, /* And this one on instead. */
policy_hash + 1);
if (r < 0)
@@ -488,7 +479,7 @@ int enroll_tpm2(struct crypt_device *cd,
size_t n_blobs = 0;
CLEANUP_ARRAY(blobs, n_blobs, iovec_array_free);
- if (device_key) {
+ if (c->tpm2_device_key) {
if (n_policy_hash > 1)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Combined signed PCR policies and pcrlock policies cannot be calculated offline, currently.");
@@ -500,7 +491,7 @@ int enroll_tpm2(struct crypt_device *cd,
n_blobs = 1;
r = tpm2_calculate_seal(
- seal_key_handle,
+ c->tpm2_seal_key_handle,
&device_key_public,
/* attributes= */ NULL,
/* secret= */ NULL,
@@ -511,7 +502,7 @@ int enroll_tpm2(struct crypt_device *cd,
&srk);
} else
r = tpm2_seal(tpm2_context,
- seal_key_handle,
+ c->tpm2_seal_key_handle,
policy_hash,
n_policy_hash,
pin_str,
@@ -534,7 +525,7 @@ int enroll_tpm2(struct crypt_device *cd,
log_debug_errno(r, "PCR policy hash not yet enrolled, enrolling now.");
else if (r < 0)
return r;
- else if (use_pin) {
+ else if (c->tpm2_pin) {
log_debug("This PCR set is already enrolled, re-enrolling anyway to update PIN.");
slot_to_wipe = r;
} else {
@@ -544,7 +535,7 @@ int enroll_tpm2(struct crypt_device *cd,
}
/* If possible, verify the sealed data object. */
- if ((!iovec_is_set(&pubkey) || signature_json) && !any_pcr_value_specified && !device_key) {
+ if ((!iovec_is_set(&pubkey) || signature_json) && !any_pcr_value_specified && !c->tpm2_device_key) {
_cleanup_(iovec_done_erase) struct iovec secret2 = {};
log_debug("Unsealing for verification...");
@@ -555,7 +546,7 @@ int enroll_tpm2(struct crypt_device *cd,
pubkey_pcr_mask,
signature_json,
pin_str,
- pcrlock_path ? &pcrlock_policy : NULL,
+ c->tpm2_pcrlock ? &pcrlock_policy : NULL,
primary_alg,
blobs,
n_blobs,
@@ -600,9 +591,9 @@ int enroll_tpm2(struct crypt_device *cd,
n_blobs,
policy_hash_as_iovec,
n_policy_hash,
- use_pin ? &IOVEC_MAKE(binary_salt, sizeof(binary_salt)) : NULL,
+ c->tpm2_pin ? &IOVEC_MAKE(binary_salt, sizeof(binary_salt)) : NULL,
&srk,
- pcrlock_path ? &pcrlock_policy.nv_handle : NULL,
+ c->tpm2_pcrlock ? &pcrlock_policy.nv_handle : NULL,
flags,
&v);
if (r < 0)
diff --git a/src/cryptenroll/cryptenroll-tpm2.h b/src/cryptenroll/cryptenroll-tpm2.h
index 48f6c12b7f..3d57b8f8f3 100644
--- a/src/cryptenroll/cryptenroll-tpm2.h
+++ b/src/cryptenroll/cryptenroll-tpm2.h
@@ -1,7 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "cryptenroll.h"
#include "shared-forward.h"
-int load_volume_key_tpm2(struct crypt_device *cd, const char *cd_node, const char *device, void *ret_vk, size_t *ret_vks);
-int enroll_tpm2(struct crypt_device *cd, const struct iovec *volume_key, const char *device, uint32_t seal_key_handle, const char *device_key, Tpm2PCRValue *hash_pcr_values, size_t n_hash_pcr_values, const char *pubkey_path, bool load_pcr_pubkey, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin, const char *pcrlock_path, int *ret_slot_to_wipe);
+int load_volume_key_tpm2(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk);
+int enroll_tpm2(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key, int *ret_slot_to_wipe);
diff --git a/src/cryptenroll/cryptenroll-varlink.c b/src/cryptenroll/cryptenroll-varlink.c
new file mode 100644
index 0000000000..77ead09673
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-varlink.c
@@ -0,0 +1,481 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-varlink.h"
+
+#include "alloc-util.h"
+#include "bus-polkit.h"
+#include "cryptenroll.h"
+#include "cryptenroll-list.h"
+#include "cryptenroll-varlink.h"
+#include "cryptenroll-wipe.h"
+#include "cryptsetup-util.h"
+#include "fd-util.h"
+#include "hashmap.h"
+#include "iovec-util.h"
+#include "json-util.h"
+#include "libfido2-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "varlink-io.systemd.CryptEnroll.h"
+#include "varlink-util.h"
+
+int enroll_context_notify_state(const EnrollContext *c, const char *state) {
+ int r;
+
+ assert(c);
+ assert(state);
+
+ /* Only relevant when invoked over a Varlink connection that asked for progress ('more'). */
+ if (!c->link)
+ return 0;
+
+ r = sd_varlink_notifybo(c->link, SD_JSON_BUILD_PAIR_STRING("state", state));
+ if (r < 0)
+ return r;
+
+ return sd_varlink_flush(c->link);
+}
+
+static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_enroll_type, EnrollType, enroll_type_from_string);
+
+typedef struct MethodEnrollParameters {
+ char *node;
+ EnrollType mechanism;
+ char *unlock_password;
+ char *unlock_keyfile;
+ int64_t unlock_keyfile_fd_idx;
+ char *unlock_fido2_device;
+ char *unlock_tpm2_device;
+ char *password;
+ char *fido2_device;
+ char *fido2_pin;
+ int fido2_with_client_pin;
+ int fido2_with_user_presence;
+ int fido2_with_user_verification;
+ sd_json_variant *wipe_slots;
+ sd_json_variant *wipe_types;
+} MethodEnrollParameters;
+
+static void method_enroll_parameters_done(MethodEnrollParameters *p) {
+ assert(p);
+
+ free(p->node);
+ erase_and_free(p->unlock_password);
+ free(p->unlock_keyfile);
+ free(p->unlock_fido2_device);
+ free(p->unlock_tpm2_device);
+ erase_and_free(p->password);
+ free(p->fido2_device);
+ erase_and_free(p->fido2_pin);
+ sd_json_variant_unref(p->wipe_slots);
+ sd_json_variant_unref(p->wipe_types);
+}
+
+static int parse_wipe_slots(sd_json_variant *v, EnrollContext *c) {
+ sd_json_variant *e;
+
+ assert(c);
+
+ JSON_VARIANT_ARRAY_FOREACH(e, v) {
+ if (!sd_json_variant_is_unsigned(e))
+ return -EINVAL;
+
+ uint64_t u = sd_json_variant_unsigned(e);
+ if (u > INT_MAX)
+ return -ERANGE;
+
+ if (!GREEDY_REALLOC(c->wipe_slots, c->n_wipe_slots + 1))
+ return -ENOMEM;
+
+ c->wipe_slots[c->n_wipe_slots++] = (int) u;
+ }
+
+ return 0;
+}
+
+static int parse_wipe_types(sd_json_variant *v, EnrollContext *c) {
+ sd_json_variant *e;
+
+ assert(c);
+
+ JSON_VARIANT_ARRAY_FOREACH(e, v) {
+ if (!sd_json_variant_is_string(e))
+ return -EINVAL;
+
+ /* Translate the Varlink (underscore) spelling to the internal (dash) one before lookup. */
+ _cleanup_free_ char *s = strdup(sd_json_variant_string(e));
+ if (!s)
+ return -ENOMEM;
+
+ EnrollType t = enroll_type_from_string(json_dashify(s));
+ if (t < 0)
+ return -EINVAL;
+
+ c->wipe_slots_mask |= 1U << t;
+ }
+
+ return 0;
+}
+
+static int varlink_error_for_enroll(sd_varlink *link, int error) {
+ assert(link);
+ assert(error < 0);
+
+ /* Translates the errnos the enrollment/unlock helpers return into the interface's named errors,
+ * falling back to a plain errno error. */
+
+ switch (error) {
+
+ case -EHOSTDOWN: /* check_for_homed() */
+ return sd_varlink_error(link, "io.systemd.CryptEnroll.VolumeUnderForeignManagement", NULL);
+
+ case -ENOPKG: /* credential querying disabled in headless mode but none provided */
+ return sd_varlink_error(link, "io.systemd.CryptEnroll.PasswordRequired", NULL);
+
+ case -EPERM:
+ case -ENOKEY: /* provided password/key did not unlock the volume */
+ return sd_varlink_error(link, "io.systemd.CryptEnroll.PasswordIncorrect", NULL);
+
+ case -ENODEV:
+ case -ENOTUNIQ: /* no (or no unique) FIDO2 device found */
+ return sd_varlink_error(link, "io.systemd.CryptEnroll.FidoDeviceNotFound", NULL);
+
+ case -ENOSTR: /* FIDO_ERR_ACTION_TIMEOUT */
+ return sd_varlink_error(link, "io.systemd.CryptEnroll.FidoActionTimeout", NULL);
+
+ default:
+ return sd_varlink_error_errno(link, error);
+ }
+}
+
+static int vl_method_enroll(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "node", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(MethodEnrollParameters, node), SD_JSON_MANDATORY|SD_JSON_STRICT },
+ { "mechanism", SD_JSON_VARIANT_STRING, json_dispatch_enroll_type, offsetof(MethodEnrollParameters, mechanism), SD_JSON_MANDATORY },
+ { "unlockPassword", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, unlock_password), SD_JSON_NULLABLE },
+ { "unlockKeyFile", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(MethodEnrollParameters, unlock_keyfile), SD_JSON_NULLABLE|SD_JSON_STRICT },
+ { "unlockKeyFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_int64, offsetof(MethodEnrollParameters, unlock_keyfile_fd_idx), SD_JSON_NULLABLE },
+ { "unlockFido2Device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, unlock_fido2_device), SD_JSON_NULLABLE },
+ { "unlockTpm2Device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, unlock_tpm2_device), SD_JSON_NULLABLE },
+ { "password", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, password), SD_JSON_NULLABLE },
+ { "fido2Device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, fido2_device), SD_JSON_NULLABLE },
+ { "fido2Pin", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, fido2_pin), SD_JSON_NULLABLE },
+ { "fido2WithClientPin", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodEnrollParameters, fido2_with_client_pin), SD_JSON_NULLABLE },
+ { "fido2WithUserPresence", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodEnrollParameters, fido2_with_user_presence), SD_JSON_NULLABLE },
+ { "fido2WithUserVerification", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodEnrollParameters, fido2_with_user_verification), SD_JSON_NULLABLE },
+ { "wipeSlots", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, offsetof(MethodEnrollParameters, wipe_slots), SD_JSON_NULLABLE },
+ { "wipeTypes", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, offsetof(MethodEnrollParameters, wipe_types), SD_JSON_NULLABLE },
+ VARLINK_DISPATCH_POLKIT_FIELD,
+ {}
+ };
+
+ _cleanup_(method_enroll_parameters_done) MethodEnrollParameters p = {
+ .mechanism = _ENROLL_TYPE_INVALID,
+ .unlock_keyfile_fd_idx = -1,
+ .fido2_with_client_pin = -1,
+ .fido2_with_user_presence = -1,
+ .fido2_with_user_verification = -1,
+ };
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(iovec_done_erase) struct iovec vk = {};
+ _cleanup_close_ int keyfile_fd = -EBADF;
+ Hashmap **polkit_registry = ASSERT_PTR(userdata);
+ int slot, r;
+
+ assert(link);
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ if (p.mechanism < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "mechanism");
+
+ r = varlink_verify_polkit_async(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.cryptenroll.enroll",
+ /* details= */ NULL,
+ polkit_registry);
+ if (r <= 0)
+ return r;
+
+ /* Populate the context. This is the Varlink equivalent of enroll_context_from_args(). */
+ _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL;
+ c.interactive = false;
+ c.enroll_type = p.mechanism;
+ c.unlock_type = _UNLOCK_TYPE_INVALID;
+
+ if (strdup_to(&c.node, p.node) < 0)
+ return -ENOMEM;
+
+ if (p.unlock_password) {
+ if (c.unlock_type >= 0)
+ return sd_varlink_error_invalid_parameter_name(link, "unlockPassword");
+
+ c.unlock_password = TAKE_PTR(p.unlock_password);
+ c.unlock_type = UNLOCK_PASSWORD;
+ }
+
+ if (p.unlock_keyfile || p.unlock_keyfile_fd_idx >= 0) {
+ if (c.unlock_type >= 0)
+ return sd_varlink_error_invalid_parameter_name(link, p.unlock_keyfile ? "unlockKeyFile" : "unlockKeyFileDescriptor");
+
+ if (p.unlock_keyfile && p.unlock_keyfile_fd_idx >= 0)
+ return sd_varlink_error_invalid_parameter_name(link, "unlockKeyFileDescriptor");
+
+ if (p.unlock_keyfile_fd_idx >= 0) {
+ keyfile_fd = sd_varlink_peek_dup_fd(link, p.unlock_keyfile_fd_idx);
+ if (keyfile_fd < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "unlockKeyFileDescriptor");
+
+ if (asprintf(&c.unlock_keyfile, "/proc/self/fd/%i", keyfile_fd) < 0)
+ return -ENOMEM;
+ } else
+ c.unlock_keyfile = TAKE_PTR(p.unlock_keyfile);
+
+ c.unlock_type = UNLOCK_KEYFILE;
+ }
+
+ if (p.unlock_fido2_device) {
+ if (c.unlock_type >= 0)
+ return sd_varlink_error_invalid_parameter_name(link, "unlockFido2Device");
+
+ if (!streq(p.unlock_fido2_device, "auto")) {
+ if (!path_is_normalized(p.unlock_fido2_device) || !path_is_absolute(p.unlock_fido2_device))
+ return sd_varlink_error_invalid_parameter_name(link, "unlockFido2Device");
+
+ if (strdup_to(&c.unlock_fido2_device, p.unlock_fido2_device) < 0)
+ return -ENOMEM;
+ }
+
+ c.unlock_type = UNLOCK_FIDO2;
+ }
+
+ if (p.unlock_tpm2_device) {
+ if (c.unlock_type >= 0)
+ return sd_varlink_error_invalid_parameter_name(link, "unlockTpm2Device");
+
+ if (!streq(p.unlock_tpm2_device, "auto")) {
+ if (!path_is_normalized(p.unlock_tpm2_device) || !path_is_absolute(p.unlock_tpm2_device))
+ return sd_varlink_error_invalid_parameter_name(link, "unlockTpm2Device");
+
+ if (strdup_to(&c.unlock_tpm2_device, p.unlock_tpm2_device) < 0)
+ return -ENOMEM;
+ }
+
+ c.unlock_type = UNLOCK_TPM2;
+ }
+
+ /* If no unlock method is specified, return a recognizable error. We generate invalid parameter name
+ * for "unlockPassword", simply because it is the best-known unlock method */
+ if (c.unlock_type < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "unlockPassword");
+
+ /* Mechanism-specific parameters */
+ switch (c.enroll_type) {
+
+ case ENROLL_PASSWORD:
+ if (p.password) {
+ c.passphrase = TAKE_PTR(p.password);
+ c.passphrase_size = strlen(c.passphrase);
+ }
+ break;
+
+ case ENROLL_RECOVERY:
+ break;
+
+ case ENROLL_FIDO2:
+ /* enroll_fido2() requires a concrete device, so if none was given (NULL), discover one. */
+
+ if (streq_ptr(p.fido2_device, "auto"))
+ p.fido2_device = mfree(p.fido2_device);
+
+ if (p.fido2_device) {
+ if (!path_is_normalized(p.fido2_device) || !path_is_absolute(p.fido2_device))
+ return sd_varlink_error_invalid_parameter_name(link, "fido2Device");
+
+ r = strdup_to(&c.fido2_device, p.fido2_device);
+ if (r < 0)
+ return r;
+ }
+
+ if (!c.fido2_device) {
+ r = fido2_find_device_auto(&c.fido2_device);
+ if (r < 0)
+ return varlink_error_for_enroll(link, r);
+ }
+
+ if (p.fido2_with_client_pin >= 0)
+ SET_FLAG(c.fido2_lock_with, FIDO2ENROLL_PIN, p.fido2_with_client_pin);
+ if (p.fido2_with_user_presence >= 0)
+ SET_FLAG(c.fido2_lock_with, FIDO2ENROLL_UP, p.fido2_with_user_presence);
+ if (p.fido2_with_user_verification >= 0)
+ SET_FLAG(c.fido2_lock_with, FIDO2ENROLL_UV, p.fido2_with_user_verification);
+
+ c.fido2_pin = TAKE_PTR(p.fido2_pin);
+ break;
+
+ default:
+ return sd_varlink_error_invalid_parameter_name(link, "mechanism");
+ }
+
+ if (p.wipe_slots) {
+ r = parse_wipe_slots(p.wipe_slots, &c);
+ if (r == -ENOMEM)
+ return r;
+ if (r < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "wipeSlots");
+ }
+ if (p.wipe_types) {
+ r = parse_wipe_types(p.wipe_types, &c);
+ if (r == -ENOMEM)
+ return r;
+ if (r < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "wipeTypes");
+ }
+
+ /* If the caller asked for 'more', remember the link so we can send progress (e.g. FIDO2 touch). */
+ if (FLAGS_SET(flags, SD_VARLINK_METHOD_MORE))
+ c.link = sd_varlink_ref(link);
+
+ r = DLOPEN_CRYPTSETUP(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED);
+ if (r < 0)
+ return r;
+
+ r = prepare_luks(&c, &cd, &vk);
+ if (r < 0)
+ return varlink_error_for_enroll(link, r);
+
+ _cleanup_(erase_and_freep) char *recovery_key = NULL;
+ slot = enroll_now(&c, cd, &vk, &recovery_key);
+ if (slot < 0)
+ return varlink_error_for_enroll(link, slot);
+
+ /* Wipe any slots the caller selected, keeping the one we just enrolled. */
+ c.wipe_except_slot = slot;
+ _cleanup_free_ int *wiped_slots = NULL;
+ size_t n_wiped_slots = 0;
+ r = wipe_slots(&c, cd, &wiped_slots, &n_wiped_slots);
+ if (r < 0)
+ return varlink_error_for_enroll(link, r);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *wiped_slots_json = NULL;
+ FOREACH_ARRAY(s, wiped_slots, n_wiped_slots) {
+ r = sd_json_variant_append_arrayb(&wiped_slots_json, SD_JSON_BUILD_INTEGER(*s));
+ if (r < 0)
+ return r;
+ }
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *rk = NULL;
+ if (recovery_key) {
+ r = sd_json_variant_new_string(&rk, recovery_key);
+ if (r < 0)
+ return r;
+
+ sd_json_variant_sensitive(rk);
+ }
+
+ return sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR_INTEGER("slot", slot),
+ JSON_BUILD_PAIR_VARIANT_NON_NULL("wipedSlots", wiped_slots_json),
+ JSON_BUILD_PAIR_VARIANT_NON_NULL("recoveryKey", rk));
+}
+
+static int vl_method_list_slots(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "node", SD_JSON_VARIANT_STRING, json_dispatch_const_path, 0, SD_JSON_MANDATORY|SD_JSON_STRICT },
+ {}
+ };
+
+ _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL;
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_free_ EnrolledSlot *slots = NULL;
+ const char *node = NULL;
+ size_t n_slots;
+ int r;
+
+ assert(link);
+
+ if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE))
+ return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL);
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &node);
+ if (r != 0)
+ return r;
+
+ /* NB: No polkit authentication here for now, given this is read access only. */
+
+ c.interactive = false;
+ if (strdup_to(&c.node, node) < 0)
+ return -ENOMEM;
+
+ r = DLOPEN_CRYPTSETUP(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED);
+ if (r < 0)
+ return r;
+
+ /* We only need to read the LUKS2 header, no volume key required. */
+ r = prepare_luks(&c, &cd, /* ret_volume_key= */ NULL);
+ if (r < 0)
+ return varlink_error_for_enroll(link, r);
+
+ r = collect_enrolled_slots(cd, &slots, &n_slots);
+ if (r < 0)
+ return r;
+
+ FOREACH_ARRAY(s, slots, n_slots) {
+ /* enroll_type_to_string() returns NULL for unrecognized types; conflicts are reported
+ * with no type too. The dash spelling is underscorified for the wire by the build macro. */
+ const char *type = s->conflict ? NULL : enroll_type_to_string(s->type);
+
+ r = sd_varlink_notifybo(
+ link,
+ SD_JSON_BUILD_PAIR_INTEGER("slot", s->slot),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY_UNDERSCORIFY("type", type));
+ if (r < 0)
+ return r;
+ }
+
+ return sd_varlink_reply(link, NULL);
+}
+
+int cryptenroll_varlink_server(void) {
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
+ _cleanup_hashmap_free_ Hashmap *polkit_registry = NULL;
+ int r;
+
+ r = varlink_server_new(
+ &varlink_server,
+ SD_VARLINK_SERVER_ROOT_ONLY|SD_VARLINK_SERVER_MYSELF_ONLY|SD_VARLINK_SERVER_INPUT_SENSITIVE|SD_VARLINK_SERVER_INHERIT_USERDATA|SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT|SD_VARLINK_SERVER_HANDLE_SIGINT|SD_VARLINK_SERVER_HANDLE_SIGTERM,
+ &polkit_registry);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate Varlink server: %m");
+
+ r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_CryptEnroll);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add Varlink interface: %m");
+
+ r = sd_varlink_server_bind_method_many(
+ varlink_server,
+ "io.systemd.CryptEnroll.Enroll", vl_method_enroll,
+ "io.systemd.CryptEnroll.ListSlots", vl_method_list_slots);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind Varlink methods: %m");
+
+ r = sd_varlink_server_loop_auto(varlink_server);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run Varlink event loop: %m");
+
+ return 0;
+}
diff --git a/src/cryptenroll/cryptenroll-varlink.h b/src/cryptenroll/cryptenroll-varlink.h
new file mode 100644
index 0000000000..89efede84b
--- /dev/null
+++ b/src/cryptenroll/cryptenroll-varlink.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "cryptenroll.h"
+
+/* Sends a progress 'state' notification over c->link, if (and only if) the enrollment was triggered via a
+ * Varlink call with the 'more' flag set. A no-op (returning 0) otherwise. */
+int enroll_context_notify_state(const EnrollContext *c, const char *state);
+
+int cryptenroll_varlink_server(void);
diff --git a/src/cryptenroll/cryptenroll-wipe.c b/src/cryptenroll/cryptenroll-wipe.c
index a8638d2b3a..39e8593973 100644
--- a/src/cryptenroll/cryptenroll-wipe.c
+++ b/src/cryptenroll/cryptenroll-wipe.c
@@ -296,24 +296,29 @@ static bool slots_remain(struct crypt_device *cd, Set *wipe_slots, Set *keep_slo
return false;
}
-int wipe_slots(struct crypt_device *cd,
- const int explicit_slots[],
- size_t n_explicit_slots,
- WipeScope by_scope,
- unsigned by_mask,
- int except_slot) {
+int wipe_slots(const EnrollContext *c,
+ struct crypt_device *cd,
+ int **ret_wiped_slots,
+ size_t *ret_n_wiped_slots) {
_cleanup_set_free_ Set *wipe_slots = NULL, *wipe_tokens = NULL, *keep_slots = NULL;
- _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL;
- size_t n_ordered_slots = 0, n_ordered_tokens = 0;
+ _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL, *wiped_slots = NULL;
+ size_t n_ordered_slots = 0, n_ordered_tokens = 0, n_wiped_slots = 0;
int r, slot_max, ret;
void *e;
+ assert_se(c);
assert_se(cd);
+ assert_se(!!ret_wiped_slots == !!ret_n_wiped_slots);
/* Shortcut if nothing to wipe. */
- if (n_explicit_slots == 0 && by_mask == 0 && by_scope == WIPE_EXPLICIT)
+ if (c->n_wipe_slots == 0 && c->wipe_slots_mask == 0 && c->wipe_slots_scope == WIPE_EXPLICIT) {
+ if (ret_wiped_slots)
+ *ret_wiped_slots = NULL;
+ if (ret_n_wiped_slots)
+ *ret_n_wiped_slots = 0;
return 0;
+ }
/* So this is a bit more complicated than I'd wish, but we want support three different axis for wiping slots:
*
@@ -334,23 +339,23 @@ int wipe_slots(struct crypt_device *cd,
return log_oom();
/* Let's maintain one set of slots for the slots we definitely want to keep */
- if (except_slot >= 0)
- if (set_put(keep_slots, INT_TO_PTR(except_slot)) < 0)
+ if (c->wipe_except_slot >= 0)
+ if (set_put(keep_slots, INT_TO_PTR(c->wipe_except_slot)) < 0)
return log_oom();
assert_se((slot_max = sym_crypt_keyslot_max(CRYPT_LUKS2)) > 0);
/* Maintain another set of the slots we intend to wipe */
- for (size_t i = 0; i < n_explicit_slots; i++) {
- if (explicit_slots[i] >= slot_max)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Slot index %i out of range.", explicit_slots[i]);
+ for (size_t i = 0; i < c->n_wipe_slots; i++) {
+ if (c->wipe_slots[i] >= slot_max)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Slot index %i out of range.", c->wipe_slots[i]);
- if (set_put(wipe_slots, INT_TO_PTR(explicit_slots[i])) < 0)
+ if (set_put(wipe_slots, INT_TO_PTR(c->wipe_slots[i])) < 0)
return log_oom();
}
/* Now, handle the "all" and "empty passphrase" cases. */
- switch (by_scope) {
+ switch (c->wipe_slots_scope) {
case WIPE_EXPLICIT:
break; /* Nothing to do here */
@@ -373,7 +378,7 @@ int wipe_slots(struct crypt_device *cd,
}
/* Then add all slots that match a token type */
- r = find_slots_by_mask(cd, wipe_slots, keep_slots, by_mask);
+ r = find_slots_by_mask(cd, wipe_slots, keep_slots, c->wipe_slots_mask);
if (r < 0)
return r;
@@ -409,11 +414,22 @@ int wipe_slots(struct crypt_device *cd,
typesafe_qsort(ordered_tokens, n_ordered_tokens, cmp_int);
if (n_ordered_slots == 0 && n_ordered_tokens == 0) {
- log_full(except_slot < 0 ? LOG_NOTICE : LOG_DEBUG,
+ log_full(c->wipe_except_slot < 0 ? LOG_NOTICE : LOG_DEBUG,
"No slots to remove selected.");
+ if (ret_wiped_slots)
+ *ret_wiped_slots = NULL;
+ if (ret_n_wiped_slots)
+ *ret_n_wiped_slots = 0;
return 0;
}
+ /* Remember which slots we actually managed to wipe, so we can report them back to the caller. */
+ if (ret_wiped_slots) {
+ wiped_slots = new(int, n_ordered_slots);
+ if (!wiped_slots)
+ return log_oom();
+ }
+
if (DEBUG_LOGGING) {
for (size_t i = 0; i < n_ordered_slots; i++)
log_debug("Going to wipe slot %i.", ordered_slots[i]);
@@ -433,8 +449,11 @@ int wipe_slots(struct crypt_device *cd,
log_warning_errno(r, "Failed to wipe slot %i, continuing: %m", ordered_slots[i - 1]);
if (ret == 0)
ret = r;
- } else
+ } else {
log_info("Wiped slot %i.", ordered_slots[i - 1]);
+ if (wiped_slots)
+ wiped_slots[n_wiped_slots++] = ordered_slots[i - 1];
+ }
}
for (size_t i = n_ordered_tokens; i > 0; i--) {
@@ -446,5 +465,13 @@ int wipe_slots(struct crypt_device *cd,
}
}
+ if (ret_wiped_slots) {
+ /* We wiped from back to front, restore ascending order before handing the list out. */
+ typesafe_qsort(wiped_slots, n_wiped_slots, cmp_int);
+ *ret_wiped_slots = TAKE_PTR(wiped_slots);
+ }
+ if (ret_n_wiped_slots)
+ *ret_n_wiped_slots = n_wiped_slots;
+
return ret;
}
diff --git a/src/cryptenroll/cryptenroll-wipe.h b/src/cryptenroll/cryptenroll-wipe.h
index aa82f9dbbd..cb3dd71933 100644
--- a/src/cryptenroll/cryptenroll-wipe.h
+++ b/src/cryptenroll/cryptenroll-wipe.h
@@ -1,13 +1,11 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "cryptenroll.h"
#include "shared-forward.h"
-typedef enum WipeScope WipeScope;
-
-int wipe_slots(struct crypt_device *cd,
- const int explicit_slots[],
- size_t n_explicit_slots,
- WipeScope by_scope,
- unsigned by_mask,
- int except_slot);
+/* Wipes the slots selected by c->wipe_slots / c->n_wipe_slots / c->wipe_slots_scope /
+ * c->wipe_slots_mask, except for c->wipe_except_slot (set to -1 for none). If ret_wiped_slots/
+ * ret_n_wiped_slots are non-NULL they receive the (ascendingly sorted) list of slots that were actually
+ * wiped. */
+int wipe_slots(const EnrollContext *c, struct crypt_device *cd, int **ret_wiped_slots, size_t *ret_n_wiped_slots);
diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c
index eed0a11aeb..67386f31d8 100644
--- a/src/cryptenroll/cryptenroll.c
+++ b/src/cryptenroll/cryptenroll.c
@@ -3,34 +3,42 @@
#include
#include "sd-device.h"
+#include "sd-varlink.h"
+#include "alloc-util.h"
#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 "cryptenroll-recovery.h"
#include "cryptenroll-tpm2.h"
+#include "cryptenroll-varlink.h"
#include "cryptenroll-wipe.h"
#include "cryptsetup-util.h"
#include "extract-word.h"
-#include "fileio.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 "memory-util.h"
#include "options.h"
#include "pager.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"
@@ -60,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;
@@ -110,6 +122,91 @@ 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;
+
+ c->node = mfree(c->node);
+ c->unlock_keyfile = mfree(c->unlock_keyfile);
+ c->unlock_fido2_device = mfree(c->unlock_fido2_device);
+ c->unlock_tpm2_device = mfree(c->unlock_tpm2_device);
+ c->unlock_password = erase_and_free(c->unlock_password);
+ c->passphrase = erase_and_free(c->passphrase);
+ c->fido2_device = mfree(c->fido2_device);
+ c->fido2_salt_file = mfree(c->fido2_salt_file);
+ c->fido2_pin = erase_and_free(c->fido2_pin);
+ c->pkcs11_token_uri = mfree(c->pkcs11_token_uri);
+ c->tpm2_device = mfree(c->tpm2_device);
+ c->tpm2_device_key = mfree(c->tpm2_device_key);
+ c->tpm2_hash_pcr_values = mfree(c->tpm2_hash_pcr_values);
+ c->tpm2_public_key = mfree(c->tpm2_public_key);
+ c->tpm2_signature = mfree(c->tpm2_signature);
+ c->tpm2_pcrlock = mfree(c->tpm2_pcrlock);
+ c->wipe_slots = mfree(c->wipe_slots);
+ 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;
@@ -127,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) {
@@ -190,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;
@@ -222,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,
@@ -252,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;
}
@@ -305,8 +397,42 @@ 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"):
+ if (arg_unlock_type != UNLOCK_PASSWORD)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Multiple unlock methods specified at once, refusing.");
+
+ arg_unlock_type = UNLOCK_EMPTY;
+ break;
+
OPTION_LONG("unlock-key-file", "PATH",
"Use a file to unlock the volume"):
if (arg_unlock_type != UNLOCK_PASSWORD)
@@ -362,6 +488,14 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ OPTION_LONG("unlock-headless", NULL, "Try the 'headless' unlock mechanisms in turn"):
+ if (arg_unlock_type != UNLOCK_PASSWORD)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Multiple unlock methods specified at once, refusing.");
+
+ arg_unlock_type = UNLOCK_HEADLESS;
+ break;
+
OPTION_GROUP("Simple Enrollment"): {}
OPTION_LONG("password", NULL,
@@ -582,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);
@@ -660,54 +805,18 @@ static int check_for_homed(struct crypt_device *cd) {
return 0;
}
-static int load_volume_key_keyfile(
- struct crypt_device *cd,
- void *ret_vk,
- size_t *ret_vks) {
-
- _cleanup_(erase_and_freep) char *password = NULL;
- size_t password_len;
- int r;
-
- assert_se(cd);
- assert_se(ret_vk);
- assert_se(ret_vks);
-
- r = read_full_file_full(
- AT_FDCWD,
- arg_unlock_keyfile,
- UINT64_MAX,
- SIZE_MAX,
- READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET,
- NULL,
- &password,
- &password_len);
- if (r < 0)
- return log_error_errno(r, "Reading keyfile %s failed: %m", arg_unlock_keyfile);
-
- r = sym_crypt_volume_key_get(
- cd,
- CRYPT_ANY_SLOT,
- ret_vk,
- ret_vks,
- password,
- password_len);
- if (r < 0)
- return log_error_errno(r, "Unlocking via keyfile failed: %m");
-
- return r;
-}
-
-static int prepare_luks(
+int prepare_luks(
+ const EnrollContext *c,
struct crypt_device **ret_cd,
struct iovec *ret_volume_key) {
_cleanup_(crypt_freep) struct crypt_device *cd = NULL;
int r;
+ assert(c);
assert(ret_cd);
- r = sym_crypt_init(&cd, arg_node);
+ r = sym_crypt_init(&cd, c->node);
if (r < 0)
return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
@@ -715,7 +824,7 @@ static int prepare_luks(
r = sym_crypt_load(cd, CRYPT_LUKS2, NULL);
if (r < 0)
- return log_error_errno(r, "Failed to load LUKS2 superblock of %s: %m", arg_node);
+ return log_error_errno(r, "Failed to load LUKS2 superblock of %s: %m", c->node);
r = check_for_homed(cd);
if (r < 0)
@@ -731,35 +840,52 @@ static int prepare_luks(
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
_cleanup_(iovec_done_erase) struct iovec vk = {};
-
vk.iov_base = malloc(r);
if (!vk.iov_base)
return log_oom();
vk.iov_len = (size_t) r;
- switch (arg_unlock_type) {
+ switch (c->unlock_type) {
+
+ case UNLOCK_EMPTY:
+ r = load_volume_key_empty(c, cd, &vk);
+ break;
case UNLOCK_PASSWORD:
- r = load_volume_key_password(cd, arg_node, vk.iov_base, &vk.iov_len);
+ r = load_volume_key_password(c, cd, &vk);
break;
case UNLOCK_KEYFILE:
- r = load_volume_key_keyfile(cd, vk.iov_base, &vk.iov_len);
+ r = load_volume_key_keyfile(c, cd, &vk);
break;
case UNLOCK_FIDO2:
- r = load_volume_key_fido2(cd, arg_node, arg_unlock_fido2_device, vk.iov_base, &vk.iov_len);
+ r = load_volume_key_fido2(c, cd, &vk);
break;
case UNLOCK_TPM2:
- r = load_volume_key_tpm2(cd, arg_node, arg_unlock_tpm2_device, vk.iov_base, &vk.iov_len);
+ r = load_volume_key_tpm2(c, cd, &vk);
+ break;
+
+ case UNLOCK_HEADLESS:
+ if (tpm2_is_mostly_supported()) {
+ log_info("TPM2 support available, trying unlocking via TPM2…");
+
+ r = load_volume_key_tpm2(c, cd, &vk);
+ if (r >= 0)
+ break;
+
+ log_info("TPM2 unlocking didn't work, trying unlocking via empty password…");
+ } else
+ log_info("TPM2 support not available, trying unlocking via empty password…");
+
+ r = load_volume_key_empty(c, cd, &vk);
break;
default:
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown LUKS unlock method");
}
-
if (r < 0)
return r;
@@ -769,13 +895,133 @@ static int prepare_luks(
return 0;
}
+static int enroll_context_from_args(EnrollContext *c) {
+ assert(c);
+
+ /* Copies the parsed command line parameters from the static arg_* globals into a self-contained
+ * EnrollContext. The context owns its own copies of all strings/arrays, so it can be torn down
+ * independently of the arg_* destructors. */
+
+ *c = ENROLL_CONTEXT_NULL;
+
+ c->enroll_type = arg_enroll_type;
+ c->unlock_type = arg_unlock_type;
+ c->fido2_parameters_in_header = arg_fido2_parameters_in_header;
+ c->fido2_lock_with = arg_fido2_lock_with;
+ c->fido2_cred_alg = arg_fido2_cred_alg;
+ c->tpm2_seal_key_handle = arg_tpm2_seal_key_handle;
+ c->tpm2_pin = arg_tpm2_pin;
+ c->tpm2_load_public_key = arg_tpm2_load_public_key;
+ c->tpm2_public_key_pcr_mask = arg_tpm2_public_key_pcr_mask;
+ c->wipe_slots_scope = arg_wipe_slots_scope;
+ c->wipe_slots_mask = arg_wipe_slots_mask;
+
+ if (strdup_to(&c->node, arg_node) < 0 ||
+ strdup_to(&c->unlock_keyfile, arg_unlock_keyfile) < 0 ||
+ strdup_to(&c->unlock_fido2_device, arg_unlock_fido2_device) < 0 ||
+ strdup_to(&c->unlock_tpm2_device, arg_unlock_tpm2_device) < 0 ||
+ strdup_to(&c->fido2_device, arg_fido2_device) < 0 ||
+ strdup_to(&c->fido2_salt_file, arg_fido2_salt_file) < 0 ||
+ strdup_to(&c->pkcs11_token_uri, arg_pkcs11_token_uri) < 0 ||
+ strdup_to(&c->tpm2_device, arg_tpm2_device) < 0 ||
+ strdup_to(&c->tpm2_device_key, arg_tpm2_device_key) < 0 ||
+ strdup_to(&c->tpm2_public_key, arg_tpm2_public_key) < 0 ||
+ strdup_to(&c->tpm2_signature, arg_tpm2_signature) < 0 ||
+ strdup_to(&c->tpm2_pcrlock, arg_tpm2_pcrlock) < 0)
+ return log_oom();
+
+ if (arg_n_wipe_slots > 0) {
+ c->wipe_slots = newdup(int, arg_wipe_slots, arg_n_wipe_slots);
+ if (!c->wipe_slots)
+ return log_oom();
+ c->n_wipe_slots = arg_n_wipe_slots;
+ }
+
+ if (arg_tpm2_n_hash_pcr_values > 0) {
+ c->tpm2_hash_pcr_values = newdup(Tpm2PCRValue, arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values);
+ if (!c->tpm2_hash_pcr_values)
+ return log_oom();
+ c->tpm2_n_hash_pcr_values = arg_tpm2_n_hash_pcr_values;
+ }
+
+ return 0;
+}
+
+int enroll_now(
+ const EnrollContext *c,
+ struct crypt_device *cd,
+ const struct iovec *volume_key,
+ char **ret_recovery_key) {
+
+ int slot, slot_to_wipe = -1, r;
+
+ assert(c);
+ assert(cd);
+ assert(iovec_is_set(volume_key));
+
+ switch (c->enroll_type) {
+
+ case ENROLL_PASSWORD:
+ return enroll_password(c, cd, volume_key);
+
+ case ENROLL_RECOVERY:
+ return enroll_recovery(c, cd, volume_key, ret_recovery_key);
+
+ case ENROLL_PKCS11:
+ return enroll_pkcs11(c, cd, volume_key);
+
+ case ENROLL_FIDO2:
+ return enroll_fido2(c, cd, volume_key);
+
+ case ENROLL_TPM2:
+ slot = enroll_tpm2(c, cd, volume_key, &slot_to_wipe);
+ if (slot < 0)
+ return slot;
+
+ if (slot_to_wipe >= 0) {
+ assert(slot != slot_to_wipe);
+
+ /* Updating the PIN on an existing enrollment: wipe just that one slot. This is an
+ * internal one-off wipe, unrelated to the user's wipe selection, so use a throwaway
+ * context referencing a single explicit slot. */
+ _cleanup_(enroll_context_done) EnrollContext wipe_ctx = ENROLL_CONTEXT_NULL;
+ wipe_ctx.wipe_slots = newdup(int, &slot_to_wipe, 1);
+ if (!wipe_ctx.wipe_slots)
+ return log_oom();
+
+ wipe_ctx.n_wipe_slots = 1;
+
+ r = wipe_slots(&wipe_ctx, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL);
+ if (r < 0)
+ return r;
+ }
+
+ return slot;
+
+ default:
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet.");
+ }
+}
+
static int run(int argc, char *argv[]) {
_cleanup_(crypt_freep) struct crypt_device *cd = NULL;
_cleanup_(iovec_done_erase) struct iovec vk = {};
- int slot, slot_to_wipe, r;
+ _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL;
+ int slot, r;
log_setup();
+ /* A delicious drop of snake oil */
+ (void) safe_mlockall(MCL_CURRENT|MCL_FUTURE|MCL_ONFAULT);
+
+ /* If invoked as a Varlink service, hand off to the Varlink server and don't process the command
+ * line any further. */
+ r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
+ if (r > 0)
+ return cryptenroll_varlink_server();
+
r = parse_argv(argc, argv);
if (r <= 0)
return r;
@@ -784,72 +1030,66 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return r;
- /* A delicious drop of snake oil */
- (void) safe_mlockall(MCL_CURRENT|MCL_FUTURE|MCL_ONFAULT);
-
- if (arg_enroll_type < 0)
- r = prepare_luks(&cd, /* ret_volume_key= */ NULL); /* No need to unlock device if we don't need the volume key because we don't need to enroll anything */
- else
- r = prepare_luks(&cd, &vk);
+ r = enroll_context_from_args(&c);
if (r < 0)
return r;
- switch (arg_enroll_type) {
+ _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL;
- case ENROLL_PASSWORD:
- slot = enroll_password(cd, &vk);
- break;
+ /* 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);
- case ENROLL_RECOVERY:
- slot = enroll_recovery(cd, &vk);
- break;
+ if (arg_firstboot) {
+ assert(c.enroll_type < 0);
- case ENROLL_PKCS11:
- slot = enroll_pkcs11(cd, &vk, arg_pkcs11_token_uri);
- break;
+ r = cryptenroll_run_interactive(
+ &c,
+ arg_prompt_suppress_mask,
+ arg_chrome,
+ arg_mute_console ? &mute_console_link : NULL);
+ if (r <= 0)
+ return r;
- case ENROLL_FIDO2:
- slot = enroll_fido2(cd, &vk, arg_fido2_device, arg_fido2_lock_with, arg_fido2_cred_alg, arg_fido2_salt_file, arg_fido2_parameters_in_header);
- break;
+ assert(c.enroll_type >= 0);
- case ENROLL_TPM2:
- slot = enroll_tpm2(cd, &vk, arg_tpm2_device, arg_tpm2_seal_key_handle, arg_tpm2_device_key, arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values, arg_tpm2_public_key, arg_tpm2_load_public_key, arg_tpm2_public_key_pcr_mask, arg_tpm2_signature, arg_tpm2_pin, arg_tpm2_pcrlock, &slot_to_wipe);
+ } 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;
- if (slot >= 0 && slot_to_wipe >= 0) {
- assert(slot != slot_to_wipe);
-
- /* Updating PIN on an existing enrollment */
- r = wipe_slots(
- cd,
- &slot_to_wipe,
- /* n_explicit_slots= */ 1,
- WIPE_EXPLICIT,
- /* by_mask= */ 0,
- /* except_slot= */ -1);
- if (r < 0)
- return r;
- }
- break;
- case _ENROLL_TYPE_INVALID:
/* List enrolled slots if we are called without anything to enroll or wipe */
if (!wipe_requested())
return list_enrolled(cd);
/* Only slot wiping selected */
- return wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, -1);
-
- default:
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet.");
+ return wipe_slots(&c, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL);
}
- if (slot < 0)
- return slot;
- /* After we completed enrolling, remove user selected slots */
- r = wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, slot);
+ r = prepare_luks(&c, &cd, &vk);
if (r < 0)
- return r;
+ goto finish;
- return 0;
+ slot = enroll_now(&c, cd, &vk, /* ret_recovery_key= */ NULL);
+ 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)
+ goto finish;
+
+ r = 0;
+
+finish:
+ if (arg_firstboot)
+ (void) any_key_to_proceed();
+
+ return r;
}
DEFINE_MAIN_FUNCTION(run);
diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h
index 1066ecc18d..5fdb4e713c 100644
--- a/src/cryptenroll/cryptenroll.h
+++ b/src/cryptenroll/cryptenroll.h
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "libfido2-util.h"
#include "shared-forward.h"
typedef enum EnrollType {
@@ -18,6 +19,8 @@ typedef enum UnlockType {
UNLOCK_KEYFILE,
UNLOCK_FIDO2,
UNLOCK_TPM2,
+ UNLOCK_EMPTY,
+ UNLOCK_HEADLESS,
_UNLOCK_TYPE_MAX,
_UNLOCK_TYPE_INVALID = -EINVAL,
} UnlockType;
@@ -32,3 +35,89 @@ typedef enum WipeScope {
DECLARE_STRING_TABLE_LOOKUP(enroll_type, EnrollType);
DECLARE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType);
+
+/* A single bag of parameters consumed by the enrollment helpers. Populated either from the command line
+ * (see enroll_context_from_args() in cryptenroll.c), from a Varlink request (see cryptenroll-varlink.c),
+ * or by the interactive wizard (see cryptenroll-interactive.c). Owns all strings/arrays it points to. */
+typedef struct EnrollContext {
+ EnrollType enroll_type;
+ UnlockType unlock_type;
+
+ /* Target device */
+ char *node;
+
+ /* Unlock side */
+ char *unlock_keyfile;
+ char *unlock_fido2_device;
+ char *unlock_tpm2_device;
+ char *unlock_password; /* used by Varlink; NULL on CLI path */
+
+ /* New password to enroll (mechanism == password). When NULL the helpers fall back to
+ * $NEWPASSWORD / askpw. */
+ char *passphrase;
+ size_t passphrase_size;
+
+ /* FIDO2 */
+ char *fido2_device;
+ char *fido2_salt_file;
+ bool fido2_parameters_in_header;
+ Fido2EnrollFlags fido2_lock_with;
+ int fido2_cred_alg;
+ char *fido2_pin; /* optional pre-supplied token PIN; NULL means prompt (if interactive) */
+
+ /* PKCS#11 */
+ char *pkcs11_token_uri;
+
+ /* TPM2 */
+ char *tpm2_device;
+ uint32_t tpm2_seal_key_handle;
+ char *tpm2_device_key;
+ Tpm2PCRValue *tpm2_hash_pcr_values;
+ size_t tpm2_n_hash_pcr_values;
+ bool tpm2_pin;
+ char *tpm2_public_key;
+ bool tpm2_load_public_key;
+ uint32_t tpm2_public_key_pcr_mask;
+ char *tpm2_signature;
+ char *tpm2_pcrlock;
+
+ /* Wipe selection */
+ int *wipe_slots;
+ size_t n_wipe_slots;
+ WipeScope wipe_slots_scope;
+ unsigned wipe_slots_mask;
+ int wipe_except_slot; /* slot to never wipe (e.g. the one we just enrolled); -1 for none */
+
+ /* If false, the enrollment helpers must never prompt the user (no askpw, no terminal I/O,
+ * no log printing of credential material). They use the fields in this context as the
+ * sole input, and fail with ENOPKG if a required piece of input is missing. CLI and
+ * interactive callers set this to true; the Varlink dispatcher sets it to false. */
+ bool interactive;
+
+ /* Varlink link the request came in on, if the caller asked for 'more'. NULL otherwise.
+ * Owned (sd_varlink_ref'd) by the context. */
+ sd_varlink *link;
+} EnrollContext;
+
+#define ENROLL_CONTEXT_NULL \
+ (EnrollContext) { \
+ .enroll_type = _ENROLL_TYPE_INVALID, \
+ .unlock_type = UNLOCK_PASSWORD, \
+ .fido2_parameters_in_header = true, \
+ .fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP, \
+ .tpm2_load_public_key = true, \
+ .wipe_slots_scope = WIPE_EXPLICIT, \
+ .wipe_except_slot = -1, \
+ .interactive = true, \
+ }
+
+void enroll_context_done(EnrollContext *c);
+
+/* Opens & loads the LUKS2 superblock of c->node, refuses homed-managed volumes, and (if ret_volume_key is
+ * non-NULL) unlocks it according to c->unlock_type, returning the volume key. */
+int prepare_luks(const EnrollContext *c, struct crypt_device **ret_cd, struct iovec *ret_volume_key);
+
+/* Dispatches to the enroll_*() helper matching c->enroll_type and returns the keyslot the new credential
+ * was added to. For ENROLL_RECOVERY the generated key is returned via ret_recovery_key (if non-NULL).
+ * Defined in cryptenroll.c, shared by the command line and Varlink code paths. */
+int enroll_now(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key, char **ret_recovery_key);
diff --git a/src/cryptenroll/io.systemd.cryptenroll.policy b/src/cryptenroll/io.systemd.cryptenroll.policy
new file mode 100644
index 0000000000..80efa19957
--- /dev/null
+++ b/src/cryptenroll/io.systemd.cryptenroll.policy
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ The systemd Project
+ https://systemd.io
+
+
+ Enroll an unlock mechanism into an encrypted volume
+ Authentication is required to enroll a new unlock mechanism into an encrypted volume.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
diff --git a/src/cryptenroll/meson.build b/src/cryptenroll/meson.build
index aa789c0a07..3fbb5bf080 100644
--- a/src/cryptenroll/meson.build
+++ b/src/cryptenroll/meson.build
@@ -7,11 +7,13 @@ endif
systemd_cryptenroll_sources = files(
'cryptenroll.c',
'cryptenroll-fido2.c',
+ 'cryptenroll-interactive.c',
'cryptenroll-list.c',
'cryptenroll-password.c',
'cryptenroll-pkcs11.c',
'cryptenroll-recovery.c',
'cryptenroll-tpm2.c',
+ 'cryptenroll-varlink.c',
'cryptenroll-wipe.c',
)
@@ -28,3 +30,6 @@ executables += [
],
},
]
+
+install_data('io.systemd.cryptenroll.policy',
+ install_dir : polkitpolicydir)
diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c
index d351b1db5e..210c050284 100644
--- a/src/home/homectl-fido2.c
+++ b/src/home/homectl-fido2.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
+#include "ask-password-api.h" /* IWYU pragma: keep */
#include "errno-util.h"
#include "fido2-util.h"
#include "hexdecoct.h"
@@ -174,6 +175,8 @@ int identity_add_fido2_parameters(
/* user_icon= */ NULL,
/* askpw_icon= */ "user-home",
/* askpw_credential= */ "home.token-pin",
+ /* askpw_flags= */ 0,
+ /* pin= */ NULL,
lock_with,
cred_alg,
&salt,
diff --git a/src/libsystemd/sd-varlink/test-varlink-idl.c b/src/libsystemd/sd-varlink/test-varlink-idl.c
index a1ed9a0f42..ae4a6d21b8 100644
--- a/src/libsystemd/sd-varlink/test-varlink-idl.c
+++ b/src/libsystemd/sd-varlink/test-varlink-idl.c
@@ -21,6 +21,7 @@
#include "varlink-io.systemd.AskPassword.h"
#include "varlink-io.systemd.BootControl.h"
#include "varlink-io.systemd.Credentials.h"
+#include "varlink-io.systemd.CryptEnroll.h"
#include "varlink-io.systemd.FactoryReset.h"
#include "varlink-io.systemd.Hostname.h"
#include "varlink-io.systemd.Import.h"
@@ -199,6 +200,7 @@ TEST(parse_format) {
&vl_interface_io_systemd_AskPassword,
&vl_interface_io_systemd_BootControl,
&vl_interface_io_systemd_Credentials,
+ &vl_interface_io_systemd_CryptEnroll,
&vl_interface_io_systemd_FactoryReset,
&vl_interface_io_systemd_Hostname,
&vl_interface_io_systemd_Import,
diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c
index 5f69dc8924..4a0d7e6d64 100644
--- a/src/shared/libfido2-util.c
+++ b/src/shared/libfido2-util.c
@@ -746,6 +746,8 @@ int fido2_generate_hmac_hash(
const char *user_icon,
const char *askpw_icon,
const char *askpw_credential,
+ AskPasswordFlags askpw_flags,
+ const char *pin,
Fido2EnrollFlags lock_with,
int cred_alg,
const struct iovec *salt,
@@ -920,48 +922,57 @@ int fido2_generate_hmac_hash(
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Token asks for PIN but doesn't advertise 'clientPin' feature.");
- AskPasswordFlags askpw_flags = ASK_PASSWORD_ACCEPT_CACHED;
+ AskPasswordFlags pin_askpw_flags = askpw_flags | ASK_PASSWORD_ACCEPT_CACHED;
for (;;) {
- _cleanup_strv_free_erase_ char **pin = NULL;
- _cleanup_free_ char *ask_pin_msg = NULL;
- int pin_retries = -1;
+ _cleanup_strv_free_erase_ char **pins = NULL;
- r = sym_fido_dev_get_retry_count(d, &pin_retries);
- if (r != FIDO_OK) {
- log_warning("Failed to obtain number of retries before lock-out for PIN "
- "authentication, ignoring: %s", sym_fido_strerr(r));
- pin_retries = -1;
- }
-
- if (pin_retries >= 0) {
- ask_pin_msg = asprintf_safe(_("Please enter security token PIN "
- "(remaining attempts before lock-out: %d):"),
- pin_retries);
- if (!ask_pin_msg)
+ if (pin) {
+ /* A PIN was supplied by the caller: use it directly, don't prompt. */
+ pins = strv_new(pin);
+ if (!pins)
return log_oom();
+ } else if (FLAGS_SET(askpw_flags, ASK_PASSWORD_HEADLESS))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "PIN querying disabled via 'headless' option.");
+ else {
+ int pin_retries = -1;
+ r = sym_fido_dev_get_retry_count(d, &pin_retries);
+ if (r != FIDO_OK) {
+ log_warning("Failed to obtain number of retries before lock-out for PIN "
+ "authentication, ignoring: %s", sym_fido_strerr(r));
+ pin_retries = -1;
+ }
+
+ _cleanup_free_ char *ask_pin_msg = NULL;
+ if (pin_retries >= 0) {
+ ask_pin_msg = asprintf_safe(_("Please enter security token PIN "
+ "(remaining attempts before lock-out: %d):"),
+ pin_retries);
+ if (!ask_pin_msg)
+ return log_oom();
+ }
+
+ AskPasswordRequest req = {
+ .tty_fd = -EBADF,
+ .message = pin_retries >= 0
+ ? ask_pin_msg
+ : _("Please enter security token PIN:"),
+ .icon = askpw_icon,
+ .keyring = "fido2-pin",
+ .credential = askpw_credential,
+ .until = USEC_INFINITY,
+ .hup_fd = -EBADF,
+ };
+
+ r = ask_password_auto(&req, pin_askpw_flags, &pins);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire user PIN: %m");
+
+ pin_askpw_flags &= ~ASK_PASSWORD_ACCEPT_CACHED;
}
- AskPasswordRequest req = {
- .tty_fd = -EBADF,
- .message = pin_retries >= 0
- ? ask_pin_msg
- : _("Please enter security token PIN:"),
- .icon = askpw_icon,
- .keyring = "fido2-pin",
- .credential = askpw_credential,
- .until = USEC_INFINITY,
- .hup_fd = -EBADF,
- };
-
- r = ask_password_auto(&req, askpw_flags, &pin);
- if (r < 0)
- return log_error_errno(r, "Failed to acquire user PIN: %m");
-
- askpw_flags &= ~ASK_PASSWORD_ACCEPT_CACHED;
-
r = FIDO_ERR_PIN_INVALID;
- STRV_FOREACH(i, pin) {
+ STRV_FOREACH(i, pins) {
if (isempty(*i)) {
log_notice("PIN may not be empty.");
continue;
@@ -981,6 +992,11 @@ int fido2_generate_hmac_hash(
if (r != FIDO_ERR_PIN_INVALID)
break;
+ /* A caller-supplied PIN that's wrong won't get better by retrying: fail instead of
+ * looping forever (we'd never prompt). */
+ if (pin)
+ break;
+
log_notice("PIN incorrect, please try again.");
}
}
@@ -996,6 +1012,9 @@ int fido2_generate_hmac_hash(
if (r == FIDO_ERR_UNSUPPORTED_ALGORITHM)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Token doesn't support credential algorithm %s.", fido2_algorithm_to_string(cred_alg));
+ if (r == FIDO_ERR_PIN_INVALID)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "PIN incorrect.");
if (r != FIDO_OK)
return log_error_errno(SYNTHETIC_ERRNO(EIO),
"Failed to generate FIDO2 credential: %s", sym_fido_strerr(r));
@@ -1300,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 bbf6d1ca66..12a0201332 100644
--- a/src/shared/libfido2-util.h
+++ b/src/shared/libfido2-util.h
@@ -117,6 +117,8 @@ int fido2_generate_hmac_hash(
const char *user_icon,
const char *askpw_icon,
const char *askpw_credential,
+ AskPasswordFlags askpw_flags,
+ const char *pin,
Fido2EnrollFlags lock_with,
int cred_alg,
const struct iovec *salt,
@@ -135,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/src/shared/meson.build b/src/shared/meson.build
index 1b1ab668b2..7c6505e015 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -224,6 +224,7 @@ shared_sources = files(
'varlink-io.systemd.AskPassword.c',
'varlink-io.systemd.BootControl.c',
'varlink-io.systemd.Credentials.c',
+ 'varlink-io.systemd.CryptEnroll.c',
'varlink-io.systemd.FactoryReset.c',
'varlink-io.systemd.Hostname.c',
'varlink-io.systemd.Import.c',
diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c
index 69728e0cb4..a7bcfe32e3 100644
--- a/src/shared/prompt-util.c
+++ b/src/shared/prompt-util.c
@@ -213,6 +213,47 @@ int prompt_loop(
}
}
+static int boolean_is_valid(const char *name, void *userdata) {
+ return parse_boolean(name) >= 0;
+}
+
+int prompt_loop_yes_no(const char *question, bool def, bool *ret) {
+ int r;
+
+ assert(question);
+ assert(ret);
+
+ char **l = STRV_MAKE("yes", "no");
+
+ _cleanup_free_ char *reply = NULL;
+ r = prompt_loop(question,
+ GLYPH_WARNING_SIGN,
+ /* prefill= */ def ? "yes" : "no",
+ /* menu= */ l,
+ /* accepted= */ NULL,
+ /* ellipsize_percentage= */ 20,
+ /* n_columns= */ 2,
+ /* column_width= */ 40,
+ /* is_valid= */ boolean_is_valid,
+ /* refresh= */ NULL,
+ /* userdata= */ NULL,
+ PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT,
+ &reply);
+ if (r < 0)
+ return r;
+ if (r == 0) { /* Skipped (empty input): fall back to the default. */
+ *ret = def;
+ return 0;
+ }
+
+ r = parse_boolean(reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse reply: %s", reply);
+
+ *ret = r;
+ return 1;
+}
+
/* Default: bright white on blue background */
#define ANSI_COLOR_CHROME "\x1B[0;44;1;37m"
@@ -371,11 +412,12 @@ static int vl_on_reply(sd_varlink *link, sd_json_variant *parameters, const char
int mute_console(sd_varlink **ret_link) {
int r;
- assert(ret_link);
-
/* Talks to the MuteConsole service, and asks for output to the console to be muted, as long as the
* connection is retained */
+ if (!ret_link)
+ return 0;
+
_cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *link = NULL;
r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.MuteConsole");
if (r < 0)
diff --git a/src/shared/prompt-util.h b/src/shared/prompt-util.h
index 7afccd1b3d..255433b671 100644
--- a/src/shared/prompt-util.h
+++ b/src/shared/prompt-util.h
@@ -26,6 +26,8 @@ int prompt_loop(const char *text,
PromptFlags flags,
char **ret);
+int prompt_loop_yes_no(const char *question, bool def, bool *ret);
+
int chrome_show(const char *top, const char *bottom);
void chrome_hide(void);
diff --git a/src/shared/varlink-io.systemd.CryptEnroll.c b/src/shared/varlink-io.systemd.CryptEnroll.c
new file mode 100644
index 0000000000..6855299e76
--- /dev/null
+++ b/src/shared/varlink-io.systemd.CryptEnroll.c
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bus-polkit.h"
+#include "varlink-io.systemd.CryptEnroll.h"
+
+/* The credential types this interface knows about. The same enum is used for the 'mechanism' to enroll, for
+ * the slot types to wipe, and for the slot types reported by ListSlots. Note that only password, recovery key
+ * and fido2 may actually be *enrolled* via the Enroll() method; pkcs11 and tpm2 slots can be listed and wiped,
+ * but enrolling them requires the systemd-cryptenroll command line. Enroll() rejects them with an
+ * InvalidParameter error rather than via interface validation, so they are part of this enum. */
+static SD_VARLINK_DEFINE_ENUM_TYPE(
+ EnrollMechanism,
+ SD_VARLINK_FIELD_COMMENT("A regular passphrase"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(password),
+ SD_VARLINK_FIELD_COMMENT("A randomly generated recovery key"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(recovery),
+ SD_VARLINK_FIELD_COMMENT("A PKCS#11 security token (not enrollable via this interface)"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(pkcs11),
+ SD_VARLINK_FIELD_COMMENT("A FIDO2 security token"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(fido2),
+ SD_VARLINK_FIELD_COMMENT("A TPM2 device (not enrollable via this interface)"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(tpm2));
+
+static SD_VARLINK_DEFINE_METHOD_FULL(
+ Enroll,
+ SD_VARLINK_SUPPORTS_MORE,
+ SD_VARLINK_FIELD_COMMENT("Path to the LUKS2 block device or image to operate on."),
+ SD_VARLINK_DEFINE_INPUT(node, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("Which kind of credential to enroll. Only 'password', 'recovery' and 'fido2' may be enrolled via this interface for now; 'pkcs11' and 'tpm2' are rejected with an InvalidParameter error, currently."),
+ SD_VARLINK_DEFINE_INPUT_BY_TYPE(mechanism, EnrollMechanism, 0),
+
+ SD_VARLINK_FIELD_COMMENT("How to unlock the volume for the enrollment operation is inferred from which of the following fields are set: setting unlockPassword unlocks via that password, unlockKeyFile/unlockKeyFileDescriptor via a key file, unlockFido2Device via FIDO2, unlockTpm2Device via TPM2. Exactly one must be set."),
+ SD_VARLINK_DEFINE_INPUT(unlockPassword, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Path to a key file to unlock the volume with."),
+ SD_VARLINK_DEFINE_INPUT(unlockKeyFile, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Index into the file descriptors passed along with this call, identifying a key file to unlock the volume with."),
+ SD_VARLINK_DEFINE_INPUT(unlockKeyFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Path to a FIDO2 device to unlock the volume with. Leave the path empty to automatically discover a suitable device."),
+ SD_VARLINK_DEFINE_INPUT(unlockFido2Device, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Path to a TPM2 device to unlock the volume with. Leave the path empty to automatically discover a suitable device."),
+ SD_VARLINK_DEFINE_INPUT(unlockTpm2Device, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+
+ SD_VARLINK_FIELD_COMMENT("The passphrase to enroll, when mechanism is 'password'. Contains key material."),
+ SD_VARLINK_DEFINE_INPUT(password, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+
+ SD_VARLINK_FIELD_COMMENT("Path to the FIDO2 device to enroll, when mechanism is 'fido2'. Leave empty to automatically discover a suitable device."),
+ SD_VARLINK_DEFINE_INPUT(fido2Device, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The FIDO2 token PIN to use for enrollment. Contains key material."),
+ SD_VARLINK_DEFINE_INPUT(fido2Pin, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Whether unlocking the FIDO2 enrollment shall require a client PIN. Defaults to true."),
+ SD_VARLINK_DEFINE_INPUT(fido2WithClientPin, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Whether unlocking the FIDO2 enrollment shall require user presence (touch). Defaults to true."),
+ SD_VARLINK_DEFINE_INPUT(fido2WithUserPresence, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Whether unlocking the FIDO2 enrollment shall require user verification. Defaults to false."),
+ SD_VARLINK_DEFINE_INPUT(fido2WithUserVerification, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+
+ SD_VARLINK_FIELD_COMMENT("Explicit list of keyslot indexes to wipe after enrolling."),
+ SD_VARLINK_DEFINE_INPUT(wipeSlots, SD_VARLINK_INT, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Wipe all already-enrolled slots of these types after enrolling."),
+ SD_VARLINK_DEFINE_INPUT_BY_TYPE(wipeTypes, EnrollMechanism, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
+
+ VARLINK_DEFINE_POLKIT_INPUT,
+
+ SD_VARLINK_FIELD_COMMENT("The keyslot the new credential was enrolled into. Set on the terminating reply only."),
+ SD_VARLINK_DEFINE_OUTPUT(slot, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The keyslots wiped as part of this call, if any."),
+ SD_VARLINK_DEFINE_OUTPUT(wipedSlots, SD_VARLINK_INT, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The generated recovery key, when mechanism is 'recoveryKey'. Contains key material."),
+ SD_VARLINK_DEFINE_OUTPUT(recoveryKey, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Progress indicator, only sent on intermediate replies when 'more' was set. Currently the only value is 'touch', signalling that the FIDO2 token is waiting for a touch. Unset on the terminating reply."),
+ SD_VARLINK_DEFINE_OUTPUT(state, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_METHOD_FULL(
+ ListSlots,
+ SD_VARLINK_REQUIRES_MORE,
+ SD_VARLINK_FIELD_COMMENT("Path to the LUKS2 block device or image to operate on."),
+ SD_VARLINK_DEFINE_INPUT(node, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("A currently enrolled keyslot index."),
+ SD_VARLINK_DEFINE_OUTPUT(slot, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The type of the keyslot."),
+ SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, EnrollMechanism, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_ERROR(VolumeUnderForeignManagement);
+static SD_VARLINK_DEFINE_ERROR(PasswordRequired);
+static SD_VARLINK_DEFINE_ERROR(PasswordIncorrect);
+static SD_VARLINK_DEFINE_ERROR(FidoDeviceNotFound);
+static SD_VARLINK_DEFINE_ERROR(FidoActionTimeout);
+
+SD_VARLINK_DEFINE_INTERFACE(
+ io_systemd_CryptEnroll,
+ "io.systemd.CryptEnroll",
+ SD_VARLINK_INTERFACE_COMMENT("API for enrolling credentials into LUKS2 volumes via systemd-cryptenroll."),
+ SD_VARLINK_SYMBOL_COMMENT("The credential types this interface knows about, used both as the enrollment mechanism and as the slot type reported by ListSlots."),
+ &vl_type_EnrollMechanism,
+ SD_VARLINK_SYMBOL_COMMENT("Enroll a credential into a LUKS2 volume. When enrolling a FIDO2 token that requires user presence, the call must be made with the 'more' flag, so that the server can report when a touch is required via a 'touch' state notification."),
+ &vl_method_Enroll,
+ SD_VARLINK_SYMBOL_COMMENT("Enumerate the keyslots currently enrolled in a LUKS2 volume, one per reply. Must be called with the 'more' flag."),
+ &vl_method_ListSlots,
+ SD_VARLINK_SYMBOL_COMMENT("The volume is managed by another subsystem (e.g. systemd-homed) and may not be enrolled into directly."),
+ &vl_error_VolumeUnderForeignManagement,
+ SD_VARLINK_SYMBOL_COMMENT("A password is required to unlock the volume, but none was provided."),
+ &vl_error_PasswordRequired,
+ SD_VARLINK_SYMBOL_COMMENT("The provided password did not unlock the volume."),
+ &vl_error_PasswordIncorrect,
+ SD_VARLINK_SYMBOL_COMMENT("No matching FIDO2 device was found."),
+ &vl_error_FidoDeviceNotFound,
+ SD_VARLINK_SYMBOL_COMMENT("The FIDO2 token was not interacted with in time."),
+ &vl_error_FidoActionTimeout);
diff --git a/src/shared/varlink-io.systemd.CryptEnroll.h b/src/shared/varlink-io.systemd.CryptEnroll.h
new file mode 100644
index 0000000000..dcb626a47b
--- /dev/null
+++ b/src/shared/varlink-io.systemd.CryptEnroll.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-varlink-idl.h"
+
+extern const sd_varlink_interface vl_interface_io_systemd_CryptEnroll;
diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c
index 01e8cd0483..5d684cb0ab 100644
--- a/src/sysinstall/sysinstall.c
+++ b/src/sysinstall/sysinstall.c
@@ -613,29 +613,14 @@ static int prompt_confirm(void) {
putchar('\n');
- char **l = STRV_MAKE("yes", "no");
-
- _cleanup_free_ char *reply = NULL;
- r = prompt_loop(arg_summary ? "Please type 'yes' to confirm the choices above and begin the installation" :
- "Please type 'yes' to begin the installation",
- GLYPH_WARNING_SIGN,
- /* prefill= */ NULL,
- /* menu= */ l,
- /* accepted= */ l,
- /* ellipsize_percentage= */ 20,
- /* n_columns= */ 2,
- /* column_width= */ 40,
- /* is_valid= */ NULL,
- /* refresh= */ NULL,
- /* userdata= */ NULL,
- PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT,
- &reply);
+ bool yes;
+ r = prompt_loop_yes_no(arg_summary ? "Please type 'yes' to confirm the choices above and begin the installation" :
+ "Please type 'yes' to begin the installation",
+ /* def= */ false,
+ &yes);
if (r < 0)
return r;
- if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled.");
-
- if (!streq(reply, "yes"))
+ if (!yes)
return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation not confirmed, cancelling.");
return 0;
diff --git a/test/units/TEST-70-TPM2.cryptenroll.sh b/test/units/TEST-70-TPM2.cryptenroll.sh
index 07309429d7..684ff418f1 100755
--- a/test/units/TEST-70-TPM2.cryptenroll.sh
+++ b/test/units/TEST-70-TPM2.cryptenroll.sh
@@ -12,7 +12,7 @@ cryptenroll_wipe_and_check() {(
)}
at_exit() {
- rm -f "${IMAGE:-}" /tmp/cryptenroll.out /tmp/password
+ rm -f "${IMAGE:-}" "${VL_IMAGE:-}" /tmp/cryptenroll.out /tmp/password
}
trap at_exit EXIT
@@ -101,3 +101,43 @@ PIN=4321 systemd-cryptenroll --unlock-tpm2-device=auto --recovery-key "$IMAGE"
(! systemd-cryptenroll --fido2-device=auto --unlock-fido2-device=auto "$IMAGE")
rm -f "$IMAGE"
+
+# Exercise the io.systemd.CryptEnroll Varlink interface with calls equivalent to the command line ones above.
+CRYPTENROLL="$(command -v systemd-cryptenroll)"
+VL_ADDRESS="exec:$CRYPTENROLL"
+VL_IMAGE="$(mktemp /tmp/systemd-cryptenroll-varlink-XXX.image)"
+truncate -s 20M "$VL_IMAGE"
+cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$VL_IMAGE" /tmp/password
+
+# Enroll a recovery key, unlocking via key file (cf. systemd-cryptenroll --unlock-key-file= --recovery-key)
+varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \
+ "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"recovery\",\"unlockKeyFile\":\"/tmp/password\"}" | grep recoveryKey >/dev/null
+
+# Enroll a password, unlocking via key file (cf. NEWPASSWORD=… systemd-cryptenroll --unlock-key-file= --password)
+varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \
+ "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"password\",\"unlockKeyFile\":\"/tmp/password\",\"password\":\"varlinkpassword\"}"
+
+# Enroll a password, unlocking via the key file passed as a file descriptor instead of a path
+varlinkctl --push-fd=3 call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \
+ "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"password\",\"unlockKeyFileDescriptor\":0,\"password\":\"fdpassword\"}" 3/dev/null
+varlinkctl call --more "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}" | grep '"type":"password"' >/dev/null
+
+# Enroll combined with a wipe of the recovery key slot (cf. systemd-cryptenroll --wipe-slot=recovery --password).
+# The recovery key slot just got wiped, so it should be reported back in the (non-empty) wipedSlots output.
+varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \
+ "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"password\",\"unlockKeyFile\":\"/tmp/password\",\"password\":\"wipepassword\",\"wipeTypes\":[\"recovery\"]}" | grep -E '"wipedSlots":\[[0-9]+(,[0-9]+)*\]' >/dev/null
+(! varlinkctl call --more "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}" | grep '"type":"recovery"' >/dev/null)
+
+# ListSlots without 'more' is refused
+(! varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}")
+
+# PKCS#11 and TPM2 cannot be enrolled via this interface, so the Enroll() handler rejects them as invalid parameters
+(! varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \
+ "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"tpm2\",\"unlockKeyFile\":\"/tmp/password\"}")
+(! varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \
+ "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"pkcs11\",\"unlockKeyFile\":\"/tmp/password\"}")
+
+rm -f "$VL_IMAGE"
diff --git a/units/meson.build b/units/meson.build
index 569aa2e2a2..60ce3bf0ca 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -338,6 +338,20 @@ units = [
'symlinks' : ['sockets.target.wants/'],
},
{ 'file' : 'systemd-creds@.service' },
+ {
+ 'file' : 'systemd-cryptenroll.socket',
+ 'conditions' : ['HAVE_LIBCRYPTSETUP'],
+ 'symlinks' : ['sockets.target.wants/'],
+ },
+ {
+ '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
diff --git a/units/systemd-cryptenroll.socket b/units/systemd-cryptenroll.socket
new file mode 100644
index 0000000000..0ffcdb4c8a
--- /dev/null
+++ b/units/systemd-cryptenroll.socket
@@ -0,0 +1,25 @@
+# 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.
+
+[Unit]
+Description=Disk Encryption Enrollment Service Socket
+Documentation=man:systemd-cryptenroll(1)
+DefaultDependencies=no
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/systemd/io.systemd.CryptEnroll
+Symlinks=/run/varlink/registry/io.systemd.CryptEnroll
+FileDescriptorName=varlink
+SocketMode=0644
+Accept=yes
+MaxConnectionsPerSource=16
+XAttrEntryPoint=user.varlink=entrypoint
+XAttrListen=user.varlink=listen
+XAttrAccept=user.varlink=server
diff --git a/units/systemd-cryptenroll@.service b/units/systemd-cryptenroll@.service
new file mode 100644
index 0000000000..31f42cc458
--- /dev/null
+++ b/units/systemd-cryptenroll@.service
@@ -0,0 +1,18 @@
+# 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.
+
+[Unit]
+Description=Disk Encryption Enrollment Service
+Documentation=man:systemd-cryptenroll(1)
+DefaultDependencies=no
+Conflicts=shutdown.target initrd-switch-root.target
+Before=shutdown.target initrd-switch-root.target
+
+[Service]
+ExecStart=-systemd-cryptenroll