1
1
mirror of https://github.com/systemd/systemd-stable.git synced 2024-12-23 17:34:00 +03:00

Merge pull request #19995 from poettering/cred-tool

Add support for encrypted credentials
This commit is contained in:
Lennart Poettering 2021-07-08 12:59:59 +02:00 committed by GitHub
commit 19755bca19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2815 additions and 243 deletions

57
TODO
View File

@ -142,16 +142,37 @@ Features:
* expose MS_NOSYMFOLLOW in various places * expose MS_NOSYMFOLLOW in various places
* ability to insert trusted configuration and secrets into the boot parameters * allow passing creds into kernel when booting: in EFI stub, collect creds
of a kernel booting in a VM or on baremetal some way, via TPM files from ESP directory, generate CPIO archive on the fly from them, so that
protection. idea: they are dropped into /run/initramfs/creds/ and pass to kernel as additional
1. pass via /proc/bootconfig initrd. Then, use LoadCredentialEncrypted=foo:/run/initramfs/creds/foo to
2. for secrets: put secrets in node of /proc/bootconfig, decrypt them via load them.
TPM early on in PID 1, put them in $CREDENTIAL_PATH logic
3. for config: put signed data in node /proc/booconfig, validate via TPM * make LoadCredential= automatically find credentials in /etc/creds,
early on in PID 1, put data into /run/bootconfig/ as individual files /run/creds, … and so on, if path component is unqualified
4. boot loader/stub should pick these up automatically from the boot loader
file systems * teach LoadCredential=/LoadCredentialEncrypted= to load credentials from
kernel cmdline, maybe: LoadCredentialEncrypted=foobar:proc-cmdline:foobar
* credentials system:
- acquire from kernel command line
- acquire from EFI variable?
- acquire via via ask-password?
- acquire creds via keyring?
- pass creds via keyring?
- pass creds via memfd?
- acquire + decrypt creds from pkcs11?
- make systemd-cryptsetup acquire pw via creds logic
- make PAMName= acquire pw via creds logic
- make macsec/wireguard code in networkd read key via creds logic
- make gatwayd/remote read key via creds logic
- add sd_notify() command for flushing out creds not needed anymore
* teach LoadCredential= the ability to load all files from a specified dir as
individual creds
* add tpm.target or so which is delayed until TPM2 device showed up in case
firmware indicates there is one.
* tpm2: support a PIN policy, i.e. allowing windows-style short authentication * tpm2: support a PIN policy, i.e. allowing windows-style short authentication
passwords by using the TPM2 to enforce ratelimiting and such, use for passwords by using the TPM2 to enforce ratelimiting and such, use for
@ -195,19 +216,6 @@ Features:
- cryptsetup-generator: allow specification of passwords in crypttab itself - cryptsetup-generator: allow specification of passwords in crypttab itself
- support rd.luks.allow-discards= kernel cmdline params in cryptsetup generator - support rd.luks.allow-discards= kernel cmdline params in cryptsetup generator
* credentials system:
- maybe add AcquireCredential= for querying a cred via ask-password
- maybe try to acquire creds via keyring?
- maybe try to pass creds via keyring?
- maybe optionally pass creds via memfd
- maybe add support for decrypting creds via TPM
- maybe add support for decrypting/importing creds via pkcs11
- make systemd-cryptsetup acquire pw via creds logic
- make PAMName= acquire pw via creds logic
- make macsec/wireguard code in networkd read key via creds logic
- make gatwayd/remote read key via creds logic
- add sd_notify() command for flushing out creds not needed anymore
* when configuring loopback netif, and it fails due to EPERM, eat up error if * when configuring loopback netif, and it fails due to EPERM, eat up error if
it happens to be set up alright already. it happens to be set up alright already.
@ -223,9 +231,6 @@ Features:
address as conduit for some minimal connection metainfo, and use it to address as conduit for some minimal connection metainfo, and use it to
restore the "description" logic that kdbus used to have. restore the "description" logic that kdbus used to have.
* teach LoadCredential= the ability to load all files from a specified dir as
individual creds
* systemd-analyze netif that explains predictable interface (or networkctl) * systemd-analyze netif that explains predictable interface (or networkctl)
* Add service setting to run a service within the specified VRF. i.e. do the * Add service setting to run a service within the specified VRF. i.e. do the

View File

@ -2711,8 +2711,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...]; readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...]; readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...]; readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...'; readonly s PAMName = '...';
@ -3224,8 +3228,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
<!--property SetCredential is not documented!--> <!--property SetCredential is not documented!-->
<!--property SetCredentialEncrypted is not documented!-->
<!--property LoadCredential is not documented!--> <!--property LoadCredential is not documented!-->
<!--property LoadCredentialEncrypted is not documented!-->
<!--property SupplementaryGroups is not documented!--> <!--property SupplementaryGroups is not documented!-->
<!--property PAMName is not documented!--> <!--property PAMName is not documented!-->
@ -3812,8 +3820,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
<variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="SetCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/> <variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/>
<variablelist class="dbus-property" generated="True" extra-ref="PAMName"/> <variablelist class="dbus-property" generated="True" extra-ref="PAMName"/>
@ -4511,8 +4523,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...]; readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...]; readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...]; readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...'; readonly s PAMName = '...';
@ -5052,8 +5068,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
<!--property SetCredential is not documented!--> <!--property SetCredential is not documented!-->
<!--property SetCredentialEncrypted is not documented!-->
<!--property LoadCredential is not documented!--> <!--property LoadCredential is not documented!-->
<!--property LoadCredentialEncrypted is not documented!-->
<!--property SupplementaryGroups is not documented!--> <!--property SupplementaryGroups is not documented!-->
<!--property PAMName is not documented!--> <!--property PAMName is not documented!-->
@ -5638,8 +5658,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
<variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="SetCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/> <variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/>
<variablelist class="dbus-property" generated="True" extra-ref="PAMName"/> <variablelist class="dbus-property" generated="True" extra-ref="PAMName"/>
@ -6234,8 +6258,12 @@ node /org/freedesktop/systemd1/unit/home_2emount {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...]; readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...]; readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...]; readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...'; readonly s PAMName = '...';
@ -6703,8 +6731,12 @@ node /org/freedesktop/systemd1/unit/home_2emount {
<!--property SetCredential is not documented!--> <!--property SetCredential is not documented!-->
<!--property SetCredentialEncrypted is not documented!-->
<!--property LoadCredential is not documented!--> <!--property LoadCredential is not documented!-->
<!--property LoadCredentialEncrypted is not documented!-->
<!--property SupplementaryGroups is not documented!--> <!--property SupplementaryGroups is not documented!-->
<!--property PAMName is not documented!--> <!--property PAMName is not documented!-->
@ -7207,8 +7239,12 @@ node /org/freedesktop/systemd1/unit/home_2emount {
<variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="SetCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/> <variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/>
<variablelist class="dbus-property" generated="True" extra-ref="PAMName"/> <variablelist class="dbus-property" generated="True" extra-ref="PAMName"/>
@ -7924,8 +7960,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...]; readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...]; readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredentialEncrypted = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...]; readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const") @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...'; readonly s PAMName = '...';
@ -8379,8 +8419,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
<!--property SetCredential is not documented!--> <!--property SetCredential is not documented!-->
<!--property SetCredentialEncrypted is not documented!-->
<!--property LoadCredential is not documented!--> <!--property LoadCredential is not documented!-->
<!--property LoadCredentialEncrypted is not documented!-->
<!--property SupplementaryGroups is not documented!--> <!--property SupplementaryGroups is not documented!-->
<!--property PAMName is not documented!--> <!--property PAMName is not documented!-->
@ -8869,8 +8913,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
<variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="SetCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="SetCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/> <variablelist class="dbus-property" generated="True" extra-ref="LoadCredential"/>
<variablelist class="dbus-property" generated="True" extra-ref="LoadCredentialEncrypted"/>
<variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/> <variablelist class="dbus-property" generated="True" extra-ref="SupplementaryGroups"/>
<variablelist class="dbus-property" generated="True" extra-ref="PAMName"/> <variablelist class="dbus-property" generated="True" extra-ref="PAMName"/>

View File

@ -833,6 +833,7 @@ manpages = [
'8', '8',
['systemd-coredump.socket', 'systemd-coredump@.service'], ['systemd-coredump.socket', 'systemd-coredump@.service'],
'ENABLE_COREDUMP'], 'ENABLE_COREDUMP'],
['systemd-creds', '1', [], ''],
['systemd-cryptenroll', '1', [], 'HAVE_LIBCRYPTSETUP'], ['systemd-cryptenroll', '1', [], 'HAVE_LIBCRYPTSETUP'],
['systemd-cryptsetup-generator', '8', [], 'HAVE_LIBCRYPTSETUP'], ['systemd-cryptsetup-generator', '8', [], 'HAVE_LIBCRYPTSETUP'],
['systemd-cryptsetup@.service', ['systemd-cryptsetup@.service',

378
man/systemd-creds.xml Normal file
View File

@ -0,0 +1,378 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-creds"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-creds</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-creds</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-creds</refname>
<refpurpose>Lists, shows, encrypts and decrypts service credentials</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>systemd-creds</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-creds</command> is a tool for listing, showing, encrypting and decrypting unit
credentials. Credentials are limited-size binary or textual objects that may be passed to unit
processes. They are primarily used for passing cryptographic keys (both public and private) or
certificates, user account information or identity information from the host to services.</para>
<para>Credentials are configured in unit files via the <varname>LoadCredential=</varname>,
<varname>SetCredential=</varname>, <varname>LoadCredentialEncrypted=</varname> and
<varname>SetCredentialEncrypted=</varname> settings, see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details.</para>
</refsect1>
<refsect1>
<title>Commands</title>
<para>The following commands are understood:</para>
<variablelist>
<varlistentry>
<term><command>list</command></term>
<listitem><para>Show a list of credentials passed into the current execution context. This command
shows the files in the directory referenced by the <varname>$CREDENTIALS_DIRECTORY</varname>
environment variable, and is intended to be executed from within service context.</para>
<para>Along with each credential name, the size and security state is shown. The latter is one of
<literal>secure</literal> (in case the credential is backed by unswappable memory,
i.e. <literal>ramfs</literal>), <literal>weak</literal> (in case it is backed by any other type of
memory), or <literal>insecure</literal> (if having any access mode that is not 0400, i.e. if readable
by anyone but the owner).</para></listitem>
</varlistentry>
<varlistentry>
<term><command>cat</command> <replaceable>credential...</replaceable></term>
<listitem><para>Show contents of specified credentials passed into the current execution
context. Takes one or more credential names, whose contents shall be written to standard
output.</para>
<para>When combined with <option>--json=</option> or <option>--transcode=</option> the output is
transcoded in simple ways before outputting.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>setup</command></term>
<listitem><para>Generates a host encryption key for credentials, if none has been generated
before. This ensures the <filename>/var/lib/systemd/credential.secret</filename> file is initialized
with a random secret key if it doesn't exist yet. This secret key is used when encrypting/decrypting
credentials with <command>encrypt</command> or <command>decrypt</command>, and is only accessible to
the root user. Note that there's typically no need to invoke this command explicitly as it is
implicitly called when <command>encrypt</command> is invoked, and credential host key encryption
selected.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>encrypt</command> <replaceable>input</replaceable> <replaceable>output</replaceable></term>
<listitem><para>Loads the specified (unencrypted plaintext) input credential file, encrypts it and
writes the (encrypted ciphertext) version to the specified output credential file. The resulting file
may be referenced in the <varname>LoadCredentialEncrypted=</varname> setting in unit files, or its
contents used literally in <varname>SetCredentialEncrypted=</varname> settings.</para>
<para>Takes two file system paths. The file name part of the output path is embedded as name in the
encrypted credential, to ensure encrypted credentials cannot be renamed and reused for different
purposes without this being noticed. The credential name to embed may be overridden with the
<option>--name=</option> setting. The input or output paths may be specified as <literal>-</literal>,
in which case the credential data is read from/written to standard input and standard output. If the
output path is specified as <literal>-</literal> the credential name cannot be derived from the file
system path, and thus should be specified explicitly via the <option>--name=</option> switch.</para>
<para>The credential data is encrypted symmetrically with one of the following encryption
keys:</para>
<orderedlist>
<listitem><para>A secret key automatically derived from the system's TPM2 chip. This encryption key
is not stored on the host system and thus decryption is only possible with access to the original
TPM2 chip. Or in other words, the credential secured in this way can only be decrypted again by the
local machine.</para></listitem>
<listitem><para>A secret key stored in the <filename>/var/lib/systemd/credential.secret</filename>
file which is only accessible to the root user. This "host" encryption key is stored on the host
file system, and thus decryption is possible with access to the host file system and sufficient
privileges. The key is automatically generated when needed, but can also be created explicitly with
the <command>setup</command> command, see above.</para></listitem>
<listitem><para>A combination of the above: an encryption key derived from both the TPM2 chip and
the host file system. This means decryption requires both access to the original TPM2 chip and the
OS installation. This is the default mode of operation if a TPM2 chip is available and
<filename>/var/lib/systemd/</filename> resides on persistent media.</para></listitem>
</orderedlist>
<para>Which of the three keys shall be used for encryption may be configured with the
<option>--with-key=</option> switch. Depending on the use-case for the encrypted credential the key to
use may differ. For example, for credentials that shall be accessible from the initial RAM disk
(initrd) of the system encryption with the host key is not appropriate since access to the host key
is typically not available from the initrd. Thus, for such credentials only the TPM2 key should be
used.</para>
<para>Encrypted credentials are always encoded in Base64.</para>
<para>Use <command>decrypt</command> (see below) to undo the encryption operation, and acquire the
decrypted plaintext credential from the encrypted ciphertext credential.</para>
<para>The credential data is encrypted using AES256-GCM, i.e. providing both confidentiality and
integrity, keyed by a SHA256 hash of one or both of the secret keys described above.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><command>decrypt</command> <replaceable>input</replaceable>
<optional><replaceable>output</replaceable></optional></term>
<listitem><para>Undoes the effect of the <command>encrypt</command> operation: loads the specified
(encrypted ciphertext) input credential file, decrypts it and writes the (decrypted plaintext)
version to the specified output credential file.</para>
<para>Takes one or two file system paths. The file name part of the input path is compared with the
credential name embedded in the encrypted file. If it does not match decryption fails. This is done
in order to ensure that encrypted credentials are not re-purposed without this being detected. The
credential name to compare with the embedded credential name may also be overridden with the
<option>--name=</option> switch. If only one path is specified (or the output path specified as
<literal>-</literal>) it is taken as input path and the decrypted credential is written to standard
output. If the input path is specified as <literal>-</literal> the encrypted credential is read from
standard input. In this mode, the expected name embedded in the credential cannot be derived from the
path and should be specified explicitly with <option>--name=</option>.</para>
<para>Decrypting credentials requires access to the original TPM2 chip and/or credentials host key,
see above. Information about which keys are required is embedded in the encrypted credential data,
and thus decryption is entirely automatic.</para></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Options</title>
<variablelist>
<varlistentry>
<term><option>--system</option></term>
<listitem><para>When specified with the <command>list</command> and <command>cat</command> commands
operates on the credentials passed to system as a whole instead of on those passed to the current
execution context. This is useful in container environments where credentials may be passed in from
the container manager.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--transcode=</option></term>
<listitem><para>When specified with the <command>cat</command> or <command>decrypt</command>
commands, transcodes the output before showing it. Takes one of <literal>base64</literal>,
<literal>unbase64</literal>, <literal>hex</literal> or <literal>unhex</literal> as argument, in order
to encode/decode the credential data with Base64 or as series of hexadecimal values.</para>
<para>Note that this has no effect on the <command>encrypt</command> command, as encrypted
credentials are unconditionally encoded in Base64.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--newline=</option></term>
<listitem><para>When specified with <command>cat</command> or <command>decrypt</command> controls
whether to add a trailing newline character to the end of the output if it doesn't end in one,
anyway. Takes one of <literal>auto</literal>, <literal>yes</literal> or <literal>no</literal>. The
default mode of <literal>auto</literal> will suffix the output with a single newline character only
when writing credential data to a TTY.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--pretty</option></term>
<term><option>-p</option></term>
<listitem><para>When specified with <command>encrypt</command> controls whether to show the encrypted
credential as <varname>SetCredentialEncrypted=</varname> setting that may be pasted directly into a
unit file.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--name=</option><replaceable>name</replaceable></term>
<listitem><para>When specified with the <command>encrypt</command> command controls the credential
name to embed in the encrypted credential data. If not specified the name is chosen automatically
from the filename component of the specified output path. If specified as empty string no
credential name is embedded in the encrypted credential, and no verification of credential name is
done when the credential is decrypted.</para>
<para>When specified with the <command>decrypt</command> command control the credential name to
validate the credential name embedded in the encrypted credential with. If not specified the name is
chosen automatically from the filename component of the specified input path. If no credential name
is embedded in the encrypted credential file (i.e. the <option>--name=</option> with an empty string
was used when encrypted) the specified name has no effect as no credential name validation is
done.</para>
<para>Embedding the credential name in the encrypted credential is done in order to protect against
reuse of credentials for purposes they weren't originally intended for, under the assumption the
credential name is chosen carefully to encode its intended purpose.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--timestamp=</option><replaceable>timestamp</replaceable></term>
<listitem><para>When specified with the <command>encrypt</command> command controls the timestamp to
embed into the encrypted credential. Defaults to the current time. Takes a timestamp specification in
the format described in
<citerefentry><refentrytitle>systemd.time</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
<para>When specified with the <command>decrypt</command> command controls the timestamp to use to
validate the "not-after" timestamp that was configured with <option>--not-after=</option> during
encryption. If not specified defaults to the current system time.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--not-after=</option><replaceable>timestamp</replaceable></term>
<listitem><para>When specified with the <command>encrypt</command> command controls the time when the
credential shall not be used anymore. This embeds the specified timestamp in the encrypted
credential. During decryption the timestamp is checked against the current system clock, and if the
timestamp is in the past the decryption will fail. By default no such timestamp is set. Takes a
timestamp specification in the format described in
<citerefentry><refentrytitle>systemd.time</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--with-key=</option></term>
<term><option>-H</option></term>
<term><option>-T</option></term>
<listitem><para>When specified with the <command>encrypt</command> command controls the encryption
key to use. Takes one of <literal>host</literal>, <literal>tpm2</literal>,
<literal>host+tpm2</literal> or <literal>auto</literal>. See above for details on the three key
types. If set to <literal>auto</literal> (which is the default) the TPM2 key is used if a TPM2 device
is found and not running in a container. The host key is used if
<filename>/var/lib/systemd/</filename> is on persistent media. This means on typical systems the
encryption is by default bound to both the TPM2 chip and the OS installation, and both need to be
available to decrypt the credential again. If <literal>auto</literal> is selected but neither TPM2 is
available (or running in container) nor <filename>/var/lib/systemd/</filename> is on persistent
media, encryption will fail.</para>
<para>The <option>-H</option> switch is a shortcut for <option>--with-key=host</option>. Similar,
<option>-T</option> is a shortcut for <option>-with-key=tpm2</option>.</para>
<para>When encrypting credentials that shall be used in the initial RAM disk (initrd) where
<filename>/var/lib/systemd/</filename> is typically not available make sure to use
<option>--with-key=tpm2</option> mode, to disable binding against the host secret.</para>
<para>This switch has no effect on the <command>decrypt</command> command, as information on which
key to use for decryption is included in the encrypted credential already.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--tpm2-device=</option><replaceable>PATH</replaceable></term>
<listitem><para>Controls the TPM2 device to use. Expects a device node path referring to the TPM2
chip (e.g. <filename>/dev/tpmrm0</filename>). Alternatively the special value <literal>auto</literal>
may be specified, in order to automatically determine the device node of a suitable TPM2 device (of
which there must be exactly one). The special value <literal>list</literal> may be used to enumerate
all suitable TPM2 devices currently discovered.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--tpm2-pcrs=</option><arg rep="repeat">PCR</arg></term>
<listitem><para>Configures the TPM2 PCRs (Platform Configuration Registers) to bind the encryption
key to. Takes a <literal>+</literal> separated list of numeric PCR indexes in the range 0…23. If not
used, defaults to PCR 7 only. If an empty string is specified, binds the encryption key to no PCRs at
all. For details about the PCRs available, see the documentation of the switch of the same name for
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para></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="json" />
</variablelist>
</refsect1>
<refsect1>
<title>Exit status</title>
<para>On success, 0 is returned.</para>
</refsect1>
<refsect1>
<title>Examples</title>
<example>
<title>Encrypt a password for use as credential</title>
<para>The following command line encrypts the specified password <literal>hunter2</literal>, writing the result
to a file <filename>password.cred</filename>.</para>
<programlisting># echo -n hunter2 | systemd-creds encrypt - password.cred</programlisting>
<para>This decrypts the file <filename>password.cred</filename> again, revealing the literal password:</para>
<programlisting># systemd-creds decrypt password.cred
hunter2</programlisting>
</example>
<example>
<title>Encrypt a password and include it in a unit file</title>
<para>The following command line prompts the user for a password and generates a
<varname>SetCredentialEncrypted=</varname> line from it for a credential named
<literal>mysql-password</literal>, suitable for inclusion in a unit file.</para>
<programlisting># systemd-ask-password -n | systemd-creds encrypt --name=mysql-password -p - -
🔐 Password: ****
SetCredentialEncrypted=mysql-password: \
k6iUCUh0RJCQyvL8k8q1UyAAAAABAAAADAAAABAAAAASfFsBoPLIm/dlDoGAAAAAAAAAA \
NAAAAAgAAAAAH4AILIOZ3w6rTzYsBy9G7liaCAd4i+Kpvs8mAgArzwuKxd0ABDjgSeO5k \
mKQc58zM94ZffyRmuNeX1lVHE+9e2YD87KfRFNoDLS7F3YmCb347gCiSk2an9egZ7Y0Xs \
700Kr6heqQswQEemNEc62k9RJnEl2q7SbcEYguegnPQUATgAIAAsAAAASACA/B90W7E+6 \
yAR9NgiIJvxr9bpElztwzB5lUJAxtMBHIgAQACCaSV9DradOZz4EvO/LSaRyRSq2Hj0ym \
gVJk/dVzE8Uxj8H3RbsT7rIBH02CIgm/Gv1ukSXO3DMHmVQkDG0wEciyageTfrVEer8z5 \
9cUQfM5ynSaV2UjeUWEHuz4fwDsXGLB9eELXLztzUU9nsAyLvs3ZRR+eEK/A==</programlisting>
<para>The generated line can be pasted 1:1 into a unit file, and will ensure the acquired password will
be made available in the <varname>$CREDENTIALS_DIRECTORY</varname><filename>/mysql-password</filename>
credential file for the started service.</para>
<para>Utilizing the unit file drop-in logic this can be used to securely pass a password credential to
a unit. A similar, more comprehensive set of commands to insert a password into a service
<filename>xyz.service</filename>:</para>
<programlisting># mkdir -p /etc/systemd/system/xyz.service.d
# systemd-ask-password -n | systemd-creds encrypt --name=mysql-password -p - - > /etc/systemd/system/xyz.service.d/50-password.conf
# systemctl daemon-reload
# systemctl restart xyz.service</programlisting>
</example>
</refsect1>
<refsect1>
<title>See Also</title>
<para>
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>
</para>
</refsect1>
</refentry>

View File

@ -2338,7 +2338,8 @@ SystemCallErrorNumber=EPERM</programlisting>
clients via D-Bus IPC, and generally not understood as being data that requires protection. Moreover, clients via D-Bus IPC, and generally not understood as being data that requires protection. Moreover,
environment variables are propagated down the process tree, including across security boundaries environment variables are propagated down the process tree, including across security boundaries
(such as setuid/setgid executables), and hence might leak to processes that should not have access to (such as setuid/setgid executables), and hence might leak to processes that should not have access to
the secret data. Use <varname>LoadCredential=</varname> (see below) to pass data to unit processes the secret data. Use <varname>LoadCredential=</varname>, <varname>LoadCredentialEncrypted=</varname>
or <varname>SetCredentialEncrypted=</varname> (see below) to pass data to unit processes
securely.</para></listitem> securely.</para></listitem>
</varlistentry> </varlistentry>
@ -2832,6 +2833,7 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
<varlistentry> <varlistentry>
<term><varname>LoadCredential=</varname><replaceable>ID</replaceable><optional>:<replaceable>PATH</replaceable></optional></term> <term><varname>LoadCredential=</varname><replaceable>ID</replaceable><optional>:<replaceable>PATH</replaceable></optional></term>
<term><varname>LoadCredentialEncrypted=</varname><replaceable>ID</replaceable><optional>:<replaceable>PATH</replaceable></optional></term>
<listitem><para>Pass a credential to the unit. Credentials are limited-size binary or textual objects <listitem><para>Pass a credential to the unit. Credentials are limited-size binary or textual objects
that may be passed to unit processes. They are primarily used for passing cryptographic keys (both that may be passed to unit processes. They are primarily used for passing cryptographic keys (both
@ -2860,6 +2862,20 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
from the service manager into a service. This option may be used multiple times, each time defining from the service manager into a service. This option may be used multiple times, each time defining
an additional credential to pass to the unit.</para> an additional credential to pass to the unit.</para>
<para>The <varname>LoadCredentialEncrypted=</varname> setting is identical to
<varname>LoadCredential=</varname>, except that the credential data is decrypted before being passed
on to the executed processes. Specifically, the referenced path should refer to a file or socket with
an encrypted credential, as implemented by
<citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry>. This
credential is loaded, decrypted and then passed to the application in decrypted plaintext form, in
the same way a regular credential specified via <varname>LoadCredential=</varname> would be. A
credential configured this way may encrypted with a secret key derived from the system's TPM2
security chip, or with a secret key stored in
<filename>/var/lib/systemd/credentials.secret</filename>, or with both. Using encrypted credentials
improves security as credentials are not stored in plaintext and only decrypted into plaintext the
moment a service requiring them is started. Moreover, credentials may be bound to the local hardware
and installations, so that they cannot easily be analyzed offline.</para>
<para>The credential files/IPC sockets must be accessible to the service manager, but don't have to <para>The credential files/IPC sockets must be accessible to the service manager, but don't have to
be directly accessible to the unit's processes: the credential data is read and copied into separate, be directly accessible to the unit's processes: the credential data is read and copied into separate,
read-only copies for the unit that are accessible to appropriately privileged processes. This is read-only copies for the unit that are accessible to appropriately privileged processes. This is
@ -2891,6 +2907,7 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
<varlistentry> <varlistentry>
<term><varname>SetCredential=</varname><replaceable>ID</replaceable>:<replaceable>VALUE</replaceable></term> <term><varname>SetCredential=</varname><replaceable>ID</replaceable>:<replaceable>VALUE</replaceable></term>
<term><varname>SetCredentialEncrypted=</varname><replaceable>ID</replaceable>:<replaceable>VALUE</replaceable></term>
<listitem><para>The <varname>SetCredential=</varname> setting is similar to <listitem><para>The <varname>SetCredential=</varname> setting is similar to
<varname>LoadCredential=</varname> but accepts a literal value to use as data for the credential, <varname>LoadCredential=</varname> but accepts a literal value to use as data for the credential,
@ -2901,6 +2918,14 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
C-style escaping (i.e. <literal>\n</literal> to embed a newline, or <literal>\x00</literal> to embed C-style escaping (i.e. <literal>\n</literal> to embed a newline, or <literal>\x00</literal> to embed
a <constant>NUL</constant> byte).</para> a <constant>NUL</constant> byte).</para>
<para>The <varname>SetCredentialEncrypted=</varname> setting is identical to
<varname>SetCredential=</varname> but expects an encrypted credential in literal form as value. This
allows embedding confidential credentials securely directly in unit files. Use
<citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry>'
<option>-p</option> switch to generate suitable <varname>SetCredentialEncrypted=</varname> lines
directly from plaintext credentials. For further details see
<varname>LoadCredentialEncrypted=</varname> above.</para>
<para>If a credential of the same ID is listed in both <varname>LoadCredential=</varname> and <para>If a credential of the same ID is listed in both <varname>LoadCredential=</varname> and
<varname>SetCredential=</varname>, the latter will act as default if the former cannot be <varname>SetCredential=</varname>, the latter will act as default if the former cannot be
retrieved. In this case not being able to retrieve the credential from the path specified in retrieved. In this case not being able to retrieve the credential from the path specified in

View File

@ -1707,7 +1707,8 @@ install_libsystemd_static = static_library(
libcap, libcap,
libblkid, libblkid,
libmount, libmount,
libgcrypt], libgcrypt,
libopenssl],
c_args : libsystemd_c_args + (static_libsystemd_pic ? [] : ['-fno-PIC'])) c_args : libsystemd_c_args + (static_libsystemd_pic ? [] : ['-fno-PIC']))
libudev = shared_library( libudev = shared_library(
@ -2953,6 +2954,17 @@ public_programs += executable(
install : true, install : true,
install_dir : rootbindir) install_dir : rootbindir)
public_programs += executable(
'systemd-creds',
'src/creds/creds.c',
include_directories : includes,
link_with : [libshared],
dependencies : [threads,
libopenssl],
install_rpath : rootlibexecdir,
install : true,
install_dir : rootbindir)
executable( executable(
'systemd-volatile-root', 'systemd-volatile-root',
'src/volatile-root/volatile-root.c', 'src/volatile-root/volatile-root.c',

View File

@ -7,10 +7,19 @@
#include <linux/fs.h> #include <linux/fs.h>
#include "chattr-util.h" #include "chattr-util.h"
#include "errno-util.h"
#include "fd-util.h" #include "fd-util.h"
#include "macro.h" #include "macro.h"
#include "string-util.h"
int chattr_full(const char *path,
int fd,
unsigned value,
unsigned mask,
unsigned *ret_previous,
unsigned *ret_final,
ChattrApplyFlags flags) {
int chattr_full(const char *path, int fd, unsigned value, unsigned mask, unsigned *ret_previous, unsigned *ret_final, bool fallback) {
_cleanup_close_ int fd_will_close = -1; _cleanup_close_ int fd_will_close = -1;
unsigned old_attr, new_attr; unsigned old_attr, new_attr;
struct stat st; struct stat st;
@ -57,12 +66,16 @@ int chattr_full(const char *path, int fd, unsigned value, unsigned mask, unsigne
return 1; return 1;
} }
if (errno != EINVAL || !fallback) if ((errno != EINVAL && !ERRNO_IS_NOT_SUPPORTED(errno)) ||
!FLAGS_SET(flags, CHATTR_FALLBACK_BITWISE))
return -errno; return -errno;
/* When -EINVAL is returned, we assume that incompatible attributes are simultaneously /* When -EINVAL is returned, we assume that incompatible attributes are simultaneously
* specified. E.g., compress(c) and nocow(C) attributes cannot be set to files on btrfs. * specified. E.g., compress(c) and nocow(C) attributes cannot be set to files on btrfs.
* As a fallback, let's try to set attributes one by one. */ * As a fallback, let's try to set attributes one by one.
*
* Also, when we get EOPNOTSUPP (or a similar error code) we assume a flag might just not be
* supported, and we can ignore it too */
unsigned current_attr = old_attr; unsigned current_attr = old_attr;
for (unsigned i = 0; i < sizeof(unsigned) * 8; i++) { for (unsigned i = 0; i < sizeof(unsigned) * 8; i++) {
@ -76,8 +89,12 @@ int chattr_full(const char *path, int fd, unsigned value, unsigned mask, unsigne
continue; continue;
if (ioctl(fd, FS_IOC_SETFLAGS, &new_one) < 0) { if (ioctl(fd, FS_IOC_SETFLAGS, &new_one) < 0) {
if (errno != EINVAL) if (errno != EINVAL && !ERRNO_IS_NOT_SUPPORTED(errno))
return -errno; return -errno;
log_full_errno(FLAGS_SET(flags, CHATTR_WARN_UNSUPPORTED_FLAGS) ? LOG_WARNING : LOG_DEBUG,
errno,
"Unable to set file attribute 0x%x on %s, ignoring: %m", mask_one, strna(path));
continue; continue;
} }

View File

@ -34,13 +34,28 @@
FS_NOCOW_FL | \ FS_NOCOW_FL | \
FS_PROJINHERIT_FL) FS_PROJINHERIT_FL)
int chattr_full(const char *path, int fd, unsigned value, unsigned mask, unsigned *ret_previous, unsigned *ret_final, bool fallback); typedef enum ChattrApplyFlags {
CHATTR_FALLBACK_BITWISE = 1 << 0,
CHATTR_WARN_UNSUPPORTED_FLAGS = 1 << 1,
} ChattrApplyFlags;
int chattr_full(const char *path, int fd, unsigned value, unsigned mask, unsigned *ret_previous, unsigned *ret_final, ChattrApplyFlags flags);
static inline int chattr_fd(int fd, unsigned value, unsigned mask, unsigned *previous) { static inline int chattr_fd(int fd, unsigned value, unsigned mask, unsigned *previous) {
return chattr_full(NULL, fd, value, mask, previous, NULL, false); return chattr_full(NULL, fd, value, mask, previous, NULL, 0);
} }
static inline int chattr_path(const char *path, unsigned value, unsigned mask, unsigned *previous) { static inline int chattr_path(const char *path, unsigned value, unsigned mask, unsigned *previous) {
return chattr_full(path, -1, value, mask, previous, NULL, false); return chattr_full(path, -1, value, mask, previous, NULL, 0);
} }
int read_attr_fd(int fd, unsigned *ret); int read_attr_fd(int fd, unsigned *ret);
int read_attr_path(const char *p, unsigned *ret); int read_attr_path(const char *p, unsigned *ret);
/* Combination of chattr flags, that should be appropriate for secrets stored on disk: Secure Remove +
* Exclusion from Dumping + Synchronous Writing (i.e. not caching in memory) + In-Place Updating (i.e. not
* spurious copies). */
#define CHATTR_SECRET_FLAGS (FS_SECRM_FL|FS_NODUMP_FL|FS_SYNC_FL|FS_NOCOW_FL)
static inline int chattr_secret(int fd, ChattrApplyFlags flags) {
return chattr_full(NULL, fd, CHATTR_SECRET_FLAGS, CHATTR_SECRET_FLAGS, NULL, NULL, flags|CHATTR_FALLBACK_BITWISE);
}

View File

@ -1,54 +0,0 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "creds-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "path-util.h"
bool credential_name_valid(const char *s) {
/* We want that credential names are both valid in filenames (since that's our primary way to pass
* them around) and as fdnames (which is how we might want to pass them around eventually) */
return filename_is_valid(s) && fdname_is_valid(s);
}
int get_credentials_dir(const char **ret) {
const char *e;
assert(ret);
e = secure_getenv("CREDENTIALS_DIRECTORY");
if (!e)
return -ENXIO;
if (!path_is_absolute(e) || !path_is_normalized(e))
return -EINVAL;
*ret = e;
return 0;
}
int read_credential(const char *name, void **ret, size_t *ret_size) {
_cleanup_free_ char *fn = NULL;
const char *d;
int r;
assert(ret);
if (!credential_name_valid(name))
return -EINVAL;
r = get_credentials_dir(&d);
if (r < 0)
return r;
fn = path_join(d, name);
if (!fn)
return -ENOMEM;
return read_full_file_full(
AT_FDCWD, fn,
UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_SECURE,
NULL,
(char**) ret, ret_size);
}

View File

@ -1,16 +0,0 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <inttypes.h>
#include <stdbool.h>
#include <sys/types.h>
#include "fd-util.h"
#define CREDENTIAL_NAME_MAX FDNAME_MAX
bool credential_name_valid(const char *s);
int get_credentials_dir(const char **ret);
int read_credential(const char *name, void **ret, size_t *ret_size);

View File

@ -522,18 +522,17 @@ int read_full_stream_full(
size_t *ret_size) { size_t *ret_size) {
_cleanup_free_ char *buf = NULL; _cleanup_free_ char *buf = NULL;
size_t n, n_next, l; size_t n, n_next = 0, l;
int fd, r; int fd, r;
assert(f); assert(f);
assert(ret_contents); assert(ret_contents);
assert(!FLAGS_SET(flags, READ_FULL_FILE_UNBASE64 | READ_FULL_FILE_UNHEX)); assert(!FLAGS_SET(flags, READ_FULL_FILE_UNBASE64 | READ_FULL_FILE_UNHEX));
assert(size != SIZE_MAX || !FLAGS_SET(flags, READ_FULL_FILE_FAIL_WHEN_LARGER));
if (offset != UINT64_MAX && offset > LONG_MAX) if (offset != UINT64_MAX && offset > LONG_MAX) /* fseek() can only deal with "long" offsets */
return -ERANGE; return -ERANGE;
n_next = size != SIZE_MAX ? size : LINE_MAX; /* Start size */
fd = fileno(f); fd = fileno(f);
if (fd >= 0) { /* If the FILE* object is backed by an fd (as opposed to memory or such, see if (fd >= 0) { /* If the FILE* object is backed by an fd (as opposed to memory or such, see
* fmemopen()), let's optimize our buffering */ * fmemopen()), let's optimize our buffering */
@ -543,20 +542,20 @@ int read_full_stream_full(
return -errno; return -errno;
if (S_ISREG(st.st_mode)) { if (S_ISREG(st.st_mode)) {
if (size == SIZE_MAX) {
/* Try to start with the right file size if we shall read the file in full. Note
* that we increase the size to read here by one, so that the first read attempt
* already makes us notice the EOF. If the reported size of the file is zero, we
* avoid this logic however, since quite likely it might be a virtual file in procfs
* that all report a zero file size. */
if (st.st_size > 0 &&
(size == SIZE_MAX || FLAGS_SET(flags, READ_FULL_FILE_FAIL_WHEN_LARGER))) {
uint64_t rsize = uint64_t rsize =
LESS_BY((uint64_t) st.st_size, offset == UINT64_MAX ? 0 : offset); LESS_BY((uint64_t) st.st_size, offset == UINT64_MAX ? 0 : offset);
/* Safety check */ if (rsize < SIZE_MAX) /* overflow check */
if (rsize > READ_FULL_BYTES_MAX)
return -E2BIG;
/* Start with the right file size. Note that we increase the size to read
* here by one, so that the first read attempt already makes us notice the
* EOF. If the reported size of the file is zero, we avoid this logic
* however, since quite likely it might be a virtual file in procfs that all
* report a zero file size. */
if (st.st_size > 0)
n_next = rsize + 1; n_next = rsize + 1;
} }
@ -565,6 +564,17 @@ int read_full_stream_full(
} }
} }
/* If we don't know how much to read, figure it out now. If we shall read a part of the file, then
* allocate the requested size. If we shall load the full file start with LINE_MAX. Note that if
* READ_FULL_FILE_FAIL_WHEN_LARGER we consider the specified size a safety limit, and thus also start
* with LINE_MAX, under assumption the file is most likely much shorter. */
if (n_next == 0)
n_next = size != SIZE_MAX && !FLAGS_SET(flags, READ_FULL_FILE_FAIL_WHEN_LARGER) ? size : LINE_MAX;
/* Never read more than we need to determine that our own limit is hit */
if (n_next > READ_FULL_BYTES_MAX)
n_next = READ_FULL_BYTES_MAX + 1;
if (offset != UINT64_MAX && fseek(f, offset, SEEK_SET) < 0) if (offset != UINT64_MAX && fseek(f, offset, SEEK_SET) < 0)
return -errno; return -errno;
@ -573,6 +583,11 @@ int read_full_stream_full(
char *t; char *t;
size_t k; size_t k;
/* If we shall fail when reading overly large data, then read exactly one byte more than the
* specified size at max, since that'll tell us if there's anymore data beyond the limit*/
if (FLAGS_SET(flags, READ_FULL_FILE_FAIL_WHEN_LARGER) && n_next > size)
n_next = size + 1;
if (flags & READ_FULL_FILE_SECURE) { if (flags & READ_FULL_FILE_SECURE) {
t = malloc(n_next + 1); t = malloc(n_next + 1);
if (!t) { if (!t) {
@ -606,14 +621,18 @@ int read_full_stream_full(
if (feof(f)) if (feof(f))
break; break;
if (size != SIZE_MAX) { /* If we got asked to read some specific size, we already sized the buffer right, hence leave */ if (size != SIZE_MAX && !FLAGS_SET(flags, READ_FULL_FILE_FAIL_WHEN_LARGER)) { /* If we got asked to read some specific size, we already sized the buffer right, hence leave */
assert(l == size); assert(l == size);
break; break;
} }
assert(k > 0); /* we can't have read zero bytes because that would have been EOF */ assert(k > 0); /* we can't have read zero bytes because that would have been EOF */
/* Safety check */ if (FLAGS_SET(flags, READ_FULL_FILE_FAIL_WHEN_LARGER) && l > size) {
r = -E2BIG;
goto finalize;
}
if (n >= READ_FULL_BYTES_MAX) { if (n >= READ_FULL_BYTES_MAX) {
r = -E2BIG; r = -E2BIG;
goto finalize; goto finalize;

View File

@ -39,6 +39,7 @@ typedef enum {
READ_FULL_FILE_UNHEX = 1 << 2, /* hex decode what we read */ READ_FULL_FILE_UNHEX = 1 << 2, /* hex decode what we read */
READ_FULL_FILE_WARN_WORLD_READABLE = 1 << 3, /* if regular file, log at LOG_WARNING level if access mode above 0700 */ READ_FULL_FILE_WARN_WORLD_READABLE = 1 << 3, /* if regular file, log at LOG_WARNING level if access mode above 0700 */
READ_FULL_FILE_CONNECT_SOCKET = 1 << 4, /* if socket inode, connect to it and read off it */ READ_FULL_FILE_CONNECT_SOCKET = 1 << 4, /* if socket inode, connect to it and read off it */
READ_FULL_FILE_FAIL_WHEN_LARGER = 1 << 5, /* fail loading if file is larger than specified size */
} ReadFullFileFlags; } ReadFullFileFlags;
int fopen_unlocked(const char *path, const char *options, FILE **ret); int fopen_unlocked(const char *path, const char *options, FILE **ret);

View File

@ -565,38 +565,79 @@ int unbase64char(char c) {
return -EINVAL; return -EINVAL;
} }
ssize_t base64mem(const void *p, size_t l, char **out) { static void maybe_line_break(char **x, char *start, size_t line_break) {
char *r, *z; size_t n;
assert(x);
assert(*x);
assert(start);
assert(*x >= start);
if (line_break == SIZE_MAX)
return;
n = *x - start;
if (n % (line_break + 1) == line_break)
*((*x)++) = '\n';
}
ssize_t base64mem_full(
const void *p,
size_t l,
size_t line_break,
char **out) {
const uint8_t *x; const uint8_t *x;
char *r, *z;
size_t m;
assert(p || l == 0); assert(p || l == 0);
assert(out); assert(out);
assert(line_break > 0);
/* three input bytes makes four output bytes, padding is added so we must round up */ /* three input bytes makes four output bytes, padding is added so we must round up */
z = r = malloc(4 * (l + 2) / 3 + 1); m = 4 * (l + 2) / 3 + 1;
if (line_break != SIZE_MAX)
m += m / line_break;
z = r = malloc(m);
if (!r) if (!r)
return -ENOMEM; return -ENOMEM;
for (x = p; x < (const uint8_t*) p + (l / 3) * 3; x += 3) { for (x = p; x < (const uint8_t*) p + (l / 3) * 3; x += 3) {
/* x[0] == XXXXXXXX; x[1] == YYYYYYYY; x[2] == ZZZZZZZZ */ /* x[0] == XXXXXXXX; x[1] == YYYYYYYY; x[2] == ZZZZZZZZ */
maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[0] >> 2); /* 00XXXXXX */ *(z++) = base64char(x[0] >> 2); /* 00XXXXXX */
maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[0] & 3) << 4 | x[1] >> 4); /* 00XXYYYY */ *(z++) = base64char((x[0] & 3) << 4 | x[1] >> 4); /* 00XXYYYY */
maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[1] & 15) << 2 | x[2] >> 6); /* 00YYYYZZ */ *(z++) = base64char((x[1] & 15) << 2 | x[2] >> 6); /* 00YYYYZZ */
maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[2] & 63); /* 00ZZZZZZ */ *(z++) = base64char(x[2] & 63); /* 00ZZZZZZ */
} }
switch (l % 3) { switch (l % 3) {
case 2: case 2:
maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[0] >> 2); /* 00XXXXXX */ *(z++) = base64char(x[0] >> 2); /* 00XXXXXX */
maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[0] & 3) << 4 | x[1] >> 4); /* 00XXYYYY */ *(z++) = base64char((x[0] & 3) << 4 | x[1] >> 4); /* 00XXYYYY */
maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[1] & 15) << 2); /* 00YYYY00 */ *(z++) = base64char((x[1] & 15) << 2); /* 00YYYY00 */
maybe_line_break(&z, r, line_break);
*(z++) = '='; *(z++) = '=';
break; break;
case 1: case 1:
maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[0] >> 2); /* 00XXXXXX */ *(z++) = base64char(x[0] >> 2); /* 00XXXXXX */
maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[0] & 3) << 4); /* 00XX0000 */ *(z++) = base64char((x[0] & 3) << 4); /* 00XX0000 */
maybe_line_break(&z, r, line_break);
*(z++) = '='; *(z++) = '=';
maybe_line_break(&z, r, line_break);
*(z++) = '='; *(z++) = '=';
break; break;

View File

@ -33,7 +33,11 @@ int unbase64char(char c) _const_;
char *base32hexmem(const void *p, size_t l, bool padding); char *base32hexmem(const void *p, size_t l, bool padding);
int unbase32hexmem(const char *p, size_t l, bool padding, void **mem, size_t *len); int unbase32hexmem(const char *p, size_t l, bool padding, void **mem, size_t *len);
ssize_t base64mem(const void *p, size_t l, char **out); ssize_t base64mem_full(const void *p, size_t l, size_t line_break, char **ret);
static inline ssize_t base64mem(const void *p, size_t l, char **ret) {
return base64mem_full(p, l, SIZE_MAX, ret);
}
int base64_append(char **prefix, int plen, int base64_append(char **prefix, int plen,
const void *p, size_t l, const void *p, size_t l,
int margin, int width); int margin, int width);

View File

@ -29,8 +29,6 @@ basic_sources = files('''
chattr-util.h chattr-util.h
conf-files.c conf-files.c
conf-files.h conf-files.h
creds-util.c
creds-util.h
def.h def.h
dirent-util.c dirent-util.c
dirent-util.h dirent-util.h

View File

@ -820,6 +820,9 @@ static int property_get_set_credential(
HASHMAP_FOREACH(sc, c->set_credentials) { HASHMAP_FOREACH(sc, c->set_credentials) {
if (sc->encrypted != streq(property, "SetCredentialEncrypted"))
continue;
r = sd_bus_message_open_container(reply, 'r', "say"); r = sd_bus_message_open_container(reply, 'r', "say");
if (r < 0) if (r < 0)
return r; return r;
@ -850,7 +853,7 @@ static int property_get_load_credential(
sd_bus_error *error) { sd_bus_error *error) {
ExecContext *c = userdata; ExecContext *c = userdata;
char **i, **j; ExecLoadCredential *lc;
int r; int r;
assert(bus); assert(bus);
@ -862,8 +865,12 @@ static int property_get_load_credential(
if (r < 0) if (r < 0)
return r; return r;
STRV_FOREACH_PAIR(i, j, c->load_credentials) { HASHMAP_FOREACH(lc, c->load_credentials) {
r = sd_bus_message_append(reply, "(ss)", *i, *j);
if (lc->encrypted != streq(property, "LoadCredentialEncrypted"))
continue;
r = sd_bus_message_append(reply, "(ss)", lc->id, lc->path);
if (r < 0) if (r < 0)
return r; return r;
} }
@ -1144,7 +1151,9 @@ const sd_bus_vtable bus_exec_vtable[] = {
SD_BUS_PROPERTY("DynamicUser", "b", bus_property_get_bool, offsetof(ExecContext, dynamic_user), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("DynamicUser", "b", bus_property_get_bool, offsetof(ExecContext, dynamic_user), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RemoveIPC", "b", bus_property_get_bool, offsetof(ExecContext, remove_ipc), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RemoveIPC", "b", bus_property_get_bool, offsetof(ExecContext, remove_ipc), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("SetCredential", "a(say)", property_get_set_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("SetCredential", "a(say)", property_get_set_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("SetCredentialEncrypted", "a(say)", property_get_set_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("LoadCredential", "a(ss)", property_get_load_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("LoadCredential", "a(ss)", property_get_load_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("LoadCredentialEncrypted", "a(ss)", property_get_load_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("SupplementaryGroups", "as", NULL, offsetof(ExecContext, supplementary_groups), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("SupplementaryGroups", "as", NULL, offsetof(ExecContext, supplementary_groups), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("PAMName", "s", NULL, offsetof(ExecContext, pam_name), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("PAMName", "s", NULL, offsetof(ExecContext, pam_name), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ReadWritePaths", "as", NULL, offsetof(ExecContext, read_write_paths), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ReadWritePaths", "as", NULL, offsetof(ExecContext, read_write_paths), SD_BUS_VTABLE_PROPERTY_CONST),
@ -1916,7 +1925,7 @@ int bus_exec_context_set_transient_property(
return 1; return 1;
} else if (streq(name, "SetCredential")) { } else if (STR_IN_SET(name, "SetCredential", "SetCredentialEncrypted")) {
bool isempty = true; bool isempty = true;
r = sd_bus_message_enter_container(message, 'a', "(say)"); r = sd_bus_message_enter_container(message, 'a', "(say)");
@ -1964,20 +1973,24 @@ int bus_exec_context_set_transient_property(
if (old) { if (old) {
free_and_replace(old->data, copy); free_and_replace(old->data, copy);
old->size = sz; old->size = sz;
old->encrypted = streq(name, "SetCredentialEncrypted");
} else { } else {
_cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL; _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
sc = new0(ExecSetCredential, 1); sc = new(ExecSetCredential, 1);
if (!sc) if (!sc)
return -ENOMEM; return -ENOMEM;
sc->id = strdup(id); *sc = (ExecSetCredential) {
.id = strdup(id),
.data = TAKE_PTR(copy),
.size = sz,
.encrypted = streq(name, "SetCredentialEncrypted"),
};
if (!sc->id) if (!sc->id)
return -ENOMEM; return -ENOMEM;
sc->data = TAKE_PTR(copy);
sc->size = sz;
r = hashmap_ensure_put(&c->set_credentials, &exec_set_credential_hash_ops, sc->id, sc); r = hashmap_ensure_put(&c->set_credentials, &exec_set_credential_hash_ops, sc->id, sc);
if (r < 0) if (r < 0)
return r; return r;
@ -2008,7 +2021,7 @@ int bus_exec_context_set_transient_property(
return 1; return 1;
} else if (streq(name, "LoadCredential")) { } else if (STR_IN_SET(name, "LoadCredential", "LoadCredentialEncrypted")) {
bool isempty = true; bool isempty = true;
r = sd_bus_message_enter_container(message, 'a', "(ss)"); r = sd_bus_message_enter_container(message, 'a', "(ss)");
@ -2033,9 +2046,39 @@ int bus_exec_context_set_transient_property(
isempty = false; isempty = false;
if (!UNIT_WRITE_FLAGS_NOOP(flags)) { if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
r = strv_extend_strv(&c->load_credentials, STRV_MAKE(id, source), /* filter_duplicates = */ false); _cleanup_free_ char *copy = NULL;
if (r < 0) ExecLoadCredential *old;
return r;
copy = strdup(source);
if (!copy)
return -ENOMEM;
old = hashmap_get(c->load_credentials, id);
if (old) {
free_and_replace(old->path, copy);
old->encrypted = streq(name, "LoadCredentialEncrypted");
} else {
_cleanup_(exec_load_credential_freep) ExecLoadCredential *lc = NULL;
lc = new(ExecLoadCredential, 1);
if (!lc)
return -ENOMEM;
*lc = (ExecLoadCredential) {
.id = strdup(id),
.path = TAKE_PTR(copy),
.encrypted = streq(name, "LoadCredentialEncrypted"),
};
if (!lc->id)
return -ENOMEM;
r = hashmap_ensure_put(&c->load_credentials, &exec_load_credential_hash_ops, lc->id, lc);
if (r < 0)
return r;
TAKE_PTR(lc);
}
(void) unit_write_settingf(u, flags|UNIT_ESCAPE_SPECIFIERS, name, "%s=%s:%s", name, id, source); (void) unit_write_settingf(u, flags|UNIT_ESCAPE_SPECIFIERS, name, "%s=%s:%s", name, id, source);
} }
@ -2046,7 +2089,7 @@ int bus_exec_context_set_transient_property(
return r; return r;
if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) { if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) {
c->load_credentials = strv_free(c->load_credentials); c->load_credentials = hashmap_free(c->load_credentials);
(void) unit_write_settingf(u, flags, name, "%s=", name); (void) unit_write_settingf(u, flags, name, "%s=", name);
} }

View File

@ -46,6 +46,7 @@
#include "cgroup-setup.h" #include "cgroup-setup.h"
#include "chown-recursive.h" #include "chown-recursive.h"
#include "cpu-set-util.h" #include "cpu-set-util.h"
#include "creds-util.h"
#include "data-fd-util.h" #include "data-fd-util.h"
#include "def.h" #include "def.h"
#include "env-file.h" #include "env-file.h"
@ -1454,7 +1455,7 @@ static bool exec_context_has_credentials(const ExecContext *context) {
assert(context); assert(context);
return !hashmap_isempty(context->set_credentials) || return !hashmap_isempty(context->set_credentials) ||
context->load_credentials; !hashmap_isempty(context->load_credentials);
} }
#if HAVE_SECCOMP #if HAVE_SECCOMP
@ -2486,7 +2487,7 @@ static int write_credential(
return -errno; return -errno;
} }
r = loop_write(fd, data, size, /* do_pool = */ false); r = loop_write(fd, data, size, /* do_poll = */ false);
if (r < 0) if (r < 0)
return r; return r;
@ -2519,8 +2520,6 @@ static int write_credential(
return 0; return 0;
} }
#define CREDENTIALS_BYTES_MAX (1024LU * 1024LU) /* Refuse to pass more than 1M, after all this is unswappable memory */
static int acquire_credentials( static int acquire_credentials(
const ExecContext *context, const ExecContext *context,
const ExecParameters *params, const ExecParameters *params,
@ -2529,10 +2528,10 @@ static int acquire_credentials(
uid_t uid, uid_t uid,
bool ownership_ok) { bool ownership_ok) {
uint64_t left = CREDENTIALS_BYTES_MAX; uint64_t left = CREDENTIALS_TOTAL_SIZE_MAX;
_cleanup_close_ int dfd = -1; _cleanup_close_ int dfd = -1;
ExecLoadCredential *lc;
ExecSetCredential *sc; ExecSetCredential *sc;
char **id, **fn;
int r; int r;
assert(context); assert(context);
@ -2542,39 +2541,23 @@ static int acquire_credentials(
if (dfd < 0) if (dfd < 0)
return -errno; return -errno;
/* First we use the literally specified credentials. Note that they might be overridden again below, /* First, load credentials off disk (or acquire via AF_UNIX socket) */
* and thus act as a "default" if the same credential is specified multiple times */ HASHMAP_FOREACH(lc, context->load_credentials) {
HASHMAP_FOREACH(sc, context->set_credentials) { ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER;
size_t add;
add = strlen(sc->id) + sc->size;
if (add > left)
return -E2BIG;
r = write_credential(dfd, sc->id, sc->data, sc->size, uid, ownership_ok);
if (r < 0)
return r;
left -= add;
}
/* Then, load credential off disk (or acquire via AF_UNIX socket) */
STRV_FOREACH_PAIR(id, fn, context->load_credentials) {
ReadFullFileFlags flags = READ_FULL_FILE_SECURE;
_cleanup_(erase_and_freep) char *data = NULL; _cleanup_(erase_and_freep) char *data = NULL;
_cleanup_free_ char *j = NULL, *bindname = NULL; _cleanup_free_ char *j = NULL, *bindname = NULL;
bool missing_ok = true; bool missing_ok = true;
const char *source; const char *source;
size_t size, add; size_t size, add;
if (path_is_absolute(*fn)) { if (path_is_absolute(lc->path)) {
/* If this is an absolute path, read the data directly from it, and support AF_UNIX sockets */ /* If this is an absolute path, read the data directly from it, and support AF_UNIX sockets */
source = *fn; source = lc->path;
flags |= READ_FULL_FILE_CONNECT_SOCKET; flags |= READ_FULL_FILE_CONNECT_SOCKET;
/* Pass some minimal info about the unit and the credential name we are looking to acquire /* Pass some minimal info about the unit and the credential name we are looking to acquire
* via the source socket address in case we read off an AF_UNIX socket. */ * via the source socket address in case we read off an AF_UNIX socket. */
if (asprintf(&bindname, "@%" PRIx64"/unit/%s/%s", random_u64(), unit, *id) < 0) if (asprintf(&bindname, "@%" PRIx64"/unit/%s/%s", random_u64(), unit, lc->id) < 0)
return -ENOMEM; return -ENOMEM;
missing_ok = false; missing_ok = false;
@ -2583,7 +2566,7 @@ static int acquire_credentials(
/* If this is a relative path, take it relative to the credentials we received /* If this is a relative path, take it relative to the credentials we received
* ourselves. We don't support the AF_UNIX stuff in this mode, since we are operating * ourselves. We don't support the AF_UNIX stuff in this mode, since we are operating
* on a credential store, i.e. this is guaranteed to be regular files. */ * on a credential store, i.e. this is guaranteed to be regular files. */
j = path_join(params->received_credentials, *fn); j = path_join(params->received_credentials, lc->path);
if (!j) if (!j)
return -ENOMEM; return -ENOMEM;
@ -2592,34 +2575,87 @@ static int acquire_credentials(
source = NULL; source = NULL;
if (source) if (source)
r = read_full_file_full(AT_FDCWD, source, UINT64_MAX, SIZE_MAX, flags, bindname, &data, &size); r = read_full_file_full(
AT_FDCWD, source,
UINT64_MAX,
lc->encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX,
flags | (lc->encrypted ? READ_FULL_FILE_UNBASE64 : 0),
bindname,
&data, &size);
else else
r = -ENOENT; r = -ENOENT;
if (r == -ENOENT && (missing_ok || faccessat(dfd, *id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)) { if (r == -ENOENT && (missing_ok || hashmap_contains(context->set_credentials, lc->id))) {
/* Make a missing inherited credential non-fatal, let's just continue. After all apps /* Make a missing inherited credential non-fatal, let's just continue. After all apps
* will get clear errors if we don't pass such a missing credential on as they * will get clear errors if we don't pass such a missing credential on as they
* themselves will get ENOENT when trying to read them, which should not be much * themselves will get ENOENT when trying to read them, which should not be much
* worse than when we handle the error here and make it fatal. * worse than when we handle the error here and make it fatal.
* *
* Also, if the source file doesn't exist, but we already acquired the key otherwise, * Also, if the source file doesn't exist, but a fallback is set via SetCredentials=
* then don't fail either. */ * we are fine, too. */
log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", *fn); log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", lc->path);
continue; continue;
} }
if (r < 0) if (r < 0)
return log_debug_errno(r, "Failed to read credential '%s': %m", *fn); return log_debug_errno(r, "Failed to read credential '%s': %m", lc->path);
add = strlen(*id) + size; if (lc->encrypted) {
_cleanup_free_ void *plaintext = NULL;
size_t plaintext_size = 0;
r = decrypt_credential_and_warn(lc->id, now(CLOCK_REALTIME), NULL, data, size, &plaintext, &plaintext_size);
if (r < 0)
return r;
free_and_replace(data, plaintext);
size = plaintext_size;
}
add = strlen(lc->id) + size;
if (add > left) if (add > left)
return -E2BIG; return -E2BIG;
r = write_credential(dfd, *id, data, size, uid, ownership_ok); r = write_credential(dfd, lc->id, data, size, uid, ownership_ok);
if (r < 0) if (r < 0)
return r; return r;
left -= add; left -= add;
} }
/* First we use the literally specified credentials. Note that they might be overridden again below,
* and thus act as a "default" if the same credential is specified multiple times */
HASHMAP_FOREACH(sc, context->set_credentials) {
_cleanup_(erase_and_freep) void *plaintext = NULL;
const char *data;
size_t size, add;
if (faccessat(dfd, sc->id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
continue;
if (errno != ENOENT)
return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sc->id);
if (sc->encrypted) {
r = decrypt_credential_and_warn(sc->id, now(CLOCK_REALTIME), NULL, sc->data, sc->size, &plaintext, &size);
if (r < 0)
return r;
data = plaintext;
} else {
data = sc->data;
size = sc->size;
}
add = strlen(sc->id) + size;
if (add > left)
return -E2BIG;
r = write_credential(dfd, sc->id, data, size, uid, ownership_ok);
if (r < 0)
return r;
left -= add;
}
if (fchmod(dfd, 0500) < 0) /* Now take away the "w" bit */ if (fchmod(dfd, 0500) < 0) /* Now take away the "w" bit */
return -errno; return -errno;
@ -2715,7 +2751,7 @@ static int setup_credentials_internal(
} else if (try == 1) { } else if (try == 1) {
_cleanup_free_ char *opts = NULL; _cleanup_free_ char *opts = NULL;
if (asprintf(&opts, "mode=0700,nr_inodes=1024,size=%lu", CREDENTIALS_BYTES_MAX) < 0) if (asprintf(&opts, "mode=0700,nr_inodes=1024,size=%zu", (size_t) CREDENTIALS_TOTAL_SIZE_MAX) < 0)
return -ENOMEM; return -ENOMEM;
/* Fall back to "tmpfs" otherwise */ /* Fall back to "tmpfs" otherwise */
@ -4928,7 +4964,7 @@ void exec_context_done(ExecContext *c) {
c->log_namespace = mfree(c->log_namespace); c->log_namespace = mfree(c->log_namespace);
c->load_credentials = strv_free(c->load_credentials); c->load_credentials = hashmap_free(c->load_credentials);
c->set_credentials = hashmap_free(c->set_credentials); c->set_credentials = hashmap_free(c->set_credentials);
} }
@ -6597,7 +6633,17 @@ ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc) {
return mfree(sc); return mfree(sc);
} }
ExecLoadCredential *exec_load_credential_free(ExecLoadCredential *lc) {
if (!lc)
return NULL;
free(lc->id);
free(lc->path);
return mfree(lc);
}
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(exec_set_credential_hash_ops, char, string_hash_func, string_compare_func, ExecSetCredential, exec_set_credential_free); DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(exec_set_credential_hash_ops, char, string_hash_func, string_compare_func, ExecSetCredential, exec_set_credential_free);
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(exec_load_credential_hash_ops, char, string_hash_func, string_compare_func, ExecLoadCredential, exec_load_credential_free);
static const char* const exec_input_table[_EXEC_INPUT_MAX] = { static const char* const exec_input_table[_EXEC_INPUT_MAX] = {
[EXEC_INPUT_NULL] = "null", [EXEC_INPUT_NULL] = "null",

View File

@ -150,9 +150,16 @@ typedef enum ExecCleanMask {
_EXEC_CLEAN_MASK_INVALID = -EINVAL, _EXEC_CLEAN_MASK_INVALID = -EINVAL,
} ExecCleanMask; } ExecCleanMask;
/* A credential configured with LoadCredential= */
typedef struct ExecLoadCredential {
char *id, *path;
bool encrypted;
} ExecLoadCredential;
/* A credential configured with SetCredential= */ /* A credential configured with SetCredential= */
typedef struct ExecSetCredential { typedef struct ExecSetCredential {
char *id; char *id;
bool encrypted;
void *data; void *data;
size_t size; size_t size;
} ExecSetCredential; } ExecSetCredential;
@ -325,7 +332,7 @@ struct ExecContext {
usec_t timeout_clean_usec; usec_t timeout_clean_usec;
Hashmap *set_credentials; /* output id → ExecSetCredential */ Hashmap *set_credentials; /* output id → ExecSetCredential */
char **load_credentials; /* pairs of output id, path/input id */ Hashmap *load_credentials; /* output id → ExecLoadCredential */
}; };
static inline bool exec_context_restrict_namespaces_set(const ExecContext *c) { static inline bool exec_context_restrict_namespaces_set(const ExecContext *c) {
@ -458,7 +465,11 @@ bool exec_context_get_cpu_affinity_from_numa(const ExecContext *c);
ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc); ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc);
DEFINE_TRIVIAL_CLEANUP_FUNC(ExecSetCredential*, exec_set_credential_free); DEFINE_TRIVIAL_CLEANUP_FUNC(ExecSetCredential*, exec_set_credential_free);
ExecLoadCredential *exec_load_credential_free(ExecLoadCredential *lc);
DEFINE_TRIVIAL_CLEANUP_FUNC(ExecLoadCredential*, exec_load_credential_free);
extern const struct hash_ops exec_set_credential_hash_ops; extern const struct hash_ops exec_set_credential_hash_ops;
extern const struct hash_ops exec_load_credential_hash_ops;
const char* exec_output_to_string(ExecOutput i) _const_; const char* exec_output_to_string(ExecOutput i) _const_;
ExecOutput exec_output_from_string(const char *s) _pure_; ExecOutput exec_output_from_string(const char *s) _pure_;

View File

@ -139,7 +139,9 @@
{{type}}.ConfigurationDirectoryMode, config_parse_mode, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].mode) {{type}}.ConfigurationDirectoryMode, config_parse_mode, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].mode)
{{type}}.ConfigurationDirectory, config_parse_exec_directories, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].paths) {{type}}.ConfigurationDirectory, config_parse_exec_directories, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].paths)
{{type}}.SetCredential, config_parse_set_credential, 0, offsetof({{type}}, exec_context) {{type}}.SetCredential, config_parse_set_credential, 0, offsetof({{type}}, exec_context)
{{type}}.SetCredentialEncrypted, config_parse_set_credential, 1, offsetof({{type}}, exec_context)
{{type}}.LoadCredential, config_parse_load_credential, 0, offsetof({{type}}, exec_context) {{type}}.LoadCredential, config_parse_load_credential, 0, offsetof({{type}}, exec_context)
{{type}}.LoadCredentialEncrypted, config_parse_load_credential, 1, offsetof({{type}}, exec_context)
{{type}}.TimeoutCleanSec, config_parse_sec, 0, offsetof({{type}}, exec_context.timeout_clean_usec) {{type}}.TimeoutCleanSec, config_parse_sec, 0, offsetof({{type}}, exec_context.timeout_clean_usec)
{% if HAVE_PAM %} {% if HAVE_PAM %}
{{type}}.PAMName, config_parse_unit_string_printf, 0, offsetof({{type}}, exec_context.pam_name) {{type}}.PAMName, config_parse_unit_string_printf, 0, offsetof({{type}}, exec_context.pam_name)

View File

@ -4469,12 +4469,15 @@ int config_parse_set_credential(
void *data, void *data,
void *userdata) { void *userdata) {
_cleanup_free_ char *word = NULL, *k = NULL, *unescaped = NULL; _cleanup_free_ char *word = NULL, *k = NULL;
_cleanup_free_ void *d = NULL;
ExecContext *context = data; ExecContext *context = data;
ExecSetCredential *old; ExecSetCredential *old;
Unit *u = userdata; Unit *u = userdata;
const char *p; bool encrypted = ltype;
int r, l; const char *p = rvalue;
size_t size;
int r;
assert(filename); assert(filename);
assert(lvalue); assert(lvalue);
@ -4487,7 +4490,6 @@ int config_parse_set_credential(
return 0; return 0;
} }
p = rvalue;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM) if (r == -ENOMEM)
return log_oom(); return log_oom();
@ -4506,33 +4508,51 @@ int config_parse_set_credential(
return 0; return 0;
} }
/* We support escape codes here, so that users can insert trailing \n if they like */ if (encrypted) {
l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped); r = unbase64mem_full(p, SIZE_MAX, true, &d, &size);
if (l < 0) { if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, l, "Can't unescape \"%s\", ignoring: %m", p); log_syntax(unit, LOG_WARNING, filename, line, r, "Encrypted credential data not valid Base64 data, ignoring.");
return 0; return 0;
}
} else {
char *unescaped = NULL;
int l;
/* We support escape codes here, so that users can insert trailing \n if they like */
l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
if (l < 0) {
log_syntax(unit, LOG_WARNING, filename, line, l, "Can't unescape \"%s\", ignoring: %m", p);
return 0;
}
d = unescaped;
size = l;
} }
old = hashmap_get(context->set_credentials, k); old = hashmap_get(context->set_credentials, k);
if (old) { if (old) {
free_and_replace(old->data, unescaped); free_and_replace(old->data, d);
old->size = l; old->size = size;
old->encrypted = encrypted;
} else { } else {
_cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL; _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
sc = new0(ExecSetCredential, 1); sc = new(ExecSetCredential, 1);
if (!sc) if (!sc)
return log_oom(); return log_oom();
sc->id = TAKE_PTR(k); *sc = (ExecSetCredential) {
sc->data = TAKE_PTR(unescaped); .id = TAKE_PTR(k),
sc->size = l; .data = TAKE_PTR(d),
.size = size,
.encrypted = encrypted,
};
r = hashmap_ensure_put(&context->set_credentials, &exec_set_credential_hash_ops, sc->id, sc); r = hashmap_ensure_put(&context->set_credentials, &exec_set_credential_hash_ops, sc->id, sc);
if (r == -ENOMEM) if (r == -ENOMEM)
return log_oom(); return log_oom();
if (r < 0) { if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, l, log_syntax(unit, LOG_WARNING, filename, line, r,
"Duplicated credential value '%s', ignoring assignment: %s", sc->id, rvalue); "Duplicated credential value '%s', ignoring assignment: %s", sc->id, rvalue);
return 0; return 0;
} }
@ -4557,6 +4577,8 @@ int config_parse_load_credential(
_cleanup_free_ char *word = NULL, *k = NULL, *q = NULL; _cleanup_free_ char *word = NULL, *k = NULL, *q = NULL;
ExecContext *context = data; ExecContext *context = data;
ExecLoadCredential *old;
bool encrypted = ltype;
Unit *u = userdata; Unit *u = userdata;
const char *p; const char *p;
int r; int r;
@ -4568,7 +4590,7 @@ int config_parse_load_credential(
if (isempty(rvalue)) { if (isempty(rvalue)) {
/* Empty assignment resets the list */ /* Empty assignment resets the list */
context->load_credentials = strv_free(context->load_credentials); context->load_credentials = hashmap_free(context->load_credentials);
return 0; return 0;
} }
@ -4604,14 +4626,39 @@ int config_parse_load_credential(
return 0; return 0;
} }
if (path_is_absolute(q) ? !path_is_normalized(q) : !credential_name_valid(q)) { if (path_is_absolute(q) ? !path_is_normalized(q) : !credential_name_valid(q)) {
log_syntax(unit, LOG_WARNING, filename, line, r, "Credential source \"%s\" not valid, ignoring.", q); log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential source \"%s\" not valid, ignoring.", q);
return 0; return 0;
} }
} }
r = strv_consume_pair(&context->load_credentials, TAKE_PTR(k), TAKE_PTR(q)); old = hashmap_get(context->load_credentials, k);
if (r < 0) if (old) {
return log_oom(); free_and_replace(old->path, q);
old->encrypted = encrypted;
} else {
_cleanup_(exec_load_credential_freep) ExecLoadCredential *lc = NULL;
lc = new(ExecLoadCredential, 1);
if (!lc)
return log_oom();
*lc = (ExecLoadCredential) {
.id = TAKE_PTR(k),
.path = TAKE_PTR(q),
.encrypted = encrypted,
};
r = hashmap_ensure_put(&context->load_credentials, &exec_load_credential_hash_ops, lc->id, lc);
if (r == -ENOMEM)
return log_oom();
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Duplicated credential value '%s', ignoring assignment: %s", lc->id, rvalue);
return 0;
}
TAKE_PTR(lc);
}
return 0; return 0;
} }

812
src/creds/creds.c Normal file
View File

@ -0,0 +1,812 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <getopt.h>
#include "creds-util.h"
#include "dirent-util.h"
#include "escape.h"
#include "fileio.h"
#include "format-table.h"
#include "hexdecoct.h"
#include "io-util.h"
#include "json.h"
#include "main-func.h"
#include "memory-util.h"
#include "missing_magic.h"
#include "pager.h"
#include "parse-argument.h"
#include "pretty-print.h"
#include "process-util.h"
#include "stat-util.h"
#include "string-table.h"
#include "terminal-util.h"
#include "tpm2-util.h"
#include "verbs.h"
typedef enum TranscodeMode {
TRANSCODE_OFF,
TRANSCODE_BASE64,
TRANSCODE_UNBASE64,
TRANSCODE_HEX,
TRANSCODE_UNHEX,
_TRANSCODE_MAX,
_TRANSCODE_INVALID = -EINVAL,
} TranscodeMode;
static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_system = false;
static TranscodeMode arg_transcode = TRANSCODE_OFF;
static int arg_newline = -1;
static sd_id128_t arg_with_key = SD_ID128_NULL;
static const char *arg_tpm2_device = NULL;
static uint32_t arg_tpm2_pcr_mask = UINT32_MAX;
static const char *arg_name = NULL;
static bool arg_name_any = false;
static usec_t arg_timestamp = USEC_INFINITY;
static usec_t arg_not_after = USEC_INFINITY;
static bool arg_pretty = false;
static const char* transcode_mode_table[_TRANSCODE_MAX] = {
[TRANSCODE_OFF] = "off",
[TRANSCODE_BASE64] = "base64",
[TRANSCODE_UNBASE64] = "unbase64",
[TRANSCODE_HEX] = "hex",
[TRANSCODE_UNHEX] = "unhex",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(transcode_mode, TranscodeMode);
static int open_credential_directory(DIR **ret) {
_cleanup_free_ char *j = NULL;
const char *p;
DIR *d;
int r;
if (arg_system) {
_cleanup_free_ char *cd = NULL;
r = getenv_for_pid(1, "CREDENTIALS_DIRECTORY", &cd);
if (r < 0)
return r;
if (!cd)
return -ENXIO;
if (!path_is_absolute(cd) || !path_is_normalized(cd))
return -EINVAL;
j = path_join("/proc/1/root", cd);
if (!j)
return -ENOMEM;
p = j;
} else {
r = get_credentials_dir(&p);
if (r < 0)
return r;
}
d = opendir(p);
if (!d)
return -errno;
*ret = d;
return 0;
}
static int verb_list(int argc, char **argv, void *userdata) {
_cleanup_(table_unrefp) Table *t = NULL;
_cleanup_(closedirp) DIR *d = NULL;
int r;
r = open_credential_directory(&d);
if (r == -ENXIO)
return log_error_errno(r, "No credentials received. (i.e. $CREDENTIALS_PATH not set or pointing to empty directory.)");
if (r < 0)
return log_error_errno(r, "Failed to open credentials directory: %m");
t = table_new("name", "secure", "size");
if (!t)
return log_oom();
(void) table_set_align_percent(t, table_get_cell(t, 0, 2), 100);
for (;;) {
_cleanup_close_ int fd = -1;
const char *secure, *secure_color = NULL;
struct dirent *de;
struct stat st;
errno = 0;
de = readdir_no_dot(d);
if (!de) {
if (errno == 0)
break;
return log_error_errno(errno, "Failed to read credentials directory: %m");
}
if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN))
continue;
if (!credential_name_valid(de->d_name))
continue;
fd = openat(dirfd(d), de->d_name, O_PATH|O_CLOEXEC|O_NOFOLLOW);
if (fd < 0) {
if (errno == ENOENT) /* Vanished by now? */
continue;
return log_error_errno(errno, "Failed to open credential '%s': %m", de->d_name);
}
if (fstat(fd, &st) < 0)
return log_error_errno(errno, "Failed to stat credential '%s': %m", de->d_name);
if (!S_ISREG(st.st_mode))
continue;
if ((st.st_mode & 0377) != 0) {
secure = "insecure"; /* Anything that is accessible more than read-only to its owner is insecure */
secure_color = ansi_highlight_red();
} else {
r = fd_is_fs_type(fd, RAMFS_MAGIC);
if (r < 0)
return log_error_errno(r, "Failed to determine backing file system of '%s': %m", de->d_name);
secure = r ? "secure" : "weak"; /* ramfs is not swappable, hence "secure", everything else is "weak" */
secure_color = r ? ansi_highlight_green() : ansi_highlight_yellow4();
}
r = table_add_many(
t,
TABLE_STRING, de->d_name,
TABLE_STRING, secure,
TABLE_SET_COLOR, secure_color,
TABLE_SIZE, (uint64_t) st.st_size);
if (r < 0)
return table_log_add_error(r);
}
if ((arg_json_format_flags & JSON_FORMAT_OFF) && table_get_rows(t) <= 1) {
log_info("No credentials");
return 0;
}
return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
}
static int transcode(
const void *input,
size_t input_size,
void **ret_output,
size_t *ret_output_size) {
int r;
assert(input);
assert(input_size);
assert(ret_output);
assert(ret_output_size);
switch (arg_transcode) {
case TRANSCODE_BASE64: {
char *buf = NULL;
ssize_t l;
l = base64mem_full(input, input_size, 79, &buf);
if (l < 0)
return l;
*ret_output = buf;
*ret_output_size = l;
return 0;
}
case TRANSCODE_UNBASE64:
r = unbase64mem_full(input, input_size, true, ret_output, ret_output_size);
if (r == -EPIPE) /* Uneven number of chars */
return -EINVAL;
return r;
case TRANSCODE_HEX: {
char *buf;
buf = hexmem(input, input_size);
if (!buf)
return -ENOMEM;
*ret_output = buf;
*ret_output_size = input_size * 2;
return 0;
}
case TRANSCODE_UNHEX:
r = unhexmem_full(input, input_size, true, ret_output, ret_output_size);
if (r == -EPIPE) /* Uneven number of chars */
return -EINVAL;
return r;
default:
assert_not_reached("Unexpected transcoding mode");
}
}
static int print_newline(FILE *f, const char *data, size_t l) {
int fd;
assert(f);
assert(data || l == 0);
/* If turned off explicitly, don't print newline */
if (arg_newline == 0)
return 0;
/* If data already has newline, don't print either */
if (l > 0 && data[l-1] == '\n')
return 0;
/* Don't bother unless this is a tty */
fd = fileno(f);
if (fd >= 0 && isatty(fd) <= 0)
return 0;
if (fputc('\n', f) != '\n')
return log_error_errno(errno, "Failed to write trailing newline: %m");
return 1;
}
static int write_blob(FILE *f, const void *data, size_t size) {
_cleanup_(erase_and_freep) void *transcoded = NULL;
int r;
if (arg_transcode == TRANSCODE_OFF &&
arg_json_format_flags != JSON_FORMAT_OFF) {
_cleanup_(erase_and_freep) char *suffixed = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
if (memchr(data, 0, size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Credential data contains embedded NUL, can't parse as JSON.");
suffixed = memdup_suffix0(data, size);
if (!suffixed)
return log_oom();
r = json_parse(suffixed, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
if (r < 0)
return log_error_errno(r, "Failed to parse JSON: %m");
json_variant_dump(v, arg_json_format_flags, f, NULL);
return 0;
}
if (arg_transcode != TRANSCODE_OFF) {
r = transcode(data, size, &transcoded, &size);
if (r < 0)
return log_error_errno(r, "Failed to transcode data: %m");
data = transcoded;
}
if (fwrite(data, 1, size, f) != size)
return log_error_errno(errno, "Failed to write credential data: %m");
r = print_newline(f, data, size);
if (r < 0)
return r;
if (fflush(f) != 0)
return log_error_errno(errno, "Failed to flush output: %m");
return 0;
}
static int verb_cat(int argc, char **argv, void *userdata) {
_cleanup_(closedirp) DIR *d = NULL;
int r, ret = 0;
char **cn;
r = open_credential_directory(&d);
if (r == -ENXIO)
return log_error_errno(r, "No credentials passed.");
if (r < 0)
return log_error_errno(r, "Failed to open credentials directory: %m");
STRV_FOREACH(cn, strv_skip(argv, 1)) {
_cleanup_(erase_and_freep) void *data = NULL;
size_t size = 0;
if (!credential_name_valid(*cn)) {
log_error("Credential name '%s' is not valid.", *cn);
if (ret >= 0)
ret = -EINVAL;
continue;
}
r = read_full_file_full(
dirfd(d), *cn,
UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE,
NULL,
(char**) &data, &size);
if (r < 0) {
log_error_errno(r, "Failed to read credential '%s': %m", *cn);
if (ret >= 0)
ret = r;
continue;
}
r = write_blob(stdout, data, size);
if (r < 0)
return r;
}
return ret;
}
static int verb_encrypt(int argc, char **argv, void *userdata) {
_cleanup_free_ char *base64_buf = NULL, *fname = NULL;
_cleanup_(erase_and_freep) char *plaintext = NULL;
const char *input_path, *output_path, *name;
_cleanup_free_ void *output = NULL;
size_t plaintext_size, output_size;
ssize_t base64_size;
usec_t timestamp;
int r;
assert(argc == 3);
input_path = (isempty(argv[1]) || streq(argv[1], "-")) ? NULL : argv[1];
if (input_path)
r = read_full_file_full(AT_FDCWD, input_path, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &plaintext, &plaintext_size);
else
r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, &plaintext, &plaintext_size);
if (r == -E2BIG)
return log_error_errno(r, "Plaintext too long for credential (allowed size: %zu).", (size_t) CREDENTIAL_SIZE_MAX);
if (r < 0)
return log_error_errno(r, "Failed to read plaintext: %m");
output_path = (isempty(argv[2]) || streq(argv[2], "-")) ? NULL : argv[2];
if (arg_name_any)
name = NULL;
else if (arg_name)
name = arg_name;
else if (output_path) {
r = path_extract_filename(output_path, &fname);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from '%s': %m", output_path);
if (r == O_DIRECTORY)
return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", output_path);
name = fname;
} else {
log_warning("No credential name specified, not embedding credential name in encrypted data. (Disable this warning with --name=)");
name = NULL;
}
timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME);
if (arg_not_after != USEC_INFINITY && arg_not_after < timestamp)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid.");
r = encrypt_credential_and_warn(
arg_with_key,
name,
timestamp,
arg_not_after,
arg_tpm2_device,
arg_tpm2_pcr_mask,
plaintext, plaintext_size,
&output, &output_size);
if (r < 0)
return r;
base64_size = base64mem_full(output, output_size, arg_pretty ? 69 : 79, &base64_buf);
if (base64_size < 0)
return base64_size;
if (arg_pretty) {
_cleanup_free_ char *escaped = NULL, *indented = NULL, *j = NULL;
if (name) {
escaped = cescape(name);
if (!escaped)
return log_oom();
}
indented = strreplace(base64_buf, "\n", " \\\n ");
if (!indented)
return log_oom();
j = strjoin("SetCredentialEncrypted=", name, ": \\\n ", indented, "\n");
if (!j)
return log_oom();
free_and_replace(base64_buf, j);
}
if (output_path)
r = write_string_file(output_path, base64_buf, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE);
else
r = write_string_stream(stdout, base64_buf, 0);
if (r < 0)
return log_error_errno(r, "Failed to write result: %m");
return EXIT_SUCCESS;
}
static int verb_decrypt(int argc, char **argv, void *userdata) {
_cleanup_(erase_and_freep) void *plaintext = NULL;
_cleanup_free_ char *input = NULL, *fname = NULL;
_cleanup_fclose_ FILE *output_file = NULL;
const char *input_path, *output_path, *name;
size_t input_size, plaintext_size;
usec_t timestamp;
FILE *f;
int r;
assert(IN_SET(argc, 2, 3));
input_path = (isempty(argv[1]) || streq(argv[1], "-")) ? NULL : argv[1];
if (input_path)
r = read_full_file_full(AT_FDCWD, argv[1], UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &input, &input_size);
else
r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, &input, &input_size);
if (r == -E2BIG)
return log_error_errno(r, "Data too long for encrypted credential (allowed size: %zu).", (size_t) CREDENTIAL_ENCRYPTED_SIZE_MAX);
if (r < 0)
return log_error_errno(r, "Failed to read encrypted credential data: %m");
output_path = (argc < 3 || isempty(argv[2]) || streq(argv[2], "-")) ? NULL : argv[2];
if (arg_name_any)
name = NULL;
else if (arg_name)
name = arg_name;
else if (input_path) {
r = path_extract_filename(input_path, &fname);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from '%s': %m", input_path);
if (r == O_DIRECTORY)
return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", input_path);
name = fname;
} else {
log_warning("No credential name specified, not validating credential name embedded in encrypted data. (Disable this warning with --name=.)");
name = NULL;
}
timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME);
r = decrypt_credential_and_warn(
name,
timestamp,
arg_tpm2_device,
input, input_size,
&plaintext, &plaintext_size);
if (r < 0)
return r;
if (output_path) {
output_file = fopen(output_path, "we");
if (!output_file)
return log_error_errno(errno, "Failed to create output file '%s': %m", output_path);
f = output_file;
} else
f = stdout;
r = write_blob(f, plaintext, plaintext_size);
if (r < 0)
return r;
return EXIT_SUCCESS;
}
static int verb_setup(int argc, char **argv, void *userdata) {
size_t size;
int r;
r = get_credential_host_secret(CREDENTIAL_SECRET_GENERATE|CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED, NULL, &size);
if (r < 0)
return log_error_errno(r, "Failed to setup credentials host key: %m");
log_info("%zu byte credentials host key set up.", size);
return EXIT_SUCCESS;
}
static int verb_help(int argc, char **argv, void *userdata) {
_cleanup_free_ char *link = NULL;
int r;
r = terminal_urlify_man("systemd-creds", "1", &link);
if (r < 0)
return log_oom();
printf("%1$s [OPTIONS...] COMMAND ...\n"
"\n%5$sDisplay and Process Credentials.%6$s\n"
"\n%3$sCommands:%4$s\n"
" list Show installed and available versions\n"
" cat CREDENTIAL... Show specified credentials\n"
" setup Generate credentials host key, if not existing yet\n"
" encrypt INPUT OUTPUT Encrypt plaintext credential file and write to\n"
" ciphertext credential file\n"
" decrypt INPUT [OUTPUT] Decrypt ciphertext credential file and write to\n"
" plaintext credential file\n"
" -h --help Show this help\n"
" --version Show package version\n"
"\n%3$sOptions:%4$s\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Do not show the headers and footers\n"
" --json=pretty|short|off\n"
" Generate JSON output\n"
" --system Show credentials passed to system\n"
" --transcode=base64|unbase64|hex|unhex\n"
" Transcode credential data\n"
" --newline=auto|yes|no\n"
" Suffix output with newline\n"
" -p --pretty Output as SetCredentialEncrypted= line\n"
" --name=NAME Override filename included in encrypted credential\n"
" --timestamp=TIME Include specified timestamp in encrypted credential\n"
" --not-after=TIME Include specified invalidation time in encrypted\n"
" credential\n"
" --with-key=host|tpm2|host+tpm2|auto\n"
" Which keys to encrypt with\n"
" -H Shortcut for --with-key=host\n"
" -T Shortcut for --with-key=tpm2\n"
" --tpm2-device=PATH\n"
" Pick TPM2 device\n"
" --tpm2-pcrs=PCR1+PCR2+PCR3+…\n"
" Specify TPM2 PCRs to seal against\n"
"\nSee the %2$s for details.\n"
, program_invocation_short_name
, link
, ansi_underline(), ansi_normal()
, ansi_highlight(), ansi_normal()
);
return 0;
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
ARG_NO_PAGER,
ARG_NO_LEGEND,
ARG_JSON,
ARG_SYSTEM,
ARG_TRANSCODE,
ARG_NEWLINE,
ARG_WITH_KEY,
ARG_TPM2_DEVICE,
ARG_TPM2_PCRS,
ARG_NAME,
ARG_TIMESTAMP,
ARG_NOT_AFTER,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
{ "no-legend", no_argument, NULL, ARG_NO_LEGEND },
{ "json", required_argument, NULL, ARG_JSON },
{ "system", no_argument, NULL, ARG_SYSTEM },
{ "transcode", required_argument, NULL, ARG_TRANSCODE },
{ "newline", required_argument, NULL, ARG_NEWLINE },
{ "pretty", no_argument, NULL, 'p' },
{ "with-key", required_argument, NULL, ARG_WITH_KEY },
{ "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
{ "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS },
{ "name", required_argument, NULL, ARG_NAME },
{ "timestamp", required_argument, NULL, ARG_TIMESTAMP },
{ "not-after", required_argument, NULL, ARG_NOT_AFTER },
{}
};
int c, r;
assert(argc >= 0);
assert(argv);
while ((c = getopt_long(argc, argv, "hHTp", options, NULL)) >= 0) {
switch (c) {
case 'h':
return verb_help(0, NULL, NULL);
case ARG_VERSION:
return version();
case ARG_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
case ARG_NO_LEGEND:
arg_legend = false;
break;
case ARG_JSON:
r = parse_json_argument(optarg, &arg_json_format_flags);
if (r <= 0)
return r;
break;
case ARG_SYSTEM:
arg_system = true;
break;
case ARG_TRANSCODE:
if (parse_boolean(optarg) == 0) /* If specified as "false", turn transcoding off */
arg_transcode = TRANSCODE_OFF;
else {
TranscodeMode m;
m = transcode_mode_from_string(optarg);
if (m < 0)
return log_error_errno(m, "Failed to parse transcode mode: %m");
arg_transcode = m;
}
break;
case ARG_NEWLINE:
if (isempty(optarg) || streq(optarg, "auto"))
arg_newline = -1;
else {
bool b;
r = parse_boolean_argument("--newline=", optarg, &b);
if (r < 0)
return r;
arg_newline = b;
}
break;
case 'p':
arg_pretty = true;
break;
case ARG_WITH_KEY:
if (isempty(optarg) || streq(optarg, "auto"))
arg_with_key = SD_ID128_NULL;
else if (streq(optarg, "host"))
arg_with_key = CRED_AES256_GCM_BY_HOST;
else if (streq(optarg, "tpm2"))
arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC;
else if (STR_IN_SET(optarg, "host+tpm2", "tpm2+host"))
arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC;
else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown key type: %s", optarg);
break;
case 'H':
arg_with_key = CRED_AES256_GCM_BY_HOST;
break;
case 'T':
arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC;
break;
case ARG_TPM2_DEVICE: {
_cleanup_free_ char *device = NULL;
if (streq(optarg, "list"))
return tpm2_list_devices();
if (!streq(optarg, "auto")) {
device = strdup(optarg);
if (!device)
return log_oom();
}
arg_tpm2_device = TAKE_PTR(device);
break;
}
case ARG_TPM2_PCRS: {
uint32_t mask;
if (isempty(optarg)) {
arg_tpm2_pcr_mask = 0;
break;
}
r = tpm2_parse_pcrs(optarg, &mask);
if (r < 0)
return r;
if (arg_tpm2_pcr_mask == UINT32_MAX)
arg_tpm2_pcr_mask = mask;
else
arg_tpm2_pcr_mask |= mask;
break;
}
case ARG_NAME:
if (isempty(optarg)) {
arg_name = NULL;
arg_name_any = true;
break;
}
if (!credential_name_valid(optarg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name: %s", optarg);
arg_name = optarg;
arg_name_any = false;
break;
case ARG_TIMESTAMP:
r = parse_timestamp(optarg, &arg_timestamp);
if (r < 0)
return log_error_errno(r, "Failed to parse timestamp: %s", optarg);
break;
case ARG_NOT_AFTER:
r = parse_timestamp(optarg, &arg_not_after);
if (r < 0)
return log_error_errno(r, "Failed to parse --not-after= timestamp: %s", optarg);
break;
case '?':
return -EINVAL;
default:
assert_not_reached("Unhandled option");
}
}
if (arg_tpm2_pcr_mask == UINT32_MAX)
arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT;
return 1;
}
static int creds_main(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "list", VERB_ANY, 1, VERB_DEFAULT, verb_list },
{ "cat", 2, VERB_ANY, 0, verb_cat },
{ "encrypt", 3, 3, 0, verb_encrypt },
{ "decrypt", 2, 3, 0, verb_decrypt },
{ "setup", VERB_ANY, 1, 0, verb_setup },
{ "help", VERB_ANY, 1, 0, verb_help },
{}
};
return dispatch_verb(argc, argv, verbs, NULL);
}
static int run(int argc, char *argv[]) {
int r;
log_setup();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
return creds_main(argc, argv);
}
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);

View File

@ -1902,19 +1902,10 @@ static int setup_keys(void) {
if (fd < 0) if (fd < 0)
return log_error_errno(fd, "Failed to open %s: %m", k); return log_error_errno(fd, "Failed to open %s: %m", k);
/* Enable secure remove, exclusion from dump, synchronous writing and in-place updating */ r = chattr_secret(fd, CHATTR_WARN_UNSUPPORTED_FLAGS);
static const unsigned chattr_flags[] = { if (r < 0)
FS_SECRM_FL, log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING,
FS_NODUMP_FL, r, "Failed to set file attributes on '%s', ignoring: %m", k);
FS_SYNC_FL,
FS_NOCOW_FL,
};
for (size_t j = 0; j < ELEMENTSOF(chattr_flags); j++) {
r = chattr_fd(fd, chattr_flags[j], chattr_flags[j], NULL);
if (r < 0)
log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING, r,
"Failed to set file attribute 0x%x: %m", chattr_flags[j]);
}
struct FSSHeader h = { struct FSSHeader h = {
.signature = { 'K', 'S', 'H', 'H', 'R', 'H', 'L', 'P' }, .signature = { 'K', 'S', 'H', 'H', 'R', 'H', 'L', 'P' },

View File

@ -166,7 +166,8 @@ libsystemd_static = static_library(
include_directories : libsystemd_includes, include_directories : libsystemd_includes,
link_with : libbasic, link_with : libbasic,
dependencies : [threads, dependencies : [threads,
librt], librt,
libopenssl],
c_args : libsystemd_c_args) c_args : libsystemd_c_args)
libsystemd_sym = files('libsystemd.sym') libsystemd_sym = files('libsystemd.sym')

View File

@ -4,6 +4,11 @@
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#if HAVE_OPENSSL
#include <openssl/hmac.h>
#include <openssl/sha.h>
#endif
#include "sd-id128.h" #include "sd-id128.h"
#include "alloc-util.h" #include "alloc-util.h"
@ -11,7 +16,9 @@
#include "hexdecoct.h" #include "hexdecoct.h"
#include "id128-util.h" #include "id128-util.h"
#include "io-util.h" #include "io-util.h"
#if !HAVE_OPENSSL
#include "khash.h" #include "khash.h"
#endif
#include "macro.h" #include "macro.h"
#include "missing_syscall.h" #include "missing_syscall.h"
#include "random-util.h" #include "random-util.h"
@ -271,13 +278,28 @@ _public_ int sd_id128_randomize(sd_id128_t *ret) {
} }
static int get_app_specific(sd_id128_t base, sd_id128_t app_id, sd_id128_t *ret) { static int get_app_specific(sd_id128_t base, sd_id128_t app_id, sd_id128_t *ret) {
_cleanup_(khash_unrefp) khash *h = NULL;
sd_id128_t result; sd_id128_t result;
const void *p;
int r;
assert(ret); assert(ret);
#if HAVE_OPENSSL
/* We prefer doing this in-process, since we this means we are not dependent on kernel configuration,
* and this also works in locked down container environments. But some distros don't like OpenSSL's
* license and its (in-) compatibility with GPL2, hence also support khash */
uint8_t md[256/8];
if (!HMAC(EVP_sha256(),
&base, sizeof(base),
(const unsigned char*) &app_id, sizeof(app_id),
md, NULL))
return -ENOTRECOVERABLE;
/* Take only the first half. */
memcpy(&result, md, MIN(sizeof(md), sizeof(result)));
#else
_cleanup_(khash_unrefp) khash *h = NULL;
const void *p;
int r;
r = khash_new_with_key(&h, "hmac(sha256)", &base, sizeof(base)); r = khash_new_with_key(&h, "hmac(sha256)", &base, sizeof(base));
if (r < 0) if (r < 0)
return r; return r;
@ -292,6 +314,7 @@ static int get_app_specific(sd_id128_t base, sd_id128_t app_id, sd_id128_t *ret)
/* We chop off the trailing 16 bytes */ /* We chop off the trailing 16 bytes */
memcpy(&result, p, MIN(khash_get_size(h), sizeof(result))); memcpy(&result, p, MIN(khash_get_size(h), sizeof(result)));
#endif
*ret = id128_make_v4_uuid(result); *ret = id128_make_v4_uuid(result);
return 0; return 0;

View File

@ -57,21 +57,16 @@ int block_get_whole_disk(dev_t d, dev_t *ret) {
return 1; return 1;
} }
int get_block_device(const char *path, dev_t *ret) { int get_block_device_fd(int fd, dev_t *ret) {
_cleanup_close_ int fd = -1;
struct stat st; struct stat st;
int r; int r;
assert(path); assert(fd >= 0);
assert(ret); assert(ret);
/* Gets the block device directly backing a file system. If the block device is encrypted, returns /* Gets the block device directly backing a file system. If the block device is encrypted, returns
* the device mapper block device. */ * the device mapper block device. */
fd = open(path, O_RDONLY|O_NOFOLLOW|O_CLOEXEC);
if (fd < 0)
return -errno;
if (fstat(fd, &st)) if (fstat(fd, &st))
return -errno; return -errno;
@ -90,6 +85,19 @@ int get_block_device(const char *path, dev_t *ret) {
return 0; return 0;
} }
int get_block_device(const char *path, dev_t *ret) {
_cleanup_close_ int fd = -1;
assert(path);
assert(ret);
fd = open(path, O_RDONLY|O_NOFOLLOW|O_CLOEXEC);
if (fd < 0)
return -errno;
return get_block_device_fd(fd, ret);
}
int block_get_originating(dev_t dt, dev_t *ret) { int block_get_originating(dev_t dt, dev_t *ret) {
_cleanup_closedir_ DIR *d = NULL; _cleanup_closedir_ DIR *d = NULL;
_cleanup_free_ char *t = NULL; _cleanup_free_ char *t = NULL;
@ -326,6 +334,22 @@ static int blockdev_is_encrypted(const char *sysfs_path, unsigned depth_left) {
return found_encrypted; return found_encrypted;
} }
int fd_is_encrypted(int fd) {
char p[SYS_BLOCK_PATH_MAX(NULL)];
dev_t devt;
int r;
r = get_block_device_fd(fd, &devt);
if (r < 0)
return r;
if (r == 0) /* doesn't have a block device */
return false;
xsprintf_sys_block_path(p, NULL, devt);
return blockdev_is_encrypted(p, 10 /* safety net: maximum recursion depth */);
}
int path_is_encrypted(const char *path) { int path_is_encrypted(const char *path) {
char p[SYS_BLOCK_PATH_MAX(NULL)]; char p[SYS_BLOCK_PATH_MAX(NULL)];
dev_t devt; dev_t devt;

View File

@ -15,6 +15,7 @@
int block_get_whole_disk(dev_t d, dev_t *ret); int block_get_whole_disk(dev_t d, dev_t *ret);
int block_get_originating(dev_t d, dev_t *ret); int block_get_originating(dev_t d, dev_t *ret);
int get_block_device_fd(int fd, dev_t *ret);
int get_block_device(const char *path, dev_t *dev); int get_block_device(const char *path, dev_t *dev);
int get_block_device_harder(const char *path, dev_t *dev); int get_block_device_harder(const char *path, dev_t *dev);
@ -23,4 +24,5 @@ int lock_whole_block_device(dev_t devt, int operation);
int blockdev_partscan_enabled(int fd); int blockdev_partscan_enabled(int fd);
int fd_is_encrypted(int fd);
int path_is_encrypted(const char *path); int path_is_encrypted(const char *path);

View File

@ -1046,12 +1046,12 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
return 1; return 1;
} }
if (streq(field, "SetCredential")) { if (STR_IN_SET(field, "SetCredential", "SetCredentialEncrypted")) {
r = sd_bus_message_open_container(m, 'r', "sv"); r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0) if (r < 0)
return bus_log_create_error(r); return bus_log_create_error(r);
r = sd_bus_message_append_basic(m, 's', "SetCredential"); r = sd_bus_message_append_basic(m, 's', field);
if (r < 0) if (r < 0)
return bus_log_create_error(r); return bus_log_create_error(r);
@ -1062,21 +1062,16 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
if (isempty(eq)) if (isempty(eq))
r = sd_bus_message_append(m, "a(say)", 0); r = sd_bus_message_append(m, "a(say)", 0);
else { else {
_cleanup_free_ char *word = NULL, *unescaped = NULL; _cleanup_free_ char *word = NULL;
const char *p = eq; const char *p = eq;
int l;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM) if (r == -ENOMEM)
return log_oom(); return log_oom();
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to parse SetCredential= parameter: %s", eq); return log_error_errno(r, "Failed to parse %s= parameter: %s", field, eq);
if (r == 0 || !p) if (r == 0 || !p)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to SetCredential=."); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to %s=.", field);
l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
if (l < 0)
return log_error_errno(l, "Failed to unescape SetCredential= value: %s", p);
r = sd_bus_message_open_container(m, 'a', "(say)"); r = sd_bus_message_open_container(m, 'a', "(say)");
if (r < 0) if (r < 0)
@ -1090,7 +1085,25 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
if (r < 0) if (r < 0)
return bus_log_create_error(r); return bus_log_create_error(r);
r = sd_bus_message_append_array(m, 'y', unescaped, l); if (streq(field, "SetCredentialEncrypted")) {
_cleanup_free_ void *decoded = NULL;
size_t decoded_size;
r = unbase64mem(p, SIZE_MAX, &decoded, &decoded_size);
if (r < 0)
return log_error_errno(r, "Failed to base64 decode encrypted credential: %m");
r = sd_bus_message_append_array(m, 'y', decoded, decoded_size);
} else {
_cleanup_free_ char *unescaped = NULL;
int l;
l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
if (l < 0)
return log_error_errno(l, "Failed to unescape %s= value: %s", field, p);
r = sd_bus_message_append_array(m, 'y', unescaped, l);
}
if (r < 0) if (r < 0)
return bus_log_create_error(r); return bus_log_create_error(r);
@ -1114,12 +1127,12 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
return 1; return 1;
} }
if (streq(field, "LoadCredential")) { if (STR_IN_SET(field, "LoadCredential", "LoadCredentialEncrypted")) {
r = sd_bus_message_open_container(m, 'r', "sv"); r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0) if (r < 0)
return bus_log_create_error(r); return bus_log_create_error(r);
r = sd_bus_message_append_basic(m, 's', "LoadCredential"); r = sd_bus_message_append_basic(m, 's', field);
if (r < 0) if (r < 0)
return bus_log_create_error(r); return bus_log_create_error(r);
@ -1137,9 +1150,9 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
if (r == -ENOMEM) if (r == -ENOMEM)
return log_oom(); return log_oom();
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to parse LoadCredential= parameter: %s", eq); return log_error_errno(r, "Failed to parse %s= parameter: %s", field, eq);
if (r == 0 || !p) if (r == 0 || !p)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to LoadCredential=."); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to %s=.", field);
r = sd_bus_message_append(m, "a(ss)", 1, word, p); r = sd_bus_message_append(m, "a(ss)", 1, word, p);
} }

930
src/shared/creds-util.c Normal file
View File

@ -0,0 +1,930 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/file.h>
#if HAVE_OPENSSL
#include <openssl/err.h>
#endif
#include "sd-id128.h"
#include "blockdev-util.h"
#include "chattr-util.h"
#include "creds-util.h"
#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
#include "io-util.h"
#include "memory-util.h"
#include "mkdir.h"
#include "openssl-util.h"
#include "path-util.h"
#include "random-util.h"
#include "sparse-endian.h"
#include "stat-util.h"
#include "tpm2-util.h"
#include "virt.h"
bool credential_name_valid(const char *s) {
/* We want that credential names are both valid in filenames (since that's our primary way to pass
* them around) and as fdnames (which is how we might want to pass them around eventually) */
return filename_is_valid(s) && fdname_is_valid(s);
}
int get_credentials_dir(const char **ret) {
const char *e;
assert(ret);
e = secure_getenv("CREDENTIALS_DIRECTORY");
if (!e)
return -ENXIO;
if (!path_is_absolute(e) || !path_is_normalized(e))
return -EINVAL;
*ret = e;
return 0;
}
int read_credential(const char *name, void **ret, size_t *ret_size) {
_cleanup_free_ char *fn = NULL;
const char *d;
int r;
assert(ret);
if (!credential_name_valid(name))
return -EINVAL;
r = get_credentials_dir(&d);
if (r < 0)
return r;
fn = path_join(d, name);
if (!fn)
return -ENOMEM;
return read_full_file_full(
AT_FDCWD, fn,
UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_SECURE,
NULL,
(char**) ret, ret_size);
}
#if HAVE_OPENSSL
#define CREDENTIAL_HOST_SECRET_SIZE 4096
static const sd_id128_t credential_app_id =
SD_ID128_MAKE(d3,ac,ec,ba,0d,ad,4c,df,b8,c9,38,15,28,93,6c,58);
struct credential_host_secret_format {
/* The hashed machine ID of the machine this belongs to. Why? We want to ensure that each machine
* gets its own secret, even if people forget to flush out this secret file. Hence we bind it to the
* machine ID, for which there's hopefully a better chance it will be flushed out. We use a hashed
* machine ID instead of the literal one, because it's trivial to, and it might be a good idea not
* being able to directly associate a secret key file with a host. */
sd_id128_t machine_id;
/* The actual secret key */
uint8_t data[CREDENTIAL_HOST_SECRET_SIZE];
} _packed_;
static int make_credential_host_secret(
int dfd,
const sd_id128_t machine_id,
const char *fn,
void **ret_data,
size_t *ret_size) {
struct credential_host_secret_format buf;
_cleanup_free_ char *t = NULL;
_cleanup_close_ int fd = -1;
int r;
assert(dfd >= 0);
assert(fn);
fd = openat(dfd, ".", O_CLOEXEC|O_WRONLY|O_TMPFILE, 0400);
if (fd < 0) {
log_debug_errno(errno, "Failed to create temporary credential file with O_TMPFILE, proceeding without: %m");
if (asprintf(&t, "credential.secret.%016" PRIx64, random_u64()) < 0)
return -ENOMEM;
fd = openat(dfd, t, O_CLOEXEC|O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0400);
if (fd < 0)
return -errno;
}
r = chattr_secret(fd, 0);
if (r < 0)
log_debug_errno(r, "Failed to set file attributes for secrets file, ignoring: %m");
buf = (struct credential_host_secret_format) {
.machine_id = machine_id,
};
r = genuine_random_bytes(buf.data, sizeof(buf.data), RANDOM_BLOCK);
if (r < 0)
goto finish;
r = loop_write(fd, &buf, sizeof(buf), false);
if (r < 0)
goto finish;
if (fsync(fd) < 0) {
r = -errno;
goto finish;
}
if (t) {
r = rename_noreplace(dfd, t, dfd, fn);
if (r < 0)
goto finish;
t = mfree(t);
} else if (linkat(fd, "", dfd, fn, AT_EMPTY_PATH) < 0) {
r = -errno;
goto finish;
}
if (fsync(dfd) < 0) {
r = -errno;
goto finish;
}
if (ret_data) {
void *copy;
copy = memdup(buf.data, sizeof(buf.data));
if (!copy) {
r = -ENOMEM;
goto finish;
}
*ret_data = copy;
}
if (ret_size)
*ret_size = sizeof(buf.data);
r = 0;
finish:
if (t && unlinkat(dfd, t, 0) < 0)
log_debug_errno(errno, "Failed to remove temporary credential key: %m");
explicit_bzero_safe(&buf, sizeof(buf));
return r;
}
int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t *ret_size) {
_cleanup_free_ char *efn = NULL, *ep = NULL;
_cleanup_close_ int dfd = -1;
sd_id128_t machine_id;
const char *e, *fn, *p;
int r;
r = sd_id128_get_machine_app_specific(credential_app_id, &machine_id);
if (r < 0)
return r;
e = secure_getenv("SYSTEMD_CREDENTIAL_SECRET");
if (e) {
if (!path_is_normalized(e))
return -EINVAL;
if (!path_is_absolute(e))
return -EINVAL;
r = path_extract_directory(e, &ep);
if (r < 0)
return r;
r = path_extract_filename(e, &efn);
if (r < 0)
return r;
p = ep;
fn = efn;
} else {
p = "/var/lib/systemd";
fn = "credential.secret";
}
(void) mkdir_p(p, 0755);
dfd = open(p, O_CLOEXEC|O_DIRECTORY|O_RDONLY);
if (dfd < 0)
return -errno;
if (FLAGS_SET(flags, CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS)) {
r = fd_is_temporary_fs(dfd);
if (r < 0)
return r;
if (r > 0)
return -ENOMEDIUM;
}
for (unsigned attempt = 0;; attempt++) {
_cleanup_(erase_and_freep) struct credential_host_secret_format *f = NULL;
_cleanup_close_ int fd = -1;
size_t l = 0;
ssize_t n = 0;
struct stat st;
if (attempt >= 3) /* Somebody is playing games with us */
return -EIO;
fd = openat(dfd, fn, O_CLOEXEC|O_RDONLY|O_NOCTTY|O_NOFOLLOW);
if (fd < 0) {
if (errno != ENOENT || !FLAGS_SET(flags, CREDENTIAL_SECRET_GENERATE))
return -errno;
r = make_credential_host_secret(dfd, machine_id, fn, ret, ret_size);
if (r == -EEXIST) {
log_debug_errno(r, "Credential secret was created while we were creating it. Trying to read new secret.");
continue;
}
if (r < 0)
return r;
return 0;
}
if (fstat(fd, &st) < 0)
return -errno;
r = stat_verify_regular(&st);
if (r < 0)
return r;
if (st.st_nlink == 0) /* Deleted by now, try again */
continue;
if (st.st_nlink > 1)
return -EPERM; /* Our deletion check won't work if hardlinked somewhere else */
if ((st.st_mode & 07777) != 0400) /* Don't use file if not 0400 access mode */
return -EPERM;
if (st.st_size > 16*1024*1024)
return -E2BIG;
l = st.st_size;
if (l < offsetof(struct credential_host_secret_format, data) + 1)
return -EINVAL;
f = malloc(l+1);
if (!f)
return -ENOMEM;
n = read(fd, f, l+1);
if (n < 0)
return -errno;
if ((size_t) n != l) /* What? The size changed? */
return -EIO;
if (sd_id128_equal(machine_id, f->machine_id)) {
size_t sz;
if (FLAGS_SET(flags, CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED)) {
r = fd_is_encrypted(fd);
if (r < 0)
log_debug_errno(r, "Failed to determine if credential secret file '%s/%s' is encrypted.", p, fn);
else if (r == 0)
log_warning("Credential secret file '%s/%s' is not located on encrypted media, using anyway.", p, fn);
}
sz = l - offsetof(struct credential_host_secret_format, data);
assert(sz > 0);
if (ret) {
void *copy;
copy = memdup(f->data, sz);
if (!copy)
return -ENOMEM;
*ret = copy;
}
if (ret_size)
*ret_size = sz;
return 0;
}
/* Hmm, this secret is from somewhere else. Let's delete the file. Let's first acquire a lock
* to ensure we are the only ones accessing the file while we delete it. */
if (flock(fd, LOCK_EX) < 0)
return -errno;
/* Before we delete it check that the file is still linked into the file system */
if (fstat(fd, &st) < 0)
return -errno;
if (st.st_nlink == 0) /* Already deleted by now? */
continue;
if (st.st_nlink != 1) /* Safety check, someone is playing games with us */
return -EPERM;
if (unlinkat(dfd, fn, 0) < 0)
return -errno;
/* And now try again */
}
}
/* Construction is like this:
*
* A symmetric encryption key is derived from:
*
* 1. Either the "host" key (a key stored in /var/lib/credential.secret)
*
* 2. A key generated by letting the TPM2 calculate an HMAC hash of some nonce we pass to it, keyed
* by a key derived from its internal seed key.
*
* 3. The concatenation of the above.
*
* The above is hashed with SHA256 which is then used as encryption key for AES256-GCM. The encrypted
* credential is a short (unencrypted) header describing which of the three keys to use, the IV to use for
* AES256-GCM and some more meta information (sizes of certain objects) that is strictly speaking redundant,
* but kinda nice to have since we can have a more generic parser. If the TPM2 key is used this is followed
* by another (unencrypted) header, with information about the TPM2 policy used (specifically: the PCR mask
* to bind against, and a hash of the resulting policy the latter being redundant, but speeding up things a
* bit, since we can more quickly refuse PCR state), followed by a sealed/exported TPM2 HMAC key. This is
* then followed by the encrypted data, which begins with a metadata header (which contains validity
* timestamps as well as the credential name), followed by the actual credential payload. The file ends in
* the AES256-GCM tag. To make things simple, the AES256-GCM AAD covers the main and the TPM2 header in
* full. This means the whole file is either protected by AAD, or is ciphertext, or is the tag. No
* unprotected data is included.
*/
struct _packed_ encrypted_credential_header {
sd_id128_t id;
le32_t key_size;
le32_t block_size;
le32_t iv_size;
le32_t tag_size;
uint8_t iv[];
/* Followed by NUL bytes until next 8 byte boundary */
};
struct _packed_ tpm2_credential_header {
le64_t pcr_mask;
le32_t blob_size;
le32_t policy_hash_size;
uint8_t policy_hash_and_blob[];
/* Followed by NUL bytes until next 8 byte boundary */
};
struct _packed_ metadata_credential_header {
le64_t timestamp;
le64_t not_after;
le32_t name_size;
char name[];
/* Followed by NUL bytes until next 8 byte boundary */
};
/* Some generic limit for parts of the encrypted credential for which we don't know the right size ahead of
* time, but where we are really sure it won't be larger than this. Should be larger than any possible IV,
* padding, tag size and so on. This is purely used for early filtering out of invalid sizes. */
#define CREDENTIAL_FIELD_SIZE_MAX (16U*1024U)
static int sha256_hash_host_and_tpm2_key(
const void *host_key,
size_t host_key_size,
const void *tpm2_key,
size_t tpm2_key_size,
uint8_t ret[static SHA256_DIGEST_LENGTH]) {
SHA256_CTX sha256_context;
assert(host_key_size == 0 || host_key);
assert(tpm2_key_size == 0 || tpm2_key);
assert(ret);
/* Combines the host key and the TPM2 HMAC hash into a SHA256 hash value we'll use as symmetric encryption key. */
if (SHA256_Init(&sha256_context) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initial SHA256 context.");
if (host_key && SHA256_Update(&sha256_context, host_key, host_key_size) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to hash host key.");
if (tpm2_key && SHA256_Update(&sha256_context, tpm2_key, tpm2_key_size) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to hash TPM2 key.");
if (SHA256_Final(ret, &sha256_context) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize SHA256 hash.");
return 0;
}
int encrypt_credential_and_warn(
sd_id128_t with_key,
const char *name,
usec_t timestamp,
usec_t not_after,
const char *tpm2_device,
uint32_t tpm2_pcr_mask,
const void *input,
size_t input_size,
void **ret,
size_t *ret_size) {
_cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
_cleanup_(erase_and_freep) void *host_key = NULL, *tpm2_key = NULL;
size_t host_key_size = 0, tpm2_key_size = 0, tpm2_blob_size = 0, tpm2_policy_hash_size = 0, output_size, p, ml;
_cleanup_free_ void *tpm2_blob = NULL, *tpm2_policy_hash = NULL, *iv = NULL, *output = NULL;
_cleanup_free_ struct metadata_credential_header *m = NULL;
struct encrypted_credential_header *h;
int ksz, bsz, ivsz, tsz, added, r;
uint8_t md[SHA256_DIGEST_LENGTH];
const EVP_CIPHER *cc;
#if HAVE_TPM2
bool try_tpm2 = false;
#endif
sd_id128_t id;
assert(input || input_size == 0);
assert(ret);
assert(ret_size);
if (name && !credential_name_valid(name))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name: %s", name);
if (not_after != USEC_INFINITY && timestamp != USEC_INFINITY && not_after < timestamp)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid (" USEC_FMT " < " USEC_FMT ").", not_after, timestamp);
if (DEBUG_LOGGING) {
char buf[FORMAT_TIMESTAMP_MAX];
if (name)
log_debug("Including credential name '%s' in encrypted credential.", name);
if (timestamp != USEC_INFINITY)
log_debug("Including timestamp '%s' in encrypted credential.", format_timestamp(buf, sizeof(buf), timestamp));
if (not_after != USEC_INFINITY)
log_debug("Including not-after timestamp '%s' in encrypted credential.", format_timestamp(buf, sizeof(buf), not_after));
}
if (sd_id128_is_null(with_key) ||
sd_id128_in_set(with_key, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC)) {
r = get_credential_host_secret(
CREDENTIAL_SECRET_GENERATE|
CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED|
(sd_id128_is_null(with_key) ? CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS : 0),
&host_key,
&host_key_size);
if (r == -ENOMEDIUM && sd_id128_is_null(with_key))
log_debug_errno(r, "Credential host secret location on temporary file system, not using.");
else if (r < 0)
return log_error_errno(r, "Failed to determine local credential host secret: %m");
}
#if HAVE_TPM2
if (sd_id128_is_null(with_key)) {
/* If automatic mode is selected and we are running in a container, let's not try TPM2. OTOH
* if user picks TPM2 explicitly, let's always honour the request and try. */
r = detect_container();
if (r < 0)
log_debug_errno(r, "Failed to determine whether we are running in a container, ignoring: %m");
else if (r > 0)
log_debug("Running in container, not attempting to use TPM2.");
try_tpm2 = r <= 0;
}
if (try_tpm2 ||
sd_id128_in_set(with_key, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC)) {
r = tpm2_seal(tpm2_device,
tpm2_pcr_mask,
&tpm2_key,
&tpm2_key_size,
&tpm2_blob,
&tpm2_blob_size,
&tpm2_policy_hash,
&tpm2_policy_hash_size);
if (r < 0) {
if (!sd_id128_is_null(with_key))
return r;
log_debug_errno(r, "TPM2 sealing didn't work, not using: %m");
}
assert(tpm2_blob_size <= CREDENTIAL_FIELD_SIZE_MAX);
assert(tpm2_policy_hash_size <= CREDENTIAL_FIELD_SIZE_MAX);
}
#endif
if (sd_id128_is_null(with_key)) {
/* Let's settle the key type in auto mode now. */
if (host_key && tpm2_key)
id = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC;
else if (tpm2_key)
id = CRED_AES256_GCM_BY_TPM2_HMAC;
else if (host_key)
id = CRED_AES256_GCM_BY_HOST;
else
return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
"TPM2 not available and host key located on temporary file system, no encryption key available.");
} else
id = with_key;
/* Let's now take the host key and the TPM2 key and hash it together, to use as encryption key for the data */
r = sha256_hash_host_and_tpm2_key(host_key, host_key_size, tpm2_key, tpm2_key_size, md);
if (r < 0)
return r;
assert_se(cc = EVP_aes_256_gcm());
ksz = EVP_CIPHER_key_length(cc);
assert(ksz == sizeof(md));
bsz = EVP_CIPHER_block_size(cc);
assert(bsz > 0);
assert((size_t) bsz <= CREDENTIAL_FIELD_SIZE_MAX);
ivsz = EVP_CIPHER_iv_length(cc);
if (ivsz > 0) {
assert((size_t) ivsz <= CREDENTIAL_FIELD_SIZE_MAX);
iv = malloc(ivsz);
if (!iv)
return log_oom();
r = genuine_random_bytes(iv, ivsz, RANDOM_BLOCK);
if (r < 0)
return log_error_errno(r, "Failed to acquired randomized IV: %m");
}
tsz = 16; /* FIXME: On OpenSSL 3 there is EVP_CIPHER_CTX_get_tag_length(), until then let's hardcode this */
context = EVP_CIPHER_CTX_new();
if (!context)
return log_error_errno(SYNTHETIC_ERRNO(ENOMEM), "Failed to allocate encryption object: %s",
ERR_error_string(ERR_get_error(), NULL));
if (EVP_EncryptInit_ex(context, cc, NULL, md, iv) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context: %s",
ERR_error_string(ERR_get_error(), NULL));
/* Just an upper estimate */
output_size =
ALIGN8(offsetof(struct encrypted_credential_header, iv) + ivsz) +
ALIGN8(tpm2_key ? offsetof(struct tpm2_credential_header, policy_hash_and_blob) + tpm2_blob_size + tpm2_policy_hash_size : 0) +
ALIGN8(offsetof(struct metadata_credential_header, name) + strlen_ptr(name)) +
input_size + 2U * (size_t) bsz +
tsz;
output = malloc0(output_size);
if (!output)
return log_oom();
h = (struct encrypted_credential_header*) output;
h->id = id;
h->block_size = htole32(bsz);
h->key_size = htole32(ksz);
h->tag_size = htole32(tsz);
h->iv_size = htole32(ivsz);
memcpy(h->iv, iv, ivsz);
p = ALIGN8(offsetof(struct encrypted_credential_header, iv) + ivsz);
if (tpm2_key) {
struct tpm2_credential_header *t;
t = (struct tpm2_credential_header*) ((uint8_t*) output + p);
t->pcr_mask = htole64(tpm2_pcr_mask);
t->blob_size = htole32(tpm2_blob_size);
t->policy_hash_size = htole32(tpm2_policy_hash_size);
memcpy(t->policy_hash_and_blob, tpm2_blob, tpm2_blob_size);
memcpy(t->policy_hash_and_blob + tpm2_blob_size, tpm2_policy_hash, tpm2_policy_hash_size);
p += ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) + tpm2_blob_size + tpm2_policy_hash_size);
}
/* Pass the encrypted + TPM2 header as AAD */
if (EVP_EncryptUpdate(context, NULL, &added, output, p) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to write AAD data: %s",
ERR_error_string(ERR_get_error(), NULL));
/* Now construct the metadata header */
ml = strlen_ptr(name);
m = malloc0(ALIGN8(offsetof(struct metadata_credential_header, name) + ml));
if (!m)
return log_oom();
m->timestamp = htole64(timestamp);
m->not_after = htole64(not_after);
m->name_size = htole32(ml);
memcpy_safe(m->name, name, ml);
/* And encrypt the metadata header */
if (EVP_EncryptUpdate(context, (uint8_t*) output + p, &added, (const unsigned char*) m, ALIGN8(offsetof(struct metadata_credential_header, name) + ml)) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt metadata header: %s",
ERR_error_string(ERR_get_error(), NULL));
assert(added >= 0);
assert((size_t) added <= output_size - p);
p += added;
/* Then encrypt the plaintext */
if (EVP_EncryptUpdate(context, (uint8_t*) output + p, &added, input, input_size) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt data: %s",
ERR_error_string(ERR_get_error(), NULL));
assert(added >= 0);
assert((size_t) added <= output_size - p);
p += added;
/* Finalize */
if (EVP_EncryptFinal_ex(context, (uint8_t*) output + p, &added) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize data encryption: %s",
ERR_error_string(ERR_get_error(), NULL));
assert(added >= 0);
assert((size_t) added <= output_size - p);
p += added;
assert(p <= output_size - tsz);
/* Append tag */
if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_GET_TAG, tsz, (uint8_t*) output + p) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get tag: %s",
ERR_error_string(ERR_get_error(), NULL));
p += tsz;
assert(p <= output_size);
if (DEBUG_LOGGING) {
size_t base64_size;
base64_size = DIV_ROUND_UP(p * 4, 3); /* Include base64 size increase in debug output */
log_debug("Input of %zu bytes grew to output of %zu bytes (+%2zu%%).", input_size, base64_size, base64_size * 100 / input_size - 100);
}
*ret = TAKE_PTR(output);
*ret_size = p;
return 0;
}
int decrypt_credential_and_warn(
const char *validate_name,
usec_t validate_timestamp,
const char *tpm2_device,
const void *input,
size_t input_size,
void **ret,
size_t *ret_size) {
_cleanup_(erase_and_freep) void *host_key = NULL, *tpm2_key = NULL, *plaintext = NULL;
_cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
size_t host_key_size = 0, tpm2_key_size = 0, plaintext_size, p, hs;
struct encrypted_credential_header *h;
struct metadata_credential_header *m;
uint8_t md[SHA256_DIGEST_LENGTH];
bool with_tpm2, with_host_key;
const EVP_CIPHER *cc;
int r, added;
assert(input || input_size == 0);
assert(ret);
assert(ret_size);
h = (struct encrypted_credential_header*) input;
/* The ID must fit in, for the current and all future formats */
if (input_size < sizeof(h->id))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
with_host_key = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC);
with_tpm2 = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC);
if (!with_host_key && !with_tpm2)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unknown encryption format, or corrupted data: %m");
/* Now we know the minimum header size */
if (input_size < offsetof(struct encrypted_credential_header, iv))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
/* Verify some basic header values */
if (le32toh(h->key_size) != sizeof(md))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected key size in header.");
if (le32toh(h->block_size) <= 0 || le32toh(h->block_size) > CREDENTIAL_FIELD_SIZE_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected block size in header.");
if (le32toh(h->iv_size) > CREDENTIAL_FIELD_SIZE_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "IV size too large.");
if (le32toh(h->tag_size) != 16) /* FIXME: On OpenSSL 3, let's verify via EVP_CIPHER_CTX_get_tag_length() */
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected tag size in header.");
/* Ensure we have space for the full header now (we don't know the size of the name hence this is a
* lower limit only) */
if (input_size <
ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size)) +
ALIGN8((with_tpm2 ? offsetof(struct tpm2_credential_header, policy_hash_and_blob) : 0)) +
ALIGN8(offsetof(struct metadata_credential_header, name)) +
le32toh(h->tag_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
p = ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size));
if (with_tpm2) {
#if HAVE_TPM2
struct tpm2_credential_header* t = (struct tpm2_credential_header*) ((uint8_t*) input + p);
if (le64toh(t->pcr_mask) >= (UINT64_C(1) << TPM2_PCRS_MAX))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "TPM2 PCR mask out of range.");
if (le32toh(t->blob_size) > CREDENTIAL_FIELD_SIZE_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected TPM2 blob size.");
if (le32toh(t->policy_hash_size) > CREDENTIAL_FIELD_SIZE_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected TPM2 policy hash size.");
/* Ensure we have space for the full TPM2 header now (still don't know the name, and its size
* though, hence still just a lower limit test only) */
if (input_size <
ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size)) +
ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) + le32toh(t->blob_size) + le32toh(t->policy_hash_size)) +
ALIGN8(offsetof(struct metadata_credential_header, name)) +
le32toh(h->tag_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
r = tpm2_unseal(tpm2_device,
le64toh(t->pcr_mask),
t->policy_hash_and_blob,
le32toh(t->blob_size),
t->policy_hash_and_blob + le32toh(t->blob_size),
le32toh(t->policy_hash_size),
&tpm2_key,
&tpm2_key_size);
if (r < 0)
return r;
p += ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) +
le32toh(t->blob_size) +
le32toh(t->policy_hash_size));
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Credential requires TPM2 support, but TPM2 support not available.");
#endif
}
if (with_host_key) {
r = get_credential_host_secret(
0,
&host_key,
&host_key_size);
if (r < 0)
return log_error_errno(r, "Failed to determine local credential key: %m");
}
sha256_hash_host_and_tpm2_key(host_key, host_key_size, tpm2_key, tpm2_key_size, md);
assert_se(cc = EVP_aes_256_gcm());
/* Make sure cipher expectations match the header */
if (EVP_CIPHER_key_length(cc) != (int) le32toh(h->key_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected key size in header.");
if (EVP_CIPHER_block_size(cc) != (int) le32toh(h->block_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected block size in header.");
context = EVP_CIPHER_CTX_new();
if (!context)
return log_error_errno(SYNTHETIC_ERRNO(ENOMEM), "Failed to allocate decryption object: %s",
ERR_error_string(ERR_get_error(), NULL));
if (EVP_DecryptInit_ex(context, cc, NULL, NULL, NULL) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context: %s",
ERR_error_string(ERR_get_error(), NULL));
if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, le32toh(h->iv_size), NULL) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set IV size on decryption context: %s",
ERR_error_string(ERR_get_error(), NULL));
if (EVP_DecryptInit_ex(context, NULL, NULL, md, h->iv) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set IV and key: %s",
ERR_error_string(ERR_get_error(), NULL));
if (EVP_DecryptUpdate(context, NULL, &added, input, p) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to write AAD data: %s",
ERR_error_string(ERR_get_error(), NULL));
plaintext = malloc(input_size - p - le32toh(h->tag_size));
if (!plaintext)
return -ENOMEM;
if (EVP_DecryptUpdate(
context,
plaintext,
&added,
(uint8_t*) input + p,
input_size - p - le32toh(h->tag_size)) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt data: %s",
ERR_error_string(ERR_get_error(), NULL));
assert(added >= 0);
assert((size_t) added <= input_size - p - le32toh(h->tag_size));
plaintext_size = added;
if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_TAG, le32toh(h->tag_size), (uint8_t*) input + input_size - le32toh(h->tag_size)) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set tag: %s",
ERR_error_string(ERR_get_error(), NULL));
if (EVP_DecryptFinal_ex(context, (uint8_t*) plaintext + plaintext_size, &added) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Decryption failed (incorrect key?): %s",
ERR_error_string(ERR_get_error(), NULL));
plaintext_size += added;
if (plaintext_size < ALIGN8(offsetof(struct metadata_credential_header, name)))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Metadata header incomplete.");
m = plaintext;
if (le64toh(m->timestamp) != USEC_INFINITY &&
le64toh(m->not_after) != USEC_INFINITY &&
le64toh(m->timestamp) >= le64toh(m->not_after))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Timestamps of credential are not in order, refusing.");
if (le32toh(m->name_size) > CREDENTIAL_NAME_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded credential name too long, refusing.");
hs = ALIGN8(offsetof(struct metadata_credential_header, name) + le32toh(m->name_size));
if (plaintext_size < hs)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Metadata header incomplete.");
if (le32toh(m->name_size) > 0) {
_cleanup_free_ char *embedded_name = NULL;
if (memchr(m->name, 0, le32toh(m->name_size)))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded credential name contains NUL byte, refusing.");
embedded_name = memdup_suffix0(m->name, le32toh(m->name_size));
if (!embedded_name)
return log_oom();
if (!credential_name_valid(embedded_name))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded credential name is not valid, refusing.");
if (validate_name && !streq(embedded_name, validate_name)) {
r = getenv_bool_secure("SYSTEMD_CREDENTIAL_VALIDATE_NAME");
if (r < 0 && r != -ENXIO)
log_debug_errno(r, "Failed to parse $SYSTEMD_CREDENTIAL_VALIDATE_NAME: %m");
if (r != 0)
return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), "Embedded credential name '%s' does not match filename '%s', refusing.", embedded_name, validate_name);
log_debug("Embedded credential name '%s' does not match expected name '%s', but configured to use credential anyway.", embedded_name, validate_name);
}
}
if (validate_timestamp != USEC_INFINITY) {
if (le64toh(m->timestamp) != USEC_INFINITY && le64toh(m->timestamp) > validate_timestamp)
log_debug("Credential timestamp is from the future, assuming clock skew.");
if (le64toh(m->not_after) != USEC_INFINITY && le64toh(m->not_after) < validate_timestamp) {
r = getenv_bool_secure("SYSTEMD_CREDENTIAL_VALIDATE_NOT_AFTER");
if (r < 0 && r != -ENXIO)
log_debug_errno(r, "Failed to parse $SYSTEMD_CREDENTIAL_VALIDATE_NOT_AFTER: %m");
if (r != 0)
return log_error_errno(SYNTHETIC_ERRNO(ESTALE), "Credential's time passed, refusing to use.");
log_debug("Credential not-after timestamp has passed, but configured to use credential anyway.");
}
}
if (ret) {
char *without_metadata;
without_metadata = memdup((uint8_t*) plaintext + hs, plaintext_size - hs);
if (!without_metadata)
return log_oom();
*ret = without_metadata;
}
if (ret_size)
*ret_size = plaintext_size - hs;
return 0;
}
#else
int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t *ret_size) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Support for encrypted credentials not available.");
}
int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_pcr_mask, const void *input, size_t input_size, void **ret, size_t *ret_size) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Support for encrypted credentials not available.");
}
int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const void *input, size_t input_size, void **ret, size_t *ret_size) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Support for encrypted credentials not available.");
}
#endif

47
src/shared/creds-util.h Normal file
View File

@ -0,0 +1,47 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <inttypes.h>
#include <stdbool.h>
#include <sys/types.h>
#include "sd-id128.h"
#include "fd-util.h"
#include "time-util.h"
#define CREDENTIAL_NAME_MAX FDNAME_MAX
/* Put a size limit on the individual credential */
#define CREDENTIAL_SIZE_MAX (1024U*1024U)
/* Refuse to store more than 1M per service, after all this is unswappable memory. Note that for now we put
* this to the same limit as the per-credential limit, i.e. if the user has n > 1 credentials instead of 1 it
* won't get them more space. */
#define CREDENTIALS_TOTAL_SIZE_MAX CREDENTIAL_SIZE_MAX
/* Put a size limit on encrypted credentials (which is the same as the unencrypted size plus a spacious 128K of extra
* space for headers, IVs, exported TPM2 key material and so on. */
#define CREDENTIAL_ENCRYPTED_SIZE_MAX (CREDENTIAL_SIZE_MAX + 128U*1024U)
bool credential_name_valid(const char *s);
int get_credentials_dir(const char **ret);
int read_credential(const char *name, void **ret, size_t *ret_size);
typedef enum CredentialSecretFlags {
CREDENTIAL_SECRET_GENERATE = 1 << 0,
CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED = 1 << 1,
CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS = 1 << 2,
} CredentialSecretFlags;
int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t *ret_size);
/* The three modes we support: keyed only by on-disk key, only by TPM2 HMAC key, and by the combination of both */
#define CRED_AES256_GCM_BY_HOST SD_ID128_MAKE(5a,1c,6a,86,df,9d,40,96,b1,d5,a6,5e,08,62,f1,9a)
#define CRED_AES256_GCM_BY_TPM2_HMAC SD_ID128_MAKE(0c,7c,c0,7b,11,76,45,91,9c,4b,0b,ea,08,bc,20,fe)
#define CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC SD_ID128_MAKE(93,a8,94,09,48,74,44,90,90,ca,f2,fc,93,ca,b5,53)
int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_pcr_mask, const void *input, size_t input_size, void **ret, size_t *ret_size);
int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const void *input, size_t input_size, void **ret, size_t *ret_size);

View File

@ -81,6 +81,8 @@ shared_sources = files('''
coredump-util.h coredump-util.h
cpu-set-util.c cpu-set-util.c
cpu-set-util.h cpu-set-util.h
creds-util.c
creds-util.h
cryptsetup-util.c cryptsetup-util.c
cryptsetup-util.h cryptsetup-util.h
daemon-util.h daemon-util.h

View File

@ -999,6 +999,25 @@ static void test_read_full_file_offset_size(void) {
assert_se(memcmp(buf, rbuf, rbuf_size) == 0); assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
rbuf = mfree(rbuf); rbuf = mfree(rbuf);
assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, 128, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, sizeof(buf)-1, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, sizeof(buf), READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) >= 0);
assert_se(rbuf_size == sizeof(buf));
assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
rbuf = mfree(rbuf);
assert_se(read_full_file_full(AT_FDCWD, fn, 47, 128, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
assert_se(read_full_file_full(AT_FDCWD, fn, 47, sizeof(buf)-47-1, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
assert_se(read_full_file_full(AT_FDCWD, fn, 47, sizeof(buf)-47, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) >= 0);
assert_se(rbuf_size == sizeof(buf)-47);
assert_se(memcmp(buf+47, rbuf, rbuf_size) == 0);
rbuf = mfree(rbuf);
assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, sizeof(buf)+1, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) >= 0);
assert_se(rbuf_size == sizeof(buf));
assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
rbuf = mfree(rbuf);
assert_se(read_full_file_full(AT_FDCWD, fn, 1234, SIZE_MAX, 0, NULL, &rbuf, &rbuf_size) >= 0); assert_se(read_full_file_full(AT_FDCWD, fn, 1234, SIZE_MAX, 0, NULL, &rbuf, &rbuf_size) >= 0);
assert_se(rbuf_size == sizeof(buf) - 1234); assert_se(rbuf_size == sizeof(buf) - 1234);
assert_se(memcmp(buf + 1234, rbuf, rbuf_size) == 0); assert_se(memcmp(buf + 1234, rbuf, rbuf_size) == 0);

View File

@ -5,6 +5,7 @@
#include "alloc-util.h" #include "alloc-util.h"
#include "hexdecoct.h" #include "hexdecoct.h"
#include "macro.h" #include "macro.h"
#include "random-util.h"
#include "string-util.h" #include "string-util.h"
static void test_hexchar(void) { static void test_hexchar(void) {
@ -275,6 +276,37 @@ static void test_base64mem(void) {
free(b64); free(b64);
} }
static void test_base64mem_linebreak(void) {
uint8_t data[4096];
for (size_t i = 0; i < 20; i++) {
_cleanup_free_ char *encoded = NULL;
_cleanup_free_ void *decoded = NULL;
size_t decoded_size;
uint64_t n, m;
ssize_t l;
/* Try a bunch of differently sized blobs */
n = random_u64_range(sizeof(data));
random_bytes(data, n);
/* Break at various different columns */
m = 1 + random_u64_range(n + 5);
l = base64mem_full(data, n, m, &encoded);
assert_se(l >= 0);
assert_se(encoded);
assert_se((size_t) l == strlen(encoded));
assert_se(unbase64mem(encoded, SIZE_MAX, &decoded, &decoded_size) >= 0);
assert_se(decoded_size == n);
assert_se(memcmp(data, decoded, n) == 0);
for (size_t j = 0; j < (size_t) l; j++)
assert_se((encoded[j] == '\n') == (j % (m + 1) == m));
}
}
static void test_unbase64mem_one(const char *input, const char *output, int ret) { static void test_unbase64mem_one(const char *input, const char *output, int ret) {
_cleanup_free_ void *buffer = NULL; _cleanup_free_ void *buffer = NULL;
size_t size = 0; size_t size = 0;
@ -348,6 +380,7 @@ int main(int argc, char *argv[]) {
test_base32hexmem(); test_base32hexmem();
test_unbase32hexmem(); test_unbase32hexmem();
test_base64mem(); test_base64mem();
test_base64mem_linebreak();
test_unbase64mem(); test_unbase64mem();
test_hexdump(); test_hexdump();

View File

@ -1355,7 +1355,7 @@ static int fd_set_attribute(Item *item, int fd, const char *path, const struct s
return log_error_errno(procfs_fd, "Failed to re-open '%s': %m", path); return log_error_errno(procfs_fd, "Failed to re-open '%s': %m", path);
unsigned previous, current; unsigned previous, current;
r = chattr_full(NULL, procfs_fd, f, item->attribute_mask, &previous, &current, true); r = chattr_full(NULL, procfs_fd, f, item->attribute_mask, &previous, &current, CHATTR_FALLBACK_BITWISE);
if (r == -ENOANO) if (r == -ENOANO)
log_warning("Cannot set file attributes for '%s', maybe due to incompatibility in specified attributes, " log_warning("Cannot set file attributes for '%s', maybe due to incompatibility in specified attributes, "
"previous=0x%08x, current=0x%08x, expected=0x%08x, ignoring.", "previous=0x%08x, current=0x%08x, expected=0x%08x, ignoring.",

View File

@ -83,6 +83,7 @@ LimitRTTIME=
LimitSIGPENDING= LimitSIGPENDING=
LimitSTACK= LimitSTACK=
LoadCredential= LoadCredential=
LoadCredentialEncrypted=
LockPersonality= LockPersonality=
LogExtraFields= LogExtraFields=
LogLevelMax= LogLevelMax=
@ -159,6 +160,7 @@ SecureBits=
SendSIGHUP= SendSIGHUP=
SendSIGKILL= SendSIGKILL=
SetCredential= SetCredential=
SetCredentialEncrypted=
Slice= Slice=
SloppyOptions= SloppyOptions=
SmackProcessLabel= SmackProcessLabel=

View File

@ -205,6 +205,7 @@ LimitRTTIME=
LimitSIGPENDING= LimitSIGPENDING=
LimitSTACK= LimitSTACK=
LoadCredential= LoadCredential=
LoadCredentialEncrypted=
LockPersonality= LockPersonality=
LogExtraFields= LogExtraFields=
LogLevelMax= LogLevelMax=
@ -292,6 +293,7 @@ SecureBits=
SendSIGHUP= SendSIGHUP=
SendSIGKILL= SendSIGKILL=
SetCredential= SetCredential=
SetCredentialEncrypted=
Slice= Slice=
SmackProcessLabel= SmackProcessLabel=
Sockets= Sockets=

View File

@ -108,6 +108,7 @@ ListenSpecial=
ListenStream= ListenStream=
ListenUSBFunction= ListenUSBFunction=
LoadCredential= LoadCredential=
LoadCredentialEncrypted=
LockPersonality= LockPersonality=
LogExtraFields= LogExtraFields=
LogLevelMax= LogLevelMax=
@ -199,6 +200,7 @@ SendSIGHUP=
SendSIGKILL= SendSIGKILL=
Service= Service=
SetCredential= SetCredential=
SetCredentialEncrypted=
Slice= Slice=
SmackLabel= SmackLabel=
SmackLabelIPIn= SmackLabelIPIn=

View File

@ -80,6 +80,7 @@ LimitRTTIME=
LimitSIGPENDING= LimitSIGPENDING=
LimitSTACK= LimitSTACK=
LoadCredential= LoadCredential=
LoadCredentialEncrypted=
LockPersonality= LockPersonality=
LogExtraFields= LogExtraFields=
LogLevelMax= LogLevelMax=
@ -156,6 +157,7 @@ SecureBits=
SendSIGHUP= SendSIGHUP=
SendSIGKILL= SendSIGKILL=
SetCredential= SetCredential=
SetCredentialEncrypted=
Slice= Slice=
SmackProcessLabel= SmackProcessLabel=
SocketBindAllow= SocketBindAllow=

View File

@ -27,6 +27,30 @@ systemd-run -p LoadCredential=passwd:/etc/passwd \
rm '${CREDENTIALS_DIRECTORY}/passwd' \ rm '${CREDENTIALS_DIRECTORY}/passwd' \
&& { echo 'unexpected success'; exit 1; } && { echo 'unexpected success'; exit 1; }
# Now test encrypted credentials (only supported when built with OpenSSL though)
if systemctl --version | grep -q -- +OPENSSL ; then
echo -n $RANDOM >/tmp/test-54-plaintext
systemd-creds encrypt --name=test-54 /tmp/test-54-plaintext /tmp/test-54-ciphertext
systemd-creds decrypt --name=test-54 /tmp/test-54-ciphertext | cmp /tmp/test-54-plaintext
systemd-run -p LoadCredentialEncrypted=test-54:/tmp/test-54-ciphertext \
--wait \
--pipe \
cat '${CREDENTIALS_DIRECTORY}/test-54' | cmp /tmp/test-54-plaintext
echo -n $RANDOM >/tmp/test-54-plaintext
systemd-creds encrypt --name=test-54 /tmp/test-54-plaintext /tmp/test-54-ciphertext
systemd-creds decrypt --name=test-54 /tmp/test-54-ciphertext | cmp /tmp/test-54-plaintext
systemd-run -p SetCredentialEncrypted=test-54:"`cat /tmp/test-54-ciphertext`" \
--wait \
--pipe \
cat '${CREDENTIALS_DIRECTORY}/test-54' | cmp /tmp/test-54-plaintext
rm /tmp/test-54-plaintext /tmp/test-54-ciphertext
fi
systemd-analyze log-level info systemd-analyze log-level info
echo OK >/testok echo OK >/testok