From 6a2097dd78a3084d436d88a5ea5f3c9340f4a569 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 00:03:12 +0100 Subject: [PATCH 01/18] openssl-util: add helper for converting EVP_PKEY to PEM string And also give the rverse a bit of love. --- src/home/user-record-sign.c | 11 +---------- src/shared/openssl-util.c | 24 +++++++++++++++++++++--- src/shared/openssl-util.h | 3 ++- src/shared/tpm2-util.c | 2 +- src/test/test-openssl.c | 6 +++--- src/test/test-tpm2.c | 2 +- src/tpm2-setup/tpm2-setup.c | 2 +- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c index 2230b29caf..a50c186d67 100644 --- a/src/home/user-record-sign.c +++ b/src/home/user-record-sign.c @@ -31,13 +31,11 @@ static int user_record_signable_json(UserRecord *ur, char **ret) { } int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) { - _cleanup_(memstream_done) MemStream m = {}; _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL; _cleanup_free_ char *text = NULL, *key = NULL; _cleanup_free_ void *signature = NULL; size_t signature_size = 0; - FILE *f; int r; assert(ur); @@ -52,14 +50,7 @@ int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) { if (r < 0) return r; - f = memstream_init(&m); - if (!f) - return -ENOMEM; - - if (PEM_write_PUBKEY(f, private_key) <= 0) - return -EIO; - - r = memstream_finalize(&m, &key, NULL); + r = openssl_pubkey_to_pem(private_key, &key); if (r < 0) return r; diff --git a/src/shared/openssl-util.c b/src/shared/openssl-util.c index 92d4f4d955..b19c18e56e 100644 --- a/src/shared/openssl-util.c +++ b/src/shared/openssl-util.c @@ -8,6 +8,7 @@ #include "fileio.h" #include "hexdecoct.h" #include "memory-util.h" +#include "memstream-util.h" #include "openssl-util.h" #include "random-util.h" #include "string-util.h" @@ -52,24 +53,41 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(UI_METHOD*, UI_destroy_method, NULL); UNIQ_T(R, u); \ }) -int openssl_pkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret) { +int openssl_pubkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret) { assert(pem); assert(ret); + if (pem_size == SIZE_MAX) + pem_size = strlen(pem); + _cleanup_fclose_ FILE *f = NULL; f = fmemopen((void*) pem, pem_size, "r"); if (!f) return log_oom_debug(); - _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = PEM_read_PUBKEY(f, NULL, NULL, NULL); + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = PEM_read_PUBKEY(f, /* x= */ NULL, /* pam_password_cb= */ NULL, /* userdata= */ NULL); if (!pkey) return log_openssl_errors("Failed to parse PEM"); *ret = TAKE_PTR(pkey); - return 0; } +int openssl_pubkey_to_pem(EVP_PKEY *pkey, char **ret) { + assert(pkey); + assert(ret); + + _cleanup_(memstream_done) MemStream m = {}; + FILE *f = memstream_init(&m); + if (!f) + return -ENOMEM; + + if (PEM_write_PUBKEY(f, pkey) <= 0) + return -EIO; + + return memstream_finalize(&m, ret, /* ret_size= */ NULL); +} + /* Returns the number of bytes generated by the specified digest algorithm. This can be used only for * fixed-size algorithms, e.g. md5, sha1, sha256, etc. Do not use this for variable-sized digest algorithms, * e.g. shake128. Returns 0 on success, -EOPNOTSUPP if the algorithm is not supported, or < 0 for any other diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index 4ed7a4891f..d936ef54f4 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -114,7 +114,8 @@ static inline void sk_X509_free_allp(STACK_OF(X509) **sk) { sk_X509_pop_free(*sk, X509_free); } -int openssl_pkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret); +int openssl_pubkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret); +int openssl_pubkey_to_pem(EVP_PKEY *pkey, char **ret); int openssl_digest_size(const char *digest_alg, size_t *ret_digest_size); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index b4ae74f17a..18194da3e7 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -4572,7 +4572,7 @@ int tpm2_tpm2b_public_from_pem(const void *pem, size_t pem_size, TPM2B_PUBLIC *r assert(ret); _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; - r = openssl_pkey_from_pem(pem, pem_size, &pkey); + r = openssl_pubkey_from_pem(pem, pem_size, &pkey); if (r < 0) return r; diff --git a/src/test/test-openssl.c b/src/test/test-openssl.c index d0ea0f0215..9ad2aa201e 100644 --- a/src/test/test-openssl.c +++ b/src/test/test-openssl.c @@ -7,7 +7,7 @@ TEST(openssl_pkey_from_pem) { DEFINE_HEX_PTR(key_ecc, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a30444151634451674145726a6e4575424c73496c3972687068777976584e50686a346a426e500a44586e794a304b395579724e6764365335413532542b6f5376746b436a365a726c34685847337741515558706f426c532b7448717452714c35513d3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a"); _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_ecc = NULL; - assert_se(openssl_pkey_from_pem(key_ecc, key_ecc_len, &pkey_ecc) >= 0); + assert_se(openssl_pubkey_from_pem(key_ecc, key_ecc_len, &pkey_ecc) >= 0); _cleanup_free_ void *x = NULL, *y = NULL; size_t x_len, y_len; @@ -23,7 +23,7 @@ TEST(openssl_pkey_from_pem) { DEFINE_HEX_PTR(key_rsa, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541795639434950652f505852337a436f63787045300a6a575262546c3568585844436b472f584b79374b6d2f4439584942334b734f5a31436a5937375571372f674359363170697838697552756a73413464503165380a593445336c68556d374a332b6473766b626f4b64553243626d52494c2f6675627771694c4d587a41673342575278747234547545443533527a373634554650640a307a70304b68775231496230444c67772f344e67566f314146763378784b4d6478774d45683567676b73733038326332706c354a504e32587677426f744e6b4d0a5471526c745a4a35355244436170696e7153334577376675646c4e735851357746766c7432377a7637344b585165616d704c59433037584f6761304c676c536b0a79754774586b6a50542f735542544a705374615769674d5a6f714b7479563463515a58436b4a52684459614c47587673504233687a766d5671636e6b47654e540a65774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a"); _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_rsa = NULL; - assert_se(openssl_pkey_from_pem(key_rsa, key_rsa_len, &pkey_rsa) >= 0); + assert_se(openssl_pubkey_from_pem(key_rsa, key_rsa_len, &pkey_rsa) >= 0); _cleanup_free_ void *n = NULL, *e = NULL; size_t n_len, e_len; @@ -94,7 +94,7 @@ TEST(invalid) { _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; DEFINE_HEX_PTR(key, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b7b"); - assert_se(openssl_pkey_from_pem(key, key_len, &pkey) == -EIO); + assert_se(openssl_pubkey_from_pem(key, key_len, &pkey) == -EIO); ASSERT_NULL(pkey); } diff --git a/src/test/test-tpm2.c b/src/test/test-tpm2.c index 31ef6192b2..e42fcfe817 100644 --- a/src/test/test-tpm2.c +++ b/src/test/test-tpm2.c @@ -810,7 +810,7 @@ static void get_tpm2b_public_from_pem(const void *pem, size_t pem_size, TPM2B_PU assert(pem); assert(ret); - assert_se(openssl_pkey_from_pem(pem, pem_size, &pkey) >= 0); + assert_se(openssl_pubkey_from_pem(pem, pem_size, &pkey) >= 0); assert_se(tpm2_tpm2b_public_from_openssl_pkey(pkey, &p1) >= 0); assert_se(tpm2_tpm2b_public_from_pem(pem, pem_size, &p2) >= 0); assert_se(memcmp_nn(&p1, sizeof(p1), &p2, sizeof(p2)) == 0); diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index 6a9a7852e2..622cd887f3 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -186,7 +186,7 @@ static int load_public_key_disk(const char *path, struct public_key_data *ret) { } else { log_debug("Loaded SRK public key from '%s'.", path); - r = openssl_pkey_from_pem(blob, blob_size, &data.pkey); + r = openssl_pubkey_from_pem(blob, blob_size, &data.pkey); if (r < 0) return log_error_errno(r, "Failed to parse SRK public key file '%s': %m", path); From fd0dd2d4bce00b69f8badab1a71b8929e392af5c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 21:56:14 +0100 Subject: [PATCH 02/18] userdbctl: optionally show user/group data from JSON filerather than from system --- man/userdbctl.xml | 19 ++++++ shell-completion/bash/userdbctl | 2 +- src/userdb/userdbctl.c | 76 +++++++++++++++++++++-- test/units/TEST-74-AUX-UTILS.userdbctl.sh | 9 +++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/man/userdbctl.xml b/man/userdbctl.xml index f7b0c1d9eb..2db590fa72 100644 --- a/man/userdbctl.xml +++ b/man/userdbctl.xml @@ -243,6 +243,19 @@ + + + + + When used with the user or group command, read + the user definition in JSON format from the specified file, instead of querying it from the + system. If the path is specified as -, reads the JSON data from standard + input. This is useful to validate and introspect JSON user or group records quickly, and check how + they would be interpreted on the local system. + + + + @@ -263,6 +276,9 @@ List all known users records or show details of one or more specified user records. Use to tweak output mode. + If used in conjuntion with the user record data is read in JSON + format from the specified file instead of querying it from the system. For details see above. + @@ -272,6 +288,9 @@ List all known group records or show details of one or more specified group records. Use to tweak the output mode. + If used in conjuntion with the group record data is read in JSON + format from the specified file instead of querying it from the system. For details see above. + diff --git a/shell-completion/bash/userdbctl b/shell-completion/bash/userdbctl index da1419b175..b068fe81fd 100644 --- a/shell-completion/bash/userdbctl +++ b/shell-completion/bash/userdbctl @@ -44,7 +44,7 @@ _userdbctl () { [STANDALONE]='-h --help --version --no-pager --no-legend -j -N --chain -z --fuzzy -I -S -R -B' [ARG]='--output -s --service --with-nss --synthesize --with-dropin --with-varlink - --multiplexer --json --uid-min --uid-max --disposition --boundaries' + --multiplexer --json --uid-min --uid-max --disposition --boundaries --from-file' ) if __contains_word "$prev" ${OPTS[ARG]}; then diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c index 81007c4e50..8ba12afbb7 100644 --- a/src/userdb/userdbctl.c +++ b/src/userdb/userdbctl.c @@ -45,8 +45,10 @@ static uid_t arg_uid_min = 0; static uid_t arg_uid_max = UID_INVALID-1; static bool arg_fuzzy = false; static bool arg_boundaries = true; +static sd_json_variant *arg_from_file = NULL; STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_from_file, sd_json_variant_unrefp); static const char *user_disposition_to_color(UserDisposition d) { assert(d >= 0); @@ -380,7 +382,7 @@ static int display_user(int argc, char *argv[], void *userdata) { int ret = 0, r; if (arg_output < 0) - arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; if (arg_output == OUTPUT_TABLE) { table = table_new(" ", "name", "disposition", "uid", "gid", "realname", "home", "shell", "order"); @@ -402,7 +404,23 @@ static int display_user(int argc, char *argv[], void *userdata) { .uid_max = arg_uid_max, }; - if (argc > 1 && !arg_fuzzy) + if (arg_from_file) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing."); + + _cleanup_(user_record_unrefp) UserRecord *ur = user_record_new(); + if (!ur) + return log_oom(); + + r = user_record_load(ur, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + r = show_user(ur, table); + if (r < 0) + return r; + + } else if (argc > 1 && !arg_fuzzy) STRV_FOREACH(i, argv + 1) { _cleanup_(user_record_unrefp) UserRecord *ur = NULL; @@ -706,7 +724,7 @@ static int display_group(int argc, char *argv[], void *userdata) { int ret = 0, r; if (arg_output < 0) - arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; if (arg_output == OUTPUT_TABLE) { table = table_new(" ", "name", "disposition", "gid", "description", "order"); @@ -727,7 +745,23 @@ static int display_group(int argc, char *argv[], void *userdata) { .gid_max = arg_uid_max, }; - if (argc > 1 && !arg_fuzzy) + if (arg_from_file) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing."); + + _cleanup_(group_record_unrefp) GroupRecord *gr = group_record_new(); + if (!gr) + return log_oom(); + + r = group_record_load(gr, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + r = show_group(gr, table); + if (r < 0) + return r; + + } else if (argc > 1 && !arg_fuzzy) STRV_FOREACH(i, argv + 1) { _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; @@ -888,6 +922,9 @@ static int display_memberships(int argc, char *argv[], void *userdata) { _cleanup_(table_unrefp) Table *table = NULL; int ret = 0, r; + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing memberships, refusing."); + if (arg_output < 0) arg_output = OUTPUT_TABLE; @@ -982,6 +1019,9 @@ static int display_services(int argc, char *argv[], void *userdata) { _cleanup_closedir_ DIR *d = NULL; int r; + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing services, refusing."); + d = opendir("/run/systemd/userdb/"); if (!d) { if (errno == ENOENT) { @@ -1048,6 +1088,9 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { assert(argc >= 2); + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing SSH authorized keys, refusing."); + if (arg_chain) { /* If --chain is specified, the rest of the command line is the chain command */ @@ -1167,6 +1210,7 @@ static int help(int argc, char *argv[], void *userdata) { " -R Equivalent to --disposition=regular\n" " --boundaries=BOOL Show/hide UID/GID range boundaries in output\n" " -B Equivalent to --boundaries=no\n" + " -F --from-file=PATH Read JSON record from file\n" "\nSee the %s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -1215,6 +1259,7 @@ static int parse_argv(int argc, char *argv[]) { { "fuzzy", no_argument, NULL, 'z' }, { "disposition", required_argument, NULL, ARG_DISPOSITION }, { "boundaries", required_argument, NULL, ARG_BOUNDARIES }, + { "from-file", required_argument, NULL, 'F' }, {} }; @@ -1245,7 +1290,7 @@ static int parse_argv(int argc, char *argv[]) { int c; c = getopt_long(argc, argv, - arg_chain ? "+hjs:NISRzB" : "hjs:NISRzB", /* When --chain was used disable parsing of further switches */ + arg_chain ? "+hjs:NISRzBF:" : "hjs:NISRzBF:", /* When --chain was used disable parsing of further switches */ options, NULL); if (c < 0) break; @@ -1420,6 +1465,24 @@ static int parse_argv(int argc, char *argv[]) { arg_boundaries = false; break; + case 'F': { + if (isempty(optarg)) { + arg_from_file = sd_json_variant_unref(arg_from_file); + break; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + const char *fn = streq(optarg, "-") ? NULL : optarg; + unsigned line = 0; + r = sd_json_parse_file(fn ? NULL : stdin, fn ?: "", SD_JSON_PARSE_SENSITIVE, &v, &line, /* reterr_column= */ NULL); + if (r < 0) + return log_syntax(/* unit= */ NULL, LOG_ERR, fn ?: "", line, r, "JSON parse failure."); + + sd_json_variant_unref(arg_from_file); + arg_from_file = TAKE_PTR(v); + break; + } + case '?': return -EINVAL; @@ -1435,6 +1498,9 @@ static int parse_argv(int argc, char *argv[]) { if (arg_disposition_mask == UINT64_MAX) arg_disposition_mask = USER_DISPOSITION_MASK_ALL; + if (arg_from_file) + arg_boundaries = false; + return 1; } diff --git a/test/units/TEST-74-AUX-UTILS.userdbctl.sh b/test/units/TEST-74-AUX-UTILS.userdbctl.sh index b0b7472d61..5592777849 100755 --- a/test/units/TEST-74-AUX-UTILS.userdbctl.sh +++ b/test/units/TEST-74-AUX-UTILS.userdbctl.sh @@ -37,3 +37,12 @@ assert_eq "$(userdbctl user 0 -j | jq -r .userName)" root assert_eq "$(userdbctl user 2147352576 -j | jq -r .userName)" foreign-0 assert_eq "$(userdbctl user 2147352577 -j | jq -r .userName)" foreign-1 assert_eq "$(userdbctl user 2147418110 -j | jq -r .userName)" foreign-65534 + +# Make sure that -F shows same data as if we'd ask directly +userdbctl user root -j | userdbctl -F- user | cmp - <(userdbctl user root) +userdbctl user systemd-network -j | userdbctl -F- user | cmp - <(userdbctl user systemd-network) +userdbctl user 65534 -j | userdbctl -F- user | cmp - <(userdbctl user 65534) + +userdbctl group root -j | userdbctl -F- group | cmp - <(userdbctl group root) +userdbctl group systemd-network -j | userdbctl -F- group | cmp - <(userdbctl group systemd-network) +userdbctl group 65534 -j | userdbctl -F- group | cmp - <(userdbctl group 65534) From f1b6417fea8ea1fb9a57f45b845ab1db944eca23 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 00:04:03 +0100 Subject: [PATCH 03/18] 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 403778288d..e0e9eef982 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 726a12e54b..5966758a56 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 0a94128230..a4512b8d26 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 9fea621031..8f2c3d2fd7 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 b8085929b0..adf85aa5cf 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 cb5c1b74d5..c0f5aff5ea 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 edc49027b6..6322d68ad9 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" From 88392a1f600b8670e7c2540bf2fdb0c27ce091a5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 09:41:48 +0100 Subject: [PATCH 04/18] homectl: add signing key management verbs --- man/homectl.xml | 61 ++++++++ shell-completion/bash/homectl | 3 +- src/home/homectl.c | 284 +++++++++++++++++++++++++++++++--- 3 files changed, 329 insertions(+), 19 deletions(-) diff --git a/man/homectl.xml b/man/homectl.xml index abcdd88529..568f077c05 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -179,6 +179,18 @@ + + + + When used with the add-signing-key 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 .public. 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. + + + + @@ -1311,6 +1323,55 @@ + + + list-signing-keys + + Show a list of public keys that home directories can be signed with to be allowed for + local login. One such key (local.public) 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 add-signing-key below). + + + + + + get-signing-key [NAME…] + + Write the public key identified by the specified name to standard output (in PEM + format). If no name is specified defaults to local.public, i.e. the + automatically generated key for locally created home directories. + + + + + + add-signing-key [FILE…] + + 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 - 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 .public 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 , see above. + + This command is useful for permitting local home directories to be used on a remote + system. Example: + + homectl get-signing-key | ssh myotherhost homectl add-signing-key --key-name="$HOSTNAME".public + + + + + + remove-signing-key NAME… + + 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. + + + + diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl index 5e2235bc3b..6219f25594 100644 --- a/shell-completion/bash/homectl +++ b/shell-completion/bash/homectl @@ -112,7 +112,8 @@ _homectl() { --avatar --login-background --session-launcher - --session-type' + --session-type + --key-name' ) if __contains_word "$prev" ${OPTS[ARG]}; then diff --git a/src/home/homectl.c b/src/home/homectl.c index a7754c2299..6857acbcc6 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -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 }, {} }; From 87c81a34eba625c58e25faa5e7ec87217d765954 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 22:53:21 +0100 Subject: [PATCH 05/18] homectl: also import signing keys at firstboot time --- man/systemd.system-credentials.xml | 13 +++++ src/home/homectl.c | 76 ++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index aa974837cd..1dd7f8aee6 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -378,6 +378,19 @@ + + home.add-signing-key.* + + Adds a new signing key for user records to the system. The credential contents should contain + a user signing key, for example as reported by homectl get-signing-key. Multiple + keys may be specified, and they will be put in place under the name of the credential name suffix + (which must itself carry the .public suffix). For details see + homectl1. + + + + + home.create.* diff --git a/src/home/homectl.c b/src/home/homectl.c index 6857acbcc6..66f3f773f6 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2702,6 +2702,8 @@ static int create_interactively(void) { return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false); } +static int add_signing_keys_from_credentials(void); + static int verb_firstboot(int argc, char *argv[], void *userdata) { int r; @@ -2717,11 +2719,13 @@ static int verb_firstboot(int argc, char *argv[], void *userdata) { arg_prompt_new_user = false; } + int ret = 0; + + RET_GATHER(ret, add_signing_keys_from_credentials()); + r = create_from_credentials(); - if (r < 0) - return r; - if (r > 0) /* Already created users from credentials */ - return 0; + RET_GATHER(ret, r); + bool existing_users = r > 0; r = getenv_bool("SYSTEMD_HOME_FIRSTBOOT_OVERRIDE"); if (r == 0) @@ -2730,16 +2734,21 @@ static int verb_firstboot(int argc, char *argv[], void *userdata) { if (r != -ENXIO) log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m"); - r = has_regular_user(); - if (r < 0) - return r; - if (r > 0) { - log_info("Regular user already present in user database, skipping user creation."); + if (!existing_users) { + r = has_regular_user(); + if (r < 0) + return r; + + existing_users = r > 0; + } + if (existing_users) { + log_info("Regular user already present in user database, skipping interactive user creation."); return 0; } } - return create_interactively(); + RET_GATHER(ret, create_interactively()); + return ret; } static int drop_from_identity(const char *field) { @@ -5130,6 +5139,53 @@ static int verb_add_signing_key(int argc, char *argv[], void *userdata) { return ret; } +static int add_signing_keys_from_credentials(void) { + int r; + + _cleanup_close_ int fd = open_credentials_dir(); + if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */ + return 0; + if (fd < 0) + return log_error_errno(fd, "Failed to open credentials directory: %m"); + + _cleanup_free_ DirectoryEntries *des = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + if (r < 0) + return log_error_errno(r, "Failed to enumerate credentials: %m"); + + int ret = 0; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + FOREACH_ARRAY(i, des->entries, des->n_entries) { + struct dirent *de = *i; + if (de->d_type != DT_REG) + continue; + + const char *e = startswith(de->d_name, "home.add-signing-key."); + if (!e) + continue; + + if (!filename_is_valid(e)) + continue; + + if (!bus) { + r = acquire_bus(&bus); + if (r < 0) + return r; + } + + _cleanup_fclose_ FILE *f = NULL; + r = xfopenat(fd, de->d_name, "re", O_NOFOLLOW, &f); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to open credential '%s': %m", de->d_name)); + continue; + } + + RET_GATHER(ret, add_signing_key_one(bus, e, f)); + } + + return ret; +} + static int remove_signing_key_one(sd_bus *bus, const char *fn) { int r; From 4f00011b6820196b7fa09ee0c3ae2cc1236524f3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 21:25:34 +0100 Subject: [PATCH 06/18] homectl: add a 'dry-run' mode for registering/creating users Since this only covers user creation/registration for now, let's hide it behind an env var. We might reconsider this eventually and make it a proper switch one day, but who knows, it after all has this "debug tool" wiff. --- docs/ENVIRONMENT.md | 5 +++++ src/home/homectl.c | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 4b2289e646..c340e01d01 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -615,6 +615,11 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \ there already exists at least one regular user on the system. If set to "0" will make the tool skip any such query. +* `$SYSTEMD_HOME_DRY_RUN` – if set to "1" will make `homectl create` and + `homectl update` operate in a "dry-run" mode: the new user record is + assembled, and displayed in JSON format, but not actually passed to + `systemd-homed` for execution of the operation. + `kernel-install`: * `$KERNEL_INSTALL_BYPASS` – If set to "1", execution of kernel-install is skipped diff --git a/src/home/homectl.c b/src/home/homectl.c index 66f3f773f6..22570819a4 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -99,6 +99,7 @@ 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 bool arg_dry_run = false; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); @@ -1462,6 +1463,11 @@ static int create_home_common(sd_json_variant *input, bool show_enforce_password log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r)); } + if (arg_dry_run) { + sd_json_variant_dump(hr->json, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_NEWLINE, stderr, /* prefix= */ NULL); + return 0; + } + r = acquire_bus(&bus); if (r < 0) return r; @@ -1760,6 +1766,11 @@ static int update_home(int argc, char *argv[], void *userdata) { if (r < 0) return r; + if (arg_dry_run) { + sd_json_variant_dump(hr->json, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_NEWLINE, stderr, /* prefix= */ NULL); + return 0; + } + /* If we do multiple operations, let's output things more verbosely, since otherwise the repeated * authentication might be confusing. */ @@ -3181,6 +3192,13 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); + /* Eventually we should probably turn this into a proper --dry-run option, but as long as it is not hooked up everywhere let's make it an environment variable only. */ + r = getenv_bool("SYSTEMD_HOME_DRY_RUN"); + if (r >= 0) + arg_dry_run = r; + else if (r != -ENXIO) + log_debug_errno(r, "Unable to parse $SYSTEMD_HOME_DRY_RUN, ignoring: %m"); + for (;;) { int c; From ce94761debfab321d608e5c4ea876d7bc7f65097 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 17:15:56 +0100 Subject: [PATCH 07/18] user-record: add a concept of inverting per-host matching sections in user record Sometimes it is useful to apply options on all hosts except some. Add a simple concept for that. --- src/shared/user-record.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 89ba2fbef4..0d2c261a09 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -1126,6 +1126,8 @@ int per_machine_id_match(sd_json_variant *ids, sd_json_dispatch_flags_t flags) { sd_id128_t mid; int r; + assert(ids); + r = sd_id128_get_machine(&mid); if (r < 0) return json_log(ids, flags, r, "Failed to acquire machine ID: %m"); @@ -1174,6 +1176,8 @@ int per_machine_hostname_match(sd_json_variant *hns, sd_json_dispatch_flags_t fl _cleanup_free_ char *hn = NULL; int r; + assert(hns); + r = gethostname_strict(&hn); if (r == -ENXIO) { json_log(hns, flags, r, "No hostname set, not matching perMachine hostname record: %m"); @@ -1221,6 +1225,15 @@ int per_machine_match(sd_json_variant *entry, sd_json_dispatch_flags_t flags) { return true; } + m = sd_json_variant_by_key(entry, "matchNotMachineId"); + if (m) { + r = per_machine_id_match(m, flags); + if (r < 0) + return r; + if (r == 0) + return true; + } + m = sd_json_variant_by_key(entry, "matchHostname"); if (m) { r = per_machine_hostname_match(m, flags); @@ -1230,6 +1243,15 @@ int per_machine_match(sd_json_variant *entry, sd_json_dispatch_flags_t flags) { return true; } + m = sd_json_variant_by_key(entry, "matchNotHostname"); + if (m) { + r = per_machine_hostname_match(m, flags); + if (r < 0) + return r; + if (r == 0) + return true; + } + return false; } @@ -1237,7 +1259,9 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j static const sd_json_dispatch_field per_machine_dispatch_table[] = { { "matchMachineId", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchNotMachineId", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, { "matchHostname", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchNotHostname", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, { "blobDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), SD_JSON_STRICT }, { "blobManifest", SD_JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 }, { "iconName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, icon_name), SD_JSON_STRICT }, From cbf9a1c8883aaf687210abf84f6abf1438a3b594 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Feb 2025 09:52:18 +0100 Subject: [PATCH 08/18] homed: add concept for "adopting" an existing homedir locally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently homed scans /home/ via inotify for new .home + .homedir/ popping up to register as local users. Let's also add an explicit way to request this form of "adoption": a bus call that takes a path and that makes a home dir activatable locally. (Usecase: you cross boot between two systems – let's say your traditional fedora and your ParticleOS – and want to use the same homedir from both: simply mount the /home dir from the other somewhere, and then hit "homectl adopt /somewhere/lennart.home" and you have the user locally too). --- man/homectl.xml | 13 +++++++ man/org.freedesktop.home1.xml | 16 ++++++-- shell-completion/bash/homectl | 2 +- src/home/homectl.c | 34 +++++++++++++++++ src/home/homed-manager-bus.c | 46 +++++++++++++++++++++++ src/home/homed-manager.c | 37 ++++++++++++++---- src/home/homed-manager.h | 2 + src/libsystemd/sd-bus/bus-common-errors.c | 1 + src/libsystemd/sd-bus/bus-common-errors.h | 1 + 9 files changed, 141 insertions(+), 11 deletions(-) diff --git a/man/homectl.xml b/man/homectl.xml index 568f077c05..610c69ccc1 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -1184,6 +1184,19 @@ + + adopt PATH [PATH…] + + Adopts one or more existing home directories on the local system. Takes one or more paths to + *.home LUKS home directories or *.homedir/ standalone home + directories or subvolumes previously created by systemd-homed and makes them + available locally for login. The referenced files are not moved. This is an alternative for moving + such home directories into /home/ (where they would be picked up + automatically). + + + + remove USER diff --git a/man/org.freedesktop.home1.xml b/man/org.freedesktop.home1.xml index e0e9eef982..1d73e78d39 100644 --- a/man/org.freedesktop.home1.xml +++ b/man/org.freedesktop.home1.xml @@ -70,6 +70,8 @@ node /org/freedesktop/home1 { @org.freedesktop.systemd1.Privileged("true") DeactivateHome(in s user_name); RegisterHome(in s user_record); + AdoptHome(in s image_path, + in t flags); UnregisterHome(in s user_name); CreateHome(in s user_record); CreateHomeEx(in s user_record, @@ -160,6 +162,8 @@ node /org/freedesktop/home1 { + + @@ -274,6 +278,12 @@ node /org/freedesktop/home1 { is useful to register home directories locally that are not located where systemd-homed.service would find them automatically. + AdoptHome() also registers a new home directory locally. It takes a path to + a home directory itself, and will register it locally. This only works for *.home + and *.homedir/ home directories. This operation is done automatically for all such + home areas showing up in /home/, but may be requested explicitly with this call for + directories elsewhere. The flags must be set to zero, currently. + UnregisterHome() unregisters an existing home directory. It takes a user name as argument and undoes what RegisterHome() does. It does not attempt to remove the home directory itself, it just unregisters it with the local system. Note that if the home @@ -633,9 +643,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. + AdoptHome(), ListSigningKeys(), + GetSigningKey(), AddSigningKey(), and + RemoveSigningKey() were added in version 258. Home Objects diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl index 6219f25594..3bd29fc808 100644 --- a/shell-completion/bash/homectl +++ b/shell-completion/bash/homectl @@ -173,7 +173,7 @@ _homectl() { fi local -A VERBS=( - [STANDALONE]='list lock-all' + [STANDALONE]='list lock-all adopt' [CREATE]='create' [NAMES]='activate deactivate inspect authenticate remove lock unlock' [NAME]='update passwd' diff --git a/src/home/homectl.c b/src/home/homectl.c index 22570819a4..123fcd9998 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -1564,6 +1564,38 @@ static int create_home(int argc, char *argv[], void *userdata) { return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ true); } +static int verb_adopt_home(int argc, char *argv[], void *userdata) { + int r, ret = 0; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "AdoptHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "st", *i, UINT64_C(0)); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to adopt home: %s", bus_error_message(&error, r)); + if (ret == 0) + ret = r; + } + } + + return ret; +} + static int remove_home(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r, ret = 0; @@ -2808,6 +2840,7 @@ static int help(int argc, char *argv[], void *userdata) { " inspect USER… Inspect a home area\n" " authenticate USER… Authenticate a home area\n" " create USER Create a home area\n" + " adopt PATH… Add an existing home area on this system\n" " remove USER… Remove a home area\n" " update USER Update a home area\n" " passwd USER Change password of a home area\n" @@ -5242,6 +5275,7 @@ static int run(int argc, char *argv[]) { { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home }, { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, { "create", VERB_ANY, 2, 0, create_home }, + { "adopt", VERB_ANY, VERB_ANY, 0, verb_adopt_home }, { "remove", 2, VERB_ANY, 0, remove_home }, { "update", VERB_ANY, 2, 0, update_home }, { "passwd", VERB_ANY, 2, 0, passwd_home }, diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c index 5966758a56..a3fe2b6a69 100644 --- a/src/home/homed-manager-bus.c +++ b/src/home/homed-manager-bus.c @@ -519,6 +519,47 @@ static int method_register_home( return sd_bus_reply_method_return(message, NULL); } +static int method_adopt_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *image_path = NULL; + uint64_t flags = 0; + r = sd_bus_message_read(message, "st", &image_path, &flags); + if (r < 0) + return r; + + if (!path_is_absolute(image_path) || !path_is_safe(image_path)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Specified path is not absolute or not valid: %s", image_path); + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags field must be zero."); + + r = bus_verify_polkit_async( + message, + "org.freedesktop.home1.create-home", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = manager_adopt_home(m, image_path); + if (r == -EMEDIUMTYPE) + return sd_bus_error_setf(error, BUS_ERROR_UNRECOGNIZED_HOME_FORMAT, "Unrecognized format of home directory: %s", image_path); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { return generic_home_method(userdata, message, bus_home_method_unregister, error); } @@ -1091,6 +1132,11 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_NO_RESULT, method_register_home, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("AdoptHome", + SD_BUS_ARGS("s", image_path, "t", flags), + SD_BUS_NO_RESULT, + method_adopt_home, + SD_BUS_VTABLE_UNPRIVILEGED), /* Remove the JSON record from homed, but don't remove actual $HOME */ SD_BUS_METHOD_WITH_ARGS("UnregisterHome", diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index a4512b8d26..9198368cfd 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -177,7 +177,7 @@ static int on_home_inotify(sd_event_source *s, const struct inotify_event *event else if (FLAGS_SET(event->mask, IN_MOVED_TO)) log_debug("%s has been moved in, having a look.", j); - (void) manager_assess_image(m, -1, get_home_root(), event->name); + (void) manager_assess_image(m, /* dir_fd= */ -EBADF, get_home_root(), event->name); (void) bus_manager_emit_auto_login_changed(m); } @@ -841,6 +841,10 @@ static int manager_assess_image( assert(dir_path); assert(dentry_name); + /* Maybe registers the specified .home or .homedir as a home we manage. Returns: + * + * -EMEDIUMTYPE: Not a dir with .homedir suffix or a file with .home suffix */ + luks_suffix = endswith(dentry_name, ".home"); if (luks_suffix) directory_suffix = NULL; @@ -849,7 +853,7 @@ static int manager_assess_image( /* Early filter out: by name */ if (!luks_suffix && !directory_suffix) - return 0; + return -EMEDIUMTYPE; path = path_join(dir_path, dentry_name); if (!path) @@ -868,7 +872,7 @@ static int manager_assess_image( _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; if (!luks_suffix) - return 0; + return -EMEDIUMTYPE; n = strndup(dentry_name, luks_suffix - dentry_name); if (!n) @@ -876,7 +880,7 @@ static int manager_assess_image( r = split_user_name_realm(n, &user_name, &realm); if (r == -EINVAL) /* Not the right format: ignore */ - return 0; + return -EMEDIUMTYPE; if (r < 0) return log_error_errno(r, "Failed to split image name into user name/realm: %m"); @@ -889,7 +893,7 @@ static int manager_assess_image( UserStorage storage; if (!directory_suffix) - return 0; + return -EMEDIUMTYPE; n = strndup(dentry_name, directory_suffix - dentry_name); if (!n) @@ -897,7 +901,7 @@ static int manager_assess_image( r = split_user_name_realm(n, &user_name, &realm); if (r == -EINVAL) /* Not the right format: ignore */ - return 0; + return -EMEDIUMTYPE; if (r < 0) return log_error_errno(r, "Failed to split image name into user name/realm: %m"); @@ -939,7 +943,26 @@ static int manager_assess_image( return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid); } - return 0; + return -EMEDIUMTYPE; +} + +int manager_adopt_home(Manager *m, const char *path) { + int r; + + assert(m); + assert(path); + + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(path, &fn); + if (r < 0) + return r; + + _cleanup_free_ char *dir = NULL; + r = path_extract_directory(path, &dir); + if (r < 0) + return r; + + return manager_assess_image(m, /* dir_fd= */ -EBADF, dir, fn); } int manager_enumerate_images(Manager *m) { diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h index 8f2c3d2fd7..b780094d0d 100644 --- a/src/home/homed-manager.h +++ b/src/home/homed-manager.h @@ -87,6 +87,8 @@ int manager_reschedule_rebalance(Manager *m); int manager_verify_user_record(Manager *m, UserRecord *hr); +int manager_adopt_home(Manager *m, const char *path); + int manager_acquire_key_pair(Manager *m); int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error); diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index c0f5aff5ea..c66fded7f1 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -151,6 +151,7 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { 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_UNRECOGNIZED_HOME_FORMAT, EMEDIUMTYPE), 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 6322d68ad9..9148f0aba9 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -157,6 +157,7 @@ #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_UNRECOGNIZED_HOME_FORMAT "org.freedesktop.home1.UnrecognizedHomeFormat" #define BUS_ERROR_NO_UPDATE_CANDIDATE "org.freedesktop.sysupdate1.NoCandidate" From e8801cc5b3e1661965a7c533d10c88856c9b417c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Feb 2025 10:21:57 +0100 Subject: [PATCH 09/18] homectl: expose "register" verb to register a user record locally --- man/homectl.xml | 49 +++++++++++++++++ shell-completion/bash/homectl | 2 +- src/home/homectl.c | 99 +++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/man/homectl.xml b/man/homectl.xml index 610c69ccc1..1acb338bbb 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -1197,6 +1197,55 @@ + + register FILE [FILE…] + + Registers one or more users, without creating their home directories. Takes one or + more paths to JSON user record files. If the path is specified as - reads the + JSON user record from standard input. + + Registering a user makes it accessible on the local system without creating a new home + directory. This is particularly useful for making a user accessible on a system it was not originally + created on. + + Here's an example how to make a local user account with its home directory accessible on a + remote system, using SMB/CIFS file sharing. With Samba installed in its default configuration invoke + as root: + + # smbpasswd -a lennart + + Continue as regular user lennart: + +$ homectl update lennart --ssh-authorized-keys=… -N --storage=cifs --cifs-service="//$HOSTNAME/lennart" +$ homectl get-signing-key | ssh targetsystem homectl add-signing-key --key-name="$HOSTNAME".public +$ homectl inspect -E lennart | ssh targetsystem homectl register - +$ ssh lennart@targetsystem + + This first ensures the user account lennart is known to and accessible by + Samba. It then registers a local SSH access that shall be used for accessing this user, and + configures CIFS as default storage for non-local systems on the account. It then adds the local + system's account signing key to the target system. Then it registers the local user account with the + target system. Finally it logs into the account on the target system. The target system will then + connect back via SMB/CIFS to access the home directory. + + + + + + unregister USER + + Unregisters one or more user accounts. This only removes the user record from the + local system, it does not delete the home directory. The home directory can be readded via the + register or adopt command later, on this or another + system. Note that unregistering a user whose home directory is placed in /home/ + will not make the user disappear from the local user database, as all supported home directories + placed there will show up in the user database. However, the user record will become "unfixated", + i.e. lose its binding to the local system. When logged into it will automatically regain the binding, + and acquire a local UID/GID pair. + + + + remove USER diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl index 3bd29fc808..1b365fbb71 100644 --- a/shell-completion/bash/homectl +++ b/shell-completion/bash/homectl @@ -173,7 +173,7 @@ _homectl() { fi local -A VERBS=( - [STANDALONE]='list lock-all adopt' + [STANDALONE]='list lock-all register unregister adopt' [CREATE]='create' [NAMES]='activate deactivate inspect authenticate remove lock unlock' [NAME]='update passwd' diff --git a/src/home/homectl.c b/src/home/homectl.c index 123fcd9998..12d3c403f9 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -1596,6 +1596,101 @@ static int verb_adopt_home(int argc, char *argv[], void *userdata) { return ret; } +static int register_home_one(sd_bus *bus, FILE *f, const char *path) { + int r; + + assert(bus); + assert(path); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse_file(f, path, SD_JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column); + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "RegisterHome"); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_free_ char *formatted = NULL; + r = sd_json_variant_format(v, /* flags= */ 0, &formatted); + if (r < 0) + return log_error_errno(r, "Failed to format JSON record: %m"); + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to register home: %s", bus_error_message(&error, r)); + + return 0; +} + +static int verb_register_home(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; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_identity) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not accepting an arguments if --identity= is specified, refusing."); + + return register_home_one(bus, /* f= */ NULL, arg_identity); + } + + if (argc == 1 || (argc == 2 && streq(argv[1], "-"))) + return register_home_one(bus, /* f= */ stdin, ""); + + r = 0; + STRV_FOREACH(i, strv_skip(argv, 1)) { + if (streq(*i, "-")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing reading from standard input if multiple user records are specified."); + + RET_GATHER(r, register_home_one(bus, /* f= */ NULL, *i)); + } + + return r; +} + +static int verb_unregister_home(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; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + int ret = 0; + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "UnregisterHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, /* ret_reply= */ NULL); + if (r < 0) + RET_GATHER(ret, log_error_errno(r, "Failed to unregister home: %s", bus_error_message(&error, r))); + } + + return ret; +} + static int remove_home(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r, ret = 0; @@ -2841,6 +2936,8 @@ static int help(int argc, char *argv[], void *userdata) { " authenticate USER… Authenticate a home area\n" " create USER Create a home area\n" " adopt PATH… Add an existing home area on this system\n" + " register PATH… Register a user record locally\n" + " unregister USER… Unregister a user record locally\n" " remove USER… Remove a home area\n" " update USER Update a home area\n" " passwd USER Change password of a home area\n" @@ -5276,6 +5373,8 @@ static int run(int argc, char *argv[]) { { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, { "create", VERB_ANY, 2, 0, create_home }, { "adopt", VERB_ANY, VERB_ANY, 0, verb_adopt_home }, + { "register", VERB_ANY, VERB_ANY, 0, verb_register_home }, + { "unregister", 2, VERB_ANY, 0, verb_unregister_home }, { "remove", 2, VERB_ANY, 0, remove_home }, { "update", VERB_ANY, 2, 0, update_home }, { "passwd", VERB_ANY, 2, 0, passwd_home }, From cc14c14782d542b35fa12bfdadfd64ffa700cedf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Feb 2025 11:05:28 +0100 Subject: [PATCH 10/18] homectl: also support registering (rather than creating) home directories via system credentials --- man/systemd.system-credentials.xml | 14 +++++- src/home/homectl.c | 77 ++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 27 deletions(-) diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index 1dd7f8aee6..5981986548 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -394,13 +394,25 @@ home.create.* - Creates a home area for the specified user with the user record data passed in. For details see + Creates a new home area for the specified user with the user record data passed in. For + details see homectl1. + + home.register.* + + Registers an existing home area for the specified user with the user record data passed in. For details + see + homectl1. + + + + + cryptsetup.passphrase cryptsetup.tpm2-pin diff --git a/src/home/homectl.c b/src/home/homectl.c index 12d3c403f9..37b6f416f5 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -1596,17 +1596,18 @@ static int verb_adopt_home(int argc, char *argv[], void *userdata) { return ret; } -static int register_home_one(sd_bus *bus, FILE *f, const char *path) { +static int register_home_common(sd_bus *bus, sd_json_variant *v) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL; int r; - assert(bus); - assert(path); + assert(v); - _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - unsigned line = 0, column = 0; - r = sd_json_parse_file(f, path, SD_JSON_PARSE_SENSITIVE, &v, &line, &column); - if (r < 0) - return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column); + if (!bus) { + r = acquire_bus(&_bus); + if (r < 0) + return r; + bus = _bus; + } _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; r = bus_message_new_method_call(bus, &m, bus_mgr, "RegisterHome"); @@ -1630,6 +1631,21 @@ static int register_home_one(sd_bus *bus, FILE *f, const char *path) { return 0; } +static int register_home_one(sd_bus *bus, FILE *f, const char *path) { + int r; + + assert(bus); + assert(path); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse_file(f, path, SD_JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column); + + return register_home_common(bus, v); +} + static int verb_register_home(int argc, char *argv[], void *userdata) { int r; @@ -2470,11 +2486,10 @@ static int rebalance(int argc, char *argv[], void *userdata) { return 0; } -static int create_from_credentials(void) { - _cleanup_close_ int fd = -EBADF; - int ret = 0, n_created = 0, r; +static int create_or_register_from_credentials(void) { + int r; - fd = open_credentials_dir(); + _cleanup_close_ int fd = open_credentials_dir(); if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */ return 0; if (fd < 0) @@ -2485,16 +2500,22 @@ static int create_from_credentials(void) { if (r < 0) return log_error_errno(r, "Failed to enumerate credentials: %m"); + int ret = 0, n_processed = 0; FOREACH_ARRAY(i, des->entries, des->n_entries) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *identity = NULL; struct dirent *de = *i; - const char *e; - if (de->d_type != DT_REG) continue; - e = startswith(de->d_name, "home.create."); - if (!e) + enum { + OPERATION_CREATE, + OPERATION_REGISTER, + } op; + const char *e; + if ((e = startswith(de->d_name, "home.create."))) + op = OPERATION_CREATE; + if ((e = startswith(de->d_name, "home.register."))) + op = OPERATION_REGISTER; + else continue; if (!valid_user_group_name(e, 0)) { @@ -2502,21 +2523,22 @@ static int create_from_credentials(void) { continue; } + _cleanup_(sd_json_variant_unrefp) sd_json_variant *identity = NULL; + unsigned line = 0, column = 0; r = sd_json_parse_file_at( /* f= */ NULL, fd, de->d_name, /* flags= */ 0, &identity, - /* ret_line= */ NULL, - /* ret_column= */ NULL); + &line, + &column); if (r < 0) { - log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name); + log_warning_errno(r, "[%s:%u:%u] Failed to parse user record in credential, ignoring: %m", de->d_name, line, column); continue; } - sd_json_variant *un; - un = sd_json_variant_by_key(identity, "userName"); + sd_json_variant *un = sd_json_variant_by_key(identity, "userName"); if (un) { if (!sd_json_variant_is_string(un)) { log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name); @@ -2535,14 +2557,17 @@ static int create_from_credentials(void) { log_notice("Processing user '%s' from credentials.", e); - r = create_home_common(identity, /* show_enforce_password_policy_hint= */ false); + if (op == OPERATION_CREATE) + r = create_home_common(identity, /* show_enforce_password_policy_hint= */ false); + else + r = register_home_common(/* bus= */ NULL, identity); if (r >= 0) - n_created++; + n_processed++; RET_GATHER(ret, r); } - return ret < 0 ? ret : n_created; + return ret < 0 ? ret : n_processed; } static int has_regular_user(void) { @@ -2861,7 +2886,7 @@ static int verb_firstboot(int argc, char *argv[], void *userdata) { RET_GATHER(ret, add_signing_keys_from_credentials()); - r = create_from_credentials(); + r = create_or_register_from_credentials(); RET_GATHER(ret, r); bool existing_users = r > 0; From 17f48a8cc75215d2ff18a2b8242a341dd583f2c1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 27 Feb 2025 18:26:18 +0100 Subject: [PATCH 11/18] homectl: making stripping of signatures from user records optional --- man/homectl.xml | 14 ++++++++++++++ shell-completion/bash/homectl | 3 ++- src/home/homectl.c | 13 ++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/man/homectl.xml b/man/homectl.xml index 1acb338bbb..5d97c676a9 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -191,6 +191,20 @@ + + + + Takes a boolean argument. When used with create or + register, controls whether to strip cryptographic signatures from the provided + JSON user records, which has the effect of signing them with the local signing key + (local.public) instead. If this switch is set to true, added user records + hence become locally managed (and thus can be modified locally), while if it is set to false the user + records remain managed and owned by its origin (and thus cannot be modified locally). This switch + defaults to true for create and false for register. + + + + diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl index 1b365fbb71..1010094782 100644 --- a/shell-completion/bash/homectl +++ b/shell-completion/bash/homectl @@ -113,7 +113,8 @@ _homectl() { --login-background --session-launcher --session-type - --key-name' + --key-name + --seize' ) if __contains_word "$prev" ${OPTS[ARG]}; then diff --git a/src/home/homectl.c b/src/home/homectl.c index 37b6f416f5..969aabd798 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -100,6 +100,7 @@ static bool arg_blob_clear = false; static Hashmap *arg_blob_files = NULL; static char *arg_key_name = NULL; static bool arg_dry_run = false; +static bool arg_seize = true; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); @@ -1183,7 +1184,7 @@ static int acquire_new_home_record(sd_json_variant *input, UserRecord **ret) { USER_RECORD_ALLOW_PER_MACHINE| USER_RECORD_STRIP_BINDING| USER_RECORD_STRIP_STATUS| - USER_RECORD_STRIP_SIGNATURE| + (arg_seize ? USER_RECORD_STRIP_SIGNATURE : USER_RECORD_ALLOW_SIGNATURE) | USER_RECORD_LOG| USER_RECORD_PERMISSIVE); if (r < 0) @@ -3001,6 +3002,8 @@ static int help(int argc, char *argv[], void *userdata) { " --prompt-new-user firstboot: Query user interactively for user\n" " to create\n" " --key-name=NAME Key name when adding a signing key\n" + " --seize=no Do not strip existing signatures of user record\n" + " when creating\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" @@ -3235,6 +3238,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_DEV_SHM_LIMIT, ARG_DEFAULT_AREA, ARG_KEY_NAME, + ARG_SEIZE, }; static const struct option options[] = { @@ -3339,6 +3343,7 @@ static int parse_argv(int argc, char *argv[]) { { "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 }, + { "seize", required_argument, NULL, ARG_SEIZE }, {} }; @@ -4862,6 +4867,12 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_SEIZE: + r = parse_boolean_argument("--seize=", optarg, &arg_seize); + if (r < 0) + return r; + break; + case '?': return -EINVAL; From efe2ce7277c6da2a9d25906cf53f59278b2f35f6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Feb 2025 08:47:46 +0100 Subject: [PATCH 12/18] homed: rescan /home/ in more occasions automatically When unregistering a home there's a chance this "reveals" another home by the same name in /home/, hence immediately schedule a rescan, the same way we already schedule it in on remove. Also, drop the conditionalization when scheduling a rescan during remove, for the same reasons: a remove might reveal another home, and we cannot check for that ahead of time. Trying to check is kinda a pointless optimization anyway, since this is not a frequent operation and rescanning is not the end of the world. --- src/home/homed-home.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 871ccdffe9..ee9f54b322 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -915,8 +915,7 @@ static void home_remove_finish(Home *h, int ret, UserRecord *hr) { * partitions like USB sticks, or so). Sometimes these storage locations are among those we normally * automatically discover in /home or in udev. When such a home is deleted let's hence issue a rescan * after completion, so that "unfixated" entries are rediscovered. */ - if (!IN_SET(user_record_test_image_path(h->record), USER_TEST_UNDEFINED, USER_TEST_ABSENT)) - manager_enqueue_rescan(m); + (void) manager_enqueue_rescan(m); /* The image is now removed from disk. Now also remove our stored record */ r = home_unlink_record(h); @@ -2063,12 +2062,17 @@ int home_unregister(Home *h, sd_bus_error *error) { return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); } + Manager *m = ASSERT_PTR(h->manager); + r = home_unlink_record(h); if (r < 0) return r; /* And destroy the whole entry. The caller needs to be prepared for that. */ h = home_free(h); + + /* Let's rescan, who knows, maybe this revealed a directory in /home/ that we should pick up now */ + manager_enqueue_rescan(m); return 1; } From c3f54fcd3b48dda5b966b4cc0c761f422d1cb674 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Feb 2025 15:19:36 +0100 Subject: [PATCH 13/18] homed: suppress warning if quota is not on on /var/ and elsewhere --- src/home/homed-home.c | 1 - src/home/homed-manager.c | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/home/homed-home.c b/src/home/homed-home.c index ee9f54b322..9b6d2ee404 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -2499,7 +2499,6 @@ static int home_get_disk_status_directory( log_debug_errno(r, "No UID quota support on %s.", path); goto finish; } - if (r != -ESRCH) { log_debug_errno(r, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); goto finish; diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index 9198368cfd..f5868a2d6f 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -545,6 +545,8 @@ static int search_quota(uid_t uid, const char *exclude_quota_path) { if (r < 0) { if (ERRNO_IS_NOT_SUPPORTED(r)) log_debug_errno(r, "No UID quota support on %s, ignoring.", where); + else if (r == -ESRCH) + log_debug_errno(r, "UID quota not enabled on %s (for user " UID_FMT "), ignoring.", where, uid); else if (ERRNO_IS_PRIVILEGE(r)) log_debug_errno(r, "UID quota support for %s prohibited, ignoring.", where); else From c7a4216509e34be5169d45e6c047ee1c42fc8758 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Feb 2025 16:53:58 +0100 Subject: [PATCH 14/18] homed: re-establish inotify watches on SIGUSR1 Let's define a clean way how we can reestablish file watches in homed. This is a relevant in case we overmount /home/ as a whole. It's very useful for our testcase in particular. --- man/systemd-homed.service.xml | 16 ++++++++++++++++ src/home/homed-manager.c | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/man/systemd-homed.service.xml b/man/systemd-homed.service.xml index 42e5780ccc..ca96e5d196 100644 --- a/man/systemd-homed.service.xml +++ b/man/systemd-homed.service.xml @@ -107,6 +107,22 @@ generated/signed before the key pair is copied in, lose their validity. + + Signals + + + + SIGUSR1 + + Upon reception of the SIGUSR1 process signal + systemd-homed will reestablish its file watches on /home/ and + rescan the directory for home directories. + + + + + + See Also diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index f5868a2d6f..5ce0fc3300 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -201,6 +201,20 @@ static int on_home_inotify(sd_event_source *s, const struct inotify_event *event return 0; } +static int sigusr1_handler(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + assert(s); + + /* If clients send use SIGUSR1 we'll explicitly rescan for home directories. This is useful in some + * cases where inotify isn't good enough, for example if /home/ is overmunted. */ + manager_watch_home(m); + (void) manager_gc_images(m); + (void) manager_enumerate_images(m); + (void) bus_manager_emit_auto_login_changed(m); + + return 0; +} + int manager_new(Manager **ret) { _cleanup_(manager_freep) Manager *m = NULL; int r; @@ -237,6 +251,10 @@ int manager_new(Manager **ret) { if (r < 0) return r; + r = sd_event_add_signal(m->event, /* ret_event_source= */ NULL, SIGUSR1|SD_EVENT_SIGNAL_PROCMASK, sigusr1_handler, m); + if (r < 0) + return r; + (void) sd_event_set_watchdog(m->event, true); m->homes_by_uid = hashmap_new(&homes_by_uid_hash_ops); From 0e1ede4b4b6d1ce6b5b6cda5f803e4f1b5aa4a03 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Feb 2025 21:56:54 +0100 Subject: [PATCH 15/18] homectl: add interface for controlling storage for negative machine ID matches --- docs/USER_RECORD.md | 17 +++- man/homectl.xml | 34 +++++++ shell-completion/bash/homectl | 5 +- src/home/homectl.c | 187 +++++++++++++++++++++++----------- 4 files changed, 175 insertions(+), 68 deletions(-) diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 24075dc1b1..7a6c5cf9f8 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -774,14 +774,23 @@ If any of the specified IDs match the system's local machine ID (As a special case, if only a single machine ID is listed this field may be a single string rather than an array of strings.) +`matchNotMachineId` → Similar to `matchMachineId` but implements the inverse +match: this section only applies if the local machine ID does *not* match any +of the listed IDs. + `matchHostname` → An array of strings that are valid hostnames. If any of the specified hostnames match the system's local hostname, the fields in this object are honored. -If both `matchHostname` and `matchMachineId` are used within the same array entry, the object is honored when either match succeeds, -i.e. the two match types are combined in OR, not in AND. (As a special case, if only a single hostname is listed this field may be a single string rather than an array of strings.) -These two are the only two fields specific to this section. -All other fields that may be used in this section are identical to the equally named ones in the +`matchNotHostname` → Similar to `matchHostname`, but implement the inverse +match, as above. + +If any of these four fields are used within the same array entry, the object is +honored when either match succeeds, i.e. the match types are combined in OR, +not in AND. + +These four are the only fields specific to this section. All other fields that +may be used in this section are identical to the equally named ones in the `regular` section (i.e. at the top-level object). Specifically, these are: `blobDirectory`, `blobManifest`, `iconName`, `location`, `shell`, `umask`, diff --git a/man/homectl.xml b/man/homectl.xml index 5d97c676a9..5c37f1bda9 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -205,6 +205,40 @@ + + + + + + + Takes one of this, other, + any or auto. Some user record settings can be defined to match + only specific machines, or all machines but one, or all machines. With this switch it is possibly to + control to which machines to apply the settings appearing on the command line after it. If + this is specified the setting will only apply to the local system (positive + match), if other it will apply to all but the local system (negative match), if + any it will apply to all systems (unless there's a matching positive or negative + per-machine setting). If auto returns to the default logic: whether a setting + applies by default to the local system or all systems depends on the option in question. + + Note that only some user record settings can be conditioned like this. This option has no + effect on the others and is ignored there. This option may appear multiple times in a single command + line to apply settings conditioned by different matches to the same user record. See JSON User Records for details on which settings may be + used with such per-machine matching and which ones may not. + + is a shortcut for , is + short for and is short for + . + + Here's an example call that sets the storage field to luks on the local + system, but to cifs on all others: + + # homectl update lennart -T --storage=luks -N --storage=cifs + + + + diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl index 1010094782..2fed45403f 100644 --- a/shell-completion/bash/homectl +++ b/shell-completion/bash/homectl @@ -41,7 +41,7 @@ _homectl() { local -A OPTS=( [STANDALONE]='-h --help --version --no-pager --no-legend --no-ask-password - -j -E -P' + -j -E -P -A -N -T' [ARG]=' -H --host -M --machine --identity @@ -114,7 +114,8 @@ _homectl() { --session-launcher --session-type --key-name - --seize' + --seize + --match' ) if __contains_word "$prev" ${OPTS[ARG]}; then diff --git a/src/home/homectl.c b/src/home/homectl.c index 969aabd798..e51a39c470 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -70,6 +70,7 @@ static const char *arg_identity = NULL; static sd_json_variant *arg_identity_extra = NULL; static sd_json_variant *arg_identity_extra_privileged = NULL; static sd_json_variant *arg_identity_extra_this_machine = NULL; +static sd_json_variant *arg_identity_extra_other_machines = NULL; static sd_json_variant *arg_identity_extra_rlimits = NULL; static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */ static char **arg_identity_filter_rlimits = NULL; @@ -104,6 +105,7 @@ static bool arg_seize = true; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_other_machines, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); @@ -122,6 +124,7 @@ static bool identity_properties_specified(void) { !sd_json_variant_is_blank_object(arg_identity_extra) || !sd_json_variant_is_blank_object(arg_identity_extra_privileged) || !sd_json_variant_is_blank_object(arg_identity_extra_this_machine) || + !sd_json_variant_is_blank_object(arg_identity_extra_other_machines) || !sd_json_variant_is_blank_object(arg_identity_extra_rlimits) || !strv_isempty(arg_identity_filter) || !strv_isempty(arg_identity_filter_rlimits) || @@ -932,7 +935,7 @@ static int apply_identity_changes(sd_json_variant **_v) { if (r < 0) return log_error_errno(r, "Failed to merge identities: %m"); - if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) { + if (arg_identity_extra_this_machine || arg_identity_extra_other_machines || !strv_isempty(arg_identity_filter)) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *per_machine = NULL, *mmid = NULL; sd_id128_t mid; @@ -946,7 +949,7 @@ static int apply_identity_changes(sd_json_variant **_v) { per_machine = sd_json_variant_ref(sd_json_variant_by_key(v, "perMachine")); if (per_machine) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *npm = NULL, *add = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *npm = NULL, *positive = NULL, *negative = NULL; _cleanup_free_ sd_json_variant **array = NULL; sd_json_variant *z; size_t i = 0; @@ -954,7 +957,7 @@ static int apply_identity_changes(sd_json_variant **_v) { if (!sd_json_variant_is_array(per_machine)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing."); - array = new(sd_json_variant*, sd_json_variant_elements(per_machine) + 1); + array = new(sd_json_variant*, sd_json_variant_elements(per_machine) + 2); if (!array) return log_oom(); @@ -967,31 +970,41 @@ static int apply_identity_changes(sd_json_variant **_v) { array[i++] = z; u = sd_json_variant_by_key(z, "matchMachineId"); - if (!u) - continue; + if (u && sd_json_variant_equal(u, mmid)) + r = sd_json_variant_merge_object(&positive, z); + else { + u = sd_json_variant_by_key(z, "matchNotMachineId"); + if (!u || !sd_json_variant_equal(u, mmid)) + continue; - if (!sd_json_variant_equal(u, mmid)) - continue; - - r = sd_json_variant_merge_object(&add, z); + r = sd_json_variant_merge_object(&negative, z); + } if (r < 0) return log_error_errno(r, "Failed to merge perMachine entry: %m"); i--; } - r = sd_json_variant_filter(&add, arg_identity_filter); + r = sd_json_variant_filter(&positive, arg_identity_filter); if (r < 0) return log_error_errno(r, "Failed to filter perMachine: %m"); - r = sd_json_variant_merge_object(&add, arg_identity_extra_this_machine); + r = sd_json_variant_filter(&negative, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter perMachine: %m"); + + r = sd_json_variant_merge_object(&positive, arg_identity_extra_this_machine); + if (r < 0) + return log_error_errno(r, "Failed to merge in perMachine fields: %m"); + + r = sd_json_variant_merge_object(&negative, arg_identity_extra_other_machines); if (r < 0) return log_error_errno(r, "Failed to merge in perMachine fields: %m"); if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *rlv = NULL; - rlv = sd_json_variant_ref(sd_json_variant_by_key(add, "resourceLimits")); + rlv = sd_json_variant_ref(sd_json_variant_by_key(positive, "resourceLimits")); r = sd_json_variant_filter(&rlv, arg_identity_filter_rlimits); if (r < 0) @@ -1002,22 +1015,30 @@ static int apply_identity_changes(sd_json_variant **_v) { return log_error_errno(r, "Failed to set resource limits: %m"); if (sd_json_variant_is_blank_object(rlv)) { - r = sd_json_variant_filter(&add, STRV_MAKE("resourceLimits")); + r = sd_json_variant_filter(&positive, STRV_MAKE("resourceLimits")); if (r < 0) return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); } else { - r = sd_json_variant_set_field(&add, "resourceLimits", rlv); + r = sd_json_variant_set_field(&positive, "resourceLimits", rlv); if (r < 0) return log_error_errno(r, "Failed to update resource limits of identity: %m"); } } - if (!sd_json_variant_is_blank_object(add)) { - r = sd_json_variant_set_field(&add, "matchMachineId", mmid); + if (!sd_json_variant_is_blank_object(positive)) { + r = sd_json_variant_set_field(&positive, "matchMachineId", mmid); if (r < 0) return log_error_errno(r, "Failed to set matchMachineId field: %m"); - array[i++] = add; + array[i++] = positive; + } + + if (!sd_json_variant_is_blank_object(negative)) { + r = sd_json_variant_set_field(&negative, "matchNotMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchNotMachineId field: %m"); + + array[i++] = negative; } r = sd_json_variant_new_array(&npm, array, i); @@ -1027,21 +1048,34 @@ static int apply_identity_changes(sd_json_variant **_v) { sd_json_variant_unref(per_machine); per_machine = TAKE_PTR(npm); } else { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *item = sd_json_variant_ref(arg_identity_extra_this_machine); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *positive = sd_json_variant_ref(arg_identity_extra_this_machine), + *negative = sd_json_variant_ref(arg_identity_extra_other_machines); if (arg_identity_extra_rlimits) { - r = sd_json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits); + r = sd_json_variant_set_field(&positive, "resourceLimits", arg_identity_extra_rlimits); if (r < 0) return log_error_errno(r, "Failed to update resource limits of identity: %m"); } - r = sd_json_variant_set_field(&item, "matchMachineId", mmid); - if (r < 0) - return log_error_errno(r, "Failed to set matchMachineId field: %m"); + if (positive) { + r = sd_json_variant_set_field(&positive, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); - r = sd_json_variant_append_array(&per_machine, item); - if (r < 0) - return log_error_errno(r, "Failed to append to perMachine array: %m"); + r = sd_json_variant_append_array(&per_machine, positive); + if (r < 0) + return log_error_errno(r, "Failed to append to perMachine array: %m"); + } + + if (negative) { + r = sd_json_variant_set_field(&negative, "matchNotMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchNotMachineId field: %m"); + + r = sd_json_variant_append_array(&per_machine, negative); + if (r < 0) + return log_error_errno(r, "Failed to append to perMachine array: %m"); + } } r = sd_json_variant_set_field(&v, "perMachine", per_machine); @@ -3239,6 +3273,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_DEFAULT_AREA, ARG_KEY_NAME, ARG_SEIZE, + ARG_MATCH, }; static const struct option options[] = { @@ -3344,11 +3379,17 @@ static int parse_argv(int argc, char *argv[]) { { "default-area", required_argument, NULL, ARG_DEFAULT_AREA }, { "key-name", required_argument, NULL, ARG_KEY_NAME }, { "seize", required_argument, NULL, ARG_SEIZE }, + { "match", required_argument, NULL, ARG_MATCH }, {} }; int r; + /* This points to one of arg_identity_extra, arg_identity_extra_this_machine, + * arg_identity_extra_other_machines, in order to redirect changes on the next property being set to + * this part of the identity, instead of the default. */ + sd_json_variant **match_identity = NULL; + assert(argc >= 0); assert(argv); @@ -3362,7 +3403,7 @@ static int parse_argv(int argc, char *argv[]) { for (;;) { int c; - c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL); + c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL); if (c < 0) break; @@ -3416,7 +3457,7 @@ static int parse_argv(int argc, char *argv[]) { if (!valid_gecos(optarg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "realName", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "realName", optarg); if (r < 0) return log_error_errno(r, "Failed to set realName field: %m"); @@ -3546,7 +3587,7 @@ static int parse_argv(int argc, char *argv[]) { break; } - r = sd_json_variant_set_field_string(&arg_identity_extra, field, optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, field, optarg); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -3566,7 +3607,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to validate CIFS service name: %s", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "cifsService", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "cifsService", optarg); if (r < 0) return log_error_errno(r, "Failed to set cifsService field: %m"); @@ -3602,7 +3643,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse nice level: %s", optarg); - r = sd_json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc); + r = sd_json_variant_set_field_integer(match_identity ?: &arg_identity_extra, "niceLevel", nc); if (r < 0) return log_error_errno(r, "Failed to set niceLevel field: %m"); @@ -3726,7 +3767,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_string(&arg_identity_extra_this_machine, field, v); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra_this_machine, field, v); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", v); @@ -3745,7 +3786,7 @@ static int parse_argv(int argc, char *argv[]) { if (!valid_shell(optarg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "shell", optarg); if (r < 0) return log_error_errno(r, "Failed to set shell field: %m"); @@ -3764,7 +3805,7 @@ static int parse_argv(int argc, char *argv[]) { break; } - e = sd_json_variant_by_key(arg_identity_extra, "environment"); + e = sd_json_variant_by_key(match_identity ? *match_identity: arg_identity_extra, "environment"); if (e) { r = sd_json_variant_strv(e, &l); if (r < 0) @@ -3781,7 +3822,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to allocate environment list JSON: %m"); - r = sd_json_variant_set_field(&arg_identity_extra, "environment", ne); + r = sd_json_variant_set_field(match_identity ?: &arg_identity_extra, "environment", ne); if (r < 0) return log_error_errno(r, "Failed to set environment list: %m"); @@ -3789,7 +3830,6 @@ static int parse_argv(int argc, char *argv[]) { } case ARG_TIMEZONE: - if (isempty(optarg)) { r = drop_from_identity("timeZone"); if (r < 0) @@ -3801,7 +3841,7 @@ static int parse_argv(int argc, char *argv[]) { if (!timezone_is_valid(optarg, LOG_DEBUG)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "timeZone", optarg); if (r < 0) return log_error_errno(r, "Failed to set timezone field: %m"); @@ -3881,7 +3921,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s boolean: %m", field); - r = sd_json_variant_set_field_boolean(&arg_identity_extra, field, r > 0); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, field, r > 0); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -3917,7 +3957,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "diskSize", arg_disk_size); if (r < 0) return log_error_errno(r, "Failed to set diskSize field: %m"); @@ -3930,7 +3970,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); if (r < 0) return log_error_errno(r, "Failed to set diskSizeRelative field: %m"); @@ -3938,7 +3978,7 @@ static int parse_argv(int argc, char *argv[]) { } /* Automatically turn off the rebalance logic if user configured a size explicitly */ - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "rebalanceWeight", REBALANCE_WEIGHT_OFF); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "rebalanceWeight", REBALANCE_WEIGHT_OFF); if (r < 0) return log_error_errno(r, "Failed to set rebalanceWeight field: %m"); @@ -3979,7 +4019,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg); - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "luksDiscard", r); if (r < 0) return log_error_errno(r, "Failed to set discard field: %m"); @@ -3998,7 +4038,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --luks-offline-discard= parameter: %s", optarg); - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "luksOfflineDiscard", r); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "luksOfflineDiscard", r); if (r < 0) return log_error_errno(r, "Failed to set offline discard field: %m"); @@ -4027,7 +4067,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4049,7 +4089,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "luksSectorSize", ss); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "luksSectorSize", ss); if (r < 0) return log_error_errno(r, "Failed to set sector size field: %m"); @@ -4071,7 +4111,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse umask: %m"); - r = sd_json_variant_set_field_integer(&arg_identity_extra, "umask", m); + r = sd_json_variant_set_field_integer(match_identity ?: &arg_identity_extra, "umask", m); if (r < 0) return log_error_errno(r, "Failed to set umask field: %m"); @@ -4183,7 +4223,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %m", field); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); break; @@ -4216,7 +4256,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %m", field); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); break; @@ -4251,9 +4291,9 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg); r = sd_json_variant_set_field_string( - IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? - &arg_identity_extra_this_machine : - &arg_identity_extra, field, optarg); + match_identity ?: (IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? + &arg_identity_extra_this_machine : + &arg_identity_extra), field, optarg); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4284,7 +4324,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s field: %s", field, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, t); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, t); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4333,7 +4373,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to create group list JSON: %m"); - r = sd_json_variant_set_field(&arg_identity_extra, "memberOf", mo); + r = sd_json_variant_set_field(match_identity ?: &arg_identity_extra, "memberOf", mo); if (r < 0) return log_error_errno(r, "Failed to update group list: %m"); } @@ -4355,7 +4395,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "tasksMax", u); if (r < 0) return log_error_errno(r, "Failed to set tasksMax field: %m"); @@ -4385,7 +4425,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, field, u); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4414,7 +4454,7 @@ static int parse_argv(int argc, char *argv[]) { if (!CGROUP_WEIGHT_IS_OK(u)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, u); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4545,7 +4585,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --auto-resize-mode= argument: %s", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "autoResizeMode", auto_resize_mode_to_string(r)); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "autoResizeMode", auto_resize_mode_to_string(r)); if (r < 0) return log_error_errno(r, "Failed to set autoResizeMode field: %m"); @@ -4579,7 +4619,7 @@ static int parse_argv(int argc, char *argv[]) { return r; /* Add to main identity */ - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "rebalanceWeight", u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "rebalanceWeight", u); if (r < 0) return log_error_errno(r, "Failed to set rebalanceWeight field: %m"); @@ -4646,7 +4686,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "dropCaches", r); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "dropCaches", r); if (r < 0) return log_error_errno(r, "Failed to set drop caches field: %m"); @@ -4700,7 +4740,7 @@ static int parse_argv(int argc, char *argv[]) { if (capability_set_to_strv(updated, &l) < 0) return log_oom(); - r = sd_json_variant_set_field_strv(&arg_identity_extra, field, l); + r = sd_json_variant_set_field_strv(match_identity ?: &arg_identity_extra, field, l); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4814,7 +4854,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s/%s parameter: %s", field, field_scale, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, u); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4822,7 +4862,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; } else { - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r)); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r)); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field_scale); @@ -4846,7 +4886,7 @@ static int parse_argv(int argc, char *argv[]) { if (!filename_is_valid(optarg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for default area field not valid: %s", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "defaultArea", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "defaultArea", optarg); if (r < 0) return log_error_errno(r, "Failed to set default area field: %m"); @@ -4873,6 +4913,29 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case ARG_MATCH: + if (streq(optarg, "any")) + match_identity = &arg_identity_extra; + else if (streq(optarg, "this")) + match_identity = &arg_identity_extra_this_machine; + else if (streq(optarg, "other")) + match_identity = &arg_identity_extra_other_machines; + else if (streq(optarg, "auto")) + match_identity = NULL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--machine= argument not understood. Refusing."); + break; + + case 'A': + match_identity = &arg_identity_extra; + break; + case 'T': + match_identity = &arg_identity_extra_this_machine; + break; + case 'N': + match_identity = &arg_identity_extra_other_machines; + break; + case '?': return -EINVAL; From 2efffd036e6ebbb95f9f47eb5cfe32787235b371 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Feb 2025 11:07:20 +0100 Subject: [PATCH 16/18] homectl: rearrange --help text with sections We support so many verbs now, help the user with some structure --- src/home/homectl.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index e51a39c470..291ce8eaf1 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2988,32 +2988,36 @@ static int help(int argc, char *argv[], void *userdata) { printf("%1$s [OPTIONS...] COMMAND ...\n\n" "%2$sCreate, manipulate or inspect home directories.%3$s\n" - "\n%4$sCommands:%5$s\n" + "\n%4$sBasic User Manipulation Commands:%5$s\n" " list List home areas\n" - " activate USER… Activate a home area\n" - " deactivate USER… Deactivate a home area\n" " inspect USER… Inspect a home area\n" - " authenticate USER… Authenticate a home area\n" " create USER Create a home area\n" - " adopt PATH… Add an existing home area on this system\n" - " register PATH… Register a user record locally\n" - " unregister USER… Unregister a user record locally\n" - " remove USER… Remove a home area\n" " update USER Update a home area\n" " passwd USER Change password of a home area\n" " resize USER SIZE Resize a home area\n" - " lock USER… Temporarily lock an active home area\n" - " unlock USER… Unlock a temporarily locked home area\n" - " lock-all Lock all suitable home areas\n" + " remove USER… Remove a home area\n" + "\n%4$sAdvanced User Manipulation Commands:%5$s\n" + " activate USER… Activate a home area\n" + " deactivate USER… Deactivate a home area\n" " deactivate-all Deactivate all active home areas\n" - " 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" + " authenticate USER… Authenticate a home area\n" + "\n%4$sUser Migration Commands:%5$s\n" + " adopt PATH… Add an existing home area on this system\n" + " register PATH… Register a user record locally\n" + " unregister USER… Unregister a user record locally\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$sLock/Unlock Commands:%5$s\n" + " lock USER… Temporarily lock an active home area\n" + " unlock USER… Unlock a temporarily locked home area\n" + " lock-all Lock all suitable home areas\n" + "\n%4$sOther Commands:%5$s\n" + " rebalance Rebalance free space between home areas\n" + " firstboot Run first-boot home area creation wizard\n" "\n%4$sOptions:%5$s\n" " -h --help Show this help\n" " --version Show package version\n" From b9f711b66e5ccbb2935d87281adebf7652968c2e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 27 Feb 2025 17:20:29 +0100 Subject: [PATCH 17/18] test: add integration test for the functionality added in this PR --- test/units/TEST-46-HOMED.sh | 120 +++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/test/units/TEST-46-HOMED.sh b/test/units/TEST-46-HOMED.sh index 5bab0b11b7..78c91044b8 100755 --- a/test/units/TEST-46-HOMED.sh +++ b/test/units/TEST-46-HOMED.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # SPDX-License-Identifier: LGPL-2.1-or-later -# shellcheck disable=SC2016 +# shellcheck disable=SC2016,SC2209 set -eux set -o pipefail @@ -28,9 +28,18 @@ inspect() { homectl inspect --json=pretty "$USERNAME" } +wait_for_exist() { + # 2min max + for i in {1..60}; do + (( i > 1 )) && sleep 2 + homectl inspect "$1" && break + done +} + wait_for_state() { - for i in {1..10}; do - (( i > 1 )) && sleep 0.5 + # 2min max + for i in {1..60}; do + (( i > 1 )) && sleep 2 homectl inspect "$1" | grep -qF "State: $2" && break done } @@ -46,6 +55,9 @@ systemctl service-log-level systemd-homed debug mkdir -p /home mount -t tmpfs tmpfs /home -o size=290M +# Make sure systemd-homed takes notice of the overmounted /home/ +systemctl kill -sUSR1 systemd-homed + TMP_SKEL=$(mktemp -d) echo hogehoge >"$TMP_SKEL"/hoge @@ -727,6 +739,108 @@ systemctl stop user@"$(id -u subareatest)".service wait_for_state subareatest inactive homectl remove subareatest +# Test signing key logic +homectl list-signing-keys | grep -q local.public +(! (homectl list-signing-keys | grep -q signtest.public)) + +IDENTITY='{"userName":"signtest","storage":"directory","disposition":"regular","privileged":{"hashedPassword":["$y$j9T$I5Wxfm.fyg.RRWlgWw.rI1$gnQqGtbpPexqxZJkWMq8FxQi5Swc.CWeKtM8LwvEUB6"]},"enforcePasswordPolicy":false,"lastChangeUSec":1740677608017608,"lastPasswordChangeUSec":1740677608017608,"signature":[{"data":"Gl4wtc0sMjVnsH6FQwG/0M+x0nLI5cvvdtSSCttUu1gNtXqYn0UI4wZi/7zX35ERht6XHWDlP4d6V8HiAst4Dg==","key":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA6uvVaP1vh7O6nIbiOcvyIHRl4ihYSs0R7ctxtz2Zu7E=\n-----END PUBLIC KEY-----\n"}],"secret":{"password":["test"]}}' + +# Try with stripping the foreign signature first, this should just work +echo "$IDENTITY" | homectl create -P --identity=- --seize=yes +homectl remove signtest + +# No try again, and don't strip the signature. It will be refused. +(! (echo "$IDENTITY" | homectl create -P --identity=- --seize=no)) + +print_public_key() { + cat < Date: Thu, 27 Feb 2025 22:59:02 +0100 Subject: [PATCH 18/18] update TODO --- TODO | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO b/TODO index 1aeae13ab9..773edc5590 100644 --- a/TODO +++ b/TODO @@ -157,6 +157,10 @@ Features: also use this to detect unclean shutdowns, boot into special target if detected +* fix homed/homectl confusion around terminology, i.e. "home directory" + vs. "home" vs. "home area". Stick to one term for the concept, and it + probably shouldn't contain "area". + * sd-boot: do something useful if we find exactly zero entries (ignoring items such as reboot/poweroff/factory reset). Show a help text or so.