2 # SPDX-License-Identifier: LGPL-2.1-or-later
4 # This file is part of systemd.
6 # systemd is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation; either version 2.1 of the License, or
9 # (at your option) any later version.
11 # systemd is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with systemd; If not, see <https://www.gnu.org/licenses/>.
19 # pylint: disable=import-outside-toplevel,consider-using-with,unused-argument
20 # pylint: disable=unnecessary-lambda-assignment
46 from collections
.abc
import Iterable
, Iterator
, Sequence
47 from hashlib
import sha256
48 from pathlib
import Path
49 from types
import ModuleType
61 import pefile
# type: ignore
63 __version__
= '{{PROJECT_VERSION}} ({{VERSION_TAG}})'
66 # host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported]
67 'x86_64': ['x64', 'ia32'],
68 'i[3456]86': ['ia32'],
70 'armv[45678]*l': ['arm'],
71 'loongarch32': ['loongarch32'],
72 'loongarch64': ['loongarch64'],
73 'riscv32': ['riscv32'],
74 'riscv64': ['riscv64'],
76 EFI_ARCHES
: list[str] = sum(EFI_ARCH_MAP
.values(), [])
78 # Default configuration directories and file name.
79 # When the user does not specify one, the directories are searched in this order and the first file found is
81 DEFAULT_CONFIG_DIRS
= ['/etc/systemd', '/run/systemd', '/usr/local/lib/systemd', '/usr/lib/systemd']
82 DEFAULT_CONFIG_FILE
= 'ukify.conf'
86 bold
= '\033[0;1;39m' if sys
.stderr
.isatty() else ''
87 gray
= '\033[0;38;5;245m' if sys
.stderr
.isatty() else ''
88 red
= '\033[31;1m' if sys
.stderr
.isatty() else ''
89 yellow
= '\033[33;1m' if sys
.stderr
.isatty() else ''
90 reset
= '\033[0m' if sys
.stderr
.isatty() else ''
93 def guess_efi_arch() -> str:
94 arch
= os
.uname().machine
96 for glob
, mapping
in EFI_ARCH_MAP
.items():
97 if fnmatch
.fnmatch(arch
, glob
):
98 efi_arch
, *fallback
= mapping
101 raise ValueError(f
'Unsupported architecture {arch}')
103 # This makes sense only on some architectures, but it also probably doesn't
104 # hurt on others, so let's just apply the check everywhere.
106 fw_platform_size
= Path('/sys/firmware/efi/fw_platform_size')
108 size
= fw_platform_size
.read_text().strip()
109 except FileNotFoundError
:
113 efi_arch
= fallback
[0]
115 # print(f'Host arch {arch!r}, EFI arch {efi_arch!r}')
119 def page(text
: str, enabled
: Optional
[bool]) -> None:
121 # Initialize less options from $SYSTEMD_LESS or provide a suitable fallback.
122 os
.environ
['LESS'] = os
.getenv('SYSTEMD_LESS', 'FRSXMK')
128 def shell_join(cmd
: list[Union
[str, Path
]]) -> str:
129 # TODO: drop in favour of shlex.join once shlex.join supports Path.
130 return ' '.join(shlex
.quote(str(x
)) for x
in cmd
)
133 def round_up(x
: int, blocksize
: int = 4096) -> int:
134 return (x
+ blocksize
- 1) // blocksize
* blocksize
137 def try_import(modname
: str, name
: Optional
[str] = None) -> ModuleType
:
139 return __import__(modname
)
140 except ImportError as e
:
141 raise ValueError(f
'Kernel is compressed with {name or modname}, but module unavailable') from e
144 def read_env_file(text
: str) -> dict[str, str]:
147 for line
in text
.splitlines():
149 if not line
or line
.startswith('#'):
151 if m
:= re
.match(r
'([A-Z][A-Z_0-9]+)=(.*)', line
):
152 name
, val
= m
.groups()
153 if val
and val
[0] in '"\'':
154 val
= next(shlex
.shlex(val
, posix
=True))
158 print(f
'bad line {line!r}', file=sys
.stderr
)
163 def get_zboot_kernel(f
: IO
[bytes
]) -> bytes
:
164 """Decompress zboot efistub kernel if compressed. Return contents."""
165 # See linux/drivers/firmware/efi/libstub/Makefile.zboot
166 # and linux/drivers/firmware/efi/libstub/zboot-header.S
168 # 4 bytes at offset 0x08 contain the starting offset of compressed data
171 start
= struct
.unpack('<i', _start
)[0]
173 # Reading 4 bytes from address 0x0c is the size of compressed data,
174 # but it needs to be corrected according to the compressed type.
177 size
= struct
.unpack('<i', _sizes
)[0]
179 # Read 6 bytes from address 0x18, which is a nul-terminated
180 # string representing the compressed type.
182 comp_type
= f
.read(6)
184 if comp_type
.startswith(b
'gzip'):
185 gzip
= try_import('gzip')
186 return cast(bytes
, gzip
.open(f
).read(size
))
187 elif comp_type
.startswith(b
'lz4'):
188 lz4
= try_import('lz4.frame', 'lz4')
189 return cast(bytes
, lz4
.frame
.decompress(f
.read(size
)))
190 elif comp_type
.startswith(b
'lzma'):
191 lzma
= try_import('lzma')
192 return cast(bytes
, lzma
.open(f
).read(size
))
193 elif comp_type
.startswith(b
'lzo'):
194 raise NotImplementedError('lzo decompression not implemented')
195 elif comp_type
.startswith(b
'xzkern'):
196 raise NotImplementedError('xzkern decompression not implemented')
197 elif comp_type
.startswith(b
'zstd'):
198 zstd
= try_import('zstandard')
199 return cast(bytes
, zstd
.ZstdDecompressor().stream_reader(f
.read(size
)).read())
201 raise NotImplementedError(f
'unknown compressed type: {comp_type!r}')
204 def maybe_decompress(filename
: Union
[str, Path
]) -> bytes
:
205 """Decompress file if compressed. Return contents."""
206 f
= open(filename
, 'rb')
210 if start
.startswith(b
'\x7fELF'):
214 if start
.startswith(b
'MZ'):
217 if img_type
.startswith(b
'zimg'):
218 # zboot efistub kernel
219 return get_zboot_kernel(f
)
221 # not compressed aarch64 and riscv64
224 if start
.startswith(b
'\x1f\x8b'):
225 gzip
= try_import('gzip')
226 return cast(bytes
, gzip
.open(f
).read())
228 if start
.startswith(b
'\x28\xb5\x2f\xfd'):
229 zstd
= try_import('zstandard')
230 return cast(bytes
, zstd
.ZstdDecompressor().stream_reader(f
.read()).read())
232 if start
.startswith(b
'\x02\x21\x4c\x18'):
233 lz4
= try_import('lz4.frame', 'lz4')
234 return cast(bytes
, lz4
.frame
.decompress(f
.read()))
236 if start
.startswith(b
'\x04\x22\x4d\x18'):
237 print('Newer lz4 stream format detected! This may not boot!', file=sys
.stderr
)
238 lz4
= try_import('lz4.frame', 'lz4')
239 return cast(bytes
, lz4
.frame
.decompress(f
.read()))
241 if start
.startswith(b
'\x89LZO'):
242 # python3-lzo is not packaged for Fedora
243 raise NotImplementedError('lzo decompression not implemented')
245 if start
.startswith(b
'BZh'):
246 bz2
= try_import('bz2', 'bzip2')
247 return cast(bytes
, bz2
.open(f
).read())
249 if start
.startswith(b
'\x5d\x00\x00'):
250 lzma
= try_import('lzma')
251 return cast(bytes
, lzma
.open(f
).read())
253 raise NotImplementedError(f
'unknown file format (starts with {start!r})')
256 @dataclasses.dataclass
259 cmdline
: Union
[str, Path
, None]
261 devicetree_auto
: list[Path
]
266 join_profiles
: list[Path
]
267 sign_profiles
: list[str]
268 json
: Union
[Literal
['pretty'], Literal
['short'], Literal
['off']]
269 linux
: Optional
[Path
]
272 os_release
: Union
[str, Path
, None]
273 output
: Optional
[str]
275 pcr_private_keys
: list[str]
276 pcr_public_keys
: list[str]
277 pcr_certificates
: list[str]
278 pcrpkey
: Optional
[Path
]
279 pcrsig
: Union
[str, Path
, None]
280 join_pcrsig
: Optional
[Path
]
281 phase_path_groups
: Optional
[list[str]]
283 profile
: Optional
[str]
284 sb_cert
: Union
[str, Path
, None]
285 sb_cert_name
: Optional
[str]
286 sb_cert_validity
: int
288 sb_key
: Union
[str, Path
, None]
289 sbat
: Optional
[list[str]]
290 sections
: list['Section']
291 sections_by_name
: dict[str, 'Section']
292 sign_kernel
: Optional
[bool]
293 signing_engine
: Optional
[str]
294 signing_provider
: Optional
[str]
295 certificate_provider
: Optional
[str]
296 signtool
: Optional
[str]
297 splash
: Optional
[Path
]
303 files
: list[str] = dataclasses
.field(default_factory
=list)
306 def from_namespace(cls
, ns
: argparse
.Namespace
) -> 'UkifyConfig':
307 return cls(**{k
: v
for k
, v
in vars(ns
).items() if k
in inspect
.signature(cls
).parameters
})
311 # This class is here purely as a namespace for the functions
313 VERSION_PATTERN
= r
'(?P<version>[a-z0-9._+-]+) \([^ )]+\) (?:#.*)'
315 NOTES_PATTERN
= r
'^\s+Linux\s+0x[0-9a-f]+\s+OPEN\n\s+description data: (?P<version>[0-9a-f ]+)\s*$'
317 # Linux version 6.0.8-300.fc37.ppc64le (mockbuild@buildvm-ppc64le-03.iad2.fedoraproject.org)
318 # (gcc (GCC) 12.2.1 20220819 (Red Hat 12.2.1-2), GNU ld version 2.38-24.fc37)
319 # #1 SMP Fri Nov 11 14:39:11 UTC 2022
320 TEXT_PATTERN
= rb
'Linux version (?P<version>\d\.\S+) \('
323 def scrape_x86(cls
, filename
: Path
, opts
: Optional
[UkifyConfig
] = None) -> str:
324 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L136
325 # and https://docs.kernel.org/arch/x86/boot.html#the-real-mode-kernel-header
326 with
open(filename
, 'rb') as f
:
330 raise ValueError('Real-Mode Kernel Header magic not found')
332 offset
= f
.read(1)[0] + f
.read(1)[0] * 256 # Pointer to kernel version string
333 f
.seek(0x200 + offset
)
335 text
= text
.split(b
'\0', maxsplit
=1)[0]
336 decoded
= text
.decode()
338 if not (m
:= re
.match(cls
.VERSION_PATTERN
, decoded
)):
339 raise ValueError(f
'Cannot parse version-host-release uname string: {text!r}')
340 return m
.group('version')
343 def scrape_elf(cls
, filename
: Path
, opts
: Optional
[UkifyConfig
] = None) -> str:
344 readelf
= find_tool('readelf', opts
=opts
)
352 print('+', shell_join(cmd
), file=sys
.stderr
)
354 notes
= subprocess
.check_output(cmd
, stderr
=subprocess
.PIPE
, text
=True)
355 except subprocess
.CalledProcessError
as e
:
356 raise ValueError(e
.stderr
.strip()) from e
358 if not (m
:= re
.search(cls
.NOTES_PATTERN
, notes
, re
.MULTILINE
)):
359 raise ValueError('Cannot find Linux version note')
361 text
= ''.join(chr(int(c
, 16)) for c
in m
.group('version').split())
362 return text
.rstrip('\0')
365 def scrape_generic(cls
, filename
: Path
, opts
: Optional
[UkifyConfig
] = None) -> str:
367 # libarchive-c fails with
368 # ArchiveError: Unrecognized archive format (errno=84, retcode=-30, archive_p=94705420454656)
370 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L209
372 text
= maybe_decompress(filename
)
373 if not (m
:= re
.search(cls
.TEXT_PATTERN
, text
)):
374 raise ValueError(f
'Cannot find {cls.TEXT_PATTERN!r} in {filename}')
376 return m
.group('version').decode()
379 def scrape(cls
, filename
: Path
, opts
: Optional
[UkifyConfig
] = None) -> Optional
[str]:
380 for func
in (cls
.scrape_x86
, cls
.scrape_elf
, cls
.scrape_generic
):
382 version
= func(filename
, opts
=opts
)
383 print(f
'Found uname version: {version}', file=sys
.stderr
)
385 except ValueError as e
:
386 print(str(e
), file=sys
.stderr
)
390 DEFAULT_SECTIONS_TO_SHOW
= {
396 '.dtbauto': 'binary',
410 @dataclasses.dataclass
413 content
: Optional
[Path
]
414 tmpfile
: Optional
[IO
[Any
]] = None
415 measure
: bool = False
416 output_mode
: Optional
[str] = None
419 def create(cls
, name
: str, contents
: Union
[str, bytes
, Path
, None], **kwargs
: Any
) -> 'Section':
420 if isinstance(contents
, (str, bytes
)):
421 mode
= 'wt' if isinstance(contents
, str) else 'wb'
422 tmp
= tempfile
.NamedTemporaryFile(mode
=mode
, prefix
=f
'tmp{name}')
425 contents
= Path(tmp
.name
)
429 return cls(name
, contents
, tmpfile
=tmp
, **kwargs
)
432 def parse_input(cls
, s
: str) -> 'Section':
434 name
, contents
, *rest
= s
.split(':')
435 except ValueError as e
:
436 raise ValueError(f
'Cannot parse section spec (name or contents missing): {s!r}') from e
438 raise ValueError(f
'Cannot parse section spec (extraneous parameters): {s!r}')
440 if contents
.startswith('@'):
441 sec
= cls
.create(name
, Path(contents
[1:]))
443 sec
= cls
.create(name
, contents
)
449 def parse_output(cls
, s
: str) -> 'Section':
450 if not (m
:= re
.match(r
'([a-zA-Z0-9_.]+):(text|binary)(?:@(.+))?', s
)):
451 raise ValueError(f
'Cannot parse section spec: {s!r}')
453 name
, ttype
, out
= m
.groups()
454 out
= Path(out
) if out
else None
456 return cls
.create(name
, out
, output_mode
=ttype
)
458 def check_name(self
) -> None:
459 # PE section names with more than 8 characters are legal, but our stub does
461 if not self
.name
.isascii() or not self
.name
.isprintable():
462 raise ValueError(f
'Bad section name: {self.name!r}')
463 if len(self
.name
) > 8:
464 raise ValueError(f
'Section name too long: {self.name!r}')
467 @dataclasses.dataclass
470 sections
: list[Section
] = dataclasses
.field(default_factory
=list, init
=False)
472 def add_section(self
, section
: Section
) -> None:
475 # Start search at last .profile section, if there is one
476 for i
, s
in enumerate(self
.sections
):
477 if s
.name
== '.profile':
480 multiple_allowed_sections
= ['.dtbauto', '.efifw']
482 section
.name
== s
.name
for s
in self
.sections
[start
:] if s
.name
not in multiple_allowed_sections
484 raise ValueError(f
'Duplicate section {section.name}')
486 self
.sections
+= [section
]
491 def sign(input_f
: str, output_f
: str, opts
: UkifyConfig
) -> None:
492 raise NotImplementedError
495 def verify(input_f
: Path
, opts
: UkifyConfig
) -> bool:
496 raise NotImplementedError
499 def from_string(name
: str) -> type['SignTool']:
502 elif name
== 'sbsign':
504 elif name
== 'systemd-sbsign':
507 raise ValueError(f
'Invalid sign tool: {name!r}')
510 class PeSign(SignTool
):
512 def sign(input_f
: str, output_f
: str, opts
: UkifyConfig
) -> None:
513 assert opts
.sb_certdir
is not None
514 assert opts
.sb_cert_name
is not None
516 tool
= find_tool('pesign', opts
=opts
, msg
='pesign, required for signing, is not installed')
521 '-n', opts
.sb_certdir
,
522 '-c', opts
.sb_cert_name
,
527 print('+', shell_join(cmd
), file=sys
.stderr
)
528 subprocess
.check_call(cmd
)
531 def verify(input_f
: Path
, opts
: UkifyConfig
) -> bool:
532 assert input_f
is not None
534 tool
= find_tool('pesign', opts
=opts
)
535 cmd
= [tool
, '-i', input_f
, '-S']
537 print('+', shell_join(cmd
), file=sys
.stderr
)
538 info
= subprocess
.check_output(cmd
, text
=True)
540 return 'No signatures found.' in info
543 class SbSign(SignTool
):
545 def sign(input_f
: str, output_f
: str, opts
: UkifyConfig
) -> None:
546 assert opts
.sb_key
is not None
547 assert opts
.sb_cert
is not None
549 tool
= find_tool('sbsign', opts
=opts
, msg
='sbsign, required for signing, is not installed')
552 '--key', opts
.sb_key
,
553 '--cert', opts
.sb_cert
,
554 *(['--engine', opts
.signing_engine
] if opts
.signing_engine
is not None else []),
556 '--output', output_f
,
559 print('+', shell_join(cmd
), file=sys
.stderr
)
560 subprocess
.check_call(cmd
)
563 def verify(input_f
: Path
, opts
: UkifyConfig
) -> bool:
564 assert input_f
is not None
566 tool
= find_tool('sbverify', opts
=opts
)
567 cmd
= [tool
, '--list', input_f
]
569 print('+', shell_join(cmd
), file=sys
.stderr
)
570 info
= subprocess
.check_output(cmd
, text
=True)
572 return 'No signature table present' in info
575 class SystemdSbSign(SignTool
):
577 def sign(input_f
: str, output_f
: str, opts
: UkifyConfig
) -> None:
578 assert opts
.sb_key
is not None
579 assert opts
.sb_cert
is not None
583 '/usr/lib/systemd/systemd-sbsign',
585 msg
='systemd-sbsign, required for signing, is not installed',
590 '--private-key', opts
.sb_key
,
591 '--certificate', opts
.sb_cert
,
593 ['--private-key-source', f
'engine:{opts.signing_engine}']
594 if opts
.signing_engine
is not None
598 ['--private-key-source', f
'provider:{opts.signing_provider}']
599 if opts
.signing_provider
is not None
603 ['--certificate-source', f
'provider:{opts.certificate_provider}']
604 if opts
.certificate_provider
is not None
608 '--output', output_f
,
611 print('+', shell_join(cmd
), file=sys
.stderr
)
612 subprocess
.check_call(cmd
)
615 def verify(input_f
: Path
, opts
: UkifyConfig
) -> bool:
616 raise NotImplementedError('systemd-sbsign cannot yet verify if existing PE binaries are signed')
619 def parse_banks(s
: str) -> list[str]:
620 banks
= re
.split(r
',|\s+', s
)
621 # TODO: do some sanity checking here
635 def parse_phase_paths(s
: str) -> list[str]:
636 # Split on commas or whitespace here. Commas might be hard to parse visually.
637 paths
= re
.split(r
',|\s+', s
)
640 for phase
in path
.split(':'):
641 if phase
not in KNOWN_PHASES
:
642 raise argparse
.ArgumentTypeError(f
'Unknown boot phase {phase!r} ({path=})')
647 def check_splash(filename
: Optional
[Path
]) -> None:
651 # import is delayed, to avoid import when the splash image is not used
653 from PIL
import Image
657 img
= Image
.open(filename
, formats
=['BMP'])
658 print(f
'Splash image {filename} is {img.width}×{img.height} pixels', file=sys
.stderr
)
661 def check_inputs(opts
: UkifyConfig
) -> None:
662 for name
, value
in vars(opts
).items():
663 if name
in {'output', 'tools'}:
666 if isinstance(value
, Path
):
667 # Check that we can open the directory or file, or generate and exception
672 elif isinstance(value
, list):
674 if isinstance(item
, Path
):
680 check_splash(opts
.splash
)
683 def check_cert_and_keys_nonexistent(opts
: UkifyConfig
) -> None:
684 # Raise if any of the keys and certs are found on disk
685 paths
: Iterator
[Union
[str, Path
, None]] = itertools
.chain(
686 (opts
.sb_key
, opts
.sb_cert
),
687 *((priv_key
, pub_key
, cert
) for priv_key
, pub_key
, cert
, _
in key_path_groups(opts
)),
690 if path
and Path(path
).exists():
691 raise ValueError(f
'{path} is present')
696 fallback
: Optional
[str] = None,
697 opts
: Optional
[UkifyConfig
] = None,
698 msg
: str = 'Tool {name} not installed!',
699 ) -> Union
[str, Path
]:
700 if opts
and opts
.tools
:
706 if shutil
.which(name
) is not None:
710 raise ValueError(msg
.format(name
=name
))
715 def combine_signatures(pcrsigs
: list[dict[str, str]]) -> str:
716 combined
: collections
.defaultdict
[str, list[str]] = collections
.defaultdict(list)
717 for pcrsig
in pcrsigs
:
718 for bank
, sigs
in pcrsig
.items():
720 if sig
not in combined
[bank
]:
721 combined
[bank
] += [sig
]
722 return json
.dumps(combined
)
725 def key_path_groups(opts
: UkifyConfig
) -> Iterator
[tuple[str, Optional
[str], Optional
[str], Optional
[str]]]:
726 if not opts
.pcr_private_keys
:
729 n_priv
= len(opts
.pcr_private_keys
)
730 pub_keys
= opts
.pcr_public_keys
or []
731 certs
= opts
.pcr_certificates
or []
732 pp_groups
= opts
.phase_path_groups
or []
734 yield from itertools
.zip_longest(
735 opts
.pcr_private_keys
,
743 def pe_strip_section_name(name
: bytes
) -> str:
744 return name
.rstrip(b
'\x00').decode()
747 def pe_section_size(section
: pefile
.SectionStructure
) -> int:
748 return cast(int, min(section
.Misc_VirtualSize
, section
.SizeOfRawData
))
751 def call_systemd_measure(uki
: UKI
, opts
: UkifyConfig
, profile_start
: int = 0) -> str:
752 measure_tool
= find_tool(
754 '/usr/lib/systemd/systemd-measure',
759 banks
= opts
.pcr_banks
or ()
763 # First, pick up either the base sections or the profile specific sections we shall measure now
764 unique_to_measure
= {
765 s
.name
: s
for s
in uki
.sections
[profile_start
:] if s
.measure
and s
.name
!= '.dtbauto'
768 dtbauto_to_measure
: list[Optional
[Section
]] = [
769 s
for s
in uki
.sections
[profile_start
:] if s
.measure
and s
.name
== '.dtbauto'
772 if len(dtbauto_to_measure
) == 0:
773 dtbauto_to_measure
= [None]
775 # Then, if we're measuring a profile, lookup the missing sections from the base image.
776 if profile_start
!= 0:
777 for section
in uki
.sections
:
778 # If we reach the first .profile section the base is over
779 if section
.name
== '.profile':
782 # Only some sections are measured
783 if not section
.measure
:
786 # Check if this is a section we already covered above
787 if section
.name
in unique_to_measure
:
790 unique_to_measure
[section
.name
] = section
792 if opts
.measure
or opts
.policy_digest
:
794 to_measure
= unique_to_measure
.copy()
796 for dtbauto
in dtbauto_to_measure
:
797 if dtbauto
is not None:
798 to_measure
[dtbauto
.name
] = dtbauto
800 pp_groups
= opts
.phase_path_groups
or []
804 'calculate' if opts
.measure
else 'policy-digest',
807 *(f
'--{s.name.removeprefix(".")}={s.content}' for s
in to_measure
.values()),
808 *(f
'--bank={bank}' for bank
in banks
),
809 # For measurement, the keys are not relevant, so we can lump all the phase paths
810 # into one call to systemd-measure calculate.
811 *(f
'--phase={phase_path}' for phase_path
in itertools
.chain
.from_iterable(pp_groups
)),
814 # The JSON object will be used for offline signing, include the public key
815 # so that the fingerprint is included too. In case a certificate is passed, use the
816 # right parameter so that systemd-measure can extract the public key from it.
817 if opts
.policy_digest
:
818 if opts
.pcr_public_keys
:
819 cmd
+= ['--public-key', opts
.pcr_public_keys
[0]]
820 elif opts
.pcr_certificates
:
821 cmd
+= ['--certificate', opts
.pcr_certificates
[0]]
822 if opts
.certificate_provider
:
823 cmd
+= ['--certificate-source', f
'provider:{opts.certificate_provider}']
825 print('+', shell_join(cmd
), file=sys
.stderr
)
826 output
= subprocess
.check_output(cmd
, text
=True) # type: ignore
828 if opts
.policy_digest
:
829 pcrsig
= json
.loads(output
)
834 if opts
.policy_digest
:
835 combined
= combine_signatures(pcrsigs
)
836 # We need to ensure the section has space for signatures, that will be added separately later,
837 # so add some whitespace to pad the section. At most we'll need 4kb per digest (rsa4096).
838 # We might even check the key type given we have it to know the precise length, but don't
840 combined
+= ' ' * 1024 * combined
.count('"pol":')
841 uki
.add_section(Section
.create('.pcrsig', combined
))
845 if opts
.pcr_private_keys
:
847 to_measure
= unique_to_measure
.copy()
849 for dtbauto
in dtbauto_to_measure
:
850 if dtbauto
is not None:
851 to_measure
[dtbauto
.name
] = dtbauto
856 *(f
'--{s.name.removeprefix(".")}={s.content}' for s
in to_measure
.values()),
857 *(f
'--bank={bank}' for bank
in banks
),
860 for priv_key
, pub_key
, cert
, group
in key_path_groups(opts
):
861 extra
= [f
'--private-key={priv_key}']
862 if opts
.signing_engine
is not None:
863 assert pub_key
or cert
864 # Backward compatibility, we used to pass the public key as the certificate
865 # as there was no --pcr-certificate= parameter
867 f
'--private-key-source=engine:{opts.signing_engine}',
868 f
'--certificate={pub_key or cert}',
870 elif opts
.signing_provider
is not None:
871 assert pub_key
or cert
873 f
'--private-key-source=provider:{opts.signing_provider}',
874 f
'--certificate={pub_key or cert}',
877 extra
+= [f
'--certificate={cert}']
879 extra
+= [f
'--public-key={pub_key}']
881 if opts
.certificate_provider
is not None:
882 extra
+= [f
'--certificate-source=provider:{opts.certificate_provider}']
884 extra
+= [f
'--phase={phase_path}' for phase_path
in group
or ()]
886 print('+', shell_join(cmd
+ extra
), file=sys
.stderr
) # type: ignore
887 output
= subprocess
.check_output(cmd
+ extra
, text
=True) # type: ignore
888 pcrsig
= json
.loads(output
)
891 combined
= combine_signatures(pcrsigs
)
892 uki
.add_section(Section
.create('.pcrsig', combined
))
897 def join_initrds(initrds
: list[Path
]) -> Union
[Path
, bytes
, None]:
900 if len(initrds
) == 1:
905 initrd
= file.read_bytes()
907 padding
= b
'\0' * (round_up(n
, 4) - n
) # pad to 32 bit alignment
908 seq
+= [initrd
, padding
]
916 def pairwise(iterable
: Iterable
[T
]) -> Iterator
[tuple[T
, Optional
[T
]]]:
917 a
, b
= itertools
.tee(iterable
)
922 class PEError(Exception):
926 def pe_add_sections(opts
: UkifyConfig
, uki
: UKI
, output
: str) -> None:
927 pe
= pefile
.PE(uki
.executable
, fast_load
=True)
929 # Old stubs do not have the symbol/string table stripped, even though image files should not have one.
930 if symbol_table
:= pe
.FILE_HEADER
.PointerToSymbolTable
:
931 symbol_table_size
= 18 * pe
.FILE_HEADER
.NumberOfSymbols
932 if string_table_size
:= pe
.get_dword_from_offset(symbol_table
+ symbol_table_size
):
933 symbol_table_size
+= string_table_size
935 # Let's be safe and only strip it if it's at the end of the file.
936 if symbol_table
+ symbol_table_size
== len(pe
.__data
__):
937 pe
.__data
__ = pe
.__data
__[:symbol_table
]
938 pe
.FILE_HEADER
.PointerToSymbolTable
= 0
939 pe
.FILE_HEADER
.NumberOfSymbols
= 0
940 pe
.FILE_HEADER
.IMAGE_FILE_LOCAL_SYMS_STRIPPED
= True
942 # Old stubs might have been stripped, leading to unaligned raw data values, so let's fix them up here.
943 # pylint thinks that Structure doesn't have various members that it has…
944 # pylint: disable=no-member
946 for i
, section
in enumerate(pe
.sections
):
947 oldp
= section
.PointerToRawData
948 oldsz
= section
.SizeOfRawData
949 section
.PointerToRawData
= round_up(oldp
, pe
.OPTIONAL_HEADER
.FileAlignment
)
950 section
.SizeOfRawData
= round_up(oldsz
, pe
.OPTIONAL_HEADER
.FileAlignment
)
951 padp
= section
.PointerToRawData
- oldp
952 padsz
= section
.SizeOfRawData
- oldsz
954 for later_section
in pe
.sections
[i
+ 1 :]:
955 later_section
.PointerToRawData
+= padp
+ padsz
960 + pe
.__data
__[oldp
: oldp
+ oldsz
]
962 + pe
.__data
__[oldp
+ oldsz
:]
965 # We might not have any space to add new sections. Let's try our best to make some space by padding the
966 # SizeOfHeaders to a multiple of the file alignment. This is safe because the first section's data starts
967 # at a multiple of the file alignment, so all space before that is unused.
968 pe
.OPTIONAL_HEADER
.SizeOfHeaders
= round_up(
969 pe
.OPTIONAL_HEADER
.SizeOfHeaders
, pe
.OPTIONAL_HEADER
.FileAlignment
971 pe
= pefile
.PE(data
=pe
.write(), fast_load
=True)
973 # pefile has an hardcoded limit of 256MB, which is not enough when building an initrd with large firmware
974 # files and all kernel modules. See: https://github.com/erocarrera/pefile/issues/396
975 warnings
= pe
.get_warnings()
977 if 'VirtualSize is extremely large' in w
:
979 if 'VirtualAddress is beyond' in w
:
981 raise PEError(f
'pefile warnings treated as errors: {warnings}')
983 # When attaching signatures we are operating on an existing UKI which might be signed
985 security
= pe
.OPTIONAL_HEADER
.DATA_DIRECTORY
[
986 pefile
.DIRECTORY_ENTRY
['IMAGE_DIRECTORY_ENTRY_SECURITY']
988 if security
.VirtualAddress
!= 0:
989 # We could strip the signatures, but why would anyone sign the stub?
990 raise PEError('Stub image is signed, refusing')
992 # Remember how many sections originate from systemd-stub
993 n_original_sections
= len(pe
.sections
)
995 for section
in uki
.sections
:
996 new_section
= pefile
.SectionStructure(pe
.__IMAGE
_SECTION
_HEADER
_format
__, pe
=pe
)
997 new_section
.__unpack
__(b
'\0' * new_section
.sizeof())
999 offset
= pe
.sections
[-1].get_file_offset() + new_section
.sizeof()
1000 if offset
+ new_section
.sizeof() > pe
.OPTIONAL_HEADER
.SizeOfHeaders
:
1001 raise PEError(f
'Not enough header space to add section {section.name}.')
1003 assert section
.content
1004 data
= section
.content
.read_bytes()
1006 new_section
.set_file_offset(offset
)
1007 new_section
.Name
= section
.name
.encode()
1008 new_section
.Misc_VirtualSize
= len(data
)
1009 # Non-stripped stubs might still have an unaligned symbol table at the end, making their size
1010 # unaligned, so we make sure to explicitly pad the pointer to new sections to an aligned offset.
1011 new_section
.PointerToRawData
= round_up(len(pe
.__data
__), pe
.OPTIONAL_HEADER
.FileAlignment
)
1012 new_section
.SizeOfRawData
= round_up(len(data
), pe
.OPTIONAL_HEADER
.FileAlignment
)
1013 new_section
.VirtualAddress
= round_up(
1014 pe
.sections
[-1].VirtualAddress
+ pe
.sections
[-1].Misc_VirtualSize
,
1015 pe
.OPTIONAL_HEADER
.SectionAlignment
,
1018 new_section
.IMAGE_SCN_MEM_READ
= True
1019 if section
.name
== '.linux':
1020 # Old kernels that use EFI handover protocol will be executed inline.
1021 new_section
.IMAGE_SCN_CNT_CODE
= True
1023 new_section
.IMAGE_SCN_CNT_INITIALIZED_DATA
= True
1025 # Special case, mostly for .sbat: the stub will already have a .sbat section, but we want to append
1026 # the one from the kernel to it. It should be small enough to fit in the existing section, so just
1028 for i
, s
in enumerate(pe
.sections
[:n_original_sections
]):
1029 if pe_strip_section_name(s
.Name
) == section
.name
and section
.name
!= '.dtbauto':
1030 if new_section
.Misc_VirtualSize
> s
.SizeOfRawData
:
1031 raise PEError(f
'Not enough space in existing section {section.name} to append new data')
1033 padding
= bytes(new_section
.SizeOfRawData
- new_section
.Misc_VirtualSize
)
1035 pe
.__data
__[: s
.PointerToRawData
]
1038 + pe
.__data
__[pe
.sections
[i
+ 1].PointerToRawData
:]
1040 s
.SizeOfRawData
= new_section
.SizeOfRawData
1041 s
.Misc_VirtualSize
= new_section
.Misc_VirtualSize
1046 + bytes(new_section
.PointerToRawData
- len(pe
.__data
__))
1048 + bytes(new_section
.SizeOfRawData
- len(data
))
1051 pe
.FILE_HEADER
.NumberOfSections
+= 1
1052 pe
.OPTIONAL_HEADER
.SizeOfInitializedData
+= new_section
.Misc_VirtualSize
1053 pe
.__structures
__.append(new_section
)
1054 pe
.sections
.append(new_section
)
1056 # If there is a pre-signed JSON blob, we need to update the existing JSON, by appending the signature to
1057 # each corresponding digest object. We have built the unsigned UKI with enough space to fit the .sig
1058 # objects, so we can just replace the new signed JSON in the existing sections.
1060 signatures
= json
.loads(str(opts
.pcrsig
))
1061 for i
, section
in enumerate(pe
.sections
):
1062 if pe_strip_section_name(section
.Name
) == '.pcrsig':
1066 section
.PointerToRawData
: section
.PointerToRawData
+ section
.SizeOfRawData
1072 for (bank
, sigs
), (input_bank
, input_sigs
) in itertools
.product(
1073 j
.items(), signatures
.items()
1075 if input_bank
!= bank
:
1077 for sig
, input_sig
in itertools
.product(sigs
, input_sigs
):
1078 if sig
['pol'] == input_sig
['pol']:
1079 sig
['sig'] = input_sig
['sig']
1081 encoded
= json
.dumps(j
).encode()
1082 if len(encoded
) > section
.SizeOfRawData
:
1084 f
'Not enough space in existing section .pcrsig of size {section.SizeOfRawData} to append new data of size {len(encoded)}' # noqa: E501
1086 section
.Misc_VirtualSize
= len(encoded
)
1087 # bytes(n) results in an array of n zeroes
1088 padding
= bytes(section
.SizeOfRawData
- len(encoded
))
1090 pe
.__data
__[: section
.PointerToRawData
]
1093 + pe
.__data
__[section
.PointerToRawData
+ section
.SizeOfRawData
:]
1096 pe
.OPTIONAL_HEADER
.CheckSum
= 0
1097 pe
.OPTIONAL_HEADER
.SizeOfImage
= round_up(
1098 pe
.sections
[-1].VirtualAddress
+ pe
.sections
[-1].Misc_VirtualSize
,
1099 pe
.OPTIONAL_HEADER
.SectionAlignment
,
1105 def merge_sbat(input_pe
: list[Path
], input_text
: list[str]) -> str:
1110 pe
= pefile
.PE(f
, fast_load
=True)
1111 except pefile
.PEFormatError
:
1112 print(f
'{f} is not a valid PE file, not extracting SBAT section.', file=sys
.stderr
)
1115 for section
in pe
.sections
:
1116 if pe_strip_section_name(section
.Name
) == '.sbat':
1117 split
= section
.get_data().rstrip(b
'\x00').decode().splitlines()
1118 if not split
[0].startswith('sbat,'):
1119 print(f
'{f} does not contain a valid SBAT section, skipping.', file=sys
.stderr
)
1121 # Filter out the sbat line, we'll add it back later, there needs to be only one and it
1122 # needs to be first.
1125 for t
in input_text
:
1126 if t
.startswith('@'):
1127 t
= Path(t
[1:]).read_text()
1128 split
= t
.splitlines()
1129 if not split
[0].startswith('sbat,'):
1130 print(f
'{t} does not contain a valid SBAT section, skipping.', file=sys
.stderr
)
1135 'sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md\n'
1141 # Keep in sync with Device from src/boot/chid.h
1142 # uint32_t descriptor, EFI_GUID chid, uint32_t name_offset, uint32_t compatible_offset
1143 DEVICE_STRUCT_SIZE
= 4 + 16 + 4 + 4
1144 NULL_DEVICE
= b
'\0' * DEVICE_STRUCT_SIZE
1145 DEVICE_TYPE_DEVICETREE
= 1
1146 DEVICE_TYPE_UEFI_FW
= 2
1148 # Keep in sync with efifirmware.h
1149 FWHEADERMAGIC
= 'feeddead'
1150 EFIFW_HEADER_SIZE
= 4 + 4 + 4 + 4
1153 def device_make_descriptor(device_type
: int, size
: int) -> int:
1154 return (size
) |
(device_type
<< 28)
1158 offsets
: dict[str, int], devtype
: int, name
: str, compatible_or_fwid
: str, chids
: set[uuid
.UUID
]
1161 descriptor
= device_make_descriptor(devtype
, DEVICE_STRUCT_SIZE
)
1162 for chid
in sorted(chids
):
1163 data
+= struct
.pack('<I', descriptor
)
1164 data
+= chid
.bytes_le
1165 data
+= struct
.pack('<II', offsets
[name
], offsets
[compatible_or_fwid
])
1167 assert len(data
) == DEVICE_STRUCT_SIZE
* len(chids
)
1171 def pack_strings(strings
: set[str], base
: int) -> tuple[bytes
, dict[str, int]]:
1175 for string
in sorted(strings
):
1176 offsets
[string
] = base
+ len(blob
)
1177 blob
+= string
.encode('utf-8') + b
'\00'
1179 return (blob
, offsets
)
1182 def parse_hwid_dir(path
: Path
) -> bytes
:
1183 hwid_files
= path
.rglob('*.json')
1184 devstr_to_type
: dict[str, int] = {
1185 'devicetree': DEVICE_TYPE_DEVICETREE
,
1186 'uefi-fw': DEVICE_TYPE_UEFI_FW
,
1189 # all attributes in the mandatory attributes list must be present
1190 mandatory_attribute
= ['type', 'name', 'hwids']
1192 # at least one of the following attributes must be present
1193 one_of
= ['compatible', 'fwid']
1195 one_of_key_to_devtype
: dict[str, int] = {
1196 'compatible': DEVICE_TYPE_DEVICETREE
,
1197 'fwid': DEVICE_TYPE_UEFI_FW
,
1200 strings
: set[str] = set()
1201 devices
: collections
.defaultdict
[tuple[int, str, str], set[uuid
.UUID
]] = collections
.defaultdict(set)
1203 for hwid_file
in hwid_files
:
1204 data
= json
.loads(hwid_file
.read_text(encoding
='UTF-8'))
1206 for k
in mandatory_attribute
:
1208 raise ValueError(f
'hwid description file "{hwid_file}" does not contain "{k}"')
1210 if not any(key
in data
for key
in one_of
):
1211 required_keys
= ','.join(one_of
)
1212 raise ValueError(f
'hwid description file "{hwid_file}" must contain one of {required_keys}')
1214 # (devtype, name, compatible/fwid) pair uniquely identifies the device
1215 devtype
= devstr_to_type
[data
['type']]
1219 if one_of_key_to_devtype
[k
] != devtype
:
1221 f
'wrong attribute "{k}" for hwid description file "{hwid_file}", '
1222 'device type: "%s"' % devtype
1224 strings |
= {data
['name'], data
[k
]}
1225 devices
[(devtype
, data
['name'], data
[k
])] |
= {uuid
.UUID(u
) for u
in data
['hwids']}
1227 total_device_structs
= 1
1228 for dev
, uuids
in devices
.items():
1229 total_device_structs
+= len(uuids
)
1231 strings_blob
, offsets
= pack_strings(strings
, total_device_structs
* DEVICE_STRUCT_SIZE
)
1234 for (devtype
, name
, compatible_or_fwid
), uuids
in devices
.items():
1235 devices_blob
+= pack_device(offsets
, devtype
, name
, compatible_or_fwid
, uuids
)
1237 devices_blob
+= NULL_DEVICE
1239 return devices_blob
+ strings_blob
1242 def parse_efifw_dir(path
: Path
) -> bytes
:
1243 if not path
.is_dir():
1244 raise ValueError(f
'{path} is not a directory or it does not exist')
1246 # only one firmware image must be present in the directory
1247 # to uniquely identify that firmware with its ID.
1248 if len(list(path
.glob('*'))) != 1:
1249 raise ValueError(f
'{path} must contain exactly one firmware image file')
1252 for fw
in path
.iterdir():
1253 payload_blob
+= fw
.read_bytes()
1255 payload_len
= len(payload_blob
)
1256 if payload_len
== 0:
1257 raise ValueError(f
'{fw} is a zero byte file!')
1259 dirname
= path
.parts
[-1]
1260 # firmware id is the name of the directory the firmware bundle is in,
1261 # terminated by NULL.
1262 fwid
= b
'' + dirname
.encode() + b
'\0'
1263 fwid_len
= len(fwid
)
1264 magic
= bytes
.fromhex(FWHEADERMAGIC
)
1266 efifw_header_blob
= b
''
1267 efifw_header_blob
+= struct
.pack('<p', magic
)
1268 efifw_header_blob
+= struct
.pack('<I', EFIFW_HEADER_SIZE
)
1269 efifw_header_blob
+= struct
.pack('<I', fwid_len
)
1270 efifw_header_blob
+= struct
.pack('<I', payload_len
)
1273 efifw_blob
+= efifw_header_blob
+ fwid
+ payload_blob
1279 sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
1280 uki,1,UKI,uki,1,https://uapi-group.org/specifications/specs/unified_kernel_image/
1284 sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
1285 uki-addon,1,UKI Addon,addon,1,https://www.freedesktop.org/software/systemd/man/latest/systemd-stub.html
1289 def make_uki(opts
: UkifyConfig
) -> None:
1290 assert opts
.output
is not None
1292 # kernel payload signing
1294 sign_args_present
= opts
.sb_key
or opts
.sb_cert_name
1295 sign_kernel
= opts
.sign_kernel
1297 combined_sigs
= '{}'
1299 # On some distros, on some architectures, the vmlinuz is a gzip file, so we need to decompress it
1300 # if it's not a valid PE file, as it will fail to be booted by the firmware.
1303 pefile
.PE(linux
, fast_load
=True)
1304 except pefile
.PEFormatError
:
1306 decompressed
= maybe_decompress(linux
)
1307 except NotImplementedError:
1308 print(f
'{linux} is not a valid PE file and cannot be decompressed either', file=sys
.stderr
)
1310 print(f
'{linux} is compressed and cannot be loaded by UEFI, decompressing', file=sys
.stderr
)
1311 linux
= Path(tempfile
.NamedTemporaryFile(prefix
='linux-decompressed').name
)
1312 linux
.write_bytes(decompressed
)
1314 if linux
and sign_args_present
:
1315 assert opts
.signtool
is not None
1316 signtool
= SignTool
.from_string(opts
.signtool
)
1318 if sign_kernel
is None:
1319 # figure out if we should sign the kernel
1320 sign_kernel
= signtool
.verify(linux
, opts
)
1323 linux_signed
= tempfile
.NamedTemporaryFile(prefix
='linux-signed')
1324 signtool
.sign(os
.fspath(linux
), os
.fspath(Path(linux_signed
.name
)), opts
=opts
)
1325 linux
= Path(linux_signed
.name
)
1327 if opts
.uname
is None and linux
is not None:
1328 print('Kernel version not specified, starting autodetection 😖.', file=sys
.stderr
)
1329 opts
.uname
= Uname
.scrape(linux
, opts
=opts
)
1331 uki
= UKI(opts
.join_pcrsig
if opts
.join_pcrsig
else opts
.stub
)
1332 initrd
= join_initrds(opts
.initrd
)
1334 pcrpkey
: Union
[bytes
, Path
, None] = opts
.pcrpkey
1336 keyutil_tool
= find_tool('systemd-keyutil', '/usr/lib/systemd/systemd-keyutil')
1337 cmd
= [keyutil_tool
, 'public']
1339 if opts
.pcr_public_keys
and len(opts
.pcr_public_keys
) == 1:
1340 # If we're using an engine or provider, the public key will be an X.509 certificate.
1341 if opts
.signing_engine
or opts
.signing_provider
:
1342 cmd
+= ['--certificate', opts
.pcr_public_keys
[0]]
1343 if opts
.certificate_provider
:
1344 cmd
+= ['--certificate-source', f
'provider:{opts.certificate_provider}']
1346 print('+', shell_join(cmd
), file=sys
.stderr
)
1347 pcrpkey
= subprocess
.check_output(cmd
)
1349 pcrpkey
= Path(opts
.pcr_public_keys
[0])
1350 elif opts
.pcr_certificates
and len(opts
.pcr_certificates
) == 1:
1351 cmd
+= ['--certificate', opts
.pcr_certificates
[0]]
1352 if opts
.certificate_provider
:
1353 cmd
+= ['--certificate-source', f
'provider:{opts.certificate_provider}']
1355 print('+', shell_join(cmd
), file=sys
.stderr
)
1356 pcrpkey
= subprocess
.check_output(cmd
)
1357 elif opts
.pcr_private_keys
and len(opts
.pcr_private_keys
) == 1:
1358 cmd
+= ['--private-key', Path(opts
.pcr_private_keys
[0])]
1360 if opts
.signing_engine
:
1361 cmd
+= ['--private-key-source', f
'engine:{opts.signing_engine}']
1362 if opts
.signing_provider
:
1363 cmd
+= ['--private-key-source', f
'provider:{opts.signing_provider}']
1365 print('+', shell_join(cmd
), file=sys
.stderr
)
1366 pcrpkey
= subprocess
.check_output(cmd
)
1370 if opts
.hwids
is not None:
1371 hwids
= parse_hwid_dir(opts
.hwids
)
1374 # name, content, measure?
1375 ('.osrel', opts
.os_release
, True),
1376 ('.cmdline', opts
.cmdline
, True),
1377 ('.dtb', opts
.devicetree
, True),
1378 *(('.dtbauto', dtb
, True) for dtb
in opts
.devicetree_auto
),
1379 ('.hwids', hwids
, True),
1380 ('.uname', opts
.uname
, True),
1381 ('.splash', opts
.splash
, True),
1382 ('.pcrpkey', pcrpkey
, True),
1383 ('.linux', linux
, True),
1384 ('.initrd', initrd
, True),
1385 *(('.efifw', parse_efifw_dir(fw
), False) for fw
in opts
.efifw
),
1386 ('.ucode', opts
.microcode
, True),
1389 # If we're building a PE profile binary, the ".profile" section has to be the first one.
1390 if opts
.profile
and not opts
.join_profiles
:
1391 uki
.add_section(Section
.create('.profile', opts
.profile
, measure
=True))
1393 for name
, content
, measure
in sections
:
1395 uki
.add_section(Section
.create(name
, content
, measure
=measure
))
1397 # systemd-measure doesn't know about those extra sections
1398 for section
in opts
.sections
:
1399 uki
.add_section(section
)
1401 # Don't add a sbat section to profile PE binaries.
1402 if (opts
.join_profiles
or not opts
.profile
) and not opts
.pcrsig
:
1403 if linux
is not None:
1404 # Merge the .sbat sections from stub, kernel and parameter, so that revocation can be done on
1406 input_pes
= [opts
.stub
, linux
]
1408 opts
.sbat
= [STUB_SBAT
]
1410 # Addons don't use the stub so we add SBAT manually
1413 opts
.sbat
= [ADDON_SBAT
]
1414 uki
.add_section(Section
.create('.sbat', merge_sbat(input_pes
, opts
.sbat
), measure
=linux
is not None))
1416 # If we're building a UKI with additional profiles, the .profile section for the base profile has to be
1417 # the last one so that everything before it is shared between profiles. The only thing we don't share
1418 # between profiles is the .pcrsig section which is appended later and doesn't make sense to share.
1419 if opts
.profile
and opts
.join_profiles
:
1420 uki
.add_section(Section
.create('.profile', opts
.profile
, measure
=True))
1422 # PCR measurement and signing
1426 and (opts
.join_profiles
or not opts
.profile
)
1428 not opts
.sign_profiles
1429 or (opts
.profile
and read_env_file(opts
.profile
).get('ID') in opts
.sign_profiles
)
1432 combined_sigs
= call_systemd_measure(uki
, opts
=opts
)
1450 for profile
in opts
.join_profiles
:
1451 pe
= pefile
.PE(profile
, fast_load
=True)
1452 prev_len
= len(uki
.sections
)
1454 names
= [pe_strip_section_name(s
.Name
) for s
in pe
.sections
]
1455 names
= [n
for n
in names
if n
in to_import
]
1458 raise ValueError(f
'Found no valid sections in PE profile binary {profile}')
1460 if names
[0] != '.profile':
1462 f
'Expected .profile section as first valid section in PE profile binary {profile} but got {names[0]}' # noqa: E501
1465 if names
.count('.profile') > 1:
1466 raise ValueError(f
'Profile PE binary {profile} contains multiple .profile sections')
1468 for pesection
in pe
.sections
:
1469 n
= pe_strip_section_name(pesection
.Name
)
1471 if n
not in to_import
:
1475 f
"Copying section '{n}' from '{profile}': {pe_section_size(pesection)} bytes",
1479 Section
.create(n
, pesection
.get_data(length
=pe_section_size(pesection
)), measure
=True)
1482 if opts
.sign_profiles
:
1483 pesection
= next(s
for s
in pe
.sections
if pe_strip_section_name(s
.Name
) == '.profile')
1484 id = read_env_file(pesection
.get_data(length
=pe_section_size(pesection
)).decode()).get('ID')
1485 if not id or id not in opts
.sign_profiles
:
1486 print(f
'Not signing expected PCR measurements for "{id}" profile', file=sys
.stderr
)
1489 s
= call_systemd_measure(uki
, opts
=opts
, profile_start
=prev_len
)
1491 combined_sigs
= combine_signatures([json
.loads(combined_sigs
), json
.loads(s
)])
1495 if sign_args_present
:
1496 unsigned
= tempfile
.NamedTemporaryFile(prefix
='uki')
1497 unsigned_output
= unsigned
.name
1499 unsigned_output
= opts
.output
1501 pe_add_sections(opts
, uki
, unsigned_output
)
1505 if sign_args_present
:
1506 assert opts
.signtool
is not None
1507 signtool
= SignTool
.from_string(opts
.signtool
)
1509 signtool
.sign(os
.fspath(unsigned_output
), os
.fspath(opts
.output
), opts
)
1511 # We end up with no executable bits, let's reapply them
1512 os
.umask(umask
:= os
.umask(0))
1513 os
.chmod(opts
.output
, 0o777 & ~umask
)
1515 print(f
'Wrote {"signed" if sign_args_present else "unsigned"} {opts.output}', file=sys
.stderr
)
1516 if opts
.policy_digest
:
1517 print(combined_sigs
)
1520 @contextlib.contextmanager
1521 def temporary_umask(mask
: int) -> Iterator
[None]:
1522 # Drop <mask> bits from umask
1524 os
.umask(old | mask
)
1531 def generate_key_cert_pair(
1534 keylength
: int = 2048,
1535 ) -> tuple[bytes
, bytes
]:
1536 from cryptography
import x509
1537 from cryptography
.hazmat
.primitives
import hashes
, serialization
1538 from cryptography
.hazmat
.primitives
.asymmetric
import rsa
1540 # We use a keylength of 2048 bits. That is what Microsoft documents as
1541 # supported/expected:
1542 # https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-secure-boot-key-creation-and-management-guidance?view=windows-11#12-public-key-cryptography
1544 now
= datetime
.datetime
.now(datetime
.timezone
.utc
)
1546 key
= rsa
.generate_private_key(
1547 public_exponent
=65537,
1551 x509
.CertificateBuilder()
1553 x509
.Name([x509
.NameAttribute(x509
.oid
.NameOID
.COMMON_NAME
, common_name
)]),
1556 x509
.Name([x509
.NameAttribute(x509
.oid
.NameOID
.COMMON_NAME
, common_name
)]),
1562 now
+ datetime
.timedelta(days
=valid_days
),
1565 x509
.random_serial_number(),
1571 x509
.BasicConstraints(ca
=False, path_length
=None),
1576 algorithm
=hashes
.SHA256(),
1580 cert_pem
= cert
.public_bytes(
1581 encoding
=serialization
.Encoding
.PEM
,
1583 key_pem
= key
.private_bytes(
1584 encoding
=serialization
.Encoding
.PEM
,
1585 format
=serialization
.PrivateFormat
.TraditionalOpenSSL
,
1586 encryption_algorithm
=serialization
.NoEncryption(),
1589 return key_pem
, cert_pem
1592 def generate_priv_pub_key_pair(keylength
: int = 2048) -> tuple[bytes
, bytes
]:
1593 from cryptography
.hazmat
.primitives
import serialization
1594 from cryptography
.hazmat
.primitives
.asymmetric
import rsa
1596 key
= rsa
.generate_private_key(
1597 public_exponent
=65537,
1600 priv_key_pem
= key
.private_bytes(
1601 encoding
=serialization
.Encoding
.PEM
,
1602 format
=serialization
.PrivateFormat
.TraditionalOpenSSL
,
1603 encryption_algorithm
=serialization
.NoEncryption(),
1605 pub_key_pem
= key
.public_key().public_bytes(
1606 encoding
=serialization
.Encoding
.PEM
,
1607 format
=serialization
.PublicFormat
.SubjectPublicKeyInfo
,
1610 return priv_key_pem
, pub_key_pem
1613 def generate_keys(opts
: UkifyConfig
) -> None:
1616 # This will generate keys and certificates and write them to the paths that
1617 # are specified as input paths.
1618 if opts
.sb_key
and opts
.sb_cert
:
1619 fqdn
= socket
.getfqdn()
1621 cn
= f
'SecureBoot signing key on host {fqdn}'
1623 # The length of CN must not exceed 64 bytes
1624 cn
= cn
[:61] + '...'
1626 key_pem
, cert_pem
= generate_key_cert_pair(
1628 valid_days
=opts
.sb_cert_validity
,
1630 print(f
'Writing SecureBoot private key to {opts.sb_key}', file=sys
.stderr
)
1631 with
temporary_umask(0o077):
1632 Path(opts
.sb_key
).write_bytes(key_pem
)
1633 print(f
'Writing SecureBoot certificate to {opts.sb_cert}', file=sys
.stderr
)
1634 Path(opts
.sb_cert
).write_bytes(cert_pem
)
1638 for priv_key
, pub_key
, _
, _
in key_path_groups(opts
):
1639 priv_key_pem
, pub_key_pem
= generate_priv_pub_key_pair()
1641 print(f
'Writing private key for PCR signing to {priv_key}', file=sys
.stderr
)
1642 with
temporary_umask(0o077):
1643 Path(priv_key
).write_bytes(priv_key_pem
)
1645 print(f
'Writing public key for PCR signing to {pub_key}', file=sys
.stderr
)
1646 Path(pub_key
).write_bytes(pub_key_pem
)
1652 'genkey: --secureboot-private-key=/--secureboot-certificate= or --pcr-private-key/--pcr-public-key must be specified' # noqa: E501
1656 def inspect_section(
1658 section
: pefile
.SectionStructure
,
1659 ) -> tuple[str, Optional
[dict[str, Union
[int, str]]]]:
1660 name
= pe_strip_section_name(section
.Name
)
1662 # find the config for this section in opts and whether to show it
1663 config
= opts
.sections_by_name
.get(name
, None)
1664 show
= config
or opts
.all
or (name
in DEFAULT_SECTIONS_TO_SHOW
and not opts
.sections
)
1668 ttype
= config
.output_mode
if config
else DEFAULT_SECTIONS_TO_SHOW
.get(name
, 'binary')
1670 size
= pe_section_size(section
)
1671 data
= section
.get_data(length
=size
)
1672 digest
= sha256(data
).hexdigest()
1674 struct
: dict[str, Union
[int, str]] = {
1681 struct
['text'] = data
.decode()
1682 except UnicodeDecodeError as e
:
1683 print(f
'Section {name!r} is not valid text: {e}', file=sys
.stderr
)
1684 struct
['text'] = '(not valid UTF-8)'
1686 if config
and config
.content
:
1687 assert isinstance(config
.content
, Path
)
1688 config
.content
.write_bytes(data
)
1690 if opts
.json
== 'off':
1691 print(f
'{name}:\n size: {size} bytes\n sha256: {digest}')
1693 text
= textwrap
.indent(cast(str, struct
['text']).rstrip(), ' ' * 4)
1694 print(f
' text:\n{text}')
1699 def inspect_sections(opts
: UkifyConfig
) -> None:
1700 indent
= 4 if opts
.json
== 'pretty' else None
1702 for file in opts
.files
:
1703 pe
= pefile
.PE(file, fast_load
=True)
1704 gen
= (inspect_section(opts
, section
) for section
in pe
.sections
)
1705 descs
= {key
: val
for (key
, val
) in gen
if val
}
1706 if opts
.json
!= 'off':
1707 json
.dump(descs
, sys
.stdout
, indent
=indent
)
1710 @dataclasses.dataclass(frozen
=True)
1713 def config_list_prepend(
1714 namespace
: argparse
.Namespace
,
1715 group
: Optional
[str],
1719 "Prepend value to namespace.<dest>"
1723 old
= getattr(namespace
, dest
, [])
1726 setattr(namespace
, dest
, value
+ old
)
1729 def config_set_if_unset(
1730 namespace
: argparse
.Namespace
,
1731 group
: Optional
[str],
1735 "Set namespace.<dest> to value only if it was None"
1739 if getattr(namespace
, dest
) is None:
1740 setattr(namespace
, dest
, value
)
1744 namespace
: argparse
.Namespace
,
1745 group
: Optional
[str],
1749 "Set namespace.<dest> to value only if it was None"
1753 setattr(namespace
, dest
, value
)
1756 def config_set_group(
1757 namespace
: argparse
.Namespace
,
1758 group
: Optional
[str],
1762 "Set namespace.<dest>[idx] to value, with idx derived from group"
1764 # pylint: disable=protected-access
1765 if group
not in namespace
._groups
:
1766 namespace
._groups
+= [group
]
1767 idx
= namespace
._groups
.index(group
)
1769 old
= getattr(namespace
, dest
, None)
1775 old
+ ([None] * (idx
- len(old
))) + [value
],
1779 def parse_boolean(s
: str) -> bool:
1780 "Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false"
1782 if s_l
in {'1', 'true', 'yes', 'y', 't', 'on'}:
1784 if s_l
in {'0', 'false', 'no', 'n', 'f', 'off'}:
1786 raise ValueError('f"Invalid boolean literal: {s!r}')
1788 # arguments for argparse.ArgumentParser.add_argument()
1789 name
: Union
[str, tuple[str, str]]
1790 dest
: Optional
[str] = None
1791 metavar
: Optional
[str] = None
1792 type: Optional
[Callable
[[str], Any
]] = None
1793 nargs
: Optional
[str] = None
1794 action
: Optional
[Union
[str, Callable
[[str], Any
], builtins
.type[argparse
.Action
]]] = None
1796 version
: Optional
[str] = None
1797 choices
: Optional
[tuple[str, ...]] = None
1798 const
: Optional
[Any
] = None
1799 help: Optional
[str] = None
1801 # metadata for config file parsing
1802 config_key
: Optional
[str] = None
1803 config_push
: Callable
[[argparse
.Namespace
, Optional
[str], str, Any
], None] = config_set_if_unset
1805 def _names(self
) -> tuple[str, ...]:
1806 return self
.name
if isinstance(self
.name
, tuple) else (self
.name
,)
1808 def argparse_dest(self
) -> str:
1809 # It'd be nice if argparse exported this, but I don't see that in the API
1812 return self
._names
()[0].lstrip('-').replace('-', '_')
1814 def add_to(self
, parser
: argparse
.ArgumentParser
) -> None:
1817 for key
in dataclasses
.asdict(self
)
1818 if (key
not in ('name', 'config_key', 'config_push') and (val
:= getattr(self
, key
)) is not None)
1820 args
= self
._names
()
1821 parser
.add_argument(*args
, **kwargs
)
1825 namespace
: argparse
.Namespace
,
1827 group
: Optional
[str],
1831 assert f
'{section}/{key}' == self
.config_key
1832 dest
= self
.argparse_dest()
1834 conv
: Callable
[[str], Any
]
1835 if self
.action
== argparse
.BooleanOptionalAction
:
1836 # We need to handle this case separately: the options are called
1837 # --foo and --no-foo, and no argument is parsed. But in the config
1838 # file, we have Foo=yes or Foo=no.
1839 conv
= self
.parse_boolean
1843 conv
= lambda s
: s
# noqa: E731
1845 # This is a bit ugly, but --initrd and --devicetree-auto are the only options
1846 # with multiple args on the command line and a space-separated list in the
1848 if self
.name
in ['--initrd', '--devicetree-auto']:
1849 value
= [conv(v
) for v
in value
.split()]
1853 self
.config_push(namespace
, group
, dest
, value
)
1855 def config_example(self
) -> tuple[Optional
[str], Optional
[str], Optional
[str]]:
1856 if not self
.config_key
:
1857 return None, None, None
1858 section_name
, key
= self
.config_key
.split('/', 1)
1859 if section_name
.endswith(':'):
1860 section_name
+= 'NAME'
1862 value
= '|'.join(self
.choices
)
1864 value
= self
.metavar
or self
.argparse_dest().upper()
1865 return (section_name
, key
, value
)
1868 VERBS
= ('build', 'genkey', 'inspect')
1875 help=argparse
.SUPPRESS
,
1880 version
=f
'ukify {__version__}',
1884 help='print parsed config and exit',
1885 action
='store_true',
1891 help='configuration file',
1896 help='vmlinuz file [.linux section]',
1897 config_key
='UKI/Linux',
1901 metavar
='TEXT|@PATH',
1902 help='path to os-release file [.osrel section]',
1903 config_key
='UKI/OSRelease',
1907 metavar
='TEXT|@PATH',
1908 help='kernel command line [.cmdline section]',
1909 config_key
='UKI/Cmdline',
1916 help='initrd file [part of .initrd section]',
1917 config_key
='UKI/Initrd',
1918 config_push
=ConfigItem
.config_list_prepend
,
1926 help='Directory with efi firmware binary file [.efifw section]',
1927 config_key
='UKI/Firmware',
1928 config_push
=ConfigItem
.config_list_prepend
,
1934 help='microcode file [.ucode section]',
1935 config_key
='UKI/Microcode',
1941 help='splash image bitmap file [.splash section]',
1942 config_key
='UKI/Splash',
1948 help='Device Tree file [.dtb section]',
1949 config_key
='UKI/DeviceTree',
1952 '--devicetree-auto',
1956 help='DeviceTree file for automatic selection [.dtbauto section]',
1958 config_key
='UKI/DeviceTreeAuto',
1959 config_push
=ConfigItem
.config_list_prepend
,
1965 help='Directory with HWID text files [.hwids section]',
1966 config_key
='UKI/HWIDs',
1971 help='"uname -r" information [.uname section]',
1972 config_key
='UKI/Uname',
1976 metavar
='TEXT|@PATH',
1977 help='SBAT policy [.sbat section]',
1980 config_key
='UKI/SBAT',
1986 help='embedded public key to seal secrets to [.pcrpkey section]',
1987 config_key
='UKI/PCRPKey',
1992 metavar
='NAME:TEXT|@PATH',
1995 help='section as name and contents [NAME section] or section to print',
1999 metavar
='TEST|@PATH',
2000 help='Profile information [.profile section]',
2001 config_key
='UKI/Profile',
2005 dest
='join_profiles',
2009 help='A PE binary containing an additional profile to add to the UKI',
2013 dest
='sign_profiles',
2017 help='Which profiles to sign expected PCR measurements for',
2021 metavar
='TEST|@PATH',
2022 help='Signed PCR policy JSON [.pcrsig section] to append to an existing UKI',
2023 config_key
='UKI/PCRSig',
2028 help='A PE binary containing a UKI without a .pcrsig to join with --pcrsig',
2033 choices
=('ia32', 'x64', 'arm', 'aa64', 'riscv32', 'riscv64', 'loongarch32', 'loongarch64'),
2034 help='target EFI architecture',
2035 config_key
='UKI/EFIArch',
2040 help='path to the sd-stub file [.text,.data,… sections]',
2041 config_key
='UKI/Stub',
2047 config_key
='UKI/PCRBanks',
2052 help='OpenSSL engine to use for signing',
2053 config_key
='UKI/SigningEngine',
2056 '--signing-provider',
2058 help='OpenSSL provider to use for signing',
2059 config_key
='UKI/SigningProvider',
2062 '--certificate-provider',
2064 help='OpenSSL provider to load certificate from',
2065 config_key
='UKI/CertificateProvider',
2069 choices
=('sbsign', 'pesign', 'systemd-sbsign'),
2072 'whether to use sbsign or pesign. It will also be inferred by the other '
2073 'parameters given: when using --secureboot-{private-key/certificate}, sbsign '
2074 'will be used, otherwise pesign will be used'
2076 config_key
='UKI/SecureBootSigningTool',
2079 '--secureboot-private-key',
2081 help='required by --signtool=sbsign|systemd-sbsign. Path to key file or engine/provider designation for SB signing', # noqa: E501
2082 config_key
='UKI/SecureBootPrivateKey',
2085 '--secureboot-certificate',
2088 'required by --signtool=sbsign. sbsign needs a path to certificate file or engine-specific designation for SB signing' # noqa: E501
2090 config_key
='UKI/SecureBootCertificate',
2093 '--secureboot-certificate-dir',
2095 default
='/etc/pki/pesign',
2097 'required by --signtool=pesign. Path to nss certificate database directory for PE signing. Default is /etc/pki/pesign' # noqa: E501
2099 config_key
='UKI/SecureBootCertificateDir',
2100 config_push
=ConfigItem
.config_set
,
2103 '--secureboot-certificate-name',
2104 dest
='sb_cert_name',
2106 'required by --signtool=pesign. pesign needs a certificate nickname of nss certificate database entry to use for PE signing' # noqa: E501
2108 config_key
='UKI/SecureBootCertificateName',
2111 '--secureboot-certificate-validity',
2114 dest
='sb_cert_validity',
2116 help="period of validity (in days) for a certificate created by 'genkey'",
2117 config_key
='UKI/SecureBootCertificateValidity',
2118 config_push
=ConfigItem
.config_set
,
2122 action
=argparse
.BooleanOptionalAction
,
2123 help='Sign the embedded kernel',
2124 config_key
='UKI/SignKernel',
2127 '--pcr-private-key',
2128 dest
='pcr_private_keys',
2130 help='private part of the keypair or engine/provider designation for signing PCR signatures',
2131 config_key
='PCRSignature:/PCRPrivateKey',
2132 config_push
=ConfigItem
.config_set_group
,
2136 dest
='pcr_public_keys',
2139 help='public part of the keypair or engine/provider designation for signing PCR signatures',
2140 config_key
='PCRSignature:/PCRPublicKey',
2141 config_push
=ConfigItem
.config_set_group
,
2144 '--pcr-certificate',
2145 dest
='pcr_certificates',
2148 help='certificate part of the keypair or engine/provider designation for signing PCR signatures',
2149 config_key
='PCRSignature:/PCRCertificate',
2150 config_push
=ConfigItem
.config_set_group
,
2154 dest
='phase_path_groups',
2155 metavar
='PHASE-PATH…',
2156 type=parse_phase_paths
,
2158 help='phase-paths to create signatures for',
2159 config_key
='PCRSignature:/Phases',
2160 config_push
=ConfigItem
.config_set_group
,
2166 help='Directories to search for tools (systemd-measure, …)',
2171 help='output file path',
2175 action
=argparse
.BooleanOptionalAction
,
2176 help='print systemd-measure output for the UKI',
2180 action
=argparse
.BooleanOptionalAction
,
2181 help='print systemd-measure policy digests for the UKI',
2185 choices
=('pretty', 'short', 'off'),
2187 help='generate JSON output',
2192 action
='store_const',
2194 help='equivalent to --json=pretty',
2198 help='print all sections',
2199 action
='store_true',
2203 CONFIGFILE_ITEMS
= {item
.config_key
: item
for item
in CONFIG_ITEMS
if item
.config_key
}
2206 def apply_config(namespace
: argparse
.Namespace
, filename
: Union
[str, Path
, None] = None) -> None:
2207 if filename
is None:
2208 if namespace
.config
:
2209 # Config set by the user, use that.
2210 filename
= namespace
.config
2211 print(f
'Using config file: {filename}', file=sys
.stderr
)
2213 # Try to look for a config file then use the first one found.
2214 for config_dir
in DEFAULT_CONFIG_DIRS
:
2215 filename
= Path(config_dir
) / DEFAULT_CONFIG_FILE
2216 if filename
.is_file():
2217 # Found a config file, use it.
2218 print(f
'Using found config file: {filename}', file=sys
.stderr
)
2221 # No config file specified or found, nothing to do.
2224 # Fill in ._groups based on --pcr-public-key=, --pcr-private-key=, and --phases=.
2225 assert '_groups' not in namespace
2226 n_pcr_priv
= len(namespace
.pcr_private_keys
or ())
2227 namespace
._groups
= list(range(n_pcr_priv
)) # pylint: disable=protected-access
2229 cp
= configparser
.ConfigParser(
2230 comment_prefixes
='#',
2231 inline_comment_prefixes
='#',
2233 empty_lines_in_values
=False,
2237 # Do not make keys lowercase
2238 cp
.optionxform
= lambda option
: option
# type: ignore
2240 # The API is not great.
2241 read
= cp
.read(filename
)
2243 raise OSError(f
'Failed to read {filename}')
2245 for section_name
, section
in cp
.items():
2246 idx
= section_name
.find(':')
2248 section_name
, group
= section_name
[: idx
+ 1], section_name
[idx
+ 1 :]
2249 if not section_name
or not group
:
2250 raise ValueError('Section name components cannot be empty')
2252 raise ValueError('Section name cannot contain more than one ":"')
2255 for key
, value
in section
.items():
2256 if item
:= CONFIGFILE_ITEMS
.get(f
'{section_name}/{key}'):
2257 item
.apply_config(namespace
, section_name
, group
, key
, value
)
2259 print(f
'Unknown config setting [{section_name}] {key}=', file=sys
.stderr
)
2262 def config_example() -> Iterator
[str]:
2263 prev_section
: Optional
[str] = None
2264 for item
in CONFIG_ITEMS
:
2265 section
, key
, value
= item
.config_example()
2267 if prev_section
!= section
:
2270 yield f
'[{section}]'
2271 prev_section
= section
2272 yield f
'{key} = {value}'
2275 class PagerHelpAction(argparse
._HelpAction
): # pylint: disable=protected-access
2278 parser
: argparse
.ArgumentParser
,
2279 namespace
: argparse
.Namespace
,
2280 values
: Union
[str, Sequence
[Any
], None] = None,
2281 option_string
: Optional
[str] = None,
2283 page(parser
.format_help(), True)
2287 def create_parser() -> argparse
.ArgumentParser
:
2288 p
= argparse
.ArgumentParser(
2289 description
='Build and sign Unified Kernel Images',
2291 + textwrap
.dedent("""\
2292 ukify {b}build{e} [--linux=LINUX] [--initrd=INITRD] [options…]
2293 ukify {b}genkey{e} [options…]
2294 ukify {b}inspect{e} FILE… [options…]
2295 """).format(b
=Style
.bold
, e
=Style
.reset
),
2298 epilog
='\n '.join(('config file:', *config_example())),
2299 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
2302 for item
in CONFIG_ITEMS
:
2305 # Suppress printing of usage synopsis on errors
2306 p
.error
= lambda message
: p
.exit(2, f
'{p.prog}: error: {message}\n') # type: ignore
2311 action
=PagerHelpAction
,
2312 help='show this help message and exit',
2318 def resolve_at_path(value
: Optional
[str]) -> Union
[Path
, str, None]:
2319 if value
and value
.startswith('@'):
2320 return Path(value
[1:])
2325 def finalize_options(opts
: argparse
.Namespace
) -> None:
2326 # Figure out which syntax is being used, one of:
2327 # ukify verb --arg --arg --arg
2328 # ukify linux initrd…
2329 if len(opts
.positional
) >= 1 and opts
.positional
[0] == 'inspect':
2330 opts
.verb
= opts
.positional
[0]
2331 opts
.files
= opts
.positional
[1:]
2333 raise ValueError('file(s) to inspect must be specified')
2334 if len(opts
.files
) > 1 and opts
.json
!= 'off':
2335 # We could allow this in the future, but we need to figure out the right structure
2336 raise ValueError('JSON output is not allowed with multiple files')
2337 elif len(opts
.positional
) == 1 and opts
.positional
[0] in VERBS
:
2338 opts
.verb
= opts
.positional
[0]
2339 elif opts
.linux
or opts
.initrd
:
2340 raise ValueError('--linux=/--initrd= options cannot be used with positional arguments')
2342 print("Assuming obsolete command line syntax with no verb. Please use 'build'.", file=sys
.stderr
)
2344 opts
.linux
= Path(opts
.positional
[0])
2345 # If we have initrds from parsing config files, append our positional args at the end
2346 opts
.initrd
= (opts
.initrd
or []) + [Path(arg
) for arg
in opts
.positional
[1:]]
2349 # Check that --pcr-public-key=, --pcr-private-key=, and --phases=
2350 # have either the same number of arguments or are not specified at all.
2351 # Also check that --pcr-public-key= and --pcr-certificate= are not set at the same time.
2352 # But allow a single public key, for offline PCR signing, to pre-populate the JSON object
2353 # with the certificate's fingerprint.
2354 n_pcr_cert
= None if opts
.pcr_certificates
is None else len(opts
.pcr_certificates
)
2355 n_pcr_pub
= None if opts
.pcr_public_keys
is None else len(opts
.pcr_public_keys
)
2356 n_pcr_priv
= None if opts
.pcr_private_keys
is None else len(opts
.pcr_private_keys
)
2357 n_phase_path_groups
= None if opts
.phase_path_groups
is None else len(opts
.phase_path_groups
)
2358 if opts
.policy_digest
and n_pcr_priv
is not None:
2359 raise ValueError('--pcr-private-key= cannot be specified with --policy-digest')
2362 and (n_pcr_pub
is None or n_pcr_pub
!= 1)
2363 and (n_pcr_cert
is None or n_pcr_cert
!= 1)
2365 raise ValueError('--policy-digest requires exactly one --pcr-public-key= or --pcr-certificate=')
2366 if n_pcr_pub
is not None and n_pcr_priv
is not None and n_pcr_pub
!= n_pcr_priv
:
2367 raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=')
2368 if n_pcr_cert
is not None and n_pcr_priv
is not None and n_pcr_cert
!= n_pcr_priv
:
2369 raise ValueError('--pcr-certificate= specifications must match --pcr-private-key=')
2370 if n_pcr_pub
is not None and n_pcr_cert
is not None:
2371 raise ValueError('--pcr-public-key= and --pcr-certificate= cannot be used at the same time')
2372 if n_phase_path_groups
is not None and n_phase_path_groups
!= n_pcr_priv
:
2373 raise ValueError('--phases= specifications must match --pcr-private-key=')
2375 opts
.cmdline
= resolve_at_path(opts
.cmdline
)
2377 if isinstance(opts
.cmdline
, str):
2378 # Drop whitespace from the command line. If we're reading from a file,
2379 # we copy the contents verbatim. But configuration specified on the command line
2380 # or in the config file may contain additional whitespace that has no meaning.
2381 opts
.cmdline
= ' '.join(opts
.cmdline
.split())
2383 opts
.os_release
= resolve_at_path(opts
.os_release
)
2385 if not opts
.os_release
and opts
.linux
:
2386 p
= Path('/etc/os-release')
2388 p
= Path('/usr/lib/os-release')
2391 if opts
.efi_arch
is None:
2392 opts
.efi_arch
= guess_efi_arch()
2394 if opts
.stub
is None and not opts
.join_pcrsig
:
2395 if opts
.linux
is not None:
2396 opts
.stub
= Path(f
'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
2398 opts
.stub
= Path(f
'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub')
2400 if opts
.signing_engine
and opts
.signing_provider
:
2401 raise ValueError('Only one of --signing-engine= and --signing-provider= may be specified')
2403 if opts
.signing_engine
is None and opts
.signing_provider
is None and opts
.sb_key
:
2404 opts
.sb_key
= Path(opts
.sb_key
)
2406 if opts
.certificate_provider
is None and opts
.sb_cert
:
2407 opts
.sb_cert
= Path(opts
.sb_cert
)
2409 if bool(opts
.sb_key
) ^
bool(opts
.sb_cert
):
2410 # one param only given, sbsign needs both
2412 '--secureboot-private-key= and --secureboot-certificate= must be specified together'
2414 elif bool(opts
.sb_key
) and bool(opts
.sb_cert
):
2415 # both param given, infer sbsign and in case it was given, ensure signtool=sbsign
2416 if opts
.signtool
and opts
.signtool
not in ('sbsign', 'systemd-sbsign'):
2418 f
'Cannot provide --signtool={opts.signtool} with --secureboot-private-key= and --secureboot-certificate=' # noqa: E501
2420 if not opts
.signtool
:
2421 opts
.signtool
= 'sbsign'
2422 elif bool(opts
.sb_cert_name
):
2423 # sb_cert_name given, infer pesign and in case it was given, ensure signtool=pesign
2424 if opts
.signtool
and opts
.signtool
!= 'pesign':
2426 f
'Cannot provide --signtool={opts.signtool} with --secureboot-certificate-name='
2428 opts
.signtool
= 'pesign'
2430 if opts
.signing_provider
and opts
.signtool
!= 'systemd-sbsign':
2431 raise ValueError('--signing-provider= can only be used with --signtool=systemd-sbsign')
2433 if opts
.certificate_provider
and opts
.signtool
!= 'systemd-sbsign':
2434 raise ValueError('--certificate-provider= can only be used with --signtool=systemd-sbsign')
2436 if opts
.sign_kernel
and not opts
.sb_key
and not opts
.sb_cert_name
:
2438 '--sign-kernel requires either --secureboot-private-key= and --secureboot-certificate= (for sbsign) or --secureboot-certificate-name= (for pesign) to be specified' # noqa: E501
2441 opts
.profile
= resolve_at_path(opts
.profile
)
2442 if opts
.profile
and isinstance(opts
.profile
, Path
):
2443 opts
.profile
= opts
.profile
.read_text()
2445 if opts
.join_profiles
and not opts
.profile
:
2446 # If any additional profiles are added, we need a base profile as well so add one if
2447 # one wasn't explicitly provided
2448 opts
.profile
= 'ID=main'
2450 if opts
.pcrsig
and not opts
.join_pcrsig
:
2451 raise ValueError('--pcrsig requires --join-pcrsig')
2452 if opts
.join_pcrsig
and not opts
.pcrsig
:
2453 raise ValueError('--join-pcrsig requires --pcrsig')
2454 if opts
.pcrsig
and (
2458 or opts
.join_profiles
2467 or opts
.devicetree_auto
2468 or opts
.pcr_private_keys
2469 or opts
.pcr_public_keys
2470 or opts
.pcr_certificates
2472 raise ValueError('--pcrsig and --join-pcrsig cannot be used with other sections')
2474 opts
.pcrsig
= resolve_at_path(opts
.pcrsig
)
2475 if isinstance(opts
.pcrsig
, Path
):
2476 opts
.pcrsig
= opts
.pcrsig
.read_text()
2478 if opts
.verb
== 'build' and opts
.output
is None:
2479 if opts
.linux
is None:
2480 raise ValueError('--output= must be specified when building a PE addon')
2481 suffix
= '.efi' if opts
.sb_key
or opts
.sb_cert_name
else '.unsigned.efi'
2482 opts
.output
= opts
.linux
.name
+ suffix
2484 # Now that we know if we're inputting or outputting, really parse section config
2485 f
= Section
.parse_output
if opts
.verb
== 'inspect' else Section
.parse_input
2486 opts
.sections
= [f(s
) for s
in opts
.sections
]
2487 # A convenience dictionary to make it easy to look up sections
2488 opts
.sections_by_name
= {s
.name
: s
for s
in opts
.sections
}
2491 def parse_args(args
: Optional
[list[str]] = None) -> argparse
.Namespace
:
2492 opts
= create_parser().parse_args(args
)
2494 # argparse puts some unknown options in opts.positional. Make sure we don't
2495 # try to interpret something that is an option as a positional argument.
2496 if any((bad_opt
:= o
).startswith('-') for o
in opts
.positional
):
2497 raise ValueError(f
'Unknown option: {bad_opt.partition("=")[0]}')
2500 finalize_options(opts
)
2505 opts
= UkifyConfig
.from_namespace(parse_args())
2507 # TODO: replace pprint() with some fancy formatting.
2508 pprint
.pprint(vars(opts
))
2509 elif opts
.verb
== 'build':
2512 elif opts
.verb
== 'genkey':
2513 check_cert_and_keys_nonexistent(opts
)
2515 elif opts
.verb
== 'inspect':
2516 inspect_sections(opts
)
2521 if __name__
== '__main__':