]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/ukify/ukify.py
ukify: move verb mangling to finalize_options()
[thirdparty/systemd.git] / src / ukify / ukify.py
CommitLineData
f4780cbe 1#!/usr/bin/env python3
eb81a60c 2# SPDX-License-Identifier: LGPL-2.1-or-later
d9c8f075
ZJS
3#
4# This file is part of systemd.
5#
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.
10#
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.
15#
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/>.
f4780cbe
ZJS
18
19# pylint: disable=missing-docstring,invalid-name,import-outside-toplevel
20# pylint: disable=consider-using-with,unspecified-encoding,line-too-long
21# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
c4fc2546
ZJS
22# pylint: disable=too-many-branches,too-many-lines,too-many-instance-attributes
23# pylint: disable=too-many-arguments,unnecessary-lambda-assignment,fixme
24# pylint: disable=unused-argument
f4780cbe
ZJS
25
26import argparse
5143a47a 27import configparser
f4780cbe
ZJS
28import collections
29import dataclasses
30import fnmatch
31import itertools
32import json
33import os
34import pathlib
5143a47a 35import pprint
6fa79138 36import pydoc
f4780cbe
ZJS
37import re
38import shlex
c8add4c2 39import shutil
f4780cbe 40import subprocess
5143a47a 41import sys
f4780cbe 42import tempfile
a758f95c
ZJS
43from typing import (Any,
44 Callable,
45 IO,
46 Optional,
6fa79138 47 Sequence,
a758f95c 48 Union)
f4780cbe 49
a758f95c 50import pefile # type: ignore
f4780cbe 51
67b65ac6 52__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
30ec2eae 53
f4780cbe 54EFI_ARCH_MAP = {
7081db29
ZJS
55 # host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported]
56 'x86_64' : ['x64', 'ia32'],
57 'i[3456]86' : ['ia32'],
58 'aarch64' : ['aa64'],
59 'arm[45678]*l' : ['arm'],
60 'loongarch32' : ['loongarch32'],
61 'loongarch64' : ['loongarch64'],
62 'riscv32' : ['riscv32'],
63 'riscv64' : ['riscv64'],
f4780cbe
ZJS
64}
65EFI_ARCHES: list[str] = sum(EFI_ARCH_MAP.values(), [])
66
67def guess_efi_arch():
68 arch = os.uname().machine
69
70 for glob, mapping in EFI_ARCH_MAP.items():
71 if fnmatch.fnmatch(arch, glob):
72 efi_arch, *fallback = mapping
73 break
74 else:
75 raise ValueError(f'Unsupported architecture {arch}')
76
77 # This makes sense only on some architectures, but it also probably doesn't
78 # hurt on others, so let's just apply the check everywhere.
79 if fallback:
80 fw_platform_size = pathlib.Path('/sys/firmware/efi/fw_platform_size')
81 try:
82 size = fw_platform_size.read_text().strip()
83 except FileNotFoundError:
84 pass
85 else:
86 if int(size) == 32:
87 efi_arch = fallback[0]
88
89 print(f'Host arch {arch!r}, EFI arch {efi_arch!r}')
90 return efi_arch
91
92
6fa79138
ZJS
93def page(text: str, enabled: Optional[bool]) -> None:
94 if enabled:
95 # Initialize less options from $SYSTEMD_LESS or provide a suitable fallback.
96 os.environ['LESS'] = os.getenv('SYSTEMD_LESS', 'FRSXMK')
97 pydoc.pager(text)
98 else:
99 print(text)
100
101
f4780cbe
ZJS
102def shell_join(cmd):
103 # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
104 return ' '.join(shlex.quote(str(x)) for x in cmd)
105
106
f4780cbe
ZJS
107def round_up(x, blocksize=4096):
108 return (x + blocksize - 1) // blocksize * blocksize
109
110
667578bb
ZJS
111def try_import(modname, name=None):
112 try:
113 return __import__(modname)
114 except ImportError as e:
115 raise ValueError(f'Kernel is compressed with {name or modname}, but module unavailable') from e
116
117
483c9c1b
ZJS
118def maybe_decompress(filename):
119 """Decompress file if compressed. Return contents."""
120 f = open(filename, 'rb')
121 start = f.read(4)
122 f.seek(0)
123
124 if start.startswith(b'\x7fELF'):
125 # not compressed
126 return f.read()
127
bf9f07a6
TP
128 if start.startswith(b'MZ'):
129 # not compressed aarch64 and riscv64
130 return f.read()
131
483c9c1b 132 if start.startswith(b'\x1f\x8b'):
667578bb 133 gzip = try_import('gzip')
483c9c1b
ZJS
134 return gzip.open(f).read()
135
136 if start.startswith(b'\x28\xb5\x2f\xfd'):
667578bb 137 zstd = try_import('zstd')
483c9c1b
ZJS
138 return zstd.uncompress(f.read())
139
140 if start.startswith(b'\x02\x21\x4c\x18'):
667578bb 141 lz4 = try_import('lz4.frame', 'lz4')
483c9c1b
ZJS
142 return lz4.frame.decompress(f.read())
143
144 if start.startswith(b'\x04\x22\x4d\x18'):
145 print('Newer lz4 stream format detected! This may not boot!')
667578bb 146 lz4 = try_import('lz4.frame', 'lz4')
483c9c1b
ZJS
147 return lz4.frame.decompress(f.read())
148
149 if start.startswith(b'\x89LZO'):
150 # python3-lzo is not packaged for Fedora
151 raise NotImplementedError('lzo decompression not implemented')
152
153 if start.startswith(b'BZh'):
667578bb 154 bz2 = try_import('bz2', 'bzip2')
483c9c1b
ZJS
155 return bz2.open(f).read()
156
157 if start.startswith(b'\x5d\x00\x00'):
667578bb 158 lzma = try_import('lzma')
483c9c1b
ZJS
159 return lzma.open(f).read()
160
161 raise NotImplementedError(f'unknown file format (starts with {start})')
162
163
164class Uname:
165 # This class is here purely as a namespace for the functions
166
167 VERSION_PATTERN = r'(?P<version>[a-z0-9._-]+) \([^ )]+\) (?:#.*)'
168
169 NOTES_PATTERN = r'^\s+Linux\s+0x[0-9a-f]+\s+OPEN\n\s+description data: (?P<version>[0-9a-f ]+)\s*$'
170
171 # Linux version 6.0.8-300.fc37.ppc64le (mockbuild@buildvm-ppc64le-03.iad2.fedoraproject.org)
172 # (gcc (GCC) 12.2.1 20220819 (Red Hat 12.2.1-2), GNU ld version 2.38-24.fc37)
173 # #1 SMP Fri Nov 11 14:39:11 UTC 2022
174 TEXT_PATTERN = rb'Linux version (?P<version>\d\.\S+) \('
175
176 @classmethod
177 def scrape_x86(cls, filename, opts=None):
178 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L136
179 # and https://www.kernel.org/doc/html/latest/x86/boot.html#the-real-mode-kernel-header
180 with open(filename, 'rb') as f:
181 f.seek(0x202)
182 magic = f.read(4)
183 if magic != b'HdrS':
184 raise ValueError('Real-Mode Kernel Header magic not found')
185 f.seek(0x20E)
186 offset = f.read(1)[0] + f.read(1)[0]*256 # Pointer to kernel version string
187 f.seek(0x200 + offset)
188 text = f.read(128)
189 text = text.split(b'\0', maxsplit=1)[0]
190 text = text.decode()
191
192 if not (m := re.match(cls.VERSION_PATTERN, text)):
193 raise ValueError(f'Cannot parse version-host-release uname string: {text!r}')
194 return m.group('version')
195
196 @classmethod
197 def scrape_elf(cls, filename, opts=None):
198 readelf = find_tool('readelf', opts=opts)
199
200 cmd = [
201 readelf,
202 '--notes',
203 filename,
204 ]
205
206 print('+', shell_join(cmd))
33bdec18
ZJS
207 try:
208 notes = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
209 except subprocess.CalledProcessError as e:
210 raise ValueError(e.stderr.strip()) from e
483c9c1b
ZJS
211
212 if not (m := re.search(cls.NOTES_PATTERN, notes, re.MULTILINE)):
213 raise ValueError('Cannot find Linux version note')
214
215 text = ''.join(chr(int(c, 16)) for c in m.group('version').split())
216 return text.rstrip('\0')
217
218 @classmethod
219 def scrape_generic(cls, filename, opts=None):
220 # import libarchive
221 # libarchive-c fails with
222 # ArchiveError: Unrecognized archive format (errno=84, retcode=-30, archive_p=94705420454656)
223
224 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L209
225
226 text = maybe_decompress(filename)
227 if not (m := re.search(cls.TEXT_PATTERN, text)):
228 raise ValueError(f'Cannot find {cls.TEXT_PATTERN!r} in {filename}')
229
230 return m.group('version').decode()
231
232 @classmethod
233 def scrape(cls, filename, opts=None):
234 for func in (cls.scrape_x86, cls.scrape_elf, cls.scrape_generic):
235 try:
236 version = func(filename, opts=opts)
237 print(f'Found uname version: {version}')
238 return version
239 except ValueError as e:
240 print(str(e))
241 return None
242
243
f4780cbe
ZJS
244@dataclasses.dataclass
245class Section:
246 name: str
247 content: pathlib.Path
a758f95c 248 tmpfile: Optional[IO] = None
f4780cbe
ZJS
249 measure: bool = False
250
251 @classmethod
789a6427 252 def create(cls, name, contents, **kwargs):
09595fd5 253 if isinstance(contents, (str, bytes)):
54c84c8a
ZJS
254 mode = 'wt' if isinstance(contents, str) else 'wb'
255 tmp = tempfile.NamedTemporaryFile(mode=mode, prefix=f'tmp{name}')
f4780cbe
ZJS
256 tmp.write(contents)
257 tmp.flush()
258 contents = pathlib.Path(tmp.name)
259 else:
260 tmp = None
261
789a6427 262 return cls(name, contents, tmpfile=tmp, **kwargs)
f4780cbe
ZJS
263
264 @classmethod
265 def parse_arg(cls, s):
266 try:
267 name, contents, *rest = s.split(':')
268 except ValueError as e:
269 raise ValueError(f'Cannot parse section spec (name or contents missing): {s!r}') from e
270 if rest:
271 raise ValueError(f'Cannot parse section spec (extraneous parameters): {s!r}')
272
273 if contents.startswith('@'):
274 contents = pathlib.Path(contents[1:])
275
276 return cls.create(name, contents)
277
278 def size(self):
279 return self.content.stat().st_size
280
281 def check_name(self):
282 # PE section names with more than 8 characters are legal, but our stub does
283 # not support them.
284 if not self.name.isascii() or not self.name.isprintable():
285 raise ValueError(f'Bad section name: {self.name!r}')
286 if len(self.name) > 8:
287 raise ValueError(f'Section name too long: {self.name!r}')
288
289
290@dataclasses.dataclass
291class UKI:
a758f95c 292 executable: list[Union[pathlib.Path, str]]
f4780cbe 293 sections: list[Section] = dataclasses.field(default_factory=list, init=False)
f4780cbe
ZJS
294
295 def add_section(self, section):
f4780cbe
ZJS
296 if section.name in [s.name for s in self.sections]:
297 raise ValueError(f'Duplicate section {section.name}')
298
f4780cbe
ZJS
299 self.sections += [section]
300
301
302def parse_banks(s):
303 banks = re.split(r',|\s+', s)
304 # TODO: do some sanity checking here
305 return banks
306
307
308KNOWN_PHASES = (
309 'enter-initrd',
310 'leave-initrd',
311 'sysinit',
312 'ready',
313 'shutdown',
314 'final',
315)
316
317def parse_phase_paths(s):
318 # Split on commas or whitespace here. Commas might be hard to parse visually.
319 paths = re.split(r',|\s+', s)
320
321 for path in paths:
322 for phase in path.split(':'):
323 if phase not in KNOWN_PHASES:
324 raise argparse.ArgumentTypeError(f'Unknown boot phase {phase!r} ({path=})')
325
326 return paths
327
328
329def check_splash(filename):
330 if filename is None:
331 return
332
333 # import is delayed, to avoid import when the splash image is not used
334 try:
335 from PIL import Image
336 except ImportError:
337 return
338
339 img = Image.open(filename, formats=['BMP'])
340 print(f'Splash image {filename} is {img.width}×{img.height} pixels')
341
342
343def check_inputs(opts):
344 for name, value in vars(opts).items():
345 if name in {'output', 'tools'}:
346 continue
347
5143a47a
ZJS
348 if isinstance(value, pathlib.Path):
349 # Open file to check that we can read it, or generate an exception
350 value.open().close()
351 elif isinstance(value, list):
352 for item in value:
353 if isinstance(item, pathlib.Path):
354 item.open().close()
f4780cbe
ZJS
355
356 check_splash(opts.splash)
357
358
359def find_tool(name, fallback=None, opts=None):
360 if opts and opts.tools:
22ad038a
DDM
361 for d in opts.tools:
362 tool = d / name
363 if tool.exists():
364 return tool
f4780cbe 365
c8add4c2
JJ
366 if shutil.which(name) is not None:
367 return name
368
e673c5c2
EGE
369 if fallback is None:
370 print(f"Tool {name} not installed!")
f4780cbe 371
e673c5c2 372 return fallback
f4780cbe
ZJS
373
374def combine_signatures(pcrsigs):
375 combined = collections.defaultdict(list)
376 for pcrsig in pcrsigs:
377 for bank, sigs in pcrsig.items():
378 for sig in sigs:
379 if sig not in combined[bank]:
380 combined[bank] += [sig]
381 return json.dumps(combined)
382
383
9a1cb203
ZJS
384def key_path_groups(opts):
385 if not opts.pcr_private_keys:
386 return
387
388 n_priv = len(opts.pcr_private_keys or ())
389 pub_keys = opts.pcr_public_keys or [None] * n_priv
390 pp_groups = opts.phase_path_groups or [None] * n_priv
391
392 yield from zip(opts.pcr_private_keys,
393 pub_keys,
394 pp_groups)
395
396
f4780cbe
ZJS
397def call_systemd_measure(uki, linux, opts):
398 measure_tool = find_tool('systemd-measure',
399 '/usr/lib/systemd/systemd-measure',
400 opts=opts)
401
402 banks = opts.pcr_banks or ()
403
404 # PCR measurement
405
406 if opts.measure:
407 pp_groups = opts.phase_path_groups or []
408
409 cmd = [
410 measure_tool,
411 'calculate',
412 f'--linux={linux}',
413 *(f"--{s.name.removeprefix('.')}={s.content}"
414 for s in uki.sections
415 if s.measure),
416 *(f'--bank={bank}'
417 for bank in banks),
418 # For measurement, the keys are not relevant, so we can lump all the phase paths
419 # into one call to systemd-measure calculate.
420 *(f'--phase={phase_path}'
421 for phase_path in itertools.chain.from_iterable(pp_groups)),
422 ]
423
424 print('+', shell_join(cmd))
425 subprocess.check_call(cmd)
426
427 # PCR signing
428
429 if opts.pcr_private_keys:
f4780cbe
ZJS
430 pcrsigs = []
431
432 cmd = [
433 measure_tool,
434 'sign',
435 f'--linux={linux}',
436 *(f"--{s.name.removeprefix('.')}={s.content}"
437 for s in uki.sections
438 if s.measure),
439 *(f'--bank={bank}'
440 for bank in banks),
441 ]
442
9a1cb203 443 for priv_key, pub_key, group in key_path_groups(opts):
f4780cbe
ZJS
444 extra = [f'--private-key={priv_key}']
445 if pub_key:
446 extra += [f'--public-key={pub_key}']
447 extra += [f'--phase={phase_path}' for phase_path in group or ()]
448
449 print('+', shell_join(cmd + extra))
450 pcrsig = subprocess.check_output(cmd + extra, text=True)
451 pcrsig = json.loads(pcrsig)
452 pcrsigs += [pcrsig]
453
454 combined = combine_signatures(pcrsigs)
455 uki.add_section(Section.create('.pcrsig', combined))
456
457
54c84c8a 458def join_initrds(initrds):
a3b227d2 459 if not initrds:
09595fd5 460 return None
c4fc2546 461 if len(initrds) == 1:
09595fd5
DDM
462 return initrds[0]
463
464 seq = []
465 for file in initrds:
466 initrd = file.read_bytes()
c126c8ac
YW
467 n = len(initrd)
468 padding = b'\0' * (round_up(n, 4) - n) # pad to 32 bit alignment
09595fd5
DDM
469 seq += [initrd, padding]
470
471 return b''.join(seq)
54c84c8a
ZJS
472
473
c811aba0
DDM
474def pairwise(iterable):
475 a, b = itertools.tee(iterable)
476 next(b, None)
477 return zip(a, b)
478
479
7081db29 480class PEError(Exception):
3fc5eed4
JJ
481 pass
482
3fc1ae89 483
3fc5eed4
JJ
484def pe_add_sections(uki: UKI, output: str):
485 pe = pefile.PE(uki.executable, fast_load=True)
ac3412c3 486
7f72dca7
JJ
487 # Old stubs do not have the symbol/string table stripped, even though image files should not have one.
488 if symbol_table := pe.FILE_HEADER.PointerToSymbolTable:
489 symbol_table_size = 18 * pe.FILE_HEADER.NumberOfSymbols
490 if string_table_size := pe.get_dword_from_offset(symbol_table + symbol_table_size):
491 symbol_table_size += string_table_size
492
493 # Let's be safe and only strip it if it's at the end of the file.
494 if symbol_table + symbol_table_size == len(pe.__data__):
495 pe.__data__ = pe.__data__[:symbol_table]
496 pe.FILE_HEADER.PointerToSymbolTable = 0
497 pe.FILE_HEADER.NumberOfSymbols = 0
498 pe.FILE_HEADER.IMAGE_FILE_LOCAL_SYMS_STRIPPED = True
499
ac3412c3 500 # Old stubs might have been stripped, leading to unaligned raw data values, so let's fix them up here.
c4fc2546
ZJS
501 # pylint thinks that Structure doesn't have various members that it has…
502 # pylint: disable=no-member
503
ac3412c3
DDM
504 for i, section in enumerate(pe.sections):
505 oldp = section.PointerToRawData
506 oldsz = section.SizeOfRawData
507 section.PointerToRawData = round_up(oldp, pe.OPTIONAL_HEADER.FileAlignment)
508 section.SizeOfRawData = round_up(oldsz, pe.OPTIONAL_HEADER.FileAlignment)
509 padp = section.PointerToRawData - oldp
510 padsz = section.SizeOfRawData - oldsz
511
512 for later_section in pe.sections[i+1:]:
513 later_section.PointerToRawData += padp + padsz
514
515 pe.__data__ = pe.__data__[:oldp] + bytes(padp) + pe.__data__[oldp:oldp+oldsz] + bytes(padsz) + pe.__data__[oldp+oldsz:]
516
517 # We might not have any space to add new sections. Let's try our best to make some space by padding the
518 # SizeOfHeaders to a multiple of the file alignment. This is safe because the first section's data starts
519 # at a multiple of the file alignment, so all space before that is unused.
520 pe.OPTIONAL_HEADER.SizeOfHeaders = round_up(pe.OPTIONAL_HEADER.SizeOfHeaders, pe.OPTIONAL_HEADER.FileAlignment)
521 pe = pefile.PE(data=pe.write(), fast_load=True)
3fc1ae89 522
3fc5eed4
JJ
523 warnings = pe.get_warnings()
524 if warnings:
7081db29 525 raise PEError(f'pefile warnings treated as errors: {warnings}')
3fc1ae89 526
3fc5eed4
JJ
527 security = pe.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']]
528 if security.VirtualAddress != 0:
529 # We could strip the signatures, but why would anyone sign the stub?
7081db29 530 raise PEError('Stub image is signed, refusing.')
3fc5eed4
JJ
531
532 for section in uki.sections:
533 new_section = pefile.SectionStructure(pe.__IMAGE_SECTION_HEADER_format__, pe=pe)
534 new_section.__unpack__(b'\0' * new_section.sizeof())
535
536 offset = pe.sections[-1].get_file_offset() + new_section.sizeof()
537 if offset + new_section.sizeof() > pe.OPTIONAL_HEADER.SizeOfHeaders:
7081db29 538 raise PEError(f'Not enough header space to add section {section.name}.')
3fc5eed4
JJ
539
540 data = section.content.read_bytes()
541
542 new_section.set_file_offset(offset)
543 new_section.Name = section.name.encode()
544 new_section.Misc_VirtualSize = len(data)
ac3412c3
DDM
545 # Non-stripped stubs might still have an unaligned symbol table at the end, making their size
546 # unaligned, so we make sure to explicitly pad the pointer to new sections to an aligned offset.
547 new_section.PointerToRawData = round_up(len(pe.__data__), pe.OPTIONAL_HEADER.FileAlignment)
3fc5eed4
JJ
548 new_section.SizeOfRawData = round_up(len(data), pe.OPTIONAL_HEADER.FileAlignment)
549 new_section.VirtualAddress = round_up(
550 pe.sections[-1].VirtualAddress + pe.sections[-1].Misc_VirtualSize,
551 pe.OPTIONAL_HEADER.SectionAlignment,
552 )
553
554 new_section.IMAGE_SCN_MEM_READ = True
555 if section.name == '.linux':
556 # Old kernels that use EFI handover protocol will be executed inline.
557 new_section.IMAGE_SCN_CNT_CODE = True
558 else:
559 new_section.IMAGE_SCN_CNT_INITIALIZED_DATA = True
560
ac3412c3 561 pe.__data__ = pe.__data__[:] + bytes(new_section.PointerToRawData - len(pe.__data__)) + data + bytes(new_section.SizeOfRawData - len(data))
3fc5eed4
JJ
562
563 pe.FILE_HEADER.NumberOfSections += 1
564 pe.OPTIONAL_HEADER.SizeOfInitializedData += new_section.Misc_VirtualSize
565 pe.__structures__.append(new_section)
566 pe.sections.append(new_section)
567
568 pe.OPTIONAL_HEADER.CheckSum = 0
569 pe.OPTIONAL_HEADER.SizeOfImage = round_up(
570 pe.sections[-1].VirtualAddress + pe.sections[-1].Misc_VirtualSize,
571 pe.OPTIONAL_HEADER.SectionAlignment,
572 )
573
574 pe.write(output)
3fc1ae89 575
c1e8d172
EGE
576def signer_sign(cmd):
577 print('+', shell_join(cmd))
578 subprocess.check_call(cmd)
3fc1ae89 579
c1e8d172
EGE
580def find_sbsign(opts=None):
581 return find_tool('sbsign', opts=opts)
f4780cbe 582
c1e8d172
EGE
583def sbsign_sign(sbsign_tool, input_f, output_f, opts=None):
584 sign_invocation = [
f4780cbe
ZJS
585 sbsign_tool,
586 '--key', opts.sb_key,
587 '--cert', opts.sb_cert,
c1e8d172
EGE
588 input_f,
589 '--output', output_f,
f4780cbe 590 ]
f4780cbe 591 if opts.signing_engine is not None:
c1e8d172
EGE
592 sign_invocation += ['--engine', opts.signing_engine]
593 signer_sign(sign_invocation)
594
595def find_pesign(opts=None):
596 return find_tool('pesign', opts=opts)
597
598def pesign_sign(pesign_tool, input_f, output_f, opts=None):
599 sign_invocation = [
600 pesign_tool, '-s', '--force',
601 '-n', opts.sb_certdir,
602 '-c', opts.sb_cert_name,
603 '-i', input_f,
604 '-o', output_f,
605 ]
606 signer_sign(sign_invocation)
f4780cbe 607
c1e8d172
EGE
608SBVERIFY = {
609 'name': 'sbverify',
610 'option': '--list',
611 'output': 'No signature table present',
612}
f4780cbe 613
c1e8d172
EGE
614PESIGCHECK = {
615 'name': 'pesign',
616 'option': '-i',
617 'output': 'No signatures found.',
618 'flags': '-S'
619}
f4780cbe 620
c1e8d172
EGE
621def verify(tool, opts):
622 verify_tool = find_tool(tool['name'], opts=opts)
623 cmd = [
624 verify_tool,
625 tool['option'],
626 opts.linux,
627 ]
628 if 'flags' in tool:
629 cmd.append(tool['flags'])
630
631 print('+', shell_join(cmd))
632 info = subprocess.check_output(cmd, text=True)
633
634 return tool['output'] in info
635
636def make_uki(opts):
637 # kernel payload signing
638
639 sign_tool = None
640 if opts.signtool == 'sbsign':
641 sign_tool = find_sbsign(opts=opts)
642 sign = sbsign_sign
643 verify_tool = SBVERIFY
644 else:
645 sign_tool = find_pesign(opts=opts)
646 sign = pesign_sign
647 verify_tool = PESIGCHECK
648
649 sign_args_present = opts.sb_key or opts.sb_cert_name
f4780cbe 650
c1e8d172
EGE
651 if sign_tool is None and sign_args_present:
652 raise ValueError(f'{opts.signtool}, required for signing, is not installed')
653
654 sign_kernel = opts.sign_kernel
655 if sign_kernel is None and opts.linux is not None and sign_args_present:
656 # figure out if we should sign the kernel
657 sign_kernel = verify(verify_tool, opts)
f4780cbe
ZJS
658
659 if sign_kernel:
660 linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed')
8d89d66c 661 linux = pathlib.Path(linux_signed.name)
c1e8d172 662 sign(sign_tool, opts.linux, linux, opts=opts)
f4780cbe
ZJS
663 else:
664 linux = opts.linux
665
00e5933f 666 if opts.uname is None and opts.linux is not None:
483c9c1b
ZJS
667 print('Kernel version not specified, starting autodetection 😖.')
668 opts.uname = Uname.scrape(opts.linux, opts=opts)
669
f4780cbe 670 uki = UKI(opts.stub)
54c84c8a 671 initrd = join_initrds(opts.initrd)
f4780cbe 672
30fd9a2d 673 # TODO: derive public key from opts.pcr_private_keys?
f4780cbe
ZJS
674 pcrpkey = opts.pcrpkey
675 if pcrpkey is None:
676 if opts.pcr_public_keys and len(opts.pcr_public_keys) == 1:
677 pcrpkey = opts.pcr_public_keys[0]
678
679 sections = [
680 # name, content, measure?
681 ('.osrel', opts.os_release, True ),
682 ('.cmdline', opts.cmdline, True ),
683 ('.dtb', opts.devicetree, True ),
b6f2e686 684 ('.uname', opts.uname, True ),
f4780cbe
ZJS
685 ('.splash', opts.splash, True ),
686 ('.pcrpkey', pcrpkey, True ),
54c84c8a 687 ('.initrd', initrd, True ),
f4780cbe
ZJS
688
689 # linux shall be last to leave breathing room for decompression.
690 # We'll add it later.
691 ]
692
693 for name, content, measure in sections:
694 if content:
695 uki.add_section(Section.create(name, content, measure=measure))
696
697 # systemd-measure doesn't know about those extra sections
698 for section in opts.sections:
699 uki.add_section(section)
700
701 # PCR measurement and signing
702
703 call_systemd_measure(uki, linux, opts=opts)
704
c67d5a02 705 # UKI or addon creation - addons don't use the stub so we add SBAT manually
f4780cbe 706
00e5933f
LB
707 if linux is not None:
708 uki.add_section(Section.create('.linux', linux, measure=True))
c67d5a02
LB
709 elif opts.sbat:
710 uki.add_section(Section.create('.sbat', opts.sbat, measure=False))
f4780cbe 711
c1e8d172 712 if sign_args_present:
f4780cbe
ZJS
713 unsigned = tempfile.NamedTemporaryFile(prefix='uki')
714 output = unsigned.name
715 else:
716 output = opts.output
717
3fc5eed4 718 pe_add_sections(uki, output)
3fc1ae89 719
f4780cbe
ZJS
720 # UKI signing
721
c1e8d172
EGE
722 if sign_args_present:
723 sign(sign_tool, unsigned.name, opts.output, opts=opts)
f4780cbe
ZJS
724
725 # We end up with no executable bits, let's reapply them
726 os.umask(umask := os.umask(0))
727 os.chmod(opts.output, 0o777 & ~umask)
728
c1e8d172 729 print(f"Wrote {'signed' if sign_args_present else 'unsigned'} {opts.output}")
f4780cbe
ZJS
730
731
5143a47a
ZJS
732@dataclasses.dataclass(frozen=True)
733class ConfigItem:
734 @staticmethod
a758f95c
ZJS
735 def config_list_prepend(
736 namespace: argparse.Namespace,
737 group: Optional[str],
738 dest: str,
739 value: Any,
740 ) -> None:
5143a47a
ZJS
741 "Prepend value to namespace.<dest>"
742
743 assert not group
744
745 old = getattr(namespace, dest, [])
746 setattr(namespace, dest, value + old)
747
748 @staticmethod
a758f95c
ZJS
749 def config_set_if_unset(
750 namespace: argparse.Namespace,
751 group: Optional[str],
752 dest: str,
753 value: Any,
754 ) -> None:
5143a47a
ZJS
755 "Set namespace.<dest> to value only if it was None"
756
757 assert not group
758
759 if getattr(namespace, dest) is None:
760 setattr(namespace, dest, value)
761
762 @staticmethod
a758f95c
ZJS
763 def config_set_group(
764 namespace: argparse.Namespace,
765 group: Optional[str],
766 dest: str,
767 value: Any,
768 ) -> None:
5143a47a
ZJS
769 "Set namespace.<dest>[idx] to value, with idx derived from group"
770
c4fc2546 771 # pylint: disable=protected-access
5143a47a
ZJS
772 if group not in namespace._groups:
773 namespace._groups += [group]
774 idx = namespace._groups.index(group)
775
776 old = getattr(namespace, dest, None)
777 if old is None:
778 old = []
779 setattr(namespace, dest,
780 old + ([None] * (idx - len(old))) + [value])
781
782 @staticmethod
783 def parse_boolean(s: str) -> bool:
784 "Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false"
785 s_l = s.lower()
786 if s_l in {'1', 'true', 'yes', 'y', 't', 'on'}:
787 return True
788 if s_l in {'0', 'false', 'no', 'n', 'f', 'off'}:
789 return False
790 raise ValueError('f"Invalid boolean literal: {s!r}')
791
792 # arguments for argparse.ArgumentParser.add_argument()
a758f95c
ZJS
793 name: Union[str, tuple[str, str]]
794 dest: Optional[str] = None
795 metavar: Optional[str] = None
796 type: Optional[Callable] = None
797 nargs: Optional[str] = None
798 action: Optional[Union[str, Callable]] = None
799 default: Any = None
800 version: Optional[str] = None
801 choices: Optional[tuple[str, ...]] = None
802 help: Optional[str] = None
5143a47a
ZJS
803
804 # metadata for config file parsing
a758f95c
ZJS
805 config_key: Optional[str] = None
806 config_push: Callable[[argparse.Namespace, Optional[str], str, Any], None] = \
807 config_set_if_unset
5143a47a 808
a758f95c 809 def _names(self) -> tuple[str, ...]:
5143a47a
ZJS
810 return self.name if isinstance(self.name, tuple) else (self.name,)
811
812 def argparse_dest(self) -> str:
813 # It'd be nice if argparse exported this, but I don't see that in the API
814 if self.dest:
815 return self.dest
816 return self._names()[0].lstrip('-').replace('-', '_')
817
818 def add_to(self, parser: argparse.ArgumentParser):
819 kwargs = { key:val
820 for key in dataclasses.asdict(self)
821 if (key not in ('name', 'config_key', 'config_push') and
822 (val := getattr(self, key)) is not None) }
823 args = self._names()
824 parser.add_argument(*args, **kwargs)
825
826 def apply_config(self, namespace, section, group, key, value) -> None:
827 assert f'{section}/{key}' == self.config_key
828 dest = self.argparse_dest()
829
a758f95c 830 conv: Callable[[str], Any]
5143a47a
ZJS
831 if self.action == argparse.BooleanOptionalAction:
832 # We need to handle this case separately: the options are called
833 # --foo and --no-foo, and no argument is parsed. But in the config
834 # file, we have Foo=yes or Foo=no.
835 conv = self.parse_boolean
836 elif self.type:
837 conv = self.type
838 else:
839 conv = lambda s:s
840
a3b227d2
ZJS
841 # This is a bit ugly, but --initrd is the only option which is specified
842 # with multiple args on the command line and a space-separated list in the
843 # config file.
844 if self.name == '--initrd':
5143a47a
ZJS
845 value = [conv(v) for v in value.split()]
846 else:
847 value = conv(value)
848
849 self.config_push(namespace, group, dest, value)
850
a758f95c 851 def config_example(self) -> tuple[Optional[str], Optional[str], Optional[str]]:
5143a47a
ZJS
852 if not self.config_key:
853 return None, None, None
854 section_name, key = self.config_key.split('/', 1)
855 if section_name.endswith(':'):
856 section_name += 'NAME'
857 if self.choices:
858 value = '|'.join(self.choices)
859 else:
860 value = self.metavar or self.argparse_dest().upper()
861 return (section_name, key, value)
862
863
a3b227d2
ZJS
864VERBS = ('build',)
865
5143a47a 866CONFIG_ITEMS = [
a3b227d2
ZJS
867 ConfigItem(
868 'positional',
869 metavar = 'VERB',
870 nargs = '*',
871 help = f"operation to perform ({','.join(VERBS)})",
872 ),
873
5143a47a
ZJS
874 ConfigItem(
875 '--version',
876 action = 'version',
877 version = f'ukify {__version__}',
878 ),
879
880 ConfigItem(
881 '--summary',
882 help = 'print parsed config and exit',
883 action = 'store_true',
884 ),
885
886 ConfigItem(
a3b227d2 887 '--linux',
5143a47a 888 type = pathlib.Path,
5143a47a
ZJS
889 help = 'vmlinuz file [.linux section]',
890 config_key = 'UKI/Linux',
891 ),
892
893 ConfigItem(
a3b227d2
ZJS
894 '--initrd',
895 metavar = 'INITRD',
5143a47a 896 type = pathlib.Path,
a3b227d2
ZJS
897 action = 'append',
898 help = 'initrd file [part of .initrd section]',
5143a47a
ZJS
899 config_key = 'UKI/Initrd',
900 config_push = ConfigItem.config_list_prepend,
901 ),
902
903 ConfigItem(
904 ('--config', '-c'),
905 metavar = 'PATH',
906 help = 'configuration file',
907 ),
908
909 ConfigItem(
910 '--cmdline',
911 metavar = 'TEXT|@PATH',
912 help = 'kernel command line [.cmdline section]',
913 config_key = 'UKI/Cmdline',
914 ),
915
916 ConfigItem(
917 '--os-release',
918 metavar = 'TEXT|@PATH',
919 help = 'path to os-release file [.osrel section]',
920 config_key = 'UKI/OSRelease',
921 ),
922
923 ConfigItem(
924 '--devicetree',
925 metavar = 'PATH',
926 type = pathlib.Path,
927 help = 'Device Tree file [.dtb section]',
928 config_key = 'UKI/DeviceTree',
929 ),
930 ConfigItem(
931 '--splash',
932 metavar = 'BMP',
933 type = pathlib.Path,
934 help = 'splash image bitmap file [.splash section]',
935 config_key = 'UKI/Splash',
936 ),
937 ConfigItem(
938 '--pcrpkey',
939 metavar = 'KEY',
940 type = pathlib.Path,
941 help = 'embedded public key to seal secrets to [.pcrpkey section]',
942 config_key = 'UKI/PCRPKey',
943 ),
944 ConfigItem(
945 '--uname',
946 metavar='VERSION',
947 help='"uname -r" information [.uname section]',
948 config_key = 'UKI/Uname',
949 ),
950
951 ConfigItem(
952 '--efi-arch',
953 metavar = 'ARCH',
954 choices = ('ia32', 'x64', 'arm', 'aa64', 'riscv64'),
955 help = 'target EFI architecture',
956 config_key = 'UKI/EFIArch',
957 ),
958
959 ConfigItem(
960 '--stub',
961 type = pathlib.Path,
962 help = 'path to the sd-stub file [.text,.data,… sections]',
963 config_key = 'UKI/Stub',
964 ),
965
c67d5a02
LB
966 ConfigItem(
967 '--sbat',
968 metavar = 'TEXT|@PATH',
969 help = 'SBAT policy [.sbat section] for addons',
970 default = """sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
971uki.addon,1,UKI Addon,uki.addon,1,https://www.freedesktop.org/software/systemd/man/systemd-stub.html
972""",
973 config_key = 'Addon/SBAT',
974 ),
975
5143a47a
ZJS
976 ConfigItem(
977 '--section',
978 dest = 'sections',
979 metavar = 'NAME:TEXT|@PATH',
980 type = Section.parse_arg,
981 action = 'append',
982 default = [],
983 help = 'additional section as name and contents [NAME section]',
984 ),
985
986 ConfigItem(
987 '--pcr-banks',
988 metavar = 'BANK…',
989 type = parse_banks,
990 config_key = 'UKI/PCRBanks',
991 ),
992
993 ConfigItem(
994 '--signing-engine',
995 metavar = 'ENGINE',
996 help = 'OpenSSL engine to use for signing',
997 config_key = 'UKI/SigningEngine',
998 ),
c1e8d172
EGE
999 ConfigItem(
1000 '--signtool',
1001 choices = ('sbsign', 'pesign'),
1002 dest = 'signtool',
1003 default = 'sbsign',
1004 help = 'whether to use sbsign or pesign. Default is sbsign.',
1005 config_key = 'UKI/SecureBootSigningTool',
1006 ),
5143a47a
ZJS
1007 ConfigItem(
1008 '--secureboot-private-key',
1009 dest = 'sb_key',
c1e8d172 1010 help = 'required by --signtool=sbsign. Path to key file or engine-specific designation for SB signing',
5143a47a
ZJS
1011 config_key = 'UKI/SecureBootPrivateKey',
1012 ),
1013 ConfigItem(
1014 '--secureboot-certificate',
1015 dest = 'sb_cert',
c1e8d172 1016 help = 'required by --signtool=sbsign. sbsign needs a path to certificate file or engine-specific designation for SB signing',
5143a47a
ZJS
1017 config_key = 'UKI/SecureBootCertificate',
1018 ),
c1e8d172
EGE
1019 ConfigItem(
1020 '--secureboot-certificate-dir',
1021 dest = 'sb_certdir',
1022 default = '/etc/pki/pesign',
1023 help = 'required by --signtool=pesign. Path to nss certificate database directory for PE signing. Default is /etc/pki/pesign',
1024 config_key = 'UKI/SecureBootCertificateDir',
1025 ),
1026 ConfigItem(
1027 '--secureboot-certificate-name',
1028 dest = 'sb_cert_name',
1029 help = 'required by --signtool=pesign. pesign needs a certificate nickname of nss certificate database entry to use for PE signing',
1030 config_key = 'UKI/SecureBootCertificateName',
1031 ),
5143a47a
ZJS
1032
1033 ConfigItem(
1034 '--sign-kernel',
1035 action = argparse.BooleanOptionalAction,
1036 help = 'Sign the embedded kernel',
1037 config_key = 'UKI/SignKernel',
1038 ),
1039
1040 ConfigItem(
1041 '--pcr-private-key',
1042 dest = 'pcr_private_keys',
1043 metavar = 'PATH',
1044 type = pathlib.Path,
1045 action = 'append',
1046 help = 'private part of the keypair for signing PCR signatures',
1047 config_key = 'PCRSignature:/PCRPrivateKey',
1048 config_push = ConfigItem.config_set_group,
1049 ),
1050 ConfigItem(
1051 '--pcr-public-key',
1052 dest = 'pcr_public_keys',
1053 metavar = 'PATH',
1054 type = pathlib.Path,
1055 action = 'append',
1056 help = 'public part of the keypair for signing PCR signatures',
1057 config_key = 'PCRSignature:/PCRPublicKey',
1058 config_push = ConfigItem.config_set_group,
1059 ),
1060 ConfigItem(
1061 '--phases',
1062 dest = 'phase_path_groups',
1063 metavar = 'PHASE-PATH…',
1064 type = parse_phase_paths,
1065 action = 'append',
1066 help = 'phase-paths to create signatures for',
1067 config_key = 'PCRSignature:/Phases',
1068 config_push = ConfigItem.config_set_group,
1069 ),
1070
1071 ConfigItem(
1072 '--tools',
1073 type = pathlib.Path,
1074 action = 'append',
1075 help = 'Directories to search for tools (systemd-measure, …)',
1076 ),
1077
1078 ConfigItem(
1079 ('--output', '-o'),
1080 type = pathlib.Path,
1081 help = 'output file path',
1082 ),
1083
1084 ConfigItem(
1085 '--measure',
1086 action = argparse.BooleanOptionalAction,
1087 help = 'print systemd-measure output for the UKI',
1088 ),
1089]
1090
1091CONFIGFILE_ITEMS = { item.config_key:item
1092 for item in CONFIG_ITEMS
1093 if item.config_key }
1094
1095
1096def apply_config(namespace, filename=None):
1097 if filename is None:
1098 filename = namespace.config
1099 if filename is None:
1100 return
1101
1102 # Fill in ._groups based on --pcr-public-key=, --pcr-private-key=, and --phases=.
1103 assert '_groups' not in namespace
1104 n_pcr_priv = len(namespace.pcr_private_keys or ())
c4fc2546 1105 namespace._groups = list(range(n_pcr_priv)) # pylint: disable=protected-access
5143a47a
ZJS
1106
1107 cp = configparser.ConfigParser(
1108 comment_prefixes='#',
1109 inline_comment_prefixes='#',
1110 delimiters='=',
1111 empty_lines_in_values=False,
1112 interpolation=None,
1113 strict=False)
1114 # Do not make keys lowercase
1115 cp.optionxform = lambda option: option
1116
1117 cp.read(filename)
1118
1119 for section_name, section in cp.items():
1120 idx = section_name.find(':')
1121 if idx >= 0:
1122 section_name, group = section_name[:idx+1], section_name[idx+1:]
1123 if not section_name or not group:
1124 raise ValueError('Section name components cannot be empty')
1125 if ':' in group:
1126 raise ValueError('Section name cannot contain more than one ":"')
1127 else:
1128 group = None
1129 for key, value in section.items():
1130 if item := CONFIGFILE_ITEMS.get(f'{section_name}/{key}'):
1131 item.apply_config(namespace, section_name, group, key, value)
1132 else:
1133 print(f'Unknown config setting [{section_name}] {key}=')
1134
1135
1136def config_example():
1137 prev_section = None
1138 for item in CONFIG_ITEMS:
1139 section, key, value = item.config_example()
1140 if section:
1141 if prev_section != section:
1142 if prev_section:
1143 yield ''
1144 yield f'[{section}]'
1145 prev_section = section
1146 yield f'{key} = {value}'
1147
1148
6fa79138
ZJS
1149class PagerHelpAction(argparse._HelpAction): # pylint: disable=protected-access
1150 def __call__(
1151 self,
1152 parser: argparse.ArgumentParser,
1153 namespace: argparse.Namespace,
1154 values: Union[str, Sequence[Any], None] = None,
1155 option_string: Optional[str] = None
1156 ) -> None:
1157 page(parser.format_help(), True)
1158 parser.exit()
1159
1160
5143a47a 1161def create_parser():
f4780cbe
ZJS
1162 p = argparse.ArgumentParser(
1163 description='Build and sign Unified Kernel Images',
1164 allow_abbrev=False,
6fa79138 1165 add_help=False,
f4780cbe 1166 usage='''\
794385f5 1167ukify [options…] VERB
5143a47a
ZJS
1168''',
1169 epilog='\n '.join(('config file:', *config_example())),
1170 formatter_class=argparse.RawDescriptionHelpFormatter,
1171 )
1172
1173 for item in CONFIG_ITEMS:
1174 item.add_to(p)
f4780cbe
ZJS
1175
1176 # Suppress printing of usage synopsis on errors
1177 p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
1178
6fa79138
ZJS
1179 # Make --help paged
1180 p.add_argument(
1181 '-h', '--help',
1182 action=PagerHelpAction,
1183 help='show this help message and exit',
1184 )
1185
5143a47a 1186 return p
f4780cbe 1187
095ff238 1188
5143a47a 1189def finalize_options(opts):
b09a5315
ZJS
1190 # Figure out which syntax is being used, one of:
1191 # ukify verb --arg --arg --arg
1192 # ukify linux initrd…
1193 if len(opts.positional) == 1 and opts.positional[0] in VERBS:
1194 opts.verb = opts.positional[0]
1195 elif opts.linux or opts.initrd:
1196 raise ValueError('--linux/--initrd options cannot be used with positional arguments')
1197 else:
1198 print("Assuming obsolete commandline syntax with no verb. Please use 'build'.")
1199 if opts.positional:
1200 opts.linux = pathlib.Path(opts.positional[0])
1201 # If we have initrds from parsing config files, append our positional args at the end
1202 opts.initrd = (opts.initrd or []) + [pathlib.Path(arg) for arg in opts.positional[1:]]
1203 opts.verb = 'build'
1204
1205 # Check that --pcr-public-key=, --pcr-private-key=, and --phases=
1206 # have either the same number of arguments are are not specified at all.
1207 n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys)
1208 n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys)
1209 n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups)
1210 if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv:
1211 raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=')
1212 if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv:
1213 raise ValueError('--phases= specifications must match --pcr-private-key=')
1214
f4780cbe 1215 if opts.cmdline and opts.cmdline.startswith('@'):
5143a47a
ZJS
1216 opts.cmdline = pathlib.Path(opts.cmdline[1:])
1217 elif opts.cmdline:
1218 # Drop whitespace from the commandline. If we're reading from a file,
1219 # we copy the contents verbatim. But configuration specified on the commandline
1220 # or in the config file may contain additional whitespace that has no meaning.
1221 opts.cmdline = ' '.join(opts.cmdline.split())
1222
1223 if opts.os_release and opts.os_release.startswith('@'):
1224 opts.os_release = pathlib.Path(opts.os_release[1:])
1225 elif not opts.os_release and opts.linux:
f4780cbe
ZJS
1226 p = pathlib.Path('/etc/os-release')
1227 if not p.exists():
5143a47a 1228 p = pathlib.Path('/usr/lib/os-release')
f4780cbe
ZJS
1229 opts.os_release = p
1230
1231 if opts.efi_arch is None:
1232 opts.efi_arch = guess_efi_arch()
1233
1234 if opts.stub is None:
f644ea3e
LB
1235 if opts.linux is not None:
1236 opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
1237 else:
1238 opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub')
f4780cbe
ZJS
1239
1240 if opts.signing_engine is None:
5143a47a
ZJS
1241 if opts.sb_key:
1242 opts.sb_key = pathlib.Path(opts.sb_key)
1243 if opts.sb_cert:
1244 opts.sb_cert = pathlib.Path(opts.sb_cert)
f4780cbe 1245
c1e8d172
EGE
1246 if opts.signtool == 'sbsign':
1247 if bool(opts.sb_key) ^ bool(opts.sb_cert):
1248 raise ValueError('--secureboot-private-key= and --secureboot-certificate= must be specified together when using --signtool=sbsign')
1249 else:
1250 if not bool(opts.sb_cert_name):
1251 raise ValueError('--certificate-name must be specified when using --signtool=pesign')
f4780cbe 1252
c1e8d172
EGE
1253 if opts.sign_kernel and not opts.sb_key and not opts.sb_cert_name:
1254 raise ValueError('--sign-kernel requires either --secureboot-private-key= and --secureboot-certificate= (for sbsign) or --secureboot-certificate-name= (for pesign) to be specified')
f4780cbe 1255
5143a47a
ZJS
1256 if opts.output is None:
1257 if opts.linux is None:
1258 raise ValueError('--output= must be specified when building a PE addon')
c1e8d172 1259 suffix = '.efi' if opts.sb_key or opts.sb_cert_name else '.unsigned.efi'
5143a47a
ZJS
1260 opts.output = opts.linux.name + suffix
1261
1262 for section in opts.sections:
1263 section.check_name()
1264
1265 if opts.summary:
1266 # TODO: replace pprint() with some fancy formatting.
1267 pprint.pprint(vars(opts))
1268 sys.exit()
1269
1270
1271def parse_args(args=None):
b09a5315 1272 opts = create_parser().parse_args(args)
5143a47a 1273 apply_config(opts)
5143a47a 1274 finalize_options(opts)
f4780cbe
ZJS
1275 return opts
1276
1277
1278def main():
1279 opts = parse_args()
1280 check_inputs(opts)
a3b227d2 1281 assert opts.verb == 'build'
f4780cbe
ZJS
1282 make_uki(opts)
1283
1284
1285if __name__ == '__main__':
1286 main()