1
0
mirror of https://github.com/systemd/systemd.git synced 2025-08-26 17:49:52 +03:00

Merge pull request #34295 from poettering/uki-with-many-ukify

ukify: add multi-profile UKI support
This commit is contained in:
Yu Watanabe
2024-09-07 08:08:48 +09:00
committed by GitHub
2 changed files with 224 additions and 25 deletions

View File

@ -228,6 +228,31 @@
</listitem>
</varlistentry>
<varlistentry>
<term><option>--extend=<replaceable>PATH</replaceable></option></term>
<listitem><para>Takes a path to an existing PE file to import into the newly generated PE file. If
this option is used all UKI PE sections of the specified PE file are copied into the target PE file
before any new PE sections are appended. 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>--measure-base=<replaceable>PATH</replaceable></option></term>
<listitem><para>Takes a path to an existing PE file to use as base profile, for measuring
multi-profile UKIs. When calculating the PCR values, this has the effect that the sections
specified on the command line are combined with any sections from the PE file specified here (up to
the first <literal>.profile</literal> section, and only if not already specified on the command
line). Typically, this is used together with <option>--extend=</option> to both import and use as
measurement base an existing UKI.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--tools=<replaceable>DIRS</replaceable></option></term>
@ -400,6 +425,17 @@
<xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Profile=<replaceable>PATH</replaceable></varname></term>
<term><option>--profile=<replaceable>PATH</replaceable></option></term>
<listitem><para>A path to a UKI profile to place in an <literal>.profile</literal> section. This
option is useful for creating multi-profile UKIs, and is typically used in combination with
<option>--extend=</option>, to extend the specified UKI with an additional profile.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>PCRBanks=<replaceable>PATH</replaceable></varname></term>
<term><option>--pcr-banks=<replaceable>PATH</replaceable></option></term>
@ -692,6 +728,48 @@ Writing public key for PCR signing to /etc/kernel/pcr-system.pub.pem
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 PE</title>
<para>First, create the base UKI:</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' \
--output=base.efi
</programlisting>
<para>Then, extend the base UKI with information about profile @0:</para>
<programlisting>$ ukify build \
--extend=base.efi \
--profile='TITLE=Base' \
--output=base-with-profile-0.efi
</programlisting>
<para>Add a second profile (@1):</para>
<programlisting>$ ukify build \
--extend=base-with-profile-0.efi \
--profile='TITLE=Boot into Storage Target Mode
ID=storagetm' \
--cmdline='quiet rw rd.systemd.unit=stroage-target-mode.target' \
--output=base-with-profile-0-1.efi
</programlisting>
<para>Add a third profile (@2):</para>
<programlisting>$ ukify build \
--extend=base-with-profile-0-1.efi \
--profile='TITLE=Factory Reset
ID=factory-reset' \
--cmdline='quiet rw systemd.unit=factory-reset.target' \
--output=base-with-profile-0-1-2.efi
</programlisting>
<para>The resulting UKI <filename>base-with-profile-0-1-2.efi</filename> will now contain three profiles.</para>
</example>
</refsect1>
<refsect1>

View File

@ -313,6 +313,7 @@ DEFAULT_SECTIONS_TO_SHOW = {
'.pcrsig' : 'text',
'.sbat' : 'text',
'.sbom' : 'binary',
'.profile' : 'text',
}
@dataclasses.dataclass
@ -380,7 +381,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 in range(len(self.sections)):
if self.sections[i].name == ".profile":
start = i+1
if section.name in [s.name for s in self.sections[start:]]:
raise ValueError(f'Duplicate section {section.name}')
self.sections += [section]
@ -492,7 +500,19 @@ def key_path_groups(opts):
pp_groups)
def pe_strip_section_name(name):
return name.rstrip(b"\x00").decode()
def call_systemd_measure(uki, linux, opts):
if not opts.measure and not opts.pcr_private_keys:
return
measure_sections = ('.linux', '.osrel', '.cmdline', '.initrd',
'.ucode', '.splash', '.dtb', '.uname',
'.sbat', '.pcrpkey', '.profile')
measure_tool = find_tool('systemd-measure',
'/usr/lib/systemd/systemd-measure',
opts=opts)
@ -501,16 +521,67 @@ def call_systemd_measure(uki, linux, opts):
# PCR measurement
to_measure = []
tflist = []
# First, pick up the sections we shall measure now */
for s in uki.sections:
print(s)
if not s.measure:
continue
if s.content is not None:
assert(s.name != ".linux" or linux is None)
to_measure.append(f"--{s.name.removeprefix('.')}={s.content}")
else:
raise ValueError(f"Don't know how to measure section {s.name}");
if linux is not None:
to_measure.append(f'--linux={linux}')
# And now iterate through the base profile and measure what we haven't measured above
if opts.measure_base is not None:
pe = pefile.PE(opts.measure_base, fast_load=True)
# Find matching PE section in base image
for base_section in pe.sections:
name = pe_strip_section_name(base_section.Name)
# If we reach the first .profile section the base is over
if name == ".profile":
break
# Only some sections are measured
if name not in measure_sections:
continue
# Check if this is a section we already covered above
already_covered = False
for s in uki.sections:
if s.measure and name == s.name:
already_covered = True
break;
if already_covered:
continue
# Split out section and use as base
tf = tempfile.NamedTemporaryFile()
tf.write(base_section.get_data(length=base_section.Misc_VirtualSize))
tf.flush()
tflist.append(tf)
to_measure.append(f"--{name.removeprefix('.')}={tf.name}")
if opts.measure:
pp_groups = opts.phase_path_groups or []
cmd = [
measure_tool,
'calculate',
f'--linux={linux}',
*(f"--{s.name.removeprefix('.')}={s.content}"
for s in uki.sections
if s.measure),
*to_measure,
*(f'--bank={bank}'
for bank in banks),
# For measurement, the keys are not relevant, so we can lump all the phase paths
@ -530,10 +601,7 @@ def call_systemd_measure(uki, linux, opts):
cmd = [
measure_tool,
'sign',
f'--linux={linux}',
*(f"--{s.name.removeprefix('.')}={s.content}"
for s in uki.sections
if s.measure),
*to_measure,
*(f'--bank={bank}'
for bank in banks),
]
@ -631,6 +699,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())
@ -664,8 +735,8 @@ 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):
if s.Name.rstrip(b"\x00").decode() == section.name:
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.')
@ -701,7 +772,7 @@ def merge_sbat(input_pe: [pathlib.Path], input_text: [str]) -> str:
continue
for section in pe.sections:
if section.Name.rstrip(b"\x00").decode() == ".sbat":
if pe_strip_section_name(section.Name) == ".sbat":
split = section.get_data().rstrip(b"\x00").decode().splitlines()
if not split[0].startswith('sbat,'):
print(f"{f} does not contain a valid SBAT section, skipping.")
@ -783,6 +854,28 @@ def verify(tool, opts):
return tool['output'] in info
def import_to_extend(uki, opts):
if opts.extend is None:
return
import_sections = ('.linux', '.osrel', '.cmdline', '.initrd',
'.ucode', '.splash', '.dtb', '.uname',
'.sbat', '.pcrsig', '.pcrpkey', '.profile')
pe = pefile.PE(opts.extend, fast_load=True)
for section in pe.sections:
n = pe_strip_section_name(section.Name)
if n not in import_sections:
continue
print(f"Copying section '{n}' from '{opts.extend}': {section.Misc_VirtualSize} bytes")
uki.add_section(Section.create(n, section.get_data(length=section.Misc_VirtualSize), measure=False))
def make_uki(opts):
# kernel payload signing
@ -847,8 +940,12 @@ def make_uki(opts):
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
# Import an existing UKI for extension
import_to_extend(uki, opts)
sections = [
# name, content, measure?
('.profile', opts.profile, True ),
('.osrel', opts.os_release, True ),
('.cmdline', opts.cmdline, True ),
('.dtb', opts.devicetree, True ),
@ -870,21 +967,22 @@ def make_uki(opts):
for section in opts.sections:
uki.add_section(section)
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
if opts.extend is None:
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))
# PCR measurement and signing
@ -1041,7 +1139,7 @@ def generate_keys(opts):
def inspect_section(opts, section):
name = section.Name.rstrip(b"\x00").decode()
name = pe_strip_section_name(section.Name)
# find the config for this section in opts and whether to show it
config = opts.sections_by_name.get(name, None)
@ -1359,6 +1457,13 @@ CONFIG_ITEMS = [
help = 'section as name and contents [NAME section] or section to print',
),
ConfigItem(
'--profile',
metavar='TEST|@PATH',
help='Profile information [.profile section]',
config_key = 'UKI/Uname',
),
ConfigItem(
'--efi-arch',
metavar = 'ARCH',
@ -1374,6 +1479,22 @@ CONFIG_ITEMS = [
config_key = 'UKI/Stub',
),
ConfigItem(
'--extend',
metavar = 'UKI',
type = pathlib.Path,
help = 'path to existing UKI file whose relevant sections to insert into the UKI first',
config_key = 'UKI/Extend',
),
ConfigItem(
'--measure-base',
metavar = 'UKI',
type = pathlib.Path,
help = 'path to existing UKI file whose relevant sections shall be used as base for PCR11 prediction',
config_key = 'UKI/MeasureBase',
),
ConfigItem(
'--pcr-banks',
metavar = 'BANK…',
@ -1678,7 +1799,7 @@ def finalize_options(opts):
opts.efi_arch = guess_efi_arch()
if opts.stub is None:
if opts.linux is not None:
if opts.linux is not None or opts.extend is not None:
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
else:
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub')