ukify: add 'build' verb

The old syntax with linux + initrds as positional arguments is still accepted,
but a warning is emitted. We should remove the support for this after the
next release or so.

Adding a single verb by itself is not very useful, but opens the door to adding
other verbs.
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2023-06-06 13:23:49 +02:00
parent 320266b961
commit a3b227d28a
3 changed files with 137 additions and 35 deletions

View file

@ -23,9 +23,8 @@
<refsynopsisdiv>
<cmdsynopsis>
<command>/usr/lib/systemd/ukify</command>
<arg choice="opt"><replaceable>LINUX</replaceable></arg>
<arg choice="opt" rep="repeat"><replaceable>INITRD</replaceable></arg>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="plain">build</arg>
</cmdsynopsis>
</refsynopsisdiv>
@ -35,13 +34,18 @@
<para>Note: this command is experimental for now. While it is intended to become a regular component of
systemd, it might still change in behaviour and interface.</para>
<para><command>ukify</command> is a tool that combines components (e.g.: a kernel and an initrd with
a UEFI boot stub) to create a
<para><command>ukify</command> is a tool that combines components (usually a kernel, an initrd, and a
UEFI boot stub) to create a
<ulink url="https://uapi-group.org/specifications/specs/unified_kernel_image/">Unified Kernel Image (UKI)</ulink>
— a PE binary that can be executed by the firmware to start the embedded linux kernel.
See <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>
for details about the stub.</para>
<para>The two primary options that should be specified for the <command>build</command> verb are
<varname>Linux=</varname>/<option>--linux=</option>, and
<varname>Initrd=</varname>/<option>--initrd=</option>. <varname>Initrd=</varname> accepts multiple
whitespace-separated paths and <option>--initrd=</option> can be specified multiple times.</para>
<para>Additional sections will be inserted into the UKI, either automatically or only if a specific
option is provided. See the discussions of
<varname>Cmdline=</varname>/<option>--cmdline=</option>,
@ -173,14 +177,14 @@
<variablelist>
<varlistentry>
<term><varname>Linux=<replaceable>LINUX</replaceable></varname></term>
<term>positional argument <replaceable>LINUX</replaceable></term>
<term><option>--linux=<replaceable>LINUX</replaceable></option></term>
<listitem><para>A path to the kernel binary.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>Initrd=<replaceable>INITRD</replaceable>...</varname></term>
<term>positional argument <replaceable>INITRD</replaceable></term>
<term><option>--initrd=<replaceable>LINUX</replaceable></option></term>
<listitem><para>Zero or more initrd paths. In the configuration file, items are separated by
whitespace. The initrds are combined in the order of specification, with the initrds specified in
@ -399,9 +403,9 @@
<example>
<title>Minimal invocation</title>
<programlisting>$ ukify \
/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
<programlisting>$ ukify build \
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
--cmdline='quiet rw'
</programlisting>
@ -411,10 +415,10 @@
<example>
<title>All the bells and whistles</title>
<programlisting># /usr/lib/systemd/ukify \
/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
early_cpio \
/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
<programlisting># /usr/lib/systemd/ukify build \
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
--initrd=early_cpio \
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
--pcr-private-key=pcr-private-initrd-key.pem \
--pcr-public-key=pcr-public-initrd-key.pem \
--phases='enter-initrd' \
@ -468,9 +472,9 @@ Phases=enter-initrd:leave-initrd
enter-initrd:leave-initrd:sysinit
enter-initrd:leave-initrd:sysinit:ready
# /usr/lib/systemd/ukify -c ukify.conf \
/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
/some/path/initramfs-6.0.9-300.fc37.x86_64.img
# /usr/lib/systemd/ukify -c ukify.conf build \
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img
</programlisting>
<para>One "initrd" (<filename index='false'>early_cpio</filename>) is specified in the config file, and
@ -482,7 +486,7 @@ Phases=enter-initrd:leave-initrd
<example>
<title>Kernel command line auxiliary PE</title>
<programlisting>ukify \
<programlisting>ukify build \
--secureboot-private-key=sb.key \
--secureboot-certificate=sb.cert \
--cmdline='debug' \

View file

@ -52,7 +52,7 @@ def test_round_up():
def test_namespace_creation():
ns = ukify.create_parser().parse_args(())
assert ns.linux is None
assert ns.initrd == []
assert ns.initrd is None
def test_config_example():
ex = ukify.config_example()
@ -143,7 +143,7 @@ def test_parse_args_minimal():
assert opts.os_release in (pathlib.Path('/etc/os-release'),
pathlib.Path('/usr/lib/os-release'))
def test_parse_args_many():
def test_parse_args_many_deprecated():
opts = ukify.parse_args(
['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
'--cmdline=a b c',
@ -186,9 +186,57 @@ def test_parse_args_many():
assert opts.output == pathlib.Path('OUTPUT')
assert opts.measure is False
def test_parse_args_many():
opts = ukify.parse_args(
['build',
'--linux=/ARG1',
'--initrd=///ARG2',
'--initrd=/ARG3 WITH SPACE',
'--cmdline=a b c',
'--os-release=K1=V1\nK2=V2',
'--devicetree=DDDDTTTT',
'--splash=splash',
'--pcrpkey=PATH',
'--uname=1.2.3',
'--stub=STUBPATH',
'--pcr-private-key=PKEY1',
'--pcr-public-key=PKEY2',
'--pcr-banks=SHA1,SHA256',
'--signing-engine=ENGINE',
'--secureboot-private-key=SBKEY',
'--secureboot-certificate=SBCERT',
'--sign-kernel',
'--no-sign-kernel',
'--tools=TOOLZ///',
'--output=OUTPUT',
'--measure',
'--no-measure',
])
assert opts.linux == pathlib.Path('/ARG1')
assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
assert opts.cmdline == 'a b c'
assert opts.os_release == 'K1=V1\nK2=V2'
assert opts.devicetree == pathlib.Path('DDDDTTTT')
assert opts.splash == pathlib.Path('splash')
assert opts.pcrpkey == pathlib.Path('PATH')
assert opts.uname == '1.2.3'
assert opts.stub == pathlib.Path('STUBPATH')
assert opts.pcr_private_keys == [pathlib.Path('PKEY1')]
assert opts.pcr_public_keys == [pathlib.Path('PKEY2')]
assert opts.pcr_banks == ['SHA1', 'SHA256']
assert opts.signing_engine == 'ENGINE'
assert opts.sb_key == 'SBKEY'
assert opts.sb_cert == 'SBCERT'
assert opts.sign_kernel is False
assert opts.tools == [pathlib.Path('TOOLZ/')]
assert opts.output == pathlib.Path('OUTPUT')
assert opts.measure is False
def test_parse_sections():
opts = ukify.parse_args(
['/ARG1', '/ARG2',
['build',
'--linux=/ARG1',
'--initrd=/ARG2',
'--section=test:TESTTESTTEST',
'--section=test2:@FILE',
])
@ -239,7 +287,10 @@ def test_config_priority(tmp_path):
'''))
opts = ukify.parse_args(
['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
['build',
'--linux=/ARG1',
'--initrd=///ARG2',
'--initrd=/ARG3 WITH SPACE',
'--cmdline= a b c ',
'--os-release=K1=V1\nK2=V2',
'--devicetree=DDDDTTTT',
@ -302,7 +353,7 @@ def test_help(capsys):
assert '--section' in out.out
assert not out.err
def test_help_error(capsys):
def test_help_error_deprecated(capsys):
with pytest.raises(SystemExit):
ukify.parse_args(['a', 'b', '--no-such-option'])
out = capsys.readouterr()
@ -310,6 +361,14 @@ def test_help_error(capsys):
assert '--no-such-option' in out.err
assert len(out.err.splitlines()) == 1
def test_help_error(capsys):
with pytest.raises(SystemExit):
ukify.parse_args(['build', '--no-such-option'])
out = capsys.readouterr()
assert not out.out
assert '--no-such-option' in out.err
assert len(out.err.splitlines()) == 1
@pytest.fixture(scope='session')
def kernel_initrd():
try:
@ -326,7 +385,7 @@ def kernel_initrd():
initrd = f"{item['root']}{item['initrd'][0].split(' ')[0]}"
except (KeyError, IndexError):
continue
return [linux, initrd]
return ['--linux', linux, '--initrd', initrd]
else:
return None
@ -345,7 +404,11 @@ def test_basic_operation(kernel_initrd, tmpdir):
pytest.skip('linux+initrd not found')
output = f'{tmpdir}/basic.efi'
opts = ukify.parse_args(kernel_initrd + [f'--output={output}'])
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
])
try:
ukify.check_inputs(opts)
except OSError as e:
@ -362,6 +425,7 @@ def test_sections(kernel_initrd, tmpdir):
output = f'{tmpdir}/basic.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -386,6 +450,7 @@ def test_sections(kernel_initrd, tmpdir):
def test_addon(kernel_initrd, tmpdir):
output = f'{tmpdir}/addon.efi'
args = [
'build',
f'--output={output}',
'--cmdline=ARG1 ARG2 ARG3',
'--section=.test:CONTENTZ',
@ -422,7 +487,8 @@ def test_uname_scraping(kernel_initrd):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
uname = ukify.Uname.scrape(kernel_initrd[0])
assert kernel_initrd[0] == '--linux'
uname = ukify.Uname.scrape(kernel_initrd[1])
assert re.match(r'\d+\.\d+\.\d+', uname)
def test_efi_signing_sbsign(kernel_initrd, tmpdir):
@ -437,6 +503,7 @@ def test_efi_signing_sbsign(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -480,6 +547,7 @@ def test_efi_signing_pesign(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -514,6 +582,7 @@ def test_pcr_signing(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -576,8 +645,12 @@ def test_pcr_signing2(kernel_initrd, tmpdir):
microcode.write(b'1234567890')
output = f'{tmpdir}/signed.efi'
assert kernel_initrd[0] == '--linux'
opts = ukify.parse_args([
kernel_initrd[0], microcode.name, kernel_initrd[1],
'build',
*kernel_initrd[:2],
f'--initrd={microcode.name}',
*kernel_initrd[2:],
f'--output={output}',
'--uname=1.2.3',
'--cmdline=ARG1 ARG2 ARG3',

View file

@ -438,7 +438,7 @@ def call_systemd_measure(uki, linux, opts):
def join_initrds(initrds):
if len(initrds) == 0:
if not initrds:
return None
if len(initrds) == 1:
return initrds[0]
@ -820,7 +820,10 @@ class ConfigItem:
else:
conv = lambda s:s
if self.nargs == '*':
# This is a bit ugly, but --initrd is the only option which is specified
# with multiple args on the command line and a space-separated list in the
# config file.
if self.name == '--initrd':
value = [conv(v) for v in value.split()]
else:
value = conv(value)
@ -840,7 +843,16 @@ class ConfigItem:
return (section_name, key, value)
VERBS = ('build',)
CONFIG_ITEMS = [
ConfigItem(
'positional',
metavar = 'VERB',
nargs = '*',
help = f"operation to perform ({','.join(VERBS)})",
),
ConfigItem(
'--version',
action = 'version',
@ -854,20 +866,18 @@ CONFIG_ITEMS = [
),
ConfigItem(
'linux',
metavar = 'LINUX',
'--linux',
type = pathlib.Path,
nargs = '?',
help = 'vmlinuz file [.linux section]',
config_key = 'UKI/Linux',
),
ConfigItem(
'initrd',
metavar = 'INITRD',
'--initrd',
metavar = 'INITRD',
type = pathlib.Path,
nargs = '*',
help = 'initrd files [.initrd section]',
action = 'append',
help = 'initrd file [part of .initrd section]',
config_key = 'UKI/Initrd',
config_push = ConfigItem.config_list_prepend,
),
@ -1199,6 +1209,20 @@ def parse_args(args=None):
p = create_parser()
opts = p.parse_args(args)
# Figure out which syntax is being used, one of:
# ukify verb --arg --arg --arg
# ukify linux initrd…
if len(opts.positional) == 1 and opts.positional[0] in VERBS:
opts.verb = opts.positional[0]
elif opts.linux or opts.initrd:
raise ValueError('--linux/--initrd options cannot be used with positional arguments')
else:
print("Assuming obsolete commandline syntax with no verb. Please use 'build'.")
if opts.positional:
opts.linux = pathlib.Path(opts.positional[0])
opts.initrd = [pathlib.Path(arg) for arg in opts.positional[1:]]
opts.verb = 'build'
# Check that --pcr-public-key=, --pcr-private-key=, and --phases=
# have either the same number of arguments are are not specified at all.
n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys)
@ -1219,6 +1243,7 @@ def parse_args(args=None):
def main():
opts = parse_args()
check_inputs(opts)
assert opts.verb == 'build'
make_uki(opts)