From 77ee077dfe45f920deb39e0c235d73e336fc8de8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 28 May 2026 15:33:49 +0200 Subject: [PATCH 01/13] shared: add a generic prompt_loop_yes_no() helper Factor a yes/no variant of prompt_loop() out into prompt-util.[ch], so the various interactive tools can share a single implementation, and convert systemd-sysinstall's installation confirmation question over to it. --- src/shared/prompt-util.c | 41 +++++++++++++++++++++++++++++++++++++ src/shared/prompt-util.h | 2 ++ src/sysinstall/sysinstall.c | 27 ++++++------------------ 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c index 69728e0cb4..7837a90fa4 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" 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/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; From 846b0efb04a60f0e4f88084994e6094306caff13 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Jun 2026 14:21:36 +0200 Subject: [PATCH 02/13] mute-console: allow NULL being passed --- src/shared/prompt-util.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c index 7837a90fa4..a7bcfe32e3 100644 --- a/src/shared/prompt-util.c +++ b/src/shared/prompt-util.c @@ -412,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) From 39d08de81bdb401928cef029375b96cc466db9dc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 28 May 2026 12:35:21 +0200 Subject: [PATCH 03/13] cryptenroll: collect all enrollment parameters in an EnrollContext Introduce an EnrollContext structure that carries everything the enrollment and unlocking helpers need, and route all enroll_*()/load_volume_key_*()/ wipe_slots() calls through it. The command line still populates the existing arg_* globals as before; once parsing is complete they are copied into a self-contained EnrollContext (which owns its strings/arrays) and the rest of the code only ever reads from the context. This is preparation for the upcoming varlinkification of systemd-cryptenroll: a Varlink dispatcher (and later an interactive first-boot wizard) can populate the very same EnrollContext without going through the arg_* parsing layer. To support non-interactive (e.g. Varlink) callers, the context carries an 'interactive' flag: when false, every credential prompt is disabled and the helpers fail with -ENOPKG (the established "querying disabled via headless" code) instead of blocking on a tty. Passwords, FIDO2 PINs and PKCS#11 PINs are all covered, and an optional FIDO2 PIN can be supplied directly via the context. enroll_recovery() additionally grows a quiet mode that returns the recovery key instead of printing it. This also adds one new field to EnrollContext which didn't exist before: the unlock_password is useful for the Varlink hookup later. No change in command line behaviour. --- src/cryptenroll/cryptenroll-fido2.c | 48 ++++---- src/cryptenroll/cryptenroll-fido2.h | 5 +- src/cryptenroll/cryptenroll-password.c | 47 +++++--- src/cryptenroll/cryptenroll-password.h | 5 +- src/cryptenroll/cryptenroll-pkcs11.c | 13 +- src/cryptenroll/cryptenroll-pkcs11.h | 3 +- src/cryptenroll/cryptenroll-recovery.c | 54 +++++---- src/cryptenroll/cryptenroll-recovery.h | 5 +- src/cryptenroll/cryptenroll-tpm2.c | 115 +++++++++--------- src/cryptenroll/cryptenroll-tpm2.h | 5 +- src/cryptenroll/cryptenroll-wipe.c | 29 ++--- src/cryptenroll/cryptenroll-wipe.h | 12 +- src/cryptenroll/cryptenroll.c | 158 +++++++++++++++++++------ src/cryptenroll/cryptenroll.h | 77 ++++++++++++ src/home/homectl-fido2.c | 3 + src/shared/libfido2-util.c | 89 ++++++++------ src/shared/libfido2-util.h | 2 + 17 files changed, 436 insertions(+), 234 deletions(-) diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c index 600207e947..7500e84c18 100644 --- a/src/cryptenroll/cryptenroll-fido2.c +++ b/src/cryptenroll/cryptenroll-fido2.c @@ -15,11 +15,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 +26,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 +55,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 +69,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 +82,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, @@ -112,7 +108,7 @@ int enroll_fido2( 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 +117,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 +148,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-password.c b/src/cryptenroll/cryptenroll-password.c index e189cce23a..6e78d6ab2f 100644 --- a/src/cryptenroll/cryptenroll-password.c +++ b/src/cryptenroll/cryptenroll-password.c @@ -14,18 +14,17 @@ #include "strv.h" int load_volume_key_password( + const EnrollContext *c, struct crypt_device *cd, - const char *cd_node, - void *ret_vk, - size_t *ret_vks) { + 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); r = getenv_steal_erase("PASSWORD", &envpw); if (r < 0) @@ -34,22 +33,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 +88,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 +107,7 @@ int load_volume_key_password( } int enroll_password( + const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key) { @@ -111,18 +116,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..897a8a2791 100644 --- a/src/cryptenroll/cryptenroll-password.h +++ b/src/cryptenroll/cryptenroll-password.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_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_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-wipe.c b/src/cryptenroll/cryptenroll-wipe.c index a8638d2b3a..b7d554c390 100644 --- a/src/cryptenroll/cryptenroll-wipe.c +++ b/src/cryptenroll/cryptenroll-wipe.c @@ -296,12 +296,8 @@ 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) { _cleanup_set_free_ Set *wipe_slots = NULL, *wipe_tokens = NULL, *keep_slots = NULL; _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL; @@ -309,10 +305,11 @@ int wipe_slots(struct crypt_device *cd, int r, slot_max, ret; void *e; + assert_se(c); assert_se(cd); /* 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) 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 +331,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 +370,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,7 +406,7 @@ 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."); return 0; } diff --git a/src/cryptenroll/cryptenroll-wipe.h b/src/cryptenroll/cryptenroll-wipe.h index aa82f9dbbd..4e71519181 100644 --- a/src/cryptenroll/cryptenroll-wipe.h +++ b/src/cryptenroll/cryptenroll-wipe.h @@ -1,13 +1,9 @@ /* 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). */ +int wipe_slots(const EnrollContext *c, struct crypt_device *cd); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index eed0a11aeb..0099c3c4a5 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -3,7 +3,9 @@ #include #include "sd-device.h" +#include "sd-varlink.h" +#include "alloc-util.h" #include "blockdev-list.h" #include "blockdev-util.h" #include "build.h" @@ -22,6 +24,7 @@ #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" @@ -110,6 +113,29 @@ static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = { DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); +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->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 determine_default_node(void) { int r; @@ -661,21 +687,21 @@ static int check_for_homed(struct crypt_device *cd) { } static int load_volume_key_keyfile( + const EnrollContext *c, struct crypt_device *cd, - void *ret_vk, - size_t *ret_vks) { + 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); - assert_se(ret_vks); r = read_full_file_full( AT_FDCWD, - arg_unlock_keyfile, + c->unlock_keyfile, UINT64_MAX, SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, @@ -683,13 +709,13 @@ static int load_volume_key_keyfile( &password, &password_len); if (r < 0) - return log_error_errno(r, "Reading keyfile %s failed: %m", arg_unlock_keyfile); + 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, - ret_vks, + ret_vk->iov_base, + &ret_vk->iov_len, password, password_len); if (r < 0) @@ -699,15 +725,17 @@ static int load_volume_key_keyfile( } static 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 +743,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 +759,33 @@ 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_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; default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown LUKS unlock method"); } - if (r < 0) return r; @@ -769,9 +795,62 @@ 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; +} + static int run(int argc, char *argv[]) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_(iovec_done_erase) struct iovec vk = {}; + _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL; int slot, slot_to_wipe, r; log_setup(); @@ -787,45 +866,49 @@ static int run(int argc, char *argv[]) { /* 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) { + if (c.enroll_type < 0) + r = prepare_luks(&c, &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(&c, &cd, &vk); + if (r < 0) + return r; + + switch (c.enroll_type) { case ENROLL_PASSWORD: - slot = enroll_password(cd, &vk); + slot = enroll_password(&c, cd, &vk); break; case ENROLL_RECOVERY: - slot = enroll_recovery(cd, &vk); + slot = enroll_recovery(&c, cd, &vk, /* ret_recovery_key= */ NULL); break; case ENROLL_PKCS11: - slot = enroll_pkcs11(cd, &vk, arg_pkcs11_token_uri); + slot = enroll_pkcs11(&c, cd, &vk); break; 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); + slot = enroll_fido2(&c, cd, &vk); break; 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); + slot = enroll_tpm2(&c, cd, &vk, &slot_to_wipe); 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); + /* Updating PIN on an existing enrollment: wipe just that one slot. This is an + * internal one-off wipe that is unrelated to the user's wipe selection, so use a + * throwaway context referencing a single explicit slot. */ + EnrollContext wipe_ctx = ENROLL_CONTEXT_NULL; + wipe_ctx.wipe_slots = &slot_to_wipe; + wipe_ctx.n_wipe_slots = 1; + + r = wipe_slots(&wipe_ctx, cd); if (r < 0) return r; } @@ -836,7 +919,7 @@ static int run(int argc, char *argv[]) { 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); + return wipe_slots(&c, cd); default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet."); @@ -844,8 +927,9 @@ static int run(int argc, char *argv[]) { 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); + /* After we completed enrolling, remove user selected slots (keeping the one we just added) */ + c.wipe_except_slot = slot; + r = wipe_slots(&c, cd); if (r < 0) return r; diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index 1066ecc18d..3e59848704 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 { @@ -32,3 +33,79 @@ 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; + + /* 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); 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/shared/libfido2-util.c b/src/shared/libfido2-util.c index 5f69dc8924..a7c776d0ce 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)); diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index bbf6d1ca66..6b23b62688 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, From fd3fb3a85ca3792201c4b85a90a7c2f7e632865e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Jun 2026 11:23:19 +0200 Subject: [PATCH 04/13] cryptenroll: add an "unlock_password" field to ExecContext This is preparation for the Varlinkification, as then we want to pass the password in via IPC instead of prompting the user. Note that this only adds the field, and applies it, but never actually sets it. That's for the varlinkification later. --- src/cryptenroll/cryptenroll-password.c | 14 ++++++++++++++ src/cryptenroll/cryptenroll.c | 1 + src/cryptenroll/cryptenroll.h | 1 + 3 files changed, 16 insertions(+) diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c index 6e78d6ab2f..0d3da9f2e7 100644 --- a/src/cryptenroll/cryptenroll-password.c +++ b/src/cryptenroll/cryptenroll-password.c @@ -26,6 +26,20 @@ int load_volume_key_password( assert_se(cd); assert_se(ret_vk); + 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) return log_error_errno(r, "Failed to acquire password from environment: %m"); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index 0099c3c4a5..c9f17c761f 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -121,6 +121,7 @@ void enroll_context_done(EnrollContext *c) { 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); diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index 3e59848704..b675023ee3 100644 --- a/src/cryptenroll/cryptenroll.h +++ b/src/cryptenroll/cryptenroll.h @@ -48,6 +48,7 @@ typedef struct EnrollContext { 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. */ From 0df41e9329d6425fbf422483e29e3f7e2b6a33b8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 28 May 2026 13:32:13 +0200 Subject: [PATCH 05/13] cryptenroll: expose enrollment as an io.systemd.CryptEnroll Varlink service Add a Varlink interface for systemd-cryptenroll, building on the EnrollContext introduced previously. A single Enroll method covers password, recovery-key and FIDO2 enrollment; PKCS#11 and TPM2 are not exposed for now (they are not part of the EnrollMechanism allowlist, so the generic InvalidParameter error applies). A ListSlots method enumerates the currently enrolled keyslots. The dispatcher populates the same EnrollContext the command line uses and then runs the shared enroll_now()/prepare_luks()/wipe_slots() paths, so both front-ends behave identically. FIDO2 enrollment that requires user presence reports an imminent touch via a non-terminating "state":"touch" reply when the caller passes 'more'. Credential material (password, FIDO2 PIN, recovery key) is handled as sensitive, and key files may be passed either by path or as an fd index. The server is allocated root-only plus caller's-own-UID, with the listening socket created in 0644 mode. Replaces: #31096 --- src/cryptenroll/cryptenroll-fido2.c | 7 + src/cryptenroll/cryptenroll-list.c | 79 +-- src/cryptenroll/cryptenroll-list.h | 13 + src/cryptenroll/cryptenroll-varlink.c | 481 ++++++++++++++++++ src/cryptenroll/cryptenroll-varlink.h | 10 + src/cryptenroll/cryptenroll-wipe.c | 40 +- src/cryptenroll/cryptenroll-wipe.h | 6 +- src/cryptenroll/cryptenroll.c | 136 +++-- src/cryptenroll/cryptenroll.h | 9 + src/cryptenroll/io.systemd.cryptenroll.policy | 30 ++ src/cryptenroll/meson.build | 4 + src/libsystemd/sd-varlink/test-varlink-idl.c | 2 + src/shared/meson.build | 1 + src/shared/varlink-io.systemd.CryptEnroll.c | 108 ++++ src/shared/varlink-io.systemd.CryptEnroll.h | 6 + units/meson.build | 9 + units/systemd-cryptenroll.socket | 25 + units/systemd-cryptenroll@.service | 18 + 18 files changed, 895 insertions(+), 89 deletions(-) create mode 100644 src/cryptenroll/cryptenroll-varlink.c create mode 100644 src/cryptenroll/cryptenroll-varlink.h create mode 100644 src/cryptenroll/io.systemd.cryptenroll.policy create mode 100644 src/shared/varlink-io.systemd.CryptEnroll.c create mode 100644 src/shared/varlink-io.systemd.CryptEnroll.h create mode 100644 units/systemd-cryptenroll.socket create mode 100644 units/systemd-cryptenroll@.service diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c index 7500e84c18..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" @@ -107,6 +108,12 @@ 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( c->fido2_device, /* rp_id= */ "io.systemd.cryptsetup", 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-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 b7d554c390..39e8593973 100644 --- a/src/cryptenroll/cryptenroll-wipe.c +++ b/src/cryptenroll/cryptenroll-wipe.c @@ -297,20 +297,28 @@ static bool slots_remain(struct crypt_device *cd, Set *wipe_slots, Set *keep_slo } int wipe_slots(const EnrollContext *c, - struct crypt_device *cd) { + 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 (c->n_wipe_slots == 0 && c->wipe_slots_mask == 0 && c->wipe_slots_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: * @@ -408,9 +416,20 @@ int wipe_slots(const EnrollContext *c, if (n_ordered_slots == 0 && n_ordered_tokens == 0) { 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]); @@ -430,8 +449,11 @@ int wipe_slots(const EnrollContext *c, 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--) { @@ -443,5 +465,13 @@ int wipe_slots(const EnrollContext *c, } } + 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 4e71519181..cb3dd71933 100644 --- a/src/cryptenroll/cryptenroll-wipe.h +++ b/src/cryptenroll/cryptenroll-wipe.h @@ -5,5 +5,7 @@ #include "shared-forward.h" /* 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). */ -int wipe_slots(const EnrollContext *c, struct crypt_device *cd); + * 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 c9f17c761f..cf67973fbf 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -16,6 +16,7 @@ #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" @@ -725,7 +726,7 @@ static int load_volume_key_keyfile( return r; } -static int prepare_luks( +int prepare_luks( const EnrollContext *c, struct crypt_device **ret_cd, struct iovec *ret_volume_key) { @@ -848,14 +849,81 @@ static int enroll_context_from_args(EnrollContext *c) { 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 = {}; _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL; - int slot, slot_to_wipe, r; + 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; @@ -864,73 +932,35 @@ 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); - r = enroll_context_from_args(&c); if (r < 0) return r; - if (c.enroll_type < 0) - r = prepare_luks(&c, &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(&c, &cd, &vk); - if (r < 0) - return r; + /* If we are called without anything to enroll, we just need the LUKS device, not the volume key. */ + if (c.enroll_type < 0) { + r = prepare_luks(&c, &cd, /* ret_volume_key= */ NULL); + if (r < 0) + return r; - switch (c.enroll_type) { - - case ENROLL_PASSWORD: - slot = enroll_password(&c, cd, &vk); - break; - - case ENROLL_RECOVERY: - slot = enroll_recovery(&c, cd, &vk, /* ret_recovery_key= */ NULL); - break; - - case ENROLL_PKCS11: - slot = enroll_pkcs11(&c, cd, &vk); - break; - - case ENROLL_FIDO2: - slot = enroll_fido2(&c, cd, &vk); - break; - - case ENROLL_TPM2: - slot = enroll_tpm2(&c, cd, &vk, &slot_to_wipe); - - if (slot >= 0 && slot_to_wipe >= 0) { - assert(slot != slot_to_wipe); - - /* Updating PIN on an existing enrollment: wipe just that one slot. This is an - * internal one-off wipe that is unrelated to the user's wipe selection, so use a - * throwaway context referencing a single explicit slot. */ - EnrollContext wipe_ctx = ENROLL_CONTEXT_NULL; - wipe_ctx.wipe_slots = &slot_to_wipe; - wipe_ctx.n_wipe_slots = 1; - - r = wipe_slots(&wipe_ctx, cd); - 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(&c, cd); - - 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); } + + r = prepare_luks(&c, &cd, &vk); + if (r < 0) + return r; + + slot = enroll_now(&c, cd, &vk, /* ret_recovery_key= */ NULL); if (slot < 0) return slot; /* After we completed enrolling, remove user selected slots (keeping the one we just added) */ c.wipe_except_slot = slot; - r = wipe_slots(&c, cd); + r = wipe_slots(&c, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL); if (r < 0) return r; diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index b675023ee3..36aa852f42 100644 --- a/src/cryptenroll/cryptenroll.h +++ b/src/cryptenroll/cryptenroll.h @@ -110,3 +110,12 @@ typedef struct EnrollContext { } 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..990b3dbc17 100644 --- a/src/cryptenroll/meson.build +++ b/src/cryptenroll/meson.build @@ -12,6 +12,7 @@ systemd_cryptenroll_sources = files( 'cryptenroll-pkcs11.c', 'cryptenroll-recovery.c', 'cryptenroll-tpm2.c', + 'cryptenroll-varlink.c', 'cryptenroll-wipe.c', ) @@ -28,3 +29,6 @@ executables += [ ], }, ] + +install_data('io.systemd.cryptenroll.policy', + install_dir : polkitpolicydir) 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/meson.build b/src/shared/meson.build index def42815c7..08d3635080 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -222,6 +222,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/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/units/meson.build b/units/meson.build index 569aa2e2a2..f813426216 100644 --- a/units/meson.build +++ b/units/meson.build @@ -338,6 +338,15 @@ 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-exit.service' }, { 'file' : 'systemd-factory-reset@.service.in' }, { 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 From 2bbcf6c66fb7c618890970f7fe2513f7372ad7a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 12:19:34 +0200 Subject: [PATCH 06/13] cryptenroll: move load_volume_key_keyfile() to cryptenroll-password.c Conceptually a keyfile and a password are pretty much the same thing, hence put them in the same file. --- src/cryptenroll/cryptenroll-password.c | 39 ++++++++++++++++++++++++++ src/cryptenroll/cryptenroll-password.h | 2 ++ src/cryptenroll/cryptenroll.c | 39 -------------------------- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c index 0d3da9f2e7..f58321063a 100644 --- a/src/cryptenroll/cryptenroll-password.c +++ b/src/cryptenroll/cryptenroll-password.c @@ -7,12 +7,51 @@ #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_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, + 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", 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, diff --git a/src/cryptenroll/cryptenroll-password.h b/src/cryptenroll/cryptenroll-password.h index 897a8a2791..35ae1760a8 100644 --- a/src/cryptenroll/cryptenroll-password.h +++ b/src/cryptenroll/cryptenroll-password.h @@ -4,5 +4,7 @@ #include "cryptenroll.h" #include "shared-forward.h" +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.c b/src/cryptenroll/cryptenroll.c index cf67973fbf..ce21f450ba 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -20,7 +20,6 @@ #include "cryptenroll-wipe.h" #include "cryptsetup-util.h" #include "extract-word.h" -#include "fileio.h" #include "format-table.h" #include "libfido2-util.h" #include "log.h" @@ -688,44 +687,6 @@ static int check_for_homed(struct crypt_device *cd) { return 0; } -static 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, - 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", 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 prepare_luks( const EnrollContext *c, struct crypt_device **ret_cd, From 5f2d4fb6233caf821bb304cc3dd47ac8cb0cb332 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 11:29:32 +0200 Subject: [PATCH 07/13] cryptenroll: add --unlock-empty to unlock via an empty password --- man/systemd-cryptenroll.xml | 10 ++++++++++ src/cryptenroll/cryptenroll-password.c | 24 ++++++++++++++++++++++++ src/cryptenroll/cryptenroll-password.h | 1 + src/cryptenroll/cryptenroll.c | 12 ++++++++++++ src/cryptenroll/cryptenroll.h | 1 + 5 files changed, 48 insertions(+) diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml index 707a8d25e8..61906d3ce3 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. + + + + diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c index f58321063a..595279c80d 100644 --- a/src/cryptenroll/cryptenroll-password.c +++ b/src/cryptenroll/cryptenroll-password.c @@ -14,6 +14,30 @@ #include "string-util.h" #include "strv.h" +int load_volume_key_empty( + const EnrollContext *c, + struct crypt_device *cd, + 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, diff --git a/src/cryptenroll/cryptenroll-password.h b/src/cryptenroll/cryptenroll-password.h index 35ae1760a8..035a1becc5 100644 --- a/src/cryptenroll/cryptenroll-password.h +++ b/src/cryptenroll/cryptenroll-password.h @@ -4,6 +4,7 @@ #include "cryptenroll.h" #include "shared-forward.h" +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); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index ce21f450ba..4b8aa6c241 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -334,6 +334,14 @@ static int parse_argv(int argc, char *argv[]) { 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) @@ -730,6 +738,10 @@ int prepare_luks( switch (c->unlock_type) { + case UNLOCK_EMPTY: + r = load_volume_key_empty(c, cd, &vk); + break; + case UNLOCK_PASSWORD: r = load_volume_key_password(c, cd, &vk); break; diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index 36aa852f42..49fa3b2a35 100644 --- a/src/cryptenroll/cryptenroll.h +++ b/src/cryptenroll/cryptenroll.h @@ -19,6 +19,7 @@ typedef enum UnlockType { UNLOCK_KEYFILE, UNLOCK_FIDO2, UNLOCK_TPM2, + UNLOCK_EMPTY, _UNLOCK_TYPE_MAX, _UNLOCK_TYPE_INVALID = -EINVAL, } UnlockType; From e2983318764eb782093916c5c2a5a310045297f8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 11:37:48 +0200 Subject: [PATCH 08/13] cryptenroll: add --unlock-headless as a new pseudo-unlock mechanism THis simply tries TPM2 if available, and falls back to empty password. --- man/systemd-cryptenroll.xml | 12 ++++++++++++ src/cryptenroll/cryptenroll.c | 23 +++++++++++++++++++++++ src/cryptenroll/cryptenroll.h | 1 + 3 files changed, 36 insertions(+) diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml index 61906d3ce3..c88d38fa43 100644 --- a/man/systemd-cryptenroll.xml +++ b/man/systemd-cryptenroll.xml @@ -317,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. + + + diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index 4b8aa6c241..8c488e554f 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -397,6 +397,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, @@ -758,6 +766,21 @@ int prepare_luks( 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"); } diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index 49fa3b2a35..5fdb4e713c 100644 --- a/src/cryptenroll/cryptenroll.h +++ b/src/cryptenroll/cryptenroll.h @@ -20,6 +20,7 @@ typedef enum UnlockType { UNLOCK_FIDO2, UNLOCK_TPM2, UNLOCK_EMPTY, + UNLOCK_HEADLESS, _UNLOCK_TYPE_MAX, _UNLOCK_TYPE_INVALID = -EINVAL, } UnlockType; From 0089a46b6bec0d4bbd2157a082f6d3d78576363e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 28 May 2026 14:15:56 +0200 Subject: [PATCH 09/13] cryptenroll: add interactive --firstboot enrollment wizard Add a --firstboot mode that interactively walks the user through enrolling a passphrase, a recovery key, or a FIDO2 token, with one menu entry per suitable token currently plugged in (driven by a new fido2_enumerate_devices() helper). Pressing enter at the top-level menu leaves the volume unchanged; for each already-enrolled credential type the wizard offers to wipe it as part of the operation. It populates the same EnrollContext the command line and Varlink paths use, so the actual enrollment goes through the shared enroll_now() path. A companion --prompt-suppress= option takes a list of slot types: if a slot of any listed type already exists, the wizard does nothing and exits successfully. This lets it be hooked into the boot process while staying quiet once the system has been set up. The accompanying systemd-cryptenroll-firstboot.service runs this from the initrd, after systemd-repart has created the encrypted volume but before we transition to the host, suppressing itself once a password, recovery key or FIDO2 token is enrolled. To make that work, determine_default_node() now looks below /sysroot/ when running in the initrd, since the host file systems aren't at their final location yet. While the wizard is active it draws the same installer-style chrome (blue bars at the top and bottom of the terminal) as systemd-sysinstall, using the shared prompt_loop_yes_no() helper for its wipe confirmations. Honours the systemd.firstboot= kernel command line option. Fixes: #36298 --- man/systemd-cryptenroll.xml | 59 +++++ src/cryptenroll/cryptenroll-interactive.c | 243 ++++++++++++++++++ src/cryptenroll/cryptenroll-interactive.h | 12 + src/cryptenroll/cryptenroll.c | 265 +++++++++++++++----- src/cryptenroll/meson.build | 1 + src/shared/libfido2-util.c | 103 ++++++++ src/shared/libfido2-util.h | 13 + units/meson.build | 5 + units/systemd-cryptenroll-firstboot.service | 41 +++ 9 files changed, 674 insertions(+), 68 deletions(-) create mode 100644 src/cryptenroll/cryptenroll-interactive.c create mode 100644 src/cryptenroll/cryptenroll-interactive.h create mode 100644 units/systemd-cryptenroll-firstboot.service diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml index c88d38fa43..9f090ec94b 100644 --- a/man/systemd-cryptenroll.xml +++ b/man/systemd-cryptenroll.xml @@ -362,6 +362,65 @@ + + Interactive Enrollment + + The following options are understood that drive an interactive enrollment wizard, intended to be + run on first boot (similar in spirit to + systemd-firstboot1): + + + + + + Run an interactive wizard that lets the user enroll a passphrase, a recovery key, or a + FIDO2 security token (one menu entry per suitable token currently plugged in). If credentials of any + of these types are already enrolled, the wizard additionally offers to wipe them. Pressing enter at + the top-level menu leaves the volume unchanged. This option may not be combined with an explicit + enrollment type or with . As with the other commands, the volume to + operate on is the one backing /var/ if none is specified. Honours the + systemd.firstboot= kernel command line option. + + + + + + + + Takes a comma-separated list of slot types, out of password, + recovery, pkcs11, fido2 and + tpm2. Only useful together with : if a slot of any of + the listed types is already enrolled on the volume, the wizard does nothing and exits successfully. + This is useful to hook the wizard into the boot process while ensuring it stays quiet once suitable + credentials have been set up. + + + + + + + + Takes a boolean argument, defaults to on. Only useful together with + : if enabled the interactive wizard draws a coloured bar across the top and + bottom of the terminal to visually frame the prompt. Pass to disable this + decoration, for example when running on a terminal that does not render it well. + + + + + + + + Takes a boolean argument, defaults to off. Only useful together with + : if enabled the kernel and PID 1 are asked to refrain from writing log + messages to the console while the interactive wizard is shown, so that such output does not interfere + with the prompt. The console is unmuted again once the wizard exits. + + + + + + PKCS#11 Enrollment diff --git a/src/cryptenroll/cryptenroll-interactive.c b/src/cryptenroll/cryptenroll-interactive.c new file mode 100644 index 0000000000..6dc584b18b --- /dev/null +++ b/src/cryptenroll/cryptenroll-interactive.c @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "ansi-color.h" +#include "cryptenroll.h" +#include "cryptenroll-interactive.h" +#include "cryptenroll-list.h" +#include "cryptsetup-util.h" +#include "glyph-util.h" +#include "libfido2-util.h" +#include "log.h" +#include "proc-cmdline.h" +#include "prompt-util.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" + +static int collect_existing_types(struct crypt_device *cd, unsigned *ret_mask) { + _cleanup_free_ EnrolledSlot *slots = NULL; + size_t n_slots; + unsigned mask = 0; + int r; + + assert(cd); + assert(ret_mask); + + r = collect_enrolled_slots(cd, &slots, &n_slots); + if (r < 0) + return r; + + FOREACH_ARRAY(s, slots, n_slots) + if (!s->conflict && s->type >= 0) + mask |= 1U << s->type; + + *ret_mask = mask; + return 0; +} + +static int choose_mechanism(EnrollContext *c) { + int r; + + assert(c); + + /* Top-level menu. Each iteration re-enumerates the FIDO2 tokens, so a token plugged in after the + * menu was first shown appears once the user picks "Rescan". An empty answer leaves the volume + * untouched. */ + + for (;;) { + Fido2DeviceInfo *devices = NULL; + size_t n_devices = 0; + CLEANUP_ARRAY(devices, n_devices, fido2_device_info_free_many); + + /* Best effort: if FIDO2 isn't available we simply offer no token rows. */ + (void) fido2_enumerate_devices(&devices, &n_devices); + + _cleanup_strv_free_ char **menu = strv_new("Enroll a recovery key", "Enroll a passphrase"); + if (!menu) + return log_oom(); + + FOREACH_ARRAY(d, devices, n_devices) { + _cleanup_free_ char *label = NULL; + + label = strjoin("Enroll FIDO2 security token: ", + d->manufacturer ?: "Security Token", + d->product ? " " : "", strempty(d->product), + " (", d->path, ")"); + if (!label) + return log_oom(); + + if (strv_consume(&menu, TAKE_PTR(label)) < 0) + return log_oom(); + } + + if (strv_extend(&menu, "Rescan for FIDO2 security tokens") < 0) + return log_oom(); + + _cleanup_free_ char *choice = NULL; + r = prompt_loop( + "Select enrollment option", + GLYPH_LOCK_AND_KEY, + /* prefill= */ NULL, + menu, + /* accepted= */ menu, /* only accept exact menu entries (or their numbers) */ + /* ellipsize_percentage= */ 60, + /* n_columns= */ 1, + /* column_width= */ SIZE_MAX, /* auto-size to the widest entry */ + /* is_valid= */ NULL, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_MAY_SKIP|PROMPT_SHOW_MENU|PROMPT_SHOW_MENU_NOW|PROMPT_HIDE_MENU_HINT, + &choice); + if (r < 0) + return r; + if (!choice) { + /* Empty answer: do nothing. */ + log_info("No selection made, leaving volume unchanged."); + return 0; + } + + /* Map the chosen label back to its index. */ + size_t idx = SIZE_MAX; + STRV_FOREACH(m, menu) + if (streq(*m, choice)) { + idx = m - menu; + break; + } + + assert(idx != SIZE_MAX); + + if (idx == 0) + c->enroll_type = ENROLL_RECOVERY; + else if (idx == 1) + c->enroll_type = ENROLL_PASSWORD; + else if (idx + 1 == strv_length(menu)) /* "Rescan" */ + continue; + else { + c->enroll_type = ENROLL_FIDO2; + assert(idx > 1); + assert(idx + 1 < strv_length(menu)); + + if (strdup_to(&c->fido2_device, devices[idx - 2].path) < 0) + return log_oom(); + } + + return 1; + } +} + +static int ask_wipe(EnrollContext *c, unsigned existing_mask) { + static const EnrollType candidates[] = { + ENROLL_PASSWORD, + ENROLL_RECOVERY, + ENROLL_FIDO2, + }; + int r; + + assert(c); + + /* For each already-enrolled type the wizard understands, offer to wipe it alongside the new + * enrollment. */ + + FOREACH_ELEMENT(t, candidates) { + if (!FLAGS_SET(existing_mask, 1U << *t)) + continue; + + _cleanup_free_ char *question = NULL; + question = strjoin("A ", enroll_type_to_string(*t), " slot is already enrolled. Wipe it as part of this enrollment?"); + if (!question) + return log_oom(); + + bool wipe; + r = prompt_loop_yes_no(question, /* def= */ false, &wipe); + if (r < 0) + return r; + + if (wipe) + c->wipe_slots_mask |= 1U << *t; + } + + return 0; +} + +int cryptenroll_run_interactive( + EnrollContext *c, + unsigned prompt_suppress_mask, + bool chrome, + sd_varlink **mute_console_link) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + unsigned existing_mask = 0; + int r; + + assert(c); + assert(c->node); + + /* Honour the systemd.firstboot= kernel command line option, just like systemd-firstboot. */ + bool enabled; + r = proc_cmdline_get_bool("systemd.firstboot", PROC_CMDLINE_TRUE_WHEN_MISSING, &enabled); + if (r < 0) + log_warning_errno(r, "Failed to parse systemd.firstboot= kernel command line option, ignoring: %m"); + else if (!enabled) { + log_debug("systemd.firstboot=no set, skipping interactive enrollment."); + return 0; + } + + /* Open the volume just to inspect its header (no unlocking needed yet). */ + r = prepare_luks(c, &cd, /* ret_volume_key= */ NULL); + if (r < 0) + return r; + + r = collect_existing_types(cd, &existing_mask); + if (r < 0) + return r; + + /* If a credential of a suppressed type is already enrolled, do nothing. This lets the wizard be + * wired into first boot but stay quiet once the system has been set up. */ + if ((existing_mask & prompt_suppress_mask) != 0) { + log_debug("A credential of a suppressed type is already enrolled, skipping interactive setup."); + return 0; + } + + if (existing_mask == 0) { + log_debug("No recognized LUKS slots, we're unlikely able to unlock, skipping interactive setup."); + return 0; + } + + (void) mute_console(mute_console_link); + + /* Draw the installer-style chrome (blue bars at the top and bottom) around the wizard, matching + * systemd-sysinstall. The caller hides it again via a deferred chrome_hide() once enrollment is + * complete. */ + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); + + if (chrome) + (void) chrome_show("Additional Disk Encryption Key Enrollment", /* bottom= */ NULL); + + printf("%s%s%sLet's enroll additional disk encryption mechanisms for recovering access to the system.%s\n\n", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "", + ansi_highlight(), ansi_normal()); + + _cleanup_free_ char *s = NULL; + for (EnrollType t = 0; t < _ENROLL_TYPE_MAX; t++) { + if (!FLAGS_SET(existing_mask, 1 << t)) + continue; + + if (!strextend_with_separator(&s, ", ", enroll_type_to_string(t))) + return log_oom(); + } + + printf("Currently enrolled mechanisms: %s\n\n", s); + + r = choose_mechanism(c); + if (r <= 0) + return r; + + r = ask_wipe(c, existing_mask); + if (r < 0) + return r; + + return 1; +} diff --git a/src/cryptenroll/cryptenroll-interactive.h b/src/cryptenroll/cryptenroll-interactive.h new file mode 100644 index 0000000000..413e0d8b93 --- /dev/null +++ b/src/cryptenroll/cryptenroll-interactive.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptenroll.h" + +/* Runs the interactive first-boot enrollment wizard, populating *c (c->enroll_type, c->fido2_device, + * c->wipe_slots_mask). On return c->enroll_type is set to the chosen mechanism, or left + * _ENROLL_TYPE_INVALID if the user skipped or the wizard was suppressed. + * + * prompt_suppress_mask is a bitmask of (1U << EnrollType): if a slot of any such type already exists on the + * volume, the wizard does nothing (so it can be hooked into first boot but stay quiet on later boots). */ +int cryptenroll_run_interactive(EnrollContext *c, unsigned prompt_suppress_mask, bool chrome, sd_varlink **mute_console_link); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index 8c488e554f..67386f31d8 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -9,8 +9,10 @@ #include "blockdev-list.h" #include "blockdev-util.h" #include "build.h" +#include "cleanup-util.h" #include "cryptenroll.h" #include "cryptenroll-fido2.h" +#include "cryptenroll-interactive.h" #include "cryptenroll-list.h" #include "cryptenroll-password.h" #include "cryptenroll-pkcs11.h" @@ -21,6 +23,8 @@ #include "cryptsetup-util.h" #include "extract-word.h" #include "format-table.h" +#include "help-util.h" +#include "initrd-util.h" #include "libfido2-util.h" #include "log.h" #include "main-func.h" @@ -30,10 +34,11 @@ #include "parse-argument.h" #include "parse-util.h" #include "pkcs11-util.h" -#include "pretty-print.h" #include "process-util.h" +#include "prompt-util.h" #include "string-table.h" #include "string-util.h" +#include "terminal-util.h" #include "tpm2-pcr.h" #include "tpm2-util.h" @@ -63,6 +68,10 @@ static int *arg_wipe_slots = NULL; static size_t arg_n_wipe_slots = 0; static WipeScope arg_wipe_slots_scope = WIPE_EXPLICIT; static unsigned arg_wipe_slots_mask = 0; /* Bitmask of (1U << EnrollType), for wiping all slots of specific types */ +static bool arg_firstboot = false; +static bool arg_chrome = true; +static bool arg_mute_console = false; +static unsigned arg_prompt_suppress_mask = 0; /* Bitmask of (1U << EnrollType): if any such slot exists, --firstboot does nothing */ static Fido2EnrollFlags arg_fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP; #if HAVE_LIBFIDO2 static int arg_fido2_cred_alg = COSE_ES256; @@ -113,6 +122,20 @@ static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = { DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); +static int enroll_type_mask_from_string(const char *name) { + assert(name); + + /* Maps an enroll type name (command line spelling) to its (1U << EnrollType) bitmask, or returns a + * negative errno if the name is not a known type. Callers merge the returned mask into their own + * accumulator. Shared by the various places that parse type lists into a bitmask. */ + + EnrollType t = enroll_type_from_string(name); + if (t < 0) + return -EINVAL; + + return 1 << t; +} + void enroll_context_done(EnrollContext *c) { if (!c) return; @@ -137,6 +160,53 @@ void enroll_context_done(EnrollContext *c) { c->link = sd_varlink_unref(c->link); } +static int resolve_default_node(const char *path, char **ret) { + int r; + + /* Resolves the path of the underlying LUKS2 block device of the file system at the given mount + * point, i.e. the raw partition behind the dm-crypt mapping that file system sits on. Returns + * -ENXIO if the file system is not backed by a (single) LUKS2 device. Logs only at debug level, so + * the caller can try the next candidate path. */ + + assert(path); + assert(ret); + + dev_t devno; + r = get_block_device(path, &devno); + if (r < 0) + return log_debug_errno(r, "Failed to determine block device backing %s: %m", path); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), + "File system %s is not backed by a (single) whole block device.", path); + + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + r = sd_device_new_from_devnum(&dev, 'b', devno); + if (r < 0) + return log_debug_errno(r, "Unable to access backing block device for %s: %m", path); + + const char *dm_uuid; + r = sd_device_get_property_value(dev, "DM_UUID", &dm_uuid); + if (r == -ENOENT) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "Backing block device of %s is not a DM device.", path); + if (r < 0) + return log_debug_errno(r, "Unable to query DM_UUID udev property of backing block device for %s: %m", path); + + if (!startswith(dm_uuid, "CRYPT-LUKS2-")) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "Block device backing %s is not a LUKS2 device.", path); + + _cleanup_(sd_device_unrefp) sd_device *origin = NULL; + r = block_device_get_originating(dev, &origin, /* recursive= */ false); + if (r < 0) + return log_debug_errno(r, "Failed to get originating device of LUKS2 device backing %s: %m", path); + + const char *dp; + r = sd_device_get_devname(origin, &dp); + if (r < 0) + return log_debug_errno(r, "Failed to get device path for LUKS2 device backing %s: %m", path); + + return strdup_to(ret, dp); +} + static int determine_default_node(void) { int r; @@ -154,47 +224,30 @@ static int determine_default_node(void) { * Or to say this differently: it makes sense to support well systems with /var/ being on /. It also * makes sense to support well systems with them being separate, and /var/ being variable and * persistent. But any other kind of system appears much less interesting to support, and in that - * case people should just specify the device name explicitly. */ + * case people should just specify the device name explicitly. + * + * When invoked from the initrd the host's file systems are not mounted at their final location yet, + * but below /sysroot/, hence look there instead. */ - dev_t devno; - r = get_block_device("/var", &devno); - if (r < 0) - return log_error_errno(r, "Failed to determine block device backing /var/: %m"); - if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), - "File system /var/ is on not backed by a (single) whole block device."); + const char *candidates[2]; + size_t n_candidates = 0; - _cleanup_(sd_device_unrefp) sd_device *dev = NULL; - r = sd_device_new_from_devnum(&dev, 'b', devno); - if (r < 0) - return log_error_errno(r, "Unable to access backing block device for /var/: %m"); + if (in_initrd()) { + candidates[n_candidates++] = "/sysroot/var"; + candidates[n_candidates++] = "/sysroot"; + } else + candidates[n_candidates++] = "/var"; - const char *dm_uuid; - r = sd_device_get_property_value(dev, "DM_UUID", &dm_uuid); - if (r == -ENOENT) - return log_error_errno(r, "Backing block device of /var/ is not a DM device: %m"); - if (r < 0) - return log_error_errno(r, "Unable to query DM_UUID udev property of backing block device for /var/: %m"); + FOREACH_ARRAY(path, candidates, n_candidates) { + r = resolve_default_node(*path, &arg_node); + if (r >= 0) { + log_info("No device specified, defaulting to '%s' (backing %s).", arg_node, *path); + return 0; + } + } - if (!startswith(dm_uuid, "CRYPT-LUKS2-")) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Block device backing /var/ is not a LUKS2 device."); - - _cleanup_(sd_device_unrefp) sd_device *origin = NULL; - r = block_device_get_originating(dev, &origin, /* recursive= */ false); - if (r < 0) - return log_error_errno(r, "Failed to get originating device of LUKS2 device backing /var/: %m"); - - const char *dp; - r = sd_device_get_devname(origin, &dp); - if (r < 0) - return log_error_errno(r, "Failed to get device path for LUKS2 device backing /var/: %m"); - - r = free_and_strdup_warn(&arg_node, dp); - if (r < 0) - return r; - - log_info("No device specified, defaulting to '%s'.", arg_node); - return 0; + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "Failed to automatically determine a LUKS2 block device to operate on, please specify one explicitly."); } static int parse_wipe_slot(const char *arg) { @@ -217,21 +270,15 @@ static int parse_wipe_slot(const char *arg) { if (r < 0) return log_error_errno(r, "Failed to parse slot list: %s", arg); + int mask; + if (streq(slot, "all")) arg_wipe_slots_scope = WIPE_ALL; else if (streq(slot, "empty")) { if (arg_wipe_slots_scope != WIPE_ALL) /* if "all" was specified before, that wins */ arg_wipe_slots_scope = WIPE_EMPTY_PASSPHRASE; - } else if (streq(slot, "password")) - arg_wipe_slots_mask |= 1U << ENROLL_PASSWORD; - else if (streq(slot, "recovery")) - arg_wipe_slots_mask |= 1U << ENROLL_RECOVERY; - else if (streq(slot, "pkcs11")) - arg_wipe_slots_mask |= 1U << ENROLL_PKCS11; - else if (streq(slot, "fido2")) - arg_wipe_slots_mask |= 1U << ENROLL_FIDO2; - else if (streq(slot, "tpm2")) - arg_wipe_slots_mask |= 1U << ENROLL_TPM2; + } else if ((mask = enroll_type_mask_from_string(slot)) >= 0) + arg_wipe_slots_mask |= mask; else { unsigned n; @@ -249,15 +296,34 @@ static int parse_wipe_slot(const char *arg) { } } -static int help(void) { - _cleanup_free_ char *link = NULL; +static int parse_prompt_suppress(const char *arg) { int r; - pager_open(arg_pager_flags); + assert(arg); - r = terminal_urlify_man("systemd-cryptenroll", "1", &link); - if (r < 0) - return log_oom(); + /* Parses a comma-separated list of the slot types the --firstboot wizard knows how to enroll. If a + * slot of any listed type already exists on the volume, the wizard does nothing. */ + + for (const char *p = arg;;) { + _cleanup_free_ char *type = NULL; + int mask; + + r = extract_first_word(&p, &type, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == 0) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to parse type list: %s", arg); + + mask = enroll_type_mask_from_string(type); + if (mask < 0) + return log_error_errno(mask, "Unknown slot type: %s", type); + + arg_prompt_suppress_mask |= mask; + } +} + +static int help(void) { + int r; static const char* const groups[] = { NULL, @@ -279,21 +345,20 @@ static int help(void) { (void) table_sync_column_widths(0, tables[0], tables[1], tables[2], tables[3], tables[4], tables[5]); - printf("%s [OPTIONS...] [BLOCK-DEVICE]\n\n" - "%sEnroll a security token or authentication credential to a LUKS volume.%s\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal()); + pager_open(arg_pager_flags); + + help_cmdline("[OPTIONS...] [BLOCK-DEVICE]"); + help_abstract("Enroll a security token or authentication credential to a LUKS volume."); for (size_t i = 0; i < ELEMENTSOF(groups); i++) { - printf("\n%s%s:%s\n", ansi_underline(), groups[i] ?: "Options", ansi_normal()); + help_section(groups[i] ?: "Options"); r = table_print_or_warn(tables[i]); if (r < 0) return r; } - printf("\nSee the %s for details.\n", link); + help_man_page_reference("systemd-cryptenroll", "1"); return 0; } @@ -332,6 +397,32 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + OPTION_LONG("firstboot", NULL, + "Interactively enroll a credential (first-boot wizard)"): + arg_firstboot = true; + break; + + OPTION_LONG("prompt-suppress", "TYPE1,TYPE2,…", + "Skip the --firstboot wizard if a slot of any listed type exists"): + r = parse_prompt_suppress(opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("chrome", "BOOL", + "In first-boot mode: if false don't show colour bar at top and bottom of terminal"): + r = parse_boolean_argument("--chrome=", opts.arg, &arg_chrome); + if (r < 0) + return r; + break; + + OPTION_LONG("mute-console", "BOOL", + "In first-boot mode, tell kernel/PID 1 to not write to the console while running"): + r = parse_boolean_argument("--mute-console=", opts.arg, &arg_mute_console); + if (r < 0) + return r; + break; + OPTION_GROUP("Unlocking"): {} OPTION_LONG("unlock-empty", NULL, "Use an empty password to unlock the volume"): @@ -625,6 +716,17 @@ static int parse_argv(int argc, char *argv[]) { if (option_parser_get_n_args(&opts) > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments, refusing."); + if (arg_firstboot) { + if (arg_enroll_type >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--firstboot may not be combined with an explicit enrollment type, refusing."); + if (wipe_requested()) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--firstboot may not be combined with --wipe-slot=, refusing."); + } else if (arg_prompt_suppress_mask != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--prompt-suppress= is only useful together with --firstboot, refusing."); + const char *arg = option_parser_get_arg(&opts, 0); if (arg) r = parse_path_argument(arg, false, &arg_node); @@ -932,8 +1034,27 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - /* If we are called without anything to enroll, we just need the LUKS device, not the volume key. */ - if (c.enroll_type < 0) { + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL; + + /* Ensure the interactive chrome (drawn by cryptenroll_run_interactive() in --firstboot mode) is + * always torn down on exit; chrome_hide() is a no-op if no chrome was shown. */ + DEFER_VOID_CALL(chrome_hide); + + if (arg_firstboot) { + assert(c.enroll_type < 0); + + r = cryptenroll_run_interactive( + &c, + arg_prompt_suppress_mask, + arg_chrome, + arg_mute_console ? &mute_console_link : NULL); + if (r <= 0) + return r; + + assert(c.enroll_type >= 0); + + } else if (c.enroll_type < 0) { + /* If we are called without anything to enroll, we just need the LUKS device, not the volume key. */ r = prepare_luks(&c, &cd, /* ret_volume_key= */ NULL); if (r < 0) return r; @@ -948,19 +1069,27 @@ static int run(int argc, char *argv[]) { r = prepare_luks(&c, &cd, &vk); if (r < 0) - return r; + goto finish; slot = enroll_now(&c, cd, &vk, /* ret_recovery_key= */ NULL); - if (slot < 0) - return slot; + if (slot < 0) { + r = slot; + goto finish; + } /* After we completed enrolling, remove user selected slots (keeping the one we just added) */ c.wipe_except_slot = slot; r = wipe_slots(&c, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL); if (r < 0) - return r; + goto finish; - return 0; + r = 0; + +finish: + if (arg_firstboot) + (void) any_key_to_proceed(); + + return r; } DEFINE_MAIN_FUNCTION(run); diff --git a/src/cryptenroll/meson.build b/src/cryptenroll/meson.build index 990b3dbc17..3fbb5bf080 100644 --- a/src/cryptenroll/meson.build +++ b/src/cryptenroll/meson.build @@ -7,6 +7,7 @@ endif systemd_cryptenroll_sources = files( 'cryptenroll.c', 'cryptenroll-fido2.c', + 'cryptenroll-interactive.c', 'cryptenroll-list.c', 'cryptenroll-password.c', 'cryptenroll-pkcs11.c', diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index a7c776d0ce..4a0d7e6d64 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -1319,6 +1319,109 @@ finish: #endif } +void fido2_device_info_done(Fido2DeviceInfo *d) { + assert(d); + + d->path = mfree(d->path); + d->manufacturer = mfree(d->manufacturer); + d->product = mfree(d->product); +} + +void fido2_device_info_free_many(Fido2DeviceInfo *a, size_t n) { + FOREACH_ARRAY(i, a, n) + fido2_device_info_done(i); + + free(a); +} + +int fido2_enumerate_devices(Fido2DeviceInfo **ret, size_t *ret_n) { +#if HAVE_LIBFIDO2 + Fido2DeviceInfo *devices = NULL; + size_t n_devices = 0, allocated = 64, found = 0; + fido_dev_info_t *di = NULL; + int r; + + CLEANUP_ARRAY(devices, n_devices, fido2_device_info_free_many); + + assert(ret); + assert(ret_n); + + r = dlopen_libfido2(LOG_ERR); + if (r < 0) + return r; + + di = sym_fido_dev_info_new(allocated); + if (!di) + return log_oom(); + + r = sym_fido_dev_info_manifest(di, allocated, &found); + if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { + /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ + r = 0; + goto finish; + } + if (r != FIDO_OK) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); + goto finish; + } + + for (size_t i = 0; i < found; i++) { + bool has_rk, has_client_pin, has_up, has_uv, has_always_uv; + const fido_dev_info_t *entry; + const char *path; + + entry = sym_fido_dev_info_ptr(di, i); + if (!entry) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get device information for FIDO device %zu.", i); + goto finish; + } + + path = sym_fido_dev_info_path(entry); + + r = check_device_is_fido2_with_hmac_secret(path, &has_rk, &has_client_pin, &has_up, &has_uv, &has_always_uv); + if (r < 0) + goto finish; + if (r == 0) /* Not a FIDO2 device suitable for enrollment, skip it */ + continue; + + if (!GREEDY_REALLOC(devices, n_devices + 1)) { + r = log_oom(); + goto finish; + } + + Fido2DeviceInfo *d = devices + n_devices; + *d = (Fido2DeviceInfo) {}; + + /* The manufacturer / product strings are optional (NULL when the device doesn't report + * them), but a failure to duplicate any string is a genuine OOM and thus fatal. */ + if (strdup_to(&d->path, path) < 0 || + strdup_to(&d->manufacturer, empty_to_null(sym_fido_dev_info_manufacturer_string(entry))) < 0 || + strdup_to(&d->product, empty_to_null(sym_fido_dev_info_product_string(entry))) < 0) { + fido2_device_info_done(d); + r = log_oom(); + goto finish; + } + + n_devices++; + } + + r = 0; + +finish: + sym_fido_dev_info_free(&di, allocated); + if (r < 0) + return r; + + *ret = TAKE_PTR(devices); + *ret_n = n_devices; + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 tokens not supported on this build."); +#endif +} + int fido2_find_device_auto(char **ret) { #if HAVE_LIBFIDO2 _cleanup_free_ char *copy = NULL; diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index 6b23b62688..12a0201332 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -137,4 +137,17 @@ static inline int parse_fido2_algorithm(const char *s, int *ret) { int fido2_list_devices(void); int fido2_find_device_auto(char **ret); +typedef struct Fido2DeviceInfo { + char *path; + char *manufacturer; /* may be NULL if the device doesn't report it */ + char *product; /* may be NULL if the device doesn't report it */ +} Fido2DeviceInfo; + +void fido2_device_info_done(Fido2DeviceInfo *d); +void fido2_device_info_free_many(Fido2DeviceInfo *a, size_t n); + +/* Enumerates the FIDO2 tokens that implement the 'hmac-secret' extension (i.e. are suitable for + * enrollment), returning a newly allocated array. */ +int fido2_enumerate_devices(Fido2DeviceInfo **ret, size_t *ret_n); + int fido2_have_device(const char *device); diff --git a/units/meson.build b/units/meson.build index f813426216..60ce3bf0ca 100644 --- a/units/meson.build +++ b/units/meson.build @@ -347,6 +347,11 @@ units = [ 'file' : 'systemd-cryptenroll@.service', 'conditions' : ['HAVE_LIBCRYPTSETUP'], }, + { + 'file' : 'systemd-cryptenroll-firstboot.service', + 'conditions' : ['HAVE_LIBCRYPTSETUP', 'ENABLE_INITRD'], + 'symlinks' : ['initrd.target.wants/'], + }, { 'file' : 'systemd-exit.service' }, { 'file' : 'systemd-factory-reset@.service.in' }, { diff --git a/units/systemd-cryptenroll-firstboot.service b/units/systemd-cryptenroll-firstboot.service new file mode 100644 index 0000000000..ccd69ef60f --- /dev/null +++ b/units/systemd-cryptenroll-firstboot.service @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +# Runs from the initrd: once systemd-repart has created/grown the partitions +# (including any encrypted ones) on first boot, but before we transition into +# the host, give the user a chance to interactively enroll a disk encryption +# credential. + +[Unit] +Description=Interactive Disk Encryption Setup +Documentation=man:systemd-cryptenroll(1) + +ConditionPathExists=/etc/initrd-release + +# We want a first boot check, but in the initrd that's not a defined +# state. Hence let's look into the mounted image directly for the primary +# indicator for the first boot state +ConditionFileNotEmpty=!/sysroot/etc/machine-id +ConditionPathIsEncrypted=/sysroot/var + +DefaultDependencies=no +After=systemd-repart.service systemd-vconsole-setup.service systemd-mute-console.socket initrd-fs.target +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target initrd.target + +[Service] +Type=oneshot +RemainAfterExit=yes +# Ignore failures: there might not be an encrypted volume to enroll into, or the +# user might decline, neither of which should hold up the boot. +ExecStart=-systemd-cryptenroll --firstboot --prompt-suppress=password,recovery,fido2 --unlock-headless --mute-console=yes +StandardOutput=tty +StandardInput=tty +StandardError=tty +TTYReset=yes From 47dc8abf8bed3983a9c5003669f7b5fb03b58f03 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 22 Jun 2026 14:27:02 +0200 Subject: [PATCH 10/13] cryptenroll: refuse reading a key longer than 4 MiB If we open this up to external processes let's tighten rules and refuse reading more than 4 MiB as key, after all this is locked memory. --- src/cryptenroll/cryptenroll-password.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c index 595279c80d..22b64e3229 100644 --- a/src/cryptenroll/cryptenroll-password.c +++ b/src/cryptenroll/cryptenroll-password.c @@ -55,8 +55,8 @@ int load_volume_key_keyfile( AT_FDCWD, c->unlock_keyfile, UINT64_MAX, - SIZE_MAX, - READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, + 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); From 2d691049aeb51d7c6c3c25f3aca7d96121f74ebf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 28 May 2026 14:23:04 +0200 Subject: [PATCH 11/13] test: cover the io.systemd.CryptEnroll Varlink interface Extend the existing systemd-cryptenroll test with varlinkctl invocations equivalent to the command line ones: enrolling a recovery key and passwords (unlocking via a key file by path and via a passed file descriptor), listing slots, combining enrollment with a type-based wipe, and the negative cases (ListSlots without 'more', and the pkcs11/tpm2 mechanisms that are not part of the EnrollMechanism allowlist). --- test/units/TEST-70-TPM2.cryptenroll.sh | 42 +++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) 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" From 65079c0a39cdc402776dcbc0f6b6ee8125929376 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Jun 2026 11:01:43 +0200 Subject: [PATCH 12/13] shell-completion: catch up with cryptenroll command line --- shell-completion/bash/systemd-cryptenroll | 12 +++- shell-completion/zsh/_systemd-cryptenroll | 74 +++++++++++++++++++++++ shell-completion/zsh/meson.build | 1 + 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 shell-completion/zsh/_systemd-cryptenroll 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', ''], From 2868b61746ead4bd0ddda8f080f47e8a6fa2dd78 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 12:40:47 +0200 Subject: [PATCH 13/13] update TODO --- TODO.md | 4 ---- 1 file changed, 4 deletions(-) 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