UKIFY(1)

πŸ¦‹ NAME πŸ¦‹

ok so welcome to ukify(1) bestie πŸ’– β€” this one's whole job is combining components into a signed Unified Kernel Image for UEFI systems. That's it, that's the mission. πŸŽ€


✨ SYNOPSIS ✨

ukify [OPTIONS...] build

ukify [OPTIONS...] genkey

ukify [OPTIONS...] inspect FILE...

πŸ’« DESCRIPTION πŸ’«

so, what even is ukify</p>? Its main character purpose is to take a bunch of components β€” usually a kernel, an initrd, and the systemd-stub(7) UEFI stub β€” and smash them together into a UAPI.5 Unified Kernel Image (UKI)[1] 🫢. That's a single PE binary that boots the system. iconic.

When the UKI actually gets executed, the stub extracts and boots the embedded linux kernel. The UKI can be started directly by the firmware or through a boot loader. And when you're using qemu[2], a UKI can also be executed through "direct kernel boot" β€” there's an example further down, iykyk πŸ‘€.

But wait, there's more. ukify can also be used to generate other types of UKI-like images, in particular extensions. See the description of the build verb below for that tea β˜•. It can also generate certificates and keys for SecureBoot and PCR signing β€” that's the genkey verb's moment ✨. And it can print detailed information about unified kernel images β€” that's what the inspect verb is for πŸ”.


🌈 COMMANDS 🌈

Here's what ukify understands:

πŸ”₯ build

This command creates a Unified Kernel Image. The two primary options you should be specifying for the build verb are Linux=/--linux= and Initrd=/--initrd=. And heads up: Initrd= accepts multiple whitespace-separated paths, and --initrd= can be specified multiple times πŸŽ€.

Additional sections get inserted into the UKI β€” either automatically, or only if you specifically provide an option for them. See the discussions of Microcode=/--microcode=, Cmdline=/--cmdline=, OSRelease=/--os-release=, DeviceTree=/--devicetree=, DeviceTreeAuto=/--devicetree-auto=, HWIDs=/--hwids=, Splash=/--splash=, PCRPKey=/--pcrpkey=, Uname=/--uname=, SBAT=/--sbat=, and --section= below πŸ‘‡.

ukify can also be used to assemble a PE binary that is not executable but contains auxiliary data β€” for example additional kernel command line entries. no cap, that's a whole vibe.

If PCR signing keys are provided via the PCRPrivateKey=/--pcr-private-key= and PCRPublicKey=/--pcr-public-key= or PCRCertificate=/--pcr-certificate= options, then the 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. systemd-measure(1) is the one doing this calculation and signing, btw πŸ’….

The calculation of PCR values is done for specific boot phase paths. Those can be specified with the Phases=/--phases= option. If you don't specify it, the default provided by systemd-measure is used. And plot twist: it's also possible to specify the PCRPrivateKey=/--pcr-private-key=, PCRPublicKey=/--pcr-public-key= or PCRCertificate=/--pcr-certificate=, and Phases=/--phases= arguments more than once. Signatures will then be performed with each of the specified keys πŸ”‘. On the command line, when both --phases= and --pcr-private-key= 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 is how you build different trust policies for different phases of the boot, bestie β€” that's the whole point. In the config file, PCRPrivateKey=, PCRPublicKey=, and Phases= are grouped into separate sections, describing separate boot phases. And if one of SigningEngine=/--signing-engine= or SigningProvider=/--signing-provider= is specified, then the private key arguments will be passed verbatim to openssl(1) as URIs, and the public key arguments will be loaded as X.509 certificates β€” so signing can be performed with an OpenSSL engine or provider respectively. slay πŸ”.

If a SecureBoot signing key is provided via the SecureBootPrivateKey=/--secureboot-private-key= option, the resulting PE binary will be signed as a whole, which allows the resulting UKI to be trusted by SecureBoot. Also peep the discussion of automatic enrollment in systemd-boot(7) πŸ‘‘.

And if the stub and/or the kernel contain ".sbat" sections, they will be merged in the UKI so that revocation updates affecting either are considered when the UKI is loaded by Shim. For more info on SBAT, see the Shim documentation[3] πŸ“š.

πŸŽ€ genkey

This command creates the keys for PCR signing, plus the key and certificate used for SecureBoot signing. Here's the cute part: the same configuration options that determine what keys and in which paths will be needed for signing when build is used, here determine which keys will be created. See the discussion of PCRPrivateKey=/--pcr-private-key=, PCRPublicKey=/--pcr-public-key=, and SecureBootPrivateKey=/--secureboot-private-key= below πŸ’–.

The generated certificate's name and lifetime are set with SecureBootCertificateCommonName=/--secureboot-certificate-common-name= and SecureBootCertificateValidity=/--secureboot-certificate-validity= 🌸.

⚠️ the output files must not exist. like, fr fr, they have to be brand new. if they're already there, this is not happening bestie πŸ’€

πŸ” inspect

Display information about the sections in a given binary or binaries. If --all is given, all sections are shown. Otherwise, if the --section= option is specified at least once, only those sections are shown. Otherwise, well-known sections that are typically included in an UKI are shown. For each section, its name, size, and sha256-digest is printed. And for text sections, the contents are printed too πŸ“.

Also see the description of -j/--json= and --section= πŸ‘€.

Other tools that may be useful for inspecting UKIs: llvm-objdump(1) -p and pe-inspect 🧚.


πŸ’œ CONFIGURATION SETTINGS πŸ’œ

Settings can appear in configuration files (the syntax with SomeSetting=value) and on the command line (the syntax with --some-setting=value). 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, that's described below, no worries 🫢.

If no config file is provided via the option --config=PATH, ukify will try to look for a default configuration file in the following paths, in this order:

  1. /etc/systemd/ukify.conf
  2. /run/systemd/ukify.conf
  3. /usr/local/lib/systemd/ukify.conf
  4. /usr/lib/systemd/ukify.conf

…and then load the first one found. And if no configuration file is specified and no default one is found, ukify will just proceed normally. chill πŸ¦‹.

The LINUX and INITRD positional arguments β€” or the equivalent Linux= and Initrd= 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 ✨.

The following options and settings are understood:

⭐ Command line-only options ⭐

  • --config=PATH β€” 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 command line option does not fully override the config file setting, that's explicitly mentioned in the descriptions of the individual options. (Added in version 254.)
  • --measure, --no-measure β€” Enable or disable a call to systemd-measure(1) to print pre-calculated PCR values. Defaults to false. (Added in version 253.)
  • --policy-digest, --no-policy-digest β€” Enable or disable a call to systemd-measure(1) to print pre-calculated TPM2 policy digests. Useful for offline signing of PCR policies. Defaults to false. (Added in version 258.)
  • --section=NAME:TEXT|@PATH, --section=NAME:text|binary[@PATH] β€” For all verbs except inspect, the first syntax is used. Specify an arbitrary additional section "NAME". The argument may be a literal string, or "@" 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 ".linux" section, which is always last. For the inspect verb, the second syntax is used. The section NAME will be inspected (if found). If the second argument is "text", the contents will be printed. If the third argument is given, the contents will be saved to the file named PATH. Note that the name is used as-is, and if the section name should start with a dot, it must be included in NAME. (Added in version 253.)
  • --join-profile=PATH β€” Takes a path to an existing PE file containing an additional profile to add to the unified kernel image. The profile can be generated beforehand with ukify. The profile does not need to be signed or contain PCR measurements. All UKI PE sections of the specified PE file are copied into the generated UKI. This is useful for generating multi-profile UKIs. Note that this only copies PE sections that are defined by the UKI specification, and ignores any other ones β€” for example ".text" or similar πŸ™ˆ. (Added in version 257.)
  • --sign-profile=ID β€” Takes a profile ID for which signed PCR measurements should be generated by ukify. This option can be used together with --join-profile= when building the final unified kernel image. If not specified, signed PCR measurements will be added for all profiles. (Added in version 258.)
  • --join-pcrsig=PATH, --pcrsig=TEXT|@PATH β€” --join-pcrsig= takes a path to an existing PE file containing a previously built UKI. --pcrsig= takes a path to an existing pcrsig JSON blob, or a verbatim inline blob. They must be used together, and without specifying any other UKI section parameters. ukify will attach the pcrsig JSON blob to the UKI. This is useful in combination with --policy-digest to create a UKI and then sign the TPM2 policy digests offline πŸ”. (Added in version 258.)
  • --tools=DIRS β€” Specify one or more directories with helper tools. ukify will look for helper tools in those directories first, and if not found, try to load them from $PATH in the usual fashion. (Added in version 253.)
  • --output=FILENAME β€” The output filename. If not specified, the name of the LINUX argument, with the suffix ".unsigned.efi" or ".signed.efi" will be used, depending on whether signing for SecureBoot was performed. (Added in version 253.)
  • --summary β€” Print a summary of loaded config and exit. This is useful to check how the options from the configuration file and the command line are combined 🧐. (Added in version 254.)
  • --all β€” Print all sections (with inspect verb). (Added in version 255.)
  • --json β€” bestie, this one makes inspect spit out JSON instead of the usual vibe ✨. What you get is an object whose keys are the names of the sections that all profiles share (aka the ones sitting before that first .profile section), and each of those keys maps to an object describing that section by its "size" and "sha256", plus a "text" field if it's a textual section. UKI sections that can show up more than once (within the same profile) get represented as an array. And if the image has profiles in it, you also get a bonus "_profiles" key holding an array with one object per profile, each one keyed by section name the exact same way (starting with its own .profile section). πŸ’…

Added in version 255.

  • -h, --help β€” prints a lil short help text and dips. 🫢
  • --version β€” prints a short version string and dips. πŸ¦‹

✨ [UKI] section ✨

  • Linux=LINUX, --linux=LINUX β€” a path to the kernel binary. that's the whole thing, no cap. 🌸

Added in version 254.

  • OSRelease=TEXT|@PATH, --os-release=TEXT|@PATH β€” the os-release description (that's the .osrel section, iykyk). The argument can be a literal string, or an "@" followed by a path name. If you don't specify it, the os-release(5) file gets picked up from the host system. But if you explicitly set it to an empty string, the .osrel section gets omitted from the UKI entirely β€” and girl, this is not recommended in most cases, because it makes the resulting artifact not be recognized as a UKI by other tools like kernel-install and bootctl. πŸ’€

Added in version 253.

  • Cmdline=TEXT|@PATH, --cmdline=TEXT|@PATH β€” the kernel command line (the .cmdline section, obviously πŸ’…). Argument is either a literal string, or "@" followed by a path name. If you don't specify it, no command line gets embedded. that's it. ✨

Added in version 253.

  • Initrd=INITRD..., --initrd=LINUX β€” zero or more initrd paths (yes, zero or more, she's flexible like that πŸ’–). In the config file, the items are separated by whitespace. The initrds get combined in the order you specified them, with the ones from the config file coming first.

Added in version 254.

  • Microcode=UCODE, --microcode=UCODE β€” path to the initrd that's holding your microcode updates. If you don't specify it, the section just won't be there. bye. πŸ‘‹

Added in version 256.

  • Splash=PATH, --splash=PATH β€” a picture to display during boot (the .splash section πŸŽ€). The argument is a path to a BMP file. If you don't specify it, the section will not be present.

Added in version 253.

  • DeviceTree=PATH, --devicetree=PATH β€” the devicetree description (the .dtb section). The argument is a path to a compiled binary DeviceTree file. If not specified, the section will not be present.

Added in version 253.

  • DeviceTreeAuto=PATH..., --devicetree-auto=PATH β€” zero or more automatically selectable DeviceTree files πŸ¦‹. In the config file, items are separated by whitespace. Each DeviceTree goes in its own separate .dtbauto section.

Added in version 257.

  • HWIDs=PATH, --hwids=PATH β€” the hardware ID device table (the .hwids section). The argument is a path to a directory full of JSON HWID device description files. Each file needs to contain a single JSON object with "name", "compatible" and "hwids" keys. The "name" and "compatible" keys must have string values, and the "hwids" key must have a list of strings as its value, where those strings must be valid UUIDs representing CHIDs/HWIDs. Example, bestie:
{
    "type": "devicetree",
    "name": "Example Laptop 16 Gen 7",
    "compatible": "example,laptop-16-g7",
    "hwids": [
        "5dc05bf4-01f6-4089-b464-a08c47ea9295",
        "3e3f8f3c-2003-46f2-811c-85554f7d5952"
    ]
}

So here "Example Laptop 16 Gen 7" is the device "name" (as defined by the manufacturer), "example,laptop-16-g7" is the "compatible" (as defined by the kernel), and "hwids" is an array of CHIDs/HWIDs (extracted, i.e., from fwupdtool hwids output). Now listen πŸ‘‘: if you don't specify this, and the /usr/lib/systemd/boot/hwids/[EFI_ARCH]/ directory exists, then the section gets automatically populated from that directory β€” (specify an empty string as the parameter for this option if you want to disable that behavior) β€” otherwise it just won't be present. And it's recommended that you specify this parameter if automatically selectable DeviceTrees are gonna be used. ✨

Added in version 257.

  • Uname=VERSION, --uname=VERSION β€” specify the kernel version (as in uname -r, the .uname section). If you don't specify it, an attempt gets made to extract the version string from the kernel image. It's recommended you pass this explicitly if you know it, because that extraction is based on heuristics and is not very reliable πŸ™ˆ. And if you don't specify it and the extraction fails, the section will not be present.

Added in version 253.

  • SBAT=TEXT|@PATH, --sbat=TEXT|@PATH β€” SBAT metadata associated with the UKI or addon. SBAT policies are lowkey iconic for revoking whole groups of UKIs or addons with a single, static policy update that does not take up space in DBX/MOKX. If you don't specify it manually, a default metadata entry gets used, consisting of
uki,1,UKI,uki,1,https://uapi-group.org/specifications/specs/unified_kernel_image/

for UKIs, and

uki-addon,1,UKI Addon,addon,1,https://www.freedesktop.org/software/systemd/man/latest/systemd-stub.html

for addons β€” this is done to ensure it's always possible to revoke them. For more info on SBAT, see the Shim documentation[3]. πŸ’…

Added in version 254.

  • PCRPKey=PATH, --pcrpkey=PATH β€” a path to a public key to embed in the .pcrpkey section. If you don't specify it, and there's exactly one PCRPublicKey=/--pcr-public-key= or PCRCertificate=/--pcr-certificate= argument, then that key gets used. Otherwise, the section will not be present. 🌸

Added in version 253.

  • Profile=PATH, --profile=PATH β€” a path to a UKI profile to place in an .profile section. This one is useful for creating multi-profile UKIs, and it's typically used in combination with --join-profile=, to extend the specified UKI with an additional profile. ✨

Added in version 257.

  • PCRBanks=PATH, --pcr-banks=PATH β€” a comma- or space-separated list of PCR banks to sign a policy for. If it's not present, all known banks get used ("sha1", "sha256", "sha384", "sha512"), which will fail if they're not supported by the system. πŸ’€

Added in version 253.

  • SignInitrdPCRs=BOOL, --sign-initrd-pcrs, --no-sign-initrd-pcrs β€” enable or disable signing of additional PCR policies that are bound to the initrd phase (after "enter-initrd" and before "leave-initrd") and the "initrd" policy reference. These additional signed policies get consumed by systemd-tpm2-setup-early.service(8) to authorize the initialization of NvPCRs. They're also useful for enrolling TPM keyslots to encrypted volumes that can only be unlocked from the initrd with systemd-cryptenroll(1), by using the --tpm2-public-key-policyref=initrd option. The extra signatures reuse the first PCRPrivateKey=/--pcr-private-key=, so one of those must be specified β€” unless --policy-digest is used to generate the policies for offline signing. Defaults to false. πŸŽ€

Added in version 262.

  • SecureBootSigningTool=SIGNER, --signtool=SIGNER β€” whether to use "sbsign", "pesign", or "systemd-sbsign". Depending on this choice, different parameters are required in order to sign an image (choose wisely bestie πŸ’…). Defaults to "sbsign".

Added in version 254.

  • SecureBootPrivateKey=SB_KEY, --secureboot-private-key=SB_KEY β€” a path to a private key to use for signing the resulting binary. If the SigningEngine=/--signing-engine= or SigningProvider=/--signing-provider= option is used, this may also be an engine- or provider-specific designation. This option is required by SecureBootSigningTool=sbsign/--signtool=sbsign and SecureBootSigningTool=systemd-sbsign/--signtool=systemd-sbsign. no cap. πŸ”₯

Added in version 253.

  • SecureBootCertificate=SB_CERT, --secureboot-certificate=SB_CERT β€” a path to a certificate to use for signing the resulting binary. If the SigningEngine=/--signing-engine= or SigningProvider=/--signing-provider= option is used, this may also be an engine- or provider-specific designation. This option is required by SecureBootSigningTool=sbsign/--signtool=sbsign and SecureBootSigningTool=systemd-sbsign/--signtool=systemd-sbsign. πŸ”₯

Added in version 253.

  • SecureBootCertificateCommonName=CN, --secureboot-certificate-common-name=CN β€” the common name and issuer name (the certificate is self-signed) of a certificate created by genkey. Defaults to "SecureBoot signing key on host HOST" with the fully qualified domain name of the machine it runs on. Must not be empty, and must not be longer than 64 bytes (that's the X.509 CN limit, periodt). πŸ’…

Added in version 262.

  • SecureBootCertificateDir=SB_PATH, --secureboot-certificate-dir=SB_PATH β€” a path to an nss certificate database directory to use for signing the resulting binary. This takes effect when SecureBootSigningTool=pesign/--signtool=pesign is used. Defaults to /etc/pki/pesign. πŸ¦‹

Added in version 254.

  • SecureBootCertificateName=SB_CERTNAME, --secureboot-certificate-name=SB_CERTNAME β€” the name of the nss certificate database entry to use for signing the resulting binary. This option is required by SecureBootSigningTool=pesign/--signtool=pesign. πŸ‘‘

Added in version 254.

  • SecureBootCertificateValidity=DAYS, --secureboot-certificate-validity=DAYS β€” the period of validity (in days) for a certificate created by genkey. Defaults to 3650, i.e. 10 years. ⭐

Added in version 254.

  • SigningEngine=ENGINE, --signing-engine=ENGINE β€” an OpenSSL engine to be used for signing the resulting binary and PCR measurements, see openssl-engine(1). πŸ’œ

Added in version 253.

  • SigningProvider=PROVIDER, --signing-provider=PROVIDER β€” an OpenSSL provider to be used for signing the resulting binary and PCR measurements, see provider(7). Heads up: this option can only be used when systemd-sbsign is used as the signing tool. πŸŽ€

Added in version 257.

  • CertificateProvider=PROVIDER, --certificate-provider=PROVIDER β€” an OpenSSL provider to be used for loading the certificate used to sign the resulting binary and PCR measurements, see provider(7). Same deal as above: this option can only be used when systemd-sbsign is used as the signing tool. ✨

Added in version 257.

  • SignKernel=BOOL, --sign-kernel, --no-sign-kernel β€” bestie, this one takes over the whole "should we sign the Linux binary itself before it gets embedded in the combined image?" decision. If you don't say anything, ukify will sign it only if you handed over a SecureBoot signing key via SecureBootPrivateKey=/--secureboot-private-key= and the binary isn't already signed. But if SignKernel=/--sign-kernel is true and the binary has already been signed? The signature gets appended anyway. No takebacks. πŸ’…

Added in version 253.


πŸ¦‹ [PCRSignature:NAME] section πŸ¦‹

Okay so in the config file these options live in their own little groups, one per section. On the command line though? You have to pass them in the same order. And if you specify sections in both places, they get combined. Teamwork, fr fr. 🀝

  • PCRPrivateKey=PATH, --pcr-private-key=PATH β€” the private key used to sign PCR policies. On the command line you can drop this one more than once, and if you do, you get multiple signatures. More keys, more signatures, iconic. πŸ”‘

Added in version 253.

  • PCRPublicKey=PATH, --pcr-public-key=PATH β€” a public key for signing PCR policies. ✨

On the command line you can repeat it just like --pcr-private-key=. If you don't give it, the public keys get extracted from the private keys. And heads up: on the command line, if you do give it, you must pass it the exact same number of times as --pcr-private-key=. Also you literally cannot use this one if --pcr-certificate= is in play. Pick a lane, bestie.

Added in version 253.

  • PCRCertificate=PATH, --pcr-certificate=PATH β€” an X.509 certificate for signing PCR policies. πŸ“œ

On the command line you can repeat it too, same deal as --pcr-private-key=. If it's absent, the public keys get pulled out of the private keys. And if it is present on the command line, it must appear the same number of times as --pcr-private-key=. Cannot be specified if --pcr-public-key= is used. It's giving mutual exclusivity. πŸ’€

Added in version 258.

  • Phases=LIST, --phases=LIST β€” a comma- or space-separated list of colon-separated phase paths, and you're signing a policy for each one. Every set of boot phase paths gets signed with the corresponding private key. If you don't include it, the default from systemd-measure(1) is used. Defaults? Love that for you. πŸŽ€

On the command line, when this argument shows up, it must appear the same number of times as the --pcr-private-key= option.

Added in version 253.

  • PolicyRef=STRING, --policyref=STRING β€” a policy reference to bind the generated signature to. 🫢

So what even is a policy reference? It's an opaque string that gets baked into the data being signed, and it's also worked into the authorization policy of any TPM2 resource that's locked against these signatures (that's the TPM2_PolicyAuthorize assertion doing its thing). Which means: a signed policy only gets accepted for a resource whose authorization policy was created with the same policy reference. Signed policies made with a different policy reference? Rejected. Straight up rejected, even if they came from the exact same signing key. The vibes have to match, no cap. βœ‹

On the command line, when this argument is present, it must appear the same number of times as the --pcr-private-key= option.

Added in version 262.

🌸 EXAMPLES 🌸

okay bestie, real talk β€” this is the part you actually came for. let's go. πŸ’–


Example 1. Minimal invocation ✨

$ ukify build \
      --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
      --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
      --cmdline='quiet rw'

That's it, that's the whole thing. This creates an unsigned UKI at ./vmlinuz.unsigned.efi. no cap, that's the minimum viable UKI. πŸ’…


Example 2. Direct kernel boot in a virtual machine πŸ–₯️

So when you're using qemu[2] with OVMF[4] (that's the UEFI Firmware for virtual machines, iykyk), you can literally just point the -kernel switch straight at a UKI. Like:

qemu-kvm -drive
if=pflash,format=qcow2,readonly=on,file=/usr/share/edk2/ovmf/OVMF_CODE_4M.qcow2
-kernel ./vmlinuz.unsigned.efi [ ... ]

(btw the path to that firmware file might need adjusting depending on your distribution β€” don't come for me if yours is somewhere else 🫢) Usually you'd also toss in another -drive argument to attach an actual disk image, but lowkey that's not required.


Example 3. All the bells and whistles πŸŽ€

okay NOW we're cooking. full main character energy:

$ ukify build \
      --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
      --initrd=early_cpio \
      --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
      --sbat='sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
uki.author.myimage,1,UKI for System,uki.author.myimage,1,https://uapi-group.org/specifications/specs/unified_kernel_image/' \
      --pcr-private-key=tpm2-pcr-initrd-private-key.pem \
      --pcr-public-key=tpm2-pcr-initrd-public-key.pem \
      --phases='enter-initrd' \
      --pcr-private-key=tpm2-pcr-private-key-system.pem \
      --pcr-public-key=tpm2-pcr-public-key-system.pem \
      --phases='enter-initrd:leave-initrd enter-initrd:leave-initrd:sysinit \
                enter-initrd:leave-initrd:sysinit:ready' \
      --pcr-banks=sha384,sha512 \
      --secureboot-private-key=secureboot-private-key.pem \
      --secureboot-certificate=secureboot-certificate.pem \
      --sign-kernel \
      --cmdline='quiet rw rhgb'

This creates a signed UKI at ./vmlinuz.signed.efi. Let me break down what just happened, because it's a lot:

  • The initrd section contains two concatenated parts: early_cpio and initramfs-6.0.9-300.fc37.x86_64.img.
  • The policy embedded in the ".pcrsig" section gets signed for the initrd (that's the enter-initrd phase) with the key tpm2-pcr-initrd-private-key.pem, and for the main system (phases leave-initrd, sysinit, ready) with the key tpm2-pcr-private-key-system.pem.
  • The Linux binary and the resulting combined image both get signed with the SecureBoot key secureboot-private-key.pem.

iconic. πŸ‘‘


Example 4. All the bells and whistles, via a config file πŸ“

Same energy as the last one, but this time the config lives in a file instead of being screamed at the command line:

$ cat ukify.conf
[UKI]
Initrd=early_cpio
Cmdline=quiet rw rhgb

SecureBootPrivateKey=secureboot-private-key.pem
SecureBootCertificate=secureboot-certificate.pem
SignKernel=yes
PCRBanks=sha384,sha512

[PCRSignature:initrd]
PCRPrivateKey=tpm2-pcr-initrd-private-key.pem
PCRPublicKey=tpm2-pcr-initrd-public-key.pem
Phases=enter-initrd

[PCRSignature:system]
PCRPrivateKey=tpm2-pcr-private-key-system.pem
PCRPublicKey=tpm2-pcr-public-key-system.pem
Phases=enter-initrd:leave-initrd
       enter-initrd:leave-initrd:sysinit
       enter-initrd:leave-initrd:sysinit:ready

$ ukify -c ukify.conf build \
        --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
        --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img

See what's happening? One "initrd" (early_cpio) is specified in the config file, and the other initrd (initramfs-6.0.9-300.fc37.x86_64.img) is specified on the command line. Why would you do this? Because it's actually useful β€” for example when the first initrd contains microcode for the CPU, and it does not need to be updated when the kernel version changes, unlike the actual initrd. big brain move fr fr. 🧠


Example 5. Kernel command line PE addon πŸ¦‹

ukify build \
      --secureboot-private-key=secureboot-private-key.pem \
      --secureboot-certificate=secureboot-certificate.pem \
      --cmdline='debug' \
      --sbat='sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
uki-addon.author,1,UKI Addon for System,uki-addon.author,1,https://www.freedesktop.org/software/systemd/man/systemd-stub.html'
      --output=debug.addon.efi

This creates a signed PE binary that contains the extra kernel command line parameter "debug", with SBAT metadata referring to the owner of the addon. cute little addon, doing its own thing. 🌈


Example 6. Decide signing policy, and create certificate and keys πŸ”

First things first β€” let's make a config file that spells out which signatures shall be made:

# cat >/etc/kernel/uki.conf <<EOF
[UKI]
SecureBootPrivateKey=/etc/kernel/secureboot-private-key.pem
SecureBootCertificate=/etc/kernel/secureboot-certificate.pem

[PCRSignature:initrd]
Phases=enter-initrd
PCRPrivateKey=/etc/systemd/tpm2-pcr-initrd-private-key.pem
PCRPublicKey=/etc/systemd/tpm2-pcr-initrd-public-key.pem

[PCRSignature:system]
Phases=enter-initrd:leave-initrd enter-initrd:leave-initrd:sysinit
       enter-initrd:leave-initrd:sysinit:ready
PCRPrivateKey=/etc/systemd/tpm2-pcr-private-key-system.pem
PCRPublicKey=/etc/systemd/tpm2-pcr-public-key-system.pem
EOF

Next up, we generate the certificate and keys:

# ukify genkey --config=/etc/kernel/uki.conf
Writing SecureBoot private key to /etc/kernel/secureboot-private-key.pem
Writing SecureBoot certificate to /etc/kernel/secureboot-certificate.pem
Writing private key for PCR signing to /etc/systemd/tpm2-pcr-initrd-private-key.pem
Writing public key for PCR signing to /etc/systemd/tpm2-pcr-initrd-public-key.pem
Writing private key for PCR signing to /etc/systemd/tpm2-pcr-private-key-system.pem
Writing public key for PCR signing to /etc/systemd/tpm2-pcr-public-key-system.pem
Both operations need to be done as root, so you actually have write access to /etc/kernel/. πŸ”₯

After that, any later invocation that uses the config file β€” like ukify build --config=/etc/kernel/uki.conf β€” will use these certificate and key files. And heads up: the kernel-install(8) plugin 60-ukify.install uses /etc/kernel/uki.conf by default, so once this file exists, installations of kernels that create a UKI on the local machine using kernel-install will perform signing using this config. automatic. πŸ’…


Example 7. Multi-Profile UKI πŸ‘―

First, create a few profiles:

$ ukify build \
      --profile='TITLE=Base' \
      --output=profile0.efi

Add a second profile (@1):

$ ukify build \
      --profile='TITLE=Boot into Storage Target Mode
ID=storagetm' \
      --cmdline='quiet rw rd.systemd.unit=storage-target-mode.target' \
      --output=profile1.efi

Add a third profile (@2):

$ ukify build \
      --profile='TITLE=Factory Reset
ID=factory-reset' \
      --cmdline='quiet rw systemd.unit=factory-reset.target' \
      --output=profile2.efi

Then, create a UKI and include all the generated profiles:

$ ukify build \
      --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
      --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
      --cmdline='quiet rw' \
      --join-profile=profile0.efi \
      --join-profile=profile1.efi \
      --join-profile=profile2.efi \
      --output=base.efi

The resulting UKI base-with-profile-0-1-2.efi will now contain three profiles. squad goals. 🫢


Example 8. Offline signing of pcrsig section 🧚

First, create a UKI and save the PCR JSON blob:

$ ukify build \
      --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
      --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
      --cmdline='quiet rw' \
      --pcr-public-key=tpm2-pcr-initrd-public-key.pem \
      --policy-digest \
      --sign-initrd-pcrs \
      --json=short \
      --output=base.efi >base.pcrs

Then, sign the PCR digests offline and insert them in the JSON blob:

#!/usr/bin/python3
import base64, json, subprocess

priv_key = '/home/zbyszek/src/systemd/tpm2-pcr-private.pem'
base_file = 'base.pcrs'

base = json.load(open(base_file))

for bank,policies in base.items():
    for policy in policies:
        tbs = base64.b16decode(policy['tbs'].upper())
        call = subprocess.run(['openssl', 'dgst', '-sha256', '-sign', priv_key],
                input=tbs,
                              check=True,
                              capture_output=True)
        sig = base64.b64encode(call.stdout).decode()
        policy['sig'] = sig

print(json.dumps(base))

Finally, attach the updated JSON blob to the UKI:

$ ukify build \
      --join-pcrsig=base.efi \
      --pcrsig=@base.pcrs \
      --json=short \
      --output=base-signed.efi

The resulting UKI base-signed.efi will now contain the signed PCR digests. chef's kiss. πŸ˜­πŸ’–


πŸ‘€ SEE ALSO πŸ‘€

go read these next, bestie, they're the whole extended universe:


πŸ“ NOTES πŸ“

the receipts, in order:

  1. UAPI.5 Unified Kernel Image (UKI) β€” https://uapi-group.org/specifications/specs/unified_kernel_image/
  2. qemu β€” https://www.qemu.org/docs/master/
  3. Shim documentation β€” https://github.com/rhboot/shim/blob/main/SBAT.md
  4. OVMF β€” https://www.linux-kvm.org/downloads/lersek/ovmf-whitepaper-c770f8c.txt

and that's the whole page, bestie!! πŸŽ€ you just read the entire UKIFY(1) manual, and honestly? you ate. this one's from systemd 262.1 β€” go forth and build your unified kernel images, I'm so proud of you. mwah. πŸ’œβœ¨

πŸ“¦ where this page comes from