cryptenroll-varlink: Allow to display recovery-key before enrolling (#43687)

Closes https://github.com/systemd/systemd/issues/43676
This commit is contained in:
Luca Boccassi
2026-09-23 16:28:19 +02:00
committed by GitHub
6 changed files with 94 additions and 9 deletions
+9 -3
View File
@@ -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)
+51 -2
View File
@@ -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"
@@ -47,6 +48,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 +67,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 +166,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 +292,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:
@@ -450,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;
@@ -468,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");
+1
View File
@@ -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);
+5
View File
@@ -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;
+10 -1
View File
@@ -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. 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."),
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));
@@ -80,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);
@@ -96,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."),
+18 -3
View File
@@ -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</tmp/password
# List enrolled slots (must be called with 'more'); we should see the password and recovery slots
varlinkctl call --more "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}" | grep '"type":"recovery"' >/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.