diff --git a/TODO b/TODO
index 75f885c94b..7c5002e6b0 100644
--- a/TODO
+++ b/TODO
@@ -142,16 +142,37 @@ Features:
* expose MS_NOSYMFOLLOW in various places
-* ability to insert trusted configuration and secrets into the boot parameters
- of a kernel booting in a VM or on baremetal some way, via TPM
- protection. idea:
- 1. pass via /proc/bootconfig
- 2. for secrets: put secrets in node of /proc/bootconfig, decrypt them via
- 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
- early on in PID 1, put data into /run/bootconfig/ as individual files
- 4. boot loader/stub should pick these up automatically from the boot loader
- file systems
+* allow passing creds into kernel when booting: in EFI stub, collect creds
+ files from ESP directory, generate CPIO archive on the fly from them, so that
+ they are dropped into /run/initramfs/creds/ and pass to kernel as additional
+ initrd. Then, use LoadCredentialEncrypted=foo:/run/initramfs/creds/foo to
+ load them.
+
+* make LoadCredential= automatically find credentials in /etc/creds,
+ /run/creds, … and so on, if path component is unqualified
+
+* 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
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
- 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
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
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)
* Add service setting to run a service within the specified VRF. i.e. do the
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index 74a920273a..3a54375400 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -2711,8 +2711,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(say) SetCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(ss) LoadCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...';
@@ -3224,8 +3228,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
+
+
@@ -3812,8 +3820,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
+
+
@@ -4511,8 +4523,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(say) SetCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(ss) LoadCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...';
@@ -5052,8 +5068,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
+
+
@@ -5638,8 +5658,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
+
+
@@ -6234,8 +6258,12 @@ node /org/freedesktop/systemd1/unit/home_2emount {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(say) SetCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(ss) LoadCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...';
@@ -6703,8 +6731,12 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
+
+
@@ -7207,8 +7239,12 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
+
+
@@ -7924,8 +7960,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(say) SetCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(say) SetCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ss) LoadCredential = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(ss) LoadCredentialEncrypted = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as SupplementaryGroups = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s PAMName = '...';
@@ -8379,8 +8419,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
+
+
@@ -8869,8 +8913,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
+
+
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 0f6519106a..6dea1b76fb 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -833,6 +833,7 @@ manpages = [
'8',
['systemd-coredump.socket', 'systemd-coredump@.service'],
'ENABLE_COREDUMP'],
+ ['systemd-creds', '1', [], ''],
['systemd-cryptenroll', '1', [], 'HAVE_LIBCRYPTSETUP'],
['systemd-cryptsetup-generator', '8', [], 'HAVE_LIBCRYPTSETUP'],
['systemd-cryptsetup@.service',
diff --git a/man/systemd-creds.xml b/man/systemd-creds.xml
new file mode 100644
index 0000000000..8ed96ca140
--- /dev/null
+++ b/man/systemd-creds.xml
@@ -0,0 +1,378 @@
+
+
+
+
+
+
+
+ systemd-creds
+ systemd
+
+
+
+ systemd-creds
+ 1
+
+
+
+ systemd-creds
+ Lists, shows, encrypts and decrypts service credentials
+
+
+
+
+ systemd-creds
+ OPTIONS
+
+
+
+
+ Description
+
+ systemd-creds 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.
+
+ Credentials are configured in unit files via the LoadCredential=,
+ SetCredential=, LoadCredentialEncrypted= and
+ SetCredentialEncrypted= settings, see
+ systemd.exec5 for
+ details.
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+ list
+
+ Show a list of credentials passed into the current execution context. This command
+ shows the files in the directory referenced by the $CREDENTIALS_DIRECTORY
+ environment variable, and is intended to be executed from within service context.
+
+ Along with each credential name, the size and security state is shown. The latter is one of
+ secure (in case the credential is backed by unswappable memory,
+ i.e. ramfs), weak (in case it is backed by any other type of
+ memory), or insecure (if having any access mode that is not 0400, i.e. if readable
+ by anyone but the owner).
+
+
+
+ cat credential...
+
+ 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.
+
+ When combined with or the output is
+ transcoded in simple ways before outputting.
+
+
+
+ setup
+
+ Generates a host encryption key for credentials, if none has been generated
+ before. This ensures the /var/lib/systemd/credential.secret file is initialized
+ with a random secret key if it doesn't exist yet. This secret key is used when encrypting/decrypting
+ credentials with encrypt or decrypt, 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 encrypt is invoked, and credential host key encryption
+ selected.
+
+
+
+ encrypt input output
+
+ 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 LoadCredentialEncrypted= setting in unit files, or its
+ contents used literally in SetCredentialEncrypted= settings.
+
+ 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
+ setting. The input or output paths may be specified as -,
+ in which case the credential data is read from/written to standard input and standard output. If the
+ output path is specified as - the credential name cannot be derived from the file
+ system path, and thus should be specified explicitly via the switch.
+
+ The credential data is encrypted symmetrically with one of the following encryption
+ keys:
+
+
+ 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.
+
+ A secret key stored in the /var/lib/systemd/credential.secret
+ 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 setup command, see above.
+
+ 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
+ /var/lib/systemd/ resides on persistent media.
+
+
+ Which of the three keys shall be used for encryption may be configured with the
+ 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.
+
+ Encrypted credentials are always encoded in Base64.
+
+ Use decrypt (see below) to undo the encryption operation, and acquire the
+ decrypted plaintext credential from the encrypted ciphertext credential.
+
+ 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.
+
+
+
+
+ decrypt input
+ output
+
+ Undoes the effect of the encrypt operation: loads the specified
+ (encrypted ciphertext) input credential file, decrypts it and writes the (decrypted plaintext)
+ version to the specified output credential file.
+
+ 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
+ switch. If only one path is specified (or the output path specified as
+ -) it is taken as input path and the decrypted credential is written to standard
+ output. If the input path is specified as - 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 .
+
+ 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.
+
+
+
+
+
+
+
+
+ Options
+
+
+
+
+
+
+ When specified with the list and cat 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.
+
+
+
+
+
+ When specified with the cat or decrypt
+ commands, transcodes the output before showing it. Takes one of base64,
+ unbase64, hex or unhex as argument, in order
+ to encode/decode the credential data with Base64 or as series of hexadecimal values.
+
+ Note that this has no effect on the encrypt command, as encrypted
+ credentials are unconditionally encoded in Base64.
+
+
+
+
+
+ When specified with cat or decrypt 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 auto, yes or no. The
+ default mode of auto will suffix the output with a single newline character only
+ when writing credential data to a TTY.
+
+
+
+
+
+
+ When specified with encrypt controls whether to show the encrypted
+ credential as SetCredentialEncrypted= setting that may be pasted directly into a
+ unit file.
+
+
+
+ name
+
+ When specified with the encrypt 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.
+
+ When specified with the decrypt 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 with an empty string
+ was used when encrypted) the specified name has no effect as no credential name validation is
+ done.
+
+ 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.
+
+
+
+ timestamp
+
+ When specified with the encrypt command controls the timestamp to
+ embed into the encrypted credential. Defaults to the current time. Takes a timestamp specification in
+ the format described in
+ systemd.time5.
+
+ When specified with the decrypt command controls the timestamp to use to
+ validate the "not-after" timestamp that was configured with during
+ encryption. If not specified defaults to the current system time.
+
+
+
+ timestamp
+
+ When specified with the encrypt 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
+ systemd.time5.
+
+
+
+
+
+
+
+ When specified with the encrypt command controls the encryption
+ key to use. Takes one of host, tpm2,
+ host+tpm2 or auto. See above for details on the three key
+ types. If set to auto (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
+ /var/lib/systemd/ 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 auto is selected but neither TPM2 is
+ available (or running in container) nor /var/lib/systemd/ is on persistent
+ media, encryption will fail.
+
+ The switch is a shortcut for . Similar,
+ is a shortcut for .
+
+ When encrypting credentials that shall be used in the initial RAM disk (initrd) where
+ /var/lib/systemd/ is typically not available make sure to use
+ mode, to disable binding against the host secret.
+
+ This switch has no effect on the decrypt command, as information on which
+ key to use for decryption is included in the encrypted credential already.
+
+
+
+ PATH
+
+ Controls the TPM2 device to use. Expects a device node path referring to the TPM2
+ chip (e.g. /dev/tpmrm0). Alternatively the special value auto
+ 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 list may be used to enumerate
+ all suitable TPM2 devices currently discovered.
+
+
+
+ PCR
+
+ Configures the TPM2 PCRs (Platform Configuration Registers) to bind the encryption
+ key to. Takes a + 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
+ systemd-cryptenroll1.
+
+
+
+
+
+
+
+
+
+ Exit status
+
+ On success, 0 is returned.
+
+
+
+ Examples
+
+
+ Encrypt a password for use as credential
+
+ The following command line encrypts the specified password hunter2, writing the result
+ to a file password.cred.
+
+ # echo -n hunter2 | systemd-creds encrypt - password.cred
+
+ This decrypts the file password.cred again, revealing the literal password:
+
+ # systemd-creds decrypt password.cred
+hunter2
+
+
+
+ Encrypt a password and include it in a unit file
+
+ The following command line prompts the user for a password and generates a
+ SetCredentialEncrypted= line from it for a credential named
+ mysql-password, suitable for inclusion in a unit file.
+
+ # 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==
+
+ The generated line can be pasted 1:1 into a unit file, and will ensure the acquired password will
+ be made available in the $CREDENTIALS_DIRECTORY/mysql-password
+ credential file for the started service.
+
+ 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
+ xyz.service:
+
+ # 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
+
+
+
+
+ See Also
+
+ systemd1,
+ systemd.exec5
+
+
+
+
diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml
index 64dcbbedea..ccb1567900 100644
--- a/man/systemd.exec.xml
+++ b/man/systemd.exec.xml
@@ -2338,7 +2338,8 @@ SystemCallErrorNumber=EPERM
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
(such as setuid/setgid executables), and hence might leak to processes that should not have access to
- the secret data. Use LoadCredential= (see below) to pass data to unit processes
+ the secret data. Use LoadCredential=, LoadCredentialEncrypted=
+ or SetCredentialEncrypted= (see below) to pass data to unit processes
securely.
@@ -2832,6 +2833,7 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
LoadCredential=ID:PATH
+ LoadCredentialEncrypted=ID:PATH
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
@@ -2860,6 +2862,20 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
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.
+ The LoadCredentialEncrypted= setting is identical to
+ LoadCredential=, 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
+ systemd-creds1. This
+ credential is loaded, decrypted and then passed to the application in decrypted plaintext form, in
+ the same way a regular credential specified via LoadCredential= 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
+ /var/lib/systemd/credentials.secret, 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.
+
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,
read-only copies for the unit that are accessible to appropriately privileged processes. This is
@@ -2891,6 +2907,7 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
SetCredential=ID:VALUE
+ SetCredentialEncrypted=ID:VALUE
The SetCredential= setting is similar to
LoadCredential= but accepts a literal value to use as data for the credential,
@@ -2901,6 +2918,14 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
C-style escaping (i.e. \n to embed a newline, or \x00 to embed
a NUL byte).
+ The SetCredentialEncrypted= setting is identical to
+ SetCredential= but expects an encrypted credential in literal form as value. This
+ allows embedding confidential credentials securely directly in unit files. Use
+ systemd-creds1'
+ switch to generate suitable SetCredentialEncrypted= lines
+ directly from plaintext credentials. For further details see
+ LoadCredentialEncrypted= above.
+
If a credential of the same ID is listed in both LoadCredential= and
SetCredential=, 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
diff --git a/meson.build b/meson.build
index 738879eb21..366f2c5607 100644
--- a/meson.build
+++ b/meson.build
@@ -1707,7 +1707,8 @@ install_libsystemd_static = static_library(
libcap,
libblkid,
libmount,
- libgcrypt],
+ libgcrypt,
+ libopenssl],
c_args : libsystemd_c_args + (static_libsystemd_pic ? [] : ['-fno-PIC']))
libudev = shared_library(
@@ -2953,6 +2954,17 @@ public_programs += executable(
install : true,
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(
'systemd-volatile-root',
'src/volatile-root/volatile-root.c',
diff --git a/src/basic/chattr-util.c b/src/basic/chattr-util.c
index 10e59875ad..b5658754a5 100644
--- a/src/basic/chattr-util.c
+++ b/src/basic/chattr-util.c
@@ -7,10 +7,19 @@
#include
#include "chattr-util.h"
+#include "errno-util.h"
#include "fd-util.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;
unsigned old_attr, new_attr;
struct stat st;
@@ -57,12 +66,16 @@ int chattr_full(const char *path, int fd, unsigned value, unsigned mask, unsigne
return 1;
}
- if (errno != EINVAL || !fallback)
+ if ((errno != EINVAL && !ERRNO_IS_NOT_SUPPORTED(errno)) ||
+ !FLAGS_SET(flags, CHATTR_FALLBACK_BITWISE))
return -errno;
/* 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.
- * 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;
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;
if (ioctl(fd, FS_IOC_SETFLAGS, &new_one) < 0) {
- if (errno != EINVAL)
+ if (errno != EINVAL && !ERRNO_IS_NOT_SUPPORTED(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;
}
diff --git a/src/basic/chattr-util.h b/src/basic/chattr-util.h
index 3f46367c82..82f91c66d9 100644
--- a/src/basic/chattr-util.h
+++ b/src/basic/chattr-util.h
@@ -34,13 +34,28 @@
FS_NOCOW_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) {
- 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) {
- 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_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);
+}
diff --git a/src/basic/creds-util.c b/src/basic/creds-util.c
deleted file mode 100644
index 58076705e7..0000000000
--- a/src/basic/creds-util.c
+++ /dev/null
@@ -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);
-}
diff --git a/src/basic/creds-util.h b/src/basic/creds-util.h
deleted file mode 100644
index 7310249424..0000000000
--- a/src/basic/creds-util.h
+++ /dev/null
@@ -1,16 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-#pragma once
-
-#include
-#include
-#include
-
-#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);
diff --git a/src/basic/fileio.c b/src/basic/fileio.c
index 99a44fdea2..19fe6e214c 100644
--- a/src/basic/fileio.c
+++ b/src/basic/fileio.c
@@ -522,18 +522,17 @@ int read_full_stream_full(
size_t *ret_size) {
_cleanup_free_ char *buf = NULL;
- size_t n, n_next, l;
+ size_t n, n_next = 0, l;
int fd, r;
assert(f);
assert(ret_contents);
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;
- n_next = size != SIZE_MAX ? size : LINE_MAX; /* Start size */
-
fd = fileno(f);
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 */
@@ -543,20 +542,20 @@ int read_full_stream_full(
return -errno;
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 =
LESS_BY((uint64_t) st.st_size, offset == UINT64_MAX ? 0 : offset);
- /* Safety 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)
+ if (rsize < SIZE_MAX) /* overflow check */
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)
return -errno;
@@ -573,6 +583,11 @@ int read_full_stream_full(
char *t;
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) {
t = malloc(n_next + 1);
if (!t) {
@@ -606,14 +621,18 @@ int read_full_stream_full(
if (feof(f))
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);
break;
}
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) {
r = -E2BIG;
goto finalize;
diff --git a/src/basic/fileio.h b/src/basic/fileio.h
index c28b17fef5..af797cfafd 100644
--- a/src/basic/fileio.h
+++ b/src/basic/fileio.h
@@ -39,6 +39,7 @@ typedef enum {
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_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;
int fopen_unlocked(const char *path, const char *options, FILE **ret);
diff --git a/src/basic/hexdecoct.c b/src/basic/hexdecoct.c
index da1add7c76..172ae8a921 100644
--- a/src/basic/hexdecoct.c
+++ b/src/basic/hexdecoct.c
@@ -565,38 +565,79 @@ int unbase64char(char c) {
return -EINVAL;
}
-ssize_t base64mem(const void *p, size_t l, char **out) {
- char *r, *z;
+static void maybe_line_break(char **x, char *start, size_t line_break) {
+ 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;
+ char *r, *z;
+ size_t m;
assert(p || l == 0);
assert(out);
+ assert(line_break > 0);
/* 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)
return -ENOMEM;
for (x = p; x < (const uint8_t*) p + (l / 3) * 3; x += 3) {
/* x[0] == XXXXXXXX; x[1] == YYYYYYYY; x[2] == ZZZZZZZZ */
+ maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[0] >> 2); /* 00XXXXXX */
+ maybe_line_break(&z, r, line_break);
*(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 */
+ maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[2] & 63); /* 00ZZZZZZ */
}
switch (l % 3) {
case 2:
+ maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[0] >> 2); /* 00XXXXXX */
+ maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[0] & 3) << 4 | x[1] >> 4); /* 00XXYYYY */
+ maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[1] & 15) << 2); /* 00YYYY00 */
+ maybe_line_break(&z, r, line_break);
*(z++) = '=';
break;
case 1:
+ maybe_line_break(&z, r, line_break);
*(z++) = base64char(x[0] >> 2); /* 00XXXXXX */
+ maybe_line_break(&z, r, line_break);
*(z++) = base64char((x[0] & 3) << 4); /* 00XX0000 */
+ maybe_line_break(&z, r, line_break);
*(z++) = '=';
+ maybe_line_break(&z, r, line_break);
*(z++) = '=';
break;
diff --git a/src/basic/hexdecoct.h b/src/basic/hexdecoct.h
index 4ace5b7a99..5218f78665 100644
--- a/src/basic/hexdecoct.h
+++ b/src/basic/hexdecoct.h
@@ -33,7 +33,11 @@ int unbase64char(char c) _const_;
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);
-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,
const void *p, size_t l,
int margin, int width);
diff --git a/src/basic/meson.build b/src/basic/meson.build
index 9b016ce5e8..666537868b 100644
--- a/src/basic/meson.build
+++ b/src/basic/meson.build
@@ -29,8 +29,6 @@ basic_sources = files('''
chattr-util.h
conf-files.c
conf-files.h
- creds-util.c
- creds-util.h
def.h
dirent-util.c
dirent-util.h
diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c
index 50daef6702..f6783e924a 100644
--- a/src/core/dbus-execute.c
+++ b/src/core/dbus-execute.c
@@ -820,6 +820,9 @@ static int property_get_set_credential(
HASHMAP_FOREACH(sc, c->set_credentials) {
+ if (sc->encrypted != streq(property, "SetCredentialEncrypted"))
+ continue;
+
r = sd_bus_message_open_container(reply, 'r', "say");
if (r < 0)
return r;
@@ -850,7 +853,7 @@ static int property_get_load_credential(
sd_bus_error *error) {
ExecContext *c = userdata;
- char **i, **j;
+ ExecLoadCredential *lc;
int r;
assert(bus);
@@ -862,8 +865,12 @@ static int property_get_load_credential(
if (r < 0)
return r;
- STRV_FOREACH_PAIR(i, j, c->load_credentials) {
- r = sd_bus_message_append(reply, "(ss)", *i, *j);
+ HASHMAP_FOREACH(lc, c->load_credentials) {
+
+ if (lc->encrypted != streq(property, "LoadCredentialEncrypted"))
+ continue;
+
+ r = sd_bus_message_append(reply, "(ss)", lc->id, lc->path);
if (r < 0)
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("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("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("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("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),
@@ -1916,7 +1925,7 @@ int bus_exec_context_set_transient_property(
return 1;
- } else if (streq(name, "SetCredential")) {
+ } else if (STR_IN_SET(name, "SetCredential", "SetCredentialEncrypted")) {
bool isempty = true;
r = sd_bus_message_enter_container(message, 'a', "(say)");
@@ -1964,20 +1973,24 @@ int bus_exec_context_set_transient_property(
if (old) {
free_and_replace(old->data, copy);
old->size = sz;
+ old->encrypted = streq(name, "SetCredentialEncrypted");
} else {
_cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
- sc = new0(ExecSetCredential, 1);
+ sc = new(ExecSetCredential, 1);
if (!sc)
return -ENOMEM;
- sc->id = strdup(id);
+ *sc = (ExecSetCredential) {
+ .id = strdup(id),
+ .data = TAKE_PTR(copy),
+ .size = sz,
+ .encrypted = streq(name, "SetCredentialEncrypted"),
+ };
+
if (!sc->id)
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);
if (r < 0)
return r;
@@ -2008,7 +2021,7 @@ int bus_exec_context_set_transient_property(
return 1;
- } else if (streq(name, "LoadCredential")) {
+ } else if (STR_IN_SET(name, "LoadCredential", "LoadCredentialEncrypted")) {
bool isempty = true;
r = sd_bus_message_enter_container(message, 'a', "(ss)");
@@ -2033,9 +2046,39 @@ int bus_exec_context_set_transient_property(
isempty = false;
if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
- r = strv_extend_strv(&c->load_credentials, STRV_MAKE(id, source), /* filter_duplicates = */ false);
- if (r < 0)
- return r;
+ _cleanup_free_ char *copy = NULL;
+ ExecLoadCredential *old;
+
+ 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);
}
@@ -2046,7 +2089,7 @@ int bus_exec_context_set_transient_property(
return r;
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);
}
diff --git a/src/core/execute.c b/src/core/execute.c
index 2a337b55a2..f760d6ae07 100644
--- a/src/core/execute.c
+++ b/src/core/execute.c
@@ -46,6 +46,7 @@
#include "cgroup-setup.h"
#include "chown-recursive.h"
#include "cpu-set-util.h"
+#include "creds-util.h"
#include "data-fd-util.h"
#include "def.h"
#include "env-file.h"
@@ -1454,7 +1455,7 @@ static bool exec_context_has_credentials(const ExecContext *context) {
assert(context);
return !hashmap_isempty(context->set_credentials) ||
- context->load_credentials;
+ !hashmap_isempty(context->load_credentials);
}
#if HAVE_SECCOMP
@@ -2486,7 +2487,7 @@ static int write_credential(
return -errno;
}
- r = loop_write(fd, data, size, /* do_pool = */ false);
+ r = loop_write(fd, data, size, /* do_poll = */ false);
if (r < 0)
return r;
@@ -2519,8 +2520,6 @@ static int write_credential(
return 0;
}
-#define CREDENTIALS_BYTES_MAX (1024LU * 1024LU) /* Refuse to pass more than 1M, after all this is unswappable memory */
-
static int acquire_credentials(
const ExecContext *context,
const ExecParameters *params,
@@ -2529,10 +2528,10 @@ static int acquire_credentials(
uid_t uid,
bool ownership_ok) {
- uint64_t left = CREDENTIALS_BYTES_MAX;
+ uint64_t left = CREDENTIALS_TOTAL_SIZE_MAX;
_cleanup_close_ int dfd = -1;
+ ExecLoadCredential *lc;
ExecSetCredential *sc;
- char **id, **fn;
int r;
assert(context);
@@ -2542,39 +2541,23 @@ static int acquire_credentials(
if (dfd < 0)
return -errno;
- /* 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) {
- 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;
+ /* First, load credentials off disk (or acquire via AF_UNIX socket) */
+ HASHMAP_FOREACH(lc, context->load_credentials) {
+ ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER;
_cleanup_(erase_and_freep) char *data = NULL;
_cleanup_free_ char *j = NULL, *bindname = NULL;
bool missing_ok = true;
const char *source;
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 */
- source = *fn;
+ source = lc->path;
flags |= READ_FULL_FILE_CONNECT_SOCKET;
/* 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. */
- 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;
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
* 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. */
- j = path_join(params->received_credentials, *fn);
+ j = path_join(params->received_credentials, lc->path);
if (!j)
return -ENOMEM;
@@ -2592,34 +2575,87 @@ static int acquire_credentials(
source = NULL;
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
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
* 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
* 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,
- * then don't fail either. */
- log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", *fn);
+ * Also, if the source file doesn't exist, but a fallback is set via SetCredentials=
+ * we are fine, too. */
+ log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", lc->path);
continue;
}
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)
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)
return r;
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 */
return -errno;
@@ -2715,7 +2751,7 @@ static int setup_credentials_internal(
} else if (try == 1) {
_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;
/* Fall back to "tmpfs" otherwise */
@@ -4928,7 +4964,7 @@ void exec_context_done(ExecContext *c) {
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);
}
@@ -6597,7 +6633,17 @@ ExecSetCredential *exec_set_credential_free(ExecSetCredential *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_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] = {
[EXEC_INPUT_NULL] = "null",
diff --git a/src/core/execute.h b/src/core/execute.h
index 4c7a5b874f..fceb8bb6f7 100644
--- a/src/core/execute.h
+++ b/src/core/execute.h
@@ -150,9 +150,16 @@ typedef enum ExecCleanMask {
_EXEC_CLEAN_MASK_INVALID = -EINVAL,
} ExecCleanMask;
+/* A credential configured with LoadCredential= */
+typedef struct ExecLoadCredential {
+ char *id, *path;
+ bool encrypted;
+} ExecLoadCredential;
+
/* A credential configured with SetCredential= */
typedef struct ExecSetCredential {
char *id;
+ bool encrypted;
void *data;
size_t size;
} ExecSetCredential;
@@ -325,7 +332,7 @@ struct ExecContext {
usec_t timeout_clean_usec;
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) {
@@ -458,7 +465,11 @@ bool exec_context_get_cpu_affinity_from_numa(const ExecContext *c);
ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc);
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_load_credential_hash_ops;
const char* exec_output_to_string(ExecOutput i) _const_;
ExecOutput exec_output_from_string(const char *s) _pure_;
diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in
index 42441eab6e..96507907f6 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -139,7 +139,9 @@
{{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}}.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}}.LoadCredentialEncrypted, config_parse_load_credential, 1, offsetof({{type}}, exec_context)
{{type}}.TimeoutCleanSec, config_parse_sec, 0, offsetof({{type}}, exec_context.timeout_clean_usec)
{% if HAVE_PAM %}
{{type}}.PAMName, config_parse_unit_string_printf, 0, offsetof({{type}}, exec_context.pam_name)
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
index 8fb3c378ee..c72f5c22da 100644
--- a/src/core/load-fragment.c
+++ b/src/core/load-fragment.c
@@ -4469,12 +4469,15 @@ int config_parse_set_credential(
void *data,
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;
ExecSetCredential *old;
Unit *u = userdata;
- const char *p;
- int r, l;
+ bool encrypted = ltype;
+ const char *p = rvalue;
+ size_t size;
+ int r;
assert(filename);
assert(lvalue);
@@ -4487,7 +4490,6 @@ int config_parse_set_credential(
return 0;
}
- p = rvalue;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM)
return log_oom();
@@ -4506,33 +4508,51 @@ int config_parse_set_credential(
return 0;
}
- /* 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;
+ if (encrypted) {
+ r = unbase64mem_full(p, SIZE_MAX, true, &d, &size);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Encrypted credential data not valid Base64 data, ignoring.");
+ 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);
if (old) {
- free_and_replace(old->data, unescaped);
- old->size = l;
+ free_and_replace(old->data, d);
+ old->size = size;
+ old->encrypted = encrypted;
} else {
_cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
- sc = new0(ExecSetCredential, 1);
+ sc = new(ExecSetCredential, 1);
if (!sc)
return log_oom();
- sc->id = TAKE_PTR(k);
- sc->data = TAKE_PTR(unescaped);
- sc->size = l;
+ *sc = (ExecSetCredential) {
+ .id = TAKE_PTR(k),
+ .data = TAKE_PTR(d),
+ .size = size,
+ .encrypted = encrypted,
+ };
r = hashmap_ensure_put(&context->set_credentials, &exec_set_credential_hash_ops, sc->id, sc);
if (r == -ENOMEM)
return log_oom();
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);
return 0;
}
@@ -4557,6 +4577,8 @@ int config_parse_load_credential(
_cleanup_free_ char *word = NULL, *k = NULL, *q = NULL;
ExecContext *context = data;
+ ExecLoadCredential *old;
+ bool encrypted = ltype;
Unit *u = userdata;
const char *p;
int r;
@@ -4568,7 +4590,7 @@ int config_parse_load_credential(
if (isempty(rvalue)) {
/* Empty assignment resets the list */
- context->load_credentials = strv_free(context->load_credentials);
+ context->load_credentials = hashmap_free(context->load_credentials);
return 0;
}
@@ -4604,14 +4626,39 @@ int config_parse_load_credential(
return 0;
}
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;
}
}
- r = strv_consume_pair(&context->load_credentials, TAKE_PTR(k), TAKE_PTR(q));
- if (r < 0)
- return log_oom();
+ old = hashmap_get(context->load_credentials, k);
+ if (old) {
+ 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;
}
diff --git a/src/creds/creds.c b/src/creds/creds.c
new file mode 100644
index 0000000000..50de685cd4
--- /dev/null
+++ b/src/creds/creds.c
@@ -0,0 +1,812 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#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);
diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c
index c8fb726d42..17fc493b22 100644
--- a/src/journal/journalctl.c
+++ b/src/journal/journalctl.c
@@ -1902,19 +1902,10 @@ static int setup_keys(void) {
if (fd < 0)
return log_error_errno(fd, "Failed to open %s: %m", k);
- /* Enable secure remove, exclusion from dump, synchronous writing and in-place updating */
- static const unsigned chattr_flags[] = {
- FS_SECRM_FL,
- FS_NODUMP_FL,
- 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]);
- }
+ r = chattr_secret(fd, CHATTR_WARN_UNSUPPORTED_FLAGS);
+ if (r < 0)
+ log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING,
+ r, "Failed to set file attributes on '%s', ignoring: %m", k);
struct FSSHeader h = {
.signature = { 'K', 'S', 'H', 'H', 'R', 'H', 'L', 'P' },
diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build
index 489ed12a73..154d9acd2a 100644
--- a/src/libsystemd/meson.build
+++ b/src/libsystemd/meson.build
@@ -166,7 +166,8 @@ libsystemd_static = static_library(
include_directories : libsystemd_includes,
link_with : libbasic,
dependencies : [threads,
- librt],
+ librt,
+ libopenssl],
c_args : libsystemd_c_args)
libsystemd_sym = files('libsystemd.sym')
diff --git a/src/libsystemd/sd-id128/sd-id128.c b/src/libsystemd/sd-id128/sd-id128.c
index d5de935c77..28ae10a198 100644
--- a/src/libsystemd/sd-id128/sd-id128.c
+++ b/src/libsystemd/sd-id128/sd-id128.c
@@ -4,6 +4,11 @@
#include
#include
+#if HAVE_OPENSSL
+#include
+#include
+#endif
+
#include "sd-id128.h"
#include "alloc-util.h"
@@ -11,7 +16,9 @@
#include "hexdecoct.h"
#include "id128-util.h"
#include "io-util.h"
+#if !HAVE_OPENSSL
#include "khash.h"
+#endif
#include "macro.h"
#include "missing_syscall.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) {
- _cleanup_(khash_unrefp) khash *h = NULL;
sd_id128_t result;
- const void *p;
- int r;
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));
if (r < 0)
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 */
memcpy(&result, p, MIN(khash_get_size(h), sizeof(result)));
+#endif
*ret = id128_make_v4_uuid(result);
return 0;
diff --git a/src/shared/blockdev-util.c b/src/shared/blockdev-util.c
index 4d545dfa58..53df7d2f0d 100644
--- a/src/shared/blockdev-util.c
+++ b/src/shared/blockdev-util.c
@@ -57,21 +57,16 @@ int block_get_whole_disk(dev_t d, dev_t *ret) {
return 1;
}
-int get_block_device(const char *path, dev_t *ret) {
- _cleanup_close_ int fd = -1;
+int get_block_device_fd(int fd, dev_t *ret) {
struct stat st;
int r;
- assert(path);
+ assert(fd >= 0);
assert(ret);
/* Gets the block device directly backing a file system. If the block device is encrypted, returns
* the device mapper block device. */
- fd = open(path, O_RDONLY|O_NOFOLLOW|O_CLOEXEC);
- if (fd < 0)
- return -errno;
-
if (fstat(fd, &st))
return -errno;
@@ -90,6 +85,19 @@ int get_block_device(const char *path, dev_t *ret) {
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) {
_cleanup_closedir_ DIR *d = 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;
}
+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) {
char p[SYS_BLOCK_PATH_MAX(NULL)];
dev_t devt;
diff --git a/src/shared/blockdev-util.h b/src/shared/blockdev-util.h
index 56c50cecba..d36707cf63 100644
--- a/src/shared/blockdev-util.h
+++ b/src/shared/blockdev-util.h
@@ -15,6 +15,7 @@
int block_get_whole_disk(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_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 fd_is_encrypted(int fd);
int path_is_encrypted(const char *path);
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index d3a5b25d18..a97ffbcea4 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -1046,12 +1046,12 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
return 1;
}
- if (streq(field, "SetCredential")) {
+ if (STR_IN_SET(field, "SetCredential", "SetCredentialEncrypted")) {
r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0)
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)
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))
r = sd_bus_message_append(m, "a(say)", 0);
else {
- _cleanup_free_ char *word = NULL, *unescaped = NULL;
+ _cleanup_free_ char *word = NULL;
const char *p = eq;
- int l;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM)
return log_oom();
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)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to SetCredential=.");
-
- l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
- if (l < 0)
- return log_error_errno(l, "Failed to unescape SetCredential= value: %s", p);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to %s=.", field);
r = sd_bus_message_open_container(m, 'a', "(say)");
if (r < 0)
@@ -1090,7 +1085,25 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
if (r < 0)
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)
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;
}
- if (streq(field, "LoadCredential")) {
+ if (STR_IN_SET(field, "LoadCredential", "LoadCredentialEncrypted")) {
r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0)
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)
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)
return log_oom();
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)
- 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);
}
diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c
new file mode 100644
index 0000000000..6c2d9dbc76
--- /dev/null
+++ b/src/shared/creds-util.c
@@ -0,0 +1,930 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#if HAVE_OPENSSL
+#include
+#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
diff --git a/src/shared/creds-util.h b/src/shared/creds-util.h
new file mode 100644
index 0000000000..caf632de6e
--- /dev/null
+++ b/src/shared/creds-util.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include
+#include
+#include
+
+#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);
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 7eb7050062..1f741f7417 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -81,6 +81,8 @@ shared_sources = files('''
coredump-util.h
cpu-set-util.c
cpu-set-util.h
+ creds-util.c
+ creds-util.h
cryptsetup-util.c
cryptsetup-util.h
daemon-util.h
diff --git a/src/test/test-fileio.c b/src/test/test-fileio.c
index 321b544448..2b6ef495aa 100644
--- a/src/test/test-fileio.c
+++ b/src/test/test-fileio.c
@@ -999,6 +999,25 @@ static void test_read_full_file_offset_size(void) {
assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
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(rbuf_size == sizeof(buf) - 1234);
assert_se(memcmp(buf + 1234, rbuf, rbuf_size) == 0);
diff --git a/src/test/test-hexdecoct.c b/src/test/test-hexdecoct.c
index c9d318b8d1..a3767b47be 100644
--- a/src/test/test-hexdecoct.c
+++ b/src/test/test-hexdecoct.c
@@ -5,6 +5,7 @@
#include "alloc-util.h"
#include "hexdecoct.h"
#include "macro.h"
+#include "random-util.h"
#include "string-util.h"
static void test_hexchar(void) {
@@ -275,6 +276,37 @@ static void test_base64mem(void) {
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) {
_cleanup_free_ void *buffer = NULL;
size_t size = 0;
@@ -348,6 +380,7 @@ int main(int argc, char *argv[]) {
test_base32hexmem();
test_unbase32hexmem();
test_base64mem();
+ test_base64mem_linebreak();
test_unbase64mem();
test_hexdump();
diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c
index 7e85c50634..b32b709ef5 100644
--- a/src/tmpfiles/tmpfiles.c
+++ b/src/tmpfiles/tmpfiles.c
@@ -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);
unsigned previous, current;
- r = chattr_full(NULL, procfs_fd, f, item->attribute_mask, &previous, ¤t, true);
+ r = chattr_full(NULL, procfs_fd, f, item->attribute_mask, &previous, ¤t, CHATTR_FALLBACK_BITWISE);
if (r == -ENOANO)
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.",
diff --git a/test/fuzz/fuzz-unit-file/directives.mount b/test/fuzz/fuzz-unit-file/directives.mount
index 451f291988..d161c81ff6 100644
--- a/test/fuzz/fuzz-unit-file/directives.mount
+++ b/test/fuzz/fuzz-unit-file/directives.mount
@@ -83,6 +83,7 @@ LimitRTTIME=
LimitSIGPENDING=
LimitSTACK=
LoadCredential=
+LoadCredentialEncrypted=
LockPersonality=
LogExtraFields=
LogLevelMax=
@@ -159,6 +160,7 @@ SecureBits=
SendSIGHUP=
SendSIGKILL=
SetCredential=
+SetCredentialEncrypted=
Slice=
SloppyOptions=
SmackProcessLabel=
diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service
index de7d2c7daf..35d1f2a104 100644
--- a/test/fuzz/fuzz-unit-file/directives.service
+++ b/test/fuzz/fuzz-unit-file/directives.service
@@ -205,6 +205,7 @@ LimitRTTIME=
LimitSIGPENDING=
LimitSTACK=
LoadCredential=
+LoadCredentialEncrypted=
LockPersonality=
LogExtraFields=
LogLevelMax=
@@ -292,6 +293,7 @@ SecureBits=
SendSIGHUP=
SendSIGKILL=
SetCredential=
+SetCredentialEncrypted=
Slice=
SmackProcessLabel=
Sockets=
diff --git a/test/fuzz/fuzz-unit-file/directives.socket b/test/fuzz/fuzz-unit-file/directives.socket
index 11f589e22c..1835167cfb 100644
--- a/test/fuzz/fuzz-unit-file/directives.socket
+++ b/test/fuzz/fuzz-unit-file/directives.socket
@@ -108,6 +108,7 @@ ListenSpecial=
ListenStream=
ListenUSBFunction=
LoadCredential=
+LoadCredentialEncrypted=
LockPersonality=
LogExtraFields=
LogLevelMax=
@@ -199,6 +200,7 @@ SendSIGHUP=
SendSIGKILL=
Service=
SetCredential=
+SetCredentialEncrypted=
Slice=
SmackLabel=
SmackLabelIPIn=
diff --git a/test/fuzz/fuzz-unit-file/directives.swap b/test/fuzz/fuzz-unit-file/directives.swap
index 582a136531..814d066fac 100644
--- a/test/fuzz/fuzz-unit-file/directives.swap
+++ b/test/fuzz/fuzz-unit-file/directives.swap
@@ -80,6 +80,7 @@ LimitRTTIME=
LimitSIGPENDING=
LimitSTACK=
LoadCredential=
+LoadCredentialEncrypted=
LockPersonality=
LogExtraFields=
LogLevelMax=
@@ -156,6 +157,7 @@ SecureBits=
SendSIGHUP=
SendSIGKILL=
SetCredential=
+SetCredentialEncrypted=
Slice=
SmackProcessLabel=
SocketBindAllow=
diff --git a/test/units/testsuite-54.sh b/test/units/testsuite-54.sh
index f8ddff4a30..d8c9ffa38b 100755
--- a/test/units/testsuite-54.sh
+++ b/test/units/testsuite-54.sh
@@ -27,6 +27,30 @@ systemd-run -p LoadCredential=passwd:/etc/passwd \
rm '${CREDENTIALS_DIRECTORY}/passwd' \
&& { 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
echo OK >/testok