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