mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 08:25:20 +09:00
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:
4
TODO
4
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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
171
man/homectl.xml
171
man/homectl.xml
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user