1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
|
#+title: Secure Booting
#+date: 2026-03-03 Mon
#+author: W. Kosior
#+email: wkosior@agh.edu.pl
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://galaxy.agh.edu.pl/~wkosior/debian-13-nocloud-amd64.qcow2][the Debian VM image]] we are going to work with. This is the [[https://cloud.debian.org/images/cloud/trixie/latest/][image 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 do it
with a graphical interface. In this case, switch to the "serial-0" tab in
QEMU's window to interact with the guest 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
Or, you can have QEMU connect your terminal to the VM's serial port. =Ctrl+a x=
can then 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
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
* 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
#+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
#+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
* 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). First,
let's try to execute it, unsigned, on our Secure Booted system.
* 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.
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).
* Signing an EFI Application with Machine Owner Key
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. Confirm with the password you chose before.
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.
** 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 ./gnu-efi/x86_64/apps/t.efi \
./t-signed.efi
sbverify --list gnu-efi/x86_64/apps/t.efi # No sigs.
sbverify --list ./t-signed.efi # Our sig present.
#+end_src
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.
* Replacing the Entire Key Chain
We shall now replace PK, KEK and db. We are going to compile our own shim, sign
it and use it with the usual Debian bootloader and kernel.
First, in the guest, backup the certificates from EFI variables (we omit dbx as
the blacklisted hash in it can as well remain there).
#+begin_src shell-script
for VAR in PK KEK db; do
efi-readvar -v $VAR -o $VAR-old.esl
done
#+end_src
Note that we are also able to extract individual certificates from the EFI
signature list (=.esl=) files.
#+begin_src shell-script
sig-list-to-certs KEK-old.esl /tmp/KEK-old
openssl x509 -inform DER -in /tmp/KEK-old-0.der -text -noout
# Just to see the contents:
openssl x509 -inform DER -in /tmp/KEK-old-1.der -text -noout
#+end_src
Also export the keys from the MokList.
#+begin_src shell-script
mokutil --export
# Just to see the contents of the exported cert(s):
openssl x509 -inform DER -in MOK-0001.der -text -noout
#+end_src
Copy the EFI signature list files and =MOK-000?.der= files out of the VM.
#+begin_src shell-script
ssh -p 30022 root@$NBD_SERVER_IP 'tar -czf - *-old.esl MOK-*.der' | \
tar -xzf - # Just an example of how it could be done.
#+end_src
We'll now build our own shim binary to use instead of the distro-shipped one.
This happens to make another clone (as a git submodule) of the gnu-efi repo that
we already have, but it's not something to worry about.
#+begin_src shell-script
git clone https://github.com/rhboot/shim/ --recursive
#+end_src
We could pass a trusted cert to the shim's build system, but we don't need to.
shim is able to use the certs in db variable instead of those from MokList. We
could also specify another bootloader filename than grubx64.efi (which is by
default hardcoded in shim).
We build the shim with a simple =make= invokation. The resulting binary is
called =shim/shimx64.efi= (inside the project directory).
We shall now generate the keys.
#+begin_src shell-script
for VAR in PK KEK db; do
# Other lengths than 2048 might not be supported by some UEFI firmwares :(
openssl req -new -newkey rsa:2048 -days $((365 * 10)) -noenc -x509 \
-keyout $VAR.key -out $VAR.pem -subj "/CN=My $VAR 2026"
done
#+end_src
Sign the shim. Use =sbsign= command from before, adapted to use the db key.
Assume the result is in a file called =my-shim-signed.efi=.
We could've also signed =mmx64.efi= (the Mok Manger) and =fbx64.efi= (shim's
fallback bootloader) but they're not needed in our scenario.
Generate new EFI signature lists and their respective =.auth= files for loading
into the variables. A GUID needs to be supplied, but is not very interesting to
us (it identifies the owner of the entry). We can use different GUIDs or a
single one for different signature lists in subsequent steps.
#+begin_src shell-script
uuidgen
uuidgen # Just to see how it works.
MY_GUID="$(uuidgen)"
# Cert in PEM format must be converted to EFI signature list.
cert-to-efi-sig-list -g $MY_GUID PK.pem PK.esl # Creates `PK.esl'.
sign-efi-sig-list -k PK.key -c PK.pem PK PK.esl PK.auth # Creates `PK.auth'.
#+end_src
The signed =.auth= file shall be used with UEFI to replace the contents of PK.
We also need =KEK.auth= and =db.auth=.
#+begin_src shell-script
cert-to-efi-sig-list -g $MY_GUID KEK.pem KEK.esl # Analogically.
# The following is not completely analogical, we now sign KEK with sth other
# than KEK.
sign-efi-sig-list -k PK.key -c PK.pem KEK KEK.esl KEK.auth
#+end_src
In the db variable, we shall store the distribution cert as well. We need to
include them.
#+begin_src shell-script
cert-to-efi-sig-list -g $MY_GUID db.pem db.esl # Analogicall, but…
# MOK-0001.der is likely the Debian cert and 0002 is likely our MOK cert from
# the earlier exercise. Use grep to confirm. We only need the former cert now
# (also, the latter has been made by us with larger RSA key than supported in
# sig list).
openssl x509 -inform DER -in MOK-0001.der -text -noout | grep Debian
openssl x509 -inform DER -in MOK-0001.der > MOK-0001.pem # To text format.
cert-to-efi-sig-list -g $MY_GUID MOK-0001.pem debian.esl
grep Debian debian.esl # If there's no match, it means we screwed up ;)
cat debian.esl >> db.esl # YES, signature lists can be simply concatenated!
sign-efi-sig-list -k KEK.key -c KEK.pem db db.esl db.auth # Sign db with KEK.
#+end_src
Now, copy all =.auth= files and shim to the guest.
#+begin_src shell-script
tar -czf - *.auth my-shim-signed.efi | \
ssh -p 30022 root@10.1.2.3 'tar -xzf -'
#+end_src
In the UEFI firmware settings, go to:
- Device Manager
- Secure Boot Configuration
Set "Secure Boot Mode" to "Custom Mode".
In "Custom Mode Boot Options" seledt
- PK Options
- Delete PK
You can (but probably not need to) do analogically with KEK and db. Reboot to
system when you're done.
#+begin_src shell-script
mokutil --sb-state # Should report "Setup Mode".
#+end_src
When the system is in "Setup Mode" (as is the case after deleting PK), it means
PK, KEK, db and dbx are writeable by the OS! It shall automatically exit Setup
Mode when we write the PK.
Enroll the keys.
#+begin_src shell-script
efi-updatevar -f PK.auth PK
# Do not turn off the VM now or you'll have to delete the PK again!
mokutil --sb-state # should no longer report Setup Mode :)
efi-updatevar -f KEK.auth KEK
efi-updatevar -f db.auth db
cp my-shim-signed.efi /boot/efi/EFI/BOOT/
# Now, change the boot order so that the firmware tries to load our new shim
# rather than the old one.
efibootmgr # lists current boot entries
efibootmgr -c --part 15 --label "My Own Shim" --disk /dev/sda \
--loader '\EFI\BOOT\my-shim-signed.efi'
# You can reboot the VM now.
#+end_src
After rebooting, verify that secure boot is on.
#+begin_src shell-script
mokutil --sb-state
#+end_src
We have now somewhat decreased the attack surface — only code from Debian (&
code signed by ourselved) can run in ring 0. All at the cost of having to
maintain our own signed shim binaries.
Try rebooting and selecting the "QEMU HARDDISK" option in the UEFI boot menu.
It tries to load the default bootx64.efi binary (the old shim from Debian,
signed by Microsoft) and fails due to secure boot. In case of OVMF, the failure
is silent (no error message, we just return to the Boot Manager Menu).
* Appending Certs
If for some reason we need to allow Microsoft-signed software (e.g., other
distros' shims), we can re-add the Microsoft key. The =-a= flag to
=sign-efi-sig-list= allows us to create a .auth file that will cause entries to
be appended to secure variable list instead of completely replacing the variable
contents.
#+begin_src shell-script
sign-efi-sig-list -a -k KEK.key -c KEK.pem db db-old.esl db-old-readd.auth
#+end_src
After transmitting the .auth file to the guest, we can do:
#+begin_src shell-script
# Note the `-a' switch needed for *appending*.
sudo efi-updatevar -a -f db-old-readd.auth db
# If there are permission errors, see if the OS protection of EFI variables from
# accidental midification is on.
lsattr /sys/firmware/efi/efivars/db*
# The following should remove the additional kernel restriction on updating
# efivars.
sudo chattr -i /sys/firmware/efi/efivars/db-*
sudo efi-updatevar -a -f db-old-readd.auth db # Should now work.
#+end_src
Reboot. The old shim from Debian (and others signed by Microsoft) should now
work as well.
|