summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2023-05-06 13:34:08 +0200
committerGitHub <noreply@github.com>2023-05-06 13:34:08 +0200
commit9dfed0d4cc10430a773ac5271cb13c3b19b59c15 (patch)
tree9ea84b6c0e4fb34d21e6ef62d235adbd11f67286
parent5b5e54e058745f47184cb88e0037fa576164000c (diff)
parent46886f130d505f483ee1305a51f04196a551e9a2 (diff)
downloadsystemd-9dfed0d4cc10430a773ac5271cb13c3b19b59c15.tar.gz
Merge pull request #27262 from keszybz/ukify-install
Add kernel-install plugin that calls ukify
-rw-r--r--TODO7
-rw-r--r--man/ukify.xml519
-rw-r--r--meson.build163
-rw-r--r--mkosi.presets/00-base/mkosi.conf.d/10-fedora.conf3
-rw-r--r--mkosi.presets/00-base/mkosi.conf.d/10-opensuse.conf1
-rw-r--r--src/boot/efi/meson.build6
-rwxr-xr-xsrc/kernel-install/60-ukify.install.in224
-rwxr-xr-xsrc/kernel-install/90-loaderentry.install.in6
-rw-r--r--src/kernel-install/meson.build22
-rwxr-xr-xsrc/kernel-install/test-kernel-install.sh32
-rw-r--r--src/ukify/test/meson.build18
-rw-r--r--src/ukify/test/setup.cfg2
-rwxr-xr-xsrc/ukify/test/test_ukify.py177
-rwxr-xr-xsrc/ukify/ukify.py642
14 files changed, 1355 insertions, 467 deletions
diff --git a/TODO b/TODO
index f1c73fefb3..54d459eadd 100644
--- a/TODO
+++ b/TODO
@@ -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
diff --git a/man/ukify.xml b/man/ukify.xml
index c6bfbdc9d9..6aa136298d 100644
--- a/man/ukify.xml
+++ b/man/ukify.xml
@@ -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>
-
- <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
+ 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
+ <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>
-
- <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>The following options are understood:</para>
-
- <variablelist>
- <varlistentry>
- <term><option>--cmdline=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
-
- <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>
-
- <varlistentry>
- <term><option>--os-release=<replaceable>TEXT</replaceable>|<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>
-
- <varlistentry>
- <term><option>--devicetree=<replaceable>PATH</replaceable></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>
-
- <varlistentry>
- <term><option>--splash=<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>
-
- <varlistentry>
- <term><option>--pcrpkey=<replaceable>PATH</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>
-
- <varlistentry>
- <term><option>--uname=<replaceable>VERSION</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>
-
- <varlistentry>
- <term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></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>
-
- <varlistentry>
- <term><option>--pcr-private-key=<replaceable>PATH</replaceable></option></term>
-
- <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>
-
- <varlistentry>
- <term><option>--pcr-public-key=<replaceable>PATH</replaceable></option></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>
-
- <varlistentry>
- <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.
- 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>
-
- <varlistentry>
- <term><option>--pcr-banks=<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>
-
- <varlistentry>
- <term><option>--secureboot-private-key=<replaceable>SB_KEY</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>
-
- <varlistentry>
- <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
- <option>--signing-engine=</option> option is used, this may also be an engine-specific
- designation.</para></listitem>
- </varlistentry>
-
- <varlistentry>
- <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><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 <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>
-
- <varlistentry>
- <term><option>--tools=<replaceable>DIRS</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>
-
- <varlistentry>
- <term><option>--measure</option></term>
- <term><option>--no-measure</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>
-
- <varlistentry>
- <term><option>--output=<replaceable>FILENAME</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>
-
- <xi:include href="standard-options.xml" xpointer="help" />
- <xi:include href="standard-options.xml" xpointer="version" />
- </variablelist>
+ <title>Configuration settings</title>
+
+ <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 <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>
+
+ <para>The following options and settings are understood:</para>
+
+ <refsect2>
+ <title>Commandline-only options</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><option>--config=<replaceable>PATH</replaceable></option></term>
+
+ <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>--measure</option></term>
+ <term><option>--no-measure</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>
+
+ <varlistentry>
+ <term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></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>
+
+ <varlistentry>
+ <term><option>--tools=<replaceable>DIRS</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>
+
+ <varlistentry>
+ <term><option>--output=<replaceable>FILENAME</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>
+
+ <varlistentry>
+ <term><option>--summary</option></term>
+
+ <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>
+
+ <xi:include href="standard-options.xml" xpointer="help" />
+ <xi:include href="standard-options.xml" xpointer="version" />
+ </variablelist>
+ </refsect2>
+
+ <refsect2>
+ <title>[UKI] section</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><varname>Linux=<replaceable>LINUX</replaceable></varname></term>
+ <term>positional argument <replaceable>LINUX</replaceable></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>
+
+ <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><varname>Cmdline=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></varname></term>
+ <term><option>--cmdline=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
+
+ <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><varname>OSRelease=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></varname></term>
+ <term><option>--os-release=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
+
+ <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><varname>DeviceTree=<replaceable>PATH</replaceable></varname></term>
+ <term><option>--devicetree=<replaceable>PATH</replaceable></option></term>
+
+ <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><varname>Splash=<replaceable>PATH</replaceable></varname></term>
+ <term><option>--splash=<replaceable>PATH</replaceable></option></term>
+
+ <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><varname>PCRPKey=<replaceable>PATH</replaceable></varname></term>
+ <term><option>--pcrpkey=<replaceable>PATH</replaceable></option></term>
+
+ <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><varname>Uname=<replaceable>VERSION</replaceable></varname></term>
+ <term><option>--uname=<replaceable>VERSION</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>
+
+ <varlistentry>
+ <term><varname>PCRBanks=<replaceable>PATH</replaceable></varname></term>
+ <term><option>--pcr-banks=<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>
+
+ <varlistentry>
+ <term><varname>SecureBootPrivateKey=<replaceable>SB_KEY</replaceable></varname></term>
+ <term><option>--secureboot-private-key=<replaceable>SB_KEY</replaceable></option></term>
+
+ <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>
+
+ <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 \
@@ -300,6 +390,45 @@
</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>
<programlisting>ukify \
@@ -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>
diff --git a/meson.build b/meson.build
index c6dfc7709c..28086b16e6 100644
--- a/meson.build
+++ b/meson.build
@@ -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
############################################################
diff --git a/mkosi.presets/00-base/mkosi.conf.d/10-fedora.conf b/mkosi.presets/00-base/mkosi.conf.d/10-fedora.conf
index 035715979c..eba6f040da 100644
--- a/mkosi.presets/00-base/mkosi.conf.d/10-fedora.conf
+++ b/mkosi.presets/00-base/mkosi.conf.d/10-fedora.conf
@@ -4,6 +4,9 @@
Distribution=fedora
[Content]
+Packages=
+ python3dist(pytest-flakes)
+
BuildPackages=
pkgconfig(libgcrypt)
pkgconfig(xencontrol)
diff --git a/mkosi.presets/00-base/mkosi.conf.d/10-opensuse.conf b/mkosi.presets/00-base/mkosi.conf.d/10-opensuse.conf
index b8bce7148e..4ed5f6ff7c 100644
--- a/mkosi.presets/00-base/mkosi.conf.d/10-opensuse.conf
+++ b/mkosi.presets/00-base/mkosi.conf.d/10-opensuse.conf
@@ -27,6 +27,7 @@ Packages=
libxkbcommon0
libzstd1
pam
+ python3-pytest-flakes
shadow
tpm2-0-tss
xz
diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build
index 67ce00838d..9a560cf193 100644
--- a/src/boot/efi/meson.build
+++ b/src/boot/efi/meson.build
@@ -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)
diff --git a/src/kernel-install/60-ukify.install.in b/src/kernel-install/60-ukify.install.in
new file mode 100755
index 0000000000..7c29f7e8af
--- /dev/null
+++ b/src/kernel-install/60-ukify.install.in
@@ -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()
diff --git a/src/kernel-install/90-loaderentry.install.in b/src/kernel-install/90-loaderentry.install.in
index e8e8cf37c3..f4ba4708ba 100755
--- a/src/kernel-install/90-loaderentry.install.in
+++ b/src/kernel-install/90-loaderentry.install.in
@@ -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
diff --git a/src/kernel-install/meson.build b/src/kernel-install/meson.build
index f5db4432c9..744071b9e9 100644
--- a/src/kernel-install/meson.build
+++ b/src/kernel-install/meson.build
@@ -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,
diff --git a/src/kernel-install/test-kernel-install.sh b/src/kernel-install/test-kernel-install.sh
index 4cbf16f0df..4be8771359 100755
--- a/src/kernel-install/test-kernel-install.sh
+++ b/src/kernel-install/test-kernel-install.sh
@@ -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"
diff --git a/src/ukify/test/meson.build b/src/ukify/test/meson.build
index e39178f892..e78e76c673 100644
--- a/src/ukify/test/meson.build
+++ b/src/ukify/test/meson.build
@@ -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
diff --git a/src/ukify/test/setup.cfg b/src/ukify/test/setup.cfg
deleted file mode 100644
index 1f655da834..0000000000
--- a/src/ukify/test/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[tool:pytest]
-addopts = --flakes
diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py
index 1b58f05d4c..d221825019 100755
--- a/src/ukify/test/test_ukify.py
+++ b/src/ukify/test/test_ukify.py
@@ -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))
diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py
index 3fbeb2b215..7e9c7cc9ae 100755
--- a/src/ukify/ukify.py
+++ b/src/ukify/ukify.py
@@ -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]')
-
- 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)
+ return p
- 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:])
-
- 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:
+ 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 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,6 +1095,27 @@ 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')
+ if opts.output is None:
+ if opts.linux is None:
+ raise ValueError('--output= must be specified when building a PE addon')
+ suffix = '.efi' if opts.sb_key else '.unsigned.efi'
+ opts.output = opts.linux.name + suffix
+
+ 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)
@@ -817,14 +1124,9 @@ usage: ukify [options…] [linux [initrd…]]
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')
- suffix = '.efi' if opts.sb_key else '.unsigned.efi'
- opts.output = opts.linux.name + suffix
+ apply_config(opts)
- for section in opts.sections:
- section.check_name()
+ finalize_options(opts)
return opts