2 # SPDX-License-Identifier: LGPL-2.1-or-later
4 # This file is part of systemd.
6 # systemd is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation; either version 2.1 of the License, or
9 # (at your option) any later version.
11 # systemd is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with systemd; If not, see <https://www.gnu.org/licenses/>.
19 # pylint: disable=import-outside-toplevel,consider-using-with,unused-argument
20 # pylint: disable=unnecessary-lambda-assignment
43 from hashlib
import sha256
44 from typing
import (Any
,
51 import pefile
# type: ignore
53 __version__
= '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
56 # host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported]
57 'x86_64' : ['x64', 'ia32'],
58 'i[3456]86' : ['ia32'],
60 'armv[45678]*l': ['arm'],
61 'loongarch32' : ['loongarch32'],
62 'loongarch64' : ['loongarch64'],
63 'riscv32' : ['riscv32'],
64 'riscv64' : ['riscv64'],
66 EFI_ARCHES
: list[str] = sum(EFI_ARCH_MAP
.values(), [])
68 # Default configuration directories and file name.
69 # When the user does not specify one, the directories are searched in this order and the first file found is used.
70 DEFAULT_CONFIG_DIRS
= ['/run/systemd', '/etc/systemd', '/usr/local/lib/systemd', '/usr/lib/systemd']
71 DEFAULT_CONFIG_FILE
= 'ukify.conf'
74 bold
= "\033[0;1;39m" if sys
.stderr
.isatty() else ""
75 gray
= "\033[0;38;5;245m" if sys
.stderr
.isatty() else ""
76 red
= "\033[31;1m" if sys
.stderr
.isatty() else ""
77 yellow
= "\033[33;1m" if sys
.stderr
.isatty() else ""
78 reset
= "\033[0m" if sys
.stderr
.isatty() else ""
82 arch
= os
.uname().machine
84 for glob
, mapping
in EFI_ARCH_MAP
.items():
85 if fnmatch
.fnmatch(arch
, glob
):
86 efi_arch
, *fallback
= mapping
89 raise ValueError(f
'Unsupported architecture {arch}')
91 # This makes sense only on some architectures, but it also probably doesn't
92 # hurt on others, so let's just apply the check everywhere.
94 fw_platform_size
= pathlib
.Path('/sys/firmware/efi/fw_platform_size')
96 size
= fw_platform_size
.read_text().strip()
97 except FileNotFoundError
:
101 efi_arch
= fallback
[0]
103 # print(f'Host arch {arch!r}, EFI arch {efi_arch!r}')
107 def page(text
: str, enabled
: Optional
[bool]) -> None:
109 # Initialize less options from $SYSTEMD_LESS or provide a suitable fallback.
110 os
.environ
['LESS'] = os
.getenv('SYSTEMD_LESS', 'FRSXMK')
117 # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
118 return ' '.join(shlex
.quote(str(x
)) for x
in cmd
)
121 def round_up(x
, blocksize
=4096):
122 return (x
+ blocksize
- 1) // blocksize
* blocksize
125 def try_import(modname
, name
=None):
127 return __import__(modname
)
128 except ImportError as e
:
129 raise ValueError(f
'Kernel is compressed with {name or modname}, but module unavailable') from e
132 def maybe_decompress(filename
):
133 """Decompress file if compressed. Return contents."""
134 f
= open(filename
, 'rb')
138 if start
.startswith(b
'\x7fELF'):
142 if start
.startswith(b
'MZ'):
143 # not compressed aarch64 and riscv64
146 if start
.startswith(b
'\x1f\x8b'):
147 gzip
= try_import('gzip')
148 return gzip
.open(f
).read()
150 if start
.startswith(b
'\x28\xb5\x2f\xfd'):
151 zstd
= try_import('zstd')
152 return zstd
.uncompress(f
.read())
154 if start
.startswith(b
'\x02\x21\x4c\x18'):
155 lz4
= try_import('lz4.frame', 'lz4')
156 return lz4
.frame
.decompress(f
.read())
158 if start
.startswith(b
'\x04\x22\x4d\x18'):
159 print('Newer lz4 stream format detected! This may not boot!')
160 lz4
= try_import('lz4.frame', 'lz4')
161 return lz4
.frame
.decompress(f
.read())
163 if start
.startswith(b
'\x89LZO'):
164 # python3-lzo is not packaged for Fedora
165 raise NotImplementedError('lzo decompression not implemented')
167 if start
.startswith(b
'BZh'):
168 bz2
= try_import('bz2', 'bzip2')
169 return bz2
.open(f
).read()
171 if start
.startswith(b
'\x5d\x00\x00'):
172 lzma
= try_import('lzma')
173 return lzma
.open(f
).read()
175 raise NotImplementedError(f
'unknown file format (starts with {start})')
179 # This class is here purely as a namespace for the functions
181 VERSION_PATTERN
= r
'(?P<version>[a-z0-9._-]+) \([^ )]+\) (?:#.*)'
183 NOTES_PATTERN
= r
'^\s+Linux\s+0x[0-9a-f]+\s+OPEN\n\s+description data: (?P<version>[0-9a-f ]+)\s*$'
185 # Linux version 6.0.8-300.fc37.ppc64le (mockbuild@buildvm-ppc64le-03.iad2.fedoraproject.org)
186 # (gcc (GCC) 12.2.1 20220819 (Red Hat 12.2.1-2), GNU ld version 2.38-24.fc37)
187 # #1 SMP Fri Nov 11 14:39:11 UTC 2022
188 TEXT_PATTERN
= rb
'Linux version (?P<version>\d\.\S+) \('
191 def scrape_x86(cls
, filename
, opts
=None):
192 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L136
193 # and https://www.kernel.org/doc/html/latest/x86/boot.html#the-real-mode-kernel-header
194 with
open(filename
, 'rb') as f
:
198 raise ValueError('Real-Mode Kernel Header magic not found')
200 offset
= f
.read(1)[0] + f
.read(1)[0]*256 # Pointer to kernel version string
201 f
.seek(0x200 + offset
)
203 text
= text
.split(b
'\0', maxsplit
=1)[0]
206 if not (m
:= re
.match(cls
.VERSION_PATTERN
, text
)):
207 raise ValueError(f
'Cannot parse version-host-release uname string: {text!r}')
208 return m
.group('version')
211 def scrape_elf(cls
, filename
, opts
=None):
212 readelf
= find_tool('readelf', opts
=opts
)
220 print('+', shell_join(cmd
))
222 notes
= subprocess
.check_output(cmd
, stderr
=subprocess
.PIPE
, text
=True)
223 except subprocess
.CalledProcessError
as e
:
224 raise ValueError(e
.stderr
.strip()) from e
226 if not (m
:= re
.search(cls
.NOTES_PATTERN
, notes
, re
.MULTILINE
)):
227 raise ValueError('Cannot find Linux version note')
229 text
= ''.join(chr(int(c
, 16)) for c
in m
.group('version').split())
230 return text
.rstrip('\0')
233 def scrape_generic(cls
, filename
, opts
=None):
235 # libarchive-c fails with
236 # ArchiveError: Unrecognized archive format (errno=84, retcode=-30, archive_p=94705420454656)
238 # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L209
240 text
= maybe_decompress(filename
)
241 if not (m
:= re
.search(cls
.TEXT_PATTERN
, text
)):
242 raise ValueError(f
'Cannot find {cls.TEXT_PATTERN!r} in {filename}')
244 return m
.group('version').decode()
247 def scrape(cls
, filename
, opts
=None):
248 for func
in (cls
.scrape_x86
, cls
.scrape_elf
, cls
.scrape_generic
):
250 version
= func(filename
, opts
=opts
)
251 print(f
'Found uname version: {version}')
253 except ValueError as e
:
257 DEFAULT_SECTIONS_TO_SHOW
= {
259 '.initrd' : 'binary',
260 '.splash' : 'binary',
271 @dataclasses.dataclass
274 content
: Optional
[pathlib
.Path
]
275 tmpfile
: Optional
[IO
] = None
276 measure
: bool = False
277 output_mode
: Optional
[str] = None
280 def create(cls
, name
, contents
, **kwargs
):
281 if isinstance(contents
, (str, bytes
)):
282 mode
= 'wt' if isinstance(contents
, str) else 'wb'
283 tmp
= tempfile
.NamedTemporaryFile(mode
=mode
, prefix
=f
'tmp{name}')
286 contents
= pathlib
.Path(tmp
.name
)
290 return cls(name
, contents
, tmpfile
=tmp
, **kwargs
)
293 def parse_input(cls
, s
):
295 name
, contents
, *rest
= s
.split(':')
296 except ValueError as e
:
297 raise ValueError(f
'Cannot parse section spec (name or contents missing): {s!r}') from e
299 raise ValueError(f
'Cannot parse section spec (extraneous parameters): {s!r}')
301 if contents
.startswith('@'):
302 contents
= pathlib
.Path(contents
[1:])
304 sec
= cls
.create(name
, contents
)
309 def parse_output(cls
, s
):
310 if not (m
:= re
.match(r
'([a-zA-Z0-9_.]+):(text|binary)(?:@(.+))?', s
)):
311 raise ValueError(f
'Cannot parse section spec: {s!r}')
313 name
, ttype
, out
= m
.groups()
314 out
= pathlib
.Path(out
) if out
else None
316 return cls
.create(name
, out
, output_mode
=ttype
)
319 return self
.content
.stat().st_size
321 def check_name(self
):
322 # PE section names with more than 8 characters are legal, but our stub does
324 if not self
.name
.isascii() or not self
.name
.isprintable():
325 raise ValueError(f
'Bad section name: {self.name!r}')
326 if len(self
.name
) > 8:
327 raise ValueError(f
'Section name too long: {self.name!r}')
330 @dataclasses.dataclass
332 executable
: list[Union
[pathlib
.Path
, str]]
333 sections
: list[Section
] = dataclasses
.field(default_factory
=list, init
=False)
335 def add_section(self
, section
):
336 if section
.name
in [s
.name
for s
in self
.sections
]:
337 raise ValueError(f
'Duplicate section {section.name}')
339 self
.sections
+= [section
]
343 banks
= re
.split(r
',|\s+', s
)
344 # TODO: do some sanity checking here
357 def parse_phase_paths(s
):
358 # Split on commas or whitespace here. Commas might be hard to parse visually.
359 paths
= re
.split(r
',|\s+', s
)
362 for phase
in path
.split(':'):
363 if phase
not in KNOWN_PHASES
:
364 raise argparse
.ArgumentTypeError(f
'Unknown boot phase {phase!r} ({path=})')
369 def check_splash(filename
):
373 # import is delayed, to avoid import when the splash image is not used
375 from PIL
import Image
379 img
= Image
.open(filename
, formats
=['BMP'])
380 print(f
'Splash image {filename} is {img.width}×{img.height} pixels')
383 def check_inputs(opts
):
384 for name
, value
in vars(opts
).items():
385 if name
in {'output', 'tools'}:
388 if isinstance(value
, pathlib
.Path
):
389 # Open file to check that we can read it, or generate an exception
391 elif isinstance(value
, list):
393 if isinstance(item
, pathlib
.Path
):
396 check_splash(opts
.splash
)
399 def check_cert_and_keys_nonexistent(opts
):
400 # Raise if any of the keys and certs are found on disk
401 paths
= itertools
.chain(
402 (opts
.sb_key
, opts
.sb_cert
),
403 *((priv_key
, pub_key
)
404 for priv_key
, pub_key
, _
in key_path_groups(opts
)))
406 if path
and path
.exists():
407 raise ValueError(f
'{path} is present')
410 def find_tool(name
, fallback
=None, opts
=None):
411 if opts
and opts
.tools
:
417 if shutil
.which(name
) is not None:
421 print(f
"Tool {name} not installed!")
425 def combine_signatures(pcrsigs
):
426 combined
= collections
.defaultdict(list)
427 for pcrsig
in pcrsigs
:
428 for bank
, sigs
in pcrsig
.items():
430 if sig
not in combined
[bank
]:
431 combined
[bank
] += [sig
]
432 return json
.dumps(combined
)
435 def key_path_groups(opts
):
436 if not opts
.pcr_private_keys
:
439 n_priv
= len(opts
.pcr_private_keys
)
440 pub_keys
= opts
.pcr_public_keys
or [None] * n_priv
441 pp_groups
= opts
.phase_path_groups
or [None] * n_priv
443 yield from zip(opts
.pcr_private_keys
,
448 def call_systemd_measure(uki
, linux
, opts
):
449 measure_tool
= find_tool('systemd-measure',
450 '/usr/lib/systemd/systemd-measure',
453 banks
= opts
.pcr_banks
or ()
458 pp_groups
= opts
.phase_path_groups
or []
464 *(f
"--{s.name.removeprefix('.')}={s.content}"
465 for s
in uki
.sections
469 # For measurement, the keys are not relevant, so we can lump all the phase paths
470 # into one call to systemd-measure calculate.
471 *(f
'--phase={phase_path}'
472 for phase_path
in itertools
.chain
.from_iterable(pp_groups
)),
475 print('+', shell_join(cmd
))
476 subprocess
.check_call(cmd
)
480 if opts
.pcr_private_keys
:
487 *(f
"--{s.name.removeprefix('.')}={s.content}"
488 for s
in uki
.sections
494 for priv_key
, pub_key
, group
in key_path_groups(opts
):
495 extra
= [f
'--private-key={priv_key}']
497 extra
+= [f
'--public-key={pub_key}']
498 extra
+= [f
'--phase={phase_path}' for phase_path
in group
or ()]
500 print('+', shell_join(cmd
+ extra
))
501 pcrsig
= subprocess
.check_output(cmd
+ extra
, text
=True)
502 pcrsig
= json
.loads(pcrsig
)
505 combined
= combine_signatures(pcrsigs
)
506 uki
.add_section(Section
.create('.pcrsig', combined
))
509 def join_initrds(initrds
):
512 if len(initrds
) == 1:
517 initrd
= file.read_bytes()
519 padding
= b
'\0' * (round_up(n
, 4) - n
) # pad to 32 bit alignment
520 seq
+= [initrd
, padding
]
525 def pairwise(iterable
):
526 a
, b
= itertools
.tee(iterable
)
531 class PEError(Exception):
535 def pe_add_sections(uki
: UKI
, output
: str):
536 pe
= pefile
.PE(uki
.executable
, fast_load
=True)
538 # Old stubs do not have the symbol/string table stripped, even though image files should not have one.
539 if symbol_table
:= pe
.FILE_HEADER
.PointerToSymbolTable
:
540 symbol_table_size
= 18 * pe
.FILE_HEADER
.NumberOfSymbols
541 if string_table_size
:= pe
.get_dword_from_offset(symbol_table
+ symbol_table_size
):
542 symbol_table_size
+= string_table_size
544 # Let's be safe and only strip it if it's at the end of the file.
545 if symbol_table
+ symbol_table_size
== len(pe
.__data
__):
546 pe
.__data
__ = pe
.__data
__[:symbol_table
]
547 pe
.FILE_HEADER
.PointerToSymbolTable
= 0
548 pe
.FILE_HEADER
.NumberOfSymbols
= 0
549 pe
.FILE_HEADER
.IMAGE_FILE_LOCAL_SYMS_STRIPPED
= True
551 # Old stubs might have been stripped, leading to unaligned raw data values, so let's fix them up here.
552 # pylint thinks that Structure doesn't have various members that it has…
553 # pylint: disable=no-member
555 for i
, section
in enumerate(pe
.sections
):
556 oldp
= section
.PointerToRawData
557 oldsz
= section
.SizeOfRawData
558 section
.PointerToRawData
= round_up(oldp
, pe
.OPTIONAL_HEADER
.FileAlignment
)
559 section
.SizeOfRawData
= round_up(oldsz
, pe
.OPTIONAL_HEADER
.FileAlignment
)
560 padp
= section
.PointerToRawData
- oldp
561 padsz
= section
.SizeOfRawData
- oldsz
563 for later_section
in pe
.sections
[i
+1:]:
564 later_section
.PointerToRawData
+= padp
+ padsz
566 pe
.__data
__ = pe
.__data
__[:oldp
] + bytes(padp
) + pe
.__data
__[oldp
:oldp
+oldsz
] + bytes(padsz
) + pe
.__data
__[oldp
+oldsz
:]
568 # We might not have any space to add new sections. Let's try our best to make some space by padding the
569 # SizeOfHeaders to a multiple of the file alignment. This is safe because the first section's data starts
570 # at a multiple of the file alignment, so all space before that is unused.
571 pe
.OPTIONAL_HEADER
.SizeOfHeaders
= round_up(pe
.OPTIONAL_HEADER
.SizeOfHeaders
, pe
.OPTIONAL_HEADER
.FileAlignment
)
572 pe
= pefile
.PE(data
=pe
.write(), fast_load
=True)
574 warnings
= pe
.get_warnings()
576 raise PEError(f
'pefile warnings treated as errors: {warnings}')
578 security
= pe
.OPTIONAL_HEADER
.DATA_DIRECTORY
[pefile
.DIRECTORY_ENTRY
['IMAGE_DIRECTORY_ENTRY_SECURITY']]
579 if security
.VirtualAddress
!= 0:
580 # We could strip the signatures, but why would anyone sign the stub?
581 raise PEError('Stub image is signed, refusing.')
583 for section
in uki
.sections
:
584 new_section
= pefile
.SectionStructure(pe
.__IMAGE
_SECTION
_HEADER
_format
__, pe
=pe
)
585 new_section
.__unpack
__(b
'\0' * new_section
.sizeof())
587 offset
= pe
.sections
[-1].get_file_offset() + new_section
.sizeof()
588 if offset
+ new_section
.sizeof() > pe
.OPTIONAL_HEADER
.SizeOfHeaders
:
589 raise PEError(f
'Not enough header space to add section {section.name}.')
591 assert section
.content
592 data
= section
.content
.read_bytes()
594 new_section
.set_file_offset(offset
)
595 new_section
.Name
= section
.name
.encode()
596 new_section
.Misc_VirtualSize
= len(data
)
597 # Non-stripped stubs might still have an unaligned symbol table at the end, making their size
598 # unaligned, so we make sure to explicitly pad the pointer to new sections to an aligned offset.
599 new_section
.PointerToRawData
= round_up(len(pe
.__data
__), pe
.OPTIONAL_HEADER
.FileAlignment
)
600 new_section
.SizeOfRawData
= round_up(len(data
), pe
.OPTIONAL_HEADER
.FileAlignment
)
601 new_section
.VirtualAddress
= round_up(
602 pe
.sections
[-1].VirtualAddress
+ pe
.sections
[-1].Misc_VirtualSize
,
603 pe
.OPTIONAL_HEADER
.SectionAlignment
,
606 new_section
.IMAGE_SCN_MEM_READ
= True
607 if section
.name
== '.linux':
608 # Old kernels that use EFI handover protocol will be executed inline.
609 new_section
.IMAGE_SCN_CNT_CODE
= True
611 new_section
.IMAGE_SCN_CNT_INITIALIZED_DATA
= True
613 # Special case, mostly for .sbat: the stub will already have a .sbat section, but we want to append
614 # the one from the kernel to it. It should be small enough to fit in the existing section, so just
616 for i
, s
in enumerate(pe
.sections
):
617 if s
.Name
.rstrip(b
"\x00").decode() == section
.name
:
618 if new_section
.Misc_VirtualSize
> s
.SizeOfRawData
:
619 raise PEError(f
'Not enough space in existing section {section.name} to append new data.')
621 padding
= bytes(new_section
.SizeOfRawData
- new_section
.Misc_VirtualSize
)
622 pe
.__data
__ = pe
.__data
__[:s
.PointerToRawData
] + data
+ padding
+ pe
.__data
__[pe
.sections
[i
+1].PointerToRawData
:]
623 s
.SizeOfRawData
= new_section
.SizeOfRawData
624 s
.Misc_VirtualSize
= new_section
.Misc_VirtualSize
627 pe
.__data
__ = pe
.__data
__[:] + bytes(new_section
.PointerToRawData
- len(pe
.__data
__)) + data
+ bytes(new_section
.SizeOfRawData
- len(data
))
629 pe
.FILE_HEADER
.NumberOfSections
+= 1
630 pe
.OPTIONAL_HEADER
.SizeOfInitializedData
+= new_section
.Misc_VirtualSize
631 pe
.__structures
__.append(new_section
)
632 pe
.sections
.append(new_section
)
634 pe
.OPTIONAL_HEADER
.CheckSum
= 0
635 pe
.OPTIONAL_HEADER
.SizeOfImage
= round_up(
636 pe
.sections
[-1].VirtualAddress
+ pe
.sections
[-1].Misc_VirtualSize
,
637 pe
.OPTIONAL_HEADER
.SectionAlignment
,
642 def merge_sbat(input_pe
: [pathlib
.Path
], input_text
: [str]) -> str:
647 pe
= pefile
.PE(f
, fast_load
=True)
648 except pefile
.PEFormatError
:
649 print(f
"{f} is not a valid PE file, not extracting SBAT section.")
652 for section
in pe
.sections
:
653 if section
.Name
.rstrip(b
"\x00").decode() == ".sbat":
654 split
= section
.get_data().rstrip(b
"\x00").decode().splitlines()
655 if not split
[0].startswith('sbat,'):
656 print(f
"{f} does not contain a valid SBAT section, skipping.")
658 # Filter out the sbat line, we'll add it back later, there needs to be only one and it
663 if t
.startswith('@'):
664 t
= pathlib
.Path(t
[1:]).read_text()
665 split
= t
.splitlines()
666 if not split
[0].startswith('sbat,'):
667 print(f
"{t} does not contain a valid SBAT section, skipping.")
671 return 'sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md\n' + '\n'.join(sbat
) + "\n\x00"
673 def signer_sign(cmd
):
674 print('+', shell_join(cmd
))
675 subprocess
.check_call(cmd
)
677 def find_sbsign(opts
=None):
678 return find_tool('sbsign', opts
=opts
)
680 def sbsign_sign(sbsign_tool
, input_f
, output_f
, opts
=None):
683 '--key', opts
.sb_key
,
684 '--cert', opts
.sb_cert
,
686 '--output', output_f
,
688 if opts
.signing_engine
is not None:
689 sign_invocation
+= ['--engine', opts
.signing_engine
]
690 signer_sign(sign_invocation
)
692 def find_pesign(opts
=None):
693 return find_tool('pesign', opts
=opts
)
695 def pesign_sign(pesign_tool
, input_f
, output_f
, opts
=None):
697 pesign_tool
, '-s', '--force',
698 '-n', opts
.sb_certdir
,
699 '-c', opts
.sb_cert_name
,
703 signer_sign(sign_invocation
)
708 'output': 'No signature table present',
714 'output': 'No signatures found.',
718 def verify(tool
, opts
):
719 verify_tool
= find_tool(tool
['name'], opts
=opts
)
726 cmd
.append(tool
['flags'])
728 print('+', shell_join(cmd
))
729 info
= subprocess
.check_output(cmd
, text
=True)
731 return tool
['output'] in info
734 # kernel payload signing
737 sign_args_present
= opts
.sb_key
or opts
.sb_cert_name
738 sign_kernel
= opts
.sign_kernel
742 if sign_args_present
:
743 if opts
.signtool
== 'sbsign':
744 sign_tool
= find_sbsign(opts
=opts
)
746 verify_tool
= SBVERIFY
748 sign_tool
= find_pesign(opts
=opts
)
750 verify_tool
= PESIGCHECK
752 if sign_tool
is None:
753 raise ValueError(f
'{opts.signtool}, required for signing, is not installed')
755 if sign_kernel
is None and opts
.linux
is not None:
756 # figure out if we should sign the kernel
757 sign_kernel
= verify(verify_tool
, opts
)
760 linux_signed
= tempfile
.NamedTemporaryFile(prefix
='linux-signed')
761 linux
= pathlib
.Path(linux_signed
.name
)
762 sign(sign_tool
, opts
.linux
, linux
, opts
=opts
)
764 if opts
.uname
is None and opts
.linux
is not None:
765 print('Kernel version not specified, starting autodetection 😖.')
766 opts
.uname
= Uname
.scrape(opts
.linux
, opts
=opts
)
769 initrd
= join_initrds(opts
.initrd
)
771 pcrpkey
= opts
.pcrpkey
773 if opts
.pcr_public_keys
and len(opts
.pcr_public_keys
) == 1:
774 pcrpkey
= opts
.pcr_public_keys
[0]
775 elif opts
.pcr_private_keys
and len(opts
.pcr_private_keys
) == 1:
776 from cryptography
.hazmat
.primitives
import serialization
777 privkey
= serialization
.load_pem_private_key(opts
.pcr_private_keys
[0].read_bytes(), password
=None)
778 pcrpkey
= privkey
.public_key().public_bytes(
779 encoding
=serialization
.Encoding
.PEM
,
780 format
=serialization
.PublicFormat
.SubjectPublicKeyInfo
,
784 # name, content, measure?
785 ('.osrel', opts
.os_release
, True ),
786 ('.cmdline', opts
.cmdline
, True ),
787 ('.dtb', opts
.devicetree
, True ),
788 ('.uname', opts
.uname
, True ),
789 ('.splash', opts
.splash
, True ),
790 ('.pcrpkey', pcrpkey
, True ),
791 ('.initrd', initrd
, True ),
793 # linux shall be last to leave breathing room for decompression.
794 # We'll add it later.
797 for name
, content
, measure
in sections
:
799 uki
.add_section(Section
.create(name
, content
, measure
=measure
))
801 # systemd-measure doesn't know about those extra sections
802 for section
in opts
.sections
:
803 uki
.add_section(section
)
805 if linux
is not None:
806 # Merge the .sbat sections from stub, kernel and parameter, so that revocation can be done on either.
807 uki
.add_section(Section
.create('.sbat', merge_sbat([opts
.stub
, linux
], opts
.sbat
), measure
=True))
809 # Addons don't use the stub so we add SBAT manually
811 opts
.sbat
= ["""sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
812 uki,1,UKI,uki,1,https://www.freedesktop.org/software/systemd/man/systemd-stub.html
814 uki
.add_section(Section
.create('.sbat', merge_sbat([], opts
.sbat
), measure
=False))
816 # PCR measurement and signing
818 # We pass in the contents for .linux separately because we need them to do the measurement but can't add
819 # the section yet because we want .linux to be the last section. Make sure any other sections are added
820 # before this function is called.
821 call_systemd_measure(uki
, linux
, opts
=opts
)
825 if linux
is not None:
826 uki
.add_section(Section
.create('.linux', linux
, measure
=True))
828 if sign_args_present
:
829 unsigned
= tempfile
.NamedTemporaryFile(prefix
='uki')
830 unsigned_output
= unsigned
.name
832 unsigned_output
= opts
.output
834 pe_add_sections(uki
, unsigned_output
)
838 if sign_args_present
:
840 sign(sign_tool
, unsigned_output
, opts
.output
, opts
=opts
)
842 # We end up with no executable bits, let's reapply them
843 os
.umask(umask
:= os
.umask(0))
844 os
.chmod(opts
.output
, 0o777 & ~umask
)
846 print(f
"Wrote {'signed' if sign_args_present else 'unsigned'} {opts.output}")
849 @contextlib.contextmanager
850 def temporary_umask(mask
: int):
851 # Drop <mask> bits from umask
860 def generate_key_cert_pair(
863 keylength
: int = 2048,
866 from cryptography
import x509
867 from cryptography
.hazmat
.primitives
import serialization
, hashes
868 from cryptography
.hazmat
.primitives
.asymmetric
import rsa
870 # We use a keylength of 2048 bits. That is what Microsoft documents as
871 # supported/expected:
872 # https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-secure-boot-key-creation-and-management-guidance?view=windows-11#12-public-key-cryptography
874 now
= datetime
.datetime
.now(datetime
.UTC
)
876 key
= rsa
.generate_private_key(
877 public_exponent
=65537,
880 cert
= x509
.CertificateBuilder(
882 x509
.Name([x509
.NameAttribute(x509
.oid
.NameOID
.COMMON_NAME
, common_name
)])
884 x509
.Name([x509
.NameAttribute(x509
.oid
.NameOID
.COMMON_NAME
, common_name
)])
888 now
+ datetime
.timedelta(days
=valid_days
)
890 x509
.random_serial_number()
894 x509
.BasicConstraints(ca
=False, path_length
=None),
898 algorithm
=hashes
.SHA256(),
901 cert_pem
= cert
.public_bytes(
902 encoding
=serialization
.Encoding
.PEM
,
904 key_pem
= key
.private_bytes(
905 encoding
=serialization
.Encoding
.PEM
,
906 format
=serialization
.PrivateFormat
.TraditionalOpenSSL
,
907 encryption_algorithm
=serialization
.NoEncryption(),
910 return key_pem
, cert_pem
913 def generate_priv_pub_key_pair(keylength
: int = 2048) -> tuple[bytes
]:
914 from cryptography
.hazmat
.primitives
import serialization
915 from cryptography
.hazmat
.primitives
.asymmetric
import rsa
917 key
= rsa
.generate_private_key(
918 public_exponent
=65537,
921 priv_key_pem
= key
.private_bytes(
922 encoding
=serialization
.Encoding
.PEM
,
923 format
=serialization
.PrivateFormat
.TraditionalOpenSSL
,
924 encryption_algorithm
=serialization
.NoEncryption(),
926 pub_key_pem
= key
.public_key().public_bytes(
927 encoding
=serialization
.Encoding
.PEM
,
928 format
=serialization
.PublicFormat
.SubjectPublicKeyInfo
,
931 return priv_key_pem
, pub_key_pem
934 def generate_keys(opts
):
937 # This will generate keys and certificates and write them to the paths that
938 # are specified as input paths.
939 if opts
.sb_key
or opts
.sb_cert
:
940 fqdn
= socket
.getfqdn()
941 cn
= f
'SecureBoot signing key on host {fqdn}'
942 key_pem
, cert_pem
= generate_key_cert_pair(
944 valid_days
=opts
.sb_cert_validity
,
946 print(f
'Writing SecureBoot private key to {opts.sb_key}')
947 with
temporary_umask(0o077):
948 opts
.sb_key
.write_bytes(key_pem
)
949 print(f
'Writing SecureBoot certificate to {opts.sb_cert}')
950 opts
.sb_cert
.write_bytes(cert_pem
)
954 for priv_key
, pub_key
, _
in key_path_groups(opts
):
955 priv_key_pem
, pub_key_pem
= generate_priv_pub_key_pair()
957 print(f
'Writing private key for PCR signing to {priv_key}')
958 with
temporary_umask(0o077):
959 priv_key
.write_bytes(priv_key_pem
)
961 print(f
'Writing public key for PCR signing to {pub_key}')
962 pub_key
.write_bytes(pub_key_pem
)
967 raise ValueError('genkey: --secureboot-private-key=/--secureboot-certificate= or --pcr-private-key/--pcr-public-key must be specified')
970 def inspect_section(opts
, section
):
971 name
= section
.Name
.rstrip(b
"\x00").decode()
973 # find the config for this section in opts and whether to show it
974 config
= opts
.sections_by_name
.get(name
, None)
977 (name
in DEFAULT_SECTIONS_TO_SHOW
and not opts
.sections
))
981 ttype
= config
.output_mode
if config
else DEFAULT_SECTIONS_TO_SHOW
.get(name
, 'binary')
983 size
= section
.Misc_VirtualSize
984 # TODO: Use ignore_padding once we can depend on a newer version of pefile
985 data
= section
.get_data(length
=size
)
986 digest
= sha256(data
).hexdigest()
995 struct
['text'] = data
.decode()
996 except UnicodeDecodeError as e
:
997 print(f
"Section {name!r} is not valid text: {e}")
998 struct
['text'] = '(not valid UTF-8)'
1000 if config
and config
.content
:
1001 assert isinstance(config
.content
, pathlib
.Path
)
1002 config
.content
.write_bytes(data
)
1004 if opts
.json
== 'off':
1005 print(f
"{name}:\n size: {size} bytes\n sha256: {digest}")
1007 text
= textwrap
.indent(struct
['text'].rstrip(), ' ' * 4)
1008 print(f
" text:\n{text}")
1013 def inspect_sections(opts
):
1014 indent
= 4 if opts
.json
== 'pretty' else None
1016 for file in opts
.files
:
1017 pe
= pefile
.PE(file, fast_load
=True)
1018 gen
= (inspect_section(opts
, section
) for section
in pe
.sections
)
1019 descs
= {key
:val
for (key
, val
) in gen
if val
}
1020 if opts
.json
!= 'off':
1021 json
.dump(descs
, sys
.stdout
, indent
=indent
)
1024 @dataclasses.dataclass(frozen
=True)
1027 def config_list_prepend(
1028 namespace
: argparse
.Namespace
,
1029 group
: Optional
[str],
1033 "Prepend value to namespace.<dest>"
1037 old
= getattr(namespace
, dest
, [])
1040 setattr(namespace
, dest
, value
+ old
)
1043 def config_set_if_unset(
1044 namespace
: argparse
.Namespace
,
1045 group
: Optional
[str],
1049 "Set namespace.<dest> to value only if it was None"
1053 if getattr(namespace
, dest
) is None:
1054 setattr(namespace
, dest
, value
)
1058 namespace
: argparse
.Namespace
,
1059 group
: Optional
[str],
1063 "Set namespace.<dest> to value only if it was None"
1067 setattr(namespace
, dest
, value
)
1070 def config_set_group(
1071 namespace
: argparse
.Namespace
,
1072 group
: Optional
[str],
1076 "Set namespace.<dest>[idx] to value, with idx derived from group"
1078 # pylint: disable=protected-access
1079 if group
not in namespace
._groups
:
1080 namespace
._groups
+= [group
]
1081 idx
= namespace
._groups
.index(group
)
1083 old
= getattr(namespace
, dest
, None)
1086 setattr(namespace
, dest
,
1087 old
+ ([None] * (idx
- len(old
))) + [value
])
1090 def parse_boolean(s
: str) -> bool:
1091 "Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false"
1093 if s_l
in {'1', 'true', 'yes', 'y', 't', 'on'}:
1095 if s_l
in {'0', 'false', 'no', 'n', 'f', 'off'}:
1097 raise ValueError('f"Invalid boolean literal: {s!r}')
1099 # arguments for argparse.ArgumentParser.add_argument()
1100 name
: Union
[str, tuple[str, str]]
1101 dest
: Optional
[str] = None
1102 metavar
: Optional
[str] = None
1103 type: Optional
[Callable
] = None
1104 nargs
: Optional
[str] = None
1105 action
: Optional
[Union
[str, Callable
]] = None
1107 version
: Optional
[str] = None
1108 choices
: Optional
[tuple[str, ...]] = None
1109 const
: Optional
[Any
] = None
1110 help: Optional
[str] = None
1112 # metadata for config file parsing
1113 config_key
: Optional
[str] = None
1114 config_push
: Callable
[[argparse
.Namespace
, Optional
[str], str, Any
], None] = \
1117 def _names(self
) -> tuple[str, ...]:
1118 return self
.name
if isinstance(self
.name
, tuple) else (self
.name
,)
1120 def argparse_dest(self
) -> str:
1121 # It'd be nice if argparse exported this, but I don't see that in the API
1124 return self
._names
()[0].lstrip('-').replace('-', '_')
1126 def add_to(self
, parser
: argparse
.ArgumentParser
):
1128 for key
in dataclasses
.asdict(self
)
1129 if (key
not in ('name', 'config_key', 'config_push') and
1130 (val
:= getattr(self
, key
)) is not None) }
1131 args
= self
._names
()
1132 parser
.add_argument(*args
, **kwargs
)
1134 def apply_config(self
, namespace
, section
, group
, key
, value
) -> None:
1135 assert f
'{section}/{key}' == self
.config_key
1136 dest
= self
.argparse_dest()
1138 conv
: Callable
[[str], Any
]
1139 if self
.action
== argparse
.BooleanOptionalAction
:
1140 # We need to handle this case separately: the options are called
1141 # --foo and --no-foo, and no argument is parsed. But in the config
1142 # file, we have Foo=yes or Foo=no.
1143 conv
= self
.parse_boolean
1149 # This is a bit ugly, but --initrd is the only option which is specified
1150 # with multiple args on the command line and a space-separated list in the
1152 if self
.name
== '--initrd':
1153 value
= [conv(v
) for v
in value
.split()]
1157 self
.config_push(namespace
, group
, dest
, value
)
1159 def config_example(self
) -> tuple[Optional
[str], Optional
[str], Optional
[str]]:
1160 if not self
.config_key
:
1161 return None, None, None
1162 section_name
, key
= self
.config_key
.split('/', 1)
1163 if section_name
.endswith(':'):
1164 section_name
+= 'NAME'
1166 value
= '|'.join(self
.choices
)
1168 value
= self
.metavar
or self
.argparse_dest().upper()
1169 return (section_name
, key
, value
)
1172 VERBS
= ('build', 'genkey', 'inspect')
1179 help = argparse
.SUPPRESS
,
1185 version
= f
'ukify {__version__}',
1190 help = 'print parsed config and exit',
1191 action
= 'store_true',
1196 type = pathlib
.Path
,
1197 help = 'vmlinuz file [.linux section]',
1198 config_key
= 'UKI/Linux',
1204 type = pathlib
.Path
,
1206 help = 'initrd file [part of .initrd section]',
1207 config_key
= 'UKI/Initrd',
1208 config_push
= ConfigItem
.config_list_prepend
,
1214 type = pathlib
.Path
,
1215 help = 'configuration file',
1220 metavar
= 'TEXT|@PATH',
1221 help = 'kernel command line [.cmdline section]',
1222 config_key
= 'UKI/Cmdline',
1227 metavar
= 'TEXT|@PATH',
1228 help = 'path to os-release file [.osrel section]',
1229 config_key
= 'UKI/OSRelease',
1235 type = pathlib
.Path
,
1236 help = 'Device Tree file [.dtb section]',
1237 config_key
= 'UKI/DeviceTree',
1242 type = pathlib
.Path
,
1243 help = 'splash image bitmap file [.splash section]',
1244 config_key
= 'UKI/Splash',
1249 type = pathlib
.Path
,
1250 help = 'embedded public key to seal secrets to [.pcrpkey section]',
1251 config_key
= 'UKI/PCRPKey',
1256 help='"uname -r" information [.uname section]',
1257 config_key
= 'UKI/Uname',
1263 choices
= ('ia32', 'x64', 'arm', 'aa64', 'riscv64'),
1264 help = 'target EFI architecture',
1265 config_key
= 'UKI/EFIArch',
1270 type = pathlib
.Path
,
1271 help = 'path to the sd-stub file [.text,.data,… sections]',
1272 config_key
= 'UKI/Stub',
1277 metavar
= 'TEXT|@PATH',
1278 help = 'SBAT policy [.sbat section]',
1281 config_key
= 'UKI/SBAT',
1287 metavar
= 'NAME:TEXT|@PATH',
1290 help = 'section as name and contents [NAME section] or section to print',
1297 config_key
= 'UKI/PCRBanks',
1303 help = 'OpenSSL engine to use for signing',
1304 config_key
= 'UKI/SigningEngine',
1308 choices
= ('sbsign', 'pesign'),
1310 help = 'whether to use sbsign or pesign. It will also be inferred by the other \
1311 parameters given: when using --secureboot-{private-key/certificate}, sbsign \
1312 will be used, otherwise pesign will be used',
1313 config_key
= 'UKI/SecureBootSigningTool',
1316 '--secureboot-private-key',
1318 help = 'required by --signtool=sbsign. Path to key file or engine-specific designation for SB signing',
1319 config_key
= 'UKI/SecureBootPrivateKey',
1322 '--secureboot-certificate',
1324 help = 'required by --signtool=sbsign. sbsign needs a path to certificate file or engine-specific designation for SB signing',
1325 config_key
= 'UKI/SecureBootCertificate',
1328 '--secureboot-certificate-dir',
1329 dest
= 'sb_certdir',
1330 default
= '/etc/pki/pesign',
1331 help = 'required by --signtool=pesign. Path to nss certificate database directory for PE signing. Default is /etc/pki/pesign',
1332 config_key
= 'UKI/SecureBootCertificateDir',
1333 config_push
= ConfigItem
.config_set
1336 '--secureboot-certificate-name',
1337 dest
= 'sb_cert_name',
1338 help = 'required by --signtool=pesign. pesign needs a certificate nickname of nss certificate database entry to use for PE signing',
1339 config_key
= 'UKI/SecureBootCertificateName',
1342 '--secureboot-certificate-validity',
1345 dest
= 'sb_cert_validity',
1347 help = "period of validity (in days) for a certificate created by 'genkey'",
1348 config_key
= 'UKI/SecureBootCertificateValidity',
1349 config_push
= ConfigItem
.config_set
1354 action
= argparse
.BooleanOptionalAction
,
1355 help = 'Sign the embedded kernel',
1356 config_key
= 'UKI/SignKernel',
1360 '--pcr-private-key',
1361 dest
= 'pcr_private_keys',
1363 type = pathlib
.Path
,
1365 help = 'private part of the keypair for signing PCR signatures',
1366 config_key
= 'PCRSignature:/PCRPrivateKey',
1367 config_push
= ConfigItem
.config_set_group
,
1371 dest
= 'pcr_public_keys',
1373 type = pathlib
.Path
,
1375 help = 'public part of the keypair for signing PCR signatures',
1376 config_key
= 'PCRSignature:/PCRPublicKey',
1377 config_push
= ConfigItem
.config_set_group
,
1381 dest
= 'phase_path_groups',
1382 metavar
= 'PHASE-PATH…',
1383 type = parse_phase_paths
,
1385 help = 'phase-paths to create signatures for',
1386 config_key
= 'PCRSignature:/Phases',
1387 config_push
= ConfigItem
.config_set_group
,
1392 type = pathlib
.Path
,
1394 help = 'Directories to search for tools (systemd-measure, …)',
1399 type = pathlib
.Path
,
1400 help = 'output file path',
1405 action
= argparse
.BooleanOptionalAction
,
1406 help = 'print systemd-measure output for the UKI',
1411 choices
= ('pretty', 'short', 'off'),
1413 help = 'generate JSON output',
1418 action
='store_const',
1420 help='equivalent to --json=pretty',
1425 help = 'print all sections',
1426 action
= 'store_true',
1430 CONFIGFILE_ITEMS
= { item
.config_key
:item
1431 for item
in CONFIG_ITEMS
1432 if item
.config_key
}
1435 def apply_config(namespace
, filename
=None):
1436 if filename
is None:
1437 if namespace
.config
:
1438 # Config set by the user, use that.
1439 filename
= namespace
.config
1440 print(f
'Using config file: {filename}')
1442 # Try to look for a config file then use the first one found.
1443 for config_dir
in DEFAULT_CONFIG_DIRS
:
1444 filename
= pathlib
.Path(config_dir
) / DEFAULT_CONFIG_FILE
1445 if filename
.is_file():
1446 # Found a config file, use it.
1447 print(f
'Using found config file: {filename}')
1450 # No config file specified or found, nothing to do.
1453 # Fill in ._groups based on --pcr-public-key=, --pcr-private-key=, and --phases=.
1454 assert '_groups' not in namespace
1455 n_pcr_priv
= len(namespace
.pcr_private_keys
or ())
1456 namespace
._groups
= list(range(n_pcr_priv
)) # pylint: disable=protected-access
1458 cp
= configparser
.ConfigParser(
1459 comment_prefixes
='#',
1460 inline_comment_prefixes
='#',
1462 empty_lines_in_values
=False,
1465 # Do not make keys lowercase
1466 cp
.optionxform
= lambda option
: option
1468 # The API is not great.
1469 read
= cp
.read(filename
)
1471 raise IOError(f
'Failed to read {filename}')
1473 for section_name
, section
in cp
.items():
1474 idx
= section_name
.find(':')
1476 section_name
, group
= section_name
[:idx
+1], section_name
[idx
+1:]
1477 if not section_name
or not group
:
1478 raise ValueError('Section name components cannot be empty')
1480 raise ValueError('Section name cannot contain more than one ":"')
1483 for key
, value
in section
.items():
1484 if item
:= CONFIGFILE_ITEMS
.get(f
'{section_name}/{key}'):
1485 item
.apply_config(namespace
, section_name
, group
, key
, value
)
1487 print(f
'Unknown config setting [{section_name}] {key}=')
1490 def config_example():
1492 for item
in CONFIG_ITEMS
:
1493 section
, key
, value
= item
.config_example()
1495 if prev_section
!= section
:
1498 yield f
'[{section}]'
1499 prev_section
= section
1500 yield f
'{key} = {value}'
1503 class PagerHelpAction(argparse
._HelpAction
): # pylint: disable=protected-access
1506 parser
: argparse
.ArgumentParser
,
1507 namespace
: argparse
.Namespace
,
1508 values
: Union
[str, Sequence
[Any
], None] = None,
1509 option_string
: Optional
[str] = None
1511 page(parser
.format_help(), True)
1515 def create_parser():
1516 p
= argparse
.ArgumentParser(
1517 description
='Build and sign Unified Kernel Images',
1518 usage
='\n ' + textwrap
.dedent('''\
1519 ukify {b}build{e} [--linux=LINUX] [--initrd=INITRD] [options…]
1520 ukify {b}genkey{e} [options…]
1521 ukify {b}inspect{e} FILE… [options…]
1522 ''').format(b
=Style
.bold
, e
=Style
.reset
),
1525 epilog
='\n '.join(('config file:', *config_example())),
1526 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
1529 for item
in CONFIG_ITEMS
:
1532 # Suppress printing of usage synopsis on errors
1533 p
.error
= lambda message
: p
.exit(2, f
'{p.prog}: error: {message}\n')
1538 action
=PagerHelpAction
,
1539 help='show this help message and exit',
1545 def finalize_options(opts
):
1546 # Figure out which syntax is being used, one of:
1547 # ukify verb --arg --arg --arg
1548 # ukify linux initrd…
1549 if len(opts
.positional
) >= 1 and opts
.positional
[0] == 'inspect':
1550 opts
.verb
= opts
.positional
[0]
1551 opts
.files
= opts
.positional
[1:]
1553 raise ValueError('file(s) to inspect must be specified')
1554 if len(opts
.files
) > 1 and opts
.json
!= 'off':
1555 # We could allow this in the future, but we need to figure out the right structure
1556 raise ValueError('JSON output is not allowed with multiple files')
1557 elif len(opts
.positional
) == 1 and opts
.positional
[0] in VERBS
:
1558 opts
.verb
= opts
.positional
[0]
1559 elif opts
.linux
or opts
.initrd
:
1560 raise ValueError('--linux/--initrd options cannot be used with positional arguments')
1562 print("Assuming obsolete command line syntax with no verb. Please use 'build'.")
1564 opts
.linux
= pathlib
.Path(opts
.positional
[0])
1565 # If we have initrds from parsing config files, append our positional args at the end
1566 opts
.initrd
= (opts
.initrd
or []) + [pathlib
.Path(arg
) for arg
in opts
.positional
[1:]]
1569 # Check that --pcr-public-key=, --pcr-private-key=, and --phases=
1570 # have either the same number of arguments are are not specified at all.
1571 n_pcr_pub
= None if opts
.pcr_public_keys
is None else len(opts
.pcr_public_keys
)
1572 n_pcr_priv
= None if opts
.pcr_private_keys
is None else len(opts
.pcr_private_keys
)
1573 n_phase_path_groups
= None if opts
.phase_path_groups
is None else len(opts
.phase_path_groups
)
1574 if n_pcr_pub
is not None and n_pcr_pub
!= n_pcr_priv
:
1575 raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=')
1576 if n_phase_path_groups
is not None and n_phase_path_groups
!= n_pcr_priv
:
1577 raise ValueError('--phases= specifications must match --pcr-private-key=')
1579 if opts
.cmdline
and opts
.cmdline
.startswith('@'):
1580 opts
.cmdline
= pathlib
.Path(opts
.cmdline
[1:])
1582 # Drop whitespace from the command line. If we're reading from a file,
1583 # we copy the contents verbatim. But configuration specified on the command line
1584 # or in the config file may contain additional whitespace that has no meaning.
1585 opts
.cmdline
= ' '.join(opts
.cmdline
.split())
1587 if opts
.os_release
and opts
.os_release
.startswith('@'):
1588 opts
.os_release
= pathlib
.Path(opts
.os_release
[1:])
1589 elif not opts
.os_release
and opts
.linux
:
1590 p
= pathlib
.Path('/etc/os-release')
1592 p
= pathlib
.Path('/usr/lib/os-release')
1595 if opts
.efi_arch
is None:
1596 opts
.efi_arch
= guess_efi_arch()
1598 if opts
.stub
is None:
1599 if opts
.linux
is not None:
1600 opts
.stub
= pathlib
.Path(f
'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
1602 opts
.stub
= pathlib
.Path(f
'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub')
1604 if opts
.signing_engine
is None:
1606 opts
.sb_key
= pathlib
.Path(opts
.sb_key
)
1608 opts
.sb_cert
= pathlib
.Path(opts
.sb_cert
)
1610 if bool(opts
.sb_key
) ^
bool(opts
.sb_cert
):
1611 # one param only given, sbsign needs both
1612 raise ValueError('--secureboot-private-key= and --secureboot-certificate= must be specified together')
1613 elif bool(opts
.sb_key
) and bool(opts
.sb_cert
):
1614 # both param given, infer sbsign and in case it was given, ensure signtool=sbsign
1615 if opts
.signtool
and opts
.signtool
!= 'sbsign':
1616 raise ValueError(f
'Cannot provide --signtool={opts.signtool} with --secureboot-private-key= and --secureboot-certificate=')
1617 opts
.signtool
= 'sbsign'
1618 elif bool(opts
.sb_cert_name
):
1619 # sb_cert_name given, infer pesign and in case it was given, ensure signtool=pesign
1620 if opts
.signtool
and opts
.signtool
!= 'pesign':
1621 raise ValueError(f
'Cannot provide --signtool={opts.signtool} with --secureboot-certificate-name=')
1622 opts
.signtool
= 'pesign'
1624 if opts
.sign_kernel
and not opts
.sb_key
and not opts
.sb_cert_name
:
1625 raise ValueError('--sign-kernel requires either --secureboot-private-key= and --secureboot-certificate= (for sbsign) or --secureboot-certificate-name= (for pesign) to be specified')
1627 if opts
.verb
== 'build' and opts
.output
is None:
1628 if opts
.linux
is None:
1629 raise ValueError('--output= must be specified when building a PE addon')
1630 suffix
= '.efi' if opts
.sb_key
or opts
.sb_cert_name
else '.unsigned.efi'
1631 opts
.output
= opts
.linux
.name
+ suffix
1633 # Now that we know if we're inputting or outputting, really parse section config
1634 f
= Section
.parse_output
if opts
.verb
== 'inspect' else Section
.parse_input
1635 opts
.sections
= [f(s
) for s
in opts
.sections
]
1636 # A convenience dictionary to make it easy to look up sections
1637 opts
.sections_by_name
= {s
.name
:s
for s
in opts
.sections
}
1640 # TODO: replace pprint() with some fancy formatting.
1641 pprint
.pprint(vars(opts
))
1645 def parse_args(args
=None):
1646 opts
= create_parser().parse_args(args
)
1648 finalize_options(opts
)
1654 if opts
.verb
== 'build':
1657 elif opts
.verb
== 'genkey':
1658 check_cert_and_keys_nonexistent(opts
)
1660 elif opts
.verb
== 'inspect':
1661 inspect_sections(opts
)
1666 if __name__
== '__main__':