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:
/etc/systemd/ukify.conf/run/systemd/ukify.conf/usr/local/lib/systemd/ukify.conf/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 tosystemd-measure(1)to print pre-calculated PCR values. Defaults tofalse. (Added in version 253.)--policy-digest,--no-policy-digestβ Enable or disable a call tosystemd-measure(1)to print pre-calculated TPM2 policy digests. Useful for offline signing of PCR policies. Defaults tofalse. (Added in version 258.)--section=NAME:TEXT|@PATH,--section=NAME:text|binary[@PATH]β For all verbs exceptinspect, 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 theinspectverb, the second syntax is used. The sectionNAMEwill 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 namedPATH. Note that the name is used as-is, and if the section name should start with a dot, it must be included inNAME. (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 withukify. 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 byukify. 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.ukifywill attach the pcrsig JSON blob to the UKI. This is useful in combination with--policy-digestto 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.ukifywill look for helper tools in those directories first, and if not found, try to load them from$PATHin the usual fashion. (Added in version 253.)--output=FILENAMEβ The output filename. If not specified, the name of theLINUXargument, 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 (withinspectverb). (Added in version 255.)--jsonβ bestie, this one makesinspectspit 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.profilesection), 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.profilesection). π
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.osrelsection, iykyk). The argument can be a literal string, or an"@"followed by a path name. If you don't specify it, theos-release(5)file gets picked up from the host system. But if you explicitly set it to an empty string, the.osrelsection 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 likekernel-installandbootctl. π
Added in version 253.
Cmdline=TEXT|@PATH,--cmdline=TEXT|@PATHβ the kernel command line (the.cmdlinesection, 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.splashsection π). 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.dtbsection). 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.dtbautosection.
Added in version 257.
HWIDs=PATH,--hwids=PATHβ the hardware ID device table (the.hwidssection). 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 inuname -r, the.unamesection). 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.pcrpkeysection. If you don't specify it, and there's exactly onePCRPublicKey=/--pcr-public-key=orPCRCertificate=/--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.profilesection. 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 bysystemd-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 withsystemd-cryptenroll(1), by using the--tpm2-public-key-policyref=initrdoption. The extra signatures reuse the firstPCRPrivateKey=/--pcr-private-key=, so one of those must be specified β unless--policy-digestis used to generate the policies for offline signing. Defaults tofalse. π
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 theSigningEngine=/--signing-engine=orSigningProvider=/--signing-provider=option is used, this may also be an engine- or provider-specific designation. This option is required bySecureBootSigningTool=sbsign/--signtool=sbsignandSecureBootSigningTool=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 theSigningEngine=/--signing-engine=orSigningProvider=/--signing-provider=option is used, this may also be an engine- or provider-specific designation. This option is required bySecureBootSigningTool=sbsign/--signtool=sbsignandSecureBootSigningTool=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 bygenkey. 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 whenSecureBootSigningTool=pesign/--signtool=pesignis 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 bySecureBootSigningTool=pesign/--signtool=pesign. π
Added in version 254.
SecureBootCertificateValidity=DAYS,--secureboot-certificate-validity=DAYSβ the period of validity (in days) for a certificate created bygenkey. Defaults to3650, 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, seeopenssl-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, seeprovider(7). Heads up: this option can only be used whensystemd-sbsignis 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, seeprovider(7). Same deal as above: this option can only be used whensystemd-sbsignis 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 viaSecureBootPrivateKey=/--secureboot-private-key=and the binary isn't already signed. But ifSignKernel=/--sign-kernelis 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 fromsystemd-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_cpioandinitramfs-6.0.9-300.fc37.x86_64.img. - The policy embedded in the
".pcrsig"section gets signed for the initrd (that's theenter-initrdphase) with the keytpm2-pcr-initrd-private-key.pem, and for the main system (phasesleave-initrd,sysinit,ready) with the keytpm2-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:
- UAPI.5 Unified Kernel Image (UKI) β https://uapi-group.org/specifications/specs/unified_kernel_image/
- qemu β https://www.qemu.org/docs/master/
- Shim documentation β https://github.com/rhboot/shim/blob/main/SBAT.md
- 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
- arch rolling: systemd-ukify 262.1-1 Β· LGPL-2.1-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms