diff --git a/TODO b/TODO index 1aeae13ab9..773edc5590 100644 --- a/TODO +++ b/TODO @@ -157,6 +157,10 @@ Features: also use this to detect unclean shutdowns, boot into special target if detected +* fix homed/homectl confusion around terminology, i.e. "home directory" + vs. "home" vs. "home area". Stick to one term for the concept, and it + probably shouldn't contain "area". + * sd-boot: do something useful if we find exactly zero entries (ignoring items such as reboot/poweroff/factory reset). Show a help text or so. diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 4b2289e646..c340e01d01 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -615,6 +615,11 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \ there already exists at least one regular user on the system. If set to "0" will make the tool skip any such query. +* `$SYSTEMD_HOME_DRY_RUN` – if set to "1" will make `homectl create` and + `homectl update` operate in a "dry-run" mode: the new user record is + assembled, and displayed in JSON format, but not actually passed to + `systemd-homed` for execution of the operation. + `kernel-install`: * `$KERNEL_INSTALL_BYPASS` – If set to "1", execution of kernel-install is skipped diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 24075dc1b1..7a6c5cf9f8 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -774,14 +774,23 @@ If any of the specified IDs match the system's local machine ID (As a special case, if only a single machine ID is listed this field may be a single string rather than an array of strings.) +`matchNotMachineId` → Similar to `matchMachineId` but implements the inverse +match: this section only applies if the local machine ID does *not* match any +of the listed IDs. + `matchHostname` → An array of strings that are valid hostnames. If any of the specified hostnames match the system's local hostname, the fields in this object are honored. -If both `matchHostname` and `matchMachineId` are used within the same array entry, the object is honored when either match succeeds, -i.e. the two match types are combined in OR, not in AND. (As a special case, if only a single hostname is listed this field may be a single string rather than an array of strings.) -These two are the only two fields specific to this section. -All other fields that may be used in this section are identical to the equally named ones in the +`matchNotHostname` → Similar to `matchHostname`, but implement the inverse +match, as above. + +If any of these four fields are used within the same array entry, the object is +honored when either match succeeds, i.e. the match types are combined in OR, +not in AND. + +These four are the only fields specific to this section. All other fields that +may be used in this section are identical to the equally named ones in the `regular` section (i.e. at the top-level object). Specifically, these are: `blobDirectory`, `blobManifest`, `iconName`, `location`, `shell`, `umask`, diff --git a/man/homectl.xml b/man/homectl.xml index abcdd88529..5c37f1bda9 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -179,6 +179,66 @@ + + + + When used with the add-signing-key command, specify or override + the name under which to store the public key being added. The specified name can be chosen freely, + but must be suffixed with .public. If this option is not used the name is derived + from the specified filename. If a key is read from standard input this option is mandatory in order + to provide a suitable name for the key being added. + + + + + + + + Takes a boolean argument. When used with create or + register, controls whether to strip cryptographic signatures from the provided + JSON user records, which has the effect of signing them with the local signing key + (local.public) instead. If this switch is set to true, added user records + hence become locally managed (and thus can be modified locally), while if it is set to false the user + records remain managed and owned by its origin (and thus cannot be modified locally). This switch + defaults to true for create and false for register. + + + + + + + + + + + Takes one of this, other, + any or auto. Some user record settings can be defined to match + only specific machines, or all machines but one, or all machines. With this switch it is possibly to + control to which machines to apply the settings appearing on the command line after it. If + this is specified the setting will only apply to the local system (positive + match), if other it will apply to all but the local system (negative match), if + any it will apply to all systems (unless there's a matching positive or negative + per-machine setting). If auto returns to the default logic: whether a setting + applies by default to the local system or all systems depends on the option in question. + + Note that only some user record settings can be conditioned like this. This option has no + effect on the others and is ignored there. This option may appear multiple times in a single command + line to apply settings conditioned by different matches to the same user record. See JSON User Records for details on which settings may be + used with such per-machine matching and which ones may not. + + is a shortcut for , is + short for and is short for + . + + Here's an example call that sets the storage field to luks on the local + system, but to cifs on all others: + + # homectl update lennart -T --storage=luks -N --storage=cifs + + + + @@ -1172,6 +1232,68 @@ + + adopt PATH [PATH…] + + Adopts one or more existing home directories on the local system. Takes one or more paths to + *.home LUKS home directories or *.homedir/ standalone home + directories or subvolumes previously created by systemd-homed and makes them + available locally for login. The referenced files are not moved. This is an alternative for moving + such home directories into /home/ (where they would be picked up + automatically). + + + + + + register FILE [FILE…] + + Registers one or more users, without creating their home directories. Takes one or + more paths to JSON user record files. If the path is specified as - reads the + JSON user record from standard input. + + Registering a user makes it accessible on the local system without creating a new home + directory. This is particularly useful for making a user accessible on a system it was not originally + created on. + + Here's an example how to make a local user account with its home directory accessible on a + remote system, using SMB/CIFS file sharing. With Samba installed in its default configuration invoke + as root: + + # smbpasswd -a lennart + + Continue as regular user lennart: + +$ homectl update lennart --ssh-authorized-keys=… -N --storage=cifs --cifs-service="//$HOSTNAME/lennart" +$ homectl get-signing-key | ssh targetsystem homectl add-signing-key --key-name="$HOSTNAME".public +$ homectl inspect -E lennart | ssh targetsystem homectl register - +$ ssh lennart@targetsystem + + This first ensures the user account lennart is known to and accessible by + Samba. It then registers a local SSH access that shall be used for accessing this user, and + configures CIFS as default storage for non-local systems on the account. It then adds the local + system's account signing key to the target system. Then it registers the local user account with the + target system. Finally it logs into the account on the target system. The target system will then + connect back via SMB/CIFS to access the home directory. + + + + + + unregister USER + + Unregisters one or more user accounts. This only removes the user record from the + local system, it does not delete the home directory. The home directory can be readded via the + register or adopt command later, on this or another + system. Note that unregistering a user whose home directory is placed in /home/ + will not make the user disappear from the local user database, as all supported home directories + placed there will show up in the user database. However, the user record will become "unfixated", + i.e. lose its binding to the local system. When logged into it will automatically regain the binding, + and acquire a local UID/GID pair. + + + + remove USER @@ -1311,6 +1433,55 @@ + + + list-signing-keys + + Show a list of public keys that home directories can be signed with to be allowed for + local login. One such key (local.public) will be generated automatically for + signing locally created home directories, but additional public keys may be registered to accept home + directories from other origins too (see add-signing-key below). + + + + + + get-signing-key [NAME…] + + Write the public key identified by the specified name to standard output (in PEM + format). If no name is specified defaults to local.public, i.e. the + automatically generated key for locally created home directories. + + + + + + add-signing-key [FILE…] + + Add public key(s) from the specified PEM key file(s) to the list of keys that home + areas have to be signed by to be permitted for local login. If a path of - is + specified, or if no file is specified at all, the key will be read from standard input. The key file + name(s) must carry the .public suffix, and the file name(s) will be used to name + the key(s) once added, too. If a key is added from standard input the key name must be specified + explicitly via , see above. + + This command is useful for permitting local home directories to be used on a remote + system. Example: + + homectl get-signing-key | ssh myotherhost homectl add-signing-key --key-name="$HOSTNAME".public + + + + + + remove-signing-key NAME… + + Remove the public key identified by the specified name from the list of keys that + control from which origins to permit home directories for login. + + + + diff --git a/man/org.freedesktop.home1.xml b/man/org.freedesktop.home1.xml index 403778288d..1d73e78d39 100644 --- a/man/org.freedesktop.home1.xml +++ b/man/org.freedesktop.home1.xml @@ -70,6 +70,8 @@ node /org/freedesktop/home1 { @org.freedesktop.systemd1.Privileged("true") DeactivateHome(in s user_name); RegisterHome(in s user_record); + AdoptHome(in s image_path, + in t flags); UnregisterHome(in s user_name); CreateHome(in s user_record); CreateHomeEx(in s user_record, @@ -112,6 +114,15 @@ node /org/freedesktop/home1 { out h send_fd); @org.freedesktop.systemd1.Privileged("true") ReleaseHome(in s user_name); + ListSigningKeys(out a(sst) keys); + GetSigningKey(in s name, + out s der, + out t flags); + AddSigningKey(in s name, + in s pem, + in t flags); + RemoveSigningKey(in s name, + in t flags); @org.freedesktop.systemd1.Privileged("true") LockAllHomes(); @org.freedesktop.systemd1.Privileged("true") @@ -151,6 +162,8 @@ node /org/freedesktop/home1 { + + @@ -185,6 +198,14 @@ node /org/freedesktop/home1 { + + + + + + + + @@ -257,6 +278,12 @@ node /org/freedesktop/home1 { is useful to register home directories locally that are not located where systemd-homed.service would find them automatically. + AdoptHome() also registers a new home directory locally. It takes a path to + a home directory itself, and will register it locally. This only works for *.home + and *.homedir/ home directories. This operation is done automatically for all such + home areas showing up in /home/, but may be requested explicitly with this call for + directories elsewhere. The flags must be set to zero, currently. + UnregisterHome() unregisters an existing home directory. It takes a user name as argument and undoes what RegisterHome() does. It does not attempt to remove the home directory itself, it just unregisters it with the local system. Note that if the home @@ -426,6 +453,23 @@ node /org/freedesktop/home1 { Rebalance() synchronously rebalances free disk space between home areas. This only executes an operation if at least one home area using the LUKS2 backend is active and has rebalancing enabled, and is otherwise a NOP. + + ListSigningKeys() acquires a list of installed home area signing + keys. Returns an array of key names with their PEM encoded public key data. Each entry also comes with + a flags value which is currently unused and should be ignored by clients. + + GetSigningKey() acquires the PEM encoded public part of the specified home + area signing key of the specified name. Also returns a currently unused flags value that should be + ignored. The flags parameter must be set to zero, currently. + + AddSigningKey() adds a new key to the list of home area signing keys. Takes + a name string (free-form, suitable as filename, with suffix .public), the PEM + encoded public key data and a currently unused flags value that must be zero. The + flags parameter must be set to zero, currently. + + RemoveSigningKey() removes a key from the list of home area signing + keys. Takes the name of the key to remove and a currently unused flags value that must be zero. The + flags parameter must be set to zero, currently. @@ -599,6 +643,9 @@ node /org/freedesktop/home1/home { The Manager Object ActivateHomeIfReferenced(), RefHomeUnrestricted(), CreateHomeEx(), and UpdateHomeEx() were added in version 256. + AdoptHome(), ListSigningKeys(), + GetSigningKey(), AddSigningKey(), and + RemoveSigningKey() were added in version 258. Home Objects diff --git a/man/systemd-homed.service.xml b/man/systemd-homed.service.xml index 42e5780ccc..ca96e5d196 100644 --- a/man/systemd-homed.service.xml +++ b/man/systemd-homed.service.xml @@ -107,6 +107,22 @@ generated/signed before the key pair is copied in, lose their validity. + + Signals + + + + SIGUSR1 + + Upon reception of the SIGUSR1 process signal + systemd-homed will reestablish its file watches on /home/ and + rescan the directory for home directories. + + + + + + See Also diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index aa974837cd..5981986548 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -378,16 +378,41 @@ + + home.add-signing-key.* + + Adds a new signing key for user records to the system. The credential contents should contain + a user signing key, for example as reported by homectl get-signing-key. Multiple + keys may be specified, and they will be put in place under the name of the credential name suffix + (which must itself carry the .public suffix). For details see + homectl1. + + + + + home.create.* - Creates a home area for the specified user with the user record data passed in. For details see + Creates a new home area for the specified user with the user record data passed in. For + details see homectl1. + + home.register.* + + Registers an existing home area for the specified user with the user record data passed in. For details + see + homectl1. + + + + + cryptsetup.passphrase cryptsetup.tpm2-pin diff --git a/man/userdbctl.xml b/man/userdbctl.xml index f7b0c1d9eb..2db590fa72 100644 --- a/man/userdbctl.xml +++ b/man/userdbctl.xml @@ -243,6 +243,19 @@ + + + + + When used with the user or group command, read + the user definition in JSON format from the specified file, instead of querying it from the + system. If the path is specified as -, reads the JSON data from standard + input. This is useful to validate and introspect JSON user or group records quickly, and check how + they would be interpreted on the local system. + + + + @@ -263,6 +276,9 @@ List all known users records or show details of one or more specified user records. Use to tweak output mode. + If used in conjuntion with the user record data is read in JSON + format from the specified file instead of querying it from the system. For details see above. + @@ -272,6 +288,9 @@ List all known group records or show details of one or more specified group records. Use to tweak the output mode. + If used in conjuntion with the group record data is read in JSON + format from the specified file instead of querying it from the system. For details see above. + diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl index 5e2235bc3b..2fed45403f 100644 --- a/shell-completion/bash/homectl +++ b/shell-completion/bash/homectl @@ -41,7 +41,7 @@ _homectl() { local -A OPTS=( [STANDALONE]='-h --help --version --no-pager --no-legend --no-ask-password - -j -E -P' + -j -E -P -A -N -T' [ARG]=' -H --host -M --machine --identity @@ -112,7 +112,10 @@ _homectl() { --avatar --login-background --session-launcher - --session-type' + --session-type + --key-name + --seize + --match' ) if __contains_word "$prev" ${OPTS[ARG]}; then @@ -172,7 +175,7 @@ _homectl() { fi local -A VERBS=( - [STANDALONE]='list lock-all' + [STANDALONE]='list lock-all register unregister adopt' [CREATE]='create' [NAMES]='activate deactivate inspect authenticate remove lock unlock' [NAME]='update passwd' diff --git a/shell-completion/bash/userdbctl b/shell-completion/bash/userdbctl index da1419b175..b068fe81fd 100644 --- a/shell-completion/bash/userdbctl +++ b/shell-completion/bash/userdbctl @@ -44,7 +44,7 @@ _userdbctl () { [STANDALONE]='-h --help --version --no-pager --no-legend -j -N --chain -z --fuzzy -I -S -R -B' [ARG]='--output -s --service --with-nss --synthesize --with-dropin --with-varlink - --multiplexer --json --uid-min --uid-max --disposition --boundaries' + --multiplexer --json --uid-min --uid-max --disposition --boundaries --from-file' ) if __contains_word "$prev" ${OPTS[ARG]}; then diff --git a/src/home/homectl.c b/src/home/homectl.c index a7754c2299..291ce8eaf1 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -24,6 +24,7 @@ #include "fs-util.h" #include "glyph-util.h" #include "hashmap.h" +#include "hexdecoct.h" #include "home-util.h" #include "homectl-fido2.h" #include "homectl-pkcs11.h" @@ -33,6 +34,7 @@ #include "locale-util.h" #include "main-func.h" #include "memory-util.h" +#include "openssl-util.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -68,6 +70,7 @@ static const char *arg_identity = NULL; static sd_json_variant *arg_identity_extra = NULL; static sd_json_variant *arg_identity_extra_privileged = NULL; static sd_json_variant *arg_identity_extra_this_machine = NULL; +static sd_json_variant *arg_identity_extra_other_machines = NULL; static sd_json_variant *arg_identity_extra_rlimits = NULL; static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */ static char **arg_identity_filter_rlimits = NULL; @@ -96,9 +99,13 @@ static bool arg_prompt_new_user = false; static char *arg_blob_dir = NULL; static bool arg_blob_clear = false; static Hashmap *arg_blob_files = NULL; +static char *arg_key_name = NULL; +static bool arg_dry_run = false; +static bool arg_seize = true; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_other_machines, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, sd_json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); @@ -107,6 +114,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep); STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep); +STATIC_DESTRUCTOR_REGISTER(arg_key_name, freep); static const BusLocator *bus_mgr; @@ -116,6 +124,7 @@ static bool identity_properties_specified(void) { !sd_json_variant_is_blank_object(arg_identity_extra) || !sd_json_variant_is_blank_object(arg_identity_extra_privileged) || !sd_json_variant_is_blank_object(arg_identity_extra_this_machine) || + !sd_json_variant_is_blank_object(arg_identity_extra_other_machines) || !sd_json_variant_is_blank_object(arg_identity_extra_rlimits) || !strv_isempty(arg_identity_filter) || !strv_isempty(arg_identity_filter_rlimits) || @@ -926,7 +935,7 @@ static int apply_identity_changes(sd_json_variant **_v) { if (r < 0) return log_error_errno(r, "Failed to merge identities: %m"); - if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) { + if (arg_identity_extra_this_machine || arg_identity_extra_other_machines || !strv_isempty(arg_identity_filter)) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *per_machine = NULL, *mmid = NULL; sd_id128_t mid; @@ -940,7 +949,7 @@ static int apply_identity_changes(sd_json_variant **_v) { per_machine = sd_json_variant_ref(sd_json_variant_by_key(v, "perMachine")); if (per_machine) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *npm = NULL, *add = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *npm = NULL, *positive = NULL, *negative = NULL; _cleanup_free_ sd_json_variant **array = NULL; sd_json_variant *z; size_t i = 0; @@ -948,7 +957,7 @@ static int apply_identity_changes(sd_json_variant **_v) { if (!sd_json_variant_is_array(per_machine)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing."); - array = new(sd_json_variant*, sd_json_variant_elements(per_machine) + 1); + array = new(sd_json_variant*, sd_json_variant_elements(per_machine) + 2); if (!array) return log_oom(); @@ -961,31 +970,41 @@ static int apply_identity_changes(sd_json_variant **_v) { array[i++] = z; u = sd_json_variant_by_key(z, "matchMachineId"); - if (!u) - continue; + if (u && sd_json_variant_equal(u, mmid)) + r = sd_json_variant_merge_object(&positive, z); + else { + u = sd_json_variant_by_key(z, "matchNotMachineId"); + if (!u || !sd_json_variant_equal(u, mmid)) + continue; - if (!sd_json_variant_equal(u, mmid)) - continue; - - r = sd_json_variant_merge_object(&add, z); + r = sd_json_variant_merge_object(&negative, z); + } if (r < 0) return log_error_errno(r, "Failed to merge perMachine entry: %m"); i--; } - r = sd_json_variant_filter(&add, arg_identity_filter); + r = sd_json_variant_filter(&positive, arg_identity_filter); if (r < 0) return log_error_errno(r, "Failed to filter perMachine: %m"); - r = sd_json_variant_merge_object(&add, arg_identity_extra_this_machine); + r = sd_json_variant_filter(&negative, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter perMachine: %m"); + + r = sd_json_variant_merge_object(&positive, arg_identity_extra_this_machine); + if (r < 0) + return log_error_errno(r, "Failed to merge in perMachine fields: %m"); + + r = sd_json_variant_merge_object(&negative, arg_identity_extra_other_machines); if (r < 0) return log_error_errno(r, "Failed to merge in perMachine fields: %m"); if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *rlv = NULL; - rlv = sd_json_variant_ref(sd_json_variant_by_key(add, "resourceLimits")); + rlv = sd_json_variant_ref(sd_json_variant_by_key(positive, "resourceLimits")); r = sd_json_variant_filter(&rlv, arg_identity_filter_rlimits); if (r < 0) @@ -996,22 +1015,30 @@ static int apply_identity_changes(sd_json_variant **_v) { return log_error_errno(r, "Failed to set resource limits: %m"); if (sd_json_variant_is_blank_object(rlv)) { - r = sd_json_variant_filter(&add, STRV_MAKE("resourceLimits")); + r = sd_json_variant_filter(&positive, STRV_MAKE("resourceLimits")); if (r < 0) return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); } else { - r = sd_json_variant_set_field(&add, "resourceLimits", rlv); + r = sd_json_variant_set_field(&positive, "resourceLimits", rlv); if (r < 0) return log_error_errno(r, "Failed to update resource limits of identity: %m"); } } - if (!sd_json_variant_is_blank_object(add)) { - r = sd_json_variant_set_field(&add, "matchMachineId", mmid); + if (!sd_json_variant_is_blank_object(positive)) { + r = sd_json_variant_set_field(&positive, "matchMachineId", mmid); if (r < 0) return log_error_errno(r, "Failed to set matchMachineId field: %m"); - array[i++] = add; + array[i++] = positive; + } + + if (!sd_json_variant_is_blank_object(negative)) { + r = sd_json_variant_set_field(&negative, "matchNotMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchNotMachineId field: %m"); + + array[i++] = negative; } r = sd_json_variant_new_array(&npm, array, i); @@ -1021,21 +1048,34 @@ static int apply_identity_changes(sd_json_variant **_v) { sd_json_variant_unref(per_machine); per_machine = TAKE_PTR(npm); } else { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *item = sd_json_variant_ref(arg_identity_extra_this_machine); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *positive = sd_json_variant_ref(arg_identity_extra_this_machine), + *negative = sd_json_variant_ref(arg_identity_extra_other_machines); if (arg_identity_extra_rlimits) { - r = sd_json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits); + r = sd_json_variant_set_field(&positive, "resourceLimits", arg_identity_extra_rlimits); if (r < 0) return log_error_errno(r, "Failed to update resource limits of identity: %m"); } - r = sd_json_variant_set_field(&item, "matchMachineId", mmid); - if (r < 0) - return log_error_errno(r, "Failed to set matchMachineId field: %m"); + if (positive) { + r = sd_json_variant_set_field(&positive, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); - r = sd_json_variant_append_array(&per_machine, item); - if (r < 0) - return log_error_errno(r, "Failed to append to perMachine array: %m"); + r = sd_json_variant_append_array(&per_machine, positive); + if (r < 0) + return log_error_errno(r, "Failed to append to perMachine array: %m"); + } + + if (negative) { + r = sd_json_variant_set_field(&negative, "matchNotMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchNotMachineId field: %m"); + + r = sd_json_variant_append_array(&per_machine, negative); + if (r < 0) + return log_error_errno(r, "Failed to append to perMachine array: %m"); + } } r = sd_json_variant_set_field(&v, "perMachine", per_machine); @@ -1178,7 +1218,7 @@ static int acquire_new_home_record(sd_json_variant *input, UserRecord **ret) { USER_RECORD_ALLOW_PER_MACHINE| USER_RECORD_STRIP_BINDING| USER_RECORD_STRIP_STATUS| - USER_RECORD_STRIP_SIGNATURE| + (arg_seize ? USER_RECORD_STRIP_SIGNATURE : USER_RECORD_ALLOW_SIGNATURE) | USER_RECORD_LOG| USER_RECORD_PERMISSIVE); if (r < 0) @@ -1458,6 +1498,11 @@ static int create_home_common(sd_json_variant *input, bool show_enforce_password log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r)); } + if (arg_dry_run) { + sd_json_variant_dump(hr->json, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_NEWLINE, stderr, /* prefix= */ NULL); + return 0; + } + r = acquire_bus(&bus); if (r < 0) return r; @@ -1554,6 +1599,149 @@ static int create_home(int argc, char *argv[], void *userdata) { return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ true); } +static int verb_adopt_home(int argc, char *argv[], void *userdata) { + int r, ret = 0; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "AdoptHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "st", *i, UINT64_C(0)); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to adopt home: %s", bus_error_message(&error, r)); + if (ret == 0) + ret = r; + } + } + + return ret; +} + +static int register_home_common(sd_bus *bus, sd_json_variant *v) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *_bus = NULL; + int r; + + assert(v); + + if (!bus) { + r = acquire_bus(&_bus); + if (r < 0) + return r; + bus = _bus; + } + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "RegisterHome"); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_free_ char *formatted = NULL; + r = sd_json_variant_format(v, /* flags= */ 0, &formatted); + if (r < 0) + return log_error_errno(r, "Failed to format JSON record: %m"); + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to register home: %s", bus_error_message(&error, r)); + + return 0; +} + +static int register_home_one(sd_bus *bus, FILE *f, const char *path) { + int r; + + assert(bus); + assert(path); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse_file(f, path, SD_JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse user record: %m", path, line, column); + + return register_home_common(bus, v); +} + +static int verb_register_home(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_identity) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not accepting an arguments if --identity= is specified, refusing."); + + return register_home_one(bus, /* f= */ NULL, arg_identity); + } + + if (argc == 1 || (argc == 2 && streq(argv[1], "-"))) + return register_home_one(bus, /* f= */ stdin, ""); + + r = 0; + STRV_FOREACH(i, strv_skip(argv, 1)) { + if (streq(*i, "-")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing reading from standard input if multiple user records are specified."); + + RET_GATHER(r, register_home_one(bus, /* f= */ NULL, *i)); + } + + return r; +} + +static int verb_unregister_home(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + int ret = 0; + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + r = bus_message_new_method_call(bus, &m, bus_mgr, "UnregisterHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, /* ret_reply= */ NULL); + if (r < 0) + RET_GATHER(ret, log_error_errno(r, "Failed to unregister home: %s", bus_error_message(&error, r))); + } + + return ret; +} + static int remove_home(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r, ret = 0; @@ -1756,6 +1944,11 @@ static int update_home(int argc, char *argv[], void *userdata) { if (r < 0) return r; + if (arg_dry_run) { + sd_json_variant_dump(hr->json, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_NEWLINE, stderr, /* prefix= */ NULL); + return 0; + } + /* If we do multiple operations, let's output things more verbosely, since otherwise the repeated * authentication might be confusing. */ @@ -2328,11 +2521,10 @@ static int rebalance(int argc, char *argv[], void *userdata) { return 0; } -static int create_from_credentials(void) { - _cleanup_close_ int fd = -EBADF; - int ret = 0, n_created = 0, r; +static int create_or_register_from_credentials(void) { + int r; - fd = open_credentials_dir(); + _cleanup_close_ int fd = open_credentials_dir(); if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */ return 0; if (fd < 0) @@ -2343,16 +2535,22 @@ static int create_from_credentials(void) { if (r < 0) return log_error_errno(r, "Failed to enumerate credentials: %m"); + int ret = 0, n_processed = 0; FOREACH_ARRAY(i, des->entries, des->n_entries) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *identity = NULL; struct dirent *de = *i; - const char *e; - if (de->d_type != DT_REG) continue; - e = startswith(de->d_name, "home.create."); - if (!e) + enum { + OPERATION_CREATE, + OPERATION_REGISTER, + } op; + const char *e; + if ((e = startswith(de->d_name, "home.create."))) + op = OPERATION_CREATE; + if ((e = startswith(de->d_name, "home.register."))) + op = OPERATION_REGISTER; + else continue; if (!valid_user_group_name(e, 0)) { @@ -2360,21 +2558,22 @@ static int create_from_credentials(void) { continue; } + _cleanup_(sd_json_variant_unrefp) sd_json_variant *identity = NULL; + unsigned line = 0, column = 0; r = sd_json_parse_file_at( /* f= */ NULL, fd, de->d_name, /* flags= */ 0, &identity, - /* ret_line= */ NULL, - /* ret_column= */ NULL); + &line, + &column); if (r < 0) { - log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name); + log_warning_errno(r, "[%s:%u:%u] Failed to parse user record in credential, ignoring: %m", de->d_name, line, column); continue; } - sd_json_variant *un; - un = sd_json_variant_by_key(identity, "userName"); + sd_json_variant *un = sd_json_variant_by_key(identity, "userName"); if (un) { if (!sd_json_variant_is_string(un)) { log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name); @@ -2393,14 +2592,17 @@ static int create_from_credentials(void) { log_notice("Processing user '%s' from credentials.", e); - r = create_home_common(identity, /* show_enforce_password_policy_hint= */ false); + if (op == OPERATION_CREATE) + r = create_home_common(identity, /* show_enforce_password_policy_hint= */ false); + else + r = register_home_common(/* bus= */ NULL, identity); if (r >= 0) - n_created++; + n_processed++; RET_GATHER(ret, r); } - return ret < 0 ? ret : n_created; + return ret < 0 ? ret : n_processed; } static int has_regular_user(void) { @@ -2698,6 +2900,8 @@ static int create_interactively(void) { return create_home_common(/* input= */ NULL, /* show_enforce_password_policy_hint= */ false); } +static int add_signing_keys_from_credentials(void); + static int verb_firstboot(int argc, char *argv[], void *userdata) { int r; @@ -2713,11 +2917,13 @@ static int verb_firstboot(int argc, char *argv[], void *userdata) { arg_prompt_new_user = false; } - r = create_from_credentials(); - if (r < 0) - return r; - if (r > 0) /* Already created users from credentials */ - return 0; + int ret = 0; + + 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) @@ -2726,16 +2932,21 @@ static int verb_firstboot(int argc, char *argv[], void *userdata) { if (r != -ENXIO) log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m"); - r = has_regular_user(); - if (r < 0) - return r; - if (r > 0) { - log_info("Regular user already present in user database, skipping user creation."); + if (!existing_users) { + r = has_regular_user(); + if (r < 0) + return r; + + existing_users = r > 0; + } + if (existing_users) { + log_info("Regular user already present in user database, skipping interactive user creation."); return 0; } } - return create_interactively(); + RET_GATHER(ret, create_interactively()); + return ret; } static int drop_from_identity(const char *field) { @@ -2777,23 +2988,35 @@ static int help(int argc, char *argv[], void *userdata) { printf("%1$s [OPTIONS...] COMMAND ...\n\n" "%2$sCreate, manipulate or inspect home directories.%3$s\n" - "\n%4$sCommands:%5$s\n" + "\n%4$sBasic User Manipulation Commands:%5$s\n" " list List home areas\n" - " activate USER… Activate a home area\n" - " deactivate USER… Deactivate a home area\n" " inspect USER… Inspect a home area\n" - " authenticate USER… Authenticate a home area\n" " create USER Create a home area\n" - " remove USER… Remove a home area\n" " update USER Update a home area\n" " passwd USER Change password of a home area\n" " resize USER SIZE Resize a home area\n" + " 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" - " deactivate-all Deactivate all active home areas\n" + "\n%4$sOther Commands:%5$s\n" " rebalance Rebalance free space between home areas\n" - " with USER [COMMAND…] Run shell or command with access to a home area\n" " firstboot Run first-boot home area creation wizard\n" "\n%4$sOptions:%5$s\n" " -h --help Show this help\n" @@ -2816,6 +3039,9 @@ static int help(int argc, char *argv[], void *userdata) { " -j --export-format=minimal\n" " --prompt-new-user firstboot: Query user interactively for user\n" " to create\n" + " --key-name=NAME Key name when adding a signing key\n" + " --seize=no Do not strip existing signatures of user record\n" + " when creating\n" "\n%4$sGeneral User Record Properties:%5$s\n" " -c --real-name=REALNAME Real name for user\n" " --realm=REALM Realm to create user in\n" @@ -3049,6 +3275,9 @@ static int parse_argv(int argc, char *argv[]) { ARG_TMP_LIMIT, ARG_DEV_SHM_LIMIT, ARG_DEFAULT_AREA, + ARG_KEY_NAME, + ARG_SEIZE, + ARG_MATCH, }; static const struct option options[] = { @@ -3152,18 +3381,33 @@ static int parse_argv(int argc, char *argv[]) { { "tmp-limit", required_argument, NULL, ARG_TMP_LIMIT }, { "dev-shm-limit", required_argument, NULL, ARG_DEV_SHM_LIMIT }, { "default-area", required_argument, NULL, ARG_DEFAULT_AREA }, + { "key-name", required_argument, NULL, ARG_KEY_NAME }, + { "seize", required_argument, NULL, ARG_SEIZE }, + { "match", required_argument, NULL, ARG_MATCH }, {} }; int r; + /* This points to one of arg_identity_extra, arg_identity_extra_this_machine, + * arg_identity_extra_other_machines, in order to redirect changes on the next property being set to + * this part of the identity, instead of the default. */ + sd_json_variant **match_identity = NULL; + assert(argc >= 0); assert(argv); + /* Eventually we should probably turn this into a proper --dry-run option, but as long as it is not hooked up everywhere let's make it an environment variable only. */ + r = getenv_bool("SYSTEMD_HOME_DRY_RUN"); + if (r >= 0) + arg_dry_run = r; + else if (r != -ENXIO) + log_debug_errno(r, "Unable to parse $SYSTEMD_HOME_DRY_RUN, ignoring: %m"); + for (;;) { int c; - c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL); + c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL); if (c < 0) break; @@ -3217,7 +3461,7 @@ static int parse_argv(int argc, char *argv[]) { if (!valid_gecos(optarg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "realName", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "realName", optarg); if (r < 0) return log_error_errno(r, "Failed to set realName field: %m"); @@ -3347,7 +3591,7 @@ static int parse_argv(int argc, char *argv[]) { break; } - r = sd_json_variant_set_field_string(&arg_identity_extra, field, optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, field, optarg); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -3367,7 +3611,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to validate CIFS service name: %s", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "cifsService", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "cifsService", optarg); if (r < 0) return log_error_errno(r, "Failed to set cifsService field: %m"); @@ -3403,7 +3647,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse nice level: %s", optarg); - r = sd_json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc); + r = sd_json_variant_set_field_integer(match_identity ?: &arg_identity_extra, "niceLevel", nc); if (r < 0) return log_error_errno(r, "Failed to set niceLevel field: %m"); @@ -3527,7 +3771,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_string(&arg_identity_extra_this_machine, field, v); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra_this_machine, field, v); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", v); @@ -3546,7 +3790,7 @@ static int parse_argv(int argc, char *argv[]) { if (!valid_shell(optarg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "shell", optarg); if (r < 0) return log_error_errno(r, "Failed to set shell field: %m"); @@ -3565,7 +3809,7 @@ static int parse_argv(int argc, char *argv[]) { break; } - e = sd_json_variant_by_key(arg_identity_extra, "environment"); + e = sd_json_variant_by_key(match_identity ? *match_identity: arg_identity_extra, "environment"); if (e) { r = sd_json_variant_strv(e, &l); if (r < 0) @@ -3582,7 +3826,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to allocate environment list JSON: %m"); - r = sd_json_variant_set_field(&arg_identity_extra, "environment", ne); + r = sd_json_variant_set_field(match_identity ?: &arg_identity_extra, "environment", ne); if (r < 0) return log_error_errno(r, "Failed to set environment list: %m"); @@ -3590,7 +3834,6 @@ static int parse_argv(int argc, char *argv[]) { } case ARG_TIMEZONE: - if (isempty(optarg)) { r = drop_from_identity("timeZone"); if (r < 0) @@ -3602,7 +3845,7 @@ static int parse_argv(int argc, char *argv[]) { if (!timezone_is_valid(optarg, LOG_DEBUG)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "timeZone", optarg); if (r < 0) return log_error_errno(r, "Failed to set timezone field: %m"); @@ -3682,7 +3925,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s boolean: %m", field); - r = sd_json_variant_set_field_boolean(&arg_identity_extra, field, r > 0); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, field, r > 0); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -3718,7 +3961,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "diskSize", arg_disk_size); if (r < 0) return log_error_errno(r, "Failed to set diskSize field: %m"); @@ -3731,7 +3974,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); if (r < 0) return log_error_errno(r, "Failed to set diskSizeRelative field: %m"); @@ -3739,7 +3982,7 @@ static int parse_argv(int argc, char *argv[]) { } /* Automatically turn off the rebalance logic if user configured a size explicitly */ - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "rebalanceWeight", REBALANCE_WEIGHT_OFF); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "rebalanceWeight", REBALANCE_WEIGHT_OFF); if (r < 0) return log_error_errno(r, "Failed to set rebalanceWeight field: %m"); @@ -3780,7 +4023,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg); - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "luksDiscard", r); if (r < 0) return log_error_errno(r, "Failed to set discard field: %m"); @@ -3799,7 +4042,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --luks-offline-discard= parameter: %s", optarg); - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "luksOfflineDiscard", r); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "luksOfflineDiscard", r); if (r < 0) return log_error_errno(r, "Failed to set offline discard field: %m"); @@ -3828,7 +4071,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -3850,7 +4093,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "luksSectorSize", ss); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "luksSectorSize", ss); if (r < 0) return log_error_errno(r, "Failed to set sector size field: %m"); @@ -3872,7 +4115,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse umask: %m"); - r = sd_json_variant_set_field_integer(&arg_identity_extra, "umask", m); + r = sd_json_variant_set_field_integer(match_identity ?: &arg_identity_extra, "umask", m); if (r < 0) return log_error_errno(r, "Failed to set umask field: %m"); @@ -3984,7 +4227,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %m", field); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); break; @@ -4017,7 +4260,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %m", field); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); break; @@ -4052,9 +4295,9 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg); r = sd_json_variant_set_field_string( - IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? - &arg_identity_extra_this_machine : - &arg_identity_extra, field, optarg); + match_identity ?: (IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? + &arg_identity_extra_this_machine : + &arg_identity_extra), field, optarg); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4085,7 +4328,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s field: %s", field, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, t); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, t); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4134,7 +4377,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to create group list JSON: %m"); - r = sd_json_variant_set_field(&arg_identity_extra, "memberOf", mo); + r = sd_json_variant_set_field(match_identity ?: &arg_identity_extra, "memberOf", mo); if (r < 0) return log_error_errno(r, "Failed to update group list: %m"); } @@ -4156,7 +4399,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "tasksMax", u); if (r < 0) return log_error_errno(r, "Failed to set tasksMax field: %m"); @@ -4186,7 +4429,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, field, u); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4215,7 +4458,7 @@ static int parse_argv(int argc, char *argv[]) { if (!CGROUP_WEIGHT_IS_OK(u)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, u); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4346,7 +4589,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse --auto-resize-mode= argument: %s", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "autoResizeMode", auto_resize_mode_to_string(r)); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "autoResizeMode", auto_resize_mode_to_string(r)); if (r < 0) return log_error_errno(r, "Failed to set autoResizeMode field: %m"); @@ -4380,7 +4623,7 @@ static int parse_argv(int argc, char *argv[]) { return r; /* Add to main identity */ - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "rebalanceWeight", u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "rebalanceWeight", u); if (r < 0) return log_error_errno(r, "Failed to set rebalanceWeight field: %m"); @@ -4447,7 +4690,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - r = sd_json_variant_set_field_boolean(&arg_identity_extra, "dropCaches", r); + r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "dropCaches", r); if (r < 0) return log_error_errno(r, "Failed to set drop caches field: %m"); @@ -4501,7 +4744,7 @@ static int parse_argv(int argc, char *argv[]) { if (capability_set_to_strv(updated, &l) < 0) return log_oom(); - r = sd_json_variant_set_field_strv(&arg_identity_extra, field, l); + r = sd_json_variant_set_field_strv(match_identity ?: &arg_identity_extra, field, l); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4615,7 +4858,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to parse %s/%s parameter: %s", field, field_scale, optarg); - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, u); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, u); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field); @@ -4623,7 +4866,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; } else { - r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r)); + r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field_scale, UINT32_SCALE_FROM_PERMYRIAD(r)); if (r < 0) return log_error_errno(r, "Failed to set %s field: %m", field_scale); @@ -4647,12 +4890,56 @@ static int parse_argv(int argc, char *argv[]) { if (!filename_is_valid(optarg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for default area field not valid: %s", optarg); - r = sd_json_variant_set_field_string(&arg_identity_extra, "defaultArea", optarg); + r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "defaultArea", optarg); if (r < 0) return log_error_errno(r, "Failed to set default area field: %m"); break; + case ARG_KEY_NAME: + if (isempty(optarg)) { + arg_key_name = mfree(arg_key_name); + return 0; + } + + if (!filename_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key name not valid: %s", optarg); + + r = free_and_strdup_warn(&arg_key_name, optarg); + if (r < 0) + return r; + + break; + + case ARG_SEIZE: + r = parse_boolean_argument("--seize=", optarg, &arg_seize); + 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 '?': return -EINVAL; @@ -4915,26 +5202,297 @@ static int fallback_shell(int argc, char *argv[]) { return log_error_errno(errno, "Failed to execute shell '%s': %m", shell); } +static int verb_list_signing_keys(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = bus_call_method(bus, bus_mgr, "ListSigningKeys", &error, &reply, NULL); + if (r < 0) + return log_error_errno(r, "Failed to list signing keys: %s", bus_error_message(&error, r)); + + _cleanup_(table_unrefp) Table *table = table_new("name", "key"); + if (!table) + return log_oom(); + + r = sd_bus_message_enter_container(reply, 'a', "(sst)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name, *pem; + + r = sd_bus_message_read(reply, "(sst)", &name, &pem, NULL); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + _cleanup_free_ char *h = NULL; + if (!sd_json_format_enabled(arg_json_format_flags)) { + /* Let's decode the PEM key to DER (so that we lose prefix/suffix), then truncate it + * for display reasons. */ + + _cleanup_(EVP_PKEY_freep) EVP_PKEY *key = NULL; + r = openssl_pubkey_from_pem(pem, SIZE_MAX, &key); + if (r < 0) + return log_error_errno(r, "Failed to parse PEM: %m"); + + _cleanup_free_ void *der = NULL; + int n = i2d_PUBKEY(key, (unsigned char**) &der); + if (n < 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to encode key as DER: %m"); + + ssize_t m = base64mem(der, MIN(n, 64), &h); + if (m < 0) + return log_oom(); + if (n > 64) /* check if we truncated the original version */ + if (!strextend(&h, special_glyph(SPECIAL_GLYPH_ELLIPSIS))) + return log_oom(); + } + + r = table_add_many( + table, + TABLE_STRING, name, + TABLE_STRING, h ?: pem); + if (r < 0) + return table_log_add_error(r); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { + r = table_set_sort(table, (size_t) 0); + if (r < 0) + return table_log_sort_error(r); + + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); + if (r < 0) + return r; + } + + if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { + if (table_isempty(table)) + printf("No signing keys.\n"); + else + printf("\n%zu signing keys listed.\n", table_get_rows(table) - 1); + } + + return 0; +} + +static int verb_get_signing_key(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + char **keys = argc >= 2 ? strv_skip(argv, 1) : STRV_MAKE("local.public"); + int ret = 0; + STRV_FOREACH(k, keys) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = bus_call_method(bus, bus_mgr, "GetSigningKey", &error, &reply, "s", *k); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to get signing key '%s': %s", *k, bus_error_message(&error, r))); + continue; + } + + const char *pem; + r = sd_bus_message_read(reply, "st", &pem, NULL); + if (r < 0) { + RET_GATHER(ret, bus_log_parse_error(r)); + continue; + } + + fputs(pem, stdout); + if (!endswith(pem, "\n")) + fputc('\n', stdout); + + fflush(stdout); + } + + return ret; +} + +static int add_signing_key_one(sd_bus *bus, const char *fn, FILE *key) { + int r; + + assert_se(bus); + assert_se(fn); + assert_se(key); + + _cleanup_free_ char *pem = NULL; + r = read_full_stream(key, &pem, /* ret_size= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to read key '%s': %m", fn); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_mgr, "AddSigningKey", &error, /* reply= */ NULL, "sst", fn, pem, UINT64_C(0)); + if (r < 0) + return log_error_errno(r, "Failed to add signing key '%s': %s", fn, bus_error_message(&error, r)); + + return 0; +} + +static int verb_add_signing_key(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + int ret = EXIT_SUCCESS; + if (argc < 2 || streq(argv[1], "-")) { + if (!arg_key_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key name must be specified via --key-name= when reading key from standard input, refusing."); + + RET_GATHER(ret, add_signing_key_one(bus, arg_key_name, stdin)); + } else { + /* Refuse if more han one key is specified in combination with --key-name= */ + if (argc >= 3 && arg_key_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--key-name= is not supported if multiple signing keys are specified, refusing."); + + STRV_FOREACH(k, strv_skip(argv, 1)) { + + if (streq(*k, "-")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to read from standard input if multiple keys are specified."); + + _cleanup_free_ char *fn = NULL; + if (!arg_key_name) { + r = path_extract_filename(*k, &fn); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to extract filename from path '%s': %m", *k)); + continue; + } + } + + _cleanup_fclose_ FILE *f = fopen(*k, "re"); + if (!f) { + RET_GATHER(ret, log_error_errno(errno, "Failed to open '%s': %m", *k)); + continue; + } + + RET_GATHER(ret, add_signing_key_one(bus, fn ?: arg_key_name, f)); + } + } + + return ret; +} + +static int add_signing_keys_from_credentials(void) { + int r; + + _cleanup_close_ int fd = open_credentials_dir(); + if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */ + return 0; + if (fd < 0) + return log_error_errno(fd, "Failed to open credentials directory: %m"); + + _cleanup_free_ DirectoryEntries *des = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + if (r < 0) + return log_error_errno(r, "Failed to enumerate credentials: %m"); + + int ret = 0; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + FOREACH_ARRAY(i, des->entries, des->n_entries) { + struct dirent *de = *i; + if (de->d_type != DT_REG) + continue; + + const char *e = startswith(de->d_name, "home.add-signing-key."); + if (!e) + continue; + + if (!filename_is_valid(e)) + continue; + + if (!bus) { + r = acquire_bus(&bus); + if (r < 0) + return r; + } + + _cleanup_fclose_ FILE *f = NULL; + r = xfopenat(fd, de->d_name, "re", O_NOFOLLOW, &f); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to open credential '%s': %m", de->d_name)); + continue; + } + + RET_GATHER(ret, add_signing_key_one(bus, e, f)); + } + + return ret; +} + +static int remove_signing_key_one(sd_bus *bus, const char *fn) { + int r; + + assert_se(bus); + assert_se(fn); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_mgr, "RemoveSigningKey", &error, /* reply= */ NULL, "st", fn, UINT64_C(0)); + if (r < 0) + return log_error_errno(r, "Failed to remove signing key '%s': %s", fn, bus_error_message(&error, r)); + + return 0; +} + +static int verb_remove_signing_key(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = EXIT_SUCCESS; + STRV_FOREACH(k, strv_skip(argv, 1)) + RET_GATHER(r, remove_signing_key_one(bus, *k)); + + return r; +} + static int run(int argc, char *argv[]) { static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, help }, - { "list", VERB_ANY, 1, VERB_DEFAULT, list_homes }, - { "activate", 2, VERB_ANY, 0, activate_home }, - { "deactivate", 2, VERB_ANY, 0, deactivate_home }, - { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home }, - { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, - { "create", VERB_ANY, 2, 0, create_home }, - { "remove", 2, VERB_ANY, 0, remove_home }, - { "update", VERB_ANY, 2, 0, update_home }, - { "passwd", VERB_ANY, 2, 0, passwd_home }, - { "resize", 2, 3, 0, resize_home }, - { "lock", 2, VERB_ANY, 0, lock_home }, - { "unlock", 2, VERB_ANY, 0, unlock_home }, - { "with", 2, VERB_ANY, 0, with_home }, - { "lock-all", VERB_ANY, 1, 0, lock_all_homes }, - { "deactivate-all", VERB_ANY, 1, 0, deactivate_all_homes }, - { "rebalance", VERB_ANY, 1, 0, rebalance }, - { "firstboot", VERB_ANY, 1, 0, verb_firstboot }, + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "list", VERB_ANY, 1, VERB_DEFAULT, list_homes }, + { "activate", 2, VERB_ANY, 0, activate_home }, + { "deactivate", 2, VERB_ANY, 0, deactivate_home }, + { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home }, + { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, + { "create", VERB_ANY, 2, 0, create_home }, + { "adopt", VERB_ANY, VERB_ANY, 0, verb_adopt_home }, + { "register", VERB_ANY, VERB_ANY, 0, verb_register_home }, + { "unregister", 2, VERB_ANY, 0, verb_unregister_home }, + { "remove", 2, VERB_ANY, 0, remove_home }, + { "update", VERB_ANY, 2, 0, update_home }, + { "passwd", VERB_ANY, 2, 0, passwd_home }, + { "resize", 2, 3, 0, resize_home }, + { "lock", 2, VERB_ANY, 0, lock_home }, + { "unlock", 2, VERB_ANY, 0, unlock_home }, + { "with", 2, VERB_ANY, 0, with_home }, + { "lock-all", VERB_ANY, 1, 0, lock_all_homes }, + { "deactivate-all", VERB_ANY, 1, 0, deactivate_all_homes }, + { "rebalance", VERB_ANY, 1, 0, rebalance }, + { "firstboot", VERB_ANY, 1, 0, verb_firstboot }, + { "list-signing-keys", VERB_ANY, 1, 0, verb_list_signing_keys }, + { "get-signing-key", VERB_ANY, VERB_ANY, 0, verb_get_signing_key }, + { "add-signing-key", VERB_ANY, VERB_ANY, 0, verb_add_signing_key }, + { "remove-signing-key", 2, VERB_ANY, 0, verb_remove_signing_key }, {} }; diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 871ccdffe9..9b6d2ee404 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -915,8 +915,7 @@ static void home_remove_finish(Home *h, int ret, UserRecord *hr) { * partitions like USB sticks, or so). Sometimes these storage locations are among those we normally * automatically discover in /home or in udev. When such a home is deleted let's hence issue a rescan * after completion, so that "unfixated" entries are rediscovered. */ - if (!IN_SET(user_record_test_image_path(h->record), USER_TEST_UNDEFINED, USER_TEST_ABSENT)) - manager_enqueue_rescan(m); + (void) manager_enqueue_rescan(m); /* The image is now removed from disk. Now also remove our stored record */ r = home_unlink_record(h); @@ -2063,12 +2062,17 @@ int home_unregister(Home *h, sd_bus_error *error) { return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); } + Manager *m = ASSERT_PTR(h->manager); + r = home_unlink_record(h); if (r < 0) return r; /* And destroy the whole entry. The caller needs to be prepared for that. */ h = home_free(h); + + /* Let's rescan, who knows, maybe this revealed a directory in /home/ that we should pick up now */ + manager_enqueue_rescan(m); return 1; } @@ -2495,7 +2499,6 @@ static int home_get_disk_status_directory( log_debug_errno(r, "No UID quota support on %s.", path); goto finish; } - if (r != -ESRCH) { log_debug_errno(r, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); goto finish; diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c index 726a12e54b..a3fe2b6a69 100644 --- a/src/home/homed-manager-bus.c +++ b/src/home/homed-manager-bus.c @@ -6,12 +6,15 @@ #include "bus-common-errors.h" #include "bus-message-util.h" #include "bus-polkit.h" +#include "fileio.h" #include "format-util.h" #include "home-util.h" #include "homed-bus.h" #include "homed-home-bus.h" #include "homed-manager-bus.h" #include "homed-manager.h" +#include "openssl-util.h" +#include "path-util.h" #include "strv.h" #include "user-record-sign.h" #include "user-record-util.h" @@ -516,6 +519,47 @@ static int method_register_home( return sd_bus_reply_method_return(message, NULL); } +static int method_adopt_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *image_path = NULL; + uint64_t flags = 0; + r = sd_bus_message_read(message, "st", &image_path, &flags); + if (r < 0) + return r; + + if (!path_is_absolute(image_path) || !path_is_safe(image_path)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Specified path is not absolute or not valid: %s", image_path); + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags field must be zero."); + + r = bus_verify_polkit_async( + message, + "org.freedesktop.home1.create-home", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = manager_adopt_home(m, image_path); + if (r == -EMEDIUMTYPE) + return sd_bus_error_setf(error, BUS_ERROR_UNRECOGNIZED_HOME_FORMAT, "Unrecognized format of home directory: %s", image_path); + if (r < 0) + return r; + + return sd_bus_reply_method_return(message, NULL); +} + static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { return generic_home_method(userdata, message, bus_home_method_unregister, error); } @@ -753,6 +797,274 @@ static int method_rebalance(sd_bus_message *message, void *userdata, sd_bus_erro return 1; } +static int method_list_signing_keys(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(sst)"); + if (r < 0) + return r; + + /* Add our own key pair first */ + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + + _cleanup_free_ char *pem = NULL; + r = openssl_pubkey_to_pem(m->private_key, &pem); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + r = sd_bus_message_append( + reply, + "(sst)", + "local.public", + pem, + UINT64_C(0)); + if (r < 0) + return r; + + /* And then all public keys we recognize */ + EVP_PKEY *pkey; + const char *fn; + HASHMAP_FOREACH_KEY(pkey, fn, m->public_keys) { + pem = mfree(pem); + r = openssl_pubkey_to_pem(pkey, &pem); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + r = sd_bus_message_append( + reply, + "(sst)", + fn, + pem, + UINT64_C(0)); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(/* bus= */ NULL, reply, /* ret_cookie= */ NULL); +} + +static int method_get_signing_key(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *fn; + r = sd_bus_message_read(message, "s", &fn); + if (r < 0) + return r; + + /* Make sure the local key is loaded. */ + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + + EVP_PKEY *pkey; + + if (streq(fn, "local.public")) + pkey = m->private_key; + else + pkey = hashmap_get(m->public_keys, fn); + if (!pkey) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_KEY, "No key with name: %s", fn); + + _cleanup_free_ char *pem = NULL; + r = openssl_pubkey_to_pem(pkey, &pem); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_append( + reply, + "st", + pem, + UINT64_C(0)); + if (r < 0) + return r; + + return sd_bus_send(/* bus= */ NULL, reply, /* ret_cookie= */ NULL); +} + +static bool valid_public_key_name(const char *fn) { + assert(fn); + + /* Checks if the specified name is valid to export, i.e. is a filename, ends in ".public". */ + + if (!filename_is_valid(fn)) + return false; + + const char *e = endswith(fn, ".public"); + if (!e) + return false; + + return e != fn; +} + +static bool manager_has_public_key(Manager *m, EVP_PKEY *needle) { + int r; + + assert(m); + + EVP_PKEY *pkey; + HASHMAP_FOREACH(pkey, m->public_keys) { + r = EVP_PKEY_eq(pkey, needle); + if (r > 0) + return true; + + /* EVP_PKEY_eq() returns -1 and -2 too under some conditions, which we'll all treat as "not the same" */ + } + + r = EVP_PKEY_eq(m->private_key, needle); + if (r > 0) + return true; + + return false; +} + +static int method_add_signing_key(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *fn, *pem; + uint64_t flags; + r = sd_bus_message_read(message, "sst", &fn, &pem, &flags); + if (r < 0) + return r; + + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags parameter must be zero."); + if (!valid_public_key_name(fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name not valid: %s", fn); + if (streq(fn, "local.public")) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Refusing to write local public key."); + + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + r = openssl_pubkey_from_pem(pem, /* pem_size= */ SIZE_MAX, &pkey); + if (r == -EIO) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key invalid: %s", fn); + if (r < 0) + return r; + + if (hashmap_contains(m->public_keys, fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name already exists: %s", fn); + + /* Make sure the local key is loaded before can detect conflicts */ + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + + if (manager_has_public_key(m, pkey)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key already exists: %s", fn); + + r = bus_verify_polkit_async( + message, + "org.freedesktop.home1.manage-signing-keys", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + _cleanup_free_ char *pem_reformatted = NULL; + r = openssl_pubkey_to_pem(pkey, &pem_reformatted); + if (r < 0) + return log_error_errno(r, "Failed to convert public key to PEM: %m"); + + _cleanup_free_ char *fn_copy = strdup(fn); + if (!fn) + return log_oom(); + + _cleanup_free_ char *p = path_join("/var/lib/systemd/home/", fn); + if (!p) + return log_oom(); + + r = write_string_file(p, pem_reformatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_MODE_0444); + if (r < 0) + return log_error_errno(r, "Failed to write public key PEM to '%s': %m", p); + + r = hashmap_ensure_put(&m->public_keys, &public_key_hash_ops, fn_copy, pkey); + if (r < 0) { + (void) unlink(p); + return log_error_errno(r, "Failed to add public key to set: %m"); + } + + TAKE_PTR(fn_copy); + TAKE_PTR(pkey); + + return sd_bus_reply_method_return(message, NULL); +} + +static int method_remove_signing_key(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(message); + + const char *fn; + uint64_t flags; + r = sd_bus_message_read(message, "st", &fn, &flags); + if (r < 0) + return r; + + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags parameter must be zero."); + + if (!valid_public_key_name(fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name not valid: %s", fn); + + if (streq(fn, "local.public")) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Refusing to remove local key."); + + if (!hashmap_contains(m->public_keys, fn)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Public key name does not exist: %s", fn); + + r = bus_verify_polkit_async( + message, + "org.freedesktop.home1.manage-signing-keys", + /* details= */ NULL, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + _cleanup_free_ char *p = path_join("/var/lib/systemd/home/", fn); + if (!p) + return log_oom(); + + if (unlink(p) < 0) + return log_error_errno(errno, "Failed to remove '%s': %m", p); + + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + _cleanup_free_ char *fn_free = NULL; + pkey = ASSERT_PTR(hashmap_remove2(m->public_keys, fn, (void**) &fn_free)); + + return sd_bus_reply_method_return(message, NULL); +} + static const sd_bus_vtable manager_vtable[] = { SD_BUS_VTABLE_START(0), @@ -820,6 +1132,11 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_NO_RESULT, method_register_home, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("AdoptHome", + SD_BUS_ARGS("s", image_path, "t", flags), + SD_BUS_NO_RESULT, + method_adopt_home, + SD_BUS_VTABLE_UNPRIVILEGED), /* Remove the JSON record from homed, but don't remove actual $HOME */ SD_BUS_METHOD_WITH_ARGS("UnregisterHome", @@ -934,6 +1251,27 @@ static const sd_bus_vtable manager_vtable[] = { method_release_home, 0), + SD_BUS_METHOD_WITH_ARGS("ListSigningKeys", + SD_BUS_NO_ARGS, + SD_BUS_RESULT("a(sst)", keys), + method_list_signing_keys, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("GetSigningKey", + SD_BUS_RESULT("s", name), + SD_BUS_RESULT("s", der, "t", flags), + method_get_signing_key, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("AddSigningKey", + SD_BUS_RESULT("s", name, "s", pem, "t", flags), + SD_BUS_NO_RESULT, + method_add_signing_key, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RemoveSigningKey", + SD_BUS_RESULT("s", name, "t", flags), + SD_BUS_NO_RESULT, + method_remove_signing_key, + SD_BUS_VTABLE_UNPRIVILEGED), + /* An operation that acts on all homes that allow it */ SD_BUS_METHOD("LockAllHomes", NULL, NULL, method_lock_all_homes, 0), SD_BUS_METHOD("DeactivateAllHomes", NULL, NULL, method_deactivate_all_homes, 0), diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index 0a94128230..5ce0fc3300 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -177,7 +177,7 @@ static int on_home_inotify(sd_event_source *s, const struct inotify_event *event else if (FLAGS_SET(event->mask, IN_MOVED_TO)) log_debug("%s has been moved in, having a look.", j); - (void) manager_assess_image(m, -1, get_home_root(), event->name); + (void) manager_assess_image(m, /* dir_fd= */ -EBADF, get_home_root(), event->name); (void) bus_manager_emit_auto_login_changed(m); } @@ -201,6 +201,20 @@ static int on_home_inotify(sd_event_source *s, const struct inotify_event *event return 0; } +static int sigusr1_handler(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + assert(s); + + /* If clients send use SIGUSR1 we'll explicitly rescan for home directories. This is useful in some + * cases where inotify isn't good enough, for example if /home/ is overmunted. */ + manager_watch_home(m); + (void) manager_gc_images(m); + (void) manager_enumerate_images(m); + (void) bus_manager_emit_auto_login_changed(m); + + return 0; +} + int manager_new(Manager **ret) { _cleanup_(manager_freep) Manager *m = NULL; int r; @@ -237,6 +251,10 @@ int manager_new(Manager **ret) { if (r < 0) return r; + r = sd_event_add_signal(m->event, /* ret_event_source= */ NULL, SIGUSR1|SD_EVENT_SIGNAL_PROCMASK, sigusr1_handler, m); + if (r < 0) + return r; + (void) sd_event_set_watchdog(m->event, true); m->homes_by_uid = hashmap_new(&homes_by_uid_hash_ops); @@ -545,6 +563,8 @@ static int search_quota(uid_t uid, const char *exclude_quota_path) { if (r < 0) { if (ERRNO_IS_NOT_SUPPORTED(r)) log_debug_errno(r, "No UID quota support on %s, ignoring.", where); + else if (r == -ESRCH) + log_debug_errno(r, "UID quota not enabled on %s (for user " UID_FMT "), ignoring.", where, uid); else if (ERRNO_IS_PRIVILEGE(r)) log_debug_errno(r, "UID quota support for %s prohibited, ignoring.", where); else @@ -841,6 +861,10 @@ static int manager_assess_image( assert(dir_path); assert(dentry_name); + /* Maybe registers the specified .home or .homedir as a home we manage. Returns: + * + * -EMEDIUMTYPE: Not a dir with .homedir suffix or a file with .home suffix */ + luks_suffix = endswith(dentry_name, ".home"); if (luks_suffix) directory_suffix = NULL; @@ -849,7 +873,7 @@ static int manager_assess_image( /* Early filter out: by name */ if (!luks_suffix && !directory_suffix) - return 0; + return -EMEDIUMTYPE; path = path_join(dir_path, dentry_name); if (!path) @@ -868,7 +892,7 @@ static int manager_assess_image( _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; if (!luks_suffix) - return 0; + return -EMEDIUMTYPE; n = strndup(dentry_name, luks_suffix - dentry_name); if (!n) @@ -876,7 +900,7 @@ static int manager_assess_image( r = split_user_name_realm(n, &user_name, &realm); if (r == -EINVAL) /* Not the right format: ignore */ - return 0; + return -EMEDIUMTYPE; if (r < 0) return log_error_errno(r, "Failed to split image name into user name/realm: %m"); @@ -889,7 +913,7 @@ static int manager_assess_image( UserStorage storage; if (!directory_suffix) - return 0; + return -EMEDIUMTYPE; n = strndup(dentry_name, directory_suffix - dentry_name); if (!n) @@ -897,7 +921,7 @@ static int manager_assess_image( r = split_user_name_realm(n, &user_name, &realm); if (r == -EINVAL) /* Not the right format: ignore */ - return 0; + return -EMEDIUMTYPE; if (r < 0) return log_error_errno(r, "Failed to split image name into user name/realm: %m"); @@ -939,7 +963,26 @@ static int manager_assess_image( return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid); } - return 0; + return -EMEDIUMTYPE; +} + +int manager_adopt_home(Manager *m, const char *path) { + int r; + + assert(m); + assert(path); + + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(path, &fn); + if (r < 0) + return r; + + _cleanup_free_ char *dir = NULL; + r = path_extract_directory(path, &dir); + if (r < 0) + return r; + + return manager_assess_image(m, /* dir_fd= */ -EBADF, dir, fn); } int manager_enumerate_images(Manager *m) { @@ -1446,7 +1489,7 @@ int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus return user_record_sign(u, m->private_key, ret); } -DEFINE_PRIVATE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free); +DEFINE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free); static int manager_load_public_key_one(Manager *m, const char *path) { _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; @@ -1482,15 +1525,11 @@ static int manager_load_public_key_one(Manager *m, const char *path) { if (st.st_uid != 0 || (st.st_mode & 0022) != 0) return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Public key file %s is writable by more than the root user, refusing.", path); - r = hashmap_ensure_allocated(&m->public_keys, &public_key_hash_ops); - if (r < 0) - return log_oom(); - pkey = PEM_read_PUBKEY(f, &pkey, NULL, NULL); if (!pkey) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key file %s.", path); - r = hashmap_put(m->public_keys, fn, pkey); + r = hashmap_ensure_put(&m->public_keys, &public_key_hash_ops, fn, pkey); if (r < 0) return log_error_errno(r, "Failed to add public key to set: %m"); diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h index 9fea621031..b780094d0d 100644 --- a/src/home/homed-manager.h +++ b/src/home/homed-manager.h @@ -87,9 +87,13 @@ int manager_reschedule_rebalance(Manager *m); int manager_verify_user_record(Manager *m, UserRecord *hr); +int manager_adopt_home(Manager *m, const char *path); + int manager_acquire_key_pair(Manager *m); int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error); int bus_manager_emit_auto_login_changed(Manager *m); int manager_get_home_by_name(Manager *m, const char *user_name, Home **ret); + +extern const struct hash_ops public_key_hash_ops; diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf index b8085929b0..adf85aa5cf 100644 --- a/src/home/org.freedesktop.home1.conf +++ b/src/home/org.freedesktop.home1.conf @@ -149,6 +149,22 @@ send_interface="org.freedesktop.home1.Manager" send_member="Rebalance"/> + + + + + + + + auth_admin_keep + + + Manage Home Directory Signing Keys + Authentication is required to manage signing keys for home directories. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c index 2230b29caf..a50c186d67 100644 --- a/src/home/user-record-sign.c +++ b/src/home/user-record-sign.c @@ -31,13 +31,11 @@ static int user_record_signable_json(UserRecord *ur, char **ret) { } int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) { - _cleanup_(memstream_done) MemStream m = {}; _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL; _cleanup_free_ char *text = NULL, *key = NULL; _cleanup_free_ void *signature = NULL; size_t signature_size = 0; - FILE *f; int r; assert(ur); @@ -52,14 +50,7 @@ int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) { if (r < 0) return r; - f = memstream_init(&m); - if (!f) - return -ENOMEM; - - if (PEM_write_PUBKEY(f, private_key) <= 0) - return -EIO; - - r = memstream_finalize(&m, &key, NULL); + r = openssl_pubkey_to_pem(private_key, &key); if (r < 0) return r; diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index cb5c1b74d5..c66fded7f1 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -150,6 +150,8 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { SD_BUS_ERROR_MAP(BUS_ERROR_HOME_IN_USE, EADDRINUSE), SD_BUS_ERROR_MAP(BUS_ERROR_REBALANCE_NOT_NEEDED, EALREADY), SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_REFERENCED, EBADR), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_KEY, ENOKEY), + SD_BUS_ERROR_MAP(BUS_ERROR_UNRECOGNIZED_HOME_FORMAT, EMEDIUMTYPE), SD_BUS_ERROR_MAP(BUS_ERROR_NO_UPDATE_CANDIDATE, EALREADY), diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index edc49027b6..9148f0aba9 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -156,6 +156,8 @@ #define BUS_ERROR_HOME_IN_USE "org.freedesktop.home1.HomeInUse" #define BUS_ERROR_REBALANCE_NOT_NEEDED "org.freedesktop.home1.RebalanceNotNeeded" #define BUS_ERROR_HOME_NOT_REFERENCED "org.freedesktop.home1.HomeNotReferenced" +#define BUS_ERROR_NO_SUCH_KEY "org.freedesktop.home1.NoSuchKey" +#define BUS_ERROR_UNRECOGNIZED_HOME_FORMAT "org.freedesktop.home1.UnrecognizedHomeFormat" #define BUS_ERROR_NO_UPDATE_CANDIDATE "org.freedesktop.sysupdate1.NoCandidate" diff --git a/src/shared/openssl-util.c b/src/shared/openssl-util.c index 92d4f4d955..b19c18e56e 100644 --- a/src/shared/openssl-util.c +++ b/src/shared/openssl-util.c @@ -8,6 +8,7 @@ #include "fileio.h" #include "hexdecoct.h" #include "memory-util.h" +#include "memstream-util.h" #include "openssl-util.h" #include "random-util.h" #include "string-util.h" @@ -52,24 +53,41 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(UI_METHOD*, UI_destroy_method, NULL); UNIQ_T(R, u); \ }) -int openssl_pkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret) { +int openssl_pubkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret) { assert(pem); assert(ret); + if (pem_size == SIZE_MAX) + pem_size = strlen(pem); + _cleanup_fclose_ FILE *f = NULL; f = fmemopen((void*) pem, pem_size, "r"); if (!f) return log_oom_debug(); - _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = PEM_read_PUBKEY(f, NULL, NULL, NULL); + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = PEM_read_PUBKEY(f, /* x= */ NULL, /* pam_password_cb= */ NULL, /* userdata= */ NULL); if (!pkey) return log_openssl_errors("Failed to parse PEM"); *ret = TAKE_PTR(pkey); - return 0; } +int openssl_pubkey_to_pem(EVP_PKEY *pkey, char **ret) { + assert(pkey); + assert(ret); + + _cleanup_(memstream_done) MemStream m = {}; + FILE *f = memstream_init(&m); + if (!f) + return -ENOMEM; + + if (PEM_write_PUBKEY(f, pkey) <= 0) + return -EIO; + + return memstream_finalize(&m, ret, /* ret_size= */ NULL); +} + /* Returns the number of bytes generated by the specified digest algorithm. This can be used only for * fixed-size algorithms, e.g. md5, sha1, sha256, etc. Do not use this for variable-sized digest algorithms, * e.g. shake128. Returns 0 on success, -EOPNOTSUPP if the algorithm is not supported, or < 0 for any other diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index 4ed7a4891f..d936ef54f4 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -114,7 +114,8 @@ static inline void sk_X509_free_allp(STACK_OF(X509) **sk) { sk_X509_pop_free(*sk, X509_free); } -int openssl_pkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret); +int openssl_pubkey_from_pem(const void *pem, size_t pem_size, EVP_PKEY **ret); +int openssl_pubkey_to_pem(EVP_PKEY *pkey, char **ret); int openssl_digest_size(const char *digest_alg, size_t *ret_digest_size); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index b4ae74f17a..18194da3e7 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -4572,7 +4572,7 @@ int tpm2_tpm2b_public_from_pem(const void *pem, size_t pem_size, TPM2B_PUBLIC *r assert(ret); _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; - r = openssl_pkey_from_pem(pem, pem_size, &pkey); + r = openssl_pubkey_from_pem(pem, pem_size, &pkey); if (r < 0) return r; diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 89ba2fbef4..0d2c261a09 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -1126,6 +1126,8 @@ int per_machine_id_match(sd_json_variant *ids, sd_json_dispatch_flags_t flags) { sd_id128_t mid; int r; + assert(ids); + r = sd_id128_get_machine(&mid); if (r < 0) return json_log(ids, flags, r, "Failed to acquire machine ID: %m"); @@ -1174,6 +1176,8 @@ int per_machine_hostname_match(sd_json_variant *hns, sd_json_dispatch_flags_t fl _cleanup_free_ char *hn = NULL; int r; + assert(hns); + r = gethostname_strict(&hn); if (r == -ENXIO) { json_log(hns, flags, r, "No hostname set, not matching perMachine hostname record: %m"); @@ -1221,6 +1225,15 @@ int per_machine_match(sd_json_variant *entry, sd_json_dispatch_flags_t flags) { return true; } + m = sd_json_variant_by_key(entry, "matchNotMachineId"); + if (m) { + r = per_machine_id_match(m, flags); + if (r < 0) + return r; + if (r == 0) + return true; + } + m = sd_json_variant_by_key(entry, "matchHostname"); if (m) { r = per_machine_hostname_match(m, flags); @@ -1230,6 +1243,15 @@ int per_machine_match(sd_json_variant *entry, sd_json_dispatch_flags_t flags) { return true; } + m = sd_json_variant_by_key(entry, "matchNotHostname"); + if (m) { + r = per_machine_hostname_match(m, flags); + if (r < 0) + return r; + if (r == 0) + return true; + } + return false; } @@ -1237,7 +1259,9 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j static const sd_json_dispatch_field per_machine_dispatch_table[] = { { "matchMachineId", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchNotMachineId", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, { "matchHostname", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchNotHostname", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, { "blobDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), SD_JSON_STRICT }, { "blobManifest", SD_JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 }, { "iconName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, icon_name), SD_JSON_STRICT }, diff --git a/src/test/test-openssl.c b/src/test/test-openssl.c index d0ea0f0215..9ad2aa201e 100644 --- a/src/test/test-openssl.c +++ b/src/test/test-openssl.c @@ -7,7 +7,7 @@ TEST(openssl_pkey_from_pem) { DEFINE_HEX_PTR(key_ecc, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a30444151634451674145726a6e4575424c73496c3972687068777976584e50686a346a426e500a44586e794a304b395579724e6764365335413532542b6f5376746b436a365a726c34685847337741515558706f426c532b7448717452714c35513d3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a"); _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_ecc = NULL; - assert_se(openssl_pkey_from_pem(key_ecc, key_ecc_len, &pkey_ecc) >= 0); + assert_se(openssl_pubkey_from_pem(key_ecc, key_ecc_len, &pkey_ecc) >= 0); _cleanup_free_ void *x = NULL, *y = NULL; size_t x_len, y_len; @@ -23,7 +23,7 @@ TEST(openssl_pkey_from_pem) { DEFINE_HEX_PTR(key_rsa, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541795639434950652f505852337a436f63787045300a6a575262546c3568585844436b472f584b79374b6d2f4439584942334b734f5a31436a5937375571372f674359363170697838697552756a73413464503165380a593445336c68556d374a332b6473766b626f4b64553243626d52494c2f6675627771694c4d587a41673342575278747234547545443533527a373634554650640a307a70304b68775231496230444c67772f344e67566f314146763378784b4d6478774d45683567676b73733038326332706c354a504e32587677426f744e6b4d0a5471526c745a4a35355244436170696e7153334577376675646c4e735851357746766c7432377a7637344b585165616d704c59433037584f6761304c676c536b0a79754774586b6a50542f735542544a705374615769674d5a6f714b7479563463515a58436b4a52684459614c47587673504233687a766d5671636e6b47654e540a65774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a"); _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_rsa = NULL; - assert_se(openssl_pkey_from_pem(key_rsa, key_rsa_len, &pkey_rsa) >= 0); + assert_se(openssl_pubkey_from_pem(key_rsa, key_rsa_len, &pkey_rsa) >= 0); _cleanup_free_ void *n = NULL, *e = NULL; size_t n_len, e_len; @@ -94,7 +94,7 @@ TEST(invalid) { _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; DEFINE_HEX_PTR(key, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b7b"); - assert_se(openssl_pkey_from_pem(key, key_len, &pkey) == -EIO); + assert_se(openssl_pubkey_from_pem(key, key_len, &pkey) == -EIO); ASSERT_NULL(pkey); } diff --git a/src/test/test-tpm2.c b/src/test/test-tpm2.c index 31ef6192b2..e42fcfe817 100644 --- a/src/test/test-tpm2.c +++ b/src/test/test-tpm2.c @@ -810,7 +810,7 @@ static void get_tpm2b_public_from_pem(const void *pem, size_t pem_size, TPM2B_PU assert(pem); assert(ret); - assert_se(openssl_pkey_from_pem(pem, pem_size, &pkey) >= 0); + assert_se(openssl_pubkey_from_pem(pem, pem_size, &pkey) >= 0); assert_se(tpm2_tpm2b_public_from_openssl_pkey(pkey, &p1) >= 0); assert_se(tpm2_tpm2b_public_from_pem(pem, pem_size, &p2) >= 0); assert_se(memcmp_nn(&p1, sizeof(p1), &p2, sizeof(p2)) == 0); diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index 6a9a7852e2..622cd887f3 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -186,7 +186,7 @@ static int load_public_key_disk(const char *path, struct public_key_data *ret) { } else { log_debug("Loaded SRK public key from '%s'.", path); - r = openssl_pkey_from_pem(blob, blob_size, &data.pkey); + r = openssl_pubkey_from_pem(blob, blob_size, &data.pkey); if (r < 0) return log_error_errno(r, "Failed to parse SRK public key file '%s': %m", path); diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c index 81007c4e50..8ba12afbb7 100644 --- a/src/userdb/userdbctl.c +++ b/src/userdb/userdbctl.c @@ -45,8 +45,10 @@ static uid_t arg_uid_min = 0; static uid_t arg_uid_max = UID_INVALID-1; static bool arg_fuzzy = false; static bool arg_boundaries = true; +static sd_json_variant *arg_from_file = NULL; STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_from_file, sd_json_variant_unrefp); static const char *user_disposition_to_color(UserDisposition d) { assert(d >= 0); @@ -380,7 +382,7 @@ static int display_user(int argc, char *argv[], void *userdata) { int ret = 0, r; if (arg_output < 0) - arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; if (arg_output == OUTPUT_TABLE) { table = table_new(" ", "name", "disposition", "uid", "gid", "realname", "home", "shell", "order"); @@ -402,7 +404,23 @@ static int display_user(int argc, char *argv[], void *userdata) { .uid_max = arg_uid_max, }; - if (argc > 1 && !arg_fuzzy) + if (arg_from_file) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing."); + + _cleanup_(user_record_unrefp) UserRecord *ur = user_record_new(); + if (!ur) + return log_oom(); + + r = user_record_load(ur, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + r = show_user(ur, table); + if (r < 0) + return r; + + } else if (argc > 1 && !arg_fuzzy) STRV_FOREACH(i, argv + 1) { _cleanup_(user_record_unrefp) UserRecord *ur = NULL; @@ -706,7 +724,7 @@ static int display_group(int argc, char *argv[], void *userdata) { int ret = 0, r; if (arg_output < 0) - arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; if (arg_output == OUTPUT_TABLE) { table = table_new(" ", "name", "disposition", "gid", "description", "order"); @@ -727,7 +745,23 @@ static int display_group(int argc, char *argv[], void *userdata) { .gid_max = arg_uid_max, }; - if (argc > 1 && !arg_fuzzy) + if (arg_from_file) { + if (argc > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing."); + + _cleanup_(group_record_unrefp) GroupRecord *gr = group_record_new(); + if (!gr) + return log_oom(); + + r = group_record_load(gr, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + r = show_group(gr, table); + if (r < 0) + return r; + + } else if (argc > 1 && !arg_fuzzy) STRV_FOREACH(i, argv + 1) { _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; @@ -888,6 +922,9 @@ static int display_memberships(int argc, char *argv[], void *userdata) { _cleanup_(table_unrefp) Table *table = NULL; int ret = 0, r; + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing memberships, refusing."); + if (arg_output < 0) arg_output = OUTPUT_TABLE; @@ -982,6 +1019,9 @@ static int display_services(int argc, char *argv[], void *userdata) { _cleanup_closedir_ DIR *d = NULL; int r; + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing services, refusing."); + d = opendir("/run/systemd/userdb/"); if (!d) { if (errno == ENOENT) { @@ -1048,6 +1088,9 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { assert(argc >= 2); + if (arg_from_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing SSH authorized keys, refusing."); + if (arg_chain) { /* If --chain is specified, the rest of the command line is the chain command */ @@ -1167,6 +1210,7 @@ static int help(int argc, char *argv[], void *userdata) { " -R Equivalent to --disposition=regular\n" " --boundaries=BOOL Show/hide UID/GID range boundaries in output\n" " -B Equivalent to --boundaries=no\n" + " -F --from-file=PATH Read JSON record from file\n" "\nSee the %s for details.\n", program_invocation_short_name, ansi_highlight(), @@ -1215,6 +1259,7 @@ static int parse_argv(int argc, char *argv[]) { { "fuzzy", no_argument, NULL, 'z' }, { "disposition", required_argument, NULL, ARG_DISPOSITION }, { "boundaries", required_argument, NULL, ARG_BOUNDARIES }, + { "from-file", required_argument, NULL, 'F' }, {} }; @@ -1245,7 +1290,7 @@ static int parse_argv(int argc, char *argv[]) { int c; c = getopt_long(argc, argv, - arg_chain ? "+hjs:NISRzB" : "hjs:NISRzB", /* When --chain was used disable parsing of further switches */ + arg_chain ? "+hjs:NISRzBF:" : "hjs:NISRzBF:", /* When --chain was used disable parsing of further switches */ options, NULL); if (c < 0) break; @@ -1420,6 +1465,24 @@ static int parse_argv(int argc, char *argv[]) { arg_boundaries = false; break; + case 'F': { + if (isempty(optarg)) { + arg_from_file = sd_json_variant_unref(arg_from_file); + break; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + const char *fn = streq(optarg, "-") ? NULL : optarg; + unsigned line = 0; + r = sd_json_parse_file(fn ? NULL : stdin, fn ?: "", SD_JSON_PARSE_SENSITIVE, &v, &line, /* reterr_column= */ NULL); + if (r < 0) + return log_syntax(/* unit= */ NULL, LOG_ERR, fn ?: "", line, r, "JSON parse failure."); + + sd_json_variant_unref(arg_from_file); + arg_from_file = TAKE_PTR(v); + break; + } + case '?': return -EINVAL; @@ -1435,6 +1498,9 @@ static int parse_argv(int argc, char *argv[]) { if (arg_disposition_mask == UINT64_MAX) arg_disposition_mask = USER_DISPOSITION_MASK_ALL; + if (arg_from_file) + arg_boundaries = false; + return 1; } diff --git a/test/units/TEST-46-HOMED.sh b/test/units/TEST-46-HOMED.sh index 5bab0b11b7..78c91044b8 100755 --- a/test/units/TEST-46-HOMED.sh +++ b/test/units/TEST-46-HOMED.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # SPDX-License-Identifier: LGPL-2.1-or-later -# shellcheck disable=SC2016 +# shellcheck disable=SC2016,SC2209 set -eux set -o pipefail @@ -28,9 +28,18 @@ inspect() { homectl inspect --json=pretty "$USERNAME" } +wait_for_exist() { + # 2min max + for i in {1..60}; do + (( i > 1 )) && sleep 2 + homectl inspect "$1" && break + done +} + wait_for_state() { - for i in {1..10}; do - (( i > 1 )) && sleep 0.5 + # 2min max + for i in {1..60}; do + (( i > 1 )) && sleep 2 homectl inspect "$1" | grep -qF "State: $2" && break done } @@ -46,6 +55,9 @@ systemctl service-log-level systemd-homed debug mkdir -p /home mount -t tmpfs tmpfs /home -o size=290M +# Make sure systemd-homed takes notice of the overmounted /home/ +systemctl kill -sUSR1 systemd-homed + TMP_SKEL=$(mktemp -d) echo hogehoge >"$TMP_SKEL"/hoge @@ -727,6 +739,108 @@ systemctl stop user@"$(id -u subareatest)".service wait_for_state subareatest inactive homectl remove subareatest +# Test signing key logic +homectl list-signing-keys | grep -q local.public +(! (homectl list-signing-keys | grep -q signtest.public)) + +IDENTITY='{"userName":"signtest","storage":"directory","disposition":"regular","privileged":{"hashedPassword":["$y$j9T$I5Wxfm.fyg.RRWlgWw.rI1$gnQqGtbpPexqxZJkWMq8FxQi5Swc.CWeKtM8LwvEUB6"]},"enforcePasswordPolicy":false,"lastChangeUSec":1740677608017608,"lastPasswordChangeUSec":1740677608017608,"signature":[{"data":"Gl4wtc0sMjVnsH6FQwG/0M+x0nLI5cvvdtSSCttUu1gNtXqYn0UI4wZi/7zX35ERht6XHWDlP4d6V8HiAst4Dg==","key":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA6uvVaP1vh7O6nIbiOcvyIHRl4ihYSs0R7ctxtz2Zu7E=\n-----END PUBLIC KEY-----\n"}],"secret":{"password":["test"]}}' + +# Try with stripping the foreign signature first, this should just work +echo "$IDENTITY" | homectl create -P --identity=- --seize=yes +homectl remove signtest + +# No try again, and don't strip the signature. It will be refused. +(! (echo "$IDENTITY" | homectl create -P --identity=- --seize=no)) + +print_public_key() { + cat <