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.
NVIndexes in TPMs can operate in two modes:
1. Backed by TPM RAM. In this case they are only written to NVRAM on an
orderly TPM shutdown when the system goes down. (TPMA_NV_ORDERLY flag
is on)
2. Backed by TPM NVRAM. In this case the nvindex value is written to NVRAM
on every write, and things are not delayed until orderly shutdown.
Normally mode 1 sounds like the obvious choice for NvPCRs, which reset
to zero anyway at boot. However, things are more complicated since
real-life TPMs tend to have a lot less RAM than NVRAM (both are
constrained but RAM even more than NVRAM). Hence there's value in using
NVRAM right-away. However, writing to NVRAM all the time means wearing
it out (since NVRAM is more vulnerable to that).
So far we unconditionally went for mode 1, but ran into space
constraints of RAM due to that.
Let's improve things a bit, and use orderly mode for NvPCRs we expect to
write many times, and non-orderly mode for those we expect to write only
a small, fixed number of times at boot, and not anymore during runtime.
Right now, this is only the "hardware" NvPCR, which measures hw identity
at boot.
Hopefully, this stretches available resources a bit further.
This also makes sure if the flag was set differently on allocation as
we'd set now, we accept it and won't complain, to make upgrades safe.
Suggested by Andreas Fuchs.
The measurement that systemd-pcrnvdone corresponds to
`src/pcrlock/pcrlock.d/770-nvpcr-separator.pcrlock`, and 770 is supposed to
happen in the initrd (which ends at 800).
We do this in a separate service (rather than inside of
systemd-tpm2-setup), since we want failures of this measurement to
result in an instant reboot, like for most our measurements.
Failures to initialize nvpcrs, or allocate an SRK are somewhat OK (and
more likely), as long as this separator communicates clearly where they
have to have taken place, if they worked.
This locks down NvPCR initilization a bit more: we'll measure each
initialization of an NvPCR into PCR 9, thus chaining the NvPCRs to the
PCR set. After all NvPCRs are initialized we measure a barrier into PCR
9 as well.
This ensures that later additions of NvPCRs are clearly recognizable and
distuingishable from those done at boot.
The section headers used quotes as if the strings were some constants. But
AFAICT, those are just normal plain-text titles. Also lowercase them, because
this is almost like a table and it's easier to read without capitalization.
We used both, in fact "Devicetree" was more common. But we have a general rule
that we capitalize all words in names and also we have a DeviceTree=
configuration setting, which we cannot change. If we use two different
spelllings, this will make it harder for people to use the correct one in
config files. So use the "DeviceTree" spelling everywhere.
Teaches systemd-stub how to load additional initrds from addon files.
This is very similar to the support for .ucode sections in addon files,
but with different ordering. Initrds from addons have a chance to
overwrite files from the base initrd in the UKI.
This reverts commit 5e8ff010a1.
This broke all the URLs, we can't have that. (And actually, we probably don't
_want_ to make the change either. It's nicer to have all the pages in one
directory, so one doesn't have to figure out to which collection the page
belongs.)