mirror of
https://github.com/systemd/systemd.git
synced 2025-08-10 09:49:52 +03:00
ukify: Rework multi-profile UKIs
The API introduced in https://github.com/systemd/systemd/pull/34295 is less than ideal: - It doesn't consider signing at all (ukify can't sign separately yet) - Measurement is completely broken (all profile sections are marked to not be measured) - It focuses on a very niche use case of extending existing UKIs and makes the more common use case of building a UKI with several profiles included much harder than needed. Let's instead rework the API to focus on the primary use case of building a UKI with multiple profiles added to it immediately. We require the profiles to be built upfront as separate PE binaries with UKI. There's no need to sign or measure these, they're solely vehicles for profile sections. This saves us from having to complicate the command line and config parsing to support defining multiple profiles. To add the profiles when building a UKI, we introduce the new --add-profile switch which takes a path to a PE binary describing a profile. The required sections are read from each PE binary, measured and added as a profile. The integration test is disabled until the new API is merged and exposed in mkosi so that building a UKI with profiles can be left to mkosi and the integration test will only test the switching between profiles and not the building of UKIs with profiles.
This commit is contained in:
@ -228,6 +228,19 @@
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--join-profile=<replaceable>PATH</replaceable></option></term>
|
||||
|
||||
<listitem><para>Takes a path to an existing PE file containing an additional profile to add to the
|
||||
unified kernel image. The profile can be generated beforehand with <command>ukify</command>. The
|
||||
profile does not need to be signed or contain PCR measurements. All UKI PE sections of the
|
||||
specified PE file are copied into the generated UKI. This is useful for generating multi-profile
|
||||
UKIs. Note that this only copies PE sections that are defined by the UKI specification, and ignores
|
||||
any other, for example <literal>.text</literal> or similar.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--tools=<replaceable>DIRS</replaceable></option></term>
|
||||
|
||||
@ -703,6 +716,50 @@ Writing public key for PCR signing to /etc/systemd/tpm2-pcr-public-key-system.pe
|
||||
by default, so after this file has been created, installations of kernels that create a UKI on the
|
||||
local machine using <command>kernel-install</command> will perform signing using this config.</para>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<title>Multi-Profile UKI</title>
|
||||
|
||||
<para>First, create a few profiles:</para>
|
||||
|
||||
<programlisting>$ ukify build \
|
||||
--profile='TITLE=Base' \
|
||||
--output=profile0.efi
|
||||
</programlisting>
|
||||
|
||||
<para>Add a second profile (@1):</para>
|
||||
|
||||
<programlisting>$ ukify build \
|
||||
--profile='TITLE=Boot into Storage Target Mode
|
||||
ID=storagetm' \
|
||||
--cmdline='quiet rw rd.systemd.unit=stroage-target-mode.target' \
|
||||
--output=profile1.efi
|
||||
</programlisting>
|
||||
|
||||
<para>Add a third profile (@2):</para>
|
||||
|
||||
<programlisting>$ ukify build \
|
||||
--profile='TITLE=Factory Reset
|
||||
ID=factory-reset' \
|
||||
--cmdline='quiet rw systemd.unit=factory-reset.target' \
|
||||
--output=profile2.efi
|
||||
</programlisting>
|
||||
|
||||
<para>Then, create a UKI and include all the generated profiles:</para>
|
||||
|
||||
<programlisting>$ ukify build \
|
||||
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
|
||||
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
|
||||
--cmdline='quiet rw' \
|
||||
--join-profile=profile0.efi \
|
||||
--join-profile=profile1.efi \
|
||||
--join-profile=profile2.efi \
|
||||
--output=base.efi
|
||||
</programlisting>
|
||||
|
||||
<para>The resulting UKI <filename>base-with-profile-0-1-2.efi</filename> will now contain three profiles.</para>
|
||||
</example>
|
||||
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
|
@ -379,7 +379,14 @@ class UKI:
|
||||
sections: list[Section] = dataclasses.field(default_factory=list, init=False)
|
||||
|
||||
def add_section(self, section):
|
||||
if section.name in [s.name for s in self.sections]:
|
||||
start = 0
|
||||
|
||||
# Start search at last .profile section, if there is one
|
||||
for i, s in enumerate(self.sections):
|
||||
if s.name == ".profile":
|
||||
start = i + 1
|
||||
|
||||
if any(section.name == s.name for s in self.sections[start:]):
|
||||
raise ValueError(f'Duplicate section {section.name}')
|
||||
|
||||
self.sections += [section]
|
||||
@ -495,7 +502,7 @@ def pe_strip_section_name(name):
|
||||
return name.rstrip(b"\x00").decode()
|
||||
|
||||
|
||||
def call_systemd_measure(uki, opts):
|
||||
def call_systemd_measure(uki, opts, profile_start=0):
|
||||
measure_tool = find_tool('systemd-measure',
|
||||
'/usr/lib/systemd/systemd-measure',
|
||||
opts=opts)
|
||||
@ -504,6 +511,26 @@ def call_systemd_measure(uki, opts):
|
||||
|
||||
# PCR measurement
|
||||
|
||||
# First, pick up either the base sections or the profile specific sections we shall measure now
|
||||
to_measure = {s.name: s for s in uki.sections[profile_start:] if s.measure}
|
||||
|
||||
# Then, if we're measuring a profile, lookup the missing sections from the base image.
|
||||
if profile_start != 0:
|
||||
for section in uki.sections:
|
||||
# If we reach the first .profile section the base is over
|
||||
if section.name == ".profile":
|
||||
break
|
||||
|
||||
# Only some sections are measured
|
||||
if not section.measure:
|
||||
continue
|
||||
|
||||
# Check if this is a section we already covered above
|
||||
if section.name in to_measure:
|
||||
continue
|
||||
|
||||
to_measure[section.name] = section
|
||||
|
||||
if opts.measure:
|
||||
pp_groups = opts.phase_path_groups or []
|
||||
|
||||
@ -511,8 +538,7 @@ def call_systemd_measure(uki, opts):
|
||||
measure_tool,
|
||||
'calculate',
|
||||
*(f"--{s.name.removeprefix('.')}={s.content}"
|
||||
for s in uki.sections
|
||||
if s.measure),
|
||||
for s in to_measure.values()),
|
||||
*(f'--bank={bank}'
|
||||
for bank in banks),
|
||||
# For measurement, the keys are not relevant, so we can lump all the phase paths
|
||||
@ -533,8 +559,7 @@ def call_systemd_measure(uki, opts):
|
||||
measure_tool,
|
||||
'sign',
|
||||
*(f"--{s.name.removeprefix('.')}={s.content}"
|
||||
for s in uki.sections
|
||||
if s.measure),
|
||||
for s in to_measure.values()),
|
||||
*(f'--bank={bank}'
|
||||
for bank in banks),
|
||||
]
|
||||
@ -632,6 +657,9 @@ def pe_add_sections(uki: UKI, output: str):
|
||||
# We could strip the signatures, but why would anyone sign the stub?
|
||||
raise PEError('Stub image is signed, refusing.')
|
||||
|
||||
# Remember how many sections originate from systemd-stub
|
||||
n_original_sections = len(pe.sections)
|
||||
|
||||
for section in uki.sections:
|
||||
new_section = pefile.SectionStructure(pe.__IMAGE_SECTION_HEADER_format__, pe=pe)
|
||||
new_section.__unpack__(b'\0' * new_section.sizeof())
|
||||
@ -668,7 +696,7 @@ def pe_add_sections(uki: UKI, output: str):
|
||||
# Special case, mostly for .sbat: the stub will already have a .sbat section, but we want to append
|
||||
# the one from the kernel to it. It should be small enough to fit in the existing section, so just
|
||||
# swap the data.
|
||||
for i, s in enumerate(pe.sections):
|
||||
for i, s in enumerate(pe.sections[:n_original_sections]):
|
||||
if pe_strip_section_name(s.Name) == section.name:
|
||||
if new_section.Misc_VirtualSize > s.SizeOfRawData:
|
||||
raise PEError(f'Not enough space in existing section {section.name} to append new data.')
|
||||
@ -853,7 +881,6 @@ def make_uki(opts):
|
||||
|
||||
sections = [
|
||||
# name, content, measure?
|
||||
('.profile', opts.profile, True ),
|
||||
('.osrel', opts.os_release, True ),
|
||||
('.cmdline', opts.cmdline, True ),
|
||||
('.dtb', opts.devicetree, True ),
|
||||
@ -864,6 +891,10 @@ def make_uki(opts):
|
||||
('.ucode', opts.microcode, True ),
|
||||
]
|
||||
|
||||
# If we're building a PE profile binary, the ".profile" section has to be the first one.
|
||||
if opts.profile and not opts.join_profiles:
|
||||
uki.add_section(Section.create(".profile", opts.profile, measure=True))
|
||||
|
||||
for name, content, measure in sections:
|
||||
if content:
|
||||
uki.add_section(Section.create(name, content, measure=measure))
|
||||
@ -881,26 +912,65 @@ def make_uki(opts):
|
||||
|
||||
uki.add_section(Section.create('.linux', linux, measure=True, virtual_size=virtual_size))
|
||||
|
||||
if linux is not None:
|
||||
# Merge the .sbat sections from stub, kernel and parameter, so that revocation can be done on either.
|
||||
input_pes = [opts.stub, linux]
|
||||
if not opts.sbat:
|
||||
opts.sbat = ["""sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
|
||||
# Don't add a sbat section to profile PE binaries.
|
||||
if opts.join_profiles or not opts.profile:
|
||||
if linux is not None:
|
||||
# Merge the .sbat sections from stub, kernel and parameter, so that revocation can be done on either.
|
||||
input_pes = [opts.stub, linux]
|
||||
if not opts.sbat:
|
||||
opts.sbat = ["""sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
|
||||
uki,1,UKI,uki,1,https://uapi-group.org/specifications/specs/unified_kernel_image/
|
||||
"""]
|
||||
else:
|
||||
# Addons don't use the stub so we add SBAT manually
|
||||
input_pes = []
|
||||
if not opts.sbat:
|
||||
opts.sbat = ["""sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
|
||||
else:
|
||||
# Addons don't use the stub so we add SBAT manually
|
||||
input_pes = []
|
||||
if not opts.sbat:
|
||||
opts.sbat = ["""sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
|
||||
uki-addon,1,UKI Addon,addon,1,https://www.freedesktop.org/software/systemd/man/latest/systemd-stub.html
|
||||
"""]
|
||||
uki.add_section(Section.create('.sbat', merge_sbat(input_pes, opts.sbat), measure=linux is not None))
|
||||
uki.add_section(Section.create('.sbat', merge_sbat(input_pes, opts.sbat), measure=linux is not None))
|
||||
|
||||
# If we're building a UKI with additional profiles, the .profile section for the base profile has to be
|
||||
# the last one so that everything before it is shared between profiles. The only thing we don't share
|
||||
# between profiles is the .pcrsig section which is appended later and doesn't make sense to share.
|
||||
if opts.profile and opts.join_profiles:
|
||||
uki.add_section(Section.create(".profile", opts.profile, measure=True))
|
||||
|
||||
# PCR measurement and signing
|
||||
|
||||
call_systemd_measure(uki, opts=opts)
|
||||
|
||||
# UKI profiles
|
||||
|
||||
to_import = {'.linux', '.osrel', '.cmdline', '.initrd', '.ucode', '.splash', '.dtb', '.uname', '.sbat', '.profile'}
|
||||
|
||||
for profile in opts.join_profiles:
|
||||
pe = pefile.PE(profile, fast_load=True)
|
||||
prev_len = len(uki.sections)
|
||||
|
||||
names = [pe_strip_section_name(s.Name) for s in pe.sections]
|
||||
names = [n for n in names if n in to_import]
|
||||
|
||||
if len(names) == 0:
|
||||
raise ValueError(f"Found no valid sections in PE profile binary {profile}")
|
||||
|
||||
if names[0] != ".profile":
|
||||
raise ValueError(f'Expected .profile section as first valid section in PE profile binary {profile} but got {names[0]}')
|
||||
|
||||
if names.count(".profile") > 1:
|
||||
raise ValueError(f'Profile PE binary {profile} contains multiple .profile sections')
|
||||
|
||||
for section in pe.sections:
|
||||
n = pe_strip_section_name(section.Name)
|
||||
|
||||
if n not in to_import:
|
||||
continue
|
||||
|
||||
print(f"Copying section '{n}' from '{profile}': {section.Misc_VirtualSize} bytes")
|
||||
uki.add_section(Section.create(n, section.get_data(length=section.Misc_VirtualSize), measure=True))
|
||||
|
||||
call_systemd_measure(uki, opts=opts, profile_start=prev_len + 1)
|
||||
|
||||
# UKI creation
|
||||
|
||||
if sign_args_present:
|
||||
@ -1371,6 +1441,15 @@ CONFIG_ITEMS = [
|
||||
config_key = 'UKI/Profile',
|
||||
),
|
||||
|
||||
ConfigItem(
|
||||
'--join-profile',
|
||||
dest = 'join_profiles',
|
||||
metavar = 'PATH',
|
||||
action = 'append',
|
||||
default = [],
|
||||
help = 'A PE binary containing an additional profile to add to the UKI',
|
||||
),
|
||||
|
||||
ConfigItem(
|
||||
'--efi-arch',
|
||||
metavar = 'ARCH',
|
||||
@ -1718,6 +1797,11 @@ def finalize_options(opts):
|
||||
if opts.sign_kernel and not opts.sb_key and not opts.sb_cert_name:
|
||||
raise ValueError('--sign-kernel requires either --secureboot-private-key= and --secureboot-certificate= (for sbsign) or --secureboot-certificate-name= (for pesign) to be specified')
|
||||
|
||||
if opts.join_profiles and not opts.profile:
|
||||
# If any additional profiles are added, we need a base profile as well so add one if
|
||||
# one wasn't explicitly provided
|
||||
opts.profile = 'ID=main'
|
||||
|
||||
if opts.verb == 'build' and opts.output is None:
|
||||
if opts.linux is None:
|
||||
raise ValueError('--output= must be specified when building a PE addon')
|
||||
|
@ -6,5 +6,6 @@ integration_tests += [
|
||||
'storage' : 'persistent',
|
||||
'vm' : true,
|
||||
'firmware' : 'auto',
|
||||
'enabled' : false,
|
||||
},
|
||||
]
|
||||
|
Reference in New Issue
Block a user