From: Lennart Poettering Date: Thu, 28 May 2026 11:32:13 +0000 (+0200) Subject: cryptenroll: expose enrollment as an io.systemd.CryptEnroll Varlink service X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0df41e9329d6425fbf422483e29e3f7e2b6a33b8;p=thirdparty%2Fsystemd.git 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 --- diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c index 7500e84c181..b315282df1e 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 ab7637e6163..21d768b1a8a 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 9097bc42c12..33663367139 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 00000000000..77ead096731 --- /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 00000000000..89efede84ba --- /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 b7d554c3904..39e8593973a 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 4e715191818..cb3dd719331 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 c9f17c761fc..cf67973fbf4 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,89 +849,118 @@ static int enroll_context_from_args(EnrollContext *c) { 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(); - - r = parse_argv(argc, argv); - if (r <= 0) - return r; - - r = DLOPEN_CRYPTSETUP(LOG_ERR, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED); - if (r < 0) - return r; - - /* A delicious drop of snake oil */ - (void) safe_mlockall(MCL_CURRENT|MCL_FUTURE|MCL_ONFAULT); +int enroll_now( + const EnrollContext *c, + struct crypt_device *cd, + const struct iovec *volume_key, + char **ret_recovery_key) { - r = enroll_context_from_args(&c); - if (r < 0) - return r; + int slot, slot_to_wipe = -1, 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; + assert(c); + assert(cd); + assert(iovec_is_set(volume_key)); - switch (c.enroll_type) { + switch (c->enroll_type) { case ENROLL_PASSWORD: - slot = enroll_password(&c, cd, &vk); - break; + return enroll_password(c, cd, volume_key); case ENROLL_RECOVERY: - slot = enroll_recovery(&c, cd, &vk, /* ret_recovery_key= */ NULL); - break; + return enroll_recovery(c, cd, volume_key, ret_recovery_key); case ENROLL_PKCS11: - slot = enroll_pkcs11(&c, cd, &vk); - break; + return enroll_pkcs11(c, cd, volume_key); case ENROLL_FIDO2: - slot = enroll_fido2(&c, cd, &vk); - break; + return enroll_fido2(c, cd, volume_key); case ENROLL_TPM2: - slot = enroll_tpm2(&c, cd, &vk, &slot_to_wipe); + slot = enroll_tpm2(c, cd, volume_key, &slot_to_wipe); + if (slot < 0) + return slot; - if (slot >= 0 && slot_to_wipe >= 0) { + if (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; + /* 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); + r = wipe_slots(&wipe_ctx, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL); if (r < 0) return r; } - break; - case _ENROLL_TYPE_INVALID: + + 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, 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; + + r = DLOPEN_CRYPTSETUP(LOG_ERR, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED); + if (r < 0) + return r; + + r = enroll_context_from_args(&c); + 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; + /* 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 b675023ee38..36aa852f424 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 00000000000..80efa19957a --- /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 aa789c0a071..990b3dbc17e 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 a1ed9a0f429..ae4a6d21b80 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 def42815c7f..08d36350809 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 00000000000..6855299e763 --- /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 00000000000..dcb626a47b3 --- /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 569aa2e2a2c..f8134262160 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 00000000000..0ffcdb4c8a8 --- /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 00000000000..31f42cc4585 --- /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