]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homectl: reorder verb functions to match order in --help
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 15 May 2026 11:33:58 +0000 (13:33 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 15 May 2026 16:08:20 +0000 (18:08 +0200)
Just a hand-crafted moving of blocks of code up and down, no other
changes. The net diff is -2 because add_signing_keys_from_credentials
forward declaration was dropped.

src/home/homectl.c

index e9161dfb751e5c314361002df1577030d912551e..42d210d1c6c955648225362c30d9054f1128b525 100644 (file)
@@ -627,85 +627,6 @@ static int acquire_passed_secrets(const char *user_name, UserRecord **ret) {
         return 0;
 }
 
-static int verb_activate_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        int r, ret = 0;
-
-        r = acquire_bus(&bus);
-        if (r < 0)
-                return r;
-
-        STRV_FOREACH(i, strv_skip(argv, 1)) {
-                _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
-
-                r = acquire_passed_secrets(*i, &secret);
-                if (r < 0)
-                        return r;
-
-                for (;;) {
-                        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-
-                        r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHome");
-                        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);
-
-                        r = bus_message_append_secret(m, secret);
-                        if (r < 0)
-                                return bus_log_create_error(r);
-
-                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
-                        if (r < 0) {
-                                r = handle_generic_user_record_error(*i, secret, &error, r, /* emphasize_current_password= */ false);
-                                if (r < 0) {
-                                        if (ret == 0)
-                                                ret = r;
-
-                                        break;
-                                }
-                        } else
-                                break;
-                }
-        }
-
-        return ret;
-}
-
-static int verb_deactivate_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        int r, ret = 0;
-
-        r = acquire_bus(&bus);
-        if (r < 0)
-                return r;
-
-        STRV_FOREACH(i, strv_skip(argv, 1)) {
-                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-
-                r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateHome");
-                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);
-
-                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
-                if (r < 0) {
-                        log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r));
-                        if (ret == 0)
-                                ret = r;
-                }
-        }
-
-        return ret;
-}
-
 static void dump_home_record(UserRecord *hr) {
         int r;
 
@@ -799,65 +720,6 @@ static int verb_inspect_homes(int argc, char *argv[], uintptr_t _data, void *use
         }
 }
 
-static int authenticate_home(sd_bus *bus, const char *name) {
-        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
-        int r;
-
-        r = acquire_passed_secrets(name, &secret);
-        if (r < 0)
-                return r;
-
-        for (;;) {
-                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-
-                r = bus_message_new_method_call(bus, &m, bus_mgr, "AuthenticateHome");
-                if (r < 0)
-                        return bus_log_create_error(r);
-
-                r = sd_bus_message_append(m, "s", name);
-                if (r < 0)
-                        return bus_log_create_error(r);
-
-                r = bus_message_append_secret(m, secret);
-                if (r < 0)
-                        return bus_log_create_error(r);
-
-                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
-                if (r < 0) {
-                        r = handle_generic_user_record_error(name, secret, &error, r, false);
-                        if (r >= 0)
-                                continue;
-                }
-                return r;
-        }
-}
-
-static int verb_authenticate_homes(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        int r;
-
-        r = acquire_bus(&bus);
-        if (r < 0)
-                return r;
-
-        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
-
-        char **args = strv_skip(argv, 1);
-        if (args) {
-                STRV_FOREACH(arg, args)
-                        RET_GATHER(r, authenticate_home(bus, *arg));
-
-                return r;
-        } else {
-                _cleanup_free_ char *myself = getusername_malloc();
-                if (!myself)
-                        return log_oom();
-
-                return authenticate_home(bus, myself);
-        }
-}
-
 static int update_last_change(sd_json_variant **v, bool with_password, bool override) {
         sd_json_variant *c;
         usec_t n;
@@ -1592,265 +1454,89 @@ static int verb_create_home(int argc, char *argv[], uintptr_t _data, void *userd
         return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ true);
 }
 
-static int verb_adopt_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        int r, ret = 0;
+static int acquire_updated_home_record(
+                sd_bus *bus,
+                const char *username,
+                UserRecord **ret) {
 
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        r = acquire_bus(&bus);
-        if (r < 0)
-                return r;
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        int r;
 
-        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+        assert(ret);
 
-        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);
+        if (arg_identity) {
+                unsigned line = 0, column = 0;
+                sd_json_variant *un;
 
-                r = sd_bus_message_append(m, "st", *i, UINT64_C(0));
+                r = sd_json_parse_file(
+                                streq(arg_identity, "-") ? stdin : NULL,
+                                streq(arg_identity, "-") ? "<stdin>" : arg_identity,
+                                SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
+                                &json,
+                                &line,
+                                &column);
                 if (r < 0)
-                        return bus_log_create_error(r);
+                        return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
 
-                _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;
+                un = sd_json_variant_by_key(json, "userName");
+                if (un) {
+                        if (!sd_json_variant_is_string(un) || (username && !streq(sd_json_variant_string(un), username)))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match.");
+                } else {
+                        if (!username)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified.");
+
+                        r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to set userName field: %m");
                 }
-        }
 
-        return ret;
-}
+        } else {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+                int incomplete;
+                const char *text;
 
-static int register_home_common(sd_bus *bus, sd_json_variant *v) {
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL;
-        int r;
+                if (!identity_properties_specified())
+                        return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified.");
 
-        assert(v);
+                r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", username);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r));
 
-        if (!bus) {
-                r = acquire_bus(&_bus);
+                r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL);
                 if (r < 0)
-                        return r;
-                bus = _bus;
-        }
+                        return bus_log_parse_error(r);
 
-        _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);
+                if (incomplete)
+                        return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record.");
 
-        _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_json_parse(
+                                text,
+                                SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
+                                &json,
+                                /* reterr_line= */ NULL,
+                                /* reterr_column= */ NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse JSON identity: %m");
 
-        r = sd_bus_message_append(m, "s", formatted);
-        if (r < 0)
-                return bus_log_create_error(r);
+                reply = sd_bus_message_unref(reply);
 
-        _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));
+                r = sd_json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest"));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
+        }
 
-        return 0;
-}
+        r = apply_identity_changes(&json);
+        if (r < 0)
+                return r;
 
-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_MUST_BE_OBJECT|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[], uintptr_t _data, 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, "<stdio>");
-
-        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[], uintptr_t _data, 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 verb_remove_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        int r, ret = 0;
-
-        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_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-
-                r = bus_message_new_method_call(bus, &m, bus_mgr, "RemoveHome");
-                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);
-
-                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
-                if (r < 0) {
-                        log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r));
-                        if (ret == 0)
-                                ret = r;
-                }
-        }
-
-        return ret;
-}
-
-static int acquire_updated_home_record(
-                sd_bus *bus,
-                const char *username,
-                UserRecord **ret) {
-
-        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
-        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
-        int r;
-
-        assert(ret);
-
-        if (arg_identity) {
-                unsigned line = 0, column = 0;
-                sd_json_variant *un;
-
-                r = sd_json_parse_file(
-                                streq(arg_identity, "-") ? stdin : NULL,
-                                streq(arg_identity, "-") ? "<stdin>" : arg_identity,
-                                SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
-                                &json,
-                                &line,
-                                &column);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
-
-                un = sd_json_variant_by_key(json, "userName");
-                if (un) {
-                        if (!sd_json_variant_is_string(un) || (username && !streq(sd_json_variant_string(un), username)))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match.");
-                } else {
-                        if (!username)
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified.");
-
-                        r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to set userName field: %m");
-                }
-
-        } else {
-                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
-                int incomplete;
-                const char *text;
-
-                if (!identity_properties_specified())
-                        return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified.");
-
-                r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", username);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r));
-
-                r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL);
-                if (r < 0)
-                        return bus_log_parse_error(r);
-
-                if (incomplete)
-                        return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record.");
-
-                r = sd_json_parse(
-                                text,
-                                SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE,
-                                &json,
-                                /* reterr_line= */ NULL,
-                                /* reterr_column= */ NULL);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse JSON identity: %m");
-
-                reply = sd_bus_message_unref(reply);
-
-                r = sd_json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest"));
-                if (r < 0)
-                        return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
-        }
-
-        r = apply_identity_changes(&json);
-        if (r < 0)
-                return r;
-
-        STRV_FOREACH(i, arg_pkcs11_token_uri) {
-                r = identity_add_pkcs11_key_data(&json, *i);
-                if (r < 0)
-                        return r;
-        }
+        STRV_FOREACH(i, arg_pkcs11_token_uri) {
+                r = identity_add_pkcs11_key_data(&json, *i);
+                if (r < 0)
+                        return r;
+        }
 
         STRV_FOREACH(i, arg_fido2_device) {
                 r = identity_add_fido2_parameters(&json, *i, arg_fido2_lock_with, arg_fido2_cred_alg);
@@ -2265,7 +1951,7 @@ static int verb_resize_home(int argc, char *argv[], uintptr_t _data, void *userd
         return 0;
 }
 
-static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+static int verb_remove_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r, ret = 0;
 
@@ -2273,11 +1959,13 @@ static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdat
         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_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                 _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
 
-                r = bus_message_new_method_call(bus, &m, bus_mgr, "LockHome");
+                r = bus_message_new_method_call(bus, &m, bus_mgr, "RemoveHome");
                 if (r < 0)
                         return bus_log_create_error(r);
 
@@ -2287,7 +1975,7 @@ static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdat
 
                 r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                 if (r < 0) {
-                        log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r));
+                        log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r));
                         if (ret == 0)
                                 ret = r;
                 }
@@ -2296,7 +1984,7 @@ static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdat
         return ret;
 }
 
-static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+static int verb_activate_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r, ret = 0;
 
@@ -2315,7 +2003,7 @@ static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userd
                         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
 
-                        r = bus_message_new_method_call(bus, &m, bus_mgr, "UnlockHome");
+                        r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHome");
                         if (r < 0)
                                 return bus_log_create_error(r);
 
@@ -2329,7 +2017,7 @@ static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userd
 
                         r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
                         if (r < 0) {
-                                r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
+                                r = handle_generic_user_record_error(*i, secret, &error, r, /* emphasize_current_password= */ false);
                                 if (r < 0) {
                                         if (ret == 0)
                                                 ret = r;
@@ -2344,6 +2032,58 @@ static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userd
         return ret;
 }
 
+static int verb_deactivate_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        int r, ret = 0;
+
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        STRV_FOREACH(i, strv_skip(argv, 1)) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+                r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateHome");
+                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);
+
+                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r));
+                        if (ret == 0)
+                                ret = r;
+                }
+        }
+
+        return ret;
+}
+
+static int verb_deactivate_all_homes(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        int r;
+
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateAllHomes");
+        if (r < 0)
+                return bus_log_create_error(r);
+
+        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to deactivate all homes: %s", bus_error_message(&error, r));
+
+        return 0;
+}
+
 static int verb_with_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
@@ -2467,51 +2207,41 @@ static int verb_with_home(int argc, char *argv[], uintptr_t _data, void *userdat
         return ret;
 }
 
-static int verb_lock_all_homes(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+static int authenticate_home(sd_bus *bus, const char *name) {
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
         int r;
 
-        r = acquire_bus(&bus);
+        r = acquire_passed_secrets(name, &secret);
         if (r < 0)
                 return r;
 
-        r = bus_message_new_method_call(bus, &m, bus_mgr, "LockAllHomes");
-        if (r < 0)
-                return bus_log_create_error(r);
+        for (;;) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = 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 lock all homes: %s", bus_error_message(&error, r));
+                r = bus_message_new_method_call(bus, &m, bus_mgr, "AuthenticateHome");
+                if (r < 0)
+                        return bus_log_create_error(r);
 
-        return 0;
-}
+                r = sd_bus_message_append(m, "s", name);
+                if (r < 0)
+                        return bus_log_create_error(r);
 
-static int verb_deactivate_all_homes(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        int r;
+                r = bus_message_append_secret(m, secret);
+                if (r < 0)
+                        return bus_log_create_error(r);
 
-        r = acquire_bus(&bus);
-        if (r < 0)
+                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+                if (r < 0) {
+                        r = handle_generic_user_record_error(name, secret, &error, r, false);
+                        if (r >= 0)
+                                continue;
+                }
                 return r;
-
-        r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateAllHomes");
-        if (r < 0)
-                return bus_log_create_error(r);
-
-        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
-        if (r < 0)
-                return log_error_errno(r, "Failed to deactivate all homes: %s", bus_error_message(&error, r));
-
-        return 0;
+        }
 }
 
-static int verb_rebalance(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+static int verb_authenticate_homes(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r;
 
@@ -2519,855 +2249,937 @@ static int verb_rebalance(int argc, char *argv[], uintptr_t _data, void *userdat
         if (r < 0)
                 return r;
 
-        r = bus_message_new_method_call(bus, &m, bus_mgr, "Rebalance");
-        if (r < 0)
-                return bus_log_create_error(r);
+        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
 
-        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
-        if (r < 0) {
-                if (sd_bus_error_has_name(&error, BUS_ERROR_REBALANCE_NOT_NEEDED))
-                        log_info("No homes needed rebalancing.");
-                else
-                        return log_error_errno(r, "Failed to rebalance: %s", bus_error_message(&error, r));
-        } else
-                log_info("Completed rebalancing.");
+        char **args = strv_skip(argv, 1);
+        if (args) {
+                STRV_FOREACH(arg, args)
+                        RET_GATHER(r, authenticate_home(bus, *arg));
 
-        return 0;
-}
+                return r;
+        } else {
+                _cleanup_free_ char *myself = getusername_malloc();
+                if (!myself)
+                        return log_oom();
 
-static int create_or_register_from_credentials(void) {
-        int r;
+                return authenticate_home(bus, myself);
+        }
+}
 
-        _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");
+static int verb_adopt_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        int r, ret = 0;
 
-        _cleanup_free_ DirectoryEntries *des = NULL;
-        r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
         if (r < 0)
-                return log_error_errno(r, "Failed to enumerate credentials: %m");
+                return r;
 
-        int ret = 0, n_processed = 0;
-        FOREACH_ARRAY(i, des->entries, des->n_entries) {
-                struct dirent *de = *i;
-                if (de->d_type != DT_REG)
-                        continue;
+        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
 
-                enum {
-                        OPERATION_CREATE,
-                        OPERATION_REGISTER,
-                } op;
-                const char *e;
-                if ((e = startswith(de->d_name, "home.create.")))
-                        op = OPERATION_CREATE;
-                else if ((e = startswith(de->d_name, "home.register.")))
-                        op = OPERATION_REGISTER;
-                else
-                        continue;
+        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);
 
-                if (!valid_user_group_name(e, /* flags= */ 0)) {
-                        log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
-                        continue;
-                }
+                r = sd_bus_message_append(m, "st", *i, UINT64_C(0));
+                if (r < 0)
+                        return bus_log_create_error(r);
 
-                _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= */ SD_JSON_PARSE_MUST_BE_OBJECT,
-                                &identity,
-                                &line,
-                                &column);
+                _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_warning_errno(r, "[%s:%u:%u] Failed to parse user record in credential, ignoring: %m", de->d_name, line, column);
-                        continue;
+                        log_error_errno(r, "Failed to adopt home: %s", bus_error_message(&error, r));
+                        if (ret == 0)
+                                ret = r;
                 }
+        }
 
-                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);
-                                continue;
-                        }
-
-                        if (!streq(sd_json_variant_string(un), e)) {
-                                log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, sd_json_variant_string(un), e);
-                                continue;
-                        }
-                } else {
-                        r = sd_json_variant_set_field_string(&identity, "userName", e);
-                        if (r < 0)
-                                return log_warning_errno(r, "Failed to set userName field: %m");
-                }
+        return ret;
+}
 
-                log_notice("Processing user '%s' from credentials.", e);
+static int register_home_common(sd_bus *bus, sd_json_variant *v) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL;
+        int r;
 
-                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_processed++;
+        assert(v);
 
-                RET_GATHER(ret, r);
+        if (!bus) {
+                r = acquire_bus(&_bus);
+                if (r < 0)
+                        return r;
+                bus = _bus;
         }
 
-        return ret < 0 ? ret : n_processed;
-}
-
-static int has_regular_user(void) {
-        _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
-        UserDBMatch match = USERDB_MATCH_NULL;
-        int r;
+        _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);
 
-        match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR);
+        _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 = userdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator);
+        r = sd_bus_message_append(m, "s", formatted);
         if (r < 0)
-                return log_error_errno(r, "Failed to create user enumerator: %m");
+                return bus_log_create_error(r);
 
-        r = userdb_iterator_get(iterator, &match, /* ret= */ NULL);
-        if (r == -ESRCH)
-                return false;
+        _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 enumerate users: %m");
+                return log_error_errno(r, "Failed to register home: %s", bus_error_message(&error, r));
 
-        return true;
+        return 0;
 }
 
-static int username_is_ok(const char *name, void *userdata) {
+static int register_home_one(sd_bus *bus, FILE *f, const char *path) {
         int r;
 
-        assert(name);
-
-        if (!valid_user_group_name(name, /* flags= */ 0)) {
-                log_notice("Specified user name is not a valid UNIX user name, try again: %s", name);
-                return false;
-        }
+        assert(bus);
+        assert(path);
 
-        r = userdb_by_name(name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
-        if (r == -ESRCH)
-                return true;
+        _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_MUST_BE_OBJECT|SD_JSON_PARSE_SENSITIVE, &v, &line, &column);
         if (r < 0)
-                return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", name);
+                return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column);
 
-        log_notice("Specified user '%s' exists already, try again.", name);
-        return false;
+        return register_home_common(bus, v);
 }
 
-static int create_interactively(void) {
-        _cleanup_free_ char *username = NULL;
+static int verb_register_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
-        if (!arg_prompt_new_user) {
-                log_debug("Prompting for user creation was not requested.");
-                return 0;
-        }
-
-        /* Needs to be called before mute_console or it will garble the screen */
-        (void) plymouth_hide_splash();
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
 
-        _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL;
-        (void) mute_console(&mute_console_link);
+        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
 
-        (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0);
+        if (arg_identity) {
+                if (argc > 1)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not accepting an arguments if --identity= is specified, refusing.");
 
-        if (arg_chrome)
-                chrome_show("Create a User Account", /* bottom= */ NULL);
+                return register_home_one(bus, /* f= */ NULL, arg_identity);
+        }
 
-        DEFER_VOID_CALL(chrome_hide);
+        if (argc == 1 || (argc == 2 && streq(argv[1], "-")))
+                return register_home_one(bus, /* f= */ stdin, "<stdio>");
 
-        if (emoji_enabled()) {
-                fputs(glyph(GLYPH_HOME), stdout);
-                putchar(' ');
+        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));
         }
-        printf("Please create your user account!\n\n");
 
-        r = prompt_loop("Please enter user name to create",
-                        GLYPH_IDCARD,
-                        /* menu= */ NULL,
-                        /* accepted= */ NULL,
-                        /* ellipsize_percentage= */ 60,
-                        /* n_columns= */ 3,
-                        /* column_width= */ 20,
-                        username_is_ok,
-                        /* refresh= */ NULL,
-                        /* userdata= */ NULL,
-                        PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE,
-                        &username);
+        return r;
+}
+
+static int verb_unregister_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        int r;
+
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
         if (r < 0)
                 return r;
-        if (isempty(username))
-                return 0;
 
-        r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
+        (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 verb_list_signing_keys(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        int r;
+
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
         if (r < 0)
-                return log_error_errno(r, "Failed to set userName field: %m");
+                return r;
 
-        /* Let's not insist on a strong password in the firstboot interactive interface. Insisting on this is
-         * really annoying, as the user cannot just invoke the tool again with "--enforce-password-policy=no"
-         * because after all the tool is called from the boot process, and not from an interactive
-         * shell. Moreover, when setting up an initial system we can assume the user owns it, and hence we
-         * don't need to hard enforce some policy on password strength some organization or OS vendor
-         * requires. Note that this just disables the *strict* enforcement of the password policy. Even with
-         * this disabled we'll still tell the user in the UI that the password is too weak and suggest better
-         * ones, even if we then accept the weak ones if the user insists, by repeating it. */
-        r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
+        _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 set enforcePasswordPolicy field: %m");
+                return log_error_errno(r, "Failed to list signing keys: %s", bus_error_message(&error, r));
 
-        if (arg_prompt_groups) {
-                _cleanup_strv_free_ char **groups = NULL;
+        _cleanup_(table_unrefp) Table *table = table_new("name", "key");
+        if (!table)
+                return log_oom();
 
-                putchar('\n');
+        r = sd_bus_message_enter_container(reply, 'a', "(sst)");
+        if (r < 0)
+                return bus_log_parse_error(r);
 
-                r = prompt_groups(username, &groups);
+        for (;;) {
+                const char *name, *pem;
+
+                r = sd_bus_message_read(reply, "(sst)", &name, &pem, NULL);
                 if (r < 0)
-                        return r;
+                        return bus_log_parse_error(r);
+                if (r == 0)
+                        break;
 
-                if (!strv_isempty(groups)) {
-                        r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups);
+                _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. */
+
+                        r = dlopen_libcrypto(LOG_DEBUG);
                         if (r < 0)
-                                return log_error_errno(r, "Failed to set memberOf field: %m");
+                                return r;
+
+                        _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 = sym_i2d_PUBKEY(key, (unsigned char**) &der);
+                        if (n < 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to encode key as DER.");
+
+                        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, glyph(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);
         }
 
-        if (arg_prompt_shell) {
-                _cleanup_free_ char *shell = NULL;
+        r = sd_bus_message_exit_container(reply);
+        if (r < 0)
+                return bus_log_parse_error(r);
 
-                putchar('\n');
+        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 = prompt_shell(username, &shell);
+                r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
                 if (r < 0)
                         return r;
-
-                if (!isempty(shell)) {
-                        log_info("Selected %s as the shell for user %s", shell, username);
-
-                        r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", shell);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to set shell field: %m");
-                }
         }
 
-        putchar('\n');
-
-        r = create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false);
-        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);
+        }
 
-        log_info("Successfully created account '%s'.", username);
         return 0;
 }
 
-static int add_signing_keys_from_credentials(void);
-
-static int verb_firstboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
+static int verb_get_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
-        /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
-         * tool. */
-
-        bool enabled;
-        r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled);
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
         if (r < 0)
-                return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
-        if (r > 0 && !enabled) {
-                log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
-                arg_prompt_new_user = false;
-        }
+                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;
+                }
 
-        RET_GATHER(ret, add_signing_keys_from_credentials());
-
-        r = create_or_register_from_credentials();
-        RET_GATHER(ret, r);
-        bool existing_users = r > 0;
-
-        r = getenv_bool("SYSTEMD_HOME_FIRSTBOOT_OVERRIDE");
-        if (r == 0)
-                return 0;
-        if (r < 0) {
-                if (r != -ENXIO)
-                        log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m");
+                const char *pem;
+                r = sd_bus_message_read(reply, "st", &pem, NULL);
+                if (r < 0) {
+                        RET_GATHER(ret, bus_log_parse_error(r));
+                        continue;
+                }
 
-                if (!existing_users) {
-                        r = has_regular_user();
-                        if (r < 0)
-                                return r;
+                fputs(pem, stdout);
+                if (!endswith(pem, "\n"))
+                        fputc('\n', stdout);
 
-                        existing_users = r > 0;
-                }
-                if (existing_users) {
-                        log_info("Regular user already present in user database, skipping interactive user creation.");
-                        return 0;
-                }
+                fflush(stdout);
         }
 
-        RET_GATHER(ret, create_interactively());
         return ret;
 }
 
-#define drop_from_identity(...) _drop_from_identity(STRV_MAKE(__VA_ARGS__))
-
-static int _drop_from_identity(char **fields) {
+static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) {
         int r;
 
-        /* If we are called to update an identity record and drop some field, let's keep track of what to
-         * remove from the old record */
-        r = strv_extend_strv(&arg_identity_filter, fields, /* filter_duplicates= */ true);
-        if (r < 0)
-                return log_oom();
-
-        /* Let's also drop the field if it was previously set to a new value on the same command line */
-        r = sd_json_variant_filter(&arg_identity_extra, fields);
-        if (r < 0)
-                return log_error_errno(r, "Failed to filter JSON identity data: %m");
+        assert_se(bus);
+        assert_se(fn);
+        assert_se(key);
 
-        r = sd_json_variant_filter(&arg_identity_extra_this_machine, fields);
+        _cleanup_free_ char *pem = NULL;
+        r = read_full_stream(key, &pem, /* ret_size= */ NULL);
         if (r < 0)
-                return log_error_errno(r, "Failed to filter JSON identity data: %m");
+                return log_error_errno(r, "Failed to read key '%s': %m", fn);
 
-        r = sd_json_variant_filter(&arg_identity_extra_privileged, fields);
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        r = bus_call_method(bus, bus_mgr, "AddSigningKey", &error, /* ret_reply= */ NULL, "sst", fn, pem, UINT64_C(0));
         if (r < 0)
-                return log_error_errno(r, "Failed to filter JSON identity data: %m");
+                return log_error_errno(r, "Failed to add signing key '%s': %s", fn, bus_error_message(&error, r));
 
         return 0;
 }
 
-static int parse_ssh_authorized_keys(sd_json_variant **identity, const char *field, const char *arg) {
-        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
-        _cleanup_strv_free_ char **l = NULL, **add = NULL;
+static int verb_add_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
-        assert(identity);
-        assert(field);
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
 
-        if (isempty(arg))
-                return drop_from_identity(field);
+        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.");
 
-        if (arg[0] == '@') {
-                /* If prefixed with '@', read from a file */
+                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.");
 
-                _cleanup_fclose_ FILE *f = fopen(arg + 1, "re");
-                if (!f)
-                        return log_error_errno(errno, "Failed to open '%s': %m", arg + 1);
+                STRV_FOREACH(k, strv_skip(argv, 1)) {
 
-                for (;;) {
-                        _cleanup_free_ char *line = NULL;
+                        if (streq(*k, "-"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified.");
 
-                        r = read_line(f, LONG_LINE_MAX, &line);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to read from '%s': %m", arg + 1);
-                        if (r == 0)
-                                break;
+                        _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;
+                                }
+                        }
 
-                        if (isempty(line) || line[0] == '#')
+                        _cleanup_fclose_ FILE *f = fopen(*k, "re");
+                        if (!f) {
+                                RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k));
                                 continue;
+                        }
 
-                        r = strv_consume(&add, TAKE_PTR(line));
-                        if (r < 0)
-                                return log_oom();
+                        RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f));
                 }
-        } else {
-                /* Otherwise, assume it's a literal key. Let's do some superficial checks
-                 * before accepting it though. */
+        }
 
-                if (string_has_cc(arg, NULL))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                               "Authorized key contains control characters, refusing.");
-                if (arg[0] == '#')
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?");
+        return ret;
+}
 
-                add = strv_new(arg);
-                if (!add)
-                        return log_oom();
-        }
+static int add_signing_keys_from_credentials(void) {
+        int r;
 
-        v = sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
-        if (v) {
-                r = sd_json_variant_strv(v, &l);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse %s list: %m", field);
-        }
+        _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");
 
-        r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ true);
+        _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_oom();
+                return log_error_errno(r, "Failed to enumerate credentials: %m");
 
-        v = sd_json_variant_unref(v);
+        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;
 
-        r = sd_json_variant_new_array_strv(&v, l);
-        if (r < 0)
-                return log_oom();
+                const char *e = startswith(de->d_name, "home.add-signing-key.");
+                if (!e)
+                        continue;
 
-        r = sd_json_variant_set_field(identity, field, v);
-        if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
+                if (!filename_is_valid(e))
+                        continue;
 
-        return 0;
+                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 parse_string_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int remove_signing_key_one(sd_bus *bus, const char *fn) {
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg))
-                return drop_from_identity(field);
+        assert_se(bus);
+        assert_se(fn);
 
-        r = sd_json_variant_set_field_string(identity, field, arg);
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        r = bus_call_method(bus, bus_mgr, "RemoveSigningKey", &error, /* ret_reply= */ NULL, "st", fn, UINT64_C(0));
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
+                return log_error_errno(r, "Failed to remove signing key '%s': %s", fn, bus_error_message(&error, r));
+
         return 0;
 }
 
-static int parse_home_directory_field(sd_json_variant **identity, const char *field, const char *arg) {
-        _cleanup_free_ char *hd = NULL;
+static int verb_remove_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (!isempty(arg)) {
-                r = parse_path_argument(arg, /* suppress_root= */ false, &hd);
-                if (r < 0)
-                        return r;
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
 
-                if (!valid_home(hd))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd);
-        }
+        r = EXIT_SUCCESS;
+        STRV_FOREACH(k, strv_skip(argv, 1))
+                RET_GATHER(r, remove_signing_key_one(bus, *k));
 
-        return parse_string_field(identity, field, hd);
+        return r;
 }
 
-static int parse_realm_field(sd_json_variant **identity, const char *field, const char *arg) {
-        int r;
+static int verb_lock_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        int r, ret = 0;
 
-        assert(identity);
-        assert(field);
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
 
-        if (!isempty(arg)) {
-                r = dns_name_is_valid(arg);
+        STRV_FOREACH(i, strv_skip(argv, 1)) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+                r = bus_message_new_method_call(bus, &m, bus_mgr, "LockHome");
                 if (r < 0)
-                        return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", arg);
-                if (r == 0)
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain.", arg);
+                        return bus_log_create_error(r);
+
+                r = sd_bus_message_append(m, "s", *i);
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r));
+                        if (ret == 0)
+                                ret = r;
+                }
         }
 
-        return parse_string_field(identity, field, arg);
+        return ret;
 }
 
-static int parse_path_field(sd_json_variant **identity, const char *field, const char *arg) {
-        _cleanup_free_ char *v = NULL;
-        int r;
+static int verb_unlock_home(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        int r, ret = 0;
 
-        assert(identity);
-        assert(field);
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
 
-        if (!isempty(arg)) {
-                r = parse_path_argument(arg, /* suppress_root= */ false, &v);
+        STRV_FOREACH(i, strv_skip(argv, 1)) {
+                _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+                r = acquire_passed_secrets(*i, &secret);
                 if (r < 0)
                         return r;
-        }
 
-        return parse_string_field(identity, field, v);
-}
+                for (;;) {
+                        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
 
-static int parse_filename_field(sd_json_variant **identity, const char *field, const char *arg) {
-        assert(identity);
-        assert(field);
+                        r = bus_message_new_method_call(bus, &m, bus_mgr, "UnlockHome");
+                        if (r < 0)
+                                return bus_log_create_error(r);
 
-        if (!isempty(arg) && !filename_is_valid(arg))
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "Parameter for %s field not a valid filename: %s", field, arg);
+                        r = sd_bus_message_append(m, "s", *i);
+                        if (r < 0)
+                                return bus_log_create_error(r);
 
-        return parse_string_field(identity, field, arg);
+                        r = bus_message_append_secret(m, secret);
+                        if (r < 0)
+                                return bus_log_create_error(r);
+
+                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+                        if (r < 0) {
+                                r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
+                                if (r < 0) {
+                                        if (ret == 0)
+                                                ret = r;
+
+                                        break;
+                                }
+                        } else
+                                break;
+                }
+        }
+
+        return ret;
 }
 
-static int parse_unsigned_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int verb_lock_all_homes(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg))
-                return drop_from_identity(field);
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
 
-        unsigned n;
-        r = safe_atou(arg, &n);
+        r = bus_message_new_method_call(bus, &m, bus_mgr, "LockAllHomes");
         if (r < 0)
-                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+                return bus_log_create_error(r);
 
-        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
+                return log_error_errno(r, "Failed to lock all homes: %s", bus_error_message(&error, r));
+
         return 0;
 }
 
-static int parse_u64_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int verb_rebalance(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg))
-                return drop_from_identity(field);
-
-        uint64_t n;
-        r = safe_atou64(arg, &n);
+        r = acquire_bus(&bus);
         if (r < 0)
-                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+                return r;
 
-        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        r = bus_message_new_method_call(bus, &m, bus_mgr, "Rebalance");
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
+                return bus_log_create_error(r);
+
+        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+        if (r < 0) {
+                if (sd_bus_error_has_name(&error, BUS_ERROR_REBALANCE_NOT_NEEDED))
+                        log_info("No homes needed rebalancing.");
+                else
+                        return log_error_errno(r, "Failed to rebalance: %s", bus_error_message(&error, r));
+        } else
+                log_info("Completed rebalancing.");
+
         return 0;
 }
 
-static int parse_size_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int create_or_register_from_credentials(void) {
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg))
-                return drop_from_identity(field);
-
-        uint64_t n;
-        r = parse_size(arg, 1024, &n);
-        if (r < 0)
-                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+        _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");
 
-        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        _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 set %s field: %m", field);
-        return 0;
-}
+                return log_error_errno(r, "Failed to enumerate credentials: %m");
 
-static int parse_boolean_field(sd_json_variant **identity, const char *field, const char *arg) {
-        int r;
+        int ret = 0, n_processed = 0;
+        FOREACH_ARRAY(i, des->entries, des->n_entries) {
+                struct dirent *de = *i;
+                if (de->d_type != DT_REG)
+                        continue;
 
-        assert(identity);
-        assert(field);
+                enum {
+                        OPERATION_CREATE,
+                        OPERATION_REGISTER,
+                } op;
+                const char *e;
+                if ((e = startswith(de->d_name, "home.create.")))
+                        op = OPERATION_CREATE;
+                else if ((e = startswith(de->d_name, "home.register.")))
+                        op = OPERATION_REGISTER;
+                else
+                        continue;
 
-        if (isempty(arg))
-                return drop_from_identity(field);
+                if (!valid_user_group_name(e, /* flags= */ 0)) {
+                        log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
+                        continue;
+                }
 
-        r = parse_boolean(arg);
-        if (r < 0)
-                return log_error_errno(r, "Failed to parse boolean parameter %s: %s", field, arg);
+                _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= */ SD_JSON_PARSE_MUST_BE_OBJECT,
+                                &identity,
+                                &line,
+                                &column);
+                if (r < 0) {
+                        log_warning_errno(r, "[%s:%u:%u] Failed to parse user record in credential, ignoring: %m", de->d_name, line, column);
+                        continue;
+                }
 
-        r = sd_json_variant_set_field_boolean(identity, field, r > 0);
-        if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
-        return 0;
-}
+                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);
+                                continue;
+                        }
 
-static int parse_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
-        int r;
+                        if (!streq(sd_json_variant_string(un), e)) {
+                                log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, sd_json_variant_string(un), e);
+                                continue;
+                        }
+                } else {
+                        r = sd_json_variant_set_field_string(&identity, "userName", e);
+                        if (r < 0)
+                                return log_warning_errno(r, "Failed to set userName field: %m");
+                }
 
-        assert(identity);
-        assert(field);
+                log_notice("Processing user '%s' from credentials.", e);
 
-        if (isempty(arg))
-                return drop_from_identity(field);
+                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_processed++;
 
-        mode_t mode;
-        r = parse_mode(arg, &mode);
-        if (r < 0)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", arg);
+                RET_GATHER(ret, r);
+        }
 
-        r = sd_json_variant_set_field_unsigned(identity, field, mode);
-        if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
-        return 0;
+        return ret < 0 ? ret : n_processed;
 }
 
-static int parse_timestamp_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int has_regular_user(void) {
+        _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+        UserDBMatch match = USERDB_MATCH_NULL;
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg))
-                return drop_from_identity(field);
+        match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR);
 
-        usec_t n;
-        r = parse_timestamp(arg, &n);
+        r = userdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator);
         if (r < 0)
-                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+                return log_error_errno(r, "Failed to create user enumerator: %m");
 
-        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        r = userdb_iterator_get(iterator, &match, /* ret= */ NULL);
+        if (r == -ESRCH)
+                return false;
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
-        return 0;
+                return log_error_errno(r, "Failed to enumerate users: %m");
+
+        return true;
 }
 
-static int parse_time_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int username_is_ok(const char *name, void *userdata) {
         int r;
 
-        assert(identity);
-        assert(field);
+        assert(name);
 
-        if (isempty(arg))
-                return drop_from_identity(field);
+        if (!valid_user_group_name(name, /* flags= */ 0)) {
+                log_notice("Specified user name is not a valid UNIX user name, try again: %s", name);
+                return false;
+        }
 
-        usec_t n;
-        r = parse_sec(arg, &n);
+        r = userdb_by_name(name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
+        if (r == -ESRCH)
+                return true;
         if (r < 0)
-                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+                return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", name);
 
-        r = sd_json_variant_set_field_unsigned(identity, field, n);
-        if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
-        return 0;
+        log_notice("Specified user '%s' exists already, try again.", name);
+        return false;
 }
 
-static int parse_uid_field(sd_json_variant **identity, const char *field, const char *arg) {
-        uid_t uid;
+static int create_interactively(void) {
+        _cleanup_free_ char *username = NULL;
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg))
-                return drop_from_identity(field);
+        if (!arg_prompt_new_user) {
+                log_debug("Prompting for user creation was not requested.");
+                return 0;
+        }
 
-        r = parse_uid(arg, &uid);
-        if (r < 0)
-                return log_error_errno(r, "Failed to parse UID '%s'.", arg);
+        /* Needs to be called before mute_console or it will garble the screen */
+        (void) plymouth_hide_splash();
 
-        const char *bad_range =
-                uid_is_system(uid) ? "in system range" :
-                uid_is_greeter(uid) ? "in greeter range" :
-                uid_is_dynamic(uid) ? "in dynamic ragne" :
-                uid == UID_NOBODY ? "nobody UID" : NULL;
-        if (bad_range)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID "UID_FMT" is %s, refusing.", uid, bad_range);
+        _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL;
+        (void) mute_console(&mute_console_link);
 
-        r = sd_json_variant_set_field_unsigned(identity, field, uid);
-        if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
-        return 0;
-}
+        (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0);
 
-static int parse_nice_field(sd_json_variant **identity, const char *field, const char *arg) {
-        int nc, r;
+        if (arg_chrome)
+                chrome_show("Create a User Account", /* bottom= */ NULL);
 
-        assert(identity);
-        assert(field);
+        DEFER_VOID_CALL(chrome_hide);
 
-        if (isempty(arg))
-                return drop_from_identity(field);
+        if (emoji_enabled()) {
+                fputs(glyph(GLYPH_HOME), stdout);
+                putchar(' ');
+        }
+        printf("Please create your user account!\n\n");
 
-        r = parse_nice(arg, &nc);
+        r = prompt_loop("Please enter user name to create",
+                        GLYPH_IDCARD,
+                        /* menu= */ NULL,
+                        /* accepted= */ NULL,
+                        /* ellipsize_percentage= */ 60,
+                        /* n_columns= */ 3,
+                        /* column_width= */ 20,
+                        username_is_ok,
+                        /* refresh= */ NULL,
+                        /* userdata= */ NULL,
+                        PROMPT_MAY_SKIP|PROMPT_SILENT_VALIDATE,
+                        &username);
         if (r < 0)
-                return log_error_errno(r, "Failed to parse nice level '%s': %m", arg);
+                return r;
+        if (isempty(username))
+                return 0;
 
-        r = sd_json_variant_set_field_integer(identity, field, nc);
+        r = sd_json_variant_set_field_string(&arg_identity_extra, "userName", username);
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
-        return 0;
-}
+                return log_error_errno(r, "Failed to set userName field: %m");
 
-static int parse_auto_resize_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
-        int r;
+        /* Let's not insist on a strong password in the firstboot interactive interface. Insisting on this is
+         * really annoying, as the user cannot just invoke the tool again with "--enforce-password-policy=no"
+         * because after all the tool is called from the boot process, and not from an interactive
+         * shell. Moreover, when setting up an initial system we can assume the user owns it, and hence we
+         * don't need to hard enforce some policy on password strength some organization or OS vendor
+         * requires. Note that this just disables the *strict* enforcement of the password policy. Even with
+         * this disabled we'll still tell the user in the UI that the password is too weak and suggest better
+         * ones, even if we then accept the weak ones if the user insists, by repeating it. */
+        r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m");
 
-        assert(identity);
-        assert(field);
+        if (arg_prompt_groups) {
+                _cleanup_strv_free_ char **groups = NULL;
 
-        if (!isempty(arg)) {
-                r = auto_resize_mode_from_string(arg);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
-                arg = auto_resize_mode_to_string(r);
-        }
+                putchar('\n');
 
-        return parse_string_field(identity, field, arg);
-}
+                r = prompt_groups(username, &groups);
+                if (r < 0)
+                        return r;
 
-static int parse_rebalance_weight(sd_json_variant **identity, const char *field, const char *arg) {
-        int r;
+                if (!strv_isempty(groups)) {
+                        r = sd_json_variant_set_field_strv(&arg_identity_extra, "memberOf", groups);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to set memberOf field: %m");
+                }
+        }
 
-        assert(identity);
-        assert(field);
+        if (arg_prompt_shell) {
+                _cleanup_free_ char *shell = NULL;
 
-        if (isempty(arg))
-                return drop_from_identity(field);
+                putchar('\n');
 
-        uint64_t u;
-        if (streq(arg, "off"))
-                u = REBALANCE_WEIGHT_OFF;
-        else {
-                r = safe_atou64(arg, &u);
+                r = prompt_shell(username, &shell);
                 if (r < 0)
-                        return log_error_errno(r, "Failed to parse rebalance weight parameter: %s", arg);
+                        return r;
 
-                if (u < REBALANCE_WEIGHT_MIN || u > REBALANCE_WEIGHT_MAX)
-                        return log_error_errno(SYNTHETIC_ERRNO(ERANGE),
-                                               "Rebalancing weight out of valid range %" PRIu64 "%s%" PRIu64 ": %s",
-                                               REBALANCE_WEIGHT_MIN, glyph(GLYPH_ELLIPSIS), REBALANCE_WEIGHT_MAX,
-                                               arg);
+                if (!isempty(shell)) {
+                        log_info("Selected %s as the shell for user %s", shell, username);
+
+                        r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", shell);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to set shell field: %m");
+                }
         }
 
-        /* Drop from per machine stuff and everywhere */
-        r = drop_from_identity(field);
+        putchar('\n');
+
+        r = create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false);
         if (r < 0)
                 return r;
 
-        r = sd_json_variant_set_field_unsigned(identity, field, u);
-        if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
+        log_info("Successfully created account '%s'.", username);
         return 0;
 }
 
-static int parse_rlimit_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int verb_firstboot(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg)) {
-                /* Remove all resource limits */
-
-                r = drop_from_identity(field);
-                if (r < 0)
-                        return r;
+        /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
+         * tool. */
 
-                arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits);
-                *identity = sd_json_variant_unref(*identity);
-                return 0;
+        bool enabled;
+        r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
+        if (r > 0 && !enabled) {
+                log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
+                arg_prompt_new_user = false;
         }
 
-        const char *eq = strchr(arg, '=');
-        if (!eq)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", arg);
-
-        _cleanup_free_ char *s = strndup(arg, eq - arg);
-        if (!s)
-                return log_oom();
+        int ret = 0;
 
-        int limit = rlimit_from_string_harder(s);
-        if (limit < 0)
-                return log_error_errno(limit, "Unknown resource limit type: %s", s);
+        RET_GATHER(ret, add_signing_keys_from_credentials());
 
-        const char *rlimit_field = strjoina("RLIMIT_", rlimit_to_string(limit));
+        r = create_or_register_from_credentials();
+        RET_GATHER(ret, r);
+        bool existing_users = r > 0;
 
-        if (isempty(eq + 1)) {
-                /* Remove only the specific rlimit */
+        r = getenv_bool("SYSTEMD_HOME_FIRSTBOOT_OVERRIDE");
+        if (r == 0)
+                return 0;
+        if (r < 0) {
+                if (r != -ENXIO)
+                        log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m");
 
-                r = strv_extend(&arg_identity_filter_rlimits, rlimit_field);
-                if (r < 0)
-                        return r;
+                if (!existing_users) {
+                        r = has_regular_user();
+                        if (r < 0)
+                                return r;
 
-                r = sd_json_variant_filter(identity, STRV_MAKE(rlimit_field));
-                if (r < 0)
-                        return log_error_errno(r, "Failed to filter JSON identity data: %m");
-                return 0;
+                        existing_users = r > 0;
+                }
+                if (existing_users) {
+                        log_info("Regular user already present in user database, skipping interactive user creation.");
+                        return 0;
+                }
         }
 
-        _cleanup_(sd_json_variant_unrefp) sd_json_variant *jcur = NULL, *jmax = NULL;
-        struct rlimit rl;
+        RET_GATHER(ret, create_interactively());
+        return ret;
+}
 
-        r = rlimit_parse(limit, eq + 1, &rl);
+#define drop_from_identity(...) _drop_from_identity(STRV_MAKE(__VA_ARGS__))
+
+static int _drop_from_identity(char **fields) {
+        int r;
+
+        /* If we are called to update an identity record and drop some field, let's keep track of what to
+         * remove from the old record */
+        r = strv_extend_strv(&arg_identity_filter, fields, /* filter_duplicates= */ true);
         if (r < 0)
-                return log_error_errno(r, "Failed to parse resource limit value: %s", eq + 1);
+                return log_oom();
 
-        r = rl.rlim_cur == RLIM_INFINITY ? sd_json_variant_new_null(&jcur) : sd_json_variant_new_unsigned(&jcur, rl.rlim_cur);
+        /* Let's also drop the field if it was previously set to a new value on the same command line */
+        r = sd_json_variant_filter(&arg_identity_extra, fields);
         if (r < 0)
-                return log_error_errno(r, "Failed to allocate json variant: %m");
+                return log_error_errno(r, "Failed to filter JSON identity data: %m");
 
-        r = rl.rlim_max == RLIM_INFINITY ? sd_json_variant_new_null(&jmax) : sd_json_variant_new_unsigned(&jmax, rl.rlim_max);
+        r = sd_json_variant_filter(&arg_identity_extra_this_machine, fields);
         if (r < 0)
-                return log_error_errno(r, "Failed to allocate json variant: %m");
+                return log_error_errno(r, "Failed to filter JSON identity data: %m");
 
-        r = sd_json_variant_set_fieldbo(identity, rlimit_field,
-                                        SD_JSON_BUILD_PAIR_VARIANT("cur", jcur),
-                                        SD_JSON_BUILD_PAIR_VARIANT("max", jmax));
+        r = sd_json_variant_filter(&arg_identity_extra_privileged, fields);
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", rlimit_field);
+                return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
         return 0;
 }
 
-static int parse_disk_size_field(sd_json_variant **identity, const char *arg) {
+static int parse_ssh_authorized_keys(sd_json_variant **identity, const char *field, const char *arg) {
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        _cleanup_strv_free_ char **l = NULL, **add = NULL;
         int r;
 
         assert(identity);
+        assert(field);
 
-        if (isempty(arg)) {
-                r = drop_from_identity("diskSize", "diskSizeRelative", "rebalanceWeight");
-                if (r < 0)
-                        return r;
+        if (isempty(arg))
+                return drop_from_identity(field);
 
-                arg_disk_size = arg_disk_size_relative = UINT64_MAX;
-                return 0;
-        }
+        if (arg[0] == '@') {
+                /* If prefixed with '@', read from a file */
 
-        r = parse_permyriad(arg);
-        if (r < 0) {
-                r = parse_disk_size(arg, &arg_disk_size);
-                if (r < 0)
-                        return r;
+                _cleanup_fclose_ FILE *f = fopen(arg + 1, "re");
+                if (!f)
+                        return log_error_errno(errno, "Failed to open '%s': %m", arg + 1);
 
-                r = drop_from_identity("diskSizeRelative");
-                if (r < 0)
-                        return r;
+                for (;;) {
+                        _cleanup_free_ char *line = NULL;
 
-                r = sd_json_variant_set_field_unsigned(identity, "diskSize", arg_disk_size);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to set %s field: %m", "diskSize");
+                        r = read_line(f, LONG_LINE_MAX, &line);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to read from '%s': %m", arg + 1);
+                        if (r == 0)
+                                break;
 
-                arg_disk_size_relative = UINT64_MAX;
+                        if (isempty(line) || line[0] == '#')
+                                continue;
+
+                        r = strv_consume(&add, TAKE_PTR(line));
+                        if (r < 0)
+                                return log_oom();
+                }
         } else {
-                /* Normalize to UINT32_MAX == 100% */
-                arg_disk_size_relative = UINT32_SCALE_FROM_PERMYRIAD(r);
+                /* Otherwise, assume it's a literal key. Let's do some superficial checks
+                 * before accepting it though. */
 
-                r = drop_from_identity("diskSize");
-                if (r < 0)
-                        return r;
+                if (string_has_cc(arg, NULL))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Authorized key contains control characters, refusing.");
+                if (arg[0] == '#')
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?");
 
-                r = sd_json_variant_set_field_unsigned(identity, "diskSizeRelative", arg_disk_size_relative);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to set %s field: %m", "diskSizeRelative");
+                add = strv_new(arg);
+                if (!add)
+                        return log_oom();
+        }
 
-                arg_disk_size = UINT64_MAX;
+        v = sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
+        if (v) {
+                r = sd_json_variant_strv(v, &l);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse %s list: %m", field);
         }
 
-        /* Automatically turn off the rebalance logic if user configured a size explicitly */
-        r = sd_json_variant_set_field_unsigned(identity, "rebalanceWeight", REBALANCE_WEIGHT_OFF);
+        r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ true);
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", "rebalanceWeight");
-        return 0;
-}
+                return log_oom();
 
-static int parse_sector_size_field(sd_json_variant **identity, const char *field, const char *arg) {
-        uint64_t ss;
-        int r;
+        v = sd_json_variant_unref(v);
 
-        assert(identity);
-        assert(field);
-
-        if (isempty(arg))
-                return drop_from_identity(field);
-
-        r = parse_sector_size(arg, &ss);
+        r = sd_json_variant_new_array_strv(&v, l);
         if (r < 0)
-                return r;
+                return log_oom();
 
-        r = sd_json_variant_set_field_unsigned(identity, field, ss);
+        r = sd_json_variant_set_field(identity, field, v);
         if (r < 0)
                 return log_error_errno(r, "Failed to set %s field: %m", field);
+
         return 0;
 }
 
-static int parse_weight_field(sd_json_variant **identity, const char *field, const char *arg) {
+static int parse_string_field(sd_json_variant **identity, const char *field, const char *arg) {
         int r;
 
         assert(identity);
@@ -3376,96 +3188,76 @@ static int parse_weight_field(sd_json_variant **identity, const char *field, con
         if (isempty(arg))
                 return drop_from_identity(field);
 
-        uint64_t u;
-        r = safe_atou64(arg, &u);
-        if (r < 0)
-                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
-
-        if (!CGROUP_WEIGHT_IS_OK(u))
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "Weight %" PRIu64 " is out of valid range for field %s.", u, field);
-
-        r = sd_json_variant_set_field_unsigned(identity, field, u);
+        r = sd_json_variant_set_field_string(identity, field, arg);
         if (r < 0)
                 return log_error_errno(r, "Failed to set %s field: %m", field);
         return 0;
 }
 
-static int parse_environment_field(sd_json_variant **identity, const char *field, const char *arg) {
-        _cleanup_strv_free_ char **l = NULL;
-        _cleanup_(sd_json_variant_unrefp) sd_json_variant *ne = NULL;
+static int parse_home_directory_field(sd_json_variant **identity, const char *field, const char *arg) {
+        _cleanup_free_ char *hd = NULL;
         int r;
 
         assert(identity);
         assert(field);
 
-        if (isempty(arg))
-                return drop_from_identity(field);
-
-        sd_json_variant *e = sd_json_variant_by_key(*identity, field);
-        if (e) {
-                r = sd_json_variant_strv(e, &l);
+        if (!isempty(arg)) {
+                r = parse_path_argument(arg, /* suppress_root= */ false, &hd);
                 if (r < 0)
-                        return log_error_errno(r, "Failed to parse JSON environment field: %m");
+                        return r;
+
+                if (!valid_home(hd))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd);
         }
 
-        r = strv_env_replace_strdup_passthrough(&l, arg);
-        if (r < 0)
-                return log_error_errno(r, "Cannot assign environment variable %s: %m", arg);
+        return parse_string_field(identity, field, hd);
+}
 
-        strv_sort(l);
+static int parse_realm_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
 
-        r = sd_json_variant_new_array_strv(&ne, l);
-        if (r < 0)
-                return log_error_errno(r, "Failed to allocate json list: %m");
+        assert(identity);
+        assert(field);
 
-        r = sd_json_variant_set_field(identity, field, ne);
-        if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field);
-        return 0;
+        if (!isempty(arg)) {
+                r = dns_name_is_valid(arg);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", arg);
+                if (r == 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain.", arg);
+        }
+
+        return parse_string_field(identity, field, arg);
 }
 
-static int parse_language_field(char ***languages, const char *arg) {
+static int parse_path_field(sd_json_variant **identity, const char *field, const char *arg) {
+        _cleanup_free_ char *v = NULL;
         int r;
 
-        assert(languages);
+        assert(identity);
+        assert(field);
 
-        if (isempty(arg)) {
-                r = drop_from_identity("preferredLanguage", "additionalLanguages");
+        if (!isempty(arg)) {
+                r = parse_path_argument(arg, /* suppress_root= */ false, &v);
                 if (r < 0)
                         return r;
-
-                strv_freep(languages);
-                return 0;
         }
 
-        for (const char *p = arg;;) {
-                _cleanup_free_ char *word = NULL;
-
-                r = extract_first_word(&p, &word, ",:", /* flags= */ 0);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse locale list: %m");
-                if (r == 0)
-                        return 0;
-
-                if (!locale_is_valid(word))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word);
+        return parse_string_field(identity, field, v);
+}
 
-                if (locale_is_installed(word) <= 0)
-                        log_warning("Locale '%s' is not installed, accepting anyway.", word);
+static int parse_filename_field(sd_json_variant **identity, const char *field, const char *arg) {
+        assert(identity);
+        assert(field);
 
-                r = strv_consume(languages, TAKE_PTR(word));
-                if (r < 0)
-                        return log_oom();
+        if (!isempty(arg) && !filename_is_valid(arg))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Parameter for %s field not a valid filename: %s", field, arg);
 
-                strv_uniq(*languages);
-        }
+        return parse_string_field(identity, field, arg);
 }
 
-static int parse_group_field(
-                sd_json_variant **identity,
-                const char *field,
-                const char *arg) {
+static int parse_unsigned_field(sd_json_variant **identity, const char *field, const char *arg) {
         int r;
 
         assert(identity);
@@ -3474,1848 +3266,2054 @@ static int parse_group_field(
         if (isempty(arg))
                 return drop_from_identity(field);
 
-        for (const char *p = arg;;) {
-                _cleanup_free_ char *word = NULL;
-                _cleanup_strv_free_ char **list = NULL;
-
-                r = extract_first_word(&p, &word, ",", /* flags= */ 0);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse group list: %m");
-                if (r == 0)
-                        return 0;
-
-                if (!valid_user_group_name(word, /* flags= */ 0))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word);
+        unsigned n;
+        r = safe_atou(arg, &n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
 
-                _cleanup_(sd_json_variant_unrefp) sd_json_variant *mo =
-                        sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
+        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
 
-                r = sd_json_variant_strv(mo, &list);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse group list: %m");
+static int parse_u64_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
 
-                r = strv_extend(&list, word);
-                if (r < 0)
-                        return log_oom();
+        assert(identity);
+        assert(field);
 
-                strv_sort_uniq(list);
+        if (isempty(arg))
+                return drop_from_identity(field);
 
-                mo = sd_json_variant_unref(mo);
-                r = sd_json_variant_new_array_strv(&mo, list);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to allocate json list: %m");
+        uint64_t n;
+        r = safe_atou64(arg, &n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
 
-                r = sd_json_variant_set_field(identity, field, mo);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to set %s field: %m", field);
-        }
+        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
 }
 
-static int parse_capability_set_field(
-                sd_json_variant **identity,
-                uint64_t *capability_set,
-                const char *field,
-                const char *arg) {
-
-        _cleanup_strv_free_ char **l = NULL;
+static int parse_size_field(sd_json_variant **identity, const char *field, const char *arg) {
         int r;
 
         assert(identity);
-        assert(capability_set);
         assert(field);
-        assert(arg);
-
-        r = parse_capability_set(arg, CAP_MASK_UNSET, capability_set);
-        if (r == 0)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", arg);
-        if (r < 0)
-                return log_error_errno(r, "Failed to parse capability string '%s': %m", arg);
 
-        if (*capability_set == CAP_MASK_UNSET)
+        if (isempty(arg))
                 return drop_from_identity(field);
 
-        if (capability_set_to_strv(*capability_set, &l) < 0)
-                return log_oom();
+        uint64_t n;
+        r = parse_size(arg, 1024, &n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
 
-        r = sd_json_variant_set_field_strv(identity, field, l);
+        r = sd_json_variant_set_field_unsigned(identity, field, n);
         if (r < 0)
                 return log_error_errno(r, "Failed to set %s field: %m", field);
         return 0;
 }
 
-static int parse_tmpfs_limit_field(
-                sd_json_variant **identity,
-                const char *field,
-                const char *field_scale,
-                const char *arg) {
+static int parse_boolean_field(sd_json_variant **identity, const char *field, const char *arg) {
         int r;
 
         assert(identity);
         assert(field);
-        assert(field_scale);
 
         if (isempty(arg))
-                return drop_from_identity(field, field_scale);
+                return drop_from_identity(field);
 
-        r = parse_permyriad(arg);
-        if (r < 0) {
-                uint64_t u;
+        r = parse_boolean(arg);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse boolean parameter %s: %s", field, arg);
 
-                r = parse_size(arg, 1024, &u);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse %s/%s parameter: %s", field, field_scale, arg);
+        r = sd_json_variant_set_field_boolean(identity, field, r > 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
 
-                r = sd_json_variant_set_field_unsigned(identity, field, u);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to set %s field: %m", field);
+static int parse_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
 
-                return drop_from_identity(field_scale);
-        }
+        assert(identity);
+        assert(field);
 
-        r = sd_json_variant_set_field_unsigned(identity, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r));
+        if (isempty(arg))
+                return drop_from_identity(field);
+
+        mode_t mode;
+        r = parse_mode(arg, &mode);
         if (r < 0)
-                return log_error_errno(r, "Failed to set %s field: %m", field_scale);
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", arg);
 
-        return drop_from_identity(field);
+        r = sd_json_variant_set_field_unsigned(identity, field, mode);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
 }
 
-static int parse_pkcs11_token_uri_field(const char *arg) {
+static int parse_timestamp_field(sd_json_variant **identity, const char *field, const char *arg) {
         int r;
 
-        assert(arg);
+        assert(identity);
+        assert(field);
 
-        if (streq(arg, "list"))
-                return pkcs11_list_tokens();
+        if (isempty(arg))
+                return drop_from_identity(field);
 
-        /* If --pkcs11-token-uri= is specified we always drop everything old */
-        r = drop_from_identity("pkcs11TokenUri", "pkcs11EncryptedKey");
+        usec_t n;
+        r = parse_timestamp(arg, &n);
         if (r < 0)
-                return r;
+                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
 
-        if (isempty(arg)) {
-                arg_pkcs11_token_uri = strv_free(arg_pkcs11_token_uri);
-                return 1;
-        }
+        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
 
-        if (streq(arg, "auto")) {
-                char *found;
-                r = pkcs11_find_token_auto(&found);
-                if (r < 0)
-                        return r;
+static int parse_time_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
 
-                r = strv_consume(&arg_pkcs11_token_uri, found);
-        } else {
-                if (!pkcs11_uri_valid(arg))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", arg);
+        assert(identity);
+        assert(field);
 
-                r = strv_extend(&arg_pkcs11_token_uri, arg);
-        }
+        if (isempty(arg))
+                return drop_from_identity(field);
+
+        usec_t n;
+        r = parse_sec(arg, &n);
         if (r < 0)
-                return r;
+                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
 
-        strv_uniq(arg_pkcs11_token_uri);
-        return 1;
+        r = sd_json_variant_set_field_unsigned(identity, field, n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
 }
 
-static int parse_fido2_device_field(const char *arg) {
+static int parse_uid_field(sd_json_variant **identity, const char *field, const char *arg) {
+        uid_t uid;
         int r;
 
-        assert(arg);
+        assert(identity);
+        assert(field);
 
-        if (streq(arg, "list"))
-                return fido2_list_devices();
+        if (isempty(arg))
+                return drop_from_identity(field);
 
-        r = drop_from_identity("fido2HmacCredential", "fido2HmacSalt");
+        r = parse_uid(arg, &uid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse UID '%s'.", arg);
+
+        const char *bad_range =
+                uid_is_system(uid) ? "in system range" :
+                uid_is_greeter(uid) ? "in greeter range" :
+                uid_is_dynamic(uid) ? "in dynamic ragne" :
+                uid == UID_NOBODY ? "nobody UID" : NULL;
+        if (bad_range)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID "UID_FMT" is %s, refusing.", uid, bad_range);
+
+        r = sd_json_variant_set_field_unsigned(identity, field, uid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
+
+static int parse_nice_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int nc, r;
+
+        assert(identity);
+        assert(field);
+
+        if (isempty(arg))
+                return drop_from_identity(field);
+
+        r = parse_nice(arg, &nc);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse nice level '%s': %m", arg);
+
+        r = sd_json_variant_set_field_integer(identity, field, nc);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
+
+static int parse_auto_resize_mode_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
+
+        assert(identity);
+        assert(field);
+
+        if (!isempty(arg)) {
+                r = auto_resize_mode_from_string(arg);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+                arg = auto_resize_mode_to_string(r);
+        }
+
+        return parse_string_field(identity, field, arg);
+}
+
+static int parse_rebalance_weight(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
+
+        assert(identity);
+        assert(field);
+
+        if (isempty(arg))
+                return drop_from_identity(field);
+
+        uint64_t u;
+        if (streq(arg, "off"))
+                u = REBALANCE_WEIGHT_OFF;
+        else {
+                r = safe_atou64(arg, &u);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse rebalance weight parameter: %s", arg);
+
+                if (u < REBALANCE_WEIGHT_MIN || u > REBALANCE_WEIGHT_MAX)
+                        return log_error_errno(SYNTHETIC_ERRNO(ERANGE),
+                                               "Rebalancing weight out of valid range %" PRIu64 "%s%" PRIu64 ": %s",
+                                               REBALANCE_WEIGHT_MIN, glyph(GLYPH_ELLIPSIS), REBALANCE_WEIGHT_MAX,
+                                               arg);
+        }
+
+        /* Drop from per machine stuff and everywhere */
+        r = drop_from_identity(field);
         if (r < 0)
                 return r;
 
+        r = sd_json_variant_set_field_unsigned(identity, field, u);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
+
+static int parse_rlimit_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
+
+        assert(identity);
+        assert(field);
+
         if (isempty(arg)) {
-                arg_fido2_device = strv_free(arg_fido2_device);
-                return 1;
+                /* Remove all resource limits */
+
+                r = drop_from_identity(field);
+                if (r < 0)
+                        return r;
+
+                arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits);
+                *identity = sd_json_variant_unref(*identity);
+                return 0;
         }
 
-        if (streq(arg, "auto")) {
-                char *found;
-                r = fido2_find_device_auto(&found);
+        const char *eq = strchr(arg, '=');
+        if (!eq)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", arg);
+
+        _cleanup_free_ char *s = strndup(arg, eq - arg);
+        if (!s)
+                return log_oom();
+
+        int limit = rlimit_from_string_harder(s);
+        if (limit < 0)
+                return log_error_errno(limit, "Unknown resource limit type: %s", s);
+
+        const char *rlimit_field = strjoina("RLIMIT_", rlimit_to_string(limit));
+
+        if (isempty(eq + 1)) {
+                /* Remove only the specific rlimit */
+
+                r = strv_extend(&arg_identity_filter_rlimits, rlimit_field);
                 if (r < 0)
                         return r;
 
-                r = strv_consume(&arg_fido2_device, found);
-        } else
-                r = strv_extend(&arg_fido2_device, arg);
+                r = sd_json_variant_filter(identity, STRV_MAKE(rlimit_field));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to filter JSON identity data: %m");
+                return 0;
+        }
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *jcur = NULL, *jmax = NULL;
+        struct rlimit rl;
+
+        r = rlimit_parse(limit, eq + 1, &rl);
         if (r < 0)
-                return r;
+                return log_error_errno(r, "Failed to parse resource limit value: %s", eq + 1);
 
-        strv_uniq(arg_fido2_device);
-        return 1;
+        r = rl.rlim_cur == RLIM_INFINITY ? sd_json_variant_new_null(&jcur) : sd_json_variant_new_unsigned(&jcur, rl.rlim_cur);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate json variant: %m");
+
+        r = rl.rlim_max == RLIM_INFINITY ? sd_json_variant_new_null(&jmax) : sd_json_variant_new_unsigned(&jmax, rl.rlim_max);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate json variant: %m");
+
+        r = sd_json_variant_set_fieldbo(identity, rlimit_field,
+                                        SD_JSON_BUILD_PAIR_VARIANT("cur", jcur),
+                                        SD_JSON_BUILD_PAIR_VARIANT("max", jmax));
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", rlimit_field);
+        return 0;
 }
 
-static int help(void) {
-        _cleanup_free_ char *link = NULL;
+static int parse_disk_size_field(sd_json_variant **identity, const char *arg) {
         int r;
 
-        pager_open(arg_pager_flags);
+        assert(identity);
 
-        r = terminal_urlify_man("homectl", "1", &link);
+        if (isempty(arg)) {
+                r = drop_from_identity("diskSize", "diskSizeRelative", "rebalanceWeight");
+                if (r < 0)
+                        return r;
+
+                arg_disk_size = arg_disk_size_relative = UINT64_MAX;
+                return 0;
+        }
+
+        r = parse_permyriad(arg);
+        if (r < 0) {
+                r = parse_disk_size(arg, &arg_disk_size);
+                if (r < 0)
+                        return r;
+
+                r = drop_from_identity("diskSizeRelative");
+                if (r < 0)
+                        return r;
+
+                r = sd_json_variant_set_field_unsigned(identity, "diskSize", arg_disk_size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set %s field: %m", "diskSize");
+
+                arg_disk_size_relative = UINT64_MAX;
+        } else {
+                /* Normalize to UINT32_MAX == 100% */
+                arg_disk_size_relative = UINT32_SCALE_FROM_PERMYRIAD(r);
+
+                r = drop_from_identity("diskSize");
+                if (r < 0)
+                        return r;
+
+                r = sd_json_variant_set_field_unsigned(identity, "diskSizeRelative", arg_disk_size_relative);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set %s field: %m", "diskSizeRelative");
+
+                arg_disk_size = UINT64_MAX;
+        }
+
+        /* Automatically turn off the rebalance logic if user configured a size explicitly */
+        r = sd_json_variant_set_field_unsigned(identity, "rebalanceWeight", REBALANCE_WEIGHT_OFF);
         if (r < 0)
-                return log_oom();
+                return log_error_errno(r, "Failed to set %s field: %m", "rebalanceWeight");
+        return 0;
+}
 
-        printf("%1$s [OPTIONS...] COMMAND ...\n\n"
-               "%2$sCreate, manipulate or inspect home directories.%3$s\n"
-               "\n%4$sBasic User Manipulation Commands:%5$s\n"
-               "  list                         List home areas\n"
-               "  inspect USER…                Inspect a home area\n"
-               "  create USER                  Create 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"
-               "  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"
-               "  with USER [COMMAND…]         Run shell or command with access to a home area\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"
-               "     --no-pager                Do not pipe output into a pager\n"
-               "     --no-legend               Do not show the headers and footers\n"
-               "     --no-ask-password         Do not ask for system passwords\n"
-               "     --offline                 Don't update record embedded in home directory\n"
-               "  -H --host=[USER@]HOST        Operate on remote host\n"
-               "  -M --machine=CONTAINER       Operate on local container\n"
-               "     --identity=PATH           Read JSON identity from file\n"
-               "     --json=FORMAT             Output inspection data in JSON (takes one of\n"
-               "                               pretty, short, off)\n"
-               "  -j                           Equivalent to --json=pretty (on TTY) or\n"
-               "                               --json=short (otherwise)\n"
-               "     --export-format=          Strip JSON inspection data (full, stripped,\n"
-               "                               minimal)\n"
-               "  -E                           When specified once equals -j --export-format=\n"
-               "                               stripped, when specified twice equals\n"
-               "                               -j --export-format=minimal\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"
-               "     --prompt-new-user         firstboot: Query user interactively for user\n"
-               "                               to create\n"
-               "     --prompt-groups=no        In first-boot mode, don't prompt for auxiliary\n"
-               "                               group memberships\n"
-               "     --prompt-shell=no         In first-boot mode, don't prompt for shells\n"
-               "     --chrome=no               In first-boot mode, don't show colour bar at top\n"
-               "                               and bottom of terminal\n"
-               "     --mute-console=yes        In first-boot mode, tell kernel/PID 1 to not\n"
-               "                               write to the console while running\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"
-               "     --alias=ALIAS             Define alias usernames for this account\n"
-               "     --email-address=EMAIL     Email address for user\n"
-               "     --location=LOCATION       Set location of user on earth\n"
-               "     --birth-date=[DATE]       Set user birth date (YYYY-MM-DD)\n"
-               "     --icon-name=NAME          Icon name for user\n"
-               "  -d --home-dir=PATH           Home directory\n"
-               "  -u --uid=UID                 Numeric UID for user\n"
-               "  -G --member-of=GROUP         Add user to group\n"
-               "     --capability-bounding-set=CAPS\n"
-               "                               Bounding POSIX capability set\n"
-               "     --capability-ambient-set=CAPS\n"
-               "                               Ambient POSIX capability set\n"
-               "     --access-mode=MODE        User home directory access mode\n"
-               "     --umask=MODE              Umask for user when logging in\n"
-               "     --skel=PATH               Skeleton directory to use\n"
-               "     --shell=PATH              Shell for account\n"
-               "     --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n"
-               "     --timezone=TIMEZONE       Set a time-zone\n"
-               "     --language=LOCALE         Set preferred languages\n"
-               "     --default-area=AREA       Select default area\n"
-               "\n%4$sAuthentication User Record Properties:%5$s\n"
-               "     --ssh-authorized-keys=KEYS\n"
-               "                               Specify SSH public keys\n"
-               "     --pkcs11-token-uri=URI    URI to PKCS#11 security token containing\n"
-               "                               private key and matching X.509 certificate\n"
-               "     --fido2-device=PATH       Path to FIDO2 hidraw device with hmac-secret\n"
-               "                               extension\n"
-               "     --fido2-with-client-pin=BOOL\n"
-               "                               Whether to require entering a PIN to unlock the\n"
-               "                               account\n"
-               "     --fido2-with-user-presence=BOOL\n"
-               "                               Whether to require user presence to unlock the\n"
-               "                               account\n"
-               "     --fido2-with-user-verification=BOOL\n"
-               "                               Whether to require user verification to unlock\n"
-               "                               the account\n"
-               "     --recovery-key=BOOL       Add a recovery key\n"
-               "\n%4$sBlob Directory User Record Properties:%5$s\n"
-               "  -b --blob=[FILENAME=]PATH\n"
-               "                               Path to a replacement blob directory, or replace\n"
-               "                               an individual files in the blob directory.\n"
-               "     --avatar=PATH             Path to user avatar picture\n"
-               "     --login-background=PATH   Path to user login background picture\n"
-               "\n%4$sAccount Management User Record Properties:%5$s\n"
-               "     --locked=BOOL             Set locked account state\n"
-               "     --not-before=TIMESTAMP    Do not allow logins before\n"
-               "     --not-after=TIMESTAMP     Do not allow logins after\n"
-               "     --rate-limit-interval=SECS\n"
-               "                               Login rate-limit interval in seconds\n"
-               "     --rate-limit-burst=NUMBER\n"
-               "                               Login rate-limit attempts per interval\n"
-               "\n%4$sPassword Policy User Record Properties:%5$s\n"
-               "     --password-hint=HINT      Set Password hint\n"
-               "     --enforce-password-policy=BOOL\n"
-               "                               Control whether to enforce system's password\n"
-               "                               policy for this user\n"
-               "  -P                           Same as --enforce-password-policy=no\n"
-               "     --password-change-now=BOOL\n"
-               "                               Require the password to be changed on next login\n"
-               "     --password-change-min=TIME\n"
-               "                               Require minimum time between password changes\n"
-               "     --password-change-max=TIME\n"
-               "                               Require maximum time between password changes\n"
-               "     --password-change-warn=TIME\n"
-               "                               How much time to warn before password expiry\n"
-               "     --password-change-inactive=TIME\n"
-               "                               How much time to block password after expiry\n"
-               "\n%4$sResource Management User Record Properties:%5$s\n"
-               "     --disk-size=BYTES         Size to assign the user on disk\n"
-               "     --nice=NICE               Nice level for user\n"
-               "     --rlimit=LIMIT=VALUE[:VALUE]\n"
-               "                               Set resource limits\n"
-               "     --tasks-max=MAX           Set maximum number of per-user tasks\n"
-               "     --memory-high=BYTES       Set high memory threshold in bytes\n"
-               "     --memory-max=BYTES        Set maximum memory limit\n"
-               "     --cpu-weight=WEIGHT       Set CPU weight\n"
-               "     --io-weight=WEIGHT        Set IO weight\n"
-               "     --tmp-limit=BYTES|PERCENT Set limit on /tmp/\n"
-               "     --dev-shm-limit=BYTES|PERCENT\n"
-               "                               Set limit on /dev/shm/\n"
-               "\n%4$sStorage User Record Properties:%5$s\n"
-               "     --storage=STORAGE         Storage type to use (luks, fscrypt, directory,\n"
-               "                               subvolume, cifs)\n"
-               "     --image-path=PATH         Path to image file/directory\n"
-               "     --drop-caches=BOOL        Whether to automatically drop caches on logout\n"
-               "\n%4$sLUKS Storage User Record Properties:%5$s\n"
-               "     --fs-type=TYPE            File system type to use in case of luks\n"
-               "                               storage (btrfs, ext4, xfs)\n"
-               "     --luks-discard=BOOL       Whether to use 'discard' feature of file system\n"
-               "                               when activated (mounted)\n"
-               "     --luks-offline-discard=BOOL\n"
-               "                               Whether to trim file on logout\n"
-               "     --luks-cipher=CIPHER      Cipher to use for LUKS encryption\n"
-               "     --luks-cipher-mode=MODE   Cipher mode to use for LUKS encryption\n"
-               "     --luks-volume-key-size=BITS\n"
-               "                               Volume key size to use for LUKS encryption\n"
-               "     --luks-pbkdf-type=TYPE    Password-based Key Derivation Function to use\n"
-               "     --luks-pbkdf-hash-algorithm=ALGORITHM\n"
-               "                               PBKDF hash algorithm to use\n"
-               "     --luks-pbkdf-time-cost=SECS\n"
-               "                               Time cost for PBKDF in seconds\n"
-               "     --luks-pbkdf-memory-cost=BYTES\n"
-               "                               Memory cost for PBKDF in bytes\n"
-               "     --luks-pbkdf-parallel-threads=NUMBER\n"
-               "                               Number of parallel threads for PKBDF\n"
-               "     --luks-sector-size=BYTES\n"
-               "                               Sector size for LUKS encryption in bytes\n"
-               "     --luks-extra-mount-options=OPTIONS\n"
-               "                               LUKS extra mount options\n"
-               "     --auto-resize-mode=MODE   Automatically grow/shrink home on login/logout\n"
-               "     --rebalance-weight=WEIGHT Weight while rebalancing\n"
-               "\n%4$sMounting User Record Properties:%5$s\n"
-               "     --nosuid=BOOL             Control the 'nosuid' flag of the home mount\n"
-               "     --nodev=BOOL              Control the 'nodev' flag of the home mount\n"
-               "     --noexec=BOOL             Control the 'noexec' flag of the home mount\n"
-               "\n%4$sCIFS User Record Properties:%5$s\n"
-               "     --cifs-domain=DOMAIN      CIFS (Windows) domain\n"
-               "     --cifs-user-name=USER     CIFS (Windows) user name\n"
-               "     --cifs-service=SERVICE    CIFS (Windows) service to mount as home area\n"
-               "     --cifs-extra-mount-options=OPTIONS\n"
-               "                               CIFS (Windows) extra mount options\n"
-               "\n%4$sLogin Behaviour User Record Properties:%5$s\n"
-               "     --stop-delay=SECS         How long to leave user services running after\n"
-               "                               logout\n"
-               "     --kill-processes=BOOL     Whether to kill user processes when sessions\n"
-               "                               terminate\n"
-               "     --auto-login=BOOL         Try to log this user in automatically\n"
-               "     --session-launcher=LAUNCHER\n"
-               "                               Preferred session launcher file\n"
-               "     --session-type=TYPE       Preferred session type\n"
-               "\nSee the %6$s for details.\n",
-               program_invocation_short_name,
-               ansi_highlight(),
-               ansi_normal(),
-               ansi_underline(),
-               ansi_normal(),
-               link);
+static int parse_sector_size_field(sd_json_variant **identity, const char *field, const char *arg) {
+        uint64_t ss;
+        int r;
+
+        assert(identity);
+        assert(field);
+
+        if (isempty(arg))
+                return drop_from_identity(field);
+
+        r = parse_sector_size(arg, &ss);
+        if (r < 0)
+                return r;
 
+        r = sd_json_variant_set_field_unsigned(identity, field, ss);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
         return 0;
 }
 
-static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        return help();
+static int parse_weight_field(sd_json_variant **identity, const char *field, const char *arg) {
+        int r;
+
+        assert(identity);
+        assert(field);
+
+        if (isempty(arg))
+                return drop_from_identity(field);
+
+        uint64_t u;
+        r = safe_atou64(arg, &u);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse %s parameter: %s", field, arg);
+
+        if (!CGROUP_WEIGHT_IS_OK(u))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Weight %" PRIu64 " is out of valid range for field %s.", u, field);
+
+        r = sd_json_variant_set_field_unsigned(identity, field, u);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
 }
 
-static int parse_argv(int argc, char *argv[]) {
-        _cleanup_strv_free_ char **arg_languages = NULL;
+static int parse_environment_field(sd_json_variant **identity, const char *field, const char *arg) {
+        _cleanup_strv_free_ char **l = NULL;
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *ne = NULL;
+        int r;
 
-        enum {
-                ARG_VERSION = 0x100,
-                ARG_NO_PAGER,
-                ARG_NO_LEGEND,
-                ARG_NO_ASK_PASSWORD,
-                ARG_OFFLINE,
-                ARG_REALM,
-                ARG_ALIAS,
-                ARG_EMAIL_ADDRESS,
-                ARG_DISK_SIZE,
-                ARG_ACCESS_MODE,
-                ARG_STORAGE,
-                ARG_FS_TYPE,
-                ARG_IMAGE_PATH,
-                ARG_UMASK,
-                ARG_LUKS_DISCARD,
-                ARG_LUKS_OFFLINE_DISCARD,
-                ARG_JSON,
-                ARG_SETENV,
-                ARG_TIMEZONE,
-                ARG_LANGUAGE,
-                ARG_LOCKED,
-                ARG_SSH_AUTHORIZED_KEYS,
-                ARG_LOCATION,
-                ARG_BIRTH_DATE,
-                ARG_ICON_NAME,
-                ARG_PASSWORD_HINT,
-                ARG_NICE,
-                ARG_RLIMIT,
-                ARG_NOT_BEFORE,
-                ARG_NOT_AFTER,
-                ARG_LUKS_CIPHER,
-                ARG_LUKS_CIPHER_MODE,
-                ARG_LUKS_VOLUME_KEY_SIZE,
-                ARG_NOSUID,
-                ARG_NODEV,
-                ARG_NOEXEC,
-                ARG_CIFS_DOMAIN,
-                ARG_CIFS_USER_NAME,
-                ARG_CIFS_SERVICE,
-                ARG_CIFS_EXTRA_MOUNT_OPTIONS,
-                ARG_TASKS_MAX,
-                ARG_MEMORY_HIGH,
-                ARG_MEMORY_MAX,
-                ARG_CPU_WEIGHT,
-                ARG_IO_WEIGHT,
-                ARG_LUKS_PBKDF_TYPE,
-                ARG_LUKS_PBKDF_HASH_ALGORITHM,
-                ARG_LUKS_PBKDF_FORCE_ITERATIONS,
-                ARG_LUKS_PBKDF_TIME_COST,
-                ARG_LUKS_PBKDF_MEMORY_COST,
-                ARG_LUKS_PBKDF_PARALLEL_THREADS,
-                ARG_LUKS_SECTOR_SIZE,
-                ARG_RATE_LIMIT_INTERVAL,
-                ARG_RATE_LIMIT_BURST,
-                ARG_STOP_DELAY,
-                ARG_KILL_PROCESSES,
-                ARG_ENFORCE_PASSWORD_POLICY,
-                ARG_PASSWORD_CHANGE_NOW,
-                ARG_PASSWORD_CHANGE_MIN,
-                ARG_PASSWORD_CHANGE_MAX,
-                ARG_PASSWORD_CHANGE_WARN,
-                ARG_PASSWORD_CHANGE_INACTIVE,
-                ARG_EXPORT_FORMAT,
-                ARG_AUTO_LOGIN,
-                ARG_SESSION_LAUNCHER,
-                ARG_SESSION_TYPE,
-                ARG_PKCS11_TOKEN_URI,
-                ARG_FIDO2_DEVICE,
-                ARG_FIDO2_WITH_PIN,
-                ARG_FIDO2_WITH_UP,
-                ARG_FIDO2_WITH_UV,
-                ARG_RECOVERY_KEY,
-                ARG_DROP_CACHES,
-                ARG_LUKS_EXTRA_MOUNT_OPTIONS,
-                ARG_AUTO_RESIZE_MODE,
-                ARG_REBALANCE_WEIGHT,
-                ARG_FIDO2_CRED_ALG,
-                ARG_CAPABILITY_BOUNDING_SET,
-                ARG_CAPABILITY_AMBIENT_SET,
-                ARG_PROMPT_NEW_USER,
-                ARG_AVATAR,
-                ARG_LOGIN_BACKGROUND,
-                ARG_TMP_LIMIT,
-                ARG_DEV_SHM_LIMIT,
-                ARG_DEFAULT_AREA,
-                ARG_KEY_NAME,
-                ARG_SEIZE,
-                ARG_MATCH,
-                ARG_PROMPT_SHELL,
-                ARG_PROMPT_GROUPS,
-                ARG_CHROME,
-                ARG_MUTE_CONSOLE,
-        };
-
-        static const struct option options[] = {
-                { "help",                         no_argument,       NULL, 'h'                             },
-                { "version",                      no_argument,       NULL, ARG_VERSION                     },
-                { "no-pager",                     no_argument,       NULL, ARG_NO_PAGER                    },
-                { "no-legend",                    no_argument,       NULL, ARG_NO_LEGEND                   },
-                { "no-ask-password",              no_argument,       NULL, ARG_NO_ASK_PASSWORD             },
-                { "offline",                      no_argument,       NULL, ARG_OFFLINE                     },
-                { "host",                         required_argument, NULL, 'H'                             },
-                { "machine",                      required_argument, NULL, 'M'                             },
-                { "identity",                     required_argument, NULL, 'I'                             },
-                { "real-name",                    required_argument, NULL, 'c'                             },
-                { "comment",                      required_argument, NULL, 'c'                             }, /* Compat alias to keep thing in sync with useradd(8) */
-                { "realm",                        required_argument, NULL, ARG_REALM                       },
-                { "alias",                        required_argument, NULL, ARG_ALIAS                       },
-                { "email-address",                required_argument, NULL, ARG_EMAIL_ADDRESS               },
-                { "location",                     required_argument, NULL, ARG_LOCATION                    },
-                { "birth-date",                   required_argument, NULL, ARG_BIRTH_DATE                  },
-                { "password-hint",                required_argument, NULL, ARG_PASSWORD_HINT               },
-                { "icon-name",                    required_argument, NULL, ARG_ICON_NAME                   },
-                { "home-dir",                     required_argument, NULL, 'd'                             }, /* Compatible with useradd(8) */
-                { "uid",                          required_argument, NULL, 'u'                             }, /* Compatible with useradd(8) */
-                { "member-of",                    required_argument, NULL, 'G'                             },
-                { "groups",                       required_argument, NULL, 'G'                             }, /* Compat alias to keep thing in sync with useradd(8) */
-                { "skel",                         required_argument, NULL, 'k'                             }, /* Compatible with useradd(8) */
-                { "shell",                        required_argument, NULL, 's'                             }, /* Compatible with useradd(8) */
-                { "setenv",                       required_argument, NULL, ARG_SETENV                      },
-                { "timezone",                     required_argument, NULL, ARG_TIMEZONE                    },
-                { "language",                     required_argument, NULL, ARG_LANGUAGE                    },
-                { "locked",                       required_argument, NULL, ARG_LOCKED                      },
-                { "not-before",                   required_argument, NULL, ARG_NOT_BEFORE                  },
-                { "not-after",                    required_argument, NULL, ARG_NOT_AFTER                   },
-                { "expiredate",                   required_argument, NULL, 'e'                             }, /* Compat alias to keep thing in sync with useradd(8) */
-                { "ssh-authorized-keys",          required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS         },
-                { "disk-size",                    required_argument, NULL, ARG_DISK_SIZE                   },
-                { "access-mode",                  required_argument, NULL, ARG_ACCESS_MODE                 },
-                { "umask",                        required_argument, NULL, ARG_UMASK                       },
-                { "nice",                         required_argument, NULL, ARG_NICE                        },
-                { "rlimit",                       required_argument, NULL, ARG_RLIMIT                      },
-                { "tasks-max",                    required_argument, NULL, ARG_TASKS_MAX                   },
-                { "memory-high",                  required_argument, NULL, ARG_MEMORY_HIGH                 },
-                { "memory-max",                   required_argument, NULL, ARG_MEMORY_MAX                  },
-                { "cpu-weight",                   required_argument, NULL, ARG_CPU_WEIGHT                  },
-                { "io-weight",                    required_argument, NULL, ARG_IO_WEIGHT                   },
-                { "storage",                      required_argument, NULL, ARG_STORAGE                     },
-                { "image-path",                   required_argument, NULL, ARG_IMAGE_PATH                  },
-                { "fs-type",                      required_argument, NULL, ARG_FS_TYPE                     },
-                { "luks-discard",                 required_argument, NULL, ARG_LUKS_DISCARD                },
-                { "luks-offline-discard",         required_argument, NULL, ARG_LUKS_OFFLINE_DISCARD        },
-                { "luks-cipher",                  required_argument, NULL, ARG_LUKS_CIPHER                 },
-                { "luks-cipher-mode",             required_argument, NULL, ARG_LUKS_CIPHER_MODE            },
-                { "luks-volume-key-size",         required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE        },
-                { "luks-pbkdf-type",              required_argument, NULL, ARG_LUKS_PBKDF_TYPE             },
-                { "luks-pbkdf-hash-algorithm",    required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM   },
-                { "luks-pbkdf-force-iterations",  required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS },
-                { "luks-pbkdf-time-cost",         required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST        },
-                { "luks-pbkdf-memory-cost",       required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST      },
-                { "luks-pbkdf-parallel-threads",  required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS },
-                { "luks-sector-size",             required_argument, NULL, ARG_LUKS_SECTOR_SIZE            },
-                { "nosuid",                       required_argument, NULL, ARG_NOSUID                      },
-                { "nodev",                        required_argument, NULL, ARG_NODEV                       },
-                { "noexec",                       required_argument, NULL, ARG_NOEXEC                      },
-                { "cifs-user-name",               required_argument, NULL, ARG_CIFS_USER_NAME              },
-                { "cifs-domain",                  required_argument, NULL, ARG_CIFS_DOMAIN                 },
-                { "cifs-service",                 required_argument, NULL, ARG_CIFS_SERVICE                },
-                { "cifs-extra-mount-options",     required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS    },
-                { "rate-limit-interval",          required_argument, NULL, ARG_RATE_LIMIT_INTERVAL         },
-                { "rate-limit-burst",             required_argument, NULL, ARG_RATE_LIMIT_BURST            },
-                { "stop-delay",                   required_argument, NULL, ARG_STOP_DELAY                  },
-                { "kill-processes",               required_argument, NULL, ARG_KILL_PROCESSES              },
-                { "enforce-password-policy",      required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY     },
-                { "password-change-now",          required_argument, NULL, ARG_PASSWORD_CHANGE_NOW         },
-                { "password-change-min",          required_argument, NULL, ARG_PASSWORD_CHANGE_MIN         },
-                { "password-change-max",          required_argument, NULL, ARG_PASSWORD_CHANGE_MAX         },
-                { "password-change-warn",         required_argument, NULL, ARG_PASSWORD_CHANGE_WARN        },
-                { "password-change-inactive",     required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE    },
-                { "auto-login",                   required_argument, NULL, ARG_AUTO_LOGIN                  },
-                { "session-launcher",             required_argument, NULL, ARG_SESSION_LAUNCHER,           },
-                { "session-type",                 required_argument, NULL, ARG_SESSION_TYPE,               },
-                { "json",                         required_argument, NULL, ARG_JSON                        },
-                { "export-format",                required_argument, NULL, ARG_EXPORT_FORMAT               },
-                { "pkcs11-token-uri",             required_argument, NULL, ARG_PKCS11_TOKEN_URI            },
-                { "fido2-credential-algorithm",   required_argument, NULL, ARG_FIDO2_CRED_ALG              },
-                { "fido2-device",                 required_argument, NULL, ARG_FIDO2_DEVICE                },
-                { "fido2-with-client-pin",        required_argument, NULL, ARG_FIDO2_WITH_PIN              },
-                { "fido2-with-user-presence",     required_argument, NULL, ARG_FIDO2_WITH_UP               },
-                { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV               },
-                { "recovery-key",                 required_argument, NULL, ARG_RECOVERY_KEY                },
-                { "drop-caches",                  required_argument, NULL, ARG_DROP_CACHES                 },
-                { "luks-extra-mount-options",     required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS    },
-                { "auto-resize-mode",             required_argument, NULL, ARG_AUTO_RESIZE_MODE            },
-                { "rebalance-weight",             required_argument, NULL, ARG_REBALANCE_WEIGHT            },
-                { "capability-bounding-set",      required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET     },
-                { "capability-ambient-set",       required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET      },
-                { "prompt-new-user",              no_argument,       NULL, ARG_PROMPT_NEW_USER             },
-                { "blob",                         required_argument, NULL, 'b'                             },
-                { "avatar",                       required_argument, NULL, ARG_AVATAR                      },
-                { "login-background",             required_argument, NULL, ARG_LOGIN_BACKGROUND            },
-                { "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                    },
-                { "seize",                        required_argument, NULL, ARG_SEIZE                       },
-                { "match",                        required_argument, NULL, ARG_MATCH                       },
-                { "prompt-shell",                 required_argument, NULL, ARG_PROMPT_SHELL                },
-                { "prompt-groups",                required_argument, NULL, ARG_PROMPT_GROUPS               },
-                { "chrome",                       required_argument, NULL, ARG_CHROME                      },
-                { "mute-console",                 required_argument, NULL, ARG_MUTE_CONSOLE                },
-                {}
-        };
+        assert(identity);
+        assert(field);
 
-        int r;
+        if (isempty(arg))
+                return drop_from_identity(field);
 
-        /* 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;
+        sd_json_variant *e = sd_json_variant_by_key(*identity, field);
+        if (e) {
+                r = sd_json_variant_strv(e, &l);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse JSON environment field: %m");
+        }
 
-        assert(argc >= 0);
-        assert(argv);
+        r = strv_env_replace_strdup_passthrough(&l, arg);
+        if (r < 0)
+                return log_error_errno(r, "Cannot assign environment variable %s: %m", arg);
 
-        /* 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");
+        strv_sort(l);
 
-        for (;;) {
-                int c;
+        r = sd_json_variant_new_array_strv(&ne, l);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate json list: %m");
 
-                c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL);
-                if (c < 0)
-                        break;
+        r = sd_json_variant_set_field(identity, field, ne);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
 
-                switch (c) {
+static int parse_language_field(char ***languages, const char *arg) {
+        int r;
 
-                case 'h':
-                        return help();
+        assert(languages);
 
-                case ARG_VERSION:
-                        return version();
+        if (isempty(arg)) {
+                r = drop_from_identity("preferredLanguage", "additionalLanguages");
+                if (r < 0)
+                        return r;
 
-                case ARG_NO_PAGER:
-                        arg_pager_flags |= PAGER_DISABLE;
-                        break;
+                strv_freep(languages);
+                return 0;
+        }
 
-                case ARG_NO_LEGEND:
-                        arg_legend = false;
-                        break;
+        for (const char *p = arg;;) {
+                _cleanup_free_ char *word = NULL;
 
-                case ARG_NO_ASK_PASSWORD:
-                        arg_ask_password = false;
-                        break;
+                r = extract_first_word(&p, &word, ",:", /* flags= */ 0);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse locale list: %m");
+                if (r == 0)
+                        return 0;
 
-                case ARG_OFFLINE:
-                        arg_offline = true;
-                        break;
+                if (!locale_is_valid(word))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word);
 
-                case 'H':
-                        arg_transport = BUS_TRANSPORT_REMOTE;
-                        arg_host = optarg;
-                        break;
+                if (locale_is_installed(word) <= 0)
+                        log_warning("Locale '%s' is not installed, accepting anyway.", word);
 
-                case 'M':
-                        r = parse_machine_argument(optarg, &arg_host, &arg_transport);
-                        if (r < 0)
-                                return r;
-                        break;
+                r = strv_consume(languages, TAKE_PTR(word));
+                if (r < 0)
+                        return log_oom();
 
-                case 'I':
-                        arg_identity = optarg;
-                        break;
+                strv_uniq(*languages);
+        }
+}
 
-                case 'c':
-                        if (!isempty(optarg) && !valid_gecos(optarg))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Invalid GECOS field '%s'.", optarg);
+static int parse_group_field(
+                sd_json_variant **identity,
+                const char *field,
+                const char *arg) {
+        int r;
 
-                        r = parse_string_field(match_identity ?: &arg_identity_extra, "realName", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        assert(identity);
+        assert(field);
 
-                case ARG_ALIAS:
-                        r = parse_group_field(&arg_identity_extra, "aliases", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        if (isempty(arg))
+                return drop_from_identity(field);
 
-                case 'd':
-                        r = parse_home_directory_field(&arg_identity_extra, "homeDirectory", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        for (const char *p = arg;;) {
+                _cleanup_free_ char *word = NULL;
+                _cleanup_strv_free_ char **list = NULL;
 
-                case ARG_REALM:
-                        r = parse_realm_field(&arg_identity_extra, "realm", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+                r = extract_first_word(&p, &word, ",", /* flags= */ 0);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse group list: %m");
+                if (r == 0)
+                        return 0;
 
-                case ARG_EMAIL_ADDRESS:
-                case ARG_LOCATION:
-                case ARG_ICON_NAME:
-                case ARG_CIFS_USER_NAME:
-                case ARG_CIFS_DOMAIN:
-                case ARG_CIFS_EXTRA_MOUNT_OPTIONS:
-                case ARG_LUKS_EXTRA_MOUNT_OPTIONS:
-                case ARG_SESSION_LAUNCHER:
-                case ARG_SESSION_TYPE: {
-                        const char *field =
-                                           c == ARG_EMAIL_ADDRESS ? "emailAddress" :
-                                                c == ARG_LOCATION ? "location" :
-                                               c == ARG_ICON_NAME ? "iconName" :
-                                          c == ARG_CIFS_USER_NAME ? "cifsUserName" :
-                                             c == ARG_CIFS_DOMAIN ? "cifsDomain" :
-                                c == ARG_CIFS_EXTRA_MOUNT_OPTIONS ? "cifsExtraMountOptions" :
-                                c == ARG_LUKS_EXTRA_MOUNT_OPTIONS ? "luksExtraMountOptions" :
-                                        c == ARG_SESSION_LAUNCHER ? "preferredSessionLauncher" :
-                                            c == ARG_SESSION_TYPE ? "preferredSessionType" :
-                                                                    NULL;
-                        assert(field);
+                if (!valid_user_group_name(word, /* flags= */ 0))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word);
 
-                        r = parse_string_field(match_identity ?: &arg_identity_extra, field, optarg);
-                        if (r < 0)
-                                return r;
-                        break;
-                }
+                _cleanup_(sd_json_variant_unrefp) sd_json_variant *mo =
+                        sd_json_variant_ref(sd_json_variant_by_key(*identity, field));
 
-                case ARG_BIRTH_DATE:
-                        if (isempty(optarg)) {
-                                r = drop_from_identity("birthDate");
-                                if (r < 0)
-                                        return r;
-                        } else {
-                                r = parse_birth_date(optarg, /* ret= */ NULL);
-                                if (r < 0)
-                                        return log_error_errno(r, "Invalid birth date (expected YYYY-MM-DD): %s", optarg);
+                r = sd_json_variant_strv(mo, &list);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse group list: %m");
 
-                                r = parse_string_field(&arg_identity_extra, "birthDate", optarg);
-                                if (r < 0)
-                                        return r;
-                        }
-                        break;
+                r = strv_extend(&list, word);
+                if (r < 0)
+                        return log_oom();
 
-                case ARG_CIFS_SERVICE:
-                        if (!isempty(optarg)) {
-                                r = parse_cifs_service(optarg, /* ret_host= */ NULL, /* ret_service= */ NULL, /* ret_path= */ NULL);
-                                if (r < 0)
-                                        return log_error_errno(r, "Failed to validate CIFS service name: %s", optarg);
-                        }
+                strv_sort_uniq(list);
 
-                        r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsService", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+                mo = sd_json_variant_unref(mo);
+                r = sd_json_variant_new_array_strv(&mo, list);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate json list: %m");
 
-                case ARG_PASSWORD_HINT:
-                        r = parse_string_field(&arg_identity_extra_privileged, "passwordHint", optarg);
-                        if (r < 0)
-                                return r;
+                r = sd_json_variant_set_field(identity, field, mo);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set %s field: %m", field);
+        }
+}
 
-                        string_erase(optarg);
-                        break;
+static int parse_capability_set_field(
+                sd_json_variant **identity,
+                uint64_t *capability_set,
+                const char *field,
+                const char *arg) {
 
-                case ARG_NICE:
-                        r = parse_nice_field(match_identity ?: &arg_identity_extra, "niceLevel", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        _cleanup_strv_free_ char **l = NULL;
+        int r;
 
-                case ARG_RLIMIT:
-                        r = parse_rlimit_field(&arg_identity_extra_rlimits, "resourceLimits", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        assert(identity);
+        assert(capability_set);
+        assert(field);
+        assert(arg);
 
-                case 'u':
-                        r = parse_uid_field(&arg_identity_extra, "uid", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        r = parse_capability_set(arg, CAP_MASK_UNSET, capability_set);
+        if (r == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", arg);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse capability string '%s': %m", arg);
 
-                case 'k':
-                case ARG_IMAGE_PATH: {
-                        const char *field = c == 'k' ? "skeletonDirectory" : "imagePath";
+        if (*capability_set == CAP_MASK_UNSET)
+                return drop_from_identity(field);
 
-                        r = parse_path_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg);
-                        if (r < 0)
-                                return r;
-                        break;
-                }
+        if (capability_set_to_strv(*capability_set, &l) < 0)
+                return log_oom();
 
-                case 's':
-                        if (!isempty(optarg) && !valid_shell(optarg))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Shell '%s' not valid.", optarg);
+        r = sd_json_variant_set_field_strv(identity, field, l);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field);
+        return 0;
+}
 
-                        r = parse_string_field(match_identity ?: &arg_identity_extra, "shell", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+static int parse_tmpfs_limit_field(
+                sd_json_variant **identity,
+                const char *field,
+                const char *field_scale,
+                const char *arg) {
+        int r;
 
-                case ARG_SETENV:
-                        r = parse_environment_field(match_identity ?: &arg_identity_extra, "environment", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        assert(identity);
+        assert(field);
+        assert(field_scale);
 
-                case ARG_TIMEZONE:
-                        if (!isempty(optarg) && !timezone_is_valid(optarg, LOG_DEBUG))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Timezone '%s' is not valid.", optarg);
+        if (isempty(arg))
+                return drop_from_identity(field, field_scale);
 
-                        r = parse_string_field(match_identity ?: &arg_identity_extra, "timeZone", optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        r = parse_permyriad(arg);
+        if (r < 0) {
+                uint64_t u;
+
+                r = parse_size(arg, 1024, &u);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse %s/%s parameter: %s", field, field_scale, arg);
+
+                r = sd_json_variant_set_field_unsigned(identity, field, u);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set %s field: %m", field);
+
+                return drop_from_identity(field_scale);
+        }
+
+        r = sd_json_variant_set_field_unsigned(identity, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r));
+        if (r < 0)
+                return log_error_errno(r, "Failed to set %s field: %m", field_scale);
+
+        return drop_from_identity(field);
+}
+
+static int parse_pkcs11_token_uri_field(const char *arg) {
+        int r;
+
+        assert(arg);
+
+        if (streq(arg, "list"))
+                return pkcs11_list_tokens();
+
+        /* If --pkcs11-token-uri= is specified we always drop everything old */
+        r = drop_from_identity("pkcs11TokenUri", "pkcs11EncryptedKey");
+        if (r < 0)
+                return r;
+
+        if (isempty(arg)) {
+                arg_pkcs11_token_uri = strv_free(arg_pkcs11_token_uri);
+                return 1;
+        }
+
+        if (streq(arg, "auto")) {
+                char *found;
+                r = pkcs11_find_token_auto(&found);
+                if (r < 0)
+                        return r;
+
+                r = strv_consume(&arg_pkcs11_token_uri, found);
+        } else {
+                if (!pkcs11_uri_valid(arg))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", arg);
+
+                r = strv_extend(&arg_pkcs11_token_uri, arg);
+        }
+        if (r < 0)
+                return r;
+
+        strv_uniq(arg_pkcs11_token_uri);
+        return 1;
+}
+
+static int parse_fido2_device_field(const char *arg) {
+        int r;
+
+        assert(arg);
+
+        if (streq(arg, "list"))
+                return fido2_list_devices();
+
+        r = drop_from_identity("fido2HmacCredential", "fido2HmacSalt");
+        if (r < 0)
+                return r;
+
+        if (isempty(arg)) {
+                arg_fido2_device = strv_free(arg_fido2_device);
+                return 1;
+        }
+
+        if (streq(arg, "auto")) {
+                char *found;
+                r = fido2_find_device_auto(&found);
+                if (r < 0)
+                        return r;
+
+                r = strv_consume(&arg_fido2_device, found);
+        } else
+                r = strv_extend(&arg_fido2_device, arg);
+        if (r < 0)
+                return r;
+
+        strv_uniq(arg_fido2_device);
+        return 1;
+}
+
+static int help(void) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        pager_open(arg_pager_flags);
+
+        r = terminal_urlify_man("homectl", "1", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%1$s [OPTIONS...] COMMAND ...\n\n"
+               "%2$sCreate, manipulate or inspect home directories.%3$s\n"
+               "\n%4$sBasic User Manipulation Commands:%5$s\n"
+               "  list                         List home areas\n"
+               "  inspect USER…                Inspect a home area\n"
+               "  create USER                  Create 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"
+               "  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"
+               "  with USER [COMMAND…]         Run shell or command with access to a home area\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"
+               "     --no-pager                Do not pipe output into a pager\n"
+               "     --no-legend               Do not show the headers and footers\n"
+               "     --no-ask-password         Do not ask for system passwords\n"
+               "     --offline                 Don't update record embedded in home directory\n"
+               "  -H --host=[USER@]HOST        Operate on remote host\n"
+               "  -M --machine=CONTAINER       Operate on local container\n"
+               "     --identity=PATH           Read JSON identity from file\n"
+               "     --json=FORMAT             Output inspection data in JSON (takes one of\n"
+               "                               pretty, short, off)\n"
+               "  -j                           Equivalent to --json=pretty (on TTY) or\n"
+               "                               --json=short (otherwise)\n"
+               "     --export-format=          Strip JSON inspection data (full, stripped,\n"
+               "                               minimal)\n"
+               "  -E                           When specified once equals -j --export-format=\n"
+               "                               stripped, when specified twice equals\n"
+               "                               -j --export-format=minimal\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"
+               "     --prompt-new-user         firstboot: Query user interactively for user\n"
+               "                               to create\n"
+               "     --prompt-groups=no        In first-boot mode, don't prompt for auxiliary\n"
+               "                               group memberships\n"
+               "     --prompt-shell=no         In first-boot mode, don't prompt for shells\n"
+               "     --chrome=no               In first-boot mode, don't show colour bar at top\n"
+               "                               and bottom of terminal\n"
+               "     --mute-console=yes        In first-boot mode, tell kernel/PID 1 to not\n"
+               "                               write to the console while running\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"
+               "     --alias=ALIAS             Define alias usernames for this account\n"
+               "     --email-address=EMAIL     Email address for user\n"
+               "     --location=LOCATION       Set location of user on earth\n"
+               "     --birth-date=[DATE]       Set user birth date (YYYY-MM-DD)\n"
+               "     --icon-name=NAME          Icon name for user\n"
+               "  -d --home-dir=PATH           Home directory\n"
+               "  -u --uid=UID                 Numeric UID for user\n"
+               "  -G --member-of=GROUP         Add user to group\n"
+               "     --capability-bounding-set=CAPS\n"
+               "                               Bounding POSIX capability set\n"
+               "     --capability-ambient-set=CAPS\n"
+               "                               Ambient POSIX capability set\n"
+               "     --access-mode=MODE        User home directory access mode\n"
+               "     --umask=MODE              Umask for user when logging in\n"
+               "     --skel=PATH               Skeleton directory to use\n"
+               "     --shell=PATH              Shell for account\n"
+               "     --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n"
+               "     --timezone=TIMEZONE       Set a time-zone\n"
+               "     --language=LOCALE         Set preferred languages\n"
+               "     --default-area=AREA       Select default area\n"
+               "\n%4$sAuthentication User Record Properties:%5$s\n"
+               "     --ssh-authorized-keys=KEYS\n"
+               "                               Specify SSH public keys\n"
+               "     --pkcs11-token-uri=URI    URI to PKCS#11 security token containing\n"
+               "                               private key and matching X.509 certificate\n"
+               "     --fido2-device=PATH       Path to FIDO2 hidraw device with hmac-secret\n"
+               "                               extension\n"
+               "     --fido2-with-client-pin=BOOL\n"
+               "                               Whether to require entering a PIN to unlock the\n"
+               "                               account\n"
+               "     --fido2-with-user-presence=BOOL\n"
+               "                               Whether to require user presence to unlock the\n"
+               "                               account\n"
+               "     --fido2-with-user-verification=BOOL\n"
+               "                               Whether to require user verification to unlock\n"
+               "                               the account\n"
+               "     --recovery-key=BOOL       Add a recovery key\n"
+               "\n%4$sBlob Directory User Record Properties:%5$s\n"
+               "  -b --blob=[FILENAME=]PATH\n"
+               "                               Path to a replacement blob directory, or replace\n"
+               "                               an individual files in the blob directory.\n"
+               "     --avatar=PATH             Path to user avatar picture\n"
+               "     --login-background=PATH   Path to user login background picture\n"
+               "\n%4$sAccount Management User Record Properties:%5$s\n"
+               "     --locked=BOOL             Set locked account state\n"
+               "     --not-before=TIMESTAMP    Do not allow logins before\n"
+               "     --not-after=TIMESTAMP     Do not allow logins after\n"
+               "     --rate-limit-interval=SECS\n"
+               "                               Login rate-limit interval in seconds\n"
+               "     --rate-limit-burst=NUMBER\n"
+               "                               Login rate-limit attempts per interval\n"
+               "\n%4$sPassword Policy User Record Properties:%5$s\n"
+               "     --password-hint=HINT      Set Password hint\n"
+               "     --enforce-password-policy=BOOL\n"
+               "                               Control whether to enforce system's password\n"
+               "                               policy for this user\n"
+               "  -P                           Same as --enforce-password-policy=no\n"
+               "     --password-change-now=BOOL\n"
+               "                               Require the password to be changed on next login\n"
+               "     --password-change-min=TIME\n"
+               "                               Require minimum time between password changes\n"
+               "     --password-change-max=TIME\n"
+               "                               Require maximum time between password changes\n"
+               "     --password-change-warn=TIME\n"
+               "                               How much time to warn before password expiry\n"
+               "     --password-change-inactive=TIME\n"
+               "                               How much time to block password after expiry\n"
+               "\n%4$sResource Management User Record Properties:%5$s\n"
+               "     --disk-size=BYTES         Size to assign the user on disk\n"
+               "     --nice=NICE               Nice level for user\n"
+               "     --rlimit=LIMIT=VALUE[:VALUE]\n"
+               "                               Set resource limits\n"
+               "     --tasks-max=MAX           Set maximum number of per-user tasks\n"
+               "     --memory-high=BYTES       Set high memory threshold in bytes\n"
+               "     --memory-max=BYTES        Set maximum memory limit\n"
+               "     --cpu-weight=WEIGHT       Set CPU weight\n"
+               "     --io-weight=WEIGHT        Set IO weight\n"
+               "     --tmp-limit=BYTES|PERCENT Set limit on /tmp/\n"
+               "     --dev-shm-limit=BYTES|PERCENT\n"
+               "                               Set limit on /dev/shm/\n"
+               "\n%4$sStorage User Record Properties:%5$s\n"
+               "     --storage=STORAGE         Storage type to use (luks, fscrypt, directory,\n"
+               "                               subvolume, cifs)\n"
+               "     --image-path=PATH         Path to image file/directory\n"
+               "     --drop-caches=BOOL        Whether to automatically drop caches on logout\n"
+               "\n%4$sLUKS Storage User Record Properties:%5$s\n"
+               "     --fs-type=TYPE            File system type to use in case of luks\n"
+               "                               storage (btrfs, ext4, xfs)\n"
+               "     --luks-discard=BOOL       Whether to use 'discard' feature of file system\n"
+               "                               when activated (mounted)\n"
+               "     --luks-offline-discard=BOOL\n"
+               "                               Whether to trim file on logout\n"
+               "     --luks-cipher=CIPHER      Cipher to use for LUKS encryption\n"
+               "     --luks-cipher-mode=MODE   Cipher mode to use for LUKS encryption\n"
+               "     --luks-volume-key-size=BITS\n"
+               "                               Volume key size to use for LUKS encryption\n"
+               "     --luks-pbkdf-type=TYPE    Password-based Key Derivation Function to use\n"
+               "     --luks-pbkdf-hash-algorithm=ALGORITHM\n"
+               "                               PBKDF hash algorithm to use\n"
+               "     --luks-pbkdf-time-cost=SECS\n"
+               "                               Time cost for PBKDF in seconds\n"
+               "     --luks-pbkdf-memory-cost=BYTES\n"
+               "                               Memory cost for PBKDF in bytes\n"
+               "     --luks-pbkdf-parallel-threads=NUMBER\n"
+               "                               Number of parallel threads for PKBDF\n"
+               "     --luks-sector-size=BYTES\n"
+               "                               Sector size for LUKS encryption in bytes\n"
+               "     --luks-extra-mount-options=OPTIONS\n"
+               "                               LUKS extra mount options\n"
+               "     --auto-resize-mode=MODE   Automatically grow/shrink home on login/logout\n"
+               "     --rebalance-weight=WEIGHT Weight while rebalancing\n"
+               "\n%4$sMounting User Record Properties:%5$s\n"
+               "     --nosuid=BOOL             Control the 'nosuid' flag of the home mount\n"
+               "     --nodev=BOOL              Control the 'nodev' flag of the home mount\n"
+               "     --noexec=BOOL             Control the 'noexec' flag of the home mount\n"
+               "\n%4$sCIFS User Record Properties:%5$s\n"
+               "     --cifs-domain=DOMAIN      CIFS (Windows) domain\n"
+               "     --cifs-user-name=USER     CIFS (Windows) user name\n"
+               "     --cifs-service=SERVICE    CIFS (Windows) service to mount as home area\n"
+               "     --cifs-extra-mount-options=OPTIONS\n"
+               "                               CIFS (Windows) extra mount options\n"
+               "\n%4$sLogin Behaviour User Record Properties:%5$s\n"
+               "     --stop-delay=SECS         How long to leave user services running after\n"
+               "                               logout\n"
+               "     --kill-processes=BOOL     Whether to kill user processes when sessions\n"
+               "                               terminate\n"
+               "     --auto-login=BOOL         Try to log this user in automatically\n"
+               "     --session-launcher=LAUNCHER\n"
+               "                               Preferred session launcher file\n"
+               "     --session-type=TYPE       Preferred session type\n"
+               "\nSee the %6$s for details.\n",
+               program_invocation_short_name,
+               ansi_highlight(),
+               ansi_normal(),
+               ansi_underline(),
+               ansi_normal(),
+               link);
+
+        return 0;
+}
+
+static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        return help();
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        _cleanup_strv_free_ char **arg_languages = NULL;
+
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_NO_PAGER,
+                ARG_NO_LEGEND,
+                ARG_NO_ASK_PASSWORD,
+                ARG_OFFLINE,
+                ARG_REALM,
+                ARG_ALIAS,
+                ARG_EMAIL_ADDRESS,
+                ARG_DISK_SIZE,
+                ARG_ACCESS_MODE,
+                ARG_STORAGE,
+                ARG_FS_TYPE,
+                ARG_IMAGE_PATH,
+                ARG_UMASK,
+                ARG_LUKS_DISCARD,
+                ARG_LUKS_OFFLINE_DISCARD,
+                ARG_JSON,
+                ARG_SETENV,
+                ARG_TIMEZONE,
+                ARG_LANGUAGE,
+                ARG_LOCKED,
+                ARG_SSH_AUTHORIZED_KEYS,
+                ARG_LOCATION,
+                ARG_BIRTH_DATE,
+                ARG_ICON_NAME,
+                ARG_PASSWORD_HINT,
+                ARG_NICE,
+                ARG_RLIMIT,
+                ARG_NOT_BEFORE,
+                ARG_NOT_AFTER,
+                ARG_LUKS_CIPHER,
+                ARG_LUKS_CIPHER_MODE,
+                ARG_LUKS_VOLUME_KEY_SIZE,
+                ARG_NOSUID,
+                ARG_NODEV,
+                ARG_NOEXEC,
+                ARG_CIFS_DOMAIN,
+                ARG_CIFS_USER_NAME,
+                ARG_CIFS_SERVICE,
+                ARG_CIFS_EXTRA_MOUNT_OPTIONS,
+                ARG_TASKS_MAX,
+                ARG_MEMORY_HIGH,
+                ARG_MEMORY_MAX,
+                ARG_CPU_WEIGHT,
+                ARG_IO_WEIGHT,
+                ARG_LUKS_PBKDF_TYPE,
+                ARG_LUKS_PBKDF_HASH_ALGORITHM,
+                ARG_LUKS_PBKDF_FORCE_ITERATIONS,
+                ARG_LUKS_PBKDF_TIME_COST,
+                ARG_LUKS_PBKDF_MEMORY_COST,
+                ARG_LUKS_PBKDF_PARALLEL_THREADS,
+                ARG_LUKS_SECTOR_SIZE,
+                ARG_RATE_LIMIT_INTERVAL,
+                ARG_RATE_LIMIT_BURST,
+                ARG_STOP_DELAY,
+                ARG_KILL_PROCESSES,
+                ARG_ENFORCE_PASSWORD_POLICY,
+                ARG_PASSWORD_CHANGE_NOW,
+                ARG_PASSWORD_CHANGE_MIN,
+                ARG_PASSWORD_CHANGE_MAX,
+                ARG_PASSWORD_CHANGE_WARN,
+                ARG_PASSWORD_CHANGE_INACTIVE,
+                ARG_EXPORT_FORMAT,
+                ARG_AUTO_LOGIN,
+                ARG_SESSION_LAUNCHER,
+                ARG_SESSION_TYPE,
+                ARG_PKCS11_TOKEN_URI,
+                ARG_FIDO2_DEVICE,
+                ARG_FIDO2_WITH_PIN,
+                ARG_FIDO2_WITH_UP,
+                ARG_FIDO2_WITH_UV,
+                ARG_RECOVERY_KEY,
+                ARG_DROP_CACHES,
+                ARG_LUKS_EXTRA_MOUNT_OPTIONS,
+                ARG_AUTO_RESIZE_MODE,
+                ARG_REBALANCE_WEIGHT,
+                ARG_FIDO2_CRED_ALG,
+                ARG_CAPABILITY_BOUNDING_SET,
+                ARG_CAPABILITY_AMBIENT_SET,
+                ARG_PROMPT_NEW_USER,
+                ARG_AVATAR,
+                ARG_LOGIN_BACKGROUND,
+                ARG_TMP_LIMIT,
+                ARG_DEV_SHM_LIMIT,
+                ARG_DEFAULT_AREA,
+                ARG_KEY_NAME,
+                ARG_SEIZE,
+                ARG_MATCH,
+                ARG_PROMPT_SHELL,
+                ARG_PROMPT_GROUPS,
+                ARG_CHROME,
+                ARG_MUTE_CONSOLE,
+        };
+
+        static const struct option options[] = {
+                { "help",                         no_argument,       NULL, 'h'                             },
+                { "version",                      no_argument,       NULL, ARG_VERSION                     },
+                { "no-pager",                     no_argument,       NULL, ARG_NO_PAGER                    },
+                { "no-legend",                    no_argument,       NULL, ARG_NO_LEGEND                   },
+                { "no-ask-password",              no_argument,       NULL, ARG_NO_ASK_PASSWORD             },
+                { "offline",                      no_argument,       NULL, ARG_OFFLINE                     },
+                { "host",                         required_argument, NULL, 'H'                             },
+                { "machine",                      required_argument, NULL, 'M'                             },
+                { "identity",                     required_argument, NULL, 'I'                             },
+                { "real-name",                    required_argument, NULL, 'c'                             },
+                { "comment",                      required_argument, NULL, 'c'                             }, /* Compat alias to keep thing in sync with useradd(8) */
+                { "realm",                        required_argument, NULL, ARG_REALM                       },
+                { "alias",                        required_argument, NULL, ARG_ALIAS                       },
+                { "email-address",                required_argument, NULL, ARG_EMAIL_ADDRESS               },
+                { "location",                     required_argument, NULL, ARG_LOCATION                    },
+                { "birth-date",                   required_argument, NULL, ARG_BIRTH_DATE                  },
+                { "password-hint",                required_argument, NULL, ARG_PASSWORD_HINT               },
+                { "icon-name",                    required_argument, NULL, ARG_ICON_NAME                   },
+                { "home-dir",                     required_argument, NULL, 'd'                             }, /* Compatible with useradd(8) */
+                { "uid",                          required_argument, NULL, 'u'                             }, /* Compatible with useradd(8) */
+                { "member-of",                    required_argument, NULL, 'G'                             },
+                { "groups",                       required_argument, NULL, 'G'                             }, /* Compat alias to keep thing in sync with useradd(8) */
+                { "skel",                         required_argument, NULL, 'k'                             }, /* Compatible with useradd(8) */
+                { "shell",                        required_argument, NULL, 's'                             }, /* Compatible with useradd(8) */
+                { "setenv",                       required_argument, NULL, ARG_SETENV                      },
+                { "timezone",                     required_argument, NULL, ARG_TIMEZONE                    },
+                { "language",                     required_argument, NULL, ARG_LANGUAGE                    },
+                { "locked",                       required_argument, NULL, ARG_LOCKED                      },
+                { "not-before",                   required_argument, NULL, ARG_NOT_BEFORE                  },
+                { "not-after",                    required_argument, NULL, ARG_NOT_AFTER                   },
+                { "expiredate",                   required_argument, NULL, 'e'                             }, /* Compat alias to keep thing in sync with useradd(8) */
+                { "ssh-authorized-keys",          required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS         },
+                { "disk-size",                    required_argument, NULL, ARG_DISK_SIZE                   },
+                { "access-mode",                  required_argument, NULL, ARG_ACCESS_MODE                 },
+                { "umask",                        required_argument, NULL, ARG_UMASK                       },
+                { "nice",                         required_argument, NULL, ARG_NICE                        },
+                { "rlimit",                       required_argument, NULL, ARG_RLIMIT                      },
+                { "tasks-max",                    required_argument, NULL, ARG_TASKS_MAX                   },
+                { "memory-high",                  required_argument, NULL, ARG_MEMORY_HIGH                 },
+                { "memory-max",                   required_argument, NULL, ARG_MEMORY_MAX                  },
+                { "cpu-weight",                   required_argument, NULL, ARG_CPU_WEIGHT                  },
+                { "io-weight",                    required_argument, NULL, ARG_IO_WEIGHT                   },
+                { "storage",                      required_argument, NULL, ARG_STORAGE                     },
+                { "image-path",                   required_argument, NULL, ARG_IMAGE_PATH                  },
+                { "fs-type",                      required_argument, NULL, ARG_FS_TYPE                     },
+                { "luks-discard",                 required_argument, NULL, ARG_LUKS_DISCARD                },
+                { "luks-offline-discard",         required_argument, NULL, ARG_LUKS_OFFLINE_DISCARD        },
+                { "luks-cipher",                  required_argument, NULL, ARG_LUKS_CIPHER                 },
+                { "luks-cipher-mode",             required_argument, NULL, ARG_LUKS_CIPHER_MODE            },
+                { "luks-volume-key-size",         required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE        },
+                { "luks-pbkdf-type",              required_argument, NULL, ARG_LUKS_PBKDF_TYPE             },
+                { "luks-pbkdf-hash-algorithm",    required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM   },
+                { "luks-pbkdf-force-iterations",  required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS },
+                { "luks-pbkdf-time-cost",         required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST        },
+                { "luks-pbkdf-memory-cost",       required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST      },
+                { "luks-pbkdf-parallel-threads",  required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS },
+                { "luks-sector-size",             required_argument, NULL, ARG_LUKS_SECTOR_SIZE            },
+                { "nosuid",                       required_argument, NULL, ARG_NOSUID                      },
+                { "nodev",                        required_argument, NULL, ARG_NODEV                       },
+                { "noexec",                       required_argument, NULL, ARG_NOEXEC                      },
+                { "cifs-user-name",               required_argument, NULL, ARG_CIFS_USER_NAME              },
+                { "cifs-domain",                  required_argument, NULL, ARG_CIFS_DOMAIN                 },
+                { "cifs-service",                 required_argument, NULL, ARG_CIFS_SERVICE                },
+                { "cifs-extra-mount-options",     required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS    },
+                { "rate-limit-interval",          required_argument, NULL, ARG_RATE_LIMIT_INTERVAL         },
+                { "rate-limit-burst",             required_argument, NULL, ARG_RATE_LIMIT_BURST            },
+                { "stop-delay",                   required_argument, NULL, ARG_STOP_DELAY                  },
+                { "kill-processes",               required_argument, NULL, ARG_KILL_PROCESSES              },
+                { "enforce-password-policy",      required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY     },
+                { "password-change-now",          required_argument, NULL, ARG_PASSWORD_CHANGE_NOW         },
+                { "password-change-min",          required_argument, NULL, ARG_PASSWORD_CHANGE_MIN         },
+                { "password-change-max",          required_argument, NULL, ARG_PASSWORD_CHANGE_MAX         },
+                { "password-change-warn",         required_argument, NULL, ARG_PASSWORD_CHANGE_WARN        },
+                { "password-change-inactive",     required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE    },
+                { "auto-login",                   required_argument, NULL, ARG_AUTO_LOGIN                  },
+                { "session-launcher",             required_argument, NULL, ARG_SESSION_LAUNCHER,           },
+                { "session-type",                 required_argument, NULL, ARG_SESSION_TYPE,               },
+                { "json",                         required_argument, NULL, ARG_JSON                        },
+                { "export-format",                required_argument, NULL, ARG_EXPORT_FORMAT               },
+                { "pkcs11-token-uri",             required_argument, NULL, ARG_PKCS11_TOKEN_URI            },
+                { "fido2-credential-algorithm",   required_argument, NULL, ARG_FIDO2_CRED_ALG              },
+                { "fido2-device",                 required_argument, NULL, ARG_FIDO2_DEVICE                },
+                { "fido2-with-client-pin",        required_argument, NULL, ARG_FIDO2_WITH_PIN              },
+                { "fido2-with-user-presence",     required_argument, NULL, ARG_FIDO2_WITH_UP               },
+                { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV               },
+                { "recovery-key",                 required_argument, NULL, ARG_RECOVERY_KEY                },
+                { "drop-caches",                  required_argument, NULL, ARG_DROP_CACHES                 },
+                { "luks-extra-mount-options",     required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS    },
+                { "auto-resize-mode",             required_argument, NULL, ARG_AUTO_RESIZE_MODE            },
+                { "rebalance-weight",             required_argument, NULL, ARG_REBALANCE_WEIGHT            },
+                { "capability-bounding-set",      required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET     },
+                { "capability-ambient-set",       required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET      },
+                { "prompt-new-user",              no_argument,       NULL, ARG_PROMPT_NEW_USER             },
+                { "blob",                         required_argument, NULL, 'b'                             },
+                { "avatar",                       required_argument, NULL, ARG_AVATAR                      },
+                { "login-background",             required_argument, NULL, ARG_LOGIN_BACKGROUND            },
+                { "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                    },
+                { "seize",                        required_argument, NULL, ARG_SEIZE                       },
+                { "match",                        required_argument, NULL, ARG_MATCH                       },
+                { "prompt-shell",                 required_argument, NULL, ARG_PROMPT_SHELL                },
+                { "prompt-groups",                required_argument, NULL, ARG_PROMPT_GROUPS               },
+                { "chrome",                       required_argument, NULL, ARG_CHROME                      },
+                { "mute-console",                 required_argument, NULL, ARG_MUTE_CONSOLE                },
+                {}
+        };
 
-                case ARG_LANGUAGE:
-                        r = parse_language_field(&arg_languages, optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        int r;
 
-                case ARG_NOSUID:
-                case ARG_NODEV:
-                case ARG_NOEXEC:
-                case ARG_LOCKED:
-                case ARG_KILL_PROCESSES:
-                case ARG_ENFORCE_PASSWORD_POLICY:
-                case ARG_AUTO_LOGIN:
-                case ARG_PASSWORD_CHANGE_NOW: {
-                        const char *field =
-                                                 c == ARG_LOCKED ? "locked" :
-                                                 c == ARG_NOSUID ? "mountNoSuid" :
-                                                  c == ARG_NODEV ? "mountNoDevices" :
-                                                 c == ARG_NOEXEC ? "mountNoExecute" :
-                                         c == ARG_KILL_PROCESSES ? "killProcesses" :
-                                c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" :
-                                             c == ARG_AUTO_LOGIN ? "autoLogin" :
-                                    c == ARG_PASSWORD_CHANGE_NOW ? "passwordChangeNow" :
-                                                                   NULL;
-                        assert(field);
+        /* 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;
 
-                        r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg);
-                        if (r < 0)
-                                return r;
-                        break;
-                }
+        assert(argc >= 0);
+        assert(argv);
 
-                case 'P':
-                        r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to set %s field: %m", "enforcePasswordPolicy");
-                        break;
+        /* 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");
 
-                case ARG_DISK_SIZE:
-                        r = parse_disk_size_field(match_identity ?: &arg_identity_extra_this_machine, optarg);
-                        if (r < 0)
-                                return r;
-                        break;
+        for (;;) {
+                int c;
 
-                case ARG_ACCESS_MODE:
-                        r = parse_mode_field(&arg_identity_extra, "accessMode", optarg);
-                        if (r < 0)
-                                return r;
+                c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL);
+                if (c < 0)
                         break;
 
-                case ARG_LUKS_DISCARD:
-                case ARG_LUKS_OFFLINE_DISCARD: {
-                        const char *field = c == ARG_LUKS_DISCARD ? "luksDiscard" : "luksOfflineDiscard";
-
-                        r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg);
-                        if (r < 0)
-                                return r;
-                        break;
-                }
+                switch (c) {
 
-                case ARG_LUKS_VOLUME_KEY_SIZE:
-                case ARG_LUKS_PBKDF_FORCE_ITERATIONS:
-                case ARG_LUKS_PBKDF_PARALLEL_THREADS:
-                case ARG_RATE_LIMIT_BURST: {
-                        const char *field =
-                                       c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" :
-                                c == ARG_LUKS_PBKDF_FORCE_ITERATIONS ? "luksPbkdfForceIterations" :
-                                c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" :
-                                           c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" :
-                                                                       NULL;
-                        assert(field);
+                case 'h':
+                        return help();
 
-                        r = parse_unsigned_field(match_identity ?: &arg_identity_extra, field, optarg);
-                        if (r < 0)
-                                return r;
-                        break;
-                }
+                case ARG_VERSION:
+                        return version();
 
-                case ARG_LUKS_SECTOR_SIZE:
-                        r = parse_sector_size_field(match_identity ?: &arg_identity_extra, "luksSectorSize", optarg);
-                        if (r < 0)
-                                return r;
+                case ARG_NO_PAGER:
+                        arg_pager_flags |= PAGER_DISABLE;
                         break;
 
-                case ARG_UMASK:
-                        r = parse_mode_field(match_identity ?: &arg_identity_extra, "umask", optarg);
-                        if (r < 0)
-                                return r;
+                case ARG_NO_LEGEND:
+                        arg_legend = false;
                         break;
 
-                case ARG_SSH_AUTHORIZED_KEYS:
-                        r = parse_ssh_authorized_keys(&arg_identity_extra_privileged, "sshAuthorizedKeys", optarg);
-                        if (r < 0)
-                                return r;
-
+                case ARG_NO_ASK_PASSWORD:
+                        arg_ask_password = false;
                         break;
 
-                case ARG_NOT_BEFORE:
-                case ARG_NOT_AFTER:
-                case 'e': {
-                        const char *field = c == ARG_NOT_BEFORE ? "notBeforeUSec" : "notAfterUSec";
-
-                        r = parse_timestamp_field(match_identity ?: &arg_identity_extra, field, optarg);
-                        if (r < 0)
-                                return r;
+                case ARG_OFFLINE:
+                        arg_offline = true;
                         break;
-                }
-
-                case ARG_PASSWORD_CHANGE_MIN:
-                case ARG_PASSWORD_CHANGE_MAX:
-                case ARG_PASSWORD_CHANGE_WARN:
-                case ARG_PASSWORD_CHANGE_INACTIVE: {
-                        const char *field =
-                                     c == ARG_PASSWORD_CHANGE_MIN ? "passwordChangeMinUSec" :
-                                     c == ARG_PASSWORD_CHANGE_MAX ? "passwordChangeMaxUSec" :
-                                    c == ARG_PASSWORD_CHANGE_WARN ? "passwordChangeWarnUSec" :
-                                c == ARG_PASSWORD_CHANGE_INACTIVE ? "passwordChangeInactiveUSec" :
-                                                                    NULL;
-                        assert(field);
 
-                        r = parse_time_field(match_identity ?: &arg_identity_extra, field, optarg);
-                        if (r < 0)
-                                return r;
+                case 'H':
+                        arg_transport = BUS_TRANSPORT_REMOTE;
+                        arg_host = optarg;
                         break;
-                }
-
-                case ARG_STORAGE:
-                case ARG_FS_TYPE:
-                case ARG_LUKS_CIPHER:
-                case ARG_LUKS_CIPHER_MODE:
-                case ARG_LUKS_PBKDF_TYPE:
-                case ARG_LUKS_PBKDF_HASH_ALGORITHM: {
-                        const char *field =
-                                                  c == ARG_STORAGE ? "storage" :
-                                                  c == ARG_FS_TYPE ? "fileSystemType" :
-                                              c == ARG_LUKS_CIPHER ? "luksCipher" :
-                                         c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" :
-                                          c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" :
-                                c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" :
-                                                                     NULL;
-                        assert(field);
-
-                        sd_json_variant **identity =
-                                match_identity ?:
-                                IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ?
-                                &arg_identity_extra_this_machine : &arg_identity_extra;
-
-                        if (!string_is_safe(optarg, STRING_ALLOW_GLOBS))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Parameter for field %s not valid: %s", field, optarg);
 
-                        r = parse_string_field(identity, field, optarg);
+                case 'M':
+                        r = parse_machine_argument(optarg, &arg_host, &arg_transport);
                         if (r < 0)
                                 return r;
                         break;
-                }
 
-                case ARG_LUKS_PBKDF_TIME_COST:
-                case ARG_RATE_LIMIT_INTERVAL:
-                case ARG_STOP_DELAY: {
-                        const char *field =
-                                c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" :
-                                 c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" :
-                                          c == ARG_STOP_DELAY ? "stopDelayUSec" :
-                                                                NULL;
-                        assert(field);
+                case 'I':
+                        arg_identity = optarg;
+                        break;
 
-                        r = parse_time_field(match_identity ?: &arg_identity_extra, field, optarg);
+                case 'c':
+                        if (!isempty(optarg) && !valid_gecos(optarg))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Invalid GECOS field '%s'.", optarg);
+
+                        r = parse_string_field(match_identity ?: &arg_identity_extra, "realName", optarg);
                         if (r < 0)
                                 return r;
                         break;
-                }
 
-                case 'G':
-                        r = parse_group_field(match_identity ?: &arg_identity_extra, "memberOf", optarg);
+                case ARG_ALIAS:
+                        r = parse_group_field(&arg_identity_extra, "aliases", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_TASKS_MAX:
-                        r = parse_u64_field(match_identity ?: &arg_identity_extra, "tasksMax", optarg);
+                case 'd':
+                        r = parse_home_directory_field(&arg_identity_extra, "homeDirectory", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_MEMORY_MAX:
-                case ARG_MEMORY_HIGH:
-                case ARG_LUKS_PBKDF_MEMORY_COST: {
-                        const char *field =
-                                            c == ARG_MEMORY_MAX ? "memoryMax" :
-                                           c == ARG_MEMORY_HIGH ? "memoryHigh" :
-                                c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" :
-                                                                  NULL;
-
-                        r = parse_size_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg);
+                case ARG_REALM:
+                        r = parse_realm_field(&arg_identity_extra, "realm", optarg);
                         if (r < 0)
                                 return r;
                         break;
-                }
 
-                case ARG_CPU_WEIGHT:
-                case ARG_IO_WEIGHT: {
-                        const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" :
-                                             c == ARG_IO_WEIGHT ? "ioWeight" :
-                                                                  NULL;
+                case ARG_EMAIL_ADDRESS:
+                case ARG_LOCATION:
+                case ARG_ICON_NAME:
+                case ARG_CIFS_USER_NAME:
+                case ARG_CIFS_DOMAIN:
+                case ARG_CIFS_EXTRA_MOUNT_OPTIONS:
+                case ARG_LUKS_EXTRA_MOUNT_OPTIONS:
+                case ARG_SESSION_LAUNCHER:
+                case ARG_SESSION_TYPE: {
+                        const char *field =
+                                           c == ARG_EMAIL_ADDRESS ? "emailAddress" :
+                                                c == ARG_LOCATION ? "location" :
+                                               c == ARG_ICON_NAME ? "iconName" :
+                                          c == ARG_CIFS_USER_NAME ? "cifsUserName" :
+                                             c == ARG_CIFS_DOMAIN ? "cifsDomain" :
+                                c == ARG_CIFS_EXTRA_MOUNT_OPTIONS ? "cifsExtraMountOptions" :
+                                c == ARG_LUKS_EXTRA_MOUNT_OPTIONS ? "luksExtraMountOptions" :
+                                        c == ARG_SESSION_LAUNCHER ? "preferredSessionLauncher" :
+                                            c == ARG_SESSION_TYPE ? "preferredSessionType" :
+                                                                    NULL;
+                        assert(field);
 
-                        r = parse_weight_field(match_identity ?: &arg_identity_extra, field, optarg);
+                        r = parse_string_field(match_identity ?: &arg_identity_extra, field, optarg);
                         if (r < 0)
                                 return r;
                         break;
                 }
 
-                case ARG_PKCS11_TOKEN_URI:
-                        r = parse_pkcs11_token_uri_field(optarg);
-                        if (r <= 0)
-                                return r;
-                        break;
+                case ARG_BIRTH_DATE:
+                        if (isempty(optarg)) {
+                                r = drop_from_identity("birthDate");
+                                if (r < 0)
+                                        return r;
+                        } else {
+                                r = parse_birth_date(optarg, /* ret= */ NULL);
+                                if (r < 0)
+                                        return log_error_errno(r, "Invalid birth date (expected YYYY-MM-DD): %s", optarg);
 
-                case ARG_FIDO2_CRED_ALG:
-                        r = parse_fido2_algorithm(optarg, &arg_fido2_cred_alg);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to parse COSE algorithm: %s", optarg);
+                                r = parse_string_field(&arg_identity_extra, "birthDate", optarg);
+                                if (r < 0)
+                                        return r;
+                        }
                         break;
 
-                case ARG_FIDO2_DEVICE:
-                        r = parse_fido2_device_field(optarg);
-                        if (r <= 0)
-                                return r;
-                        break;
+                case ARG_CIFS_SERVICE:
+                        if (!isempty(optarg)) {
+                                r = parse_cifs_service(optarg, /* ret_host= */ NULL, /* ret_service= */ NULL, /* ret_path= */ NULL);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to validate CIFS service name: %s", optarg);
+                        }
 
-                case ARG_FIDO2_WITH_PIN:
-                        r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL);
+                        r = parse_string_field(match_identity ?: &arg_identity_extra, "cifsService", optarg);
                         if (r < 0)
                                 return r;
-
-                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r);
                         break;
 
-                case ARG_FIDO2_WITH_UP:
-                        r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL);
+                case ARG_PASSWORD_HINT:
+                        r = parse_string_field(&arg_identity_extra_privileged, "passwordHint", optarg);
                         if (r < 0)
                                 return r;
 
-                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r);
+                        string_erase(optarg);
                         break;
 
-                case ARG_FIDO2_WITH_UV:
-                        r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL);
+                case ARG_NICE:
+                        r = parse_nice_field(match_identity ?: &arg_identity_extra, "niceLevel", optarg);
                         if (r < 0)
                                 return r;
-
-                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r);
                         break;
 
-                case ARG_RECOVERY_KEY:
-                        r = parse_boolean(optarg);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to parse --recovery-key= argument: %s", optarg);
-                        arg_recovery_key = r;
-
-                        r = drop_from_identity("recoveryKey", "recoveryKeyType");
+                case ARG_RLIMIT:
+                        r = parse_rlimit_field(&arg_identity_extra_rlimits, "resourceLimits", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_AUTO_RESIZE_MODE:
-                        r = parse_auto_resize_mode_field(match_identity ?: &arg_identity_extra,
-                                                         "autoResizeMode", optarg);
+                case 'u':
+                        r = parse_uid_field(&arg_identity_extra, "uid", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_REBALANCE_WEIGHT:
-                        r = parse_rebalance_weight(match_identity ?: &arg_identity_extra,
-                                                   "rebalanceWeight", optarg);
+                case 'k':
+                case ARG_IMAGE_PATH: {
+                        const char *field = c == 'k' ? "skeletonDirectory" : "imagePath";
+
+                        r = parse_path_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg);
                         if (r < 0)
                                 return r;
                         break;
+                }
 
-                case 'j':
-                        arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
-                        break;
+                case 's':
+                        if (!isempty(optarg) && !valid_shell(optarg))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Shell '%s' not valid.", optarg);
 
-                case ARG_JSON:
-                        r = parse_json_argument(optarg, &arg_json_format_flags);
-                        if (r <= 0)
+                        r = parse_string_field(match_identity ?: &arg_identity_extra, "shell", optarg);
+                        if (r < 0)
                                 return r;
-
-                        break;
-
-                case 'E':
-                        if (arg_export_format == EXPORT_FORMAT_FULL)
-                                arg_export_format = EXPORT_FORMAT_STRIPPED;
-                        else if (arg_export_format == EXPORT_FORMAT_STRIPPED)
-                                arg_export_format = EXPORT_FORMAT_MINIMAL;
-                        else
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported.");
-
-                        arg_json_format_flags &= ~SD_JSON_FORMAT_OFF;
-                        if (arg_json_format_flags == 0)
-                                arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
-                        break;
-
-                case ARG_EXPORT_FORMAT:
-                        if (streq(optarg, "help"))
-                                return DUMP_STRING_TABLE(export_format, ExportFormat, _EXPORT_FORMAT_MAX);
-
-                        arg_export_format = export_format_from_string(optarg);
-                        if (arg_export_format < 0)
-                                return log_error_errno(arg_export_format, "Invalid export format: %s", optarg);
-
                         break;
 
-                case ARG_DROP_CACHES:
-                        r = parse_boolean_field(match_identity ?: &arg_identity_extra, "dropCaches", optarg);
+                case ARG_SETENV:
+                        r = parse_environment_field(match_identity ?: &arg_identity_extra, "environment", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_CAPABILITY_AMBIENT_SET:
-                        r = parse_capability_set_field(match_identity ?: &arg_identity_extra,
-                                                       &arg_capability_ambient_set,
-                                                       "capabilityAmbientSet", optarg);
+                case ARG_TIMEZONE:
+                        if (!isempty(optarg) && !timezone_is_valid(optarg, LOG_DEBUG))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Timezone '%s' is not valid.", optarg);
+
+                        r = parse_string_field(match_identity ?: &arg_identity_extra, "timeZone", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_CAPABILITY_BOUNDING_SET:
-                        r = parse_capability_set_field(match_identity ?: &arg_identity_extra,
-                                                       &arg_capability_bounding_set,
-                                                       "capabilityBoundingSet", optarg);
+                case ARG_LANGUAGE:
+                        r = parse_language_field(&arg_languages, optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_PROMPT_NEW_USER:
-                        arg_prompt_new_user = true;
-                        break;
-
-                case 'b':
-                case ARG_AVATAR:
-                case ARG_LOGIN_BACKGROUND: {
-                        _cleanup_close_ int fd = -EBADF;
-                        _cleanup_free_ char *path = NULL, *filename = NULL;
-
-                        if (c == 'b') {
-                                char *eq;
-
-                                if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */
-                                        hashmap_clear(arg_blob_files);
-                                        arg_blob_dir = mfree(arg_blob_dir);
-                                        arg_blob_clear = true;
-                                        break;
-                                }
-
-                                eq = strrchr(optarg, '=');
-                                if (!eq) { /* --blob=/some/path replaces the blob dir */
-                                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_blob_dir);
-                                        if (r < 0)
-                                                return r;
-                                        break;
-                                }
-
-                                /* --blob=filename=/some/path replaces the file "filename" with /some/path */
-                                filename = strndup(optarg, eq - optarg);
-                                if (!filename)
-                                        return log_oom();
-
-                                if (isempty(filename))
-                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg);
-                                if (!suitable_blob_filename(filename))
-                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename);
-
-                                r = parse_path_argument(eq + 1, /* suppress_root= */ false, &path);
-                                if (r < 0)
-                                        return r;
-                        } else {
-                                const char *well_known_filename =
-                                                  c == ARG_AVATAR ? "avatar" :
-                                        c == ARG_LOGIN_BACKGROUND ? "login-background" :
-                                                                    NULL;
-                                assert(well_known_filename);
-
-                                filename = strdup(well_known_filename);
-                                if (!filename)
-                                        return log_oom();
+                case ARG_NOSUID:
+                case ARG_NODEV:
+                case ARG_NOEXEC:
+                case ARG_LOCKED:
+                case ARG_KILL_PROCESSES:
+                case ARG_ENFORCE_PASSWORD_POLICY:
+                case ARG_AUTO_LOGIN:
+                case ARG_PASSWORD_CHANGE_NOW: {
+                        const char *field =
+                                                 c == ARG_LOCKED ? "locked" :
+                                                 c == ARG_NOSUID ? "mountNoSuid" :
+                                                  c == ARG_NODEV ? "mountNoDevices" :
+                                                 c == ARG_NOEXEC ? "mountNoExecute" :
+                                         c == ARG_KILL_PROCESSES ? "killProcesses" :
+                                c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" :
+                                             c == ARG_AUTO_LOGIN ? "autoLogin" :
+                                    c == ARG_PASSWORD_CHANGE_NOW ? "passwordChangeNow" :
+                                                                   NULL;
+                        assert(field);
 
-                                r = parse_path_argument(optarg, /* suppress_root= */ false, &path);
-                                if (r < 0)
-                                        return r;
-                        }
+                        r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg);
+                        if (r < 0)
+                                return r;
+                        break;
+                }
 
-                        if (path) {
-                                fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
-                                if (fd < 0)
-                                        return log_error_errno(errno, "Failed to open %s: %m", path);
+                case 'P':
+                        r = sd_json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to set %s field: %m", "enforcePasswordPolicy");
+                        break;
 
-                                if (fd_verify_regular(fd) < 0)
-                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path);
-                        } else
-                                fd = -EBADF; /* Delete the file */
+                case ARG_DISK_SIZE:
+                        r = parse_disk_size_field(match_identity ?: &arg_identity_extra_this_machine, optarg);
+                        if (r < 0)
+                                return r;
+                        break;
 
-                        r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd));
+                case ARG_ACCESS_MODE:
+                        r = parse_mode_field(&arg_identity_extra, "accessMode", optarg);
                         if (r < 0)
-                                return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename);
-                        TAKE_PTR(filename); /* hashmap takes ownership */
-                        TAKE_FD(fd);
+                                return r;
+                        break;
+
+                case ARG_LUKS_DISCARD:
+                case ARG_LUKS_OFFLINE_DISCARD: {
+                        const char *field = c == ARG_LUKS_DISCARD ? "luksDiscard" : "luksOfflineDiscard";
 
+                        r = parse_boolean_field(match_identity ?: &arg_identity_extra, field, optarg);
+                        if (r < 0)
+                                return r;
                         break;
                 }
 
-                case ARG_TMP_LIMIT:
-                case ARG_DEV_SHM_LIMIT: {
+                case ARG_LUKS_VOLUME_KEY_SIZE:
+                case ARG_LUKS_PBKDF_FORCE_ITERATIONS:
+                case ARG_LUKS_PBKDF_PARALLEL_THREADS:
+                case ARG_RATE_LIMIT_BURST: {
                         const char *field =
-                                    c == ARG_TMP_LIMIT ? "tmpLimit" :
-                                c == ARG_DEV_SHM_LIMIT ? "devShmLimit" :
-                                                         NULL;
-                        const char *field_scale =
-                                    c == ARG_TMP_LIMIT ? "tmpLimitScale" :
-                                c == ARG_DEV_SHM_LIMIT ? "devShmLimitScale" :
-                                                         NULL;
-
+                                       c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" :
+                                c == ARG_LUKS_PBKDF_FORCE_ITERATIONS ? "luksPbkdfForceIterations" :
+                                c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" :
+                                           c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" :
+                                                                       NULL;
                         assert(field);
-                        assert(field_scale);
 
-                        r = parse_tmpfs_limit_field(match_identity ?: &arg_identity_extra,
-                                                    field, field_scale, optarg);
+                        r = parse_unsigned_field(match_identity ?: &arg_identity_extra, field, optarg);
                         if (r < 0)
                                 return r;
                         break;
                 }
 
-                case ARG_DEFAULT_AREA:
-                        r = parse_filename_field(match_identity ?: &arg_identity_extra, "defaultArea", optarg);
+                case ARG_LUKS_SECTOR_SIZE:
+                        r = parse_sector_size_field(match_identity ?: &arg_identity_extra, "luksSectorSize", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_KEY_NAME:
-                        if (!isempty(optarg) && !filename_is_valid(optarg))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Parameter for --key-name= not a valid filename: %s", optarg);
-
-                        r = free_and_strdup_warn(&arg_key_name, empty_to_null(optarg));
+                case ARG_UMASK:
+                        r = parse_mode_field(match_identity ?: &arg_identity_extra, "umask", optarg);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_SEIZE:
-                        r = parse_boolean_argument("--seize=", optarg, &arg_seize);
+                case ARG_SSH_AUTHORIZED_KEYS:
+                        r = parse_ssh_authorized_keys(&arg_identity_extra_privileged, "sshAuthorizedKeys", optarg);
                         if (r < 0)
                                 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 ARG_NOT_BEFORE:
+                case ARG_NOT_AFTER:
+                case 'e': {
+                        const char *field = c == ARG_NOT_BEFORE ? "notBeforeUSec" : "notAfterUSec";
 
-                case ARG_PROMPT_SHELL:
-                        r = parse_boolean_argument("--prompt-shell=", optarg, &arg_prompt_shell);
+                        r = parse_timestamp_field(match_identity ?: &arg_identity_extra, field, optarg);
                         if (r < 0)
                                 return r;
-
                         break;
+                }
 
-                case ARG_PROMPT_GROUPS:
-                        r = parse_boolean_argument("--prompt-groups=", optarg, &arg_prompt_groups);
+                case ARG_PASSWORD_CHANGE_MIN:
+                case ARG_PASSWORD_CHANGE_MAX:
+                case ARG_PASSWORD_CHANGE_WARN:
+                case ARG_PASSWORD_CHANGE_INACTIVE: {
+                        const char *field =
+                                     c == ARG_PASSWORD_CHANGE_MIN ? "passwordChangeMinUSec" :
+                                     c == ARG_PASSWORD_CHANGE_MAX ? "passwordChangeMaxUSec" :
+                                    c == ARG_PASSWORD_CHANGE_WARN ? "passwordChangeWarnUSec" :
+                                c == ARG_PASSWORD_CHANGE_INACTIVE ? "passwordChangeInactiveUSec" :
+                                                                    NULL;
+                        assert(field);
+
+                        r = parse_time_field(match_identity ?: &arg_identity_extra, field, optarg);
                         if (r < 0)
                                 return r;
-
                         break;
+                }
 
-                case ARG_CHROME:
-                        r = parse_boolean_argument("--chrome=", optarg, &arg_chrome);
+                case ARG_STORAGE:
+                case ARG_FS_TYPE:
+                case ARG_LUKS_CIPHER:
+                case ARG_LUKS_CIPHER_MODE:
+                case ARG_LUKS_PBKDF_TYPE:
+                case ARG_LUKS_PBKDF_HASH_ALGORITHM: {
+                        const char *field =
+                                                  c == ARG_STORAGE ? "storage" :
+                                                  c == ARG_FS_TYPE ? "fileSystemType" :
+                                              c == ARG_LUKS_CIPHER ? "luksCipher" :
+                                         c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" :
+                                          c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" :
+                                c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" :
+                                                                     NULL;
+                        assert(field);
+
+                        sd_json_variant **identity =
+                                match_identity ?:
+                                IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ?
+                                &arg_identity_extra_this_machine : &arg_identity_extra;
+
+                        if (!string_is_safe(optarg, STRING_ALLOW_GLOBS))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Parameter for field %s not valid: %s", field, optarg);
+
+                        r = parse_string_field(identity, field, optarg);
                         if (r < 0)
                                 return r;
+                        break;
+                }
+
+                case ARG_LUKS_PBKDF_TIME_COST:
+                case ARG_RATE_LIMIT_INTERVAL:
+                case ARG_STOP_DELAY: {
+                        const char *field =
+                                c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" :
+                                 c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" :
+                                          c == ARG_STOP_DELAY ? "stopDelayUSec" :
+                                                                NULL;
+                        assert(field);
 
+                        r = parse_time_field(match_identity ?: &arg_identity_extra, field, optarg);
+                        if (r < 0)
+                                return r;
                         break;
+                }
 
-                case ARG_MUTE_CONSOLE:
-                        r = parse_boolean_argument("--mute-console=", optarg, &arg_mute_console);
+                case 'G':
+                        r = parse_group_field(match_identity ?: &arg_identity_extra, "memberOf", optarg);
                         if (r < 0)
                                 return r;
+                        break;
 
+                case ARG_TASKS_MAX:
+                        r = parse_u64_field(match_identity ?: &arg_identity_extra, "tasksMax", optarg);
+                        if (r < 0)
+                                return r;
                         break;
 
-                case '?':
-                        return -EINVAL;
+                case ARG_MEMORY_MAX:
+                case ARG_MEMORY_HIGH:
+                case ARG_LUKS_PBKDF_MEMORY_COST: {
+                        const char *field =
+                                            c == ARG_MEMORY_MAX ? "memoryMax" :
+                                           c == ARG_MEMORY_HIGH ? "memoryHigh" :
+                                c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" :
+                                                                  NULL;
 
-                default:
-                        assert_not_reached();
+                        r = parse_size_field(match_identity ?: &arg_identity_extra_this_machine, field, optarg);
+                        if (r < 0)
+                                return r;
+                        break;
                 }
-        }
-
-        if (!strv_isempty(arg_languages)) {
-                char **additional;
 
-                r = sd_json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to update preferred language: %m");
+                case ARG_CPU_WEIGHT:
+                case ARG_IO_WEIGHT: {
+                        const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" :
+                                             c == ARG_IO_WEIGHT ? "ioWeight" :
+                                                                  NULL;
 
-                additional = strv_skip(arg_languages, 1);
-                if (!strv_isempty(additional)) {
-                        r = sd_json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to update additional language list: %m");
-                } else {
-                        r = drop_from_identity("additionalLanguages");
+                        r = parse_weight_field(match_identity ?: &arg_identity_extra, field, optarg);
                         if (r < 0)
                                 return r;
+                        break;
                 }
-        }
-
-        return 1;
-}
-
-static int redirect_bus_mgr(void) {
-        const char *suffix;
-
-        /* Talk to a different service if that's requested. (The same env var is also understood by homed, so
-         * that it is relatively easily possible to invoke a second instance of homed for debug purposes and
-         * have homectl talk to it, without colliding with the host version. This is handy when operating
-         * from a homed-managed account.) */
-
-        suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
-        if (suffix) {
-                static BusLocator locator = {
-                        .path = "/org/freedesktop/home1",
-                        .interface = "org.freedesktop.home1.Manager",
-                };
 
-                /* Yes, we leak this memory, but there's little point to collect this, given that we only do
-                 * this in a debug environment, do it only once, and the string shall live for out entire
-                 * process runtime. */
+                case ARG_PKCS11_TOKEN_URI:
+                        r = parse_pkcs11_token_uri_field(optarg);
+                        if (r <= 0)
+                                return r;
+                        break;
 
-                locator.destination = strjoin("org.freedesktop.home1.", suffix);
-                if (!locator.destination)
-                        return log_oom();
+                case ARG_FIDO2_CRED_ALG:
+                        r = parse_fido2_algorithm(optarg, &arg_fido2_cred_alg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse COSE algorithm: %s", optarg);
+                        break;
 
-                bus_mgr = &locator;
-        } else
-                bus_mgr = bus_home_mgr;
+                case ARG_FIDO2_DEVICE:
+                        r = parse_fido2_device_field(optarg);
+                        if (r <= 0)
+                                return r;
+                        break;
 
-        return 0;
-}
+                case ARG_FIDO2_WITH_PIN:
+                        r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL);
+                        if (r < 0)
+                                return r;
 
-static bool is_fallback_shell(const char *p) {
-        const char *q;
+                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r);
+                        break;
 
-        if (!p)
-                return false;
+                case ARG_FIDO2_WITH_UP:
+                        r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL);
+                        if (r < 0)
+                                return r;
 
-        if (p[0] == '-') {
-                /* Skip over login shell dash */
-                p++;
+                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r);
+                        break;
 
-                if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */
-                        return true;
-        }
+                case ARG_FIDO2_WITH_UV:
+                        r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL);
+                        if (r < 0)
+                                return r;
 
-        q = strrchr(p, '/'); /* Skip over path */
-        if (q)
-                p = q + 1;
+                        SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r);
+                        break;
 
-        return streq(p, "systemd-home-fallback-shell");
-}
+                case ARG_RECOVERY_KEY:
+                        r = parse_boolean(optarg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --recovery-key= argument: %s", optarg);
+                        arg_recovery_key = r;
 
-static int fallback_shell(int argc, char *argv[]) {
-        _cleanup_(user_record_unrefp) UserRecord *secret = NULL, *hr = NULL;
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        _cleanup_strv_free_ char **l = NULL;
-        _cleanup_free_ char *argv0 = NULL;
-        const char *json, *hd, *shell;
-        int r, incomplete;
+                        r = drop_from_identity("recoveryKey", "recoveryKeyType");
+                        if (r < 0)
+                                return r;
+                        break;
 
-        /* So here's the deal: if users log into a system via ssh, and their homed-managed home directory
-         * wasn't activated yet, SSH will permit the access but the home directory isn't actually available
-         * yet. SSH doesn't allow us to ask authentication questions from the PAM session stack, and doesn't
-         * run the PAM authentication stack (because it authenticates via its own key management, after
-         * all). So here's our way to support this: homectl can be invoked as a multi-call binary under the
-         * name "systemd-home-fallback-shell". If so, it will chainload a login shell, but first try to
-         * unlock the home directory of the user it is invoked as. systemd-homed will then override the shell
-         * listed in user records whose home directory is not activated yet with this pseudo-shell. Net
-         * effect: one SSH auth succeeds this pseudo shell gets invoked, which will unlock the homedir
-         * (possibly asking for a passphrase) and then chainload the regular shell. Once the login is
-         * complete the user record will look like any other. */
+                case ARG_AUTO_RESIZE_MODE:
+                        r = parse_auto_resize_mode_field(match_identity ?: &arg_identity_extra,
+                                                         "autoResizeMode", optarg);
+                        if (r < 0)
+                                return r;
+                        break;
 
-        r = acquire_bus(&bus);
-        if (r < 0)
-                return r;
+                case ARG_REBALANCE_WEIGHT:
+                        r = parse_rebalance_weight(match_identity ?: &arg_identity_extra,
+                                                   "rebalanceWeight", optarg);
+                        if (r < 0)
+                                return r;
+                        break;
 
-        for (unsigned n_tries = 0;; n_tries++) {
-                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
-                _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+                case 'j':
+                        arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+                        break;
 
-                if (n_tries >= 5)
-                        return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
-                                               "Failed to activate home dir, even after %u tries.", n_tries);
+                case ARG_JSON:
+                        r = parse_json_argument(optarg, &arg_json_format_flags);
+                        if (r <= 0)
+                                return r;
 
-                /* Let's start by checking if this all is even necessary, i.e. if the useFallback boolean field is actually set. */
-                r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", NULL); /* empty user string means: our calling user */
-                if (r < 0)
-                        return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+                        break;
 
-                r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
-                if (r < 0)
-                        return bus_log_parse_error(r);
+                case 'E':
+                        if (arg_export_format == EXPORT_FORMAT_FULL)
+                                arg_export_format = EXPORT_FORMAT_STRIPPED;
+                        else if (arg_export_format == EXPORT_FORMAT_STRIPPED)
+                                arg_export_format = EXPORT_FORMAT_MINIMAL;
+                        else
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported.");
 
-                r = sd_json_parse(json, SD_JSON_PARSE_SENSITIVE|SD_JSON_PARSE_MUST_BE_OBJECT, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse JSON identity: %m");
+                        arg_json_format_flags &= ~SD_JSON_FORMAT_OFF;
+                        if (arg_json_format_flags == 0)
+                                arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+                        break;
 
-                hr = user_record_new();
-                if (!hr)
-                        return log_oom();
+                case ARG_EXPORT_FORMAT:
+                        if (streq(optarg, "help"))
+                                return DUMP_STRING_TABLE(export_format, ExportFormat, _EXPORT_FORMAT_MAX);
 
-                r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
-                if (r < 0)
-                        return r;
+                        arg_export_format = export_format_from_string(optarg);
+                        if (arg_export_format < 0)
+                                return log_error_errno(arg_export_format, "Invalid export format: %s", optarg);
 
-                if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */
                         break;
 
-                if (!secret) {
-                        r = acquire_passed_secrets(hr->user_name, &secret);
+                case ARG_DROP_CACHES:
+                        r = parse_boolean_field(match_identity ?: &arg_identity_extra, "dropCaches", optarg);
                         if (r < 0)
                                 return r;
-                }
-
-                for (;;) {
-                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-
-                        r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHomeIfReferenced");
-                        if (r < 0)
-                                return bus_log_create_error(r);
+                        break;
 
-                        r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */
+                case ARG_CAPABILITY_AMBIENT_SET:
+                        r = parse_capability_set_field(match_identity ?: &arg_identity_extra,
+                                                       &arg_capability_ambient_set,
+                                                       "capabilityAmbientSet", optarg);
                         if (r < 0)
-                                return bus_log_create_error(r);
+                                return r;
+                        break;
 
-                        r = bus_message_append_secret(m, secret);
+                case ARG_CAPABILITY_BOUNDING_SET:
+                        r = parse_capability_set_field(match_identity ?: &arg_identity_extra,
+                                                       &arg_capability_bounding_set,
+                                                       "capabilityBoundingSet", optarg);
                         if (r < 0)
-                                return bus_log_create_error(r);
+                                return r;
+                        break;
 
-                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
-                        if (r < 0) {
-                                if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_REFERENCED))
-                                        return log_error_errno(r, "Called without reference on home taken, can't operate.");
+                case ARG_PROMPT_NEW_USER:
+                        arg_prompt_new_user = true;
+                        break;
 
-                                r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false);
-                                if (r < 0)
-                                        return r;
+                case 'b':
+                case ARG_AVATAR:
+                case ARG_LOGIN_BACKGROUND: {
+                        _cleanup_close_ int fd = -EBADF;
+                        _cleanup_free_ char *path = NULL, *filename = NULL;
 
-                                sd_bus_error_free(&error);
-                        } else
-                                break;
-                }
+                        if (c == 'b') {
+                                char *eq;
 
-                /* Try again */
-                hr = user_record_unref(hr);
-        }
+                                if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */
+                                        hashmap_clear(arg_blob_files);
+                                        arg_blob_dir = mfree(arg_blob_dir);
+                                        arg_blob_clear = true;
+                                        break;
+                                }
 
-        incomplete = getenv_bool("XDG_SESSION_INCOMPLETE"); /* pam_systemd_home reports this state via an environment variable to us. */
-        if (incomplete < 0 && incomplete != -ENXIO)
-                return log_error_errno(incomplete, "Failed to parse $XDG_SESSION_INCOMPLETE environment variable: %m");
-        if (incomplete > 0) {
-                /* We are still in an "incomplete" session here. Now upgrade it to a full one. This will make logind
-                 * start the user@.service instance for us. */
-                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-                r = sd_bus_call_method(
-                                bus,
-                                "org.freedesktop.login1",
-                                "/org/freedesktop/login1/session/self",
-                                "org.freedesktop.login1.Session",
-                                "SetClass",
-                                &error,
-                                /* ret_reply= */ NULL,
-                                "s",
-                                "user");
-                if (r < 0)
-                        return log_error_errno(r, "Failed to upgrade session: %s", bus_error_message(&error, r));
+                                eq = strrchr(optarg, '=');
+                                if (!eq) { /* --blob=/some/path replaces the blob dir */
+                                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_blob_dir);
+                                        if (r < 0)
+                                                return r;
+                                        break;
+                                }
 
-                if (setenv("XDG_SESSION_CLASS", "user", /* overwrite= */ true) < 0) /* Update the XDG_SESSION_CLASS environment variable to match the above */
-                        return log_error_errno(errno, "Failed to set $XDG_SESSION_CLASS: %m");
+                                /* --blob=filename=/some/path replaces the file "filename" with /some/path */
+                                filename = strndup(optarg, eq - optarg);
+                                if (!filename)
+                                        return log_oom();
 
-                if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */
-                        return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m");
-        }
+                                if (isempty(filename))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg);
+                                if (!suitable_blob_filename(filename))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename);
 
-        /* We are going to invoke execv() soon. Let's be extra accurate and flush/close our bus connection
-         * first, just to make sure anything queued is flushed out (though there shouldn't be anything) */
-        bus = sd_bus_flush_close_unref(bus);
+                                r = parse_path_argument(eq + 1, /* suppress_root= */ false, &path);
+                                if (r < 0)
+                                        return r;
+                        } else {
+                                const char *well_known_filename =
+                                                  c == ARG_AVATAR ? "avatar" :
+                                        c == ARG_LOGIN_BACKGROUND ? "login-background" :
+                                                                    NULL;
+                                assert(well_known_filename);
 
-        assert(!hr->use_fallback);
-        assert_se(shell = user_record_shell(hr));
-        assert_se(hd = user_record_home_directory(hr));
+                                filename = strdup(well_known_filename);
+                                if (!filename)
+                                        return log_oom();
 
-        /* Extra protection: avoid loops */
-        if (is_fallback_shell(shell))
-                return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Primary shell of '%s' is fallback shell, refusing loop.", hr->user_name);
+                                r = parse_path_argument(optarg, /* suppress_root= */ false, &path);
+                                if (r < 0)
+                                        return r;
+                        }
 
-        if (chdir(hd) < 0)
-                return log_error_errno(errno, "Failed to change directory to home directory '%s': %m", hd);
+                        if (path) {
+                                fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+                                if (fd < 0)
+                                        return log_error_errno(errno, "Failed to open %s: %m", path);
 
-        if (setenv("SHELL", shell, /* overwrite= */ true) < 0)
-                return log_error_errno(errno, "Failed to set $SHELL: %m");
+                                if (fd_verify_regular(fd) < 0)
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path);
+                        } else
+                                fd = -EBADF; /* Delete the file */
 
-        if (setenv("HOME", hd, /* overwrite= */ true) < 0)
-                return log_error_errno(errno, "Failed to set $HOME: %m");
+                        r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd));
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename);
+                        TAKE_PTR(filename); /* hashmap takes ownership */
+                        TAKE_FD(fd);
 
-        /* Paranoia: in case the client passed some passwords to us to help us unlock, unlock things now */
-        FOREACH_STRING(ue, "PASSWORD", "NEWPASSWORD", "PIN")
-                if (unsetenv(ue) < 0)
-                        return log_error_errno(errno, "Failed to unset $%s: %m", ue);
+                        break;
+                }
 
-        r = path_extract_filename(shell, &argv0);
-        if (r < 0)
-                return log_error_errno(r, "Unable to extract file name from '%s': %m", shell);
-        if (r == O_DIRECTORY)
-                return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Shell '%s' is a path to a directory, refusing.", shell);
+                case ARG_TMP_LIMIT:
+                case ARG_DEV_SHM_LIMIT: {
+                        const char *field =
+                                    c == ARG_TMP_LIMIT ? "tmpLimit" :
+                                c == ARG_DEV_SHM_LIMIT ? "devShmLimit" :
+                                                         NULL;
+                        const char *field_scale =
+                                    c == ARG_TMP_LIMIT ? "tmpLimitScale" :
+                                c == ARG_DEV_SHM_LIMIT ? "devShmLimitScale" :
+                                                         NULL;
 
-        /* Invoke this as login shell, by setting argv[0][0] to '-' (unless we ourselves weren't called as login shell) */
-        if (!argv || isempty(argv[0]) || argv[0][0] == '-') {
-                _cleanup_free_ char *prefixed = strjoin("-", argv0);
-                if (!prefixed)
-                        return log_oom();
+                        assert(field);
+                        assert(field_scale);
 
-                free_and_replace(argv0, prefixed);
-        }
+                        r = parse_tmpfs_limit_field(match_identity ?: &arg_identity_extra,
+                                                    field, field_scale, optarg);
+                        if (r < 0)
+                                return r;
+                        break;
+                }
 
-        l = strv_new(argv0);
-        if (!l)
-                return log_oom();
+                case ARG_DEFAULT_AREA:
+                        r = parse_filename_field(match_identity ?: &arg_identity_extra, "defaultArea", optarg);
+                        if (r < 0)
+                                return r;
+                        break;
 
-        if (strv_extend_strv(&l, strv_skip(argv, 1), /* filter_duplicates= */ false) < 0)
-                return log_oom();
+                case ARG_KEY_NAME:
+                        if (!isempty(optarg) && !filename_is_valid(optarg))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Parameter for --key-name= not a valid filename: %s", optarg);
 
-        execv(shell, l);
-        return log_error_errno(errno, "Failed to execute shell '%s': %m", shell);
-}
+                        r = free_and_strdup_warn(&arg_key_name, empty_to_null(optarg));
+                        if (r < 0)
+                                return r;
+                        break;
 
-static int verb_list_signing_keys(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        int r;
+                case ARG_SEIZE:
+                        r = parse_boolean_argument("--seize=", optarg, &arg_seize);
+                        if (r < 0)
+                                return r;
+                        break;
 
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        r = acquire_bus(&bus);
-        if (r < 0)
-                return r;
+                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;
 
-        _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));
+                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;
 
-        _cleanup_(table_unrefp) Table *table = table_new("name", "key");
-        if (!table)
-                return log_oom();
+                case ARG_PROMPT_SHELL:
+                        r = parse_boolean_argument("--prompt-shell=", optarg, &arg_prompt_shell);
+                        if (r < 0)
+                                return r;
 
-        r = sd_bus_message_enter_container(reply, 'a', "(sst)");
-        if (r < 0)
-                return bus_log_parse_error(r);
+                        break;
 
-        for (;;) {
-                const char *name, *pem;
+                case ARG_PROMPT_GROUPS:
+                        r = parse_boolean_argument("--prompt-groups=", optarg, &arg_prompt_groups);
+                        if (r < 0)
+                                return r;
 
-                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. */
-
-                        r = dlopen_libcrypto(LOG_DEBUG);
+                case ARG_CHROME:
+                        r = parse_boolean_argument("--chrome=", optarg, &arg_chrome);
                         if (r < 0)
                                 return r;
 
-                        _cleanup_(EVP_PKEY_freep) EVP_PKEY *key = NULL;
-                        r = openssl_pubkey_from_pem(pem, SIZE_MAX, &key);
+                        break;
+
+                case ARG_MUTE_CONSOLE:
+                        r = parse_boolean_argument("--mute-console=", optarg, &arg_mute_console);
                         if (r < 0)
-                                return log_error_errno(r, "Failed to parse PEM: %m");
+                                return r;
 
-                        _cleanup_free_ void *der = NULL;
-                        int n = sym_i2d_PUBKEY(key, (unsigned char**) &der);
-                        if (n < 0)
-                                return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to encode key as DER.");
+                        break;
 
-                        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, glyph(GLYPH_ELLIPSIS)))
-                                        return log_oom();
-                }
+                case '?':
+                        return -EINVAL;
 
-                r = table_add_many(
-                                table,
-                                TABLE_STRING, name,
-                                TABLE_STRING, h ?: pem);
-                if (r < 0)
-                        return table_log_add_error(r);
+                default:
+                        assert_not_reached();
+                }
         }
 
-        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);
+        if (!strv_isempty(arg_languages)) {
+                char **additional;
 
-                r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
+                r = sd_json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]);
                 if (r < 0)
-                        return r;
-        }
+                        return log_error_errno(r, "Failed to update preferred language: %m");
 
-        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);
+                additional = strv_skip(arg_languages, 1);
+                if (!strv_isempty(additional)) {
+                        r = sd_json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to update additional language list: %m");
+                } else {
+                        r = drop_from_identity("additionalLanguages");
+                        if (r < 0)
+                                return r;
+                }
         }
 
-        return 0;
+        return 1;
 }
 
-static int verb_get_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        int r;
+static int redirect_bus_mgr(void) {
+        const char *suffix;
 
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        r = acquire_bus(&bus);
-        if (r < 0)
-                return r;
+        /* Talk to a different service if that's requested. (The same env var is also understood by homed, so
+         * that it is relatively easily possible to invoke a second instance of homed for debug purposes and
+         * have homectl talk to it, without colliding with the host version. This is handy when operating
+         * from a homed-managed account.) */
 
-        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;
-                }
+        suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+        if (suffix) {
+                static BusLocator locator = {
+                        .path = "/org/freedesktop/home1",
+                        .interface = "org.freedesktop.home1.Manager",
+                };
 
-                const char *pem;
-                r = sd_bus_message_read(reply, "st", &pem, NULL);
-                if (r < 0) {
-                        RET_GATHER(ret, bus_log_parse_error(r));
-                        continue;
-                }
+                /* Yes, we leak this memory, but there's little point to collect this, given that we only do
+                 * this in a debug environment, do it only once, and the string shall live for out entire
+                 * process runtime. */
 
-                fputs(pem, stdout);
-                if (!endswith(pem, "\n"))
-                        fputc('\n', stdout);
+                locator.destination = strjoin("org.freedesktop.home1.", suffix);
+                if (!locator.destination)
+                        return log_oom();
 
-                fflush(stdout);
-        }
+                bus_mgr = &locator;
+        } else
+                bus_mgr = bus_home_mgr;
 
-        return ret;
+        return 0;
 }
 
-static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) {
-        int r;
+static bool is_fallback_shell(const char *p) {
+        const char *q;
 
-        assert_se(bus);
-        assert_se(fn);
-        assert_se(key);
+        if (!p)
+                return false;
 
-        _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);
+        if (p[0] == '-') {
+                /* Skip over login shell dash */
+                p++;
 
-        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-        r = bus_call_method(bus, bus_mgr, "AddSigningKey", &error, /* ret_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));
+                if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */
+                        return true;
+        }
 
-        return 0;
-}
+        q = strrchr(p, '/'); /* Skip over path */
+        if (q)
+                p = q + 1;
 
-static int verb_add_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        int r;
+        return streq(p, "systemd-home-fallback-shell");
+}
 
+static int fallback_shell(int argc, char *argv[]) {
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL, *hr = NULL;
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_strv_free_ char **l = NULL;
+        _cleanup_free_ char *argv0 = NULL;
+        const char *json, *hd, *shell;
+        int r, incomplete;
+
+        /* So here's the deal: if users log into a system via ssh, and their homed-managed home directory
+         * wasn't activated yet, SSH will permit the access but the home directory isn't actually available
+         * yet. SSH doesn't allow us to ask authentication questions from the PAM session stack, and doesn't
+         * run the PAM authentication stack (because it authenticates via its own key management, after
+         * all). So here's our way to support this: homectl can be invoked as a multi-call binary under the
+         * name "systemd-home-fallback-shell". If so, it will chainload a login shell, but first try to
+         * unlock the home directory of the user it is invoked as. systemd-homed will then override the shell
+         * listed in user records whose home directory is not activated yet with this pseudo-shell. Net
+         * effect: one SSH auth succeeds this pseudo shell gets invoked, which will unlock the homedir
+         * (possibly asking for a passphrase) and then chainload the regular shell. Once the login is
+         * complete the user record will look like any other. */
+
         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.");
+        for (unsigned n_tries = 0;; n_tries++) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+                _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
 
-                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.");
+                if (n_tries >= 5)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                               "Failed to activate home dir, even after %u tries.", n_tries);
 
-                STRV_FOREACH(k, strv_skip(argv, 1)) {
+                /* Let's start by checking if this all is even necessary, i.e. if the useFallback boolean field is actually set. */
+                r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", NULL); /* empty user string means: our calling user */
+                if (r < 0)
+                        return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
 
-                        if (streq(*k, "-"))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified.");
+                r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+                if (r < 0)
+                        return bus_log_parse_error(r);
 
-                        _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;
-                                }
-                        }
+                r = sd_json_parse(json, SD_JSON_PARSE_SENSITIVE|SD_JSON_PARSE_MUST_BE_OBJECT, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse JSON identity: %m");
 
-                        _cleanup_fclose_ FILE *f = fopen(*k, "re");
-                        if (!f) {
-                                RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k));
-                                continue;
-                        }
+                hr = user_record_new();
+                if (!hr)
+                        return log_oom();
 
-                        RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f));
-                }
-        }
+                r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
+                if (r < 0)
+                        return r;
 
-        return ret;
-}
+                if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */
+                        break;
 
-static int add_signing_keys_from_credentials(void) {
-        int r;
+                if (!secret) {
+                        r = acquire_passed_secrets(hr->user_name, &secret);
+                        if (r < 0)
+                                return 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");
+                for (;;) {
+                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
 
-        _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");
+                        r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHomeIfReferenced");
+                        if (r < 0)
+                                return bus_log_create_error(r);
 
-        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;
+                        r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */
+                        if (r < 0)
+                                return bus_log_create_error(r);
 
-                const char *e = startswith(de->d_name, "home.add-signing-key.");
-                if (!e)
-                        continue;
+                        r = bus_message_append_secret(m, secret);
+                        if (r < 0)
+                                return bus_log_create_error(r);
 
-                if (!filename_is_valid(e))
-                        continue;
+                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+                        if (r < 0) {
+                                if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_REFERENCED))
+                                        return log_error_errno(r, "Called without reference on home taken, can't operate.");
 
-                if (!bus) {
-                        r = acquire_bus(&bus);
-                        if (r < 0)
-                                return r;
-                }
+                                r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false);
+                                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;
+                                sd_bus_error_free(&error);
+                        } else
+                                break;
                 }
 
-                RET_GATHER(ret, add_signing_key_one(bus, e, f));
+                /* Try again */
+                hr = user_record_unref(hr);
         }
 
-        return ret;
-}
+        incomplete = getenv_bool("XDG_SESSION_INCOMPLETE"); /* pam_systemd_home reports this state via an environment variable to us. */
+        if (incomplete < 0 && incomplete != -ENXIO)
+                return log_error_errno(incomplete, "Failed to parse $XDG_SESSION_INCOMPLETE environment variable: %m");
+        if (incomplete > 0) {
+                /* We are still in an "incomplete" session here. Now upgrade it to a full one. This will make logind
+                 * start the user@.service instance for us. */
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                r = sd_bus_call_method(
+                                bus,
+                                "org.freedesktop.login1",
+                                "/org/freedesktop/login1/session/self",
+                                "org.freedesktop.login1.Session",
+                                "SetClass",
+                                &error,
+                                /* ret_reply= */ NULL,
+                                "s",
+                                "user");
+                if (r < 0)
+                        return log_error_errno(r, "Failed to upgrade session: %s", bus_error_message(&error, r));
 
-static int remove_signing_key_one(sd_bus *bus, const char *fn) {
-        int r;
+                if (setenv("XDG_SESSION_CLASS", "user", /* overwrite= */ true) < 0) /* Update the XDG_SESSION_CLASS environment variable to match the above */
+                        return log_error_errno(errno, "Failed to set $XDG_SESSION_CLASS: %m");
 
-        assert_se(bus);
-        assert_se(fn);
+                if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */
+                        return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m");
+        }
 
-        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
-        r = bus_call_method(bus, bus_mgr, "RemoveSigningKey", &error, /* ret_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));
+        /* We are going to invoke execv() soon. Let's be extra accurate and flush/close our bus connection
+         * first, just to make sure anything queued is flushed out (though there shouldn't be anything) */
+        bus = sd_bus_flush_close_unref(bus);
 
-        return 0;
-}
+        assert(!hr->use_fallback);
+        assert_se(shell = user_record_shell(hr));
+        assert_se(hd = user_record_home_directory(hr));
 
-static int verb_remove_signing_key(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        int r;
+        /* Extra protection: avoid loops */
+        if (is_fallback_shell(shell))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Primary shell of '%s' is fallback shell, refusing loop.", hr->user_name);
 
-        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
-        r = acquire_bus(&bus);
+        if (chdir(hd) < 0)
+                return log_error_errno(errno, "Failed to change directory to home directory '%s': %m", hd);
+
+        if (setenv("SHELL", shell, /* overwrite= */ true) < 0)
+                return log_error_errno(errno, "Failed to set $SHELL: %m");
+
+        if (setenv("HOME", hd, /* overwrite= */ true) < 0)
+                return log_error_errno(errno, "Failed to set $HOME: %m");
+
+        /* Paranoia: in case the client passed some passwords to us to help us unlock, unlock things now */
+        FOREACH_STRING(ue, "PASSWORD", "NEWPASSWORD", "PIN")
+                if (unsetenv(ue) < 0)
+                        return log_error_errno(errno, "Failed to unset $%s: %m", ue);
+
+        r = path_extract_filename(shell, &argv0);
         if (r < 0)
-                return r;
+                return log_error_errno(r, "Unable to extract file name from '%s': %m", shell);
+        if (r == O_DIRECTORY)
+                return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Shell '%s' is a path to a directory, refusing.", shell);
 
-        r = EXIT_SUCCESS;
-        STRV_FOREACH(k, strv_skip(argv, 1))
-                RET_GATHER(r, remove_signing_key_one(bus, *k));
+        /* Invoke this as login shell, by setting argv[0][0] to '-' (unless we ourselves weren't called as login shell) */
+        if (!argv || isempty(argv[0]) || argv[0][0] == '-') {
+                _cleanup_free_ char *prefixed = strjoin("-", argv0);
+                if (!prefixed)
+                        return log_oom();
 
-        return r;
+                free_and_replace(argv0, prefixed);
+        }
+
+        l = strv_new(argv0);
+        if (!l)
+                return log_oom();
+
+        if (strv_extend_strv(&l, strv_skip(argv, 1), /* filter_duplicates= */ false) < 0)
+                return log_oom();
+
+        execv(shell, l);
+        return log_error_errno(errno, "Failed to execute shell '%s': %m", shell);
 }
 
 static int run(int argc, char *argv[]) {