]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homectl: add signing key management verbs
authorLennart Poettering <lennart@poettering.net>
Wed, 19 Feb 2025 08:41:48 +0000 (09:41 +0100)
committerLennart Poettering <lennart@poettering.net>
Fri, 7 Mar 2025 17:14:02 +0000 (18:14 +0100)
man/homectl.xml
shell-completion/bash/homectl
src/home/homectl.c

index abcdd8852990668d3d6ba67bf4c9b80ba40523a1..568f077c05075047a38ba9a240262ed2c62b22d1 100644 (file)
         <xi:include href="version-info.xml" xpointer="v256"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--key-name=</option></term>
+
+        <listitem><para>When used with the <command>add-signing-key</command> command, specify or override
+        the name under which to store the public key being added. The specified name can be chosen freely,
+        but must be suffixed with <literal>.public</literal>. If this option is not used the name is derived
+        from the specified filename. If a key is read from standard input this option is mandatory in order
+        to provide a suitable name for the key being added.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <xi:include href="user-system-options.xml" xpointer="host" />
       <xi:include href="user-system-options.xml" xpointer="machine" />
 
 
         <xi:include href="version-info.xml" xpointer="v256"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><command>list-signing-keys</command></term>
+
+        <listitem><para>Show a list of public keys that home directories can be signed with to be allowed for
+        local login. One such key (<filename>local.public</filename>) will be generated automatically for
+        signing locally created home directories, but additional public keys may be registered to accept home
+        directories from other origins too (see <command>add-signing-key</command> below).</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>get-signing-key</command> [<replaceable>NAME…</replaceable>]</term>
+
+        <listitem><para>Write the public key identified by the specified name to standard output (in PEM
+        format). If no name is specified defaults to <filename>local.public</filename>, i.e. the
+        automatically generated key for locally created home directories.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>add-signing-key</command> [<replaceable>FILE…</replaceable>]</term>
+
+        <listitem><para>Add public key(s) from the specified PEM key file(s) to the list of keys that home
+        areas have to be signed by to be permitted for local login. If a path of <literal>-</literal> is
+        specified, or if no file is specified at all, the key will be read from standard input. The key file
+        name(s) must carry the <filename>.public</filename> suffix, and the file name(s) will be used to name
+        the key(s) once added, too. If a key is added from standard input the key name must be specified
+        explicitly via <option>--key-name=</option>, see above.</para>
+
+        <para>This command is useful for permitting local home directories to be used on a remote
+        system. Example:</para>
+
+        <programlisting>homectl get-signing-key | ssh myotherhost homectl add-signing-key --key-name="$HOSTNAME".public</programlisting>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>remove-signing-key</command> <replaceable>NAME…</replaceable></term>
+
+        <listitem><para>Remove the public key identified by the specified name from the list of keys that
+        control from which origins to permit home directories for login.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
     </variablelist>
   </refsect1>
 
index 5e2235bc3b062024f48054dcb652b0743441cb1b..6219f255948ef967337482c04ee8fbe98a1840af 100644 (file)
@@ -112,7 +112,8 @@ _homectl() {
                         --avatar
                         --login-background
                         --session-launcher
-                        --session-type'
+                        --session-type
+                        --key-name'
     )
 
     if __contains_word "$prev" ${OPTS[ARG]}; then
index a7754c22998abb7e31cc12a8a4f6209e5d926944..6857acbcc676c54118c054cd2ab7a08a2dadff2a 100644 (file)
@@ -24,6 +24,7 @@
 #include "fs-util.h"
 #include "glyph-util.h"
 #include "hashmap.h"
+#include "hexdecoct.h"
 #include "home-util.h"
 #include "homectl-fido2.h"
 #include "homectl-pkcs11.h"
@@ -33,6 +34,7 @@
 #include "locale-util.h"
 #include "main-func.h"
 #include "memory-util.h"
+#include "openssl-util.h"
 #include "pager.h"
 #include "parse-argument.h"
 #include "parse-util.h"
@@ -96,6 +98,7 @@ static bool arg_prompt_new_user = false;
 static char *arg_blob_dir = NULL;
 static bool arg_blob_clear = false;
 static Hashmap *arg_blob_files = NULL;
+static char *arg_key_name = NULL;
 
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp);
 STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp);
@@ -107,6 +110,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_key_name, freep);
 
 static const BusLocator *bus_mgr;
 
@@ -2795,6 +2799,11 @@ static int help(int argc, char *argv[], void *userdata) {
                "  rebalance                    Rebalance free space between home areas\n"
                "  with USER [COMMAND…]         Run shell or command with access to a home area\n"
                "  firstboot                    Run first-boot home area creation wizard\n"
+               "\n%4$sSigning Keys Commands:%5$s\n"
+               "  list-signing-keys            List home signing keys\n"
+               "  get-signing-key [NAME…]      Get a named home signing key\n"
+               "  add-signing-key FILE…        Add home signing key\n"
+               "  remove-signing-key NAME…     Remove home signing key\n"
                "\n%4$sOptions:%5$s\n"
                "  -h --help                    Show this help\n"
                "     --version                 Show package version\n"
@@ -2816,6 +2825,7 @@ static int help(int argc, char *argv[], void *userdata) {
                "                               -j --export-format=minimal\n"
                "     --prompt-new-user         firstboot: Query user interactively for user\n"
                "                               to create\n"
+               "     --key-name=NAME           Key name when adding a signing key\n"
                "\n%4$sGeneral User Record Properties:%5$s\n"
                "  -c --real-name=REALNAME      Real name for user\n"
                "     --realm=REALM             Realm to create user in\n"
@@ -3049,6 +3059,7 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_TMP_LIMIT,
                 ARG_DEV_SHM_LIMIT,
                 ARG_DEFAULT_AREA,
+                ARG_KEY_NAME,
         };
 
         static const struct option options[] = {
@@ -3152,6 +3163,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "tmp-limit",                    required_argument, NULL, ARG_TMP_LIMIT                   },
                 { "dev-shm-limit",                required_argument, NULL, ARG_DEV_SHM_LIMIT               },
                 { "default-area",                 required_argument, NULL, ARG_DEFAULT_AREA                },
+                { "key-name",                     required_argument, NULL, ARG_KEY_NAME                    },
                 {}
         };
 
@@ -4653,6 +4665,21 @@ static int parse_argv(int argc, char *argv[]) {
 
                         break;
 
+                case ARG_KEY_NAME:
+                        if (isempty(optarg)) {
+                                arg_key_name = mfree(arg_key_name);
+                                return 0;
+                        }
+
+                        if (!filename_is_valid(optarg))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key name not valid: %s", optarg);
+
+                        r = free_and_strdup_warn(&arg_key_name, optarg);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -4915,26 +4942,247 @@ static int fallback_shell(int argc, char *argv[]) {
         return log_error_errno(errno, "Failed to execute shell '%s': %m", shell);
 }
 
+static int verb_list_signing_keys(int argc, char *argv[], void *userdata) {
+        int r;
+
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        r = bus_call_method(bus, bus_mgr, "ListSigningKeys", &error, &reply, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to list signing keys: %s", bus_error_message(&error, r));
+
+        _cleanup_(table_unrefp) Table *table = table_new("name", "key");
+        if (!table)
+                return log_oom();
+
+        r = sd_bus_message_enter_container(reply, 'a', "(sst)");
+        if (r < 0)
+                return bus_log_parse_error(r);
+
+        for (;;) {
+                const char *name, *pem;
+
+                r = sd_bus_message_read(reply, "(sst)", &name, &pem, NULL);
+                if (r < 0)
+                        return bus_log_parse_error(r);
+                if (r == 0)
+                        break;
+
+                _cleanup_free_ char *h = NULL;
+                if (!sd_json_format_enabled(arg_json_format_flags)) {
+                        /* Let's decode the PEM key to DER (so that we lose prefix/suffix), then truncate it
+                         * for display reasons. */
+
+                        _cleanup_(EVP_PKEY_freep) EVP_PKEY *key = NULL;
+                        r = openssl_pubkey_from_pem(pem, SIZE_MAX, &key);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse PEM: %m");
+
+                        _cleanup_free_ void *der = NULL;
+                        int n = i2d_PUBKEY(key, (unsigned char**) &der);
+                        if (n < 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to encode key as DER: %m");
+
+                        ssize_t m = base64mem(der, MIN(n, 64), &h);
+                        if (m < 0)
+                                return log_oom();
+                        if (n > 64) /* check if we truncated the original version */
+                                if (!strextend(&h, special_glyph(SPECIAL_GLYPH_ELLIPSIS)))
+                                        return log_oom();
+                }
+
+                r = table_add_many(
+                                table,
+                                TABLE_STRING, name,
+                                TABLE_STRING, h ?: pem);
+                if (r < 0)
+                        return table_log_add_error(r);
+        }
+
+        r = sd_bus_message_exit_container(reply);
+        if (r < 0)
+                return bus_log_parse_error(r);
+
+        if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) {
+                r = table_set_sort(table, (size_t) 0);
+                if (r < 0)
+                        return table_log_sort_error(r);
+
+                r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
+                if (r < 0)
+                        return r;
+        }
+
+        if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) {
+                if (table_isempty(table))
+                        printf("No signing keys.\n");
+                else
+                        printf("\n%zu signing keys listed.\n", table_get_rows(table) - 1);
+        }
+
+        return 0;
+}
+
+static int verb_get_signing_key(int argc, char *argv[], void *userdata) {
+        int r;
+
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        char **keys = argc >= 2 ? strv_skip(argv, 1) : STRV_MAKE("local.public");
+        int ret = 0;
+        STRV_FOREACH(k, keys) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+                r = bus_call_method(bus, bus_mgr, "GetSigningKey", &error, &reply, "s", *k);
+                if (r < 0) {
+                        RET_GATHER(ret, log_error_errno(r, "Failed to get signing key '%s': %s", *k, bus_error_message(&error, r)));
+                        continue;
+                }
+
+                const char *pem;
+                r = sd_bus_message_read(reply, "st", &pem, NULL);
+                if (r < 0) {
+                        RET_GATHER(ret, bus_log_parse_error(r));
+                        continue;
+                }
+
+                fputs(pem, stdout);
+                if (!endswith(pem, "\n"))
+                        fputc('\n', stdout);
+
+                fflush(stdout);
+        }
+
+        return ret;
+}
+
+static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) {
+        int r;
+
+        assert_se(bus);
+        assert_se(fn);
+        assert_se(key);
+
+        _cleanup_free_ char *pem = NULL;
+        r = read_full_stream(key, &pem, /* ret_size= */ NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to read key '%s': %m", fn);
+
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        r = bus_call_method(bus, bus_mgr, "AddSigningKey", &error, /* reply= */ NULL, "sst", fn, pem, UINT64_C(0));
+        if (r < 0)
+                return log_error_errno(r, "Failed to add signing key '%s': %s", fn, bus_error_message(&error, r));
+
+        return 0;
+}
+
+static int verb_add_signing_key(int argc, char *argv[], void *userdata) {
+        int r;
+
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        int ret = EXIT_SUCCESS;
+        if (argc < 2 || streq(argv[1], "-")) {
+                if (!arg_key_name)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key name must be specified via --key-name= when reading key from standard input, refusing.");
+
+                RET_GATHER(ret, add_signing_key_one(bus, arg_key_name, stdin));
+        } else {
+                /* Refuse if more han one key is specified in combination with --key-name= */
+                if (argc >= 3 && arg_key_name)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--key-name= is not supported if multiple signing keys are specified, refusing.");
+
+                STRV_FOREACH(k, strv_skip(argv, 1)) {
+
+                        if (streq(*k, "-"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified.");
+
+                        _cleanup_free_ char *fn = NULL;
+                        if (!arg_key_name) {
+                                r = path_extract_filename(*k, &fn);
+                                if (r < 0) {
+                                        RET_GATHER(ret, log_error_errno(r, "Failed to extract filename from path '%s': %m", *k));
+                                        continue;
+                                }
+                        }
+
+                        _cleanup_fclose_ FILE *f = fopen(*k, "re");
+                        if (!f) {
+                                RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k));
+                                continue;
+                        }
+
+                        RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f));
+                }
+        }
+
+        return ret;
+}
+
+static int remove_signing_key_one(sd_bus *bus, const char *fn) {
+        int r;
+
+        assert_se(bus);
+        assert_se(fn);
+
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        r = bus_call_method(bus, bus_mgr, "RemoveSigningKey", &error, /* reply= */ NULL, "st", fn, UINT64_C(0));
+        if (r < 0)
+                return log_error_errno(r, "Failed to remove signing key '%s': %s", fn, bus_error_message(&error, r));
+
+        return 0;
+}
+
+static int verb_remove_signing_key(int argc, char *argv[], void *userdata) {
+        int r;
+
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        r = EXIT_SUCCESS;
+        STRV_FOREACH(k, strv_skip(argv, 1))
+                RET_GATHER(r, remove_signing_key_one(bus, *k));
+
+        return r;
+}
+
 static int run(int argc, char *argv[]) {
         static const Verb verbs[] = {
-                { "help",           VERB_ANY, VERB_ANY, 0,            help                 },
-                { "list",           VERB_ANY, 1,        VERB_DEFAULT, list_homes           },
-                { "activate",       2,        VERB_ANY, 0,            activate_home        },
-                { "deactivate",     2,        VERB_ANY, 0,            deactivate_home      },
-                { "inspect",        VERB_ANY, VERB_ANY, 0,            inspect_home         },
-                { "authenticate",   VERB_ANY, VERB_ANY, 0,            authenticate_home    },
-                { "create",         VERB_ANY, 2,        0,            create_home          },
-                { "remove",         2,        VERB_ANY, 0,            remove_home          },
-                { "update",         VERB_ANY, 2,        0,            update_home          },
-                { "passwd",         VERB_ANY, 2,        0,            passwd_home          },
-                { "resize",         2,        3,        0,            resize_home          },
-                { "lock",           2,        VERB_ANY, 0,            lock_home            },
-                { "unlock",         2,        VERB_ANY, 0,            unlock_home          },
-                { "with",           2,        VERB_ANY, 0,            with_home            },
-                { "lock-all",       VERB_ANY, 1,        0,            lock_all_homes       },
-                { "deactivate-all", VERB_ANY, 1,        0,            deactivate_all_homes },
-                { "rebalance",      VERB_ANY, 1,        0,            rebalance            },
-                { "firstboot",      VERB_ANY, 1,        0,            verb_firstboot       },
+                { "help",               VERB_ANY, VERB_ANY, 0,            help                     },
+                { "list",               VERB_ANY, 1,        VERB_DEFAULT, list_homes               },
+                { "activate",           2,        VERB_ANY, 0,            activate_home            },
+                { "deactivate",         2,        VERB_ANY, 0,            deactivate_home          },
+                { "inspect",            VERB_ANY, VERB_ANY, 0,            inspect_home             },
+                { "authenticate",       VERB_ANY, VERB_ANY, 0,            authenticate_home        },
+                { "create",             VERB_ANY, 2,        0,            create_home              },
+                { "remove",             2,        VERB_ANY, 0,            remove_home              },
+                { "update",             VERB_ANY, 2,        0,            update_home              },
+                { "passwd",             VERB_ANY, 2,        0,            passwd_home              },
+                { "resize",             2,        3,        0,            resize_home              },
+                { "lock",               2,        VERB_ANY, 0,            lock_home                },
+                { "unlock",             2,        VERB_ANY, 0,            unlock_home              },
+                { "with",               2,        VERB_ANY, 0,            with_home                },
+                { "lock-all",           VERB_ANY, 1,        0,            lock_all_homes           },
+                { "deactivate-all",     VERB_ANY, 1,        0,            deactivate_all_homes     },
+                { "rebalance",          VERB_ANY, 1,        0,            rebalance                },
+                { "firstboot",          VERB_ANY, 1,        0,            verb_firstboot           },
+                { "list-signing-keys",  VERB_ANY, 1,        0,            verb_list_signing_keys   },
+                { "get-signing-key",    VERB_ANY, VERB_ANY, 0,            verb_get_signing_key     },
+                { "add-signing-key",    VERB_ANY, VERB_ANY, 0,            verb_add_signing_key     },
+                { "remove-signing-key", 2,        VERB_ANY, 0,            verb_remove_signing_key  },
                 {}
         };