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>
`UkifyConfig.sbat` is an optional list, so the default of
`config_set_if_unset` would only accept a single value.
Further, it is defined with an empty list as a default, causing
configuration values to always be ignored, as the default is not
`None`.
Set its `config_push` to `ConfigItem.config_list_prepend` to collect
the `SBAT` lines from the config.
Signed-off-by: Wolfgang Bumiller <w.bumiller@errno.eu>
`ConfigItem.config_list_prepend` assumed that the passed value was a
list. This just happens to be the case for the `--initrd` and
`--devicetree-auto` parameters, as those get special treatment in
`ConfigItem.apply_config` as they use space separated lists.
However, this is not generally the case: When trying to set
`UKI/Firmware` the following error occurred:
Traceback (most recent call last):
File "/usr/bin/ukify", line 2565, in <module>
main()
~~~~^^
File "/usr/bin/ukify", line 2548, in main
opts = UkifyConfig.from_namespace(parse_args())
~~~~~~~~~~^^
File "/usr/bin/ukify", line 2542, in parse_args
apply_config(opts)
~~~~~~~~~~~~^^^^^^
File "/usr/bin/ukify", line 2295, in apply_config
item.apply_config(namespace, section_name, group, key, value)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/bin/ukify", line 1892, in apply_config
self.config_push(namespace, group, dest, value)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/bin/ukify", line 1765, in config_list_prepend
setattr(namespace, dest, value + old)
~~~~~~^~~~~
TypeError: unsupported operand type(s) for +: 'PosixPath' and 'list'
Since we can thus get both lists and strings, check the type and
convert non-list values to `[value]`.
Signed-off-by: Wolfgang Bumiller <w.bumiller@errno.eu>
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.
Our tools will strip trailing NULs, so this is not causing harm, except for
the corner case when the .sbat section is exactly of the maximum size and now
it wouldn't fit because we're one byte short.
But it also is not needed. Sections have an encoded size of the data and the
reader must use that and must be prepared to handle a text section that does
not end in NUL. Appending of the NUL was added in
c3f7501c4d, without any discussion of this
change. Since we didn't insert the NUL before, the tools must have been
prepared to work without it. I think it's better to keep the code clean and
not do this unnecessary step.
The padding was done to expand the new section contents to the expected size of
the new section. And this then would be used for the content in the existing
section. The new section cannot be larger than the old section, but it can be
smaller. If the new section was smaller, then we'd not write enough padding and
the output file would be corrupted.
This was observed in CI when the .sbat section in the stub was padded to 1k.
The UKI with an .sbat section that was merged and was fairly short would hit
this scenario and be corrupted.
SBAT section of UKI may contain \u000 null characters. Rstrip them, and if there's anything left in the middle,
escape them so they are displayed as text.
Fixes#38606
Fixes https://github.com/systemd/systemd/issues/34780. The number in the header
is the size of the *compressed* data, so for gzip we'd read the initial part of
the decompressed data (equal to the size of the compressed data) and not find
the version string. Later on, Fedora switched to zstd compression, and there we
correctly use the number as the size of the compressed data, so we stopped
hitting the issue, but we should still fix it for older kernels.
I verified that the fix works for gzip-compressed kernels. I also made the same
change for the code for lzma compression. I'm pretty sure it is the right thing,
even though I don't have such a kernel at hand to test.
>>> ukify.Uname.scrape('/lib/modules/6.12.0-0.rc2.24.fc42.aarch64/vmlinuz')
Real-Mode Kernel Header magic not found
+ readelf --notes /lib/modules/6.12.0-0.rc2.24.fc42.aarch64/vmlinuz
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start
Found uname version: 6.12.0-0.rc2.24.fc42.aarch64
Otherwise it will fail as it's an archive, not a PE file:
Invalid DOS header magic
Can't open image /boot/vmlinuz.old
/boot/vmlinuz.old is compressed and cannot be loaded by UEFI, decompressing
+ sbverify --list /boot/vmlinuz.old
=========================== short test summary info ============================
FAILED ../src/ukify/test/test_ukify.py::test_efi_signing_sbsign[3650] - subprocess.CalledProcessError: Command '['sbverify', '--list', PosixPath('/boot/vmlinuz.old')]' returned non-zero exit status 1.
FAILED ../src/ukify/test/test_ukify.py::test_efi_signing_sbsign[None] - subprocess.CalledProcessError: Command '['sbverify', '--list', PosixPath('/boot/vmlinuz.old')]' returned non-zero exit status 1.
FAILED ../src/ukify/test/test_ukify.py::test_inspect - subprocess.CalledProcessError: Command '['sbverify', '--list', PosixPath('/boot/vmlinuz.old')]' returned non-zero exit status 1.
Follow-up for 0dd03215f1
For some reason, argparse treats undefined options as positional args in
certain scenarios:
$ src/ukify/ukify.py --badopt='11'
ukify.py: error: unrecognized arguments: --badopt=11
$ src/ukify/ukify.py --badopt '11'
ukify.py: error: unrecognized arguments: --badopt
$ src/ukify/ukify.py --badopt '11 12'
Assuming obsolete command line syntax with no verb. Please use 'build'.
Traceback (most recent call last):
File "/home/zbyszek/src/systemd/src/ukify/ukify.py", line 2497, in <module>
main()
~~~~^^
File "/home/zbyszek/src/systemd/src/ukify/ukify.py", line 2485, in main
check_inputs(opts)
~~~~~~~~~~~~^^^^^^
File "/home/zbyszek/src/systemd/src/ukify/ukify.py", line 671, in check_inputs
value.open().close()
~~~~~~~~~~^^
File "/usr/lib64/python3.13/pathlib/_local.py", line 537, in open
return io.open(self, mode, buffering, encoding, errors, newline)
~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '--badopt=11 12'
I suspect that this is some crap compat for Windows, where option parsing is
an even bigger mess than here.
Being told about positional args, when no positional args were specified is
confusing, so add a check for this.
In Python, exception messages are often embedded in surrounding text, so in
general they should not contain punctuation.
Also, no need to instantiate the exception object if no arguments are used.
On some distros on some architectures (e.g.: Ubuntu arm64) the kernel is shipped as
a gzipped file, which the UEFI firmware does not understand.
If pefile fails to parse it, try to decompress it.
The header starts with 'zstd', not 'zstd22':
$ ukify build --linux vmlinuz-6.13+unreleased-cloud-arm64 --initrd /boot/initrd.img-6.12.12-amd64 --output uki
Kernel version not specified, starting autodetection 😖.
Real-Mode Kernel Header magic not found
+ readelf --notes vmlinuz-6.13+unreleased-cloud-arm64
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start
Traceback (most recent call last):
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 2510, in <module>
main()
~~~~^^
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 2499, in main
make_uki(opts)
~~~~~~~~^^^^^^
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 1328, in make_uki
opts.uname = Uname.scrape(linux, opts=opts)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 384, in scrape
version = func(filename, opts=opts)
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 374, in scrape_generic
text = maybe_decompress(filename)
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 221, in maybe_decompress
return get_zboot_kernel(f)
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 201, in get_zboot_kernel
raise NotImplementedError(f'unknown compressed type: {comp_type!r}')
NotImplementedError: unknown compressed type: b'zstd\x00\x00'
The zstd library does not support stream decompression, and it
requires the zstd header to contain extra metadata, that the kernel
build does not append:
$ file -k vmlinuz-6.13+unreleased-cloud-arm64
vmlinuz-6.13+unreleased-cloud-arm64: PE32+ executable (EFI application) Aarch64 (stripped to external PDB), for MS Windows, 2 sections\012- data
$ ukify build --linux vmlinuz-6.13+unreleased-cloud-arm64 --initrd /boot/initrd.img-6.12.12-amd64 --output uki
Kernel version not specified, starting autodetection 😖.
Real-Mode Kernel Header magic not found
+ readelf --notes vmlinuz-6.13+unreleased-cloud-arm64
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start
Traceback (most recent call last):
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 2508, in <module>
main()
~~~~^^
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 2497, in main
make_uki(opts)
~~~~~~~~^^^^^^
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 1326, in make_uki
opts.uname = Uname.scrape(linux, opts=opts)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 382, in scrape
version = func(filename, opts=opts)
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 372, in scrape_generic
text = maybe_decompress(filename)
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 219, in maybe_decompress
return get_zboot_kernel(f)
File "/home/bluca/git/systemd/src/ukify/ukify.py", line 199, in get_zboot_kernel
return cast(bytes, zstd.uncompress(f.read(size)))
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
zstd.Error: Input data invalid or missing content size in frame header.
This appears to be by design:
https://github.com/sergey-dryabzhinsky/python-zstd/issues/53
Switch to python3-zstandard, which works.
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.
pefile has an hardcoded limit to 256MB per section:
https://github.com/erocarrera/pefile/issues/396
When building an initrd with large firmware files and
lots of kernel modules, this limit can be reached.
Skip over those warnings.
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.
With 19812661f1, we make sure at runtime
in the stub itself that SizeOfImage from the Linux EFISTUB PE binary is
taken into account, so there's no need to take this into account in ukify
itself. By reverting the ukify change, we again ensure that Misc_VirtualSize
reflects the actual size of the Linux EFISTUB PE binary in the .linux section
which lots of tooling depends on. It also makes sure we don't measure a bunch
of extra zeroes in the stub which should fix systemd-pcrlock measurements as
well.
This effectively reverts 2188c759f9 and
0005411352.
Fixes#35851
We should only use Misc_VirtualSize if it's smaller than SizeOfRawData,
since in that case it'll be the non-aligned section size. Otherwise we
have to use SizeOfRawData to get the size on disk.
UKIs can be used to bundle uefi firmwares that can be measured and
used on a confidential computing environment. There can be more than one
firmware blob bundle, each one for a specific platform. Also firmware images
can themselves be containers like IGVM files that can in turn bundle the
actual firmware blob. This change is specifically for uefi firmwares, not
IGVM container files.
This change adds support to introduce a .efifw section in UKI that can be
used for firmware blobs/images. There can be multiple such sections and each
section can contain a single firmware image.
The matching .hwids entry for a specific platform can be used to select the
most appropriate firmware blob.
ukify tool has been also changed to support addition of a firmware image
in UKI.
Since firmware gets measured automatically, we do not need to measure it
separately as a part of the UKI.
We should only use Misc_VirtualSize if it's smaller than SizeOfRawData,
since in that case it'll be the non-aligned section size. Otherwise we
have to use SizeOfRawData to get the size on disk.
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.