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