systemd-measure calculates the TPM PCR values a Unified Kernel Image should produce, before you find out the hard way at boot. This guide covers inspecting PCR 11, calculating measurements, and signing the result for later use. The examples target systemd 255.4-1ubuntu8.17, installed here.
Allow about 20 minutes for a calculation-only workflow. You need a UKI's component files, or a currently booted system with TPM2 access. Signing additionally needs an available TPM2 device, an RSA private key, and careful handling of the resulting policy. Do not enrol a key into a LUKS volume until you have tested recovery with another decryption method.
Check the binary and package before relying on examples. These are ordinary read-only commands:
$ /usr/lib/systemd/systemd-measure --version
systemd 255 (255.4-1ubuntu8.17)
$ dpkg-query -W -f='${Package} ${Version}\n' systemd
systemd 255.4-1ubuntu8.17
The documented interface is experimental and may change. The command has three verbs: status, calculate, and sign. status is also the default when no verb is supplied.
Checkpoint: make sure you are running the installed path above, not a similarly named helper from another systemd installation.
Start with the local state. This reads TPM measurements and does not alter them:
$ /usr/lib/systemd/systemd-measure status --no-pager
11:sha256=...
12:sha256=...
13:sha256=...
Your hashes will differ. The default output covers PCR 11, 12, and 13 using the available default banks. To request one bank explicitly, repeat --bank when needed:
$ /usr/lib/systemd/systemd-measure status --bank=sha256 --json=pretty --no-pager
If this fails, check that the machine has a suitable TPM2 device. Enumerate devices without changing anything:
$ /usr/lib/systemd/systemd-measure --tpm2-device=list status --no-pager
A TPM2 device is required for status, and also for sign, even though a signature is portable and is not bound to the TPM's current state.
For a reproducible calculation, pass the ELF kernel and whichever UKI sections are present. Only --linux is mandatory; the other paths are optional and each option may be used once:
$ /usr/lib/systemd/systemd-measure calculate \
--linux=/path/to/vmlinux \
--osrel=/path/to/os-release.txt \
--cmdline=/path/to/cmdline.txt \
--initrd=/path/to/initrd.cpio \
--splash=/path/to/splash.bmp \
--dtb=/path/to/devicetree.dtb \
--bank=sha256 \
--json=pretty
Expected output is a JSON representation of PCR 11 measurements for the selected bank. With the default non-JSON format, the shape is similar to 11:sha256=<hex-digest>. Do not copy the digest from this example into a policy: it depends on the exact bytes of every supplied component and on the selected boot phases.
--linux is the ELF kernel a UKI wraps, not necessarily the final PE/EFI file.--osrel, --cmdline, --initrd, --splash, --dtb, --uname, --sbat, and --pcrpkey each correspond to a UKI section.For the currently booted measurement instead, use --current in place of the component paths:
$ /usr/lib/systemd/systemd-measure calculate --current --bank=sha256
11:sha256=<current-pcr-11-digest>
This is a snapshot of current PCR 11 state, not a reconstruction of a future kernel. Treat it as an expectation only when the boot inputs and phase sequence are controlled.
By default, calculations cover four useful phases: entering the initrd, leaving it, completing sysinit, and reaching system readiness. To request a particular phase, pass --phase. The value is a colon-separated path:
$ /usr/lib/systemd/systemd-measure calculate \
--linux=/path/to/vmlinux \
--initrd=/path/to/initrd.cpio \
--phase=enter-initrd \
--phase=enter-initrd:leave-initrd \
--bank=sha256
Repeat the option for multiple outputs. A signature restricted to enter-initrd can be useful for a secret needed only while the initrd is running, but it will not be interchangeable with a signature covering later runtime. Verify the phase policy with the service that consumes it before deploying it.
Warning: signing is security-sensitive. Keep the private key readable only by the intended operator, and redirect output to a new file rather than overwriting an existing policy.
$ umask 077
$ /usr/lib/systemd/systemd-measure sign \
--linux=/path/to/vmlinux \
--osrel=/path/to/os-release.txt \
--cmdline=/path/to/cmdline.txt \
--initrd=/path/to/initrd.cpio \
--pcrpkey=/path/to/tpm2-pcr-public.pem \
--bank=sha256 \
--private-key=/path/to/tpm2-pcr-private.pem \
--public-key=/path/to/tpm2-pcr-public.pem \
--json=pretty \
> /path/to/tpm2-pcr-signature.json
$ jq empty /path/to/tpm2-pcr-signature.json
The output is a JSON signature object. --private-key and --public-key are the RSA pair used to sign and verify the result. --pcrpkey is different: it selects the public key embedded in the UKI's .pcrpkey section. They are often the same PEM public key, but they serve different roles. If --public-key is omitted, systemd-measure derives it from the private key.
Unlike calculation, signing requires a suitable TPM2 device. Check it with --tpm2-device=list first, or select one explicitly with --tpm2-device=/dev/tpmrm0. Do not use --tpm2-device=auto unless exactly one suitable device is expected.
To add a second key or phase policy, use --append. The existing JSON file is read but not modified:
$ /usr/lib/systemd/systemd-measure sign \
--append=/path/to/tpm2-pcr-signature.json \
--linux=/path/to/vmlinux \
--initrd=/path/to/initrd.cpio \
--bank=sha256 \
--private-key=/path/to/initrd-private.pem \
--public-key=/path/to/initrd-public.pem \
--phase=enter-initrd \
--json=pretty \
> /path/to/tpm2-pcr-signature-combined.json
$ jq empty /path/to/tpm2-pcr-signature-combined.json
Recovery: added signatures are deduplicated. Keep the original until the combined object has been checked and tested. Recovery is straightforward: remove the newly created combined file and continue using the unchanged original. This does not revoke a key already enrolled elsewhere.
--pcrpkey with --public-key. One describes UKI data and the other verifies the generated signature.systemd-cryptenroll changes disk metadata and needs a tested recovery path.