]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homed: add apis for managing home signing keys
authorLennart Poettering <lennart@poettering.net>
Tue, 18 Feb 2025 23:04:03 +0000 (00:04 +0100)
committerLennart Poettering <lennart@poettering.net>
Fri, 7 Mar 2025 17:13:40 +0000 (18:13 +0100)
This makes it easier to actually migrate home directories between
systems.

man/org.freedesktop.home1.xml
src/home/homed-manager-bus.c
src/home/homed-manager.c
src/home/homed-manager.h
src/home/org.freedesktop.home1.conf
src/home/org.freedesktop.home1.policy
src/libsystemd/sd-bus/bus-common-errors.c
src/libsystemd/sd-bus/bus-common-errors.h

index 403778288d76db30feafaf1db3b65d584016cbe3..e0e9eef982b297d133e9304727693ea503499b1e 100644 (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>
index 726a12e54b95baffb270d73cbc682922798a0c2e..5966758a56181af494ad15c871fa16ceb466a0f3 100644 (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),
index 0a94128230b288293be8fb48fd27ec5a4f7e3738..a4512b8d266ecfd56bbf0951833f62a04428f051 100644 (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");
 
index 9fea621031155dc3c755b9371a09595df2213e53..8f2c3d2fd7fa442d8919c9f7e4db8c4c6dac5e5c 100644 (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;
index b8085929b0108a32b989387aed07f24f355556b7..adf85aa5cf0d2b30c16b8bd52ce3a2620c50b3bd 100644 (file)
                        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"
index d3317772acb45c34112767eefe04513e2e33fd72..145543fdefb935b14045e33bd49c0d97bff5a5a9 100644 (file)
                         <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>
index cb5c1b74d5fc62738e5425e4edf4f83384ba1493..c0f5aff5ea2de3dc962051ec4532235ead5fe324 100644 (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),
 
index edc49027b6e205cdb80b9ad194e72b5db9747635..6322d68ad98144ec51bf6746f45a7fb98206cad7 100644 (file)
 #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"