]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/ukify/ukify.py
ukify: Use pefile to add sections to EFI stub
[thirdparty/systemd.git] / src / ukify / ukify.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: LGPL-2.1-or-later
3
4 # pylint: disable=missing-docstring,invalid-name,import-outside-toplevel
5 # pylint: disable=consider-using-with,unspecified-encoding,line-too-long
6 # pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
7 # pylint: disable=too-many-branches
8
9 import argparse
10 import collections
11 import dataclasses
12 import fnmatch
13 import itertools
14 import json
15 import os
16 import pathlib
17 import re
18 import shlex
19 import shutil
20 import subprocess
21 import tempfile
22 import typing
23
24 import pefile
25
26 __version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
27
28 EFI_ARCH_MAP = {
29 # host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported]
30 'x86_64' : ['x64', 'ia32'],
31 'i[3456]86' : ['ia32'],
32 'aarch64' : ['aa64'],
33 'arm[45678]*l' : ['arm'],
34 'riscv64' : ['riscv64'],
35 }
36 EFI_ARCHES: list[str] = sum(EFI_ARCH_MAP.values(), [])
37
38 def guess_efi_arch():
39 arch = os.uname().machine
40
41 for glob, mapping in EFI_ARCH_MAP.items():
42 if fnmatch.fnmatch(arch, glob):
43 efi_arch, *fallback = mapping
44 break
45 else:
46 raise ValueError(f'Unsupported architecture {arch}')
47
48 # This makes sense only on some architectures, but it also probably doesn't
49 # hurt on others, so let's just apply the check everywhere.
50 if fallback:
51 fw_platform_size = pathlib.Path('/sys/firmware/efi/fw_platform_size')
52 try:
53 size = fw_platform_size.read_text().strip()
54 except FileNotFoundError:
55 pass
56 else:
57 if int(size) == 32:
58 efi_arch = fallback[0]
59
60 print(f'Host arch {arch!r}, EFI arch {efi_arch!r}')
61 return efi_arch
62
63
64 def shell_join(cmd):
65 # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
66 return ' '.join(shlex.quote(str(x)) for x in cmd)
67
68
69 def path_is_readable(s: typing.Optional[str]) -> typing.Optional[pathlib.Path]:
70 """Convert a filename string to a Path and verify access."""
71 if s is None:
72 return None
73 p = pathlib.Path(s)
74 try:
75 p.open().close()
76 except IsADirectoryError:
77 pass
78 return p
79
80
81 def round_up(x, blocksize=4096):
82 return (x + blocksize - 1) // blocksize * blocksize
83
84
85 def try_import(modname, name=None):
86 try:
87 return __import__(modname)
88 except ImportError as e:
89 raise ValueError(f'Kernel is compressed with {name or modname}, but module unavailable') from e
90
91
92 def maybe_decompress(filename):
93 """Decompress file if compressed. Return contents."""
94 f = open(filename, 'rb')
95 start = f.read(4)
96 f.seek(0)
97
98 if start.startswith(b'\x7fELF'):
99 # not compressed
100 return f.read()
101
102 if start.startswith(b'MZ'):
103 # not compressed aarch64 and riscv64
104 return f.read()
105
106 if start.startswith(b'\x1f\x8b'):
107 gzip = try_import('gzip')
108 return gzip.open(f).read()
109
110 if start.startswith(b'\x28\xb5\x2f\xfd'):
111 zstd = try_import('zstd')
112 return zstd.uncompress(f.read())
113
114 if start.startswith(b'\x02\x21\x4c\x18'):
115 lz4 = try_import('lz4.frame', 'lz4')
116 return lz4.frame.decompress(f.read())
117
118 if start.startswith(b'\x04\x22\x4d\x18'):
119 print('Newer lz4 stream format detected! This may not boot!')
120 lz4 = try_import('lz4.frame', 'lz4')
121 return lz4.frame.decompress(f.read())
122
123 if start.startswith(b'\x89LZO'):
124 # python3-lzo is not packaged for Fedora
125 raise NotImplementedError('lzo decompression not implemented')
126
127 if start.startswith(b'BZh'):
128 bz2 = try_import('bz2', 'bzip2')
129 return bz2.open(f).read()
130
131 if start.startswith(b'\x5d\x00\x00'):
132 lzma = try_import('lzma')
133 return lzma.open(f).read()
134
135 raise NotImplementedError(f'unknown file format (starts with {start})')
136
137
138 class Uname:
139 # This class is here purely as a namespace for the functions
140
141 VERSION_PATTERN = r'(?P<version>[a-z0-9._-]+) \([^ )]+\) (?:#.*)'
142
143 NOTES_PATTERN = r'^\s+Linux\s+0x[0-9a-f]+\s+OPEN\n\s+description data: (?P<version>[0-9a-f ]+)\s*$'
144
145 # Linux version 6.0.8-300.fc37.ppc64le (mockbuild@buildvm-ppc64le-03.iad2.fedoraproject.org)
146 # (gcc (GCC) 12.2.1 20220819 (Red Hat 12.2.1-2), GNU ld version 2.38-24.fc37)
147 # #1 SMP Fri Nov 11 14:39:11 UTC 2022
148 TEXT_PATTERN = rb'Linux version (?P<version>\d\.\S+) \('
149
150 @classmethod
151 def scrape_x86(cls, filename, opts=None):
152 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L136
153 # and https://www.kernel.org/doc/html/latest/x86/boot.html#the-real-mode-kernel-header
154 with open(filename, 'rb') as f:
155 f.seek(0x202)
156 magic = f.read(4)
157 if magic != b'HdrS':
158 raise ValueError('Real-Mode Kernel Header magic not found')
159 f.seek(0x20E)
160 offset = f.read(1)[0] + f.read(1)[0]*256 # Pointer to kernel version string
161 f.seek(0x200 + offset)
162 text = f.read(128)
163 text = text.split(b'\0', maxsplit=1)[0]
164 text = text.decode()
165
166 if not (m := re.match(cls.VERSION_PATTERN, text)):
167 raise ValueError(f'Cannot parse version-host-release uname string: {text!r}')
168 return m.group('version')
169
170 @classmethod
171 def scrape_elf(cls, filename, opts=None):
172 readelf = find_tool('readelf', opts=opts)
173
174 cmd = [
175 readelf,
176 '--notes',
177 filename,
178 ]
179
180 print('+', shell_join(cmd))
181 try:
182 notes = subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True)
183 except subprocess.CalledProcessError as e:
184 raise ValueError(e.stderr.strip()) from e
185
186 if not (m := re.search(cls.NOTES_PATTERN, notes, re.MULTILINE)):
187 raise ValueError('Cannot find Linux version note')
188
189 text = ''.join(chr(int(c, 16)) for c in m.group('version').split())
190 return text.rstrip('\0')
191
192 @classmethod
193 def scrape_generic(cls, filename, opts=None):
194 # import libarchive
195 # libarchive-c fails with
196 # ArchiveError: Unrecognized archive format (errno=84, retcode=-30, archive_p=94705420454656)
197
198 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L209
199
200 text = maybe_decompress(filename)
201 if not (m := re.search(cls.TEXT_PATTERN, text)):
202 raise ValueError(f'Cannot find {cls.TEXT_PATTERN!r} in {filename}')
203
204 return m.group('version').decode()
205
206 @classmethod
207 def scrape(cls, filename, opts=None):
208 for func in (cls.scrape_x86, cls.scrape_elf, cls.scrape_generic):
209 try:
210 version = func(filename, opts=opts)
211 print(f'Found uname version: {version}')
212 return version
213 except ValueError as e:
214 print(str(e))
215 return None
216
217
218 @dataclasses.dataclass
219 class Section:
220 name: str
221 content: pathlib.Path
222 tmpfile: typing.Optional[typing.IO] = None
223 measure: bool = False
224
225 @classmethod
226 def create(cls, name, contents, **kwargs):
227 if isinstance(contents, (str, bytes)):
228 mode = 'wt' if isinstance(contents, str) else 'wb'
229 tmp = tempfile.NamedTemporaryFile(mode=mode, prefix=f'tmp{name}')
230 tmp.write(contents)
231 tmp.flush()
232 contents = pathlib.Path(tmp.name)
233 else:
234 tmp = None
235
236 return cls(name, contents, tmpfile=tmp, **kwargs)
237
238 @classmethod
239 def parse_arg(cls, s):
240 try:
241 name, contents, *rest = s.split(':')
242 except ValueError as e:
243 raise ValueError(f'Cannot parse section spec (name or contents missing): {s!r}') from e
244 if rest:
245 raise ValueError(f'Cannot parse section spec (extraneous parameters): {s!r}')
246
247 if contents.startswith('@'):
248 contents = pathlib.Path(contents[1:])
249
250 return cls.create(name, contents)
251
252 def size(self):
253 return self.content.stat().st_size
254
255 def check_name(self):
256 # PE section names with more than 8 characters are legal, but our stub does
257 # not support them.
258 if not self.name.isascii() or not self.name.isprintable():
259 raise ValueError(f'Bad section name: {self.name!r}')
260 if len(self.name) > 8:
261 raise ValueError(f'Section name too long: {self.name!r}')
262
263
264 @dataclasses.dataclass
265 class UKI:
266 executable: list[typing.Union[pathlib.Path, str]]
267 sections: list[Section] = dataclasses.field(default_factory=list, init=False)
268
269 def add_section(self, section):
270 if section.name in [s.name for s in self.sections]:
271 raise ValueError(f'Duplicate section {section.name}')
272
273 self.sections += [section]
274
275
276 def parse_banks(s):
277 banks = re.split(r',|\s+', s)
278 # TODO: do some sanity checking here
279 return banks
280
281
282 KNOWN_PHASES = (
283 'enter-initrd',
284 'leave-initrd',
285 'sysinit',
286 'ready',
287 'shutdown',
288 'final',
289 )
290
291 def parse_phase_paths(s):
292 # Split on commas or whitespace here. Commas might be hard to parse visually.
293 paths = re.split(r',|\s+', s)
294
295 for path in paths:
296 for phase in path.split(':'):
297 if phase not in KNOWN_PHASES:
298 raise argparse.ArgumentTypeError(f'Unknown boot phase {phase!r} ({path=})')
299
300 return paths
301
302
303 def check_splash(filename):
304 if filename is None:
305 return
306
307 # import is delayed, to avoid import when the splash image is not used
308 try:
309 from PIL import Image
310 except ImportError:
311 return
312
313 img = Image.open(filename, formats=['BMP'])
314 print(f'Splash image {filename} is {img.width}×{img.height} pixels')
315
316
317 def check_inputs(opts):
318 for name, value in vars(opts).items():
319 if name in {'output', 'tools'}:
320 continue
321
322 if not isinstance(value, pathlib.Path):
323 continue
324
325 # Open file to check that we can read it, or generate an exception
326 value.open().close()
327
328 check_splash(opts.splash)
329
330
331 def find_tool(name, fallback=None, opts=None):
332 if opts and opts.tools:
333 for d in opts.tools:
334 tool = d / name
335 if tool.exists():
336 return tool
337
338 if shutil.which(name) is not None:
339 return name
340
341 return fallback
342
343
344 def combine_signatures(pcrsigs):
345 combined = collections.defaultdict(list)
346 for pcrsig in pcrsigs:
347 for bank, sigs in pcrsig.items():
348 for sig in sigs:
349 if sig not in combined[bank]:
350 combined[bank] += [sig]
351 return json.dumps(combined)
352
353
354 def call_systemd_measure(uki, linux, opts):
355 measure_tool = find_tool('systemd-measure',
356 '/usr/lib/systemd/systemd-measure',
357 opts=opts)
358
359 banks = opts.pcr_banks or ()
360
361 # PCR measurement
362
363 if opts.measure:
364 pp_groups = opts.phase_path_groups or []
365
366 cmd = [
367 measure_tool,
368 'calculate',
369 f'--linux={linux}',
370 *(f"--{s.name.removeprefix('.')}={s.content}"
371 for s in uki.sections
372 if s.measure),
373 *(f'--bank={bank}'
374 for bank in banks),
375 # For measurement, the keys are not relevant, so we can lump all the phase paths
376 # into one call to systemd-measure calculate.
377 *(f'--phase={phase_path}'
378 for phase_path in itertools.chain.from_iterable(pp_groups)),
379 ]
380
381 print('+', shell_join(cmd))
382 subprocess.check_call(cmd)
383
384 # PCR signing
385
386 if opts.pcr_private_keys:
387 n_priv = len(opts.pcr_private_keys or ())
388 pp_groups = opts.phase_path_groups or [None] * n_priv
389 pub_keys = opts.pcr_public_keys or [None] * n_priv
390
391 pcrsigs = []
392
393 cmd = [
394 measure_tool,
395 'sign',
396 f'--linux={linux}',
397 *(f"--{s.name.removeprefix('.')}={s.content}"
398 for s in uki.sections
399 if s.measure),
400 *(f'--bank={bank}'
401 for bank in banks),
402 ]
403
404 for priv_key, pub_key, group in zip(opts.pcr_private_keys,
405 pub_keys,
406 pp_groups):
407 extra = [f'--private-key={priv_key}']
408 if pub_key:
409 extra += [f'--public-key={pub_key}']
410 extra += [f'--phase={phase_path}' for phase_path in group or ()]
411
412 print('+', shell_join(cmd + extra))
413 pcrsig = subprocess.check_output(cmd + extra, text=True)
414 pcrsig = json.loads(pcrsig)
415 pcrsigs += [pcrsig]
416
417 combined = combine_signatures(pcrsigs)
418 uki.add_section(Section.create('.pcrsig', combined))
419
420
421 def join_initrds(initrds):
422 if len(initrds) == 0:
423 return None
424 elif len(initrds) == 1:
425 return initrds[0]
426
427 seq = []
428 for file in initrds:
429 initrd = file.read_bytes()
430 n = len(initrd)
431 padding = b'\0' * (round_up(n, 4) - n) # pad to 32 bit alignment
432 seq += [initrd, padding]
433
434 return b''.join(seq)
435
436
437 def pairwise(iterable):
438 a, b = itertools.tee(iterable)
439 next(b, None)
440 return zip(a, b)
441
442
443 class PeError(Exception):
444 pass
445
446
447 def pe_add_sections(uki: UKI, output: str):
448 pe = pefile.PE(uki.executable, fast_load=True)
449 assert len(pe.__data__) % pe.OPTIONAL_HEADER.FileAlignment == 0
450
451 warnings = pe.get_warnings()
452 if warnings:
453 raise PeError(f'pefile warnings treated as errors: {warnings}')
454
455 security = pe.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']]
456 if security.VirtualAddress != 0:
457 # We could strip the signatures, but why would anyone sign the stub?
458 raise PeError(f'Stub image is signed, refusing.')
459
460 for section in uki.sections:
461 new_section = pefile.SectionStructure(pe.__IMAGE_SECTION_HEADER_format__, pe=pe)
462 new_section.__unpack__(b'\0' * new_section.sizeof())
463
464 offset = pe.sections[-1].get_file_offset() + new_section.sizeof()
465 if offset + new_section.sizeof() > pe.OPTIONAL_HEADER.SizeOfHeaders:
466 raise PeError(f'Not enough header space to add section {section.name}.')
467
468 data = section.content.read_bytes()
469
470 new_section.set_file_offset(offset)
471 new_section.Name = section.name.encode()
472 new_section.Misc_VirtualSize = len(data)
473 new_section.PointerToRawData = len(pe.__data__)
474 new_section.SizeOfRawData = round_up(len(data), pe.OPTIONAL_HEADER.FileAlignment)
475 new_section.VirtualAddress = round_up(
476 pe.sections[-1].VirtualAddress + pe.sections[-1].Misc_VirtualSize,
477 pe.OPTIONAL_HEADER.SectionAlignment,
478 )
479
480 new_section.IMAGE_SCN_MEM_READ = True
481 if section.name == '.linux':
482 # Old kernels that use EFI handover protocol will be executed inline.
483 new_section.IMAGE_SCN_CNT_CODE = True
484 else:
485 new_section.IMAGE_SCN_CNT_INITIALIZED_DATA = True
486
487 assert len(pe.__data__) % pe.OPTIONAL_HEADER.FileAlignment == 0
488 pe.__data__ = pe.__data__[:] + data + b'\0' * (new_section.SizeOfRawData - len(data))
489
490 pe.FILE_HEADER.NumberOfSections += 1
491 pe.OPTIONAL_HEADER.SizeOfInitializedData += new_section.Misc_VirtualSize
492 pe.__structures__.append(new_section)
493 pe.sections.append(new_section)
494
495 pe.OPTIONAL_HEADER.CheckSum = 0
496 pe.OPTIONAL_HEADER.SizeOfImage = round_up(
497 pe.sections[-1].VirtualAddress + pe.sections[-1].Misc_VirtualSize,
498 pe.OPTIONAL_HEADER.SectionAlignment,
499 )
500
501 pe.write(output)
502
503
504 def make_uki(opts):
505 # kernel payload signing
506
507 sbsign_tool = find_tool('sbsign', opts=opts)
508 sbsign_invocation = [
509 sbsign_tool,
510 '--key', opts.sb_key,
511 '--cert', opts.sb_cert,
512 ]
513
514 if opts.signing_engine is not None:
515 sbsign_invocation += ['--engine', opts.signing_engine]
516
517 sign_kernel = opts.sign_kernel
518 if sign_kernel is None and opts.sb_key:
519 # figure out if we should sign the kernel
520 sbverify_tool = find_tool('sbverify', opts=opts)
521
522 cmd = [
523 sbverify_tool,
524 '--list',
525 opts.linux,
526 ]
527
528 print('+', shell_join(cmd))
529 info = subprocess.check_output(cmd, text=True)
530
531 # sbverify has wonderful API
532 if 'No signature table present' in info:
533 sign_kernel = True
534
535 if sign_kernel:
536 linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed')
537 linux = linux_signed.name
538
539 cmd = [
540 *sbsign_invocation,
541 opts.linux,
542 '--output', linux,
543 ]
544
545 print('+', shell_join(cmd))
546 subprocess.check_call(cmd)
547 else:
548 linux = opts.linux
549
550 if opts.uname is None:
551 print('Kernel version not specified, starting autodetection 😖.')
552 opts.uname = Uname.scrape(opts.linux, opts=opts)
553
554 uki = UKI(opts.stub)
555 initrd = join_initrds(opts.initrd)
556
557 # TODO: derive public key from opts.pcr_private_keys?
558 pcrpkey = opts.pcrpkey
559 if pcrpkey is None:
560 if opts.pcr_public_keys and len(opts.pcr_public_keys) == 1:
561 pcrpkey = opts.pcr_public_keys[0]
562
563 sections = [
564 # name, content, measure?
565 ('.osrel', opts.os_release, True ),
566 ('.cmdline', opts.cmdline, True ),
567 ('.dtb', opts.devicetree, True ),
568 ('.splash', opts.splash, True ),
569 ('.pcrpkey', pcrpkey, True ),
570 ('.initrd', initrd, True ),
571 ('.uname', opts.uname, False),
572
573 # linux shall be last to leave breathing room for decompression.
574 # We'll add it later.
575 ]
576
577 for name, content, measure in sections:
578 if content:
579 uki.add_section(Section.create(name, content, measure=measure))
580
581 # systemd-measure doesn't know about those extra sections
582 for section in opts.sections:
583 uki.add_section(section)
584
585 # PCR measurement and signing
586
587 call_systemd_measure(uki, linux, opts=opts)
588
589 # UKI creation
590
591 uki.add_section(Section.create('.linux', linux, measure=True))
592
593 if opts.sb_key:
594 unsigned = tempfile.NamedTemporaryFile(prefix='uki')
595 output = unsigned.name
596 else:
597 output = opts.output
598
599 pe_add_sections(uki, output)
600
601 # UKI signing
602
603 if opts.sb_key:
604 cmd = [
605 *sbsign_invocation,
606 unsigned.name,
607 '--output', opts.output,
608 ]
609 print('+', shell_join(cmd))
610 subprocess.check_call(cmd)
611
612 # We end up with no executable bits, let's reapply them
613 os.umask(umask := os.umask(0))
614 os.chmod(opts.output, 0o777 & ~umask)
615
616 print(f"Wrote {'signed' if opts.sb_key else 'unsigned'} {opts.output}")
617
618
619 def parse_args(args=None):
620 p = argparse.ArgumentParser(
621 description='Build and sign Unified Kernel Images',
622 allow_abbrev=False,
623 usage='''\
624 usage: ukify [options…] linux initrd…
625 ukify -h | --help
626 ''')
627
628 # Suppress printing of usage synopsis on errors
629 p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
630
631 p.add_argument('linux',
632 type=pathlib.Path,
633 help='vmlinuz file [.linux section]')
634 p.add_argument('initrd',
635 type=pathlib.Path,
636 nargs='*',
637 help='initrd files [.initrd section]')
638
639 p.add_argument('--cmdline',
640 metavar='TEXT|@PATH',
641 help='kernel command line [.cmdline section]')
642
643 p.add_argument('--os-release',
644 metavar='TEXT|@PATH',
645 help='path to os-release file [.osrel section]')
646
647 p.add_argument('--devicetree',
648 metavar='PATH',
649 type=pathlib.Path,
650 help='Device Tree file [.dtb section]')
651 p.add_argument('--splash',
652 metavar='BMP',
653 type=pathlib.Path,
654 help='splash image bitmap file [.splash section]')
655 p.add_argument('--pcrpkey',
656 metavar='KEY',
657 type=pathlib.Path,
658 help='embedded public key to seal secrets to [.pcrpkey section]')
659 p.add_argument('--uname',
660 metavar='VERSION',
661 help='"uname -r" information [.uname section]')
662
663 p.add_argument('--efi-arch',
664 metavar='ARCH',
665 choices=('ia32', 'x64', 'arm', 'aa64', 'riscv64'),
666 help='target EFI architecture')
667
668 p.add_argument('--stub',
669 type=pathlib.Path,
670 help='path to the sd-stub file [.text,.data,… sections]')
671
672 p.add_argument('--section',
673 dest='sections',
674 metavar='NAME:TEXT|@PATH',
675 type=Section.parse_arg,
676 action='append',
677 default=[],
678 help='additional section as name and contents [NAME section]')
679
680 p.add_argument('--pcr-private-key',
681 dest='pcr_private_keys',
682 metavar='PATH',
683 type=pathlib.Path,
684 action='append',
685 help='private part of the keypair for signing PCR signatures')
686 p.add_argument('--pcr-public-key',
687 dest='pcr_public_keys',
688 metavar='PATH',
689 type=pathlib.Path,
690 action='append',
691 help='public part of the keypair for signing PCR signatures')
692 p.add_argument('--phases',
693 dest='phase_path_groups',
694 metavar='PHASE-PATH…',
695 type=parse_phase_paths,
696 action='append',
697 help='phase-paths to create signatures for')
698
699 p.add_argument('--pcr-banks',
700 metavar='BANK…',
701 type=parse_banks)
702
703 p.add_argument('--signing-engine',
704 metavar='ENGINE',
705 help='OpenSSL engine to use for signing')
706 p.add_argument('--secureboot-private-key',
707 dest='sb_key',
708 help='path to key file or engine-specific designation for SB signing')
709 p.add_argument('--secureboot-certificate',
710 dest='sb_cert',
711 help='path to certificate file or engine-specific designation for SB signing')
712
713 p.add_argument('--sign-kernel',
714 action=argparse.BooleanOptionalAction,
715 help='Sign the embedded kernel')
716
717 p.add_argument('--tools',
718 type=pathlib.Path,
719 action='append',
720 help='Directories to search for tools (systemd-measure, ...)')
721
722 p.add_argument('--output', '-o',
723 type=pathlib.Path,
724 help='output file path')
725
726 p.add_argument('--measure',
727 action=argparse.BooleanOptionalAction,
728 help='print systemd-measure output for the UKI')
729
730 p.add_argument('--version',
731 action='version',
732 version=f'ukify {__version__}')
733
734 opts = p.parse_args(args)
735
736 path_is_readable(opts.linux)
737 for initrd in opts.initrd or ():
738 path_is_readable(initrd)
739 path_is_readable(opts.devicetree)
740 path_is_readable(opts.pcrpkey)
741 for key in opts.pcr_private_keys or ():
742 path_is_readable(key)
743 for key in opts.pcr_public_keys or ():
744 path_is_readable(key)
745
746 if opts.cmdline and opts.cmdline.startswith('@'):
747 opts.cmdline = path_is_readable(opts.cmdline[1:])
748
749 if opts.os_release is not None and opts.os_release.startswith('@'):
750 opts.os_release = path_is_readable(opts.os_release[1:])
751 elif opts.os_release is None:
752 p = pathlib.Path('/etc/os-release')
753 if not p.exists():
754 p = path_is_readable('/usr/lib/os-release')
755 opts.os_release = p
756
757 if opts.efi_arch is None:
758 opts.efi_arch = guess_efi_arch()
759
760 if opts.stub is None:
761 opts.stub = path_is_readable(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
762
763 if opts.signing_engine is None:
764 opts.sb_key = path_is_readable(opts.sb_key) if opts.sb_key else None
765 opts.sb_cert = path_is_readable(opts.sb_cert) if opts.sb_cert else None
766
767 if bool(opts.sb_key) ^ bool(opts.sb_cert):
768 raise ValueError('--secureboot-private-key= and --secureboot-certificate= must be specified together')
769
770 if opts.sign_kernel and not opts.sb_key:
771 raise ValueError('--sign-kernel requires --secureboot-private-key= and --secureboot-certificate= to be specified')
772
773 n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys)
774 n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys)
775 n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups)
776 if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv:
777 raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=')
778 if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv:
779 raise ValueError('--phases= specifications must match --pcr-private-key=')
780
781 if opts.output is None:
782 suffix = '.efi' if opts.sb_key else '.unsigned.efi'
783 opts.output = opts.linux.name + suffix
784
785 for section in opts.sections:
786 section.check_name()
787
788 return opts
789
790
791 def main():
792 opts = parse_args()
793 check_inputs(opts)
794 make_uki(opts)
795
796
797 if __name__ == '__main__':
798 main()