homed: add apis for managing home signing keys

This makes it easier to actually migrate home directories between
systems.
This commit is contained in:
Lennart Poettering
2025-02-19 00:04:03 +01:00
parent fd0dd2d4bc
commit f1b6417fea
8 changed files with 361 additions and 6 deletions

View File

@@ -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 {
<variablelist class="dbus-method" generated="True" extra-ref="ReleaseHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ListSigningKeys()"/>
<variablelist class="dbus-method" generated="True" extra-ref="GetSigningKey()"/>
<variablelist class="dbus-method" generated="True" extra-ref="AddSigningKey()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RemoveSigningKey()"/>
<variablelist class="dbus-method" generated="True" extra-ref="LockAllHomes()"/>
<variablelist class="dbus-method" generated="True" extra-ref="DeactivateAllHomes()"/>
@@ -426,6 +443,23 @@ node /org/freedesktop/home1 {
<para><function>Rebalance()</function> 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.</para>
<para><function>ListSigningKeys()</function> 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.</para>
<para><function>GetSigningKey()</function> 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 <varname>flags</varname> parameter must be set to zero, currently.</para>
<para><function>AddSigningKey()</function> adds a new key to the list of home area signing keys. Takes
a name string (free-form, suitable as filename, with suffix <literal>.public</literal>), the PEM
encoded public key data and a currently unused flags value that must be zero. The
<varname>flags</varname> parameter must be set to zero, currently.</para>
<para><function>RemoveSigningKey()</function> 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
<varname>flags</varname> parameter must be set to zero, currently.</para>
</refsect2>
<refsect2>
@@ -599,6 +633,9 @@ node /org/freedesktop/home1/home {
<title>The Manager Object</title>
<para><function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function>,
<function>CreateHomeEx()</function>, and <function>UpdateHomeEx()</function> were added in version 256.</para>
<para><function>ListSigningKeys()</function>, <function>GetSigningKey()</function>,
<function>AddSigningKey()</function>, and <function>RemoveSigningKey()</function> were added in version
258.</para>
</refsect2>
<refsect2>
<title>Home Objects</title>

View File

@@ -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),

View File

@@ -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");

View File

@@ -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;

View File

@@ -149,6 +149,22 @@
send_interface="org.freedesktop.home1.Manager"
send_member="Rebalance"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="ListSigningKeys"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="GetSigningKey"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="RemoveSigningKey"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="AddSigningKey"/>
<!-- Home object -->
<allow send_destination="org.freedesktop.home1"

View File

@@ -88,4 +88,14 @@
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
<action id="org.freedesktop.home1.manage-signing-keys">
<description gettext-domain="systemd">Manage Home Directory Signing Keys</description>
<message gettext-domain="systemd">Authentication is required to manage signing keys for home directories.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>

View File

@@ -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),

View File

@@ -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"