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..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" @@ -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"); 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..391861b0e8 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. 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."), 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.