]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/ukify/ukify.py
core: Settle log target if we're going to be closing all fds
[thirdparty/systemd.git] / src / ukify / ukify.py
CommitLineData
f4780cbe 1#!/usr/bin/env python3
eb81a60c 2# SPDX-License-Identifier: LGPL-2.1-or-later
f4780cbe
ZJS
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
9import argparse
10import collections
11import dataclasses
12import fnmatch
13import itertools
14import json
15import os
16import pathlib
17import re
18import shlex
c8add4c2 19import shutil
f4780cbe
ZJS
20import subprocess
21import tempfile
22import typing
23
f4780cbe 24
67b65ac6 25__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
30ec2eae 26
f4780cbe
ZJS
27EFI_ARCH_MAP = {
28 # host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported]
29 'x86_64' : ['x64', 'ia32'],
30 'i[3456]86' : ['ia32'],
31 'aarch64' : ['aa64'],
32 'arm[45678]*l' : ['arm'],
33 'riscv64' : ['riscv64'],
34}
35EFI_ARCHES: list[str] = sum(EFI_ARCH_MAP.values(), [])
36
37def guess_efi_arch():
38 arch = os.uname().machine
39
40 for glob, mapping in EFI_ARCH_MAP.items():
41 if fnmatch.fnmatch(arch, glob):
42 efi_arch, *fallback = mapping
43 break
44 else:
45 raise ValueError(f'Unsupported architecture {arch}')
46
47 # This makes sense only on some architectures, but it also probably doesn't
48 # hurt on others, so let's just apply the check everywhere.
49 if fallback:
50 fw_platform_size = pathlib.Path('/sys/firmware/efi/fw_platform_size')
51 try:
52 size = fw_platform_size.read_text().strip()
53 except FileNotFoundError:
54 pass
55 else:
56 if int(size) == 32:
57 efi_arch = fallback[0]
58
59 print(f'Host arch {arch!r}, EFI arch {efi_arch!r}')
60 return efi_arch
61
62
63def shell_join(cmd):
64 # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
65 return ' '.join(shlex.quote(str(x)) for x in cmd)
66
67
09595fd5 68def path_is_readable(s: typing.Optional[str]) -> typing.Optional[pathlib.Path]:
095ff238
ZJS
69 """Convert a filename string to a Path and verify access."""
70 if s is None:
71 return None
72 p = pathlib.Path(s)
737dab1a
DDM
73 try:
74 p.open().close()
75 except IsADirectoryError:
76 pass
095ff238
ZJS
77 return p
78
79
599c930e 80def pe_next_section_offset(filename):
1f6da5d9
ZJS
81 import pefile
82
c87ff622 83 pe = pefile.PE(filename, fast_load=True)
f4780cbe 84 section = pe.sections[-1]
599c930e 85 return pe.OPTIONAL_HEADER.ImageBase + section.VirtualAddress + section.Misc_VirtualSize
f4780cbe
ZJS
86
87
88def round_up(x, blocksize=4096):
89 return (x + blocksize - 1) // blocksize * blocksize
90
91
667578bb
ZJS
92def try_import(modname, name=None):
93 try:
94 return __import__(modname)
95 except ImportError as e:
96 raise ValueError(f'Kernel is compressed with {name or modname}, but module unavailable') from e
97
98
483c9c1b
ZJS
99def maybe_decompress(filename):
100 """Decompress file if compressed. Return contents."""
101 f = open(filename, 'rb')
102 start = f.read(4)
103 f.seek(0)
104
105 if start.startswith(b'\x7fELF'):
106 # not compressed
107 return f.read()
108
109 if start.startswith(b'\x1f\x8b'):
667578bb 110 gzip = try_import('gzip')
483c9c1b
ZJS
111 return gzip.open(f).read()
112
113 if start.startswith(b'\x28\xb5\x2f\xfd'):
667578bb 114 zstd = try_import('zstd')
483c9c1b
ZJS
115 return zstd.uncompress(f.read())
116
117 if start.startswith(b'\x02\x21\x4c\x18'):
667578bb 118 lz4 = try_import('lz4.frame', 'lz4')
483c9c1b
ZJS
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!')
667578bb 123 lz4 = try_import('lz4.frame', 'lz4')
483c9c1b
ZJS
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'):
667578bb 131 bz2 = try_import('bz2', 'bzip2')
483c9c1b
ZJS
132 return bz2.open(f).read()
133
134 if start.startswith(b'\x5d\x00\x00'):
667578bb 135 lzma = try_import('lzma')
483c9c1b
ZJS
136 return lzma.open(f).read()
137
138 raise NotImplementedError(f'unknown file format (starts with {start})')
139
140
141class 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))
33bdec18
ZJS
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
483c9c1b
ZJS
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
f4780cbe
ZJS
221@dataclasses.dataclass
222class Section:
223 name: str
224 content: pathlib.Path
09595fd5 225 tmpfile: typing.Optional[typing.IO] = None
789a6427 226 flags: list[str] = dataclasses.field(default_factory=lambda: ['data', 'readonly'])
09595fd5 227 offset: typing.Optional[int] = None
f4780cbe
ZJS
228 measure: bool = False
229
230 @classmethod
789a6427 231 def create(cls, name, contents, **kwargs):
09595fd5 232 if isinstance(contents, (str, bytes)):
54c84c8a
ZJS
233 mode = 'wt' if isinstance(contents, str) else 'wb'
234 tmp = tempfile.NamedTemporaryFile(mode=mode, prefix=f'tmp{name}')
f4780cbe
ZJS
235 tmp.write(contents)
236 tmp.flush()
237 contents = pathlib.Path(tmp.name)
238 else:
239 tmp = None
240
789a6427 241 return cls(name, contents, tmpfile=tmp, **kwargs)
f4780cbe
ZJS
242
243 @classmethod
244 def parse_arg(cls, s):
245 try:
246 name, contents, *rest = s.split(':')
247 except ValueError as e:
248 raise ValueError(f'Cannot parse section spec (name or contents missing): {s!r}') from e
249 if rest:
250 raise ValueError(f'Cannot parse section spec (extraneous parameters): {s!r}')
251
252 if contents.startswith('@'):
253 contents = pathlib.Path(contents[1:])
254
255 return cls.create(name, contents)
256
257 def size(self):
258 return self.content.stat().st_size
259
260 def check_name(self):
261 # PE section names with more than 8 characters are legal, but our stub does
262 # not support them.
263 if not self.name.isascii() or not self.name.isprintable():
264 raise ValueError(f'Bad section name: {self.name!r}')
265 if len(self.name) > 8:
266 raise ValueError(f'Section name too long: {self.name!r}')
267
268
269@dataclasses.dataclass
270class UKI:
09595fd5 271 executable: list[typing.Union[pathlib.Path, str]]
f4780cbe 272 sections: list[Section] = dataclasses.field(default_factory=list, init=False)
09595fd5 273 offset: typing.Optional[int] = dataclasses.field(default=None, init=False)
f4780cbe
ZJS
274
275 def __post_init__(self):
599c930e 276 self.offset = round_up(pe_next_section_offset(self.executable))
f4780cbe
ZJS
277
278 def add_section(self, section):
279 assert self.offset
280 assert section.offset is None
281
282 if section.name in [s.name for s in self.sections]:
283 raise ValueError(f'Duplicate section {section.name}')
284
285 section.offset = self.offset
286 self.offset += round_up(section.size())
287 self.sections += [section]
288
289
290def parse_banks(s):
291 banks = re.split(r',|\s+', s)
292 # TODO: do some sanity checking here
293 return banks
294
295
296KNOWN_PHASES = (
297 'enter-initrd',
298 'leave-initrd',
299 'sysinit',
300 'ready',
301 'shutdown',
302 'final',
303)
304
305def parse_phase_paths(s):
306 # Split on commas or whitespace here. Commas might be hard to parse visually.
307 paths = re.split(r',|\s+', s)
308
309 for path in paths:
310 for phase in path.split(':'):
311 if phase not in KNOWN_PHASES:
312 raise argparse.ArgumentTypeError(f'Unknown boot phase {phase!r} ({path=})')
313
314 return paths
315
316
317def check_splash(filename):
318 if filename is None:
319 return
320
321 # import is delayed, to avoid import when the splash image is not used
322 try:
323 from PIL import Image
324 except ImportError:
325 return
326
327 img = Image.open(filename, formats=['BMP'])
328 print(f'Splash image {filename} is {img.width}×{img.height} pixels')
329
330
331def check_inputs(opts):
332 for name, value in vars(opts).items():
333 if name in {'output', 'tools'}:
334 continue
335
336 if not isinstance(value, pathlib.Path):
337 continue
338
339 # Open file to check that we can read it, or generate an exception
340 value.open().close()
341
342 check_splash(opts.splash)
343
344
345def find_tool(name, fallback=None, opts=None):
346 if opts and opts.tools:
22ad038a
DDM
347 for d in opts.tools:
348 tool = d / name
349 if tool.exists():
350 return tool
f4780cbe 351
c8add4c2
JJ
352 if shutil.which(name) is not None:
353 return name
354
355 return fallback
f4780cbe
ZJS
356
357
358def combine_signatures(pcrsigs):
359 combined = collections.defaultdict(list)
360 for pcrsig in pcrsigs:
361 for bank, sigs in pcrsig.items():
362 for sig in sigs:
363 if sig not in combined[bank]:
364 combined[bank] += [sig]
365 return json.dumps(combined)
366
367
368def call_systemd_measure(uki, linux, opts):
369 measure_tool = find_tool('systemd-measure',
370 '/usr/lib/systemd/systemd-measure',
371 opts=opts)
372
373 banks = opts.pcr_banks or ()
374
375 # PCR measurement
376
377 if opts.measure:
378 pp_groups = opts.phase_path_groups or []
379
380 cmd = [
381 measure_tool,
382 'calculate',
383 f'--linux={linux}',
384 *(f"--{s.name.removeprefix('.')}={s.content}"
385 for s in uki.sections
386 if s.measure),
387 *(f'--bank={bank}'
388 for bank in banks),
389 # For measurement, the keys are not relevant, so we can lump all the phase paths
390 # into one call to systemd-measure calculate.
391 *(f'--phase={phase_path}'
392 for phase_path in itertools.chain.from_iterable(pp_groups)),
393 ]
394
395 print('+', shell_join(cmd))
396 subprocess.check_call(cmd)
397
398 # PCR signing
399
400 if opts.pcr_private_keys:
401 n_priv = len(opts.pcr_private_keys or ())
402 pp_groups = opts.phase_path_groups or [None] * n_priv
403 pub_keys = opts.pcr_public_keys or [None] * n_priv
404
405 pcrsigs = []
406
407 cmd = [
408 measure_tool,
409 'sign',
410 f'--linux={linux}',
411 *(f"--{s.name.removeprefix('.')}={s.content}"
412 for s in uki.sections
413 if s.measure),
414 *(f'--bank={bank}'
415 for bank in banks),
416 ]
417
418 for priv_key, pub_key, group in zip(opts.pcr_private_keys,
419 pub_keys,
420 pp_groups):
421 extra = [f'--private-key={priv_key}']
422 if pub_key:
423 extra += [f'--public-key={pub_key}']
424 extra += [f'--phase={phase_path}' for phase_path in group or ()]
425
426 print('+', shell_join(cmd + extra))
427 pcrsig = subprocess.check_output(cmd + extra, text=True)
428 pcrsig = json.loads(pcrsig)
429 pcrsigs += [pcrsig]
430
431 combined = combine_signatures(pcrsigs)
432 uki.add_section(Section.create('.pcrsig', combined))
433
434
54c84c8a 435def join_initrds(initrds):
09595fd5
DDM
436 if len(initrds) == 0:
437 return None
438 elif len(initrds) == 1:
439 return initrds[0]
440
441 seq = []
442 for file in initrds:
443 initrd = file.read_bytes()
c126c8ac
YW
444 n = len(initrd)
445 padding = b'\0' * (round_up(n, 4) - n) # pad to 32 bit alignment
09595fd5
DDM
446 seq += [initrd, padding]
447
448 return b''.join(seq)
54c84c8a
ZJS
449
450
c811aba0
DDM
451def pairwise(iterable):
452 a, b = itertools.tee(iterable)
453 next(b, None)
454 return zip(a, b)
455
456
3fc1ae89
DDM
457def pe_validate(filename):
458 import pefile
459
c87ff622 460 pe = pefile.PE(filename, fast_load=True)
3fc1ae89
DDM
461
462 sections = sorted(pe.sections, key=lambda s: (s.VirtualAddress, s.Misc_VirtualSize))
463
c811aba0 464 for l, r in pairwise(sections):
3fc1ae89
DDM
465 if l.VirtualAddress + l.Misc_VirtualSize > r.VirtualAddress + r.Misc_VirtualSize:
466 raise ValueError(f'Section "{l.Name.decode()}" ({l.VirtualAddress}, {l.Misc_VirtualSize}) overlaps with section "{r.Name.decode()}" ({r.VirtualAddress}, {r.Misc_VirtualSize})')
467
468
f4780cbe
ZJS
469def make_uki(opts):
470 # kernel payload signing
471
472 sbsign_tool = find_tool('sbsign', opts=opts)
473 sbsign_invocation = [
474 sbsign_tool,
475 '--key', opts.sb_key,
476 '--cert', opts.sb_cert,
477 ]
478
479 if opts.signing_engine is not None:
480 sbsign_invocation += ['--engine', opts.signing_engine]
481
482 sign_kernel = opts.sign_kernel
483 if sign_kernel is None and opts.sb_key:
484 # figure out if we should sign the kernel
485 sbverify_tool = find_tool('sbverify', opts=opts)
486
487 cmd = [
488 sbverify_tool,
489 '--list',
490 opts.linux,
491 ]
492
493 print('+', shell_join(cmd))
494 info = subprocess.check_output(cmd, text=True)
495
496 # sbverify has wonderful API
497 if 'No signature table present' in info:
498 sign_kernel = True
499
500 if sign_kernel:
501 linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed')
502 linux = linux_signed.name
503
504 cmd = [
505 *sbsign_invocation,
506 opts.linux,
507 '--output', linux,
508 ]
509
510 print('+', shell_join(cmd))
511 subprocess.check_call(cmd)
512 else:
513 linux = opts.linux
514
483c9c1b
ZJS
515 if opts.uname is None:
516 print('Kernel version not specified, starting autodetection 😖.')
517 opts.uname = Uname.scrape(opts.linux, opts=opts)
518
f4780cbe 519 uki = UKI(opts.stub)
54c84c8a 520 initrd = join_initrds(opts.initrd)
f4780cbe 521
30fd9a2d 522 # TODO: derive public key from opts.pcr_private_keys?
f4780cbe
ZJS
523 pcrpkey = opts.pcrpkey
524 if pcrpkey is None:
525 if opts.pcr_public_keys and len(opts.pcr_public_keys) == 1:
526 pcrpkey = opts.pcr_public_keys[0]
527
528 sections = [
529 # name, content, measure?
530 ('.osrel', opts.os_release, True ),
531 ('.cmdline', opts.cmdline, True ),
532 ('.dtb', opts.devicetree, True ),
533 ('.splash', opts.splash, True ),
534 ('.pcrpkey', pcrpkey, True ),
54c84c8a 535 ('.initrd', initrd, True ),
f4780cbe
ZJS
536 ('.uname', opts.uname, False),
537
538 # linux shall be last to leave breathing room for decompression.
539 # We'll add it later.
540 ]
541
542 for name, content, measure in sections:
543 if content:
544 uki.add_section(Section.create(name, content, measure=measure))
545
546 # systemd-measure doesn't know about those extra sections
547 for section in opts.sections:
548 uki.add_section(section)
549
550 # PCR measurement and signing
551
552 call_systemd_measure(uki, linux, opts=opts)
553
554 # UKI creation
555
556 uki.add_section(
557 Section.create('.linux', linux, measure=True,
558 flags=['code', 'readonly']))
559
560 if opts.sb_key:
561 unsigned = tempfile.NamedTemporaryFile(prefix='uki')
562 output = unsigned.name
563 else:
564 output = opts.output
565
789a6427 566 objcopy_tool = find_tool('llvm-objcopy', 'objcopy', opts=opts)
f4780cbe
ZJS
567
568 cmd = [
569 objcopy_tool,
570 opts.stub,
571 *itertools.chain.from_iterable(
789a6427
DDM
572 ('--add-section', f'{s.name}={s.content}',
573 '--set-section-flags', f"{s.name}={','.join(s.flags)}")
f4780cbe 574 for s in uki.sections),
f4780cbe
ZJS
575 output,
576 ]
789a6427
DDM
577
578 if pathlib.Path(objcopy_tool).name != 'llvm-objcopy':
579 cmd += itertools.chain.from_iterable(
580 ('--change-section-vma', f'{s.name}=0x{s.offset:x}') for s in uki.sections)
581
f4780cbe
ZJS
582 print('+', shell_join(cmd))
583 subprocess.check_call(cmd)
584
3fc1ae89
DDM
585 pe_validate(output)
586
f4780cbe
ZJS
587 # UKI signing
588
589 if opts.sb_key:
590 cmd = [
591 *sbsign_invocation,
592 unsigned.name,
593 '--output', opts.output,
594 ]
595 print('+', shell_join(cmd))
596 subprocess.check_call(cmd)
597
598 # We end up with no executable bits, let's reapply them
599 os.umask(umask := os.umask(0))
600 os.chmod(opts.output, 0o777 & ~umask)
601
602 print(f"Wrote {'signed' if opts.sb_key else 'unsigned'} {opts.output}")
603
604
605def parse_args(args=None):
606 p = argparse.ArgumentParser(
607 description='Build and sign Unified Kernel Images',
608 allow_abbrev=False,
609 usage='''\
54c84c8a 610usage: ukify [options…] linux initrd…
f4780cbe
ZJS
611 ukify -h | --help
612''')
613
614 # Suppress printing of usage synopsis on errors
615 p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
616
617 p.add_argument('linux',
618 type=pathlib.Path,
619 help='vmlinuz file [.linux section]')
620 p.add_argument('initrd',
621 type=pathlib.Path,
54c84c8a
ZJS
622 nargs='*',
623 help='initrd files [.initrd section]')
f4780cbe
ZJS
624
625 p.add_argument('--cmdline',
626 metavar='TEXT|@PATH',
627 help='kernel command line [.cmdline section]')
628
629 p.add_argument('--os-release',
630 metavar='TEXT|@PATH',
631 help='path to os-release file [.osrel section]')
632
633 p.add_argument('--devicetree',
634 metavar='PATH',
635 type=pathlib.Path,
636 help='Device Tree file [.dtb section]')
637 p.add_argument('--splash',
638 metavar='BMP',
639 type=pathlib.Path,
640 help='splash image bitmap file [.splash section]')
641 p.add_argument('--pcrpkey',
642 metavar='KEY',
643 type=pathlib.Path,
644 help='embedded public key to seal secrets to [.pcrpkey section]')
645 p.add_argument('--uname',
646 metavar='VERSION',
647 help='"uname -r" information [.uname section]')
648
649 p.add_argument('--efi-arch',
650 metavar='ARCH',
651 choices=('ia32', 'x64', 'arm', 'aa64', 'riscv64'),
652 help='target EFI architecture')
653
654 p.add_argument('--stub',
655 type=pathlib.Path,
0b75493d 656 help='path to the sd-stub file [.text,.data,… sections]')
f4780cbe
ZJS
657
658 p.add_argument('--section',
659 dest='sections',
660 metavar='NAME:TEXT|@PATH',
661 type=Section.parse_arg,
662 action='append',
663 default=[],
664 help='additional section as name and contents [NAME section]')
665
666 p.add_argument('--pcr-private-key',
667 dest='pcr_private_keys',
668 metavar='PATH',
669 type=pathlib.Path,
670 action='append',
671 help='private part of the keypair for signing PCR signatures')
672 p.add_argument('--pcr-public-key',
673 dest='pcr_public_keys',
674 metavar='PATH',
675 type=pathlib.Path,
676 action='append',
677 help='public part of the keypair for signing PCR signatures')
678 p.add_argument('--phases',
679 dest='phase_path_groups',
680 metavar='PHASE-PATH…',
681 type=parse_phase_paths,
682 action='append',
683 help='phase-paths to create signatures for')
684
685 p.add_argument('--pcr-banks',
686 metavar='BANK…',
687 type=parse_banks)
688
689 p.add_argument('--signing-engine',
690 metavar='ENGINE',
691 help='OpenSSL engine to use for signing')
692 p.add_argument('--secureboot-private-key',
693 dest='sb_key',
694 help='path to key file or engine-specific designation for SB signing')
695 p.add_argument('--secureboot-certificate',
696 dest='sb_cert',
697 help='path to certificate file or engine-specific designation for SB signing')
698
699 p.add_argument('--sign-kernel',
700 action=argparse.BooleanOptionalAction,
701 help='Sign the embedded kernel')
702
703 p.add_argument('--tools',
704 type=pathlib.Path,
4f312ba0 705 action='append',
22ad038a 706 help='Directories to search for tools (systemd-measure, llvm-objcopy, ...)')
f4780cbe
ZJS
707
708 p.add_argument('--output', '-o',
709 type=pathlib.Path,
710 help='output file path')
711
712 p.add_argument('--measure',
713 action=argparse.BooleanOptionalAction,
714 help='print systemd-measure output for the UKI')
715
30ec2eae
ZJS
716 p.add_argument('--version',
717 action='version',
718 version=f'ukify {__version__}')
719
f4780cbe
ZJS
720 opts = p.parse_args(args)
721
095ff238
ZJS
722 path_is_readable(opts.linux)
723 for initrd in opts.initrd or ():
724 path_is_readable(initrd)
725 path_is_readable(opts.devicetree)
726 path_is_readable(opts.pcrpkey)
727 for key in opts.pcr_private_keys or ():
728 path_is_readable(key)
729 for key in opts.pcr_public_keys or ():
730 path_is_readable(key)
731
f4780cbe 732 if opts.cmdline and opts.cmdline.startswith('@'):
095ff238 733 opts.cmdline = path_is_readable(opts.cmdline[1:])
f4780cbe
ZJS
734
735 if opts.os_release is not None and opts.os_release.startswith('@'):
095ff238 736 opts.os_release = path_is_readable(opts.os_release[1:])
f4780cbe
ZJS
737 elif opts.os_release is None:
738 p = pathlib.Path('/etc/os-release')
739 if not p.exists():
095ff238 740 p = path_is_readable('/usr/lib/os-release')
f4780cbe
ZJS
741 opts.os_release = p
742
743 if opts.efi_arch is None:
744 opts.efi_arch = guess_efi_arch()
745
746 if opts.stub is None:
095ff238 747 opts.stub = path_is_readable(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
f4780cbe
ZJS
748
749 if opts.signing_engine is None:
095ff238
ZJS
750 opts.sb_key = path_is_readable(opts.sb_key) if opts.sb_key else None
751 opts.sb_cert = path_is_readable(opts.sb_cert) if opts.sb_cert else None
f4780cbe
ZJS
752
753 if bool(opts.sb_key) ^ bool(opts.sb_cert):
754 raise ValueError('--secureboot-private-key= and --secureboot-certificate= must be specified together')
755
756 if opts.sign_kernel and not opts.sb_key:
757 raise ValueError('--sign-kernel requires --secureboot-private-key= and --secureboot-certificate= to be specified')
758
759 n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys)
760 n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys)
761 n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups)
762 if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv:
763 raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=')
764 if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv:
765 raise ValueError('--phases= specifications must match --pcr-private-key=')
766
767 if opts.output is None:
768 suffix = '.efi' if opts.sb_key else '.unsigned.efi'
769 opts.output = opts.linux.name + suffix
770
771 for section in opts.sections:
772 section.check_name()
773
774 return opts
775
776
777def main():
778 opts = parse_args()
779 check_inputs(opts)
780 make_uki(opts)
781
782
783if __name__ == '__main__':
784 main()