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.
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.
The JSON output keyed every section by name, so a UKI with repeated
sections only showed the last one: in a multi-profile UKI all but one
.profile/.cmdline/.pcrsig were dropped, as were extra .dtbauto/.efifw.
Report the shared base sections by name at the top level (so .cmdline
etc. stay where they were), each profile as its own by-name object under
a new "_profiles" array, and the alternative-set sections (.dtbauto,
.efifw) as arrays.
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
If the option is not manually specified, and the hwids directory exists
for the target EFI architecture, default to it. Allow passing '' to skip.
Fixes https://github.com/systemd/systemd/issues/40542
The primary motivation for this is to allow users of ukify to build
UKI-like objects, without having them later be detected as a UKI by
tools like kernel-install and bootctl.
The common code used by these tools to determine if a PE binary is a UKI
checks that both .osrel and .linux sections are present. Hence, adding
a mechansim to skip .osrel provides a way to avoid being labeled a UKI.
Over the time, the functionality in ukify has grown. This should all be briefly
mentioned in the first section so the user does't have to read the whole page
to figure out what types of functionality are implemnted.
Also add an example of direct kernel boot. It's a nifty technology (and frankly
underutilized, considering how cool it is is).
Public keys and certificates are not the same, as the latter embeds more
information that the former, and other tools like sd-measure have distinct
parameters for each of them.
Add a new --pcr-certificate= parameter to ukify, and use it to pass certs
down to sd-measure, as an alternative to --pcr-public-key=. Do not allow
specifying both.
Add a build parameter to take an existing UKI and attach a .pcrsig section
to it. This allows one to create a UKI with a .pcrpkey section with
--policy-digest to get the json output from sd-measure, sign the digest
offline, and attach the .pcrsig section with the signature later.
Let's allow configuring which UKI profiles we generate signed PCR
measurements for since there are various types of profiles for
which we do not want to generate signed PCR measurements so that they
can not unlock the encrypted rootfs.
Fixes#36173.
Uses the newly added policy-digest verb of systemd-measure, for the same
purpose: build a UKI and get digests for the .pcrsig section out, so
that they can be offline signed and reattached
In the troff output, this doesn't seem to make any difference. But in the
html output, the whitespace is sometimes preserved, creating an additional
gap before the following content. Drop it everywhere to avoid this.
The API introduced in https://github.com/systemd/systemd/pull/34295
is less than ideal:
- It doesn't consider signing at all (ukify can't sign separately yet)
- Measurement is completely broken (all profile sections are marked to
not be measured)
- It focuses on a very niche use case of extending existing UKIs and makes
the more common use case of building a UKI with several profiles included
much harder than needed.
Let's instead rework the API to focus on the primary use case of building
a UKI with multiple profiles added to it immediately. We require the profiles
to be built upfront as separate PE binaries with UKI. There's no need to sign
or measure these, they're solely vehicles for profile sections. This saves us
from having to complicate the command line and config parsing to support defining
multiple profiles.
To add the profiles when building a UKI, we introduce the new --add-profile
switch which takes a path to a PE binary describing a profile. The required
sections are read from each PE binary, measured and added as a profile.
The integration test is disabled until the new API is merged and exposed in
mkosi so that building a UKI with profiles can be left to mkosi and the integration
test will only test the switching between profiles and not the building of UKIs
with profiles.
This options is pretty simple, it allows specifying an UKI whose
sections to import first, and place at the beginning of the new UKI.
This is useful for generating multi-profile UKIs piecemeal: generate the
base UKI first, then append a profile, and another one and another one.
The sections imported this way are not included in any PCR signature,
the assumption is that that already happened before in the imported UKI.
Previously, the order was quite chaotic, even sometimes interleaved with
entirely unrelated switches. Let's clean this up and use the same order
as in the spec.
This doesn't change anything real, but I think it's a worthy clean-up in
particular as this order is documented as the PCR measurement order of
these sections, hence there's actually a bit of relevance to always
communicate the same order everywhere.
This was lost on refactor, and only addons had a default uki
line in the .sbat. Add it back, and differentiate between the
default for UKIs vs the default for addons, so that they can
be revoked separately. These are only defaults and users are
encouraged to provide their own.
Follow-up for a8b645dec8
As I noticed a lot of missing information when trying to implement checking
for missing info. I reimplemented the version information script to be more
robust, and here is the result.
Follow up to ec07c3c80b
If the user does not specify a config file to use, ukify will try looking for one at {/run,/etc,/usr/local/lib,/usr/lib}/systemd/ukify.conf in order and then use the first one found. Also made sure the --config input is a pathlib.Path by specifying its type in its CONFIG_ITEMS entry.
Big cheers to Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> for helping!
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.