Measure the volume key and unlock keyslot through io.systemd.PCRExtend
instead of driving the TPM directly via tpm2-util, matching how the
verity and imds measurements already work.
Bank selection and the TPM context now live entirely in
systemd-pcrextend. As a result the tpm2-measure-bank= crypttab option
can no longer be honored per volume and is now a deprecated no-op.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
NV indexes created in the storage hierarchy can be undefined and
redefined with TPM owner auth. Because of this, NvPCRs need some way
to prevent them from being redfined in a way that allows spoof
measurements to be replayed.
The current approach requires knowledge of a secret ("anchor secret")
in order to derive the initial NvPCR measurement and to derive a
measurement to an existing PCR (9). The credential is protected by the
TPM with a PCR policy. Without access to the credential, it's not
possible to replay measurements to a newly defined NvPCR without
breaking the binding with the measurement in PCR 9. However, this
approach has a couple of issues:
- The credential is currently only protected by PCR11. As it's not
protected by the rest of the boot chain, it's possible to boot other
operating systems in order to replay the PCR11 measurements and
recover the secret. Note that as the NvPCR anchoring happens in early
boot, the credential is stored in the ESP.
- Someone with privileged access to a system can just create a new
credential containing a known secret and store this in /var/lib and
the ESP. The NvPCRs are anchored with this known secret on subsequent
boots, and therefore the measurements can no longer be trusted.
Imagine the scenario where privileged access is theoretically possible
as a result of some vulnerability. After upgrading the system to fix
this vulnerability, the system should be able to attest that it is
now in a good state. However, if an adversary were able to use their
priviliges to replace the credential, they are able to obtain
persistence and the NvPCR measurements are no longer trustworthy.
This PR changes things to take a different approach. Instead of
requiring knowledge of a secret, the NvPCRs are now created in a way
that requires a policy to be satisfied for writing. The write policy has
2 branches:
- TPM2_PolicyNvWritten(true), which can be satisified without any
further authorization if the NvPCR has already been extended.
- TPM2_PolicyAuthorize(pcrPubKey, SHA256("nvpcr-init")) which can be
satisfied with a signed PCR policy, and must be used to perform the
initial extend to a NvPCR.
The intention here is that the signed PCR policy that can be used to
authorize the initial extend to the NvPCR can only be satisfied during
early boot. During later boot phases, this signed PCR policy must not be
valid. This means that if a NvPCR is undefined and redefined, it won't
be possible to satisfy its write policy in order to able to perform the
initial extend.
In order to anchor the NvPCRs and prevent them from being undefined and
then redefined with a different policy that does allow them to be
extended, the names of the NvPCRs are measured to PCR9. Verifiers must
check that the names of attested NvPCRs match the measurements in PCR9.
This uses the PCR signing key from the currently booted UKI to create
the NvPCRs. If this changes between boots, then tpm2-setup automatically
recreates new NvPCRs with an updated write policy to reflect this. I've
tried to be careful to not undefine arbitrary NV indexes in this case,
so it checks that the existing NV index looks like a NvPCR (ie, it has
the expected attributes) before undefining it.
I did originally try to preserve the old behaviour for existing systems,
but it makes things a lot more complicated. As the new implementation
already creates new NvPCRs when the PCR signing key changes, I ended up
just automatically upgrading the old NvPCRs as well. Again, I check here
that any existing NV index looks like an old style NvPCR (ie, it has the
expected attributes) before undefining it.
I did notice that the initial NvPCR measurement isn't going into the
log. I don't know if that was an intentional choice, but I've preserved
that behaviour in this PR.
This also adds a new option to ukify (--sign-initrd-pcrs) which creates
signed policies (one per PCR bank) that can only be satisfied from the
initrd. These policies are used for initializing the NvPCRs, but can also
be used for protecting TPM2 keyslots enrolled with systemd-cryptenroll
(by using the --tpm2-public-key-policyref=initrd option).
There is one outstanding issue. The NvPCR definitions support different
algorithms, but the use of PolicyAuthorize means that they can only support
SHA-256 for now. This is because the signed policy algorithm must match
the name algorithm, and some additional work is required to support
signed PCR policies for algorithms other than SHA256. I've left a note in
tpm2_nvpcr_initialize that details what's required, and I'll take a look
at that in a subsequent PR.
--component-all is documented to include the default component-less
installation. Do not drop it merely because the context operates on a
root/image, or because all its transfers are currently disabled by
features.
This lets --component-all --feature-all enable-feature write the
default component feature drop-ins instead of succeeding with no
components selected.
TEST-72-SYSUPDATE covers both all transfers disabled by features and
the same default component feature operation under --root=.
Repro: create a default feata.feature plus a transfer gated by feata,
then run:
build/systemd-sysupdate --root="$root" --component-all --feature-all enable-feature
Before: no drop-in was written.
Follow-up for: 4481661a75
When recording installdb entries under --root=, keep the leading slash
after stripping the root. Compare current transfer target paths in the
same root-relative form during cleanup.
This prevents cleanup from treating still-owned resources below --root=
as orphaned.
TEST-72-SYSUPDATE covers --root= cleanup keeping a still-owned file and
its matching installdb entry.
Repro: create a rooted transfer for /target/foo-@v.bin, add a
matching installdb entry for /target/./foo-@v.bin, then run:
build/systemd-sysupdate --root="$root" --verify=no cleanup
Before: foo-1.bin and the installdb entry were removed.
Follow-up for: d82e256bb9
Definitions enumerated under --root= are already rooted. Passing those
paths to the config parsers with the same root prefixes the root again,
so feature and transfer files are parsed from the wrong path.
Repro: create root/etc/sysupdate.d/rootfeat.feature and
01-root.transfer, then run:
build/systemd-sysupdate --root="$root" --verify=no --offline features rootfeat
Before: parsing failed at line 1 with a bogus Source Type= error.
Fixes#42783.
Follow-up for: e1384cfb09
systemd-escape currently only processes strings passed as
command line arguments. This is awkward for callers that already
have a generated list of strings, because they need to loop around
the tool or use xargs and carefully preserve whitespace and other
special characters.
Add --stdin to read one string per line from standard input and
write one escaped result per output line. Keep command line strings
mutually exclusive with --stdin so the input source remains
unambiguous.
Use an explicit option instead of treating '-' specially, since '-'
is itself a valid string to escape. The existing escape, unescape,
mangle, path, suffix, and template rules are reused unchanged.
The io.systemd.Unit context exposes CollectMode, but StartTransient
rejects it as unsupported. This prevents Varlink clients from selecting
whether failed transient units should be garbage-collected.
Accept CollectMode when creating transient units and persist the setting
in the runtime unit configuration. Extend the integration test to verify
the value in both the Varlink response and the resulting unit.
X entries inherit the cleanup age from the closest parent directory rule.
Inherit the age-by fields too, so parent rules such as m:1d keep their
full cleanup policy.
Reproducer:
mkdir -p /tmp/tmpfiles-ageby/parent/{child,other}
printf old >/tmp/tmpfiles-ageby/parent/child/file
printf old >/tmp/tmpfiles-ageby/parent/other/file
touch -d '3 days ago' /tmp/tmpfiles-ageby/parent/{child,other}/file
systemd-tmpfiles --clean - <<'EOT'
d /tmp/tmpfiles-ageby/parent - - - m:1d
X /tmp/tmpfiles-ageby/parent/child - - - -
EOT
Before:
child/file remained because X used the default age-by set.
other/file was removed by the parent m: rule.
Follow-up for 7f7a50dd15.
Resolve /etc/machine-id before checking the mount state and before
writing back, so symlinked targets are committed correctly.
Fixes: #43104
Follow-up for: efabf4e044
PrivateIPC= sets up a private mqueue mount for the service. When
CONFIG_POSIX_MQUEUE is disabled, mounting mqueue fails with ENODEV and
prevents the service from starting.
Treat ENODEV as an unavailable optional kernel feature while preserving
the private IPC namespace. Keep all other mount errors fatal.
Add coverage verifying that PrivateIPC= still creates a separate IPC
namespace when POSIX message queues are unavailable.
Fixes#42582
A calendar timer that is already waiting keeps the base timestamp it was
last armed from. When the wall clock is set backwards, that base can end
up in the future relative to the new realtime, and passing it to
calendar_spec_next_usec() schedules the next elapse relative to the old
future time instead of recalculating from now. systemctl list-timers
then keeps showing the stale pre-adjustment elapse (e.g. "3 years left"
after the clock moved back two years) and the timer never catches up.
Clamp the selected calendar base to the current realtime before asking
calendar_spec_next_usec() for the next occurrence. The clamp only kicks
in when the base is genuinely in the future, so Persistent=yes timers
whose last trigger is still in the past keep their catch-up behaviour and
we don't reintroduce the missed-run regression seen after suspend.
Fixes#6036.
C lines copy symlinks as symlinks. The post-copy type check should
inspect the source symlink itself, not follow its target.
Reproducer:
src=/tmp/tmpfiles-src-link.$$
dst=/tmp/tmpfiles-dst-link.$$
conf=/tmp/tmpfiles-conf.$$
ln -s missing-target "$src"
printf 'C %s - - - - %s\n' "$dst" "$src" >"$conf"
systemd-tmpfiles --create "$conf"
Before:
Failed to stat($src): No such file or directory
status=73
$dst -> missing-target already existed
Follow-up for 8f6fb95cd0.
Opening the destination only proves that it exists. If copy_tree_at()
failed for any reason other than an existing destination, report that
copy error.
Reproducer:
name=tmpfiles-copy-bug.$$
src=/tmp/$name.src; dst=/tmp/$name.dst; conf=/tmp/$name.conf
mkdir "$src" "$dst"
printf payload >"$src/file"
chmod 500 "$dst"
printf 'C %s - - - - %s\n' "$dst" "$src" >"$conf"
systemd-tmpfiles --create "$conf"
Before:
exit=0
copied=no
Follow-up for 8f6fb95cd0.
Relative symlink targets are resolved from the directory containing the
link. Make the L? existence check use the same rule.
Reproducer:
tmp=$(mktemp -d /tmp/tmpfiles-link.XXXXXX)
conf=$(mktemp /tmp/tmpfiles-conf.XXXXXX)
printf data >"$tmp/target"
printf 'L? %s/link - - - - target\n' "$tmp" >"$conf"
systemd-tmpfiles --create --dry-run "$conf"
Before:
The target was checked root-relative, not beside the link.
Follow-up for b5dc805583.
%t and the other directory specifiers are parsed as normal paths.
They are prefixed by --root= later, so the specifier itself must stay
rootless.
Reproducer:
tmp=$(mktemp -d /tmp/tmpfiles-root.XXXXXX)
conf=$(mktemp /tmp/tmpfiles-conf.XXXXXX)
printf 'd %%t/tmpfiles-repro 0755 - - -\n' >"$conf"
systemd-tmpfiles --root="$tmp" --create --dry-run "$conf"
Before:
Would create directory $tmp/$tmp/run/tmpfiles-repro
Follow-up for de61a04b18.
Track which machine the cached address list belongs to and refresh it
when ResolveRecord() advances to another machine. This keeps A/AAAA
reuse for the same machine while avoiding mixed name/address records.
Reproducer: register two machines with private addresses 10.88.1.1
and 10.88.2.1, then call io.systemd.Resolve.Hook.ResolveRecord
with both A questions in one request.
Before: the answer for the second machine used its own name but
reused the first machine's address, e.g. resolve-bug-m2 returned
10.88.1.1 instead of 10.88.2.1.
Follow-up: 3cd929f837
Workers may return a newer embedded identity while processing
an owner update. Accept self-modifiable changes, but require
privileged changes to carry a trusted signature.
Follow-up for 70a5db5822
The modern hwdb.bin format contains the filenames of the input data that
makes up the database. This is useful but in offline builds where
--root is used, the filenames are the full build paths including the
specified root. This introduces build paths and thus information
leakage and non-reproducible data.
Solve this by stripping the root prefix off the original path when
passing to import_file.
Add TEST-17-UDEV.hwdb.sh to verify that hwdb.bin files contain the
path inside the root, but not the path of the root.
Reject credentials whose not-after timestamp is equal to the timestamp
at each encryption entry point.
Reproducer: systemd-creds --timestamp=TS --not-after=TS encrypt in out
Before: encryption succeeded, but decrypt refused the credential as not
in order.
Follow-up: 21bc0b6fa1
Use the image metadata size when building JSON output, matching the
value already shown by the text output.
Reproducer: systemd-dissect --json=short image.raw
Before: text output showed Size, but JSON omitted the top-level size
field.
Follow-up: be5bee2a13
Return failure for --json check-new when no candidate is available.
Let sysupdated accept that exact JSON no-update result from workers.
Reproducer: systemd-sysupdate --verify=no --json=short check-new
Before: the command printed {"available":null} and exited successfully.
Follow-up: 42c0b689a8
Skip generated fstab and crypttab output when --dry-run=yes is used,
after confirming there is eligible content to generate.
Reproducer: systemd-repart --dry-run=yes --generate-fstab=/tmp/root/etc/fstab -
Before: the command exited successfully and still wrote the requested
fstab file.
Follow-up: 1a0541d44c
Validate passwd.shell.root after reading the credential so it uses
the same target-root shell checks as --root-shell= and prompted input.
Reproducer: CREDENTIALS_DIRECTORY=... systemd-firstboot --root=...
with passwd.shell.root=/bin/nonexistentshell
Before: /etc/passwd was written with the nonexistent root shell and
the command exited successfully.
Follow-up: 416f7b3a11
Validate passwd.shell.<user> credentials with the same login-shell
rules used for sysusers.d shell fields before writing passwd entries.
Reproducer: CREDENTIALS_DIRECTORY=... systemd-sysusers --root=...
with --inline 'u creduser 999 "Cred User" / -'
Before: relative-shell was written as the login shell and the command
exited successfully.
Follow-up: 99e9f896fb
Reject --app-specific= for the new verb. The option only makes sense
for verbs that derive IDs from an existing base ID.
Reproducer: systemd-id128 new --app-specific=4f68bce3e8cd4db196e7fbcaf984b709
Before: the command printed a random ID and exited successfully.
Follow-up: 0d1d512f7f
Since 5c6bb28999 image_discover() uses
chaseat() to chase the path to the image. This however breaks the raw
image check in image_make() as "path" is now not the symlink itself, but
the symlink target.
So with:
$ ls -l /var/lib/machines
total 872104
lrwxrwxrwx. 1 root root 12 Jul 14 06:10 foo.raw -> foo.squashfs
-rw-r--r--. 1 root root 5368709120 Jul 13 02:07 foo.squashfs
The endswith(path, ".raw") check is now performed on "/.../foo.squashfs"
instead of "/.../foo.raw", making it false and thus ignoring the image
symlink completely.
Address this by also checking if the pretty name is set - if so, and the
path is a regular file, the caller must've been image_find() or
image_discover() which already checked if the original path ends in .raw
and is a regular file.
Follow-up for 5c6bb28999.
Resolves: #41656
Pass relax_extension_release_check through the directory extraction
path instead of hardcoding false. Directory extensions now honor the
same --force relaxation as dissected images.
Reproducer:
cp -a /tmp/app0 /tmp/app10
sudo portablectl attach --force --runtime \
--extension /tmp/app10 /tmp/rootdir app0
Before:
directory image extraction always used strict extension-release name
checks. --force relaxed other extension paths but still rejected a
renamed directory extension with matching metadata.
Follow-up: 06768b90a3
unmerge_hierarchy() joined the persisted work_dir value directly with
--root=. An empty value therefore resolved to the root itself and was passed
to rm_rf().
Require the decoded metadata to name a non-empty, safe, normalized relative
path before constructing the removal target. Add coverage using a disposable
root with deliberately emptied metadata.
Follow-up for 9cfad502f4
Reproducer:
journalctl -F _SYSTEMD_UNIT -u test.service --no-pager
journalctl -u test.service --no-pager -n 1
Before, the field listing ignored the unit filter and listed unrelated
units, while the normal journal query applied the filter.
sd_journal_query_unique() cannot represent journalctl filters such as
unit, boot, time, cursor, or grep filters. Walking the journal line by
line would make field listing linear in the number of entries and
duplicate unique-value handling.
Keep the scope-option predicate next to add_filters(), and reject field
listings combined with options that actually limit the journal.
Display-only options such as --pager-end are left alone.
Normalize each remove argument with determine_image() before building
the RemoveImage request. Relative paths are converted before reaching
portabled.
Reproducer:
cd /tmp
portablectl remove ./portable-repro
Before:
./portable-repro was sent unchanged to portabled and rejected as an
invalid image name/path. Removing an image by a relative path failed
even though the path referred to a valid local image.
Follow-up: 61d0578b07
Open unit files with chase_and_openat() rooted at the image instead of
plain openat(). Symlinks are now resolved under the image root and must
end at a regular file.
Reproducer:
ln -s /tmp/portable-host-unit \
IMAGE/usr/lib/systemd/system/PREFIX.service
portablectl inspect --cat IMAGE PREFIX.service
Before:
inspect followed the absolute symlink on the host and printed host
file contents as image unit metadata. Image inspection could leak or
trust files outside the portable image.
Follow-up: 61d0578b07
Parse the WithExtensions metadata block whenever --force selects that
bus method. This keeps the following unit-file array aligned with the
reply layout.
Reproducer:
portablectl inspect --force /usr/share/minimal_0.raw \
minimal-app0.service
Before:
inspect printed the image metadata but dropped the matching unit
list. Scripts could not see which unit files were selected when
--force was used with the WithExtensions bus reply.
Follow-up: bdfa3f3a5c
systemd-repart currently forces newly created image files into NOCOW mode.
That prevents files from being reflinked into the image, making image builds
slower and increasing their disk usage on filesystems that support cloning.
Add a tristate --cow= option. By default, leave the filesystem or parent
directory COW policy unchanged. With --cow=yes, explicitly enable COW; with
--cow=no, retain the previous behavior of forcing NOCOW. Add XO_COW as the
counterpart to XO_NOCOW so xopenat_full() applies either policy while retaining
its normal creation-error cleanup.
Document the new option and extend TEST-58-REPART to verify inherited COW and
NOCOW policies as well as explicit COW and NOCOW overrides. Compare the unset
behavior with the filesystem default so the test also works on nodatacow
mounts, and skip it when the inode attribute is unsupported.
Signed-off-by: Daan De Meyer <daan@amutable.com>
## Problem
A `BrowseServices` subscription with `"ifindex":0` (browse all
interfaces) receives a continuous flap of `added`/`removed` events for a
service that is still present — within a second, and with no goodbye
packet involved.
## Root cause
For `ifindex==0`, `mdns_browser_revisit_cache()` looked up each mDNS
scope's cache separately and called `mdns_manage_services_answer()`
**once per scope**. That function derives `removed` events by diffing
the browser's *global* discovered-service list (all interfaces, filtered
only by owner family) against the single answer it's handed. So with ≥2
mDNS-relevant interfaces of the same family, a service present on
interface A isn't in interface B's answer and is spuriously removed
while B is reconciled, then re-added on the next revisit tick.
## Fix
Accumulate the pruned cache answers from every matching mDNS scope into
one combined `DnsAnswer` and reconcile **once**, so removals diff the
global list against the union across interfaces. Items are merged with
`dns_answer_add_full()` (not `dns_answer_extend()`, which defaults each
item's `until` to `USEC_INFINITY` and would skew the RFC 6762 §5.2
TTL-maintenance schedule). The single-interface (`ifindex>0`) path is
unchanged.
## Test
`TEST-89-RESOLVED-MDNS.sh` gains `testcase_browse_ifindex_zero_no_flap`:
it adds a service-less dummy mDNS link to guarantee ≥2 same-family
scopes (the flap precondition), browses `ifindex=0`, waits for
discovery, then asserts **zero** `removed` events while every publisher
stays up. The subscription uses `varlinkctl --timeout=infinity`, since
it sits idle after discovery and the default 45s idle timeout would
sever it (and the assertion) mid-observation.
## Testing status
Builds clean; `shellcheck -x` clean. First CI round: `TEST-89`
(including this testcase) passed on all mkosi platforms; the failing
jobs all traced to unrelated flakes/infra.
Following on from adding the basic varlink scaffolding to sysupdate,
let’s varlinkify a couple of the D-Bus methods. Because varlink doesn’t
have a concept of object paths, the D-Bus path structure which allows a
target to be selected has been squashed down to a target argument for
each relevant method.
Varlinkify the way to list targets, and also the way to list features
because that was simple to do at the same time.
More methods need varlinkifying in the future, but let’s do it in small
and manageable chunks.