mirror of
https://github.com/systemd/systemd.git
synced 2025-01-25 10:04:04 +03:00
Merge pull request #34608 from DaanDeMeyer/ukify
ukify: Rework multi-profile UKIs
This commit is contained in:
commit
598bb6fde4
@ -229,26 +229,14 @@
|
|||||||
</varlistentry>
|
</varlistentry>
|
||||||
|
|
||||||
<varlistentry>
|
<varlistentry>
|
||||||
<term><option>--extend=<replaceable>PATH</replaceable></option></term>
|
<term><option>--join-profile=<replaceable>PATH</replaceable></option></term>
|
||||||
|
|
||||||
<listitem><para>Takes a path to an existing PE file to import into the newly generated PE file. If
|
<listitem><para>Takes a path to an existing PE file containing an additional profile to add to the
|
||||||
this option is used all UKI PE sections of the specified PE file are copied into the target PE file
|
unified kernel image. The profile can be generated beforehand with <command>ukify</command>. The
|
||||||
before any new PE sections are appended. This is useful for generating multi-profile UKIs. Note
|
profile does not need to be signed or contain PCR measurements. All UKI PE sections of the
|
||||||
that this only copies PE sections that are defined by the UKI specification, and ignores any other,
|
specified PE file are copied into the generated UKI. This is useful for generating multi-profile
|
||||||
for example <literal>.text</literal> or similar.</para>
|
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>
|
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
@ -730,46 +718,48 @@ Writing public key for PCR signing to /etc/systemd/tpm2-pcr-public-key-system.pe
|
|||||||
</example>
|
</example>
|
||||||
|
|
||||||
<example>
|
<example>
|
||||||
<title>Multi-Profile PE</title>
|
<title>Multi-Profile UKI</title>
|
||||||
|
|
||||||
<para>First, create the base UKI:</para>
|
<para>First, create a few 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' \
|
|
||||||
--output=base.efi
|
|
||||||
</programlisting>
|
|
||||||
|
|
||||||
<para>Then, extend the base UKI with information about profile @0:</para>
|
|
||||||
|
|
||||||
<programlisting>$ ukify build \
|
<programlisting>$ ukify build \
|
||||||
--extend=base.efi \
|
|
||||||
--profile='TITLE=Base' \
|
--profile='TITLE=Base' \
|
||||||
--output=base-with-profile-0.efi
|
--output=profile0.efi
|
||||||
</programlisting>
|
</programlisting>
|
||||||
|
|
||||||
<para>Add a second profile (@1):</para>
|
<para>Add a second profile (@1):</para>
|
||||||
|
|
||||||
<programlisting>$ ukify build \
|
<programlisting>$ ukify build \
|
||||||
--extend=base-with-profile-0.efi \
|
|
||||||
--profile='TITLE=Boot into Storage Target Mode
|
--profile='TITLE=Boot into Storage Target Mode
|
||||||
ID=storagetm' \
|
ID=storagetm' \
|
||||||
--cmdline='quiet rw rd.systemd.unit=stroage-target-mode.target' \
|
--cmdline='quiet rw rd.systemd.unit=stroage-target-mode.target' \
|
||||||
--output=base-with-profile-0-1.efi
|
--output=profile1.efi
|
||||||
</programlisting>
|
</programlisting>
|
||||||
|
|
||||||
<para>Add a third profile (@2):</para>
|
<para>Add a third profile (@2):</para>
|
||||||
|
|
||||||
<programlisting>$ ukify build \
|
<programlisting>$ ukify build \
|
||||||
--extend=base-with-profile-0-1.efi \
|
|
||||||
--profile='TITLE=Factory Reset
|
--profile='TITLE=Factory Reset
|
||||||
ID=factory-reset' \
|
ID=factory-reset' \
|
||||||
--cmdline='quiet rw systemd.unit=factory-reset.target' \
|
--cmdline='quiet rw systemd.unit=factory-reset.target' \
|
||||||
--output=base-with-profile-0-1-2.efi
|
--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>
|
</programlisting>
|
||||||
|
|
||||||
<para>The resulting UKI <filename>base-with-profile-0-1-2.efi</filename> will now contain three profiles.</para>
|
<para>The resulting UKI <filename>base-with-profile-0-1-2.efi</filename> will now contain three profiles.</para>
|
||||||
</example>
|
</example>
|
||||||
|
|
||||||
</refsect1>
|
</refsect1>
|
||||||
|
|
||||||
<refsect1>
|
<refsect1>
|
||||||
|
@ -382,11 +382,11 @@ class UKI:
|
|||||||
start = 0
|
start = 0
|
||||||
|
|
||||||
# Start search at last .profile section, if there is one
|
# Start search at last .profile section, if there is one
|
||||||
for i in range(len(self.sections)):
|
for i, s in enumerate(self.sections):
|
||||||
if self.sections[i].name == ".profile":
|
if s.name == ".profile":
|
||||||
start = i+1
|
start = i + 1
|
||||||
|
|
||||||
if section.name in [s.name for s in self.sections[start:]]:
|
if any(section.name == s.name for s in self.sections[start:]):
|
||||||
raise ValueError(f'Duplicate section {section.name}')
|
raise ValueError(f'Duplicate section {section.name}')
|
||||||
|
|
||||||
self.sections += [section]
|
self.sections += [section]
|
||||||
@ -502,15 +502,7 @@ def pe_strip_section_name(name):
|
|||||||
return name.rstrip(b"\x00").decode()
|
return name.rstrip(b"\x00").decode()
|
||||||
|
|
||||||
|
|
||||||
def call_systemd_measure(uki, opts):
|
def call_systemd_measure(uki, opts, profile_start=0):
|
||||||
|
|
||||||
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',
|
measure_tool = find_tool('systemd-measure',
|
||||||
'/usr/lib/systemd/systemd-measure',
|
'/usr/lib/systemd/systemd-measure',
|
||||||
opts=opts)
|
opts=opts)
|
||||||
@ -519,52 +511,25 @@ def call_systemd_measure(uki, opts):
|
|||||||
|
|
||||||
# PCR measurement
|
# PCR measurement
|
||||||
|
|
||||||
to_measure = []
|
# First, pick up either the base sections or the profile specific sections we shall measure now
|
||||||
tflist = []
|
to_measure = {s.name: s for s in uki.sections[profile_start:] if s.measure}
|
||||||
|
|
||||||
# First, pick up the sections we shall measure now */
|
|
||||||
for s in uki.sections:
|
|
||||||
if not s.measure:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if s.content is not None:
|
|
||||||
to_measure.append(f"--{s.name.removeprefix('.')}={s.content}")
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Don't know how to measure section {s.name}");
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
# 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 we reach the first .profile section the base is over
|
||||||
if name == ".profile":
|
if section.name == ".profile":
|
||||||
break
|
break
|
||||||
|
|
||||||
# Only some sections are measured
|
# Only some sections are measured
|
||||||
if name not in measure_sections:
|
if not section.measure:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if this is a section we already covered above
|
# Check if this is a section we already covered above
|
||||||
already_covered = False
|
if section.name in to_measure:
|
||||||
for s in uki.sections:
|
|
||||||
if s.measure and name == s.name:
|
|
||||||
already_covered = True
|
|
||||||
break;
|
|
||||||
|
|
||||||
if already_covered:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Split out section and use as base
|
to_measure[section.name] = section
|
||||||
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:
|
if opts.measure:
|
||||||
pp_groups = opts.phase_path_groups or []
|
pp_groups = opts.phase_path_groups or []
|
||||||
@ -572,7 +537,8 @@ def call_systemd_measure(uki, opts):
|
|||||||
cmd = [
|
cmd = [
|
||||||
measure_tool,
|
measure_tool,
|
||||||
'calculate',
|
'calculate',
|
||||||
*to_measure,
|
*(f"--{s.name.removeprefix('.')}={s.content}"
|
||||||
|
for s in to_measure.values()),
|
||||||
*(f'--bank={bank}'
|
*(f'--bank={bank}'
|
||||||
for bank in banks),
|
for bank in banks),
|
||||||
# For measurement, the keys are not relevant, so we can lump all the phase paths
|
# For measurement, the keys are not relevant, so we can lump all the phase paths
|
||||||
@ -592,7 +558,8 @@ def call_systemd_measure(uki, opts):
|
|||||||
cmd = [
|
cmd = [
|
||||||
measure_tool,
|
measure_tool,
|
||||||
'sign',
|
'sign',
|
||||||
*to_measure,
|
*(f"--{s.name.removeprefix('.')}={s.content}"
|
||||||
|
for s in to_measure.values()),
|
||||||
*(f'--bank={bank}'
|
*(f'--bank={bank}'
|
||||||
for bank in banks),
|
for bank in banks),
|
||||||
]
|
]
|
||||||
@ -848,28 +815,6 @@ def verify(tool, opts):
|
|||||||
|
|
||||||
return tool['output'] in info
|
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):
|
def make_uki(opts):
|
||||||
# kernel payload signing
|
# kernel payload signing
|
||||||
|
|
||||||
@ -934,12 +879,8 @@ def make_uki(opts):
|
|||||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import an existing UKI for extension
|
|
||||||
import_to_extend(uki, opts)
|
|
||||||
|
|
||||||
sections = [
|
sections = [
|
||||||
# name, content, measure?
|
# name, content, measure?
|
||||||
('.profile', opts.profile, True ),
|
|
||||||
('.osrel', opts.os_release, True ),
|
('.osrel', opts.os_release, True ),
|
||||||
('.cmdline', opts.cmdline, True ),
|
('.cmdline', opts.cmdline, True ),
|
||||||
('.dtb', opts.devicetree, True ),
|
('.dtb', opts.devicetree, True ),
|
||||||
@ -950,6 +891,10 @@ def make_uki(opts):
|
|||||||
('.ucode', opts.microcode, True ),
|
('.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:
|
for name, content, measure in sections:
|
||||||
if content:
|
if content:
|
||||||
uki.add_section(Section.create(name, content, measure=measure))
|
uki.add_section(Section.create(name, content, measure=measure))
|
||||||
@ -967,7 +912,8 @@ def make_uki(opts):
|
|||||||
|
|
||||||
uki.add_section(Section.create('.linux', linux, measure=True, virtual_size=virtual_size))
|
uki.add_section(Section.create('.linux', linux, measure=True, virtual_size=virtual_size))
|
||||||
|
|
||||||
if opts.extend is None:
|
# Don't add a sbat section to profile PE binaries.
|
||||||
|
if opts.join_profiles or not opts.profile:
|
||||||
if linux is not None:
|
if linux is not None:
|
||||||
# Merge the .sbat sections from stub, kernel and parameter, so that revocation can be done on either.
|
# Merge the .sbat sections from stub, kernel and parameter, so that revocation can be done on either.
|
||||||
input_pes = [opts.stub, linux]
|
input_pes = [opts.stub, linux]
|
||||||
@ -984,10 +930,47 @@ uki-addon,1,UKI Addon,addon,1,https://www.freedesktop.org/software/systemd/man/l
|
|||||||
"""]
|
"""]
|
||||||
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
|
# PCR measurement and signing
|
||||||
|
|
||||||
call_systemd_measure(uki, opts=opts)
|
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
|
# UKI creation
|
||||||
|
|
||||||
if sign_args_present:
|
if sign_args_present:
|
||||||
@ -1453,9 +1436,18 @@ CONFIG_ITEMS = [
|
|||||||
|
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
'--profile',
|
'--profile',
|
||||||
metavar='TEST|@PATH',
|
metavar = 'TEST|@PATH',
|
||||||
help='Profile information [.profile section]',
|
help = 'Profile information [.profile section]',
|
||||||
config_key = 'UKI/Uname',
|
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(
|
ConfigItem(
|
||||||
@ -1473,22 +1465,6 @@ CONFIG_ITEMS = [
|
|||||||
config_key = 'UKI/Stub',
|
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(
|
ConfigItem(
|
||||||
'--pcr-banks',
|
'--pcr-banks',
|
||||||
metavar = 'BANK…',
|
metavar = 'BANK…',
|
||||||
@ -1793,7 +1769,7 @@ def finalize_options(opts):
|
|||||||
opts.efi_arch = guess_efi_arch()
|
opts.efi_arch = guess_efi_arch()
|
||||||
|
|
||||||
if opts.stub is None:
|
if opts.stub is None:
|
||||||
if opts.linux is not None or opts.extend is not None:
|
if opts.linux is not None:
|
||||||
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
|
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
|
||||||
else:
|
else:
|
||||||
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub')
|
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub')
|
||||||
@ -1821,6 +1797,11 @@ def finalize_options(opts):
|
|||||||
if opts.sign_kernel and not opts.sb_key and not opts.sb_cert_name:
|
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')
|
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.verb == 'build' and opts.output is None:
|
||||||
if opts.linux is None:
|
if opts.linux is None:
|
||||||
raise ValueError('--output= must be specified when building a PE addon')
|
raise ValueError('--output= must be specified when building a PE addon')
|
||||||
|
@ -6,5 +6,6 @@ integration_tests += [
|
|||||||
'storage' : 'persistent',
|
'storage' : 'persistent',
|
||||||
'vm' : true,
|
'vm' : true,
|
||||||
'firmware' : 'auto',
|
'firmware' : 'auto',
|
||||||
|
'enabled' : false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user