homed: add key management toolchain (#36513)

if Lennart shall dogfood ParticleOS he needs acceptable tooling for
making his homed home dir accessible from his legacy fedora install, and
from local and remote particleos. Let's add explicit support for
scenarios like this:

1. add high level support for enrolling the account signing key from the
fedora install
2. add high level support for "adopting" a local but foreign .home file
on a system
3. add high level support for "registering" a remote user account on a
different system

(this lacks test cases and some docs, hence marked as wip)
This commit is contained in:
Lennart Poettering
2025-03-07 22:19:33 +01:00
committed by GitHub
30 changed files with 1663 additions and 169 deletions

4
TODO
View File

@@ -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.

View File

@@ -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

View File

@@ -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`,

View File

@@ -179,6 +179,66 @@
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--key-name=</option></term>
<listitem><para>When used with the <command>add-signing-key</command> command, specify or override
the name under which to store the public key being added. The specified name can be chosen freely,
but must be suffixed with <literal>.public</literal>. If this option is not used the name is derived
from the specified filename. If a key is read from standard input this option is mandatory in order
to provide a suitable name for the key being added.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--seize=</option></term>
<listitem><para>Takes a boolean argument. When used with <command>create</command> or
<command>register</command>, controls whether to strip cryptographic signatures from the provided
JSON user records, which has the effect of signing them with the local signing key
(<filename>local.public</filename>) 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 <command>create</command> and false for <command>register</command>.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--match=</option></term>
<term><option>-A</option></term>
<term><option>-N</option></term>
<term><option>-T</option></term>
<listitem><para>Takes one of <literal>this</literal>, <literal>other</literal>,
<literal>any</literal> or <literal>auto</literal>. 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
<literal>this</literal> is specified the setting will only apply to the local system (positive
match), if <literal>other</literal> it will apply to all but the local system (negative match), if
<literal>any</literal> it will apply to all systems (unless there's a matching positive or negative
per-machine setting). If <literal>auto</literal> returns to the default logic: whether a setting
applies by default to the local system or all systems depends on the option in question.</para>
<para>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 <ulink
url="https://systemd.io/USER_RECORD">JSON User Records</ulink> for details on which settings may be
used with such per-machine matching and which ones may not.</para>
<para><option>-A</option> is a shortcut for <option>--match=any</option>, <option>-T</option> is
short for <option>--match=this</option> and <option>-N</option> is short for
<option>--match=other</option>.</para>
<para>Here's an example call that sets the storage field to <literal>luks</literal> on the local
system, but to <literal>cifs</literal> on all others:</para>
<programlisting># homectl update lennart -T --storage=luks -N --storage=cifs</programlisting>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<xi:include href="user-system-options.xml" xpointer="host" />
<xi:include href="user-system-options.xml" xpointer="machine" />
@@ -1172,6 +1232,68 @@
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
<varlistentry>
<term><command>adopt</command> <replaceable>PATH</replaceable> [<replaceable>PATH</replaceable>…]</term>
<listitem><para>Adopts one or more existing home directories on the local system. Takes one or more paths to
<filename>*.home</filename> LUKS home directories or <filename>*.homedir/</filename> standalone home
directories or subvolumes previously created by <filename>systemd-homed</filename> and makes them
available locally for login. The referenced files are not moved. This is an alternative for moving
such home directories into <filename>/home/</filename> (where they would be picked up
automatically).</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><command>register</command> <replaceable>FILE</replaceable> [<replaceable>FILE</replaceable>…]</term>
<listitem><para>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 <literal>-</literal> reads the
JSON user record from standard input.</para>
<para>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.</para>
<para>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 <literal>root</literal>:</para>
<programlisting># smbpasswd -a lennart</programlisting>
<para>Continue as regular user <literal>lennart</literal>:</para>
<programlisting>$ 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</programlisting>
<para>This first ensures the user account <literal>lennart</literal> 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.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><command>unregister</command> <replaceable>USER</replaceable></term>
<listitem><para>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
<command>register</command> or <command>adopt</command> command later, on this or another
system. Note that unregistering a user whose home directory is placed in <filename>/home/</filename>
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.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><command>remove</command> <replaceable>USER</replaceable></term>
@@ -1311,6 +1433,55 @@
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><command>list-signing-keys</command></term>
<listitem><para>Show a list of public keys that home directories can be signed with to be allowed for
local login. One such key (<filename>local.public</filename>) will be generated automatically for
signing locally created home directories, but additional public keys may be registered to accept home
directories from other origins too (see <command>add-signing-key</command> below).</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><command>get-signing-key</command> [<replaceable>NAME…</replaceable>]</term>
<listitem><para>Write the public key identified by the specified name to standard output (in PEM
format). If no name is specified defaults to <filename>local.public</filename>, i.e. the
automatically generated key for locally created home directories.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><command>add-signing-key</command> [<replaceable>FILE…</replaceable>]</term>
<listitem><para>Add public key(s) from the specified PEM key file(s) to the list of keys that home
areas have to be signed by to be permitted for local login. If a path of <literal>-</literal> is
specified, or if no file is specified at all, the key will be read from standard input. The key file
name(s) must carry the <filename>.public</filename> suffix, and the file name(s) will be used to name
the key(s) once added, too. If a key is added from standard input the key name must be specified
explicitly via <option>--key-name=</option>, see above.</para>
<para>This command is useful for permitting local home directories to be used on a remote
system. Example:</para>
<programlisting>homectl get-signing-key | ssh myotherhost homectl add-signing-key --key-name="$HOSTNAME".public</programlisting>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><command>remove-signing-key</command> <replaceable>NAME…</replaceable></term>
<listitem><para>Remove the public key identified by the specified name from the list of keys that
control from which origins to permit home directories for login.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@@ -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 {
<variablelist class="dbus-method" generated="True" extra-ref="RegisterHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="AdoptHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="UnregisterHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="CreateHome()"/>
@@ -185,6 +198,14 @@ node /org/freedesktop/home1 {
<variablelist class="dbus-method" generated="True" extra-ref="ReleaseHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ListSigningKeys()"/>
<variablelist class="dbus-method" generated="True" extra-ref="GetSigningKey()"/>
<variablelist class="dbus-method" generated="True" extra-ref="AddSigningKey()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RemoveSigningKey()"/>
<variablelist class="dbus-method" generated="True" extra-ref="LockAllHomes()"/>
<variablelist class="dbus-method" generated="True" extra-ref="DeactivateAllHomes()"/>
@@ -257,6 +278,12 @@ node /org/freedesktop/home1 {
is useful to register home directories locally that are not located where
<filename>systemd-homed.service</filename> would find them automatically.</para>
<para><function>AdoptHome()</function> 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 <filename>*.home</filename>
and <filename>*.homedir/</filename> home directories. This operation is done automatically for all such
home areas showing up in <filename>/home/</filename>, but may be requested explicitly with this call for
directories elsewhere. The <varname>flags</varname> must be set to zero, currently.</para>
<para><function>UnregisterHome()</function> unregisters an existing home directory. It takes a user
name as argument and undoes what <function>RegisterHome()</function> 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 {
<para><function>Rebalance()</function> synchronously rebalances free disk space between home
areas. This only executes an operation if at least one home area using the LUKS2 backend is active and
has rebalancing enabled, and is otherwise a NOP.</para>
<para><function>ListSigningKeys()</function> acquires a list of installed home area signing
keys. Returns an array of key names with their PEM encoded public key data. Each entry also comes with
a flags value which is currently unused and should be ignored by clients.</para>
<para><function>GetSigningKey()</function> acquires the PEM encoded public part of the specified home
area signing key of the specified name. Also returns a currently unused flags value that should be
ignored. The <varname>flags</varname> parameter must be set to zero, currently.</para>
<para><function>AddSigningKey()</function> adds a new key to the list of home area signing keys. Takes
a name string (free-form, suitable as filename, with suffix <literal>.public</literal>), the PEM
encoded public key data and a currently unused flags value that must be zero. The
<varname>flags</varname> parameter must be set to zero, currently.</para>
<para><function>RemoveSigningKey()</function> removes a key from the list of home area signing
keys. Takes the name of the key to remove and a currently unused flags value that must be zero. The
<varname>flags</varname> parameter must be set to zero, currently.</para>
</refsect2>
<refsect2>
@@ -599,6 +643,9 @@ node /org/freedesktop/home1/home {
<title>The Manager Object</title>
<para><function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function>,
<function>CreateHomeEx()</function>, and <function>UpdateHomeEx()</function> were added in version 256.</para>
<para><function>AdoptHome()</function>, <function>ListSigningKeys()</function>,
<function>GetSigningKey()</function>, <function>AddSigningKey()</function>, and
<function>RemoveSigningKey()</function> were added in version 258.</para>
</refsect2>
<refsect2>
<title>Home Objects</title>

View File

@@ -107,6 +107,22 @@
generated/signed before the key pair is copied in, lose their validity.</para>
</refsect1>
<refsect1>
<title>Signals</title>
<variablelist>
<varlistentry>
<term><constant>SIGUSR1</constant></term>
<listitem><para>Upon reception of the <constant>SIGUSR1</constant> process signal
<command>systemd-homed</command> will reestablish its file watches on <filename>/home/</filename> and
rescan the directory for home directories.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">

View File

@@ -378,16 +378,41 @@
</listitem>
</varlistentry>
<varlistentry>
<term><varname>home.add-signing-key.*</varname></term>
<listitem>
<para>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 <command>homectl get-signing-key</command>. 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 <filename>.public</filename> suffix). For details see
<citerefentry><refentrytitle>homectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>home.create.*</varname></term>
<listitem>
<para>Creates a home area for the specified user with the user record data passed in. For details see
<para>Creates a new home area for the specified user with the user record data passed in. For
details see
<citerefentry><refentrytitle>homectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<xi:include href="version-info.xml" xpointer="v256"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>home.register.*</varname></term>
<listitem>
<para>Registers an existing home area for the specified user with the user record data passed in. For details
see
<citerefentry><refentrytitle>homectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>cryptsetup.passphrase</varname></term>
<term><varname>cryptsetup.tpm2-pin</varname></term>

View File

@@ -243,6 +243,19 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--from-file=PATH</option></term>
<term><option>-f</option></term>
<listitem><para>When used with the <command>user</command> or <command>group</command> 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 <literal>-</literal>, 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.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="no-legend" />
<xi:include href="standard-options.xml" xpointer="help" />
@@ -263,6 +276,9 @@
<listitem><para>List all known users records or show details of one or more specified user
records. Use <option>--output=</option> to tweak output mode.</para>
<para>If used in conjuntion with <option>--from-file=</option> the user record data is read in JSON
format from the specified file instead of querying it from the system. For details see above.</para>
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
@@ -272,6 +288,9 @@
<listitem><para>List all known group records or show details of one or more specified group
records. Use <option>--output=</option> to tweak the output mode.</para>
<para>If used in conjuntion with <option>--from-file=</option> the group record data is read in JSON
format from the specified file instead of querying it from the system. For details see above.</para>
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>

View File

@@ -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'

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -6,12 +6,15 @@
#include "bus-common-errors.h"
#include "bus-message-util.h"
#include "bus-polkit.h"
#include "fileio.h"
#include "format-util.h"
#include "home-util.h"
#include "homed-bus.h"
#include "homed-home-bus.h"
#include "homed-manager-bus.h"
#include "homed-manager.h"
#include "openssl-util.h"
#include "path-util.h"
#include "strv.h"
#include "user-record-sign.h"
#include "user-record-util.h"
@@ -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),

View File

@@ -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");

View File

@@ -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;

View File

@@ -149,6 +149,22 @@
send_interface="org.freedesktop.home1.Manager"
send_member="Rebalance"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="ListSigningKeys"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="GetSigningKey"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="RemoveSigningKey"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="AddSigningKey"/>
<!-- Home object -->
<allow send_destination="org.freedesktop.home1"

View File

@@ -88,4 +88,14 @@
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
<action id="org.freedesktop.home1.manage-signing-keys">
<description gettext-domain="systemd">Manage Home Directory Signing Keys</description>
<message gettext-domain="systemd">Authentication is required to manage signing keys for home directories.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>

View File

@@ -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;

View File

@@ -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),

View File

@@ -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"

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 },

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 ?: "<stdin>", SD_JSON_PARSE_SENSITIVE, &v, &line, /* reterr_column= */ NULL);
if (r < 0)
return log_syntax(/* unit= */ NULL, LOG_ERR, fn ?: "<stdin>", 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;
}

View File

@@ -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 <<EOF
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA6uvVaP1vh7O6nIbiOcvyIHRl4ihYSs0R7ctxtz2Zu7E=
-----END PUBLIC KEY-----
EOF
}
# Let's now add the signing key
print_public_key | homectl add-signing-key --key-name=signtest.public
homectl get-signing-key signtest.public | cmp - <(print_public_key)
homectl list-signing-keys | grep -q local.public
homectl list-signing-keys | grep -q signtest.public
# Now create the account with this, it should work now
echo "$IDENTITY" | homectl create -P --identity=- --seize=no
# Verify we can log in
PASSWORD="test" homectl with signtest true
# Remove the key, and check again ,should fail now
homectl remove-signing-key signtest.public
wait_for_state signtest inactive
(! PASSWORD="test" homectl with signtest true)
# Verify key is really gone
homectl list-signing-keys | grep -q local.public
(! (homectl list-signing-keys | grep -q signtest.public))
# Test unregister + adopt
mkdir /home/elsewhere
mv /home/signtest.homedir /home/elsewhere/
homectl unregister signtest
print_public_key | homectl add-signing-key --key-name=signtest.public
homectl adopt /home/elsewhere/signtest.homedir
PASSWORD="test" homectl with signtest true
wait_for_state signtest inactive
# Test register
homectl unregister signtest
homectl register /home/elsewhere/signtest.homedir/.identity
homectl unregister signtest
# Test automatic fixation for anything in /home/
mv /home/elsewhere/signtest.homedir /home
rmdir /home/elsewhere
wait_for_exist signtest
PASSWORD="test" homectl with signtest true
wait_for_state signtest inactive
# add signing key via credential
homectl remove-signing-key signtest.public
(! (homectl list-signing-keys | grep -q signtest.public))
systemd-run --wait -p "SetCredential=home.add-signing-key.signtest.public:$(print_public_key)" homectl firstboot
homectl list-signing-keys | grep -q signtest.public
# register user via credential
mkdir /home/elsewhere2
mv /home/signtest.homedir /home/elsewhere2/
homectl unregister signtest
systemd-run --wait -p "LoadCredential=home.register.signtest:/home/elsewhere2/signtest.homedir/.identity" homectl firstboot
homectl inspect signtest
homectl unregister signtest
mv /home/elsewhere2/signtest.homedir /home/
rmdir /home/elsewhere2
wait_for_exist signtest
# Remove it all again
homectl remove-signing-key signtest.public
homectl remove signtest
# Test positive and negative matching
NEWPASSWORD=test homectl create --storage=directory --nice=5 -P matchtest
homectl inspect matchtest
homectl inspect matchtest | grep "Nice: 5"
PASSWORD=test homectl update -N --nice=7 -T --nice=3 matchtest
homectl inspect matchtest
homectl inspect matchtest | grep "Nice: 3"
PASSWORD=test homectl update -A --default-area=quux1 matchtest
homectl inspect matchtest
homectl inspect matchtest | grep "Area: quux1"
PASSWORD=test homectl update -N --default-area=quux2 matchtest
homectl inspect matchtest
homectl inspect matchtest | grep "Area: quux1"
PASSWORD=test homectl update -T --default-area=quux3 matchtest
homectl inspect matchtest
homectl inspect matchtest | grep "Area: quux3"
homectl remove matchtest
systemd-analyze log-level info
touch /testok

View File

@@ -37,3 +37,12 @@ assert_eq "$(userdbctl user 0 -j | jq -r .userName)" root
assert_eq "$(userdbctl user 2147352576 -j | jq -r .userName)" foreign-0
assert_eq "$(userdbctl user 2147352577 -j | jq -r .userName)" foreign-1
assert_eq "$(userdbctl user 2147418110 -j | jq -r .userName)" foreign-65534
# Make sure that -F shows same data as if we'd ask directly
userdbctl user root -j | userdbctl -F- user | cmp - <(userdbctl user root)
userdbctl user systemd-network -j | userdbctl -F- user | cmp - <(userdbctl user systemd-network)
userdbctl user 65534 -j | userdbctl -F- user | cmp - <(userdbctl user 65534)
userdbctl group root -j | userdbctl -F- group | cmp - <(userdbctl group root)
userdbctl group systemd-network -j | userdbctl -F- group | cmp - <(userdbctl group systemd-network)
userdbctl group 65534 -j | userdbctl -F- group | cmp - <(userdbctl group 65534)