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