From c753849fd66d00cc856249b58319041fe79fc810 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Tue, 8 Sep 2026 11:54:55 +0200 Subject: [PATCH 1/3] cryptenroll-varlink: Allow handing in a recovery key In some cases we want users to copy the recovery key to a save location before we enroll it, therefore allow clients to hand in a recovery key. The next commit will add a varlink method to generate the recovery key. Closes: https://github.com/systemd/systemd/issues/43676 --- src/cryptenroll/cryptenroll-recovery.c | 12 +++++++++--- src/cryptenroll/cryptenroll-varlink.c | 17 +++++++++++++++++ src/cryptenroll/cryptenroll.c | 1 + src/cryptenroll/cryptenroll.h | 5 +++++ src/shared/varlink-io.systemd.CryptEnroll.c | 4 +++- 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/cryptenroll/cryptenroll-recovery.c b/src/cryptenroll/cryptenroll-recovery.c index da7b7a1018..69aa4fca57 100644 --- a/src/cryptenroll/cryptenroll-recovery.c +++ b/src/cryptenroll/cryptenroll-recovery.c @@ -29,9 +29,15 @@ int enroll_recovery( assert_se(node = sym_crypt_get_device_name(cd)); - r = make_recovery_key(&password); - if (r < 0) - return log_error_errno(r, "Failed to generate recovery key: %m"); + if (c->recovery_key) { + password = memdup_suffix0(c->recovery_key, c->recovery_key_size); + if (!password) + return log_oom(); + } else { + r = make_recovery_key(&password); + if (r < 0) + return log_error_errno(r, "Failed to generate recovery key: %m"); + } r = cryptsetup_set_minimal_pbkdf(cd); if (r < 0) diff --git a/src/cryptenroll/cryptenroll-varlink.c b/src/cryptenroll/cryptenroll-varlink.c index d268eb9956..366845569e 100644 --- a/src/cryptenroll/cryptenroll-varlink.c +++ b/src/cryptenroll/cryptenroll-varlink.c @@ -47,6 +47,7 @@ typedef struct MethodEnrollParameters { char *unlock_fido2_device; char *unlock_tpm2_device; char *password; + char *recovery_key; char *fido2_device; char *fido2_pin; int fido2_with_client_pin; @@ -65,6 +66,7 @@ static void method_enroll_parameters_done(MethodEnrollParameters *p) { free(p->unlock_fido2_device); free(p->unlock_tpm2_device); erase_and_free(p->password); + erase_and_free(p->recovery_key); free(p->fido2_device); erase_and_free(p->fido2_pin); sd_json_variant_unref(p->wipe_slots); @@ -163,6 +165,7 @@ static int vl_method_enroll( { "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 }, + { "recoveryKey", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, recovery_key), 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 }, @@ -288,6 +291,20 @@ static int vl_method_enroll( break; case ENROLL_RECOVERY: + if (p.recovery_key) { + _cleanup_(erase_and_freep) char *mangled = NULL; + + r = normalize_recovery_key(p.recovery_key, &mangled); + if (r == -EINVAL) /* Not properly formatted, rejecting */ + return sd_varlink_error_invalid_parameter_name(link, "recoveryKey"); + if (r < 0) + return log_error_errno(r, "Failed to normalize recovery key: %m"); + if (!streq(p.recovery_key, mangled)) + return sd_varlink_error_invalid_parameter_name(link, "recoveryKey"); + + c.recovery_key = TAKE_PTR(p.recovery_key); + c.recovery_key_size = strlen(c.recovery_key); + } break; case ENROLL_FIDO2: diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index 02f52ee190..208fcc43c6 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -176,6 +176,7 @@ void enroll_context_done(EnrollContext *c) { 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->recovery_key = erase_and_free(c->recovery_key); 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); diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index 60073278b2..2915b9c1e8 100644 --- a/src/cryptenroll/cryptenroll.h +++ b/src/cryptenroll/cryptenroll.h @@ -66,6 +66,11 @@ typedef struct EnrollContext { char *passphrase; size_t passphrase_size; + /* New recovery key to enroll (mechanism == recovery). When NULL the helpers fall back to + * generate a new recovery key. */ + char *recovery_key; + size_t recovery_key_size; + /* FIDO2 */ char *fido2_device; char *fido2_salt_file; diff --git a/src/shared/varlink-io.systemd.CryptEnroll.c b/src/shared/varlink-io.systemd.CryptEnroll.c index 6855299e76..5ff9110437 100644 --- a/src/shared/varlink-io.systemd.CryptEnroll.c +++ b/src/shared/varlink-io.systemd.CryptEnroll.c @@ -42,6 +42,8 @@ static SD_VARLINK_DEFINE_METHOD_FULL( 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("A recovery key to enroll, when mechanism is 'recovery'. The key must be in the normalized form and have high entropy. Contains key material."), + SD_VARLINK_DEFINE_INPUT(recoveryKey, 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), @@ -65,7 +67,7 @@ static SD_VARLINK_DEFINE_METHOD_FULL( 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_FIELD_COMMENT("The generated recovery key or the provided recovery key, when mechanism is 'recovery'. 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)); From 9569d721829bc2655b672c15756d31867741eda4 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Tue, 8 Sep 2026 11:58:41 +0200 Subject: [PATCH 2/3] cryptenroll-varlink: Add method to generate a recovery-key We want users to use a recovery-key generated by systemd. Therefore expose a method that let's clients generate a key that can be given to the Enroll method. This way the key can be displayed before enrolling it. --- src/cryptenroll/cryptenroll-varlink.c | 36 +++++++++++++++++++-- src/shared/varlink-io.systemd.CryptEnroll.c | 9 +++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/cryptenroll/cryptenroll-varlink.c b/src/cryptenroll/cryptenroll-varlink.c index 366845569e..d744d2cf74 100644 --- a/src/cryptenroll/cryptenroll-varlink.c +++ b/src/cryptenroll/cryptenroll-varlink.c @@ -15,6 +15,7 @@ #include "json-util.h" #include "libfido2-util.h" #include "path-util.h" +#include "recovery-key.h" #include "string-util.h" #include "varlink-io.systemd.CryptEnroll.h" #include "varlink-util.h" @@ -467,6 +468,36 @@ static int vl_method_list_slots( return sd_varlink_reply(link, NULL); } +static int vl_method_make_recovery_key( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + _cleanup_(erase_and_freep) char *recovery_key = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *rk = NULL; + int r; + + assert(link); + assert(parameters); + + r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL); + if (r != 0) + return r; + + r = make_recovery_key(&recovery_key); + if (r < 0) + return r; + + 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_VARIANT("recoveryKey", rk)); +} + int cryptenroll_varlink_server(void) { _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; _cleanup_hashmap_free_ Hashmap *polkit_registry = NULL; @@ -485,8 +516,9 @@ int cryptenroll_varlink_server(void) { r = sd_varlink_server_bind_method_many( varlink_server, - "io.systemd.CryptEnroll.Enroll", vl_method_enroll, - "io.systemd.CryptEnroll.ListSlots", vl_method_list_slots); + "io.systemd.CryptEnroll.Enroll", vl_method_enroll, + "io.systemd.CryptEnroll.ListSlots", vl_method_list_slots, + "io.systemd.CryptEnroll.MakeRecoveryKey", vl_method_make_recovery_key); if (r < 0) return log_error_errno(r, "Failed to bind Varlink methods: %m"); diff --git a/src/shared/varlink-io.systemd.CryptEnroll.c b/src/shared/varlink-io.systemd.CryptEnroll.c index 5ff9110437..391861b0e8 100644 --- a/src/shared/varlink-io.systemd.CryptEnroll.c +++ b/src/shared/varlink-io.systemd.CryptEnroll.c @@ -42,7 +42,7 @@ static SD_VARLINK_DEFINE_METHOD_FULL( 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("A recovery key to enroll, when mechanism is 'recovery'. The key must be in the normalized form and have high entropy. Contains key material."), + SD_VARLINK_FIELD_COMMENT("A recovery key to enroll, when mechanism is 'recovery'. The key must be in the normalized form and have high entropy. Use MakeRecoveryKey() to generate the recovery key. Contains key material."), SD_VARLINK_DEFINE_INPUT(recoveryKey, 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."), @@ -82,6 +82,11 @@ static SD_VARLINK_DEFINE_METHOD_FULL( SD_VARLINK_FIELD_COMMENT("The type of the keyslot."), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, EnrollMechanism, SD_VARLINK_NULLABLE)); +static SD_VARLINK_DEFINE_METHOD( + MakeRecoveryKey, + SD_VARLINK_FIELD_COMMENT("The generated recovery key."), + SD_VARLINK_DEFINE_OUTPUT(recoveryKey, SD_VARLINK_STRING, 0)); + static SD_VARLINK_DEFINE_ERROR(VolumeUnderForeignManagement); static SD_VARLINK_DEFINE_ERROR(PasswordRequired); static SD_VARLINK_DEFINE_ERROR(PasswordIncorrect); @@ -98,6 +103,8 @@ SD_VARLINK_DEFINE_INTERFACE( &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("Generate a recovery key that can be enrolled using the 'recovery' mechanism in Enroll()."), + &vl_method_MakeRecoveryKey, 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."), From 4b259b5630a7d35e5b349fcf44229c3b02875479 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Wed, 9 Sep 2026 17:06:50 +0200 Subject: [PATCH 3/3] test: cryptenroll-varlink: Add tests for handing in a recovery key This adds two new tests to test the validation of the recovery key passed to `Enroll()` and the generation of a recovery key by `MakeRecoveryKey()`. --- test/units/TEST-70-TPM2.cryptenroll.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/units/TEST-70-TPM2.cryptenroll.sh b/test/units/TEST-70-TPM2.cryptenroll.sh index e8a6441b61..eff151cb98 100755 --- a/test/units/TEST-70-TPM2.cryptenroll.sh +++ b/test/units/TEST-70-TPM2.cryptenroll.sh @@ -113,6 +113,20 @@ cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-uran varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"recovery\",\"unlockKeyFile\":\"/tmp/password\"}" | grep recoveryKey >/dev/null +# Enroll a recovery key generated via MakeRecoveryKey(), unlocking via key file (cf. systemd-cryptenroll --unlock-key-file= --recovery-key) +RECOVERY_KEY=$(varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.MakeRecoveryKey "{}" | jq -e -r '.recoveryKey') + +RESULT_RECOVERY_KEY=$(varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ + "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"recovery\",\"recoveryKey\":\"$RECOVERY_KEY\",\"unlockKeyFile\":\"/tmp/password\"}" | jq -e -r '.recoveryKey') +test "$RECOVERY_KEY" = "$RESULT_RECOVERY_KEY" + +# Ensure that the enrolled recovery key works +echo -n "$RECOVERY_KEY" | cryptsetup open --type=luks2 --test-passphrase --key-file=- "$VL_IMAGE" + +# Fail to enroll a malformed 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\",\"recoveryKey\":\"SOME_RANDOM_KEY\",\"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\"}" @@ -121,9 +135,10 @@ varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ 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 +# List enrolled slots (must be called with 'more'); we should see the password and the two recovery slots we enrolled +SLOTS=$(varlinkctl call --more "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}") +test "$(grep -c '"type":"recovery"' <<<"$SLOTS")" -eq 2 +grep '"type":"password"' <<<"$SLOTS" >/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.