From f1b6417fea8ea1fb9a57f45b845ab1db944eca23 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 00:04:03 +0100 Subject: [PATCH] homed: add apis for managing home signing keys This makes it easier to actually migrate home directories between systems. --- man/org.freedesktop.home1.xml | 37 +++ src/home/homed-manager-bus.c | 292 ++++++++++++++++++++++ src/home/homed-manager.c | 8 +- src/home/homed-manager.h | 2 + src/home/org.freedesktop.home1.conf | 16 ++ src/home/org.freedesktop.home1.policy | 10 + src/libsystemd/sd-bus/bus-common-errors.c | 1 + src/libsystemd/sd-bus/bus-common-errors.h | 1 + 8 files changed, 361 insertions(+), 6 deletions(-) diff --git a/man/org.freedesktop.home1.xml b/man/org.freedesktop.home1.xml index 403778288d7..e0e9eef982b 100644 --- a/man/org.freedesktop.home1.xml +++ b/man/org.freedesktop.home1.xml @@ -112,6 +112,15 @@ node /org/freedesktop/home1 { out h send_fd); @org.freedesktop.systemd1.Privileged("true") ReleaseHome(in s user_name); + ListSigningKeys(out a(sst) keys); + GetSigningKey(in s name, + out s der, + out t flags); + AddSigningKey(in s name, + in s pem, + in t flags); + RemoveSigningKey(in s name, + in t flags); @org.freedesktop.systemd1.Privileged("true") LockAllHomes(); @org.freedesktop.systemd1.Privileged("true") @@ -185,6 +194,14 @@ node /org/freedesktop/home1 { + + + + + + + + @@ -426,6 +443,23 @@ node /org/freedesktop/home1 { Rebalance() synchronously rebalances free disk space between home areas. This only executes an operation if at least one home area using the LUKS2 backend is active and has rebalancing enabled, and is otherwise a NOP. + + ListSigningKeys() acquires a list of installed home area signing + keys. Returns an array of key names with their PEM encoded public key data. Each entry also comes with + a flags value which is currently unused and should be ignored by clients. + + GetSigningKey() acquires the PEM encoded public part of the specified home + area signing key of the specified name. Also returns a currently unused flags value that should be + ignored. The flags parameter must be set to zero, currently. + + AddSigningKey() adds a new key to the list of home area signing keys. Takes + a name string (free-form, suitable as filename, with suffix .public), the PEM + encoded public key data and a currently unused flags value that must be zero. The + flags parameter must be set to zero, currently. + + RemoveSigningKey() removes a key from the list of home area signing + keys. Takes the name of the key to remove and a currently unused flags value that must be zero. The + flags parameter must be set to zero, currently. @@ -599,6 +633,9 @@ node /org/freedesktop/home1/home { The Manager Object ActivateHomeIfReferenced(), RefHomeUnrestricted(), CreateHomeEx(), and UpdateHomeEx() were added in version 256. + ListSigningKeys(), GetSigningKey(), + AddSigningKey(), and RemoveSigningKey() were added in version + 258. Home Objects diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c index 726a12e54b9..5966758a561 100644 --- a/src/home/homed-manager-bus.c +++ b/src/home/homed-manager-bus.c @@ -6,12 +6,15 @@ #include "bus-common-errors.h" #include "bus-message-util.h" #include "bus-polkit.h" +#include "fileio.h" #include "format-util.h" #include "home-util.h" #include "homed-bus.h" #include "homed-home-bus.h" #include "homed-manager-bus.h" #include "homed-manager.h" +#include "openssl-util.h" +#include "path-util.h" #include "strv.h" #include "user-record-sign.h" #include "user-record-util.h" @@ -753,6 +756,274 @@ static int method_rebalance(sd_bus_message *message, void *userdata, sd_bus_erro return 1; } +static int method_list_signing_keys(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(sst)"); + if (r < 0) + return r; + + /* Add our own key pair first */ + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + + _cleanup_free_ char *pem = NULL; + r = openssl_pubkey_to_pem(m->private_key, &pem); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + r = sd_bus_message_append( + reply, + "(sst)", + "local.public", + pem, + UINT64_C(0)); + if (r < 0) + return r; + + /* And then all public keys we recognize */ + EVP_PKEY *pkey; + const char *fn; + HASHMAP_FOREACH_KEY(pkey, fn, m->public_keys) { + pem = mfree(pem); + r = openssl_pubkey_to_pem(pkey, &pem); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + r = sd_bus_message_append( + reply, + "(sst)", + fn, + pem, + UINT64_C(0)); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(/* bus= */ NULL, reply, /* ret_cookie= */ NULL); +} + +static int method_get_signing_key(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *fn; + r = sd_bus_message_read(message, "s", &fn); + if (r < 0) + return r; + + /* Make sure the local key is loaded. */ + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + + EVP_PKEY *pkey; + + if (streq(fn, "local.public")) + pkey = m->private_key; + else + pkey = hashmap_get(m->public_keys, fn); + if (!pkey) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_KEY, "No key with name: %s", fn); + + _cleanup_free_ char *pem = NULL; + r = openssl_pubkey_to_pem(pkey, &pem); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append( + reply, + "st", + pem, + UINT64_C(0)); + if (r < 0) + return r; + + return sd_bus_send(/* bus= */ NULL, reply, /* ret_cookie= */ NULL); +} + +static bool valid_public_key_name(const char *fn) { + assert(fn); + + /* Checks if the specified name is valid to export, i.e. is a filename, ends in ".public". */ + + if (!filename_is_valid(fn)) + return false; + + const char *e = endswith(fn, ".public"); + if (!e) + return false; + + return e != fn; +} + +static bool manager_has_public_key(Manager *m, EVP_PKEY *needle) { + int r; + + assert(m); + + EVP_PKEY *pkey; + HASHMAP_FOREACH(pkey, m->public_keys) { + r = EVP_PKEY_eq(pkey, needle); + if (r > 0) + return true; + + /* EVP_PKEY_eq() returns -1 and -2 too under some conditions, which we'll all treat as "not the same" */ + } + + r = EVP_PKEY_eq(m->private_key, needle); + if (r > 0) + return true; + + return false; +} + +static int method_add_signing_key(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *fn, *pem; + uint64_t flags; + r = sd_bus_message_read(message, "sst", &fn, &pem, &flags); + if (r < 0) + return r; + + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags parameter must be zero."); + if (!valid_public_key_name(fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name not valid: %s", fn); + if (streq(fn, "local.public")) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Refusing to write local public key."); + + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + r = openssl_pubkey_from_pem(pem, /* pem_size= */ SIZE_MAX, &pkey); + if (r == -EIO) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key invalid: %s", fn); + if (r < 0) + return r; + + if (hashmap_contains(m->public_keys, fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name already exists: %s", fn); + + /* Make sure the local key is loaded before can detect conflicts */ + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + + if (manager_has_public_key(m, pkey)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key already exists: %s", fn); + + r = bus_verify_polkit_async( + message, + "org.freedesktop.home1.manage-signing-keys", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + _cleanup_free_ char *pem_reformatted = NULL; + r = openssl_pubkey_to_pem(pkey, &pem_reformatted); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + _cleanup_free_ char *fn_copy = strdup(fn); + if (!fn) + return log_oom(); + + _cleanup_free_ char *p = path_join("/var/lib/systemd/home/", fn); + if (!p) + return log_oom(); + + r = write_string_file(p, pem_reformatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_MODE_0444); + if (r < 0) + return log_error_errno(r, "Failed to write public key PEM to '%s': %m", p); + + r = hashmap_ensure_put(&m->public_keys, &public_key_hash_ops, fn_copy, pkey); + if (r < 0) { + (void) unlink(p); + return log_error_errno(r, "Failed to add public key to set: %m"); + } + + TAKE_PTR(fn_copy); + TAKE_PTR(pkey); + + return sd_bus_reply_method_return(message, NULL); +} + +static int method_remove_signing_key(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *fn; + uint64_t flags; + r = sd_bus_message_read(message, "st", &fn, &flags); + if (r < 0) + return r; + + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags parameter must be zero."); + + if (!valid_public_key_name(fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name not valid: %s", fn); + + if (streq(fn, "local.public")) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Refusing to remove local key."); + + if (!hashmap_contains(m->public_keys, fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name does not exist: %s", fn); + + r = bus_verify_polkit_async( + message, + "org.freedesktop.home1.manage-signing-keys", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + _cleanup_free_ char *p = path_join("/var/lib/systemd/home/", fn); + if (!p) + return log_oom(); + + if (unlink(p) < 0) + return log_error_errno(errno, "Failed to remove '%s': %m", p); + + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + _cleanup_free_ char *fn_free = NULL; + pkey = ASSERT_PTR(hashmap_remove2(m->public_keys, fn, (void**) &fn_free)); + + return sd_bus_reply_method_return(message, NULL); +} + static const sd_bus_vtable manager_vtable[] = { SD_BUS_VTABLE_START(0), @@ -934,6 +1205,27 @@ static const sd_bus_vtable manager_vtable[] = { method_release_home, 0), + SD_BUS_METHOD_WITH_ARGS("ListSigningKeys", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("a(sst)", keys), + method_list_signing_keys, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("GetSigningKey", + SD_BUS_RESULT("s", name), + SD_BUS_RESULT("s", der, "t", flags), + method_get_signing_key, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("AddSigningKey", + SD_BUS_RESULT("s", name, "s", pem, "t", flags), + SD_BUS_NO_RESULT, + method_add_signing_key, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RemoveSigningKey", + SD_BUS_RESULT("s", name, "t", flags), + SD_BUS_NO_RESULT, + method_remove_signing_key, + SD_BUS_VTABLE_UNPRIVILEGED), + /* An operation that acts on all homes that allow it */ SD_BUS_METHOD("LockAllHomes", NULL, NULL, method_lock_all_homes, 0), SD_BUS_METHOD("DeactivateAllHomes", NULL, NULL, method_deactivate_all_homes, 0), diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index 0a94128230b..a4512b8d266 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -1446,7 +1446,7 @@ int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus return user_record_sign(u, m->private_key, ret); } -DEFINE_PRIVATE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free); +DEFINE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free); static int manager_load_public_key_one(Manager *m, const char *path) { _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; @@ -1482,15 +1482,11 @@ static int manager_load_public_key_one(Manager *m, const char *path) { if (st.st_uid != 0 || (st.st_mode & 0022) != 0) return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Public key file %s is writable by more than the root user, refusing.", path); - r = hashmap_ensure_allocated(&m->public_keys, &public_key_hash_ops); - if (r < 0) - return log_oom(); - pkey = PEM_read_PUBKEY(f, &pkey, NULL, NULL); if (!pkey) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key file %s.", path); - r = hashmap_put(m->public_keys, fn, pkey); + r = hashmap_ensure_put(&m->public_keys, &public_key_hash_ops, fn, pkey); if (r < 0) return log_error_errno(r, "Failed to add public key to set: %m"); diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h index 9fea6210311..8f2c3d2fd7f 100644 --- a/src/home/homed-manager.h +++ b/src/home/homed-manager.h @@ -93,3 +93,5 @@ int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus int bus_manager_emit_auto_login_changed(Manager *m); int manager_get_home_by_name(Manager *m, const char *user_name, Home **ret); + +extern const struct hash_ops public_key_hash_ops; diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf index b8085929b01..adf85aa5cf0 100644 --- a/src/home/org.freedesktop.home1.conf +++ b/src/home/org.freedesktop.home1.conf @@ -149,6 +149,22 @@ send_interface="org.freedesktop.home1.Manager" send_member="Rebalance"/> + + + + + + + + auth_admin_keep + + + Manage Home Directory Signing Keys + Authentication is required to manage signing keys for home directories. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index cb5c1b74d5f..c0f5aff5ea2 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -150,6 +150,7 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { SD_BUS_ERROR_MAP(BUS_ERROR_HOME_IN_USE, EADDRINUSE), SD_BUS_ERROR_MAP(BUS_ERROR_REBALANCE_NOT_NEEDED, EALREADY), SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_REFERENCED, EBADR), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_KEY, ENOKEY), SD_BUS_ERROR_MAP(BUS_ERROR_NO_UPDATE_CANDIDATE, EALREADY), diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index edc49027b6e..6322d68ad98 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -156,6 +156,7 @@ #define BUS_ERROR_HOME_IN_USE "org.freedesktop.home1.HomeInUse" #define BUS_ERROR_REBALANCE_NOT_NEEDED "org.freedesktop.home1.RebalanceNotNeeded" #define BUS_ERROR_HOME_NOT_REFERENCED "org.freedesktop.home1.HomeNotReferenced" +#define BUS_ERROR_NO_SUCH_KEY "org.freedesktop.home1.NoSuchKey" #define BUS_ERROR_NO_UPDATE_CANDIDATE "org.freedesktop.sysupdate1.NoCandidate" -- 2.47.3