keyfile-erase only erases a key file explicitly configured in the third
field of crypttab. Key files automatically discovered in
/etc/cryptsetup-keys.d/ and /run/cryptsetup-keys.d/ are considered shared
resources not owned by an individual volume, and are never erased. Make
this explicit in the documentation and add a code comment clarifying the
intent.
Fixes: https://github.com/systemd/systemd/issues/41127
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Switch the executable-owned call sites from the plain dlopen_<lib>()
helpers to the new DLOPEN_<LIB>() macros, so that each binary that loads
an optional library at runtime now carries its own .note.dlopen note
instead of relying solely on libsystemd-shared. Each call site passes a
priority reflecting whether it hard-depends on the library (required) or
degrades gracefully without it (recommended).
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
RSA PKCS#1 v1.5 is vulnerable to Bleichenbacher-style padding oracle
attacks, albeit very difficult and unlikely to actually happen in the
real world. Still for hardedning, switch new enrollments to RSA-OAEP,
with SHA-256 preferred and SHA-1 as fallback (probed at enrollment time,
since e.g. SoftHSM only accepts SHA-1, and older token might as well).
The actual padding scheme used to wrap a given key is recorded as a new
optional 'pkcs11-padding' / 'padding' field in the LUKS2 token JSON and
the homed user record. Decryption defaults to PKCS#1 v1.5 when absent so
existing enrollments keep working.
For libc syscall wrappers (pidfd_open, fsopen, openat2, etc.) we previously
gated the calls behind build-time HAVE_* checks. Replace these with shim
functions in src/libc/ that fall back to the raw syscall at runtime when the
loaded glibc lacks the symbol. The infrastructure lives in src/libc/libc-shim.h:
DEFINE_SYSCALL_SHIM falls back to a direct syscall, DEFINE_LIBC_SHIM returns
ENOSYS (for posix_spawn-family helpers that have no corresponding syscall), and
DEFINE_LIBC_ERRNO_SHIM sets errno=ENOSYS and returns -1 (for read/write-style
helpers). The weak reference to the libc symbol is bound via __asm__(\"name\")
rename so the bare libc identifier never appears as a C token — this avoids
both #undef boilerplate against override-header redirects and the resulting
-Wredundant-decls warning. Drop the corresponding cc.has_function() loop from
meson.build.
For optional libraries (libcryptsetup, libdw, libarchive), drop the per-symbol
HAVE_* checks. Always declare the prototypes, suppressing the redundant-decl
warnings via DISABLE_WARNING_REDUNDANT_DECLS and NOLINT, and resolve the symbols
after the main dlopen via a new DLSYM_OPTIONAL() helper that only assigns on
success. libcryptsetup's crypt_set_keyring_to_link / crypt_token_set_external_path
and libarchive's *_is_set wrappers use fallback functions as their pointer
initializers (returning -ENOSYS and 0 respectively), so call sites can invoke
the symbol unconditionally and just check for -ENOSYS where the \"not supported\"
distinction matters.
The same shim treatment applies to pidfd_spawn / posix_spawnattr_setcgroup_np
(src/libc/spawn.c) and epoll_pwait2 (src/libc/epoll.c), with corresponding
override headers in src/include/override/spawn.h and
src/include/override/sys/epoll.h. posix_spawn_wrapper() in process-util.c and
epoll_wait_usec() in sd-event.c now detect ENOSYS in the return value instead
of checking the function pointer, falling back to plain posix_spawn() and
epoll_wait() respectively. coredump-config and coredump-submit get a
dlopen_dw_has_dwfl_set_sysroot() helper. The kexec arch gate now uses
defined(__NR_kexec_file_load) directly; pidfd.h uses __has_include_next() to
decide whether to pull in glibc's header.
This lets binaries built against newer glibc / libcryptsetup / libdw /
libarchive headers still load and run on older targets where these symbols are
absent.
When a keyfile is passed with tpm2-device=, e.g.,
systemd-cryptsetup attach test_data /vol /my-pass tpm2-device=auto
the logic in attach_luks_or_plain_or_bitlk_by_tpm2() tries to process it as a
TPM blob first. This did not work properly because it passes n_blobs=1 to
acquire_tpm2_key(), and the key_file is only read when n_blobs == 0. As a
result, the code ends up calling tpm2_unseal(..., blobs=NULL, n_blobs=1, ...).
Before commit 5c6aad9 ("cryptsetup-tokens: Print tpm2-primary-alg: only when it
is known"), the segfault was not observed because tpm2_unseal() was bailing out
early when primary_alg == 0. However, after that change, it attempts to process
the blob (which is NULL) and crashes.
Fix this logic by passing n_blobs=0 to acquire_tpm2_key() so that it actually
reads the key_file. Additionally, assert 'blobs' in tpm2_unseal() as a
safeguard.
Fixes#41867
I am really not a fan of full code lines passed to macros as parameters.
Let's get rid of the 3rd parameter of FOREACH_OPTION() hence:
1. Let's return errors just as a regular value (though a negative one),
that can be handled via a OPTION_ERROR case statement for the switch.
This normalizes handling of the error, just like any other event
returned by the option parser.
2. In order to avoid exploding the amount of boilerplate in each use
(that just propagates the error on OPTION_ERROR), let's then
introduce an explicit FOREACH_OPTION_OR_RETURN(), that returns from
the calling function on its own (and makes that clear in the name).
Together this cleans up, normalizes the logic and shortens the code.
It was requested to make the 'c', 'opt', and 'arg' params the same, i.e.
defined through the FOREACH_OPTION macro. But we can't do that easily,
because 'c' was defined in the for loop definition, and we can only
define variables of the same type in that way. Also, in some cases we
need only 'c', in other cases with need 'c' and 'arg, in some cases 'c'
and 'opt', and in other cases all three. We'd need to either
conditionalize or mark those variables with _unused_ to deal with
compiler warnings. But a different approach works quite nicely: add
state.opt and state.arg to show the current option and it's argument.
(The short names are picked on purpose to reduce verbosity since those
are used a lot.)
Until now OpenSSL was linked into every binary and library that needed
cryptography, pulling libcrypto (and, for resolved, libssl) into the
address space of services that never touch them at runtime. This commit
moves all OpenSSL usage behind the same dlopen helper pattern that we
already use for other optional libraries (libpam, libseccomp, libxz, …)
so libcrypto/libssl are only loaded on demand.
The bulk of the work lives in src/shared/crypto-util.{c,h} (libcrypto)
and src/shared/ssl-util.{c,h} (libssl), which replace the previous
src/shared/openssl-util.{c,h}:
- crypto-util.{c,h} declares every libcrypto function we call via
DLSYM_PROTOTYPE() and resolves them inside dlopen_libcrypto().
- ssl-util.{c,h} holds the libssl-specific DLSYM_PROTOTYPEs,
dlopen_libssl(), and the SSL_freep cleanup helper, so translation
units that only need libcrypto do not pull in libssl declarations.
- Callers refer to the symbols through sym_* aliases rather than the
original names.
- Convenience macros that used to be provided by the OpenSSL headers
(OPENSSL_free, BN_num_bytes, the sk_TYPE_* helpers, …) are
reimplemented as sym_* wrappers so no code path needs to fall back
to the linker-resolved symbols.
- All _cleanup_ helpers are redefined in terms of the sym_* variants
(EVP_PKEY_freep, X509_freep, BIO_freep, …) so cleanup attributes
keep working without pulling in libcrypto symbols at link time.
- The public crypto-util.c entry points (openssl_pubkey_from_pem,
openssl_digest_many, openssl_hmac_many, openssl_cipher_many,
kdf_ss_derive, kdf_kb_hmac_derive, rsa_* / ecc_* helpers,
pubkey_fingerprint, digest_and_sign, pkcs7_new, x509_fingerprint,
openssl_extract_public_key, pkey_generate_volume_keys, the load_*
helpers, …) now call dlopen_libcrypto() at entry before touching any
sym_* pointer.
The call sites across the tree have been converted to call
dlopen_libcrypto()/dlopen_libssl() at the appropriate entry point
before their first sym_* use, and to use sym_* variants throughout:
- bootctl, sbsign, measure, pcrlock, pcrextend, tpm2-setup, repart,
cryptsetup, cryptenroll, homectl, homed, homework, keyutil,
sysupdate, creds, import, dissect-image, pe-binary, pkcs11-util,
pkcs7-util, tpm2-util, creds-util. resolved additionally dlopens
libssl for DoT.
The meson build files are updated to depend on libopenssl_cflags (a
new partial dependency that exposes include paths and compile flags
only, not the linker flags) instead of libopenssl for every target that
previously linked against OpenSSL. Nothing links against libcrypto or
libssl directly anymore.
A new src/sbsign/authenticode.c hosts the Authenticode ASN.1 type
definitions that used to live inline in sbsign.c. The OpenSSL
ASN1_SEQUENCE / ASN1_CHOICE / IMPLEMENT_ASN1_FUNCTIONS macros expand to
code that references libcrypto symbols directly, so to keep this
translation unit unlinked from libcrypto we redirect ASN1_item_* to
the sym_* variants via #define and wrap the ASN1_*_it() getters (which
appear as constant function pointers in static initializers) in small
trampoline functions that forward to the sym_* pointers at runtime.
test-dlopen-so gains assertions for dlopen_libcrypto and dlopen_libssl
so the dlopen contract is exercised in CI, and the openssl-specific
test was renamed from test-openssl.c to test-crypto-util.c to match
the new header naming.
Convert systemd-cryptsetup, systemd-cryptenroll, systemd-veritysetup
and systemd-integritysetup to go through the existing dlopen wrapper
for libcryptsetup instead of linking the library directly. Each binary
calls dlopen_cryptsetup() at the start of its run() and uses the sym_*
variants for every libcryptsetup entry point.
Extend cryptsetup-util.{h,c} to cover the libcryptsetup symbols that
these binaries use and that the wrapper was missing:
crypt_activate_by_token_pin, crypt_deactivate, crypt_init_data_device,
crypt_keyslot_status, crypt_set_keyring_to_link (conditional on
HAVE_CRYPT_SET_KEYRING_TO_LINK), crypt_status and
crypt_token_external_path.
With no direct callers of crypt_free() left, drop the non-sym
crypt_freep cleanup variant and rename sym_crypt_freep back to
crypt_freep via DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME, matching the
naming convention used by other dlopen wrappers (acl_freep,
xkb_context_unrefp, ...). Update the remaining users in src/shared,
src/repart, src/home and src/growfs to the new name.
The four affected meson targets switch from libcryptsetup to
libcryptsetup_cflags so they no longer record a DT_NEEDED entry for
libcryptsetup.so.12.
The synopisis is moved from the header to the a new section:
-systemd-cryptsetup attach VOLUME SOURCE-DEVICE [KEY-FILE] [CONFIG]
-systemd-cryptsetup detach VOLUME
+systemd-cryptsetup [OPTIONS...] {COMMAND} ...
Attach or detach an encrypted block device.
+Commands:
+ attach VOLUME SOURCE-DEVICE [KEY-FILE] [CONFIG] Attach an encrypted block
+ device
+ detach VOLUME Detach an encrypted block
+ device
+
+Options:
I think that's OK… With the autogenerated table that's the natural
thing to do.
Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
The fix in 8f1751a111 made me wonder if we could automatically detect
when pointers are accessed but when this might not be safe. Systemd
is already using a lot of `assert(dst)` and this change now forces
us to use them.
So this commit (ab)uses coccinelle to flag any pointer parameter
dereference not preceded by assert(param), ASSERT_PTR(param), or an
explicit NULL check. It adds integration into meson as a new "coccinelle"
test suite (just like clang-tidy) and is run in CI. The check is not
perfect but seems a reasonable heuristic.
For this RFC commit it is scoped to a subset, it excludes 25 dirs right
now and includes around 100. About 300 warnings left. Busywork that I am
happy to do if there is agreement that it is worth it.
With this in place we would have caught the bug from 8f1751a111 in CI:
```
FAIL: check-pointer-deref.cocci found issues in systemd/src/boot:
diff -u -p systemd/src/boot/measure.c /tmp/nothing/measure.c
--- systemd/src/boot/measure.c
+++ /tmp/nothing/measure.c
@@ -312,7 +312,6 @@ EFI_STATUS tpm_log_tagged_event(
if (err != EFI_SUCCESS)
return err;
- *ret_measured = true;
return EFI_SUCCESS;
}
```
This also adds a new POINTER_MAY_BE_NULL() for the cases when the
called function will do the NULL check (like `iovec_is_set()`).
So far we always conditioned our TPM magic on the UKI having detected
TPM support in the firmware. This is a bit limiting when we want to
support a software TPM that is not visible to the firmware. Hence let's
split this up, and add a separate control that can be set via the kernel
command line. However, as before, let's by default inherit the firmare
TPM discovery state into it, to retain the current behaviour unless
overriden.
With this in place, boot with "systemd.tpm2_measured_os=1
systemd.tpm2_software_fallback=1" on the kernel cmdline to get the swtpm
fallback and then a measured OS based on it.
We often have a pattern where the same verb function is used for
multiple actions. This leads to an antipattern where we figure out what
action needs to be taken from argv[0] multiple times: often once in
parse_argv() to figure out what options are allowed, then once again
implicitly in dispatch_verb(), and then again in the action verb itself.
Let's allow passing a parameter into the verb to simplify this.
This matches a pattern we have in conf-parser.h, where we have both
void *userdata (more global) and void *data (per-config item). Here,
I opted for uintptr_t userdata. It seems that most of the time we'll
want to just pass an enum value. This works OK with no casts. I also
tried a void* and union. In both cases, much more boilerplate is needed:
either a cast or a macro to help avoid compiler warnings. uintptr_t
seems generic enough to cover foreseeable usecases with no fuss.
This is a noop refactoring. See next commit for an example.
Let's pick the default NvPCR name to use inside of cryptsetup itself, instead
of in the generator. I think this is the better choice, since it means
the default can also be used if the regular verittab generator is used
instead of the gpt-auto generator.
The expected hash (SHA265 HMAC signature) uses the exact same algorithm which
is used to calculate sha256 PCR bank digest when 'tpm2-measure-pcr=' is used.
Calling mlockall in an unprivileged process most notably had the effect
of making systemd-cryptenroll OOM while trying to open a normal-sized
argon2 keyslot due to it hitting RLIMIT_MEMLOCK.
When employing MCL_FUTURE we don't actually want it to impose
immediate population of malloc()-ed pages. Hence let's set
MCL_ONFAULT everywhere.
Additionally, specify MCL_CURRENT to ensure future memory allocations
on already mapped pages are covered too. (Addresses
https://github.com/systemd/systemd/pull/40319#discussion_r2693726196)
Note that in shutdown the mlockall() is done to avoid keeping swap space
busy, hence a dedicated call w/ MCL_CURRENT and w/o MCL_ONFAULT is made.
Major distributions already have cryptsetup newer than 2.4.0.
Let's bump the minimal required version.
Note, cryptsetup 2.4.0 was released on 2021-08-18.
See also #38608.
We soon want to add the ability to extend into nvindexes in addition to
PCRs, hence rename the function to make clear it is about pcr extension.
While we are at it, switch things over to "struct iovec" as we generally
try to do it now in tpm2-util.[ch] these days.
Let's move some logic from _DEFINE_MAIN_FUNCTION() and other places
in main-func.h into functions that we implement in main-func.c to
allow moving some included headers from the header to the .c file.
This introduce LOG_ITEM() macro that checks arbitrary formats in
log_struct().
Then, drop _printf_ attribute from log_struct_internal(), as it does not
help so much, and compiler checked only the first format string.
Hopefully, this silences false-positive warnings by Coverity.
PCR 7 covers the SecureBoot policy, in particular "dbx", i.e. the
denylist of bad actors. That list is pretty much as frequently updated
as firmware these days (as fwupd took over automatic updating). This
means literal PCR 7 policies are problematic: they likely break soon,
and are as brittle as any other literal PCR policies.
hence, pick safer defaults, i.e. exclude PCR 7 from the default mask.
This means the mask is now empty.
Generally, people should really switch to signed PCR policies covering
PCR 11, in combination with systemd-pcrlock for the other PCRs.
This new field allows specification of an fd on which the password
prompt logic will look for POLLHUP events for, and if seen will abort
the query.
The usecase for this is that when we query for a pw on behalf of a
Varlink client we can abort the query automatically if the client dies.
So far you had to pick:
1. Use a signed PCR TPM2 policy to lock your disk to (i.e. UKI vendor
blesses your setup via signature)
or
2. Use a pcrlock policy (i.e. local system blesses your setup via
dynamic local policy stored in NV index)
It was not possible combine these two, because TPM2 access policies do
not allow the combination of PolicyAuthorize (used to implement #1
above) and PolicyAuthorizeNV (used to implement #2) in a single policy,
unless one is "further upstream" (and can simply remove the other from
the policy freely).
This is quite limiting of course, since we actually do want to enforce
on each TPM object that both the OS vendor policy and the local policy
must be fulfilled, without the chance for the vendor or the local system
to disable the other.
This patch addresses this: instead of trying to find a way to come up
with some adventurous scheme to combine both policy into one TPM2
policy, we simply shard the symmetric LUKS decryption key: one half we
protect via the signed PCR policy, and the other we protect via the
pcrlock policy. Only if both halves can be acquired the disk can be
decrypted.
This means:
1. we simply double the unlock key in length in case both policies shall
be used.
2. We store two resulting TPM policy hashes in the LUKS token JSON, one
for each policy
3. We store two sealed TPM policy key blobs in the LUKS token JSON, for
both halves of the LUKS unlock key.
This patch keeps the "sharding" logic relatively generic (i.e. the low
level logic is actually fine with more than 2 shards), because I figure
sooner or later we might have to encode more shards, for example if we
add further TPM2-based access policies, for example when combining FIDO2
with TPM2, or implementing TOTP for this.
This commit makes systemd-cryptsetup exit with a successful status when
the volume gets unlocked outside of the current systemd-cryptsetup
process while it was executing. This can be easily reproduced by calling
systemd-cryptsetup, and while it waits for user to input a password/PIN,
unlock the volume in a second terminal. Then after entering the password
systemd-cryptsetup will exit with a non-zero status code.
The new "password-cache" option allows customizing behavior of the
ask-password module in regards to caching credentials in the kernel
keyring. There are 3 possible values for this option:
* read-only - look for credentials in kernel keyring before asking
* on - same as read-only, but also save credentials input by user
* off - disable keyring credential cache
Currently the cache is forced upon the user and this can cause issues.
For example, if user wants to attach two volumes with two different
FIDO2 tokens in a quick succession, the attachment operation for the
second volume will use the PIN cached from the first FIDO2 token, which
of course will fail and since tokens are only attempted once, this will
cause fallback to a password prompt.
Currently, if user doesn't specify a key file, /etc/cryptsetup-keys.d/
and /run/cryptsetup-keys.d/ will be searched for a key file with name
matching the volume name. But current implementation has an important
flaw. When the auto-discovered key is a socket file - it will read the
key only once, while the socket might provide different keys for
different types of tokens. The issue is fixed by trying to discover the
key on each unlock attempt, this way we can populate the socket bind
name with something the key provider might use to differentiate between
different keys it has to provide.
systemd-cryptsetup supports a FIDO2 mode with manual parameters, where
the user provides all the information necessary for recreating the
secret, such as: credential ID, relaying party ID and the salt. This
feature works great for implementing 2FA schemes, where the salt file
is for example a secret unsealed from the TPM or some other source.
While the unlocking part is quite straightforward to set up, enrolling
such a keyslot - not so easy. There is no clearly documented
way on how to set this up and online resources are scarce on this topic
too. By implementing a straightforward way to enroll such a keyslot
directly from systemd-cryptenroll we streamline the enrollment process
and reduce chances for user error when doing such things manually.