Secure Boot & UKIs · Part 2

Seal to the Signer, Not the State

Why binding a TPM policy to exact PCR values breaks on every kernel update, and how UKIs, PCR 11 and PolicyAuthorize let the TPM trust a signing authority instead.

#tpm#tpm2#uki#secure-boot#pcr#systemd#luks

In the previous article, I looked at using a TPM to protect disk encryption keys and at the difference between Secure Boot and measured boot.

That leads to a fairly obvious next question: what should the TPM actually trust?

The straightforward answer is to seal the disk-unlock credential against PCR values representing the boot state. That works, but it creates a practical problem almost immediately.

Suppose a machine has a LUKS2-encrypted disk and the unlock credential is sealed against PCR 7 and PCR 4. PCR 7 represents Secure Boot policy, while PCR 4 includes the EFI binaries loaded during boot.

Everything works until the kernel or boot image is updated.

The new image is legitimate. It is signed correctly. Secure Boot accepts it. But it produces a different PCR 4 value, so the TPM refuses to release the credential.

Nothing has been compromised. The measurement simply changed.

That is the limitation of binding directly to PCR values: the TPM can tell that the state is different, but it cannot tell whether that difference came from an attacker or from an authorised update.

A better approach is to stop trusting one specific state and instead trust the authority allowed to approve states.

That is where Unified Kernel Images, PCR 11 and PolicyAuthorize become useful.

Start with the threat model

PCR selection only makes sense once the threat model is clear.

For TPM-backed disk unlock, I am mainly interested in two cases.

The first is offline access. Someone removes the disk, or takes the complete machine, and tries to recover the data without booting the system as intended.

The second is boot-chain tampering. Someone with physical access changes the boot environment and tries to get the TPM to release the disk credential into a system they control.

There is also the case where an attacker already has root on a correctly booted and unlocked machine. That matters, but it is a different problem. At that point the filesystem is already mounted and the encryption key exists in kernel memory. A TPM policy can place some restrictions on re-unsealing the original credential, but it does not turn root compromise into a solved problem.

With those assumptions, the useful question becomes:

Which PCRs detect changes that matter to the attacker, without also breaking every time the operator performs routine maintenance?

That distinction is important because every PCR added to the policy becomes something the machine is effectively promising not to change.

Not every PCR belongs in the unlock policy

It is tempting to bind the TPM to as much platform state as possible.

In practice, that can make a system extremely fragile.

PCRs 0 and 2 contain firmware and option-ROM measurements. A firmware update can change them.

PCRs 1 and 3 contain firmware configuration. Changing a UEFI setting may change them.

PCR 5 commonly contains boot-manager configuration and partition-table measurements. Even a legitimate partition change can therefore affect it.

These measurements are not useless. They can provide useful information about the platform.

The problem is using them as hard requirements for disk unlock.

If a PCR mostly changes because of normal administration rather than because of the attack being defended against, binding directly to it may create more operational failures than security benefit.

For this design, the interesting PCRs are instead PCR 7 and PCR 11.

Let Secure Boot validate the image

A Unified Kernel Image changes the situation considerably.

A UKI packages the kernel, initrd, kernel command line and related metadata into a single EFI executable. That executable is then signed.

With Secure Boot enabled, the firmware verifies the signature before executing the image. Modify the kernel, initrd or embedded command line and the signature no longer validates.

That means some checks that might otherwise have been implemented through PCR measurements can instead be enforced earlier by Secure Boot.

This is the distinction I covered in the previous article:

Secure Boot enforces. Measured boot records.

If Secure Boot already refuses to execute a modified image, the TPM does not necessarily need to independently bind itself to the exact hash of every component.

It does, however, need confidence that Secure Boot itself was configured as expected.

That is where PCR 7 comes in.

PCR 7 anchors Secure Boot policy

PCR 7 records Secure Boot policy information, including the Secure Boot state and key databases.

Disable Secure Boot, change the enrolled keys, modify db, or alter dbx, and PCR 7 changes.

It therefore makes a useful anchor for the TPM policy:

Expected Secure Boot policy
          │
          ▼
        PCR 7
          │
          ▼
      TPM policy

There is a subtlety worth knowing here.

PCR 7 can also reflect which trusted certificate authorised a boot component. So two binaries may both be valid under Secure Boot but still result in different PCR 7 values if different trusted keys were involved.

That can be useful when a system trusts several Secure Boot authorities but the TPM policy is intended to correspond to one particular boot path.

It also means PCR 7 is not completely static.

In particular, dbx updates — which revoke known-vulnerable boot components — can change it. A security update to the Secure Boot revocation database can therefore invalidate a TPM policy that binds to PCR 7 literally.

That is an operational issue we will come back to later.

PCR 11 gives us something more useful

PCR 7 tells us that Secure Boot is operating under the expected policy.

It does not necessarily tell us which particular authorised operating system image was booted.

For UKIs, systemd uses PCR 11 for this.

When a UKI starts, systemd-stub measures sections of the image into PCR 11 before handing control to the kernel.

The important property is that these inputs are known when the image is built.

That makes PCR 11 different from many firmware PCRs.

A firmware PCR may depend on platform-specific behaviour, boot variables, option ROMs and event ordering. The final value can be difficult to predict without actually booting the machine.

PCR 11, by contrast, can be calculated from the UKI itself.

ukify can predict the value PCR 11 will have during the relevant boot phase and sign that prediction.

So the build process can produce something stronger than simply a signed boot image.

It can effectively produce a statement saying:

This image will produce this PCR 11 state during disk unlock, and this signing key authorises that state.

That gives the TPM something much more useful to work with.

PolicyAuthorize

Without signed policies, TPM sealing usually looks something like this:

PCR 11 must equal X

The obvious problem is that a new kernel or initrd creates a new value of X.

The TPM has no concept of a legitimate update. It just sees that the number changed.

TPM 2.0 provides PolicyAuthorize specifically to introduce an external authority into the policy.

Instead of permanently binding the sealed object to one PCR value, the TPM policy can trust a public key.

The question then becomes:

Does the current PCR state match a policy
approved by this key?

With systemd-cryptenroll, that can look roughly like:

systemd-cryptenroll /dev/<device> \
    --tpm2-device=auto \
    --tpm2-public-key=<pubkey.pem> \
    --tpm2-public-key-pcrs=11 \
    --tpm2-pcrs=7

The resulting design is conceptually:

PCR 7
must match the enrolled Secure Boot state

AND

PCR 11
must match a state approved by the policy signing key

PCR 7 is still checked directly.

PCR 11 is different. Its value is allowed to change, provided the new value has been authorised by the trusted signing key.

That is the important shift.

The TPM is no longer saying:

I trust boot state X.

It is saying:

I trust boot states approved by key K.

What happens during an update

Consider the same kernel update that would have broken direct PCR binding.

A new UKI is built.

ukify calculates the PCR 11 value that image should produce during the unlock phase and signs the corresponding prediction.

The image is then deployed normally.

On the next boot, PCR 11 contains a different value because the image changed.

But that does not matter.

The TPM verifies that the current PCR 11 state corresponds to a policy signed by the key enrolled during setup.

If it does, that part of the policy succeeds.

No TPM re-enrolment is required.

The relationship now looks like this:

UKI v1 ──► PCR 11 = A ──► signed by key K
                                   │
UKI v2 ──► PCR 11 = B ──► signed by key K
                                   │
UKI v3 ──► PCR 11 = C ──► signed by key K
                                   │
                                   ▼
                              TPM trusts K

That is much easier to operate than enrolling every individual image state.

It also works naturally with A/B system images or other deployments where several different boot images may legitimately need access to the same encrypted storage.

The device only needs to know which authority it trusts.

The build system decides which images that authority approves.

Build and enrolment become separate concerns

This separation is one of the nicest properties of the design.

The TPM is enrolled once with the public policy key.

The private key remains in the build infrastructure.

Future UKIs can then be built and authorised without modifying the TPM configuration on every deployed device.

That creates a fairly clean split:

Build infrastructure

kernel + initrd
      │
      ▼
build UKI
      │
      ├── sign UKI
      │
      └── sign PCR 11 policy
               │
               ▼
            deploy


Device

Secure Boot
      │
      ▼
UKI executes
      │
      ▼
PCR 11 measured
      │
      ▼
TPM checks signed policy
      │
      ▼
disk unlock

The private signing key becomes critical infrastructure, of course, but the device itself only requires public material.

The rollback problem

There is an important consequence to this model.

A valid signature does not automatically stop being valid because a newer image exists.

If version 1 of a UKI has an authorised PCR 11 prediction, and version 2 is released later, the TPM may still accept version 1.

If version 1 subsequently turns out to contain a security vulnerability, that becomes a rollback problem.

An attacker who can boot the older signed image may still satisfy the TPM policy.

Signed PCR policies therefore solve the update problem, but they do not automatically solve the rollback problem.

Possible defences include Secure Boot revocation through dbx, signing-key rotation, or policies that include mutable TPM state such as NV indexes.

This is an important design trade-off.

Moving trust from an individual image to a signing authority makes updates much easier, but it also means the lifecycle of that authority has to be managed properly.

Secure Boot brings module signing with it

There is another practical issue that is easy to miss when designing this around the boot chain.

On many Linux distributions, enabling Secure Boot also enables kernel lockdown.

That prevents several operations that could undermine the verified boot environment, including loading unsigned kernel modules.

For distribution-provided modules this is normally handled automatically.

Out-of-tree modules are where things become more interesting.

A DKMS driver may be rebuilt every time the kernel is updated. The resulting module also needs to be signed with a key the kernel trusts.

So a realistic update pipeline often needs to handle several related signing operations:

kernel update
     │
     ├── rebuild DKMS modules
     │
     ├── sign modules
     │
     ├── build UKI
     │
     ├── sign UKI
     │
     └── sign PCR 11 prediction

If module signing is forgotten, the TPM side may work perfectly while the machine still boots without a required GPU, network or vendor driver.

What about re-unsealing after boot?

There is one more issue worth considering.

Immediately after the disk is unlocked, the PCR state that satisfied the TPM policy may still exist.

A process with sufficient TPM access could therefore potentially reconstruct the same policy session and ask the TPM to unseal the credential again.

PCR 23 is sometimes suggested as a way to prevent this.

The idea is to enrol with PCR 23 at its initial value and then extend it after the required secrets have been released.

That sounds like a one-way latch.

The problem is that PCR 23 is resettable from the locality available to ordinary system software with TPM access.

Root can therefore reset it and recreate the original state.

It can still be useful as a speed bump, but it should not be treated as a strong boundary.

PCR 11 boot phases provide a better option.

systemd can continue extending PCR 11 as boot progresses through different phases.

The TPM policy can be authorised only for the PCR 11 value that exists during the initrd phase, when disk unlock occurs.

Once the system leaves that phase, PCR 11 is extended again.

Because PCR 11 cannot simply be reset by userspace, the system cannot return to the previously authorised value.

The sequence becomes:

boot UKI
    │
    ▼
authorised PCR 11 state
    │
    ▼
disk unlock
    │
    ▼
leave initrd
    │
    ▼
PCR 11 extended
    │
    ▼
original unlock policy no longer satisfiable

That is much closer to an actual one-way transition.

What this does not solve

Signed PCR 11 policies remove one of the biggest operational problems with TPM-backed disk unlocking: normal kernel and initrd updates no longer require re-enrolling the TPM.

They do not eliminate every maintenance problem.

PCR 7 may still change because of:

The policy signing key also needs a lifecycle.

If the private key is compromised, an attacker can authorise malicious states.

If it is deliberately rotated, devices need some way of learning to trust the replacement key.

And some events are outside PCR policy entirely.

Clearing a TPM regenerates its internal seeds and makes existing sealed objects unusable. Replacing the motherboard or TPM has effectively the same result.

Those cases require recovery and re-enrolment regardless of how carefully the PCR policy was designed.

There is ongoing work in systemd around systemd-pcrlock that approaches some of these problems differently, particularly for PCRs whose legitimate future values can be predicted before an update. It is worth watching because it offers a way to manage changing platform measurements without treating every change as an unexpected failure.

The useful distinction

The part I find most useful is separating three different jobs that are often mixed together:

MechanismResponsibility
Secure BootDecide which boot images are allowed to execute
PCR 7Record whether the expected Secure Boot policy is in place
PCR 11Record the UKI and its boot phase
Signed PCR policyDecide which PCR 11 states are authorised
PolicyAuthorizeMake the TPM trust the authority approving those states

Once those responsibilities are separated, the policy becomes easier to reason about.

A TPM is very good at protecting a secret until a policy is satisfied.

What it is not good at is deciding whether an unexpected change is legitimate.

Trying to encode every acceptable machine state directly into PCR values makes that problem operationally painful.

Using signatures moves that decision somewhere more appropriate: the build and release process.

The TPM still verifies the state of the machine.

It just no longer needs that state to remain identical forever.

That is the difference between sealing to a state and sealing to whoever is authorised to approve the state.


← All writing