From 71f11a8f4c0fb50c5ff170db6ddc89e626cdc202 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 1 Oct 2024 10:15:15 +0200 Subject: [PATCH 1/5] Revert "ukify: introduce new --measure-base= switch" This reverts commit bc3e2c5a5774ae7b212817d04e04abccf30088ae. --- man/ukify.xml | 13 --------- src/ukify/ukify.py | 71 ++++------------------------------------------ 2 files changed, 6 insertions(+), 78 deletions(-) diff --git a/man/ukify.xml b/man/ukify.xml index a11eb85c91..902736d4ed 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -240,19 +240,6 @@ - - - - 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 .profile section, and only if not already specified on the command - line). Typically, this is used together with to both import and use as - measurement base an existing UKI. - - - - diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 55c40164ae..1a8c9507ee 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -503,14 +503,6 @@ def pe_strip_section_name(name): def call_systemd_measure(uki, 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) @@ -519,60 +511,15 @@ def call_systemd_measure(uki, opts): # PCR measurement - to_measure = [] - tflist = [] - - # 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) - - # 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', - *to_measure, + *(f"--{s.name.removeprefix('.')}={s.content}" + for s in uki.sections + if s.measure), *(f'--bank={bank}' for bank in banks), # For measurement, the keys are not relevant, so we can lump all the phase paths @@ -592,7 +539,9 @@ def call_systemd_measure(uki, opts): cmd = [ measure_tool, 'sign', - *to_measure, + *(f"--{s.name.removeprefix('.')}={s.content}" + for s in uki.sections + if s.measure), *(f'--bank={bank}' for bank in banks), ] @@ -1481,14 +1430,6 @@ CONFIG_ITEMS = [ 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…', From 3891d57c4f768cb648e1a85483216eadae0b101e Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 1 Oct 2024 10:18:09 +0200 Subject: [PATCH 2/5] Revert "ukify: add new --extend= switch for importing an existing UKI's sections to later extend" This reverts commit b6570095ce889b07242d36cd05fa1d1899d0bc6c. --- man/ukify.xml | 54 ------------------------------ src/ukify/ukify.py | 82 ++++++++++------------------------------------ 2 files changed, 17 insertions(+), 119 deletions(-) diff --git a/man/ukify.xml b/man/ukify.xml index 902736d4ed..35a2018120 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -228,18 +228,6 @@ - - - - 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 .text or similar. - - - - @@ -715,48 +703,6 @@ 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 kernel-install will perform signing using this config. - - - Multi-Profile PE - - First, create the base UKI: - $ 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 - - - Then, extend the base UKI with information about profile @0: - - $ ukify build \ - --extend=base.efi \ - --profile='TITLE=Base' \ - --output=base-with-profile-0.efi - - - Add a second profile (@1): - - $ 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 - - - Add a third profile (@2): - - $ 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 - - - The resulting UKI base-with-profile-0-1-2.efi will now contain three profiles. - diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 1a8c9507ee..e444204eb1 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -379,14 +379,7 @@ class UKI: sections: list[Section] = dataclasses.field(default_factory=list, init=False) def add_section(self, section): - 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:]]: + if section.name in [s.name for s in self.sections]: raise ValueError(f'Duplicate section {section.name}') self.sections += [section] @@ -498,10 +491,6 @@ def key_path_groups(opts): pp_groups) -def pe_strip_section_name(name): - return name.rstrip(b"\x00").decode() - - def call_systemd_measure(uki, opts): measure_tool = find_tool('systemd-measure', '/usr/lib/systemd/systemd-measure', @@ -639,9 +628,6 @@ 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()) @@ -678,8 +664,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[:n_original_sections]): - if pe_strip_section_name(s.Name) == section.name: + for i, s in enumerate(pe.sections): + if s.Name.rstrip(b"\x00").decode() == section.name: if new_section.Misc_VirtualSize > s.SizeOfRawData: raise PEError(f'Not enough space in existing section {section.name} to append new data.') @@ -715,7 +701,7 @@ def merge_sbat(input_pe: [pathlib.Path], input_text: [str]) -> str: continue for section in pe.sections: - if pe_strip_section_name(section.Name) == ".sbat": + if section.Name.rstrip(b"\x00").decode() == ".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.") @@ -797,28 +783,6 @@ 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 @@ -883,9 +847,6 @@ 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 ), @@ -916,22 +877,21 @@ def make_uki(opts): uki.add_section(Section.create('.linux', linux, measure=True, virtual_size=virtual_size)) - 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 + 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 @@ -1082,7 +1042,7 @@ def generate_keys(opts): def inspect_section(opts, section): - name = pe_strip_section_name(section.Name) + name = section.Name.rstrip(b"\x00").decode() # find the config for this section in opts and whether to show it config = opts.sections_by_name.get(name, None) @@ -1422,14 +1382,6 @@ 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( '--pcr-banks', metavar = 'BANK…', @@ -1734,7 +1686,7 @@ def finalize_options(opts): opts.efi_arch = guess_efi_arch() 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') else: opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub') From db0f9720d62f4d2930b09c08d98c07b4440cc9b0 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 1 Oct 2024 10:24:30 +0200 Subject: [PATCH 3/5] ukify: Introduce pe_strip_section_name() --- src/ukify/ukify.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index e444204eb1..2574b315dd 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -491,6 +491,10 @@ def key_path_groups(opts): pp_groups) +def pe_strip_section_name(name): + return name.rstrip(b"\x00").decode() + + def call_systemd_measure(uki, opts): measure_tool = find_tool('systemd-measure', '/usr/lib/systemd/systemd-measure', @@ -665,7 +669,7 @@ def pe_add_sections(uki: UKI, output: str): # 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: + 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 +705,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.") @@ -1042,7 +1046,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) From 4f1c5a832cabde43d4111d53e0574e1addb4c6f2 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 1 Oct 2024 14:23:50 +0200 Subject: [PATCH 4/5] ukify: Fix Profile config setting --- src/ukify/ukify.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 2574b315dd..625755dec1 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -1366,9 +1366,9 @@ CONFIG_ITEMS = [ ConfigItem( '--profile', - metavar='TEST|@PATH', - help='Profile information [.profile section]', - config_key = 'UKI/Uname', + metavar = 'TEST|@PATH', + help = 'Profile information [.profile section]', + config_key = 'UKI/Profile', ), ConfigItem( From 16020c3324215e0fc1bd8621d69dab33730bec77 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 1 Oct 2024 14:30:15 +0200 Subject: [PATCH 5/5] 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. --- man/ukify.xml | 57 ++++++++++ src/ukify/ukify.py | 122 +++++++++++++++++---- test/TEST-86-MULTI-PROFILE-UKI/meson.build | 1 + 3 files changed, 161 insertions(+), 19 deletions(-) diff --git a/man/ukify.xml b/man/ukify.xml index 35a2018120..c78a12ada0 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -228,6 +228,19 @@ + + + + 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 ukify. 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 .text or similar. + + + + @@ -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 kernel-install will perform signing using this config. + + + Multi-Profile UKI + + First, create a few profiles: + + $ ukify build \ + --profile='TITLE=Base' \ + --output=profile0.efi + + + Add a second profile (@1): + + $ ukify build \ + --profile='TITLE=Boot into Storage Target Mode +ID=storagetm' \ + --cmdline='quiet rw rd.systemd.unit=stroage-target-mode.target' \ + --output=profile1.efi + + + Add a third profile (@2): + + $ ukify build \ + --profile='TITLE=Factory Reset +ID=factory-reset' \ + --cmdline='quiet rw systemd.unit=factory-reset.target' \ + --output=profile2.efi + + + Then, create a UKI and include all the generated profiles: + + $ 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 + + + The resulting UKI base-with-profile-0-1-2.efi will now contain three profiles. + + diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 625755dec1..20c1c5ca5e 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -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') diff --git a/test/TEST-86-MULTI-PROFILE-UKI/meson.build b/test/TEST-86-MULTI-PROFILE-UKI/meson.build index 53042884cc..10d5957d8f 100644 --- a/test/TEST-86-MULTI-PROFILE-UKI/meson.build +++ b/test/TEST-86-MULTI-PROFILE-UKI/meson.build @@ -6,5 +6,6 @@ integration_tests += [ 'storage' : 'persistent', 'vm' : true, 'firmware' : 'auto', + 'enabled' : false, }, ]