Reword the note for --fido2-with-user-presence= to make it clear that the
setting has no effect if the token does not support the up feature, or does
not allow the user presence requirement to be enabled or disabled for the
requested operation. Drop the surrounding parentheses and the spurious
"in other contexts" qualifier.
Apply the same rewording to the sibling --fido2-with-client-pin= and
--fido2-with-user-verification= options so the three parallel FIDO2
entries stay consistent.
Fixes#23632.
The current TPM2 PIN mode is flawed as a compromised TPM directly exposes
the sealed secret which is the LUKS volume key itself (#27502 and #37386).
Goal: add Argon2id-based PIN hardening to TPM2 enrollment, making
the TPM a second factor rather than a single point of failure:
1. Password + salt -> Argon2id -> 512-bit key split into Key1 + Key2
2. Key2 (base64-encoded) is used as the PIN to seal a random secret
in the TPM
3. Key1 + unsealed secret -> HKDF-SHA256 -> final LUKS volume key
This implementation ensures that if the TPM is compromised, an attacker
still needs the password to derive Key1 and combine it with the unsealed
secret.
The --tpm2-with-pin= option now accepts three values:
- false (no PIN used)
- true (PIN hardened with Argon2id - default)
- "direct" (legacy PIN without Argon2id for backward compatibility)
Argon2id parameters are customizable via:
--tpm2-argon2id-memory=
--tpm2-argon2id-iterations=
--tpm2-argon2id-parallelism=
--tpm2-argon2id-iter-time=
These default to a function of available CPUs and physical memory, with
a benchmark that scales iterations to the target time (default: 2s) and
falls back to ARGON2ID_PARAMETERS_DEFAULT (64 MiB, 8 iter, 4 lanes) when
auto detection fails.
Also if the runtime OpenSSL lacks Argon2id support (< 3.2), the feature
silently falls back to direct PIN mode with a warning.
Added includes:
- src/cryptenroll/cryptenroll.c: cpu-set-util.h, limits-util.h, time-util.h
for Argon2id benchmark auto-tuning (cpus_online, physical_memory_scale,
now/usec_t)
- src/cryptenroll/cryptenroll-tpm2.c: crypto-util.h for Argon2IdParameters
struct in load_volume_key_tpm2()
- src/shared/tpm2-util.h: crypto-util.h for Argon2IdParameters in
tpm2_make_luks2_json() API
- src/shared/tpm2-util.c: limits-util.h, tpm2-util.h for physical_memory() validation
of Argon2id memory cost and function prototypes
- src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c: crypto-util.h for
kdf_argon2id_derive()/kdf_hkdf_sha256() on the token unlock path
systemd-measure can produce multiple signed policies for different
phases. However, a policy for a TPM resource that includes these signed
policies can currently be satisfied by any policy that is signed
with the same key.
It can be desirable to bind a resource's policy to one or more
specific phases. One way to do this could be to sign policies for
different phases or phase combinations with different keys. Another
approach is to limit the scope of signed policies using a policy
reference.
Using a policy reference works because:
- The reference is included along with the approved policy digest in the
digest that is signed.
- The reference argument is included in the authorization policy for a
resource via the TPM2_PolicyAuthorize assertion.
- During execution of the TPM2_PolicyAuthorize assertion, the TPM checks
that the session's current policy digest is the approved policy digest,
computes a digest from the approved policy digest and the supplied policy
reference, and checks that the resulting digest is the one that was
verified by TPM2_VerifySignature (via the returned ticket).
This adds a new --policyref argument to systemd-measure which binds all
of the signed policies to the specified policy reference. I did consider
making this more intelligent by auto-generating policy references for each
phase, but this approach provides the most flexibility for now. By making
use of the existing --append argument, a signer can produce multiple
signed policies with the same key that are bound to any individual phases
or combinations of phases.
The policy reference is a string without the NULL terminator. It is
supplied to the TPM via the TPM2B_NONCE type, which has a maximum size
equivalent to the size of the largest digest supported by the TPM. As
the signer doesn't know the capabilities of the target TPM,
systemd-measure limits the size of the policy reference to 32 bytes, to
fit within the size of a SHA256 digest.
This also includes the corresponding changes to systemd-cryptenroll and
systemd-repart (to add an equivalent --tpm2-public-key-policyref argument
so that the policy can be bound to the desired corresponding phase, and to
ensure that the policy reference is included in the LUKS2 token metadata),
and systemd-cryptsetup (to handle the policy reference stored in the LUKS2
token metadata).
This doesn't include policy reference support for credentials yet
because it requires a change to the credential headers.
Add a --firstboot mode that interactively walks the user through enrolling a
passphrase, a recovery key, or a FIDO2 token, with one menu entry per suitable
token currently plugged in (driven by a new fido2_enumerate_devices() helper).
Pressing enter at the top-level menu leaves the volume unchanged; for each
already-enrolled credential type the wizard offers to wipe it as part of the
operation. It populates the same EnrollContext the command line and Varlink
paths use, so the actual enrollment goes through the shared enroll_now() path.
A companion --prompt-suppress= option takes a list of slot types: if a slot of
any listed type already exists, the wizard does nothing and exits successfully.
This lets it be hooked into the boot process while staying quiet once the
system has been set up.
The accompanying systemd-cryptenroll-firstboot.service runs this from the
initrd, after systemd-repart has created the encrypted volume but before we
transition to the host, suppressing itself once a password, recovery key or
FIDO2 token is enrolled. To make that work, determine_default_node() now looks
below /sysroot/ when running in the initrd, since the host file systems aren't
at their final location yet.
While the wizard is active it draws the same installer-style chrome (blue bars
at the top and bottom of the terminal) as systemd-sysinstall, using the shared
prompt_loop_yes_no() helper for its wipe confirmations.
Honours the systemd.firstboot= kernel command line option.
Fixes: #36298
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.
We'd silently skip devices which don't have the feature in the list.
This looked wrong esp. if no devices were suitable. Instead, list them
and show which ones are usable.
$ build/systemd-cryptenroll --fido2-device=list
PATH MANUFACTURER PRODUCT HMAC SECRET
/dev/hidraw7 Yubico YubiKey OTP+FIDO+CCID ✓
/dev/hidraw10 Yubico Security Key by Yubico ✗
/dev/hidraw5 Yubico Security Key by Yubico ✗
/dev/hidraw9 Yubico Yubikey 4 OTP+U2F+CCID ✗
Some ambiguity (e.g., same-named man pages in multiple volumes)
makes it impossible to fully automate this, but the following
Python snippet (run inside the man/ directory of the systemd repo)
helped to generate the sed command lines (which were subsequently
manually reviewed, run and the false positives reverted):
from pathlib import Path
import lxml
from lxml import etree as ET
man2vol: dict[str, str] = {}
man2citerefs: dict[str, list] = {}
for file in Path(".").glob("*.xml"):
tree = ET.parse(file, lxml.etree.XMLParser(recover=True))
meta = tree.find("refmeta")
if meta is not None:
title = meta.findtext("refentrytitle")
if title is not None:
vol = meta.findtext("manvolnum")
if vol is not None:
man2vol[title] = vol
citerefs = list(tree.iter("citerefentry"))
if citerefs:
man2citerefs[title] = citerefs
for man, refs in man2citerefs.items():
for ref in refs:
title = ref.findtext("refentrytitle")
if title is not None:
has = ref.findtext("manvolnum")
try:
should_have = man2vol[title]
except KeyError: # Non-systemd man page reference? Ignore.
continue
if has != should_have:
print(
f"sed -i '\\|<citerefentry><refentrytitle>{title}"
f"</refentrytitle><manvolnum>{has}</manvolnum>"
f"</citerefentry>|s|<manvolnum>{has}</manvolnum>|"
f"<manvolnum>{should_have}</manvolnum>|' {man}.xml"
)
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.
Like much English text, the systemd documentation uses "may not" in the
sense of both "will possibly not" and "is forbidden to". In many cases
this is OK because the context makes it clear, but in others I felt it
was possible to read the "is forbidden to" sense by mistake: in
particular, I tripped over "the target file may not exist" in
systemd.unit(5) before realizing the correct interpretation.
Use "might not" or "may choose not to" in these cases to make it clear
which sense we mean.
With 1df4b21abd we started to default to
enrolling into the LUKS device backing the root fs if none was specified
(and no wipe operation is used). This changes to look for /var/ instead.
On most systems /var/ is going to be on the root fs, hence this change
is with little effect.
However, on systems where / and /var/ is separate it makes more sense to
default to /var/ because that's where the persistent and variable data
is placed (i.e. where LUKS should be used) while / doesn't really have
to be variable, could as well be immutable, or ephemeral. Hence /var/
should be a safer default.
Or to say this differently: I think it makes sense to support systems
with /var/ being on / well. I also think it makes sense to support
systems with them being separate, and /var/ being variable and
persistent. But any other kind of system I find much less interesting to
support, and in that case people should just specify the device name.
Also, while we are at it, tighten the checks a bit, insist on a dm-crypt
+ LUKS superblock before continuing.
And finally, let's print a short message indicating the device we
operate on.
cryptenroll accepts only PKCS#11 URIs that match both a certificate and a private key in a token.
This patch allows users to provide a PKCS#11 URI that points to a certificate only, and makes possible to use output of some PKCS#11 tools directly.
Internally the patch changes 'type=cert' in the provided PKCS#11 URI to 'type=private' before storing in a LUKS2 header.
Fixes: #23479
Since EC keys doesn't support encryption directly, we use ECDH protocol.
We generate a pair of EC keys in the same EC group, then derive a shared secret using the generated private key and the public key in the token.
The derived shared secret is used as a volume key. The generated public key is stored in the LUKS2 JSON token header area. The generated private key is erased.
To unlock a volume, we derive the shared secret with the stored public key and a private key in the token.
Co-authored-by: MkfsSion <mkfssion@mkfssion.com>
Instead of enrolling the local TPM to a luks volume, use the public key from a
TPM to enroll it into the luks volume. This is useful when enrolling a TPM that
is not currently accessible, for example if the TPM is located on a different
system.
I was missing an example of how to use cryptenroll. We have that, but in
another page. Instead of repeating, let's just direct the user to the right
place.
Also, reformat synopsis to the "official" non-nested syntax.
This tries to add information about when each option was added. It goes
back to version 183.
The version info is included from a separate file to allow generating it,
which would allow more control on the formatting of the final output.
The previous commit extended the accepted format of --tpm2-pcrs to allow
specifying the hash algorithm (i.e. PCR bank) and hash digest value, this
updates the man page with those changes.
Entia non sunt multiplicanda praeter necessitatem. We had a list of PCRs in the
man page which was already half out-of-date. Instead, link to web page with the
"authoritative" list. Here, drop the descriptions of what shim and grub do. Instead,
just give some short descriptions and mention what systemd components do.
systemd-pcrmachine.service and systemd-pcrfs@.service are now mentioned too.
https://github.com/uapi-group/specifications/commit/d0e590b1e2648e76ece66157ceade3f45b165b14
extended the table in the specs repo.
https://github.com/uapi-group/specifications/pull/59 adds some more text there
too.
Also, rework the recommendation: hint that PCR 11 is useful, and recommend
binding to policy signatures instead of direct PCR values. This new text is
intentionally vague: doing this correctly is hard, but let's at least not imply
that just binding to PCR 7 is useful in any way.
Also, change "string alias" to "name" in discussion of PCR names.
Inspired by https://discussion.fedoraproject.org/t/future-of-encryption-in-fedora/80397/17