Merge pull request #27262 from keszybz/ukify-install

Add kernel-install plugin that calls ukify
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2023-05-06 13:34:08 +02:00 committed by GitHub
commit 9dfed0d4cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1315 additions and 427 deletions

7
TODO
View file

@ -840,9 +840,7 @@ Features:
virtio-fs.
* for vendor-built signed initrds:
- kernel-install should be able to install pre-built unified kernel images in
type #2 drop-in dir in the ESP.
- kernel-install should be able install encrypted creds automatically for
- kernel-install should be able to install encrypted creds automatically for
machine id, root pw, rootfs uuid, resume partition uuid, and place next to
EFI kernel, for sd-stub to pick them up. These creds should be locked to
the TPM, and bind to the right PCR the kernel is measured to.
@ -1915,9 +1913,6 @@ Features:
- teach it to prepare an ESP wholesale, i.e. with mkfs.vfat invocation
- teach it to copy in unified kernel images and maybe type #1 boot loader spec entries from host
* kernel-install:
- optionally, support generating type #2 entries instead of type #1, including signing them
* logind:
- logind: optionally, ignore idle-hint logic for autosuspend, block suspend as long as a session is around
- logind: wakelock/opportunistic suspend support

View file

@ -44,212 +44,302 @@
<para>Additional sections will be inserted into the UKI, either automatically or only if a specific
option is provided. See the discussions of
<option>--cmdline=</option>,
<option>--os-release=</option>,
<option>--devicetree=</option>,
<option>--splash=</option>,
<option>--pcrpkey=</option>,
<option>--uname=</option>,
<varname>Cmdline=</varname>/<option>--cmdline=</option>,
<varname>OSRelease=</varname>/<option>--os-release=</option>,
<varname>DeviceTree=</varname>/<option>--devicetree=</option>,
<varname>Splash=</varname>/<option>--splash=</option>,
<varname>PCRPKey=</varname>/<option>--pcrpkey=</option>,
<varname>Uname=</varname>/<option>--uname=</option>,
and <option>--section=</option>
below.</para>
<para><command>ukify</command> can also be used to assemble a PE binary that is not executable but
contains auxiliary data, for example additional kernel command line entries.</para>
<para>If PCR signing keys are provided via the <option>--pcr-public-key=</option> and
<option>--pcr-private-key=</option> options, PCR values that will be seen after booting with the given
kernel, initrd, and other sections, will be calculated, signed, and embedded in the UKI.
<para>If PCR signing keys are provided via the
<varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option> and
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> options, PCR values that will be seen
after booting with the given kernel, initrd, and other sections, will be calculated, signed, and embedded
in the UKI.
<citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry> is
used to perform this calculation and signing.</para>
<para>The calculation of PCR values is done for specific boot phase paths. Those can be specified with
the <option>--phases=</option> option. If not specified, the default provided by
<command>systemd-measure</command> is used. It is also possible to specify the
<option>--pcr-private-key=</option>, <option>--pcr-public-key=</option>, and <option>--phases=</option>
arguments more than once. Signatures will be then performed with each of the specified keys. When both
<option>--phases=</option> and <option>--pcr-private-key=</option> are used, they must be specified the
same number of times, and then the n-th boot phase path set will be signed by the n-th key. This can be
used to build different trust policies for different phases of the boot.</para>
the <varname>Phases=</varname>/<option>--phases=</option> option. If not specified, the default provided
by <command>systemd-measure</command> is used. It is also possible to specify the
<varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>,
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option>, and
<varname>Phases=</varname>/<option>--phases=</option> arguments more than once. Signatures will then be
performed with each of the specified keys. On the command line, when both <option>--phases=</option> and
<option>--pcr-private-key=</option> are used, they must be specified the same number of times, and then
the n-th boot phase path set will be signed by the n-th key. This can be used to build different trust
policies for different phases of the boot. In the config file, <varname>PCRPrivateKey=</varname>,
<varname>PCRPublicKey=</varname>, and <varname>Phases=</varname> are grouped into separate sections,
describing separate boot phases.</para>
<para>If a SecureBoot signing key is provided via the <option>--secureboot-private-key=</option> option,
the resulting PE binary will be signed as a whole, allowing the resulting UKI to be trusted by
SecureBoot. Also see the discussion of automatic enrollment in
<para>If a SecureBoot signing key is provided via the
<varname>SecureBootPrivateKey=</varname>/<option>--secureboot-private-key=</option> option, the resulting
PE binary will be signed as a whole, allowing the resulting UKI to be trusted by SecureBoot. Also see the
discussion of automatic enrollment in
<citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry>.
</para>
</refsect1>
<refsect1>
<title>Options</title>
<title>Configuration settings</title>
<para>The <replaceable>LINUX</replaceable> and <replaceable>INITRD</replaceable> positional arguments are
optional. If more than one <replaceable>INITRD</replaceable> are specified, they will all be combined into
a single PE section. This is useful to for example prepend microcode before the actual initrd.</para>
<para>Settings can appear in configuration files (the syntax with <varname
index='false'>SomeSetting=<replaceable>value</replaceable></varname>) and on the command line (the syntax
with <option index='false'>--some-setting=<replaceable>value</replaceable></option>). For some command
line parameters, a single-letter shortcut is also allowed. In the configuration files, the setting must
be in the appropriate section, so the descriptions are grouped by section below. When the same setting
appears in the configuration file and on the command line, generally the command line setting has higher
priority and overwrites the config file setting completely. If some setting behaves differently, this is
described below.</para>
<para>The following options are understood:</para>
<para>The <replaceable>LINUX</replaceable> and <replaceable>INITRD</replaceable> positional arguments, or
the equivalent <varname>Linux=</varname> and <varname>Initrd=</varname> settings, are optional. If more
than one initrd is specified, they will all be combined into a single PE section. This is useful to, for
example, prepend microcode before the actual initrd.</para>
<variablelist>
<varlistentry>
<term><option>--cmdline=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
<para>The following options and settings are understood:</para>
<listitem><para>Specify the kernel command line (the <literal>.cmdline</literal> section). The
argument may be a literal string, or <literal>@</literal> followed by a path name. If not specified,
no command line will be embedded.</para></listitem>
</varlistentry>
<refsect2>
<title>Commandline-only options</title>
<varlistentry>
<term><option>--os-release=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
<variablelist>
<varlistentry>
<term><option>--config=<replaceable>PATH</replaceable></option></term>
<listitem><para>Specify the os-release description (the <literal>.osrel</literal> section). The
argument may be a literal string, or <literal>@</literal> followed by a path name. If not specified,
the <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry>
file will be picked up from the host system.</para></listitem>
</varlistentry>
<listitem><para>Load configuration from the given config file. In general, settings specified in
the config file have lower precedence than the settings specified via options. In cases where the
commandline option does not fully override the config file setting are explicitly mentioned in the
descriptions of individual options.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--devicetree=<replaceable>PATH</replaceable></option></term>
<varlistentry>
<term><option>--measure</option></term>
<term><option>--no-measure</option></term>
<listitem><para>Specify the devicetree description (the <literal>.dtb</literal> section). The
argument is a path to a compiled binary DeviceTree file. If not specified, the section will not be
present.</para></listitem>
</varlistentry>
<listitem><para>Enable or disable a call to <command>systemd-measure</command> to print
pre-calculated PCR values. Defaults to false.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--splash=<replaceable>PATH</replaceable></option></term>
<varlistentry>
<term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
<listitem><para>Specify a picture to display during boot (the <literal>.splash</literal> section).
The argument is a path to a BMP file. If not specified, the section will not be present.
</para></listitem>
</varlistentry>
<listitem><para>Specify an arbitrary additional section
<literal><replaceable>NAME</replaceable></literal>. Note that the name is used as-is, and if the
section name should start with a dot, it must be included in <replaceable>NAME</replaceable>. The
argument may be a literal string, or <literal>@</literal> followed by a path name. This option may be
specified more than once. Any sections specified in this fashion will be inserted (in order) before
the <literal>.linux</literal> section which is always last.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--pcrpkey=<replaceable>PATH</replaceable></option></term>
<varlistentry>
<term><option>--tools=<replaceable>DIRS</replaceable></option></term>
<listitem><para>Specify a path to a public key to embed in the <literal>.pcrpkey</literal> section.
If not specified, and there's exactly one <option>--pcr-public-key=</option> argument, that key will
be used. Otherwise, the section will not be present.</para></listitem>
</varlistentry>
<listitem><para>Specify one or more directories with helper tools. <command>ukify</command> will
look for helper tools in those directories first, and if not found, try to load them from
<varname>$PATH</varname> in the usual fashion.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--uname=<replaceable>VERSION</replaceable></option></term>
<varlistentry>
<term><option>--output=<replaceable>FILENAME</replaceable></option></term>
<listitem><para>Specify the kernel version (as in <command>uname -r</command>, the
<literal>.uname</literal> section). If not specified, an attempt will be made to extract the version
string from the kernel image. It is recommended to pass this explicitly if known, because the
extraction is based on heuristics and not very reliable. If not specified and extraction fails, the
section will not be present.</para></listitem>
</varlistentry>
<listitem><para>The output filename. If not specified, the name of the
<replaceable>LINUX</replaceable> argument, with the suffix <literal>.unsigned.efi</literal> or
<literal>.signed.efi</literal> will be used, depending on whether signing for SecureBoot was
performed.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
<varlistentry>
<term><option>--summary</option></term>
<listitem><para>Specify an arbitrary additional section
<literal><replaceable>NAME</replaceable></literal>. Note that the name is used as-is, and if the
section name should start with a dot, it must be included in <replaceable>NAME</replaceable>. The
argument may be a literal string, or <literal>@</literal> followed by a path name. This option may be
specified more than once. Any sections specified in this fashion will be inserted (in order) before
the <literal>.linux</literal> section which is always last.</para></listitem>
</varlistentry>
<listitem><para>Print a summary of loaded config and exit. This is useful to check how the options
form the configuration file and the commandline are combined.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--pcr-private-key=<replaceable>PATH</replaceable></option></term>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect2>
<listitem><para>Specify a private key to use for signing PCR policies. This option may be specified
more than once, in which case multiple signatures will be made.</para></listitem>
</varlistentry>
<refsect2>
<title>[UKI] section</title>
<varlistentry>
<term><option>--pcr-public-key=<replaceable>PATH</replaceable></option></term>
<variablelist>
<varlistentry>
<term><varname>Linux=<replaceable>LINUX</replaceable></varname></term>
<term>positional argument <replaceable>LINUX</replaceable></term>
<listitem><para>Specify a public key to use for signing PCR policies. This option may be specified
more than once, similarly to the <option>--pcr-private-key=</option> option. If not present, the
public keys will be extracted from the private keys. If present, the this option must be specified
the same number of times as the <option>--pcr-private-key=</option> option.</para></listitem>
</varlistentry>
<listitem><para>A path to the kernel binary.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--phases=<replaceable>LIST</replaceable></option></term>
<varlistentry>
<term><varname>Initrd=<replaceable>INITRD</replaceable>...</varname></term>
<term>positional argument <replaceable>INITRD</replaceable></term>
<listitem><para>A comma or space-separated list of colon-separated phase paths to sign a policy for.
If not present, the default of
<citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry>
will be used. When this argument is present, it must appear the same number of times as the
<option>--pcr-private-key=</option> option. Each set of boot phase paths will be signed with the
corresponding private key.</para></listitem>
</varlistentry>
<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
the config file first.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--pcr-banks=<replaceable>PATH</replaceable></option></term>
<varlistentry>
<term><varname>Cmdline=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></varname></term>
<term><option>--cmdline=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
<listitem><para>A comma or space-separated list of PCR banks to sign a policy for. If not present,
all known banks will be used (<literal>sha1</literal>, <literal>sha256</literal>,
<literal>sha384</literal>, <literal>sha512</literal>), which will fail if not supported by the
system.</para></listitem>
</varlistentry>
<listitem><para>The kernel command line (the <literal>.cmdline</literal> section). The argument may
be a literal string, or <literal>@</literal> followed by a path name. If not specified, no command
line will be embedded.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--secureboot-private-key=<replaceable>SB_KEY</replaceable></option></term>
<varlistentry>
<term><varname>OSRelease=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></varname></term>
<term><option>--os-release=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
<listitem><para>A path to a private key to use for signing of the resulting binary. If the
<option>--signing-engine=</option> option is used, this may also be an engine-specific
designation.</para></listitem>
</varlistentry>
<listitem><para>The os-release description (the <literal>.osrel</literal> section). The argument
may be a literal string, or <literal>@</literal> followed by a path name. If not specified, the
<citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry> file
will be picked up from the host system.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--secureboot-certificate=<replaceable>SB_CERT</replaceable></option></term>
<varlistentry>
<term><varname>DeviceTree=<replaceable>PATH</replaceable></varname></term>
<term><option>--devicetree=<replaceable>PATH</replaceable></option></term>
<listitem><para>A path to a certificate to use for signing of the resulting binary. If the
<option>--signing-engine=</option> option is used, this may also be an engine-specific
designation.</para></listitem>
</varlistentry>
<listitem><para>The devicetree description (the <literal>.dtb</literal> section). The argument is a
path to a compiled binary DeviceTree file. If not specified, the section will not be present.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--signing-engine=<replaceable>ENGINE</replaceable></option></term>
<varlistentry>
<term><varname>Splash=<replaceable>PATH</replaceable></varname></term>
<term><option>--splash=<replaceable>PATH</replaceable></option></term>
<listitem><para>An "engine" to for signing of the resulting binary. This option is currently passed
verbatim to the <option>--engine=</option> option of
<citerefentry project='archlinux'><refentrytitle>sbsign</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
</para></listitem>
</varlistentry>
<listitem><para>A picture to display during boot (the <literal>.splash</literal> section). The
argument is a path to a BMP file. If not specified, the section will not be present.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--sign-kernel</option></term>
<term><option>--no-sign-kernel</option></term>
<varlistentry>
<term><varname>PCRPKey=<replaceable>PATH</replaceable></varname></term>
<term><option>--pcrpkey=<replaceable>PATH</replaceable></option></term>
<listitem><para>Override the detection of whether to sign the Linux binary itself before it is
embedded in the combined image. If not specified, it will be signed if a SecureBoot signing key is
provided via the <option>--secureboot-private-key=</option> option and the binary has not already
been signed. If <option>--sign-kernel</option> is specified, and the binary has already been signed,
the signature will be appended anyway.</para></listitem>
</varlistentry>
<listitem><para>A path to a public key to embed in the <literal>.pcrpkey</literal> section. If not
specified, and there's exactly one
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> argument, that key will be used.
Otherwise, the section will not be present.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--tools=<replaceable>DIRS</replaceable></option></term>
<varlistentry>
<term><varname>Uname=<replaceable>VERSION</replaceable></varname></term>
<term><option>--uname=<replaceable>VERSION</replaceable></option></term>
<listitem><para>Specify one or more directories with helper tools. <command>ukify</command> will look
for helper tools in those directories first, and if not found, try to load them from
<varname>$PATH</varname> in the usual fashion.</para></listitem>
</varlistentry>
<listitem><para>Specify the kernel version (as in <command>uname -r</command>, the
<literal>.uname</literal> section). If not specified, an attempt will be made to extract the
version string from the kernel image. It is recommended to pass this explicitly if known, because
the extraction is based on heuristics and not very reliable. If not specified and extraction fails,
the section will not be present.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--measure</option></term>
<term><option>--no-measure</option></term>
<varlistentry>
<term><varname>PCRBanks=<replaceable>PATH</replaceable></varname></term>
<term><option>--pcr-banks=<replaceable>PATH</replaceable></option></term>
<listitem><para>Enable or disable a call to <command>systemd-measure</command> to print
pre-calculated PCR values. Defaults to false.</para></listitem>
</varlistentry>
<listitem><para>A comma or space-separated list of PCR banks to sign a policy for. If not present,
all known banks will be used (<literal>sha1</literal>, <literal>sha256</literal>,
<literal>sha384</literal>, <literal>sha512</literal>), which will fail if not supported by the
system.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--output=<replaceable>FILENAME</replaceable></option></term>
<varlistentry>
<term><varname>SecureBootPrivateKey=<replaceable>SB_KEY</replaceable></varname></term>
<term><option>--secureboot-private-key=<replaceable>SB_KEY</replaceable></option></term>
<listitem><para>The output filename. If not specified, the name of the
<replaceable>LINUX</replaceable> argument, with the suffix <literal>.unsigned.efi</literal> or
<literal>.signed.efi</literal> will be used, depending on whether signing for SecureBoot was
performed.</para></listitem>
</varlistentry>
<listitem><para>A path to a private key to use for signing of the resulting binary. If the
<varname>SigningEngine=</varname>/<option>--signing-engine=</option> option is used, this may also be
an engine-specific designation.</para></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
<varlistentry>
<term><varname>SecureBootCertificate=<replaceable>SB_CERT</replaceable></varname></term>
<term><option>--secureboot-certificate=<replaceable>SB_CERT</replaceable></option></term>
<listitem><para>A path to a certificate to use for signing of the resulting binary. If the
<varname>SigningEngine=</varname>/<option>--signing-engine=</option> option is used, this may also
be an engine-specific designation.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>SigningEngine=<replaceable>ENGINE</replaceable></varname></term>
<term><option>--signing-engine=<replaceable>ENGINE</replaceable></option></term>
<listitem><para>An "engine" to for signing of the resulting binary. This option is currently passed
verbatim to the <option>--engine=</option> option of
<citerefentry project='archlinux'><refentrytitle>sbsign</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>SignKernel=<replaceable>BOOL</replaceable></varname></term>
<term><option>--sign-kernel</option></term>
<term><option>--no-sign-kernel</option></term>
<listitem><para>Override the detection of whether to sign the Linux binary itself before it is
embedded in the combined image. If not specified, it will be signed if a SecureBoot signing key is
provided via the
<varname>SecureBootPrivateKey=</varname>/<option>--secureboot-private-key=</option> option and the
binary has not already been signed. If
<varname>SignKernel=</varname>/<option>--sign-kernel</option> is true, and the binary has already
been signed, the signature will be appended anyway.</para></listitem>
</varlistentry>
</variablelist>
</refsect2>
<refsect2>
<title>[PCRSignature:<replaceable>NAME</replaceable>] section</title>
<para>In the config file, those options are grouped by section. On the commandline, they
must be specified in the same order. The sections specified in both sources are combined.
</para>
<variablelist>
<varlistentry>
<term><varname>PCRPrivateKey=<replaceable>PATH</replaceable></varname></term>
<term><option>--pcr-private-key=<replaceable>PATH</replaceable></option></term>
<listitem><para>A private key to use for signing PCR policies. On the commandline, this option may
be specified more than once, in which case multiple signatures will be made.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>PCRPublicKey=<replaceable>PATH</replaceable></varname></term>
<term><option>--pcr-public-key=<replaceable>PATH</replaceable></option></term>
<listitem><para>A public key to use for signing PCR policies.</para>
<para>On the commandline, this option may be specified more than once, similarly to the
<option>--pcr-private-key=</option> option. If not present, the public keys will be extracted from
the private keys. On the commandline, if present, the this option must be specified the same number
of times as the <option>--pcr-private-key=</option> option.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>Phases=<replaceable>LIST</replaceable></varname></term>
<term><option>--phases=<replaceable>LIST</replaceable></option></term>
<listitem><para>A comma or space-separated list of colon-separated phase paths to sign a policy
for. Each set of boot phase paths will be signed with the corresponding private key. If not
present, the default of
<citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry>
will be used.</para>
<para>On the commandline, when this argument is present, it must appear the same number of times as
the <option>--pcr-private-key=</option> option. </para></listitem>
</varlistentry>
</variablelist>
</refsect2>
</refsect1>
<refsect1>
@ -258,7 +348,7 @@
<example>
<title>Minimal invocation</title>
<programlisting>ukify \
<programlisting>$ ukify \
/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
--cmdline='quiet rw'
@ -270,7 +360,7 @@
<example>
<title>All the bells and whistles</title>
<programlisting>/usr/lib/systemd/ukify \
<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 \
@ -299,6 +389,45 @@
combined image will be signed with the SecureBoot key <filename index='false'>sb.key</filename>.</para>
</example>
<example>
<title>All the bells and whistles, via a config file</title>
<para>This is the same as the previous example, but this time the configuration is stored in a
file:</para>
<programlisting>$ cat ukify.conf
[UKI]
Initrd=early_cpio
Cmdline=quiet rw rhgb
SecureBootPrivateKey=sb.key
SecureBootCerificate=sb.cert
SignKernel=yes
PCRBanks=sha384,sha512
[PCRSignature:initrd]
PCRPrivateKey=pcr-private-initrd-key.pem
PCRPublicKey=pcr-public-initrd-key.pem
Phases=enter-initrd
[PCRSignature:system]
PCRPrivateKey=pcr-private-system-key.pem
PCRPublicKey=pcr-public-system-key.pem
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
</programlisting>
<para>One "initrd" (<filename index='false'>early_cpio</filename>) is specified in the config file, and
the other initrd (<filename index='false'>initramfs-6.0.9-300.fc37.x86_64.img</filename>) is specified
on the commandline. This may be useful for example when the first initrd contains microcode for the CPU
and does not need to be updated when the kernel version changes, unlike the actual initrd.</para>
</example>
<example>
<title>Kernel command line auxiliary PE</title>
@ -309,7 +438,8 @@
--output=debug.cmdline.efi
</programlisting>
<para>This creates a signed PE binary that contains an additional kernel command line parameter.</para>
<para>This creates a signed PE binary that contains the additional kernel command line parameter
<literal>debug</literal>.</para>
</example>
</refsect1>
@ -319,6 +449,7 @@
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-pcrphase.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
</para>
</refsect1>

View file

@ -2178,6 +2178,9 @@ public_programs = []
# D-Bus introspection XML export
dbus_programs = []
# A list of boot stubs. Required for testing of ukify.
boot_stubs = []
basic_includes = include_directories(
'src/basic',
'src/fundamental',
@ -2507,7 +2510,9 @@ exe = executable(
versiondep],
install_rpath : rootpkglibdir,
install : conf.get('ENABLE_ANALYZE') == 1)
public_programs += exe
if conf.get('ENABLE_ANALYZE') == 1
public_programs += exe
endif
if want_tests != 'false'
test('test-compare-versions',
@ -4006,20 +4011,21 @@ if enable_sysusers
args : exe.full_path())
endif
exe = executable(
'systemd-sysusers.standalone',
'src/sysusers/sysusers.c',
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libbasic_gcrypt,
libsystemd_static],
dependencies : [userspace,
versiondep],
build_by_default: have_standalone_binaries,
install : have_standalone_binaries,
install_dir : rootbindir)
if have_standalone_binaries
exe = executable(
'systemd-sysusers.standalone',
'src/sysusers/sysusers.c',
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libbasic_gcrypt,
libsystemd_static],
dependencies : [userspace,
versiondep],
install : true,
install_dir : rootbindir)
public_programs += exe
if want_tests != 'false'
@ -4052,21 +4058,22 @@ if conf.get('ENABLE_TMPFILES') == 1
args : exe.full_path())
endif
exe = executable(
'systemd-tmpfiles.standalone',
systemd_tmpfiles_sources,
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libbasic_gcrypt,
libsystemd_static],
dependencies : [libacl,
userspace,
versiondep],
build_by_default: have_standalone_binaries,
install : have_standalone_binaries,
install_dir : rootbindir)
if have_standalone_binaries
exe = executable(
'systemd-tmpfiles.standalone',
systemd_tmpfiles_sources,
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libbasic_gcrypt,
libsystemd_static],
dependencies : [libacl,
userspace,
versiondep],
install : true,
install_dir : rootbindir)
public_programs += exe
if want_tests != 'false'
@ -4166,26 +4173,27 @@ if conf.get('ENABLE_REPART') == 1
install_dir : rootbindir)
public_programs += exe
exe = executable(
'systemd-repart.standalone',
systemd_repart_sources,
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libbasic_gcrypt,
libsystemd_static,
libshared_fdisk],
dependencies : [libblkid,
libfdisk,
libopenssl,
threads,
userspace,
versiondep],
build_by_default: have_standalone_binaries,
install_rpath : rootpkglibdir,
install : have_standalone_binaries,
install_dir : rootbindir)
if have_standalone_binaries
exe = executable(
'systemd-repart.standalone',
systemd_repart_sources,
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libbasic_gcrypt,
libsystemd_static,
libshared_fdisk],
dependencies : [libblkid,
libfdisk,
libopenssl,
threads,
userspace,
versiondep],
install_rpath : rootpkglibdir,
install : true,
install_dir : rootbindir)
public_programs += exe
endif
endif
@ -4202,21 +4210,23 @@ executable(
install : true,
install_dir : rootlibexecdir)
executable(
'systemd-shutdown.standalone',
systemd_shutdown_sources,
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libsystemd_static],
dependencies : [libmount,
userspace,
versiondep],
build_by_default: have_standalone_binaries,
install_rpath : rootpkglibdir,
install : have_standalone_binaries,
install_dir : rootlibexecdir)
if have_standalone_binaries
executable(
'systemd-shutdown.standalone',
systemd_shutdown_sources,
include_directories : includes,
c_args : '-DSTANDALONE',
link_with : [libshared_static,
libbasic,
libsystemd_static],
dependencies : [libmount,
userspace,
versiondep],
install_rpath : rootpkglibdir,
install : true,
install_dir : rootlibexecdir)
public_programs += exe
endif
executable(
@ -4345,7 +4355,7 @@ executable(
install : true,
install_dir : rootlibexecdir)
exe = custom_target(
kernel_install = custom_target(
'kernel-install',
input : kernel_install_in,
output : 'kernel-install',
@ -4353,25 +4363,32 @@ exe = custom_target(
install : want_kernel_install,
install_mode : 'rwxr-xr-x',
install_dir : bindir)
public_programs += exe
if want_tests != 'false' and want_kernel_install
test('test-kernel-install',
test_kernel_install_sh,
env : test_env,
args : [exe.full_path(), loaderentry_install])
if want_kernel_install
public_programs += exe
endif
if want_ukify
exe = custom_target(
ukify = custom_target(
'ukify',
input : 'src/ukify/ukify.py',
output : 'ukify',
command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
install : true,
install : want_ukify,
install_mode : 'rwxr-xr-x',
install_dir : rootlibexecdir)
public_programs += exe
if want_ukify
public_programs += ukify
endif
if want_tests != 'false' and want_kernel_install
args = [kernel_install.full_path(), loaderentry_install, uki_copy_install]
if want_ukify and boot_stubs.length() > 0
args += [ukify.full_path(), ukify_install, boot_stubs[0]]
endif
test('test-kernel-install',
test_kernel_install_sh,
env : test_env,
args : args)
endif
############################################################

View file

@ -4,6 +4,9 @@
Distribution=fedora
[Content]
Packages=
python3dist(pytest-flakes)
BuildPackages=
pkgconfig(libgcrypt)
pkgconfig(xencontrol)

View file

@ -27,6 +27,7 @@ Packages=
libxkbcommon0
libzstd1
pam
python3-pytest-flakes
shadow
tpm2-0-tss
xz

View file

@ -334,7 +334,7 @@ foreach efi_elf_binary : efi_elf_binaries
# FIXME: Use build_tgt.name() with meson >= 0.54.0
name = fs.name(efi_elf_binary.full_path()).split('.')[0]
name += name.startswith('linux') ? '.efi.stub' : '.efi'
boot_targets += custom_target(
exe = custom_target(
name,
output : name,
input : efi_elf_binary,
@ -351,6 +351,10 @@ foreach efi_elf_binary : efi_elf_binaries
'@INPUT@',
'@OUTPUT@',
])
boot_targets += exe
if name.startswith('linux')
boot_stubs += exe
endif
endforeach
alias_target('systemd-boot', boot_targets)

View file

@ -0,0 +1,224 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
# -*- mode: python-mode -*-
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# systemd is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with systemd; If not, see <https://www.gnu.org/licenses/>.
# pylint: disable=missing-docstring,invalid-name,import-outside-toplevel
# pylint: disable=consider-using-with,unspecified-encoding,line-too-long
# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
# pylint: disable=too-many-branches,redefined-builtin,fixme
import argparse
import os
import runpy
import shlex
from pathlib import Path
from typing import Optional
__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
try:
VERBOSE = int(os.environ['KERNEL_INSTALL_VERBOSE']) > 0
except (KeyError, ValueError):
VERBOSE = False
# Override location of ukify and the boot stub for testing and debugging.
UKIFY = os.getenv('KERNEL_INSTALL_UKIFY', '/usr/lib/systemd/ukify')
BOOT_STUB = os.getenv('KERNEL_INSTALL_BOOT_STUB')
def shell_join(cmd):
# TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
return ' '.join(shlex.quote(str(x)) for x in cmd)
def log(*args, **kwargs):
if VERBOSE:
print(*args, **kwargs)
def path_is_readable(p: Path, dir=False) -> None:
"""Verify access to a file or directory."""
try:
p.open().close()
except IsADirectoryError:
if dir:
return
raise
def mandatory_variable(name):
try:
return os.environ[name]
except KeyError as e:
raise KeyError(f'${name} must be set in the environment') from e
def parse_args(args=None):
p = argparse.ArgumentParser(
description='kernel-install plugin to build a Unified Kernel Image',
allow_abbrev=False,
usage='60-ukify.install COMMAND KERNEL_VERSION ENTRY_DIR KERNEL_IMAGE INITRD…',
)
# Suppress printing of usage synopsis on errors
p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
p.add_argument('command',
metavar='COMMAND',
help="The action to perform. Only 'add' is supported.")
p.add_argument('kernel_version',
metavar='KERNEL_VERSION',
help='Kernel version string')
p.add_argument('entry_dir',
metavar='ENTRY_DIR',
type=Path,
nargs='?',
help='Type#1 entry directory (ignored)')
p.add_argument('kernel_image',
metavar='KERNEL_IMAGE',
type=Path,
nargs='?',
help='Kernel binary')
p.add_argument('initrd',
metavar='INITRD…',
type=Path,
nargs='*',
help='Initrd files')
p.add_argument('--version',
action='version',
version=f'systemd {__version__}')
opts = p.parse_args(args)
if opts.command == 'add':
opts.staging_area = Path(mandatory_variable('KERNEL_INSTALL_STAGING_AREA'))
path_is_readable(opts.staging_area, dir=True)
opts.entry_token = mandatory_variable('KERNEL_INSTALL_ENTRY_TOKEN')
opts.machine_id = mandatory_variable('KERNEL_INSTALL_MACHINE_ID')
return opts
def we_are_wanted() -> bool:
KERNEL_INSTALL_LAYOUT = os.getenv('KERNEL_INSTALL_LAYOUT')
if KERNEL_INSTALL_LAYOUT != 'uki':
log(f'{KERNEL_INSTALL_LAYOUT=}, quitting.')
return False
KERNEL_INSTALL_UKI_GENERATOR = os.getenv('KERNEL_INSTALL_UKI_GENERATOR')
if KERNEL_INSTALL_UKI_GENERATOR != 'ukify':
log(f'{KERNEL_INSTALL_UKI_GENERATOR=}, quitting.')
return False
log('KERNEL_INSTALL_LAYOUT and KERNEL_INSTALL_UKI_GENERATOR are good')
return True
def config_file_location() -> Optional[Path]:
if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
p = Path(root) / 'uki.conf'
else:
p = Path('/etc/kernel/uki.conf')
if p.exists():
return p
return None
def kernel_cmdline_base() -> list[str]:
if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
return Path(root).joinpath('cmdline').read_text().split()
for cmdline in ('/etc/kernel/cmdline',
'/usr/lib/kernel/cmdline'):
try:
return Path(cmdline).read_text().split()
except FileNotFoundError:
continue
options = Path('/proc/cmdline').read_text().split()
return [opt for opt in options
if not opt.startswith(('BOOT_IMAGE=', 'initrd='))]
def kernel_cmdline(opts) -> str:
options = kernel_cmdline_base()
# If the boot entries are named after the machine ID, then suffix the kernel
# command line with the machine ID we use, so that the machine ID remains
# stable, even during factory reset, in the initrd (where the system's machine
# ID is not directly accessible yet), and if the root file system is volatile.
if (opts.entry_token == opts.machine_id and
not any(opt.startswith('systemd.machine_id=') for opt in options)):
options += [f'systemd.machine_id={opts.machine_id}']
# TODO: we unconditionally set the cmdline here, ignoring the setting in
# the config file. Should we not do that?
# Prepend a space so that '@' does not get misinterpreted
return ' ' + ' '.join(options)
def call_ukify(opts):
# Punish me harder.
# We want this:
# ukify = importlib.machinery.SourceFileLoader('ukify', UKIFY).load_module()
# but it throws a DeprecationWarning.
# https://stackoverflow.com/questions/67631/how-can-i-import-a-module-dynamically-given-the-full-path
# https://github.com/python/cpython/issues/65635
# offer "explanations", but to actually load a python file without a .py extension,
# the "solution" is 4+ incomprehensible lines.
# The solution with runpy gives a dictionary, which isn't great, but will do.
ukify = runpy.run_path(UKIFY, run_name='ukify')
# Create "empty" namespace. We want to override just a few settings,
# so it doesn't make sense to duplicate all the fields. We use a hack
# to pre-populate the namespace like argparse would, all defaults.
# We need to specify the two mandatory arguments to not get an error.
opts2 = ukify['create_parser']().parse_args(('A','B'))
opts2.config = config_file_location()
opts2.uname = opts.kernel_version
opts2.linux = opts.kernel_image
opts2.initrd = opts.initrd
# Note that 'uki.efi' is the name required by 90-uki-copy.install.
opts2.output = opts.staging_area / 'uki.efi'
opts2.cmdline = kernel_cmdline(opts)
if BOOT_STUB:
opts2.stub = BOOT_STUB
# opts2.summary = True
ukify['apply_config'](opts2)
ukify['finalize_options'](opts2)
ukify['check_inputs'](opts2)
ukify['make_uki'](opts2)
log(f'{opts2.output} has been created')
def main():
opts = parse_args()
if opts.command != 'add':
return
if not we_are_wanted():
return
call_ukify(opts)
if __name__ == '__main__':
main()

View file

@ -28,9 +28,9 @@ INITRD_OPTIONS_SHIFT=4
[ "$KERNEL_INSTALL_LAYOUT" = "bls" ] || exit 0
MACHINE_ID="$KERNEL_INSTALL_MACHINE_ID"
ENTRY_TOKEN="$KERNEL_INSTALL_ENTRY_TOKEN"
BOOT_ROOT="$KERNEL_INSTALL_BOOT_ROOT"
MACHINE_ID="${KERNEL_INSTALL_MACHINE_ID:?}"
ENTRY_TOKEN="${KERNEL_INSTALL_ENTRY_TOKEN:?}"
BOOT_ROOT="${KERNEL_INSTALL_BOOT_ROOT:?}"
[ -n "$BOOT_MNT" ] || BOOT_MNT="$(stat -c %m "$BOOT_ROOT")"
if [ "$BOOT_MNT" = '/' ]; then

View file

@ -1,21 +1,31 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
kernel_install_in = files('kernel-install.in')
loaderentry_install_in = files('90-loaderentry.install.in')
ukify_install = custom_target(
'60-ukify.install',
input : '60-ukify.install.in',
output : '60-ukify.install',
command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
install : want_kernel_install and want_ukify,
install_mode : 'rwxr-xr-x',
install_dir : kernelinstalldir)
loaderentry_install = custom_target(
'90-loaderentry.install',
input : loaderentry_install_in,
input : '90-loaderentry.install.in',
output : '90-loaderentry.install',
command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
install : want_kernel_install,
install_mode : 'rwxr-xr-x',
install_dir : kernelinstalldir)
kernel_install_files = files(
'50-depmod.install',
'90-uki-copy.install',
)
uki_copy_install = files('90-uki-copy.install')
kernel_install_files = [
files('50-depmod.install'),
uki_copy_install,
]
if want_kernel_install
install_data(kernel_install_files,

View file

@ -7,7 +7,11 @@ set -o pipefail
export SYSTEMD_LOG_LEVEL=debug
kernel_install="${1:?}"
plugin="${2:?}"
loaderentry_install="${2:?}"
uki_copy_install="${3:?}"
ukify="${4:-}"
ukify_install="${5:-}"
boot_stub="${6:-}"
if [[ -d "${PROJECT_BUILD_ROOT:-}" ]]; then
bootctl="${PROJECT_BUILD_ROOT}/bootctl"
else
@ -36,11 +40,15 @@ MACHINE_ID=badbadbadbadbadbad6abadbadbadbad
EOF
export KERNEL_INSTALL_CONF_ROOT="$D/sources"
export KERNEL_INSTALL_PLUGINS="$plugin"
# We "install" multiple plugins, but control which ones will be active via install.conf.
export KERNEL_INSTALL_PLUGINS="${ukify_install} ${loaderentry_install} ${uki_copy_install}"
export BOOT_ROOT="$D/boot"
export BOOT_MNT="$D/boot"
export MACHINE_ID='3e0484f3634a418b8e6a39e8828b03e3'
export KERNEL_INSTALL_UKIFY="$ukify"
export KERNEL_INSTALL_BOOT_STUB="$boot_stub"
# Test type#1 installation
"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
entry="$BOOT_ROOT/loader/entries/the-token-1.1.1.conf"
@ -91,7 +99,25 @@ grep -qE '^initrd .*/the-token/1.1.1/initrd' "$entry"
grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux"
grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd"
if test -x "$bootctl"; then
# Install UKI
if [ -f "$ukify" ]; then
cat >>"$D/sources/install.conf" <<EOF
layout=uki
uki_generator=ukify
EOF
"$kernel_install" -v add 1.1.3 "$D/sources/linux" "$D/sources/initrd"
uki="${BOOT_ROOT}/EFI/Linux/the-token-1.1.3+56.efi"
test -f "$uki"
if [ -x "$bootctl" ]; then
"$bootctl" kernel-inspect "$uki" | grep -qE 'Kernel Type: +uki$'
"$bootctl" kernel-inspect "$uki" | grep -qE 'Version: +1\.1\.3$'
"$bootctl" kernel-inspect "$uki" | grep -qE 'Cmdline: +opt1 opt2$'
fi
fi
# Test bootctl
if [ -x "$bootctl" ]; then
echo "Testing bootctl"
e2="${entry%+*}_2.conf"
cp "$entry" "$e2"

View file

@ -1,7 +1,19 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
if want_ukify and want_tests != 'false'
test('test-ukify',
files('test_ukify.py'),
env : test_env)
have_pytest_flakes = pymod.find_installation(
'python3',
required : false,
modules : ['pytest_flakes'],
).found()
args = ['-v']
if have_pytest_flakes
args += ['--flakes']
endif
test('test-ukify',
files('test_ukify.py'),
args: args,
env : test_env)
endif

View file

@ -1,2 +0,0 @@
[tool:pytest]
addopts = --flakes

View file

@ -14,6 +14,7 @@ import shutil
import subprocess
import sys
import tempfile
import textwrap
try:
import pytest
@ -46,6 +47,93 @@ def test_round_up():
assert ukify.round_up(4096) == 4096
assert ukify.round_up(4097) == 8192
def test_namespace_creation():
ns = ukify.create_parser().parse_args(('A','B'))
assert ns.linux == pathlib.Path('A')
assert ns.initrd == [pathlib.Path('B')]
def test_config_example():
ex = ukify.config_example()
assert '[UKI]' in ex
assert 'Splash = BMP' in ex
def test_apply_config(tmp_path):
config = tmp_path / 'config1.conf'
config.write_text(textwrap.dedent(
f'''
[UKI]
Linux = LINUX
Initrd = initrd1 initrd2
initrd3
Cmdline = 1 2 3 4 5
6 7 8
OSRelease = @some/path1
DeviceTree = some/path2
Splash = some/path3
Uname = 1.2.3
EFIArch=arm
Stub = some/path4
PCRBanks = sha512,sha1
SigningEngine = engine1
SecureBootPrivateKey = some/path5
SecureBootCertificate = some/path6
SignKernel = no
[PCRSignature:NAME]
PCRPrivateKey = some/path7
PCRPublicKey = some/path8
Phases = {':'.join(ukify.KNOWN_PHASES)}
'''))
ns = ukify.create_parser().parse_args(('A','B'))
ns.linux = None
ns.initrd = []
ukify.apply_config(ns, config)
assert ns.linux == pathlib.Path('LINUX')
assert ns.initrd == [pathlib.Path('initrd1'),
pathlib.Path('initrd2'),
pathlib.Path('initrd3')]
assert ns.cmdline == '1 2 3 4 5\n6 7 8'
assert ns.os_release == '@some/path1'
assert ns.devicetree == pathlib.Path('some/path2')
assert ns.splash == pathlib.Path('some/path3')
assert ns.efi_arch == 'arm'
assert ns.stub == pathlib.Path('some/path4')
assert ns.pcr_banks == ['sha512', 'sha1']
assert ns.signing_engine == 'engine1'
assert ns.sb_key == 'some/path5'
assert ns.sb_cert == 'some/path6'
assert ns.sign_kernel == False
assert ns._groups == ['NAME']
assert ns.pcr_private_keys == [pathlib.Path('some/path7')]
assert ns.pcr_public_keys == [pathlib.Path('some/path8')]
assert ns.phase_path_groups == [['enter-initrd:leave-initrd:sysinit:ready:shutdown:final']]
ukify.finalize_options(ns)
assert ns.linux == pathlib.Path('LINUX')
assert ns.initrd == [pathlib.Path('initrd1'),
pathlib.Path('initrd2'),
pathlib.Path('initrd3')]
assert ns.cmdline == '1 2 3 4 5 6 7 8'
assert ns.os_release == pathlib.Path('some/path1')
assert ns.devicetree == pathlib.Path('some/path2')
assert ns.splash == pathlib.Path('some/path3')
assert ns.efi_arch == 'arm'
assert ns.stub == pathlib.Path('some/path4')
assert ns.pcr_banks == ['sha512', 'sha1']
assert ns.signing_engine == 'engine1'
assert ns.sb_key == 'some/path5'
assert ns.sb_cert == 'some/path6'
assert ns.sign_kernel == False
assert ns._groups == ['NAME']
assert ns.pcr_private_keys == [pathlib.Path('some/path7')]
assert ns.pcr_public_keys == [pathlib.Path('some/path8')]
assert ns.phase_path_groups == [['enter-initrd:leave-initrd:sysinit:ready:shutdown:final']]
def test_parse_args_minimal():
opts = ukify.parse_args('arg1 arg2'.split())
assert opts.linux == pathlib.Path('arg1')
@ -78,6 +166,7 @@ def test_parse_args_many():
])
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')
@ -91,7 +180,7 @@ def test_parse_args_many():
assert opts.sb_key == 'SBKEY'
assert opts.sb_cert == 'SBCERT'
assert opts.sign_kernel is False
assert opts.tools == pathlib.Path('TOOLZ/')
assert opts.tools == [pathlib.Path('TOOLZ/')]
assert opts.output == pathlib.Path('OUTPUT')
assert opts.measure is False
@ -109,15 +198,92 @@ def test_parse_sections():
assert opts.sections[0].name == 'test'
assert isinstance(opts.sections[0].content, pathlib.Path)
assert opts.sections[0].tmpfile
assert opts.sections[0].offset is None
assert opts.sections[0].measure is False
assert opts.sections[1].name == 'test2'
assert opts.sections[1].content == pathlib.Path('FILE')
assert opts.sections[1].tmpfile is None
assert opts.sections[1].offset is None
assert opts.sections[1].measure is False
def test_config_priority(tmp_path):
config = tmp_path / 'config1.conf'
config.write_text(textwrap.dedent(
f'''
[UKI]
Linux = LINUX
Initrd = initrd1 initrd2
initrd3
Cmdline = 1 2 3 4 5
6 7 8
OSRelease = @some/path1
DeviceTree = some/path2
Splash = some/path3
Uname = 1.2.3
EFIArch=arm
Stub = some/path4
PCRBanks = sha512,sha1
SigningEngine = engine1
SecureBootPrivateKey = some/path5
SecureBootCertificate = some/path6
SignKernel = no
[PCRSignature:NAME]
PCRPrivateKey = some/path7
PCRPublicKey = some/path8
Phases = {':'.join(ukify.KNOWN_PHASES)}
'''))
opts = ukify.parse_args(
['/ARG1', '///ARG2', '/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',
])
ukify.apply_config(opts, config)
ukify.finalize_options(opts)
assert opts.linux == pathlib.Path('/ARG1')
assert opts.initrd == [pathlib.Path('initrd1'),
pathlib.Path('initrd2'),
pathlib.Path('initrd3'),
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'),
pathlib.Path('some/path7')]
assert opts.pcr_public_keys == [pathlib.Path('PKEY2'),
pathlib.Path('some/path8')]
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 True
def test_help(capsys):
with pytest.raises(SystemExit):
ukify.parse_args(['--help'])
@ -148,7 +314,7 @@ def kernel_initrd():
linux = f"{item['root']}{item['linux']}"
initrd = f"{item['root']}{item['initrd'][0]}"
except (KeyError, IndexError):
pass
continue
return [linux, initrd]
else:
return None
@ -242,7 +408,6 @@ def test_uname_scraping(kernel_initrd):
uname = ukify.Uname.scrape(kernel_initrd[0])
assert re.match(r'\d+\.\d+\.\d+', uname)
def test_efi_signing(kernel_initrd, tmpdir):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
@ -410,4 +575,4 @@ def test_pcr_signing2(kernel_initrd, tmpdir):
assert len(sig['sha1']) == 6 # six items for six phases paths
if __name__ == '__main__':
pytest.main([__file__, '-v'])
sys.exit(pytest.main(sys.argv))

View file

@ -1,12 +1,28 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# systemd is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with systemd; If not, see <https://www.gnu.org/licenses/>.
# pylint: disable=missing-docstring,invalid-name,import-outside-toplevel
# pylint: disable=consider-using-with,unspecified-encoding,line-too-long
# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-branches,fixme
import argparse
import configparser
import collections
import dataclasses
import fnmatch
@ -14,27 +30,33 @@ import itertools
import json
import os
import pathlib
import pprint
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import typing
from typing import (Any,
Callable,
IO,
Optional,
Union)
import pefile
import pefile # type: ignore
__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
EFI_ARCH_MAP = {
# host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported]
'x86_64' : ['x64', 'ia32'],
'i[3456]86' : ['ia32'],
'aarch64' : ['aa64'],
'arm[45678]*l' : ['arm'],
'loongarch32' : ['loongarch32'],
'loongarch64' : ['loongarch64'],
'riscv32' : ['riscv32'],
'riscv64' : ['riscv64'],
# host_arch glob : [efi_arch, 32_bit_efi_arch if mixed mode is supported]
'x86_64' : ['x64', 'ia32'],
'i[3456]86' : ['ia32'],
'aarch64' : ['aa64'],
'arm[45678]*l' : ['arm'],
'loongarch32' : ['loongarch32'],
'loongarch64' : ['loongarch64'],
'riscv32' : ['riscv32'],
'riscv64' : ['riscv64'],
}
EFI_ARCHES: list[str] = sum(EFI_ARCH_MAP.values(), [])
@ -69,18 +91,6 @@ def shell_join(cmd):
return ' '.join(shlex.quote(str(x)) for x in cmd)
def path_is_readable(s: typing.Optional[str]) -> typing.Optional[pathlib.Path]:
"""Convert a filename string to a Path and verify access."""
if s is None:
return None
p = pathlib.Path(s)
try:
p.open().close()
except IsADirectoryError:
pass
return p
def round_up(x, blocksize=4096):
return (x + blocksize - 1) // blocksize * blocksize
@ -222,7 +232,7 @@ class Uname:
class Section:
name: str
content: pathlib.Path
tmpfile: typing.Optional[typing.IO] = None
tmpfile: Optional[IO] = None
measure: bool = False
@classmethod
@ -266,7 +276,7 @@ class Section:
@dataclasses.dataclass
class UKI:
executable: list[typing.Union[pathlib.Path, str]]
executable: list[Union[pathlib.Path, str]]
sections: list[Section] = dataclasses.field(default_factory=list, init=False)
def add_section(self, section):
@ -322,11 +332,13 @@ def check_inputs(opts):
if name in {'output', 'tools'}:
continue
if not isinstance(value, pathlib.Path):
continue
# Open file to check that we can read it, or generate an exception
value.open().close()
if isinstance(value, pathlib.Path):
# Open file to check that we can read it, or generate an exception
value.open().close()
elif isinstance(value, list):
for item in value:
if isinstance(item, pathlib.Path):
item.open().close()
check_splash(opts.splash)
@ -443,7 +455,7 @@ def pairwise(iterable):
return zip(a, b)
class PeError(Exception):
class PEError(Exception):
pass
@ -485,12 +497,12 @@ def pe_add_sections(uki: UKI, output: str):
warnings = pe.get_warnings()
if warnings:
raise PeError(f'pefile warnings treated as errors: {warnings}')
raise PEError(f'pefile warnings treated as errors: {warnings}')
security = pe.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']]
if security.VirtualAddress != 0:
# We could strip the signatures, but why would anyone sign the stub?
raise PeError(f'Stub image is signed, refusing.')
raise PEError('Stub image is signed, refusing.')
for section in uki.sections:
new_section = pefile.SectionStructure(pe.__IMAGE_SECTION_HEADER_format__, pe=pe)
@ -498,7 +510,7 @@ def pe_add_sections(uki: UKI, output: str):
offset = pe.sections[-1].get_file_offset() + new_section.sizeof()
if offset + new_section.sizeof() > pe.OPTIONAL_HEADER.SizeOfHeaders:
raise PeError(f'Not enough header space to add section {section.name}.')
raise PEError(f'Not enough header space to add section {section.name}.')
data = section.content.read_bytes()
@ -653,155 +665,429 @@ def make_uki(opts):
print(f"Wrote {'signed' if opts.sb_key else 'unsigned'} {opts.output}")
def parse_args(args=None):
@dataclasses.dataclass(frozen=True)
class ConfigItem:
@staticmethod
def config_list_prepend(
namespace: argparse.Namespace,
group: Optional[str],
dest: str,
value: Any,
) -> None:
"Prepend value to namespace.<dest>"
assert not group
old = getattr(namespace, dest, [])
setattr(namespace, dest, value + old)
@staticmethod
def config_set_if_unset(
namespace: argparse.Namespace,
group: Optional[str],
dest: str,
value: Any,
) -> None:
"Set namespace.<dest> to value only if it was None"
assert not group
if getattr(namespace, dest) is None:
setattr(namespace, dest, value)
@staticmethod
def config_set_group(
namespace: argparse.Namespace,
group: Optional[str],
dest: str,
value: Any,
) -> None:
"Set namespace.<dest>[idx] to value, with idx derived from group"
if group not in namespace._groups:
namespace._groups += [group]
idx = namespace._groups.index(group)
old = getattr(namespace, dest, None)
if old is None:
old = []
setattr(namespace, dest,
old + ([None] * (idx - len(old))) + [value])
@staticmethod
def parse_boolean(s: str) -> bool:
"Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false"
s_l = s.lower()
if s_l in {'1', 'true', 'yes', 'y', 't', 'on'}:
return True
if s_l in {'0', 'false', 'no', 'n', 'f', 'off'}:
return False
raise ValueError('f"Invalid boolean literal: {s!r}')
# arguments for argparse.ArgumentParser.add_argument()
name: Union[str, tuple[str, str]]
dest: Optional[str] = None
metavar: Optional[str] = None
type: Optional[Callable] = None
nargs: Optional[str] = None
action: Optional[Union[str, Callable]] = None
default: Any = None
version: Optional[str] = None
choices: Optional[tuple[str, ...]] = None
help: Optional[str] = None
# metadata for config file parsing
config_key: Optional[str] = None
config_push: Callable[[argparse.Namespace, Optional[str], str, Any], None] = \
config_set_if_unset
def _names(self) -> tuple[str, ...]:
return self.name if isinstance(self.name, tuple) else (self.name,)
def argparse_dest(self) -> str:
# It'd be nice if argparse exported this, but I don't see that in the API
if self.dest:
return self.dest
return self._names()[0].lstrip('-').replace('-', '_')
def add_to(self, parser: argparse.ArgumentParser):
kwargs = { key:val
for key in dataclasses.asdict(self)
if (key not in ('name', 'config_key', 'config_push') and
(val := getattr(self, key)) is not None) }
args = self._names()
parser.add_argument(*args, **kwargs)
def apply_config(self, namespace, section, group, key, value) -> None:
assert f'{section}/{key}' == self.config_key
dest = self.argparse_dest()
conv: Callable[[str], Any]
if self.action == argparse.BooleanOptionalAction:
# We need to handle this case separately: the options are called
# --foo and --no-foo, and no argument is parsed. But in the config
# file, we have Foo=yes or Foo=no.
conv = self.parse_boolean
elif self.type:
conv = self.type
else:
conv = lambda s:s
if self.nargs == '*':
value = [conv(v) for v in value.split()]
else:
value = conv(value)
self.config_push(namespace, group, dest, value)
def config_example(self) -> tuple[Optional[str], Optional[str], Optional[str]]:
if not self.config_key:
return None, None, None
section_name, key = self.config_key.split('/', 1)
if section_name.endswith(':'):
section_name += 'NAME'
if self.choices:
value = '|'.join(self.choices)
else:
value = self.metavar or self.argparse_dest().upper()
return (section_name, key, value)
CONFIG_ITEMS = [
ConfigItem(
'--version',
action = 'version',
version = f'ukify {__version__}',
),
ConfigItem(
'--summary',
help = 'print parsed config and exit',
action = 'store_true',
),
ConfigItem(
'linux',
metavar = 'LINUX',
type = pathlib.Path,
nargs = '?',
help = 'vmlinuz file [.linux section]',
config_key = 'UKI/Linux',
),
ConfigItem(
'initrd',
metavar = 'INITRD…',
type = pathlib.Path,
nargs = '*',
help = 'initrd files [.initrd section]',
config_key = 'UKI/Initrd',
config_push = ConfigItem.config_list_prepend,
),
ConfigItem(
('--config', '-c'),
metavar = 'PATH',
help = 'configuration file',
),
ConfigItem(
'--cmdline',
metavar = 'TEXT|@PATH',
help = 'kernel command line [.cmdline section]',
config_key = 'UKI/Cmdline',
),
ConfigItem(
'--os-release',
metavar = 'TEXT|@PATH',
help = 'path to os-release file [.osrel section]',
config_key = 'UKI/OSRelease',
),
ConfigItem(
'--devicetree',
metavar = 'PATH',
type = pathlib.Path,
help = 'Device Tree file [.dtb section]',
config_key = 'UKI/DeviceTree',
),
ConfigItem(
'--splash',
metavar = 'BMP',
type = pathlib.Path,
help = 'splash image bitmap file [.splash section]',
config_key = 'UKI/Splash',
),
ConfigItem(
'--pcrpkey',
metavar = 'KEY',
type = pathlib.Path,
help = 'embedded public key to seal secrets to [.pcrpkey section]',
config_key = 'UKI/PCRPKey',
),
ConfigItem(
'--uname',
metavar='VERSION',
help='"uname -r" information [.uname section]',
config_key = 'UKI/Uname',
),
ConfigItem(
'--efi-arch',
metavar = 'ARCH',
choices = ('ia32', 'x64', 'arm', 'aa64', 'riscv64'),
help = 'target EFI architecture',
config_key = 'UKI/EFIArch',
),
ConfigItem(
'--stub',
type = pathlib.Path,
help = 'path to the sd-stub file [.text,.data,… sections]',
config_key = 'UKI/Stub',
),
ConfigItem(
'--section',
dest = 'sections',
metavar = 'NAME:TEXT|@PATH',
type = Section.parse_arg,
action = 'append',
default = [],
help = 'additional section as name and contents [NAME section]',
),
ConfigItem(
'--pcr-banks',
metavar = 'BANK…',
type = parse_banks,
config_key = 'UKI/PCRBanks',
),
ConfigItem(
'--signing-engine',
metavar = 'ENGINE',
help = 'OpenSSL engine to use for signing',
config_key = 'UKI/SigningEngine',
),
ConfigItem(
'--secureboot-private-key',
dest = 'sb_key',
help = 'path to key file or engine-specific designation for SB signing',
config_key = 'UKI/SecureBootPrivateKey',
),
ConfigItem(
'--secureboot-certificate',
dest = 'sb_cert',
help = 'path to certificate file or engine-specific designation for SB signing',
config_key = 'UKI/SecureBootCertificate',
),
ConfigItem(
'--sign-kernel',
action = argparse.BooleanOptionalAction,
help = 'Sign the embedded kernel',
config_key = 'UKI/SignKernel',
),
ConfigItem(
'--pcr-private-key',
dest = 'pcr_private_keys',
metavar = 'PATH',
type = pathlib.Path,
action = 'append',
help = 'private part of the keypair for signing PCR signatures',
config_key = 'PCRSignature:/PCRPrivateKey',
config_push = ConfigItem.config_set_group,
),
ConfigItem(
'--pcr-public-key',
dest = 'pcr_public_keys',
metavar = 'PATH',
type = pathlib.Path,
action = 'append',
help = 'public part of the keypair for signing PCR signatures',
config_key = 'PCRSignature:/PCRPublicKey',
config_push = ConfigItem.config_set_group,
),
ConfigItem(
'--phases',
dest = 'phase_path_groups',
metavar = 'PHASE-PATH…',
type = parse_phase_paths,
action = 'append',
help = 'phase-paths to create signatures for',
config_key = 'PCRSignature:/Phases',
config_push = ConfigItem.config_set_group,
),
ConfigItem(
'--tools',
type = pathlib.Path,
action = 'append',
help = 'Directories to search for tools (systemd-measure, …)',
),
ConfigItem(
('--output', '-o'),
type = pathlib.Path,
help = 'output file path',
),
ConfigItem(
'--measure',
action = argparse.BooleanOptionalAction,
help = 'print systemd-measure output for the UKI',
),
]
CONFIGFILE_ITEMS = { item.config_key:item
for item in CONFIG_ITEMS
if item.config_key }
def apply_config(namespace, filename=None):
if filename is None:
filename = namespace.config
if filename is None:
return
# Fill in ._groups based on --pcr-public-key=, --pcr-private-key=, and --phases=.
assert '_groups' not in namespace
n_pcr_priv = len(namespace.pcr_private_keys or ())
namespace._groups = list(range(n_pcr_priv))
cp = configparser.ConfigParser(
comment_prefixes='#',
inline_comment_prefixes='#',
delimiters='=',
empty_lines_in_values=False,
interpolation=None,
strict=False)
# Do not make keys lowercase
cp.optionxform = lambda option: option
cp.read(filename)
for section_name, section in cp.items():
idx = section_name.find(':')
if idx >= 0:
section_name, group = section_name[:idx+1], section_name[idx+1:]
if not section_name or not group:
raise ValueError('Section name components cannot be empty')
if ':' in group:
raise ValueError('Section name cannot contain more than one ":"')
else:
group = None
for key, value in section.items():
if item := CONFIGFILE_ITEMS.get(f'{section_name}/{key}'):
item.apply_config(namespace, section_name, group, key, value)
else:
print(f'Unknown config setting [{section_name}] {key}=')
def config_example():
prev_section = None
for item in CONFIG_ITEMS:
section, key, value = item.config_example()
if section:
if prev_section != section:
if prev_section:
yield ''
yield f'[{section}]'
prev_section = section
yield f'{key} = {value}'
def create_parser():
p = argparse.ArgumentParser(
description='Build and sign Unified Kernel Images',
allow_abbrev=False,
usage='''\
usage: ukify [options] [linux [initrd]]
ukify -h | --help
''')
ukify [options] [LINUX INITRD]
''',
epilog='\n '.join(('config file:', *config_example())),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
for item in CONFIG_ITEMS:
item.add_to(p)
# Suppress printing of usage synopsis on errors
p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
p.add_argument('linux',
type=pathlib.Path,
nargs="?",
help='vmlinuz file [.linux section]')
p.add_argument('initrd',
type=pathlib.Path,
nargs='*',
help='initrd files [.initrd section]')
return p
p.add_argument('--cmdline',
metavar='TEXT|@PATH',
help='kernel command line [.cmdline section]')
p.add_argument('--os-release',
metavar='TEXT|@PATH',
help='path to os-release file [.osrel section]')
p.add_argument('--devicetree',
metavar='PATH',
type=pathlib.Path,
help='Device Tree file [.dtb section]')
p.add_argument('--splash',
metavar='BMP',
type=pathlib.Path,
help='splash image bitmap file [.splash section]')
p.add_argument('--pcrpkey',
metavar='KEY',
type=pathlib.Path,
help='embedded public key to seal secrets to [.pcrpkey section]')
p.add_argument('--uname',
metavar='VERSION',
help='"uname -r" information [.uname section]')
p.add_argument('--efi-arch',
metavar='ARCH',
choices=('ia32', 'x64', 'arm', 'aa64', 'riscv64'),
help='target EFI architecture')
p.add_argument('--stub',
type=pathlib.Path,
help='path to the sd-stub file [.text,.data,… sections]')
p.add_argument('--section',
dest='sections',
metavar='NAME:TEXT|@PATH',
type=Section.parse_arg,
action='append',
default=[],
help='additional section as name and contents [NAME section]')
p.add_argument('--pcr-private-key',
dest='pcr_private_keys',
metavar='PATH',
type=pathlib.Path,
action='append',
help='private part of the keypair for signing PCR signatures')
p.add_argument('--pcr-public-key',
dest='pcr_public_keys',
metavar='PATH',
type=pathlib.Path,
action='append',
help='public part of the keypair for signing PCR signatures')
p.add_argument('--phases',
dest='phase_path_groups',
metavar='PHASE-PATH…',
type=parse_phase_paths,
action='append',
help='phase-paths to create signatures for')
p.add_argument('--pcr-banks',
metavar='BANK…',
type=parse_banks)
p.add_argument('--signing-engine',
metavar='ENGINE',
help='OpenSSL engine to use for signing')
p.add_argument('--secureboot-private-key',
dest='sb_key',
help='path to key file or engine-specific designation for SB signing')
p.add_argument('--secureboot-certificate',
dest='sb_cert',
help='path to certificate file or engine-specific designation for SB signing')
p.add_argument('--sign-kernel',
action=argparse.BooleanOptionalAction,
help='Sign the embedded kernel')
p.add_argument('--tools',
type=pathlib.Path,
action='append',
help='Directories to search for tools (systemd-measure, ...)')
p.add_argument('--output', '-o',
type=pathlib.Path,
help='output file path')
p.add_argument('--measure',
action=argparse.BooleanOptionalAction,
help='print systemd-measure output for the UKI')
p.add_argument('--version',
action='version',
version=f'ukify {__version__}')
opts = p.parse_args(args)
if opts.linux is not None:
path_is_readable(opts.linux)
for initrd in opts.initrd or ():
path_is_readable(initrd)
path_is_readable(opts.devicetree)
path_is_readable(opts.pcrpkey)
for key in opts.pcr_private_keys or ():
path_is_readable(key)
for key in opts.pcr_public_keys or ():
path_is_readable(key)
def finalize_options(opts):
if opts.cmdline and opts.cmdline.startswith('@'):
opts.cmdline = path_is_readable(opts.cmdline[1:])
opts.cmdline = pathlib.Path(opts.cmdline[1:])
elif opts.cmdline:
# Drop whitespace from the commandline. If we're reading from a file,
# we copy the contents verbatim. But configuration specified on the commandline
# or in the config file may contain additional whitespace that has no meaning.
opts.cmdline = ' '.join(opts.cmdline.split())
if opts.os_release is not None and opts.os_release.startswith('@'):
opts.os_release = path_is_readable(opts.os_release[1:])
elif opts.os_release is None and opts.linux is not None:
if opts.os_release and opts.os_release.startswith('@'):
opts.os_release = pathlib.Path(opts.os_release[1:])
elif not opts.os_release and opts.linux:
p = pathlib.Path('/etc/os-release')
if not p.exists():
p = path_is_readable('/usr/lib/os-release')
p = pathlib.Path('/usr/lib/os-release')
opts.os_release = p
if opts.efi_arch is None:
opts.efi_arch = guess_efi_arch()
if opts.stub is None:
opts.stub = path_is_readable(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
if opts.signing_engine is None:
opts.sb_key = path_is_readable(opts.sb_key) if opts.sb_key else None
opts.sb_cert = path_is_readable(opts.sb_cert) if opts.sb_cert else None
if opts.sb_key:
opts.sb_key = pathlib.Path(opts.sb_key)
if opts.sb_cert:
opts.sb_cert = pathlib.Path(opts.sb_cert)
if bool(opts.sb_key) ^ bool(opts.sb_cert):
raise ValueError('--secureboot-private-key= and --secureboot-certificate= must be specified together')
@ -809,14 +1095,6 @@ usage: ukify [options…] [linux [initrd…]]
if opts.sign_kernel and not opts.sb_key:
raise ValueError('--sign-kernel requires --secureboot-private-key= and --secureboot-certificate= to be specified')
n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys)
n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys)
n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups)
if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv:
raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=')
if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv:
raise ValueError('--phases= specifications must match --pcr-private-key=')
if opts.output is None:
if opts.linux is None:
raise ValueError('--output= must be specified when building a PE addon')
@ -826,6 +1104,30 @@ usage: ukify [options…] [linux [initrd…]]
for section in opts.sections:
section.check_name()
if opts.summary:
# TODO: replace pprint() with some fancy formatting.
pprint.pprint(vars(opts))
sys.exit()
def parse_args(args=None):
p = create_parser()
opts = p.parse_args(args)
# 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)
n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys)
n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups)
if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv:
raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=')
if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv:
raise ValueError('--phases= specifications must match --pcr-private-key=')
apply_config(opts)
finalize_options(opts)
return opts