#+title: Secure Booting #+date: 2026-03-03 Mon #+author: W. Kosior #+email: wkosior@agh.edu.pl #+HTML_HEAD: In these exercises we are going to configure Secure Boot in a virtualized environment. * Useful resources [[https://morfikov.github.io/post/jak-dodac-wlasne-klucze-dla-secure-boot-do-firmware-efi-uefi-pod-linux/#zmienne-pk-kek-db-dbx-i-moklist][A blog post about replacing the UEFI Secure Boot keys]] [[https://www.intel.com/content/dam/develop/external/us/en/documents/sf13-stts002-100p-820238.pdf][Intel's presentation about UEFI Secure Boot]] [[https://tianocore-docs.github.io/edk2-UefiDriverWritersGuide/draft/5_uefi_services/52_services_that_uefi_drivers_rarely_use/525_getvariable_and_setvariable.html#525-getvariable-and-setvariable][TianoCore documentation]] ← useful to understand how shim is able to store MOK keys list in a UEFI variable without making them accessible to the OS. * Using QEMU We shall use QEMU to virtualize the system we configure. First, let's download [[https://pluton.kt.agh.edu.pl/~wokosior/bso-vms/debian-13-nocloud-amd64.qcow2][the Debian VM image]] we are going to work with. This is the =debian-13-nocloud-amd64.qcow2= [[https://cloud.debian.org/images/cloud/trixie/latest/][from debian.org]] modified in the following ways - timezone set to "Europe/Warsaw" - credentials set to =root=​/​=security= - packages =efibootmgr=, =efitools= and =openssh-server= installed - sshd configured to allow login as root using password (=sed -i 's_#\(PermitRootLogin \).*_\1yes_' /etc/ssh/sshd_config=) Verify that you are able to start the VM and log in. You can choose to have QEMU connect your terminal to the VM's serial port. =Ctrl+a x= can be used to forcefully terminate it. #+begin_src shell-script qemu-system-x86_64 \ -nographic \ -m 1G \ -enable-kvm \ -hda debian-13-nocloud-amd64.qcow2 #+end_src Or, you can do it with a graphical interface. This option shall be more convenient, when, e.g., using the guested GRUB shell. QEMU's option to zoom in the VM screen can prove useful as well. Remember to switch to the "serial-0" tab in QEMU's window to interact with the guest OS once it boots. #+begin_src shell-script qemu-system-x86_64 \ -vga virtio \ -m 1G \ -enable-kvm \ -hda debian-13-nocloud-amd64.qcow2 #+end_src Try appending the following command line arguments to make VM's port 22 reachable at host's port 30022. Then, see if you can also log in with SSH (useful to transfer files). #+begin_example -net nic -net user,hostfwd=tcp::30022-:22 #+end_example #+begin_src shell-script ssh -p 30022 root@127.0.0.1 #+end_src *Prepare screenshot 1: successful SSH connection to the guest.* * Booting with UEFI QEMU does — by default — boot x86 guests using the legacy boot sequence. The ovmf Debian / Ubuntu package provides alternative firmware for QEMU that supports UEFI (including Secure Boot). Look into its documentation (on the host!) to see what files are useful in which scenarios. #+begin_src shell-script less /usr/share/doc/ovmf/README.Debian ls /usr/share/{ovmf,OVMF} #+end_src For our exercises we want the firmware and variables images with Microsoft certs in them. Copy these to the current directory (in fact, we would also do with only copying the variables file). We also want to tell QEMU to enable virtualization of the SMM (System Management Mode) necessary for privileged firmware code to handle Secure Boot tasks. Look into the QEMU manual for how to enable it (it is done by telling QEMU to emulate the "q35" machine type). #+begin_src shell-script man qemu-system-x86_64 # Search interactively with `/' and `n' or pipe to grep. #+end_src We boot with UEFI firmware by telling QEMU to emulate flash memory storage devices (one for code and one for EFI variables). #+begin_example -drive if=pflash,format=raw,readonly=on,file=./OVMF_CODE_4M.ms.fd \ -drive if=pflash,format=raw,file=./OVMF_VARS_4M.ms.fd \ #+end_example With the right options, QEMU shuold boot the VM with UEFI firmware and Secure Boot enabled by default. From within the guest OS, use =mokutil= and =lockdown= commands to verify that Secure Boot is indeed active and that kernel has limited our ability as root to run arbitrary ring 0 code. #+begin_src shell-script mokutil --sb-state dmesg | grep -i lockdown #+end_src *Prepare screenshot 2: the output of the two commands above.* * Our Own EFI Application EDK II is the officially recommended framework for development of UEFI software. However, simple applications (e.g., bootloaders) can just as well be built using another tool called gnu-efi. Clone [[https://github.com/ncroxon/gnu-efi][the repository]]. Afterwards, build with a simple =make= invokation. The =x86_64/apps/= directory inside project's root shall contain several demo EFI applications. We shall use =t.efi= (a "Hello, World!" application). In the subsequent sections we shall try to execute it, initially unsigned, on our Secure Booted system. *Prepare screenshot 3: the final lines of the =make= command output (indicating a successful build).* * Team Work It is a good opportunity to switch to team work. If possible, make pairs. Select one member to operate the VM guest and QEMU commands on one computer. The other member shall use the other computer to generate keys as well as build and sign software. We need to be able to somehow transfer files to our guest. Here, we propose two ways of achieving this. ** Use Network Block Device to Mount Remote Filesystem We can expose the QCOW2-formatted QEMU hard drive over the network using the =qemu-nbd= command (the VM needs to be shut down first). #+begin_src shell-script ip a # Check our IP. qemu-nbd ./debian-13-nocloud-amd64.qcow2 # listens on port 10809 by default #+end_src Then, another host can access it (here we assume that 10.1.2.3 is the IP address of the first host). #+begin_src shell-script sudo apt install nbd-client NBD_SERVER_IP=10.1.2.3 # Adjust according to needs. sudo modprobe nbd sudo nbd-client $NBD_SERVER_IP 10809 /dev/nbd0 #+end_src Now =/dev/nbd0= is the guest's drive. We can use the =fdisk= command to check which of its partitions is the ESP (EFI System Partition) and which holds the Debian OS root filesystem. #+begin_src shell-script sudo fdisk -l /dev/nbd0 #+end_src We can then make the desired filesystem available somewhere. Swap "=x=" in =/dev/nbd0px= for the appropriate partition number. #+begin_src shell-script sudo mkdir -p /mnt/some-qemu-guest-partition sudo mount /dev/nbd0px /mnt/some-qemu-guest-partition ls /mnt/some-qemu-guest-partition #+end_src Once we modify the disk and we want to boot the VM again, we need to close the NBD server. #+begin_src shell-script sudo umount /mnt/some-qemu-guest-partition sudo nbd-client -d /dev/nbd0 #+end_src The =qemu-nbd= server process on the other host shall terminate automatically (unless it has been passed the =--persistent= options). ** Use SSH Assuming we have not broken our installation (yet), we can communicate with guest OS using SSH. The ESP (EFI System Partition) has its filesystem mounted at =/boot/efi= in the guest. #+begin_src shell-script NBD_SERVER_IP=10.1.2.3 # Adjust according to needs. ssh -p 30022 root@$NBD_SERVER_IP 'ls /boot/efi/' ssh -p 30022 root@$NBD_SERVER_IP # To connect interactively. #+end_src * Failing to Run an Unsigned EFI Application Place the =t.efi= inside =/EFI/BOOT/= on the ESP. Then, boot or reboot the VM and when in GRUB menu, press "=c=" to enter GRUB console. The =chainloader= command can be used to execute another EFI binary from within GRUB. Note that GRUB's command line seems to behave better in QEMU GUI (when used in serial console, it might lack proper support for command line editing with the backspace key and arrows). Swap "=x=" in =hd0,gptx= for the number of the ESP. #+begin_src shell-script ls # Prints the partitions that GRUB sees. ls (hd0,gpt1)/ # Prints directory contents of the first partition's filesystem. chainloader (hd0,gptx)/EFI/BOOT/t.efi boot #+end_src If Secure Boot functions properly, we should *not* see the "Hello, World!" text printed by =t.efi=. GRUB should report an issue with verification of the binary. *Prepare screenshot 4: the output of the =chainloader= and =boot= GRUB commands, with the error message printed by GRUB due to failed signature verification.* Now, try doing the same after temporarily disabling Secure Boot in UEFI Firmware Settings (accessible from GRUB). You can alternatively boot the VM with different files from the OVMF Debian package (those without Secure Boot enabled). Some people report lack of the "Hello, World!" output from =t.efi=. If after executing =boot= you see just a GRUB error message telling you that image hasn't been loaded, it is OK and you can proceed. * Signing an EFI Application with Machine Owner Key For this step to work, you might need to install the =efitools= package on the host. #+begin_src shell-script sudo apt install efitools #+end_src We shall now create and register a Machine Owner Key with the shim and then sign =t.efi=. First, generate an X.509 cert in *binary format* (replace the question marks in the command below). You can also use your own name after =/CN==. Use the subequent command to check the fingerprint of your key. #+begin_src shell-script openssl req -new -newkey rsa:3072 -days $((365 * 10)) -noenc -x509 \ -keyout mok.key -out mok.der -outform ??? \ -subj '/CN=My MOK 2026' openssl x509 -inform ??? -in mok.der -text -noout -fingerprint | tail -1 #+end_src ** Import with =mokutil= Move your self-signed certificate, =mok.der=, into the VM. Look around and see how shim makes some information about certificates on the MOK list available to the OS. Notice that initially, there's no =MokNew= EFI variable. #+begin_src shell-script ls /sys/firmware/efi/efivars/Mok* #+end_src We can see that by default, the MOK list gets populated with certs of the distribution from which the shim comes. #+begin_src shell-script mokutil --list-enrolled #+end_src Still within the guest, request shim to add a trusted key to MokList upon the next reboot. #+begin_src shell-script mokutil --list-new # Empty for now. mokutil --import mok.der # Use your password of choice when prompted. mokutil --list-new # We already see our key (waiting in the queue). #+end_src Check if you can see the =MokNew= EFI variable now. It is used by the kernel to communicate key import request to the shim. After reboot the shim should automatically present the Mok Manager interface to us. Choose the "Enroll MOK" option, verify the fingerprint of the key and confirm its addition. You should now see a password prompt. *Prepare screenshot 5: Mok Manager password prompt.* Confirm with the password you chose before. After booting, see that the key has been added. #+begin_src shell-script mokutil --list-enrolled #+end_src As an extended exercise, delete the key now (we'll re-import it shortly). #+begin_src shell-script mokutil --delete mok.der #+end_src Reboot again to confirm the deletion with Mok Manager. ** Import from file with Mok Manager Let us now write the certificate to a virtual USB drive and this time import it directly from file, using the MokManager interface. #+begin_src shell-script qemu-img create -f qcow2 usb-thumb-drive.qcow2 256M sudo qemu-nbd usb-thumb-drive.qcow2 #+end_src #+begin_src shell-script sudo nbd-client $NBD_SERVER_IP 10809 /dev/nbd1 sudo fdisk /dev/nbd1 #+end_src Inside =fdisk= type: - =g= for gpt partition table - =n= for new partition - =Enter= a few times for default number and size of the partition - =t= to change partition type - =L= for help and =1= to select the "EFI system" type - =w= to commit changes In case you're curious: "msdos" is the same as 32-bit FAT. Format the partition and put your =mok.der= there. Unmount and disconnect afterwards as usual. #+begin_src shell-script sudo mkfs.msdos /dev/nbd1p1 sudo mkdir -p /mnt/qemu-thumb-drive sudo mount /dev/nbd1p1 /mnt/qemu-thumb-drive #+end_src You can use the following options to QEMU to have it present the image to the VM as a USB thumb drive. #+begin_example -drive if=none,id=stick,format=qcow2,file=./usb-thumb-drive.qcow2 \ -device nec-usb-xhci,id=xhci \ -device usb-storage,bus=xhci.0,drive=stick #+end_example Enter the GRUB command line again and load MokManager manually. Is is trusted by shim and should execute without problems. The addition of the USB drive might have led to different enumeration of drives. I.e., GRUB might now see the main VM drive as =hd1= instead of =hd0=. #+begin_src shell-script chainloader (hd1,gptx)/EFI/BOOT/mmx64.efi boot #+end_src Use the Mok Manager interface to load your cert form the virtual USB drive. *Prepare screenshot 6: Mok Manager interface with two filesystems listed as available to browse.* ** Application Signing We have imported the key, so now we can finally sign an EFI application and have it verified by the shim. We first need to convert the cert to PEM format (that's what =sbsign= tool needs). #+begin_src shell-script openssl x509 -inform der -in mok.der -out mok.pem #+end_src We can then sign =t.efi=. #+begin_src shell-script sbsign --cert mok.pem --key mok.key --output ./t-signed.efi \ ./gnu-efi/x86_64/apps/t.efi sbverify --list gnu-efi/x86_64/apps/t.efi # No sigs. sbverify --list ./t-signed.efi # Our sig present. #+end_src *Prepare screenshot 7: The output of the commands above.* Transfer =t-signed.efi= to ESP, boot the VM in Secure Boot mode into GRUB and try =chainload='ing it. If you did everything correctly, it should work now and we should *no longer* see the "bad shim signature" message (although the aforementioned issue with "Hello, World!" not displayed might persist). *Prepare screenshot 8: The last =chainload= and =boot= commands together with the output.*