diff options
author | Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> | 2023-05-06 13:34:08 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-06 13:34:08 +0200 |
commit | 9dfed0d4cc10430a773ac5271cb13c3b19b59c15 (patch) | |
tree | 9ea84b6c0e4fb34d21e6ef62d235adbd11f67286 | |
parent | 5b5e54e058745f47184cb88e0037fa576164000c (diff) | |
parent | 46886f130d505f483ee1305a51f04196a551e9a2 (diff) | |
download | systemd-9dfed0d4cc10430a773ac5271cb13c3b19b59c15.tar.gz |
Merge pull request #27262 from keszybz/ukify-install
Add kernel-install plugin that calls ukify
-rw-r--r-- | TODO | 7 | ||||
-rw-r--r-- | man/ukify.xml | 519 | ||||
-rw-r--r-- | meson.build | 163 | ||||
-rw-r--r-- | mkosi.presets/00-base/mkosi.conf.d/10-fedora.conf | 3 | ||||
-rw-r--r-- | mkosi.presets/00-base/mkosi.conf.d/10-opensuse.conf | 1 | ||||
-rw-r--r-- | src/boot/efi/meson.build | 6 | ||||
-rwxr-xr-x | src/kernel-install/60-ukify.install.in | 224 | ||||
-rwxr-xr-x | src/kernel-install/90-loaderentry.install.in | 6 | ||||
-rw-r--r-- | src/kernel-install/meson.build | 22 | ||||
-rwxr-xr-x | src/kernel-install/test-kernel-install.sh | 32 | ||||
-rw-r--r-- | src/ukify/test/meson.build | 18 | ||||
-rw-r--r-- | src/ukify/test/setup.cfg | 2 | ||||
-rwxr-xr-x | src/ukify/test/test_ukify.py | 177 | ||||
-rwxr-xr-x | src/ukify/ukify.py | 642 |
14 files changed, 1355 insertions, 467 deletions
@@ -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 |