SYSTEMD-MEASURE(1)

okay so bestie, you opened systemd-measure(1) and I'm obsessed ๐Ÿ’…โœจ


โœจ NAME โœจ

systemd-measure โ€” pre-calculate and sign expected TPM2 PCR 11 values for booted unified kernel images ๐Ÿ’–

๐Ÿฆ‹ SYNOPSIS ๐Ÿฆ‹

/usr/lib/systemd/systemd-measure [OPTIONS...]

๐Ÿ’€ EXPERIMENTAL ๐Ÿ’€

bestie, this component is experimental and subject to change without advance notice. It will be stabilized and given a backward-compatibility guarantee in a future release, but may also possibly be removed. no cap. ๐Ÿ˜ญ

๐ŸŒธ DESCRIPTION ๐ŸŒธ

okay so systemd-measure is a tool that may be used to pre-calculate and sign the expected TPM2 PCR 11 values that should be seen when a Linux UAPI.5 Unified Kernel Image (UKI)[1] based on systemd-stub(7) is booted up. ๐Ÿฆ‹

It accepts paths to the ELF kernel image file, initrd image file, devicetree file, kernel command line file, os-release(5) file, boot splash file, and TPM2 PCR PEM public key file that make up the unified kernel image, and determines the PCR values expected to be in place after booting the image. โœจ

Calculation starts with a zero-initialized PCR 11, and is executed in a fashion compatible with what systemd-stub does at boot. ๐Ÿ”ฅ

The result may optionally be signed cryptographically, to allow TPM2 policies that can only be unlocked if a certain set of kernels is booted, for which such a PCR signature can be provided. ๐Ÿ’…

It usually does not make sense to call this tool directly when constructing a UKI. Instead, ukify(1) should be used; it will invoke systemd-measure and take care of embedding the resulting measurements into the UKI. ๐Ÿซถ

๐Ÿ’… COMMANDS ๐Ÿ’…

The following commands are understood:

โœจ status โœจ

This is the default command if none is specified. This queries the local system's TPM2 PCR 11 values and displays them. The data is written in a similar format as the calculate command below, and may be used to quickly compare expectation with reality. ๐Ÿ‘‘

Added in version 252.

โœจ calculate โœจ

Pre-calculate the expected values seen in PCR register 11 after boot-up of a unified kernel image consisting of the components specified with --linux=, --osrel=, --cmdline=, --initrd=, --ucode=, --splash=, --dtb=, --uname=, --sbat=, --pcrpkey=, --profile=, --dtbauto=, --hwids=, see below. Only --linux= is mandatory. (Alternatively, specify --current to use the current values of PCR register 11 instead.) ๐ŸŒˆ

Added in version 252.

โœจ sign โœจ

As with the calculate command, pre-calculate the expected value seen in TPM2 PCR register 11 after boot-up of a unified kernel image. Then, cryptographically sign the resulting values with the private/public key pair (RSA) configured via --private-key= and --public-key=. This will write a JSON object to standard output that contains signatures for all specified PCR banks (see the --bank= option below), which may be used to unlock encrypted credentials (see systemd-creds(1)) or LUKS volumes (see systemd-cryptsetup@.service(8)). This allows binding secrets to a set of kernels for which such PCR 11 signatures can be provided. ๐Ÿ”

Note that a TPM2 device must be available for this signing to take place, even though the result is not tied to any TPM2 device or its state. literally. ๐Ÿ˜ญ

Added in version 252.

โœจ policy-digest โœจ

As with the sign command, pre-calculate the expected value seen in TPM2 PCR register 11 after boot-up of a unified kernel image. Then, compute the resulting TPM2 policy and print its digest. This will write a JSON object to standard output that contains the policy digests for all specified PCR banks (see the --bank= option below), so that it may be signed offline, for the cases where the private key is not directly accessible. Each policy contains a "pol" field with the approved PolicyPCR digest and a "tbs" field with the data to sign. The two fields have the same value when no policy reference is specified. With --policyref=, "tbs" is the concatenation of "pol" and the digest of the policy reference. If --public-key= or --certificate= are specified, the JSON object will also contain the key fingerprint. ๐Ÿ’–

Added in version 258.

โœจ OPTIONS โœจ

okay bestie, here's the whole menu of switches you can pass. they're all understood by this tool, and no, we're not skipping any of them ๐Ÿ’…


  • --linux=PATH, --osrel=PATH, --cmdline=PATH, --initrd=PATH, --ucode=PATH, --splash=PATH, --dtb=PATH, --uname=PATH, --sbat=PATH, --pcrpkey=PATH, --profile=PATH, --dtbauto=PATH, --hwids=PATH โ€” when you're using the calculate or sign verb, these tell it which files to read the unified kernel image components from. each one lines up with the equally-named section in the unified kernel PE file. the --linux= switch is the one that wants the path to the ELF kernel file that the unified PE kernel is gonna wrap. literally every other switch here except --linux= is optional. and each option can only be used at most once, so don't get cute ๐ŸŽ€

Added in version 252.

tiny exception: --profile=, --dtbauto= and --hwids= were added later, in version 257. so they're the babies of the group ๐Ÿผ

  • --current โ€” when you use this with the calculate or sign verb, it grabs the PCR 11 values that are currently in effect for the system (which should typically be the hashes of the kernel you're booted into right now). it's basically a stand-in for --linux= plus all those other switches up there. very convenient, no cap โœจ

Added in version 252.

  • --bank=DIGEST โ€” controls which PCR banks get pre-calculated PCR values (when calculate or sign is invoked) and which banks show up in the status output. you can use it more than once to name multiple banks. if you don't say anything, it defaults to the four banks "sha1", "sha256", "sha384", "sha512".
โš ๏ธ heads up bestie: some operating systems have SHA1-based signatures turned off, and if yours does, this operation will straight-up fail ๐Ÿ˜ญ. go read your OS's documentation for how to override the OS security policy around that, okay?

Added in version 252.

  • --private-key=PATH, --public-key=PATH, --certificate=PATH โ€” these take paths to a pair of PEM encoded RSA key files, and they're for the sign command.

now listen, don't mix these up: there is a difference between the --pcrpkey= and --public-key= switches. the former picks the data that goes into the .pcrpkey PE section of the unified kernel image, and the latter picks the public key of the key pair used to sign the resulting PCR 11 values. so --pcrpkey= is the key your booted system will probably use to lock disk and credential encryption to, and --public-key= is the key used for unlocking those resources again. which means, typically, you supply the same PEM key in both spots. iykyk ๐Ÿ”‘

if --public-key= isn't specified but --private-key= is, the public key gets automatically derived from the private key. free of charge, slay ๐Ÿ’…

and since v256, --certificate= can be used to specify an X.509 certificate as an alternative to --public-key= ๐ŸŒธ

Added in version 252.

  • --private-key=PATH/URI, --private-key-source=TYPE[:NAME], --certificate=PATH/URI, --certificate-source=TYPE[:NAME] โ€” as an alternative to --public-key= for the sign command, these switches let you sign with a hardware token ๐Ÿ‘‘. the private key option can take a path or a URI that gets passed to the OpenSSL engine or provider, as specified by --private-key-source= as a type:name tuple, such as engine:pkcs11. the specified OpenSSL signing engine or provider will do the signing.

the --certificate= option also takes a path or URI that gets passed to the OpenSSL provider, as specified by --certificate-source= as a "type:name" tuple, such as "provider:pkcs11". but note โ€” and this is important โ€” unlike --private-key-source=, this option only supports providers and not engines. no engines allowed, periodt ๐Ÿšซ

Added in version 256.

  • --tpm2-device=PATH โ€” controls which TPM2 device to use. it expects a device node path pointing at the TPM2 chip, like /dev/tpmrm0. or you can pass the special value "auto" and it'll automatically figure out the device node of a suitable TPM2 device (and there must be exactly one of those, no ambiguity allowed ๐Ÿ˜ค). there's also the special value "list", which enumerates all the suitable TPM2 devices currently discovered ๐Ÿ”

Added in version 252.

  • --phase=PHASE โ€” controls which boot phases to calculate expected PCR 11 values for. it takes a series of colon-separated strings that encode boot "paths" for entering a specific phase of the boot process. each string you specify gets measured by systemd-pcrphase-initrd.service, systemd-pcrphase-sysinit.service, and systemd-pcrphase.service(8) into PCR 11 during different milestones of boot. you can specify this switch multiple times to calculate PCR values for multiple boot phases all at once. if you don't use it, it defaults to:
"enter-initrd", "enter-initrd:leave-initrd", "enter-initrd:leave-initrd:sysinit", "enter-initrd:leave-initrd:sysinit:ready"

which is to say: it calculates expected PCR values for the boot phase in the initrd, during early boot, during later boot, and during system runtime โ€” but excluding the phases before the initrd or when shutting down. this setting is honoured by both calculate and sign. and when you use it with sign, it's especially useful for generating PCR signatures that can only unlock resources during specific parts of the boot process ๐Ÿ”

for more deets about PCR boot phases, check systemd-pcrphase.service(8).

Added in version 252.

  • --append=PATH โ€” when you're generating a PCR JSON signature (via the sign command), this combines it with a previously generated PCR JSON signature and outputs them as one. the path you give must point at a regular file containing a valid JSON PCR signature object. and don't worry, the file you name is not modified ๐Ÿ™ˆ. it gets read first, then the freshly generated signature gets appended to it, and the resulting object is written to standard output. use this to build a single JSON object made of signatures from a bunch of signing keys (like, one key per boot phase, for example). and it suppresses duplicates for you: if a specific signature is already in the JSON signature object, it does not get added a second time. so tidy โœจ

Added in version 253.

  • --policyref=STRING โ€” when you're generating a PCR JSON signature or policy digest (via the sign or policy-digest command), this binds the generated signatures to the specified policy reference. the policy reference is an opaque string that gets included in the data that's signed, and it's also incorporated into the authorization policy of any TPM2 resource locked against these signatures (via the TPM2_PolicyAuthorize assertion). the result? a signed policy is only accepted for a resource whose authorization policy was created with the same policy reference. signed policies made with a different policy reference get rejected โ€” even if they came from the same signing key ๐Ÿ˜ญ

you can pair this with --phase= and --append= to bind signatures for different boot phases to different policy references, so a single signing key can produce signatures that are each scoped to an individual phase or combo of phases. the matching policy reference gets selected when you lock a resource, for example via the --tpm2-public-key-policyref= option of systemd-cryptenroll(1) and systemd-repart(8) ๐Ÿ’œ

๐Ÿ’ก it's recommended to use this option to create a signed policy just for the initrd boot phase by combining it with --phase=enter-initrd, for secrets that should only be recoverable from the initrd โ€” particularly FDE keys. that's the move.

Added in version 262.

  • --json=MODE โ€” shows output formatted as JSON. it expects one of "short" (for the shortest possible output, no redundant whitespace or line breaks), "pretty" (a pretty version of the same, with indentation and line breaks), or "off" (to turn JSON output off โ€” which is the default). pick your vibe ๐ŸŒˆ
  • --no-pager โ€” do not pipe output into a pager. just raw output, straight to your face ๐Ÿ“„
  • -h, --help โ€” print a short help text and exit. the classic ๐Ÿซถ
  • --version โ€” print a short version string and exit.

๐Ÿ’ซ EXAMPLES ๐Ÿ’ซ

okay bestie this is the part where it all comes together, like the finale of your fave show but it's TPM measurements ๐Ÿซถ


โœจ Example 1. Generate a unified kernel image, and calculate the expected TPM PCR 11 value โœจ

watch this, we build a UKI and ask for the PCR 11 value at the same time:

$ ukify build \
     --linux=vmlinux \
     --initrd=initrd.cpio \
     --os-release=@os-release.txt \
     --cmdline=@cmdline.txt \
     --splash=splash.bmp \
     --devicetree=devicetree.dtb \
     --measure \
     --output=vmlinux.efi
11:sha1=d775a7b4482450ac77e03ee19bda90bd792d6ec7
11:sha256=bc6170f9ce28eb051ab465cd62be8cf63985276766cf9faf527ffefb66f45651
11:sha384=1cf67dff4757e61e5...7f49ad720be02fd07263e1f93061243aec599d1ee4b4
11:sha512=8e79acd3ddbbc8282...0c3e8ec0c714821032038f525f744960bcd082d937da

so ukify(1) is lowkey just calling systemd-measure under the hood, no cap. and those hash lines? yeah those came from systemd-measure itself. periodt ๐Ÿ’…


โœจ Example 2. Generate a private/public key pair, a unified kernel image, and a TPM PCR 11 signature for it, and embed the signature and the public key in the image โœจ

okay so now we're making keys, building the UKI, signing PCR 11, and shoving both the sig and the pubkey right into the image. iconic behavior tbh ๐Ÿ‘‘

$ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tpm2-pcr-private-key.pem
..+.+++++++++......+.........+......+.......+....+.....+.+...+..........
$ openssl rsa -pubout -in tpm2-pcr-private-key.pem -out tpm2-pcr-public-key.pem
$ systemd-measure sign \
     --linux=vmlinux \
     --osrel=os-release.txt \
     --cmdline=cmdline.txt \
     --initrd=initrd.cpio \
     --splash=splash.bmp \
     --dtb=devicetree.dtb \
     --pcrpkey=tpm2-pcr-public-key.pem \
     --bank=sha1 \
     --bank=sha256 \
     --private-key=tpm2-pcr-private-key.pem \
     --public-key=tpm2-pcr-public-key.pem >tpm2-pcr-signature.json
$ ukify build \
     --linux=vmlinux \
     --initrd=initrd.cpio \
     --os-release=@os-release.txt \
     --cmdline=@cmdline.txt \
     --splash=splash.bmp \
     --devicetree=devicetree.dtb \
     --pcr-private-key=tpm2-pcr-private-key.pem \
     --pcr-public-key=tpm2-pcr-public-key.pem \
     --pcr-banks=sha1,sha256 \
     --output=vmlinuz.efi

and then, bestie, we enroll that signed PCR policy onto a LUKS volume like the main character we are:

# systemd-cryptenroll --tpm2-device=auto \
     --tpm2-public-key=tpm2-pcr-public-key.pem \
     --tpm2-signature=tpm2-pcr-signature.json \
     /dev/sda5

and then you unlock the device with the signature:

# systemd-cryptsetup attach \
     volume5 /dev/sda5 - \
     tpm2-device=auto,tpm2-signature=/path/to/tpm2-pcr-signature.json
ngl this part is so satisfying: when that generated unified kernel image vmlinux.efi gets booted, the signature and public key files land at the exact locations systemd-cryptenroll and systemd-cryptsetup are gonna look anyway ๐Ÿ’– so likeโ€ฆ you literally don't have to specify those paths at all. they're just there. built in. slay.

โœจ Example 3. Introduce a second public key, signing the same kernel PCR measurements, but only for the initrd boot phase โœจ

this one is a glow-up of the previous example ๐Ÿ”ฅ we bring in a second signing key that only signs PCR policies locked to the initrd boot phase. the vibe? you can lock down root volumes so they only unlock before the handoff to the host system. so we've got two classes of secrets/credentials: one that can be unlocked the whole entire runtime, and one that's only usable in the initrd. two flavors, both valid, no drama ๐ŸŒˆ

$ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tpm2-pcr-private-key.pem
.+........+.+........+.......+...+...+........+....+......+..+..........
$ openssl rsa -pubout -in tpm2-pcr-private-key.pem -out tpm2-pcr-public-key.pem
$ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tpm2-pcr-initrd-private-key.pem
..+.......++........+........+......+........+....+.....+.+..+..........
$ openssl rsa -pubout -in tpm2-pcr-initrd-private-key.pem -out tpm2-pcr-initrd-public-key.pem
$ ukify build \
     --linux=vmlinux-1.2.3 \
     --initrd=initrd.cpio \
     --os-release=@os-release.txt \
     --cmdline=@cmdline.txt \
     --splash=splash.bmp \
     --devicetree=devicetree.dtb \
     --pcr-private-key=tpm2-pcr-private-key.pem \
     --pcr-public-key=tpm2-pcr-public-key.pem \
     --phases=enter-initrd,enter-initrd:leave-initrd,enter-initrd:leave-initrd:sysinit,enter-initrd:leave-initrd:sysinit:ready \
     --pcr-banks=sha1,sha256 \
     --pcr-private-key=tpm2-pcr-initrd-private-key.pem \
     --pcr-public-key=tpm2-pcr-initrd-public-key.pem \
     --phases=enter-initrd \
     --uname=1.2.3 \
     --output=vmlinux-1.2.3.efi
+ /usr/lib/systemd/systemd-measure sign --linux=vmlinux-1.2.3 \
--osrel=os-release.txt --cmdline=cmdline.txt --dtb=devicetree.dtb \
--splash=splash.bmp --initrd=initrd.cpio --bank=sha1 --bank=sha256 \
--private-key=tpm2-pcr-private-key.pem --public-key=tpm2-pcr-public-key.pem \
--phase=enter-initrd --phase=enter-initrd:leave-initrd \
--phase=enter-initrd:leave-initrd:sysinit \
--phase=enter-initrd:leave-initrd:sysinit:ready
+ /usr/lib/systemd/systemd-measure sign --linux=vmlinux-1.2.3 \
--osrel=os-release.txt --cmdline=cmdline.txt --dtb=devicetree.dtb \
--splash=splash.bmp --initrd=initrd.cpio --bank=sha1 --bank=sha256 \
--private-key=tpm2-pcr-initrd-private-key.pem \
--public-key=tpm2-pcr-initrd-public-key.pem \
--phase=enter-initrd
Wrote unsigned vmlinux-1.2.3.efi

so ukify prints out both invocations of systemd-measure as informative output โ€” those lines starting with + ๐Ÿซถ that's how we get to peek at how systemd-measure is being called. then it merges the output of both invocations into the .pcrsig section. and hey, systemd-measure can also do that merge all by itself using the --append= option โœจ

heads up bestie: in this example the .pcrpkey PE section holds the key given by the first --pcr-private-key= option, the one covering all boot phases. that .pcrpkey section is what the default policies of systemd-cryptenroll and systemd-creds use. if you want the stricter policy bound to tpm2-pcr-initrd-public-key.pem instead, just pass --tpm2-public-key= on the command line of those tools. easy ๐Ÿ’…

โœจ Example 4. Using a policy reference to add a signature only for the initrd boot phase โœจ

this is like the alternate route to the previous example ๐ŸŒธ instead of introducing a whole second signing key, we re-use the same signing key to sign a PCR policy restricted to the initrd boot phase, and we use the --policyref= option so secrets/credentials can bind only to that more restricted policy via this reference. clever right?? i'm obsessed ๐ŸŽ€

$ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tpm2-pcr-private-key.pem
.+........+.+........+.......+...+...+........+....+......+..+..........
$ openssl rsa -pubout -in tpm2-pcr-private-key.pem -out tpm2-pcr-public-key.pem
$ ukify build \
     --linux=vmlinux-1.2.3 \
     --initrd=initrd.cpio \
     --os-release=@os-release.txt \
     --cmdline=@cmdline.txt \
     --splash=splash.bmp \
     --devicetree=devicetree.dtb \
     --pcrpkey=tpm2-pcr-public-key.pem \
     --pcr-private-key=tpm2-pcr-private-key.pem \
     --phases=enter-initrd,enter-initrd:leave-initrd,enter-initrd:leave-initrd:sysinit,enter-initrd:leave-initrd:sysinit:ready \
     --policyref= \
     --pcr-banks=sha1,sha256 \
     --pcr-private-key=tpm2-pcr-private-key.pem \
     --phases=enter-initrd \
     --policyref=initrd \
     --uname=1.2.3 \
     --output=vmlinux-1.2.3.efi
+ /usr/lib/systemd/systemd-measure sign --linux=vmlinux-1.2.3 \
--osrel=os-release.txt --cmdline=cmdline.txt --dtb=devicetree.dtb \
--splash=splash.bmp --pcrpkey=tpm2-pcr-public-key.pem --initrd=initrd.cpio \
--bank=sha1 --bank=sha256 --private-key=tpm2-pcr-private-key.pem \
--phase=enter-initrd --phase=enter-initrd:leave-initrd \
--phase=enter-initrd:leave-initrd:sysinit \
--phase=enter-initrd:leave-initrd:sysinit:ready --policyref=
+ /usr/lib/systemd/systemd-measure sign --linux=vmlinux-1.2.3 \
--osrel=os-release.txt --cmdline=cmdline.txt --dtb=devicetree.dtb \
--splash=splash.bmp --pcrpkey=tpm2-pcr-public-key.pem --initrd=initrd.cpio \
--bank=sha1 --bank=sha256 --private-key=tpm2-pcr-initrd-private-key.pem \
--phase=enter-initrd --policyref=initrd
Wrote unsigned vmlinux-1.2.3.efi

again, ukify spits out both invocations of systemd-measure as informative output (the + lines ๐Ÿ’–) so you can see exactly how it's being called, and then it merges both outputs into the .pcrsig section. and yes, systemd-measure can handle that merge itself with --append= too โœจ

important bestie: in this example the .pcrsig PE section contains signed policies covering all boot phases bound to the same empty policy reference, plus a more restricted signed policy for just the initrd boot phase bound to the "initrd" policy reference. to use the stricter policy, supply --tpm2-policyref=initrd to systemd-cryptenroll. to use the more relaxed policy that includes all boot phases, just omit the --tpm2-policyref= of systemd-cryptenroll. and one more thing: policies for systemd-creds cannot currently bind to signed policies with a non-empty policy reference. that's just how it is rn ๐Ÿ˜ญ

๐Ÿšฆ EXIT STATUS ๐Ÿšฆ

  • success โ†’ 0 is returned ๐ŸŽ‰
  • failure โ†’ a non-zero failure code otherwise ๐Ÿ’€

๐Ÿ‘€ SEE ALSO ๐Ÿ‘€

the whole friend group, go say hi:


๐Ÿ“ NOTES ๐Ÿ“

  1. UAPI.5 Unified Kernel Image (UKI) โ€” https://uapi-group.org/specifications/specs/unified_kernel_image/

and that's the whole page bestie, you made it to the end ๐ŸŽ‰๐Ÿ’–โœจ this was systemd 262.1, the SYSTEMD-MEASURE(1) girlies' manual page, dated for its release era. go forth and measure those PCRs responsibly, mwah ๐Ÿ’…๐Ÿซถ๐ŸŒธ

๐Ÿ“ฆ where this page comes from
  • arch rolling: systemd 262.1-1 ยท LGPL-2.1-or-later AND CC0-1.0 AND GPL-2.0-or-later AND MIT-0
  • ๐Ÿ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms