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:
@ -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>
|
||||
|
@ -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')
|
||||
|
Reference in New Issue
Block a user