100200300400500600
  
 
 

Tinfoil session 2: Recovering secure boot vector

So having secure boot enabled with unified kernel image is great, but how to integrate it into OS lifecycle management? And how to do it without compromising the security? The simplicity at one place causes complexity at another.

When I first though of switching to secure boot the UKI was an obvious choice because you get all in one simple flakon: kernel, ramfs/initrd, boot cmdline and even a splash as a bonus. The installation is super-simple - just drop it onto ESP and add a boot option in UEFI BIOS. And if you sign it, you get anti-tampering boot protection as built-in feature. If you add luks encryption with sd-encrypt hook and tpm2 - you get a simple fully protected installation.

In theory at least, I'm planning to move to that point gradually. However as declared earlier, I want to achieve librem14-like functionality with my nitrokey. That is - kernel image tampering will simply be indicated by secure-boot (if UKI EFI image is corrupted), the only other thing we need to make is to inject nitro-key based luks unlock to enable second factor for OS cold-boot.

The unified kernel image though was generated and signed once, somewhere at first session. If the kernel or initrd is updated it will not be reflected in UKI so the old kernel will be booted with new modules on the disk. Let's try to hook pacman update process to refresh and resign the image. We'll make a script to do the image refresh and call that script from ALPM hook:

$ vim /root/resign-uki.sh
#!/bin/bash

ESP=/esp/EFI
CA=/mnt/sec/ruff/UEFI
LUKS=/dev/disk/by-uuid/149f6b60-158d-4ff0-b189-319692c87e42
USER=me@ruff.mobi

OPENED=false
open_vault() {
        cat /root/.key | sudo -u ruff -i gpg -d -u $USER | cryptsetup open $LUKS Vault -d-
        mount /dev/mapper/Vault /mnt/sec
        OPENED=true
}
close_vault() {
        if $OPENED ; then
                umount /mnt/sec
                cryptsetup close Vault
        fi
}

objcopy --add-section .osrel="/usr/lib/os-release" --change-section-vma .osrel=0x20000 \
        --add-section .cmdline="/etc/kernel/cmdline" --change-section-vma \
			.cmdline=0x30000 \
        --add-section .splash="/usr/share/systemd/bootctl/splash-arch.bmp"
			--change-section-vma .splash=0x40000 \
        --add-section .linux="/boot/vmlinuz-linux" --change-section-vma \
			.linux=0x2000000 \
        --add-section .initrd="/boot/initramfs-linux.img" --change-section-vma \
			.initrd=0x3000000 \
        "/usr/lib/systemd/boot/efi/linuxx64.efi.stub" "/boot/arch.efi"

test -d $CA || open_vault
sbsign --cert $CA/SDB.crt --key $CA/SDB.key --output $ESP/Linux/arch.efi /boot/arch.efi
close_vault

Couple of notes here. I want to track whether I have opened the vault or it has laready been opened. If I opened - will close it afterwards. Otherwise will leave as is. Second - I rely on the fact the pgp keys are imported (populated) in my user account to use it. I see little benefit re-populating keys into roor or even separate dedicated keychain for this. Now to call this script from ALPM hook, first make it executable (chmod 700) and then add following hook:

# vim /etc/pacman.d/hooks/99-uni-krnl-img.hook
[Trigger]
Type = Path
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*

Operation = Install
Operation = Upgrade

[Action]
Description = Regenerate and sign EFI UKI
When = PostTransaction
Depends = systemd
Exec = /root/resign-uki.sh

I actually was waiting for kernel update to test it, here let's try it:

$ sudo pacman -Syu
...
(4/4) Regenerate and sign EFI UKI
Das Kernelmodul »device-mapper« kann nicht initialisiert werden. Ist das Kernelmodul
 »dm_mod« geladen?
Gerät »Vault« kann nicht verwendet werden, da es gerade benutzt wird oder der Name
 ungültig ist.
gpg: verschlüsselt mit 512-Bit ECDH Schlüssel, ID 7DAE27C8C4A956A3, erzeugt 2021-10-23
      "Ruslan Marchenko (ruff) <me@ruff.mobi>"
Das Kernelmodul »device-mapper« kann nicht initialisiert werden. Ist das Kernelmodul
 »dm_mod« geladen?
Gerät »Vault« kann nicht verwendet werden, da es gerade benutzt wird oder der Name
 ungültig ist.
mount: /mnt/vault: Spezialgerät /dev/mapper/Vault ist nicht vorhanden.
warning: data remaining[19149312 vs 19159540]: gaps between PE/COFF sections?
warning: data remaining[19149312 vs 19159544]: gaps between PE/COFF sections?
Can't load key from file '/mnt/vault/ruff/UEFI/SDB.key'
140189946178560:error:02001002:system library:fopen:No such file or
 directory:crypto/bio/bss_file.c:69:fopen('/mnt/vault/ruff/UEFI/SDB.key','r')
140189946178560:error:2006D080:BIO routines:BIO_new_file:no such
 file:crypto/bio/bss_file.c:76:

Eh... well. It almost worked. It did ask for the nitrokey pin, but here's the failed part: Das Kernelmodul »device-mapper« kann nicht initialisiert werden. Ist das Kernelmodul »dm_mod« geladen? which means after kernel update dm module was replaced so when we try to mount luks partition the necessary kernel modules are already not there. That left me in crippled situation, my EFI UKI kernel is still old, and modules required to refresh it are new. To recover I need to boot into EFI BIOS, disable secure boot, boot with grub (and new kernel) and refresh/resign EFI UKI. Or downgrade the kernel while I'm still there.

But still, how do we fix that. Apparently we need to open the vault before the kernel upgrade, while we still have original modules. Which is ALPM PreTransaction hook type. Another positive moment of using pretransaction hook is that it may stop the transaction if it fails. Eg. if you forgot about running upgrade and didn't enter the pin - the kernel upgrade will stop, hence not leaving you again in this crippled state. wtih old image and new kernel

For the sake of continuity and to avoid jumping off the secureboot context let's try downgrade+upgrade path. First let's check which version we're currently running with uname -r command and then downgrade straight to this kernel. Or to make it in one go:

$ sudo pacman -U /var/cache/pacman/pkg/linux-`uname -r|sed s/-/./`-`uname -m`.pkg.tar.zst

Note - if you are like me - inadvertently rebooted the laptop (in my case I fell ill and laptop discharged and powered off while I was recovering) you will need to enter recovery mode so make sure you remember your root password. Or you have your grub boot still intact (I've damaged grub by leaving it in the middle of migration to another partition, off the ESP). Otherwise you'll need to use livecd to reset it (again, like me). From recovery mode downgrade the kernel with the above pacman -U ... command and reboot into normal mode. Don't forget to re-enable back security settings if you were forced to disable it for recovery purpose.

Now let's create PreTransaction hook to prevent kernel upgrade if we're unable to sign it. For that we need to create a new hook file of corresponding type and modify a script to handle two step execution. We cannot carry state across executions in variable anymore so need to introduce a persistent flag. Eg. a file created as a flag and state checked as existance + timestamp. After recovering my system with live usb image I think I'd also need to generate unsigned EFI UKI next to signed, to be able to boot from it with secureboot off temporarily. That means, if we are at post-transaction already (kernel upgraded) we need at least to generate new efi uki and drop it on esp, so if signing it fails we at least have an image for manual recovery.

$ sudo vim /etc/pacman.d/hooks/10-uni-krnl-img.hook
[Trigger]
Type = Path
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*

Operation = Install
Operation = Upgrade

[Action]
Description = Open key vault to sign EFI UKI later
Depends = systemd
AbortOnFail = true
When = PreTransaction
Exec = /root/resign-uki.sh open
$ sudo vim /root/resign-uki.sh

Ehm. While looking at how to best organize the code various creepy thoughts started crawling into my mind. Before going too deep into it let's try to analyze security profile of the Vault and automatic signature operation: We have UEFI signing key, encrypted with LUKS, LUKS key encrypted with Nitrokey. Encrypted key is stored on local drive. LUKS drive is using symetric ciphers which are declared to be PQ-resistant. LUKS key is encrypted with PGP using ECDSA asymetric key (stored on Nitrokey). Asymetric ciphers are declared not being PQ-resistant. However SecureBoot relying on UEFI digital signature made by asymetric RSA algorithm. So entire boot vector is not PQ-resistant and hence no point securing the key even more. Nevertheless if I make system drive (where encrypted Vault key is stored) encrypted by LUKS (maybe later, some time) it will make the key part of system secutiy and integrity profile. Not a big win but one less thing to worry about.

So if MA has access to local drive they will have encrypted volume and encrypted key to the encrypted volume. The key to the encrypted key is on a smartcard - if we trust that security model it should provide sufficient protection. However there's this script which accesses the keys in raw form. The MA can easily inject a simple obfuscated code to leak the key during signing process. Which means the script needs to be protected. Since we're dealing with PGP already we can certainly sign the whole script and before opening the vault verify signature and hence integrity of the script. However the MA can remove those checks. I'm still speaking about cold drive access though. If the system drive is encrypted, cold access is protected. Runtime access will rely on linux security model (/root home dir protection). Because in case MA can compromise the signing script so theoretically sbsign utility could be compromised by MA having root access as well.

The signed EFI UKI should ensure boot integrity (eg the kernel, initrd and commandline are not modified to provide passwordless root access to local user) and hence enforcement of the linux security model. EFI BIOS remains an element providing trust root but which is not verified by anything. To ensure EFI BIOS integrity we need to make use of TPM where MSRs are sealing lower layer integrity. We don't want to seal system drive encryption key into TPM though, that should still require second factor - Nitrokey (or other SmartCard). So TPM may seal a key to, say, boot (or any other small) partition, which contains an SC encrypted key to root volume. It will be stronger model than Heads+LibremKey - there a key is used only to detect tampering and could be opted out (forced to boot). Here integrity will be enforced by secureboot+tpm, and a key will be used to unlock the volume. The Nitrokey may still be used as a second independent integrity revalidation though (just an indication). On my second laptop (where I still didn't manage to make secureboot work) I don't have TPM so there Nitrokey HOTP based integrity check will be the only one to rely on.

Ok, now that I a bit more organized my thoughts of the end-state model and potential attack vectors, and since it does not imply any major additional requirements to the signing script, let's get back to the business or the script.

$ sudo vim /root/resign-uki.sh
#!/bin/bash

ESP=/esp/EFI
CA=/mnt/sec/ruff/UEFI
LUKS=/dev/disk/by-uuid/149f6b60-158d-4ff0-b189-319692c87e42
USER=me@ruff.mobi

open_vault() {
        cat /root/.key | sudo -u ruff -i gpg -d -u $USER | cryptsetup open $LUKS Vault -d-
        mount /dev/mapper/Vault /mnt/sec
	touch $CA/.opened
}
close_vault() {
	if [[ -f $CA/.opened && $(( $(date +%s) - $(stat $CA/.opened ) )) -lt 900 ]]
	then
                umount /mnt/sec
                cryptsetup close Vault
        fi
	test -f $CA/.opened && rm -f $CA/.opened
}

if [ "x$1" = "xopen" ]; then
        set -e
        test -d $CA || open_vault
        exit 0
fi

objcopy --add-section .osrel="/usr/lib/os-release" --change-section-vma .osrel=0x20000 \
        --add-section .cmdline="/etc/kernel/cmdline" --change-section-vma \
			.cmdline=0x30000 \
        --add-section .splash="/usr/share/systemd/bootctl/splash-arch.bmp" \
			--change-section-vma .splash=0x40000 \
        --add-section .linux="/boot/vmlinuz-linux" --change-section-vma \
			.linux=0x2000000 \
        --add-section .initrd="/boot/initramfs-linux.img" --change-section-vma \
			.initrd=0x3000000 \
        "/usr/lib/systemd/boot/efi/linuxx64.efi.stub" "$ESP/Linux/arch.uefi"

test -d $CA || open_vault
sbsign --cert $CA/SDB.crt --key $CA/SDB.key --output $ESP/Linux/arch.efi \
	$ESP/Linux/arch.uefi
close_vault

$ sudo pacman -Syu
...
:: Pre-transaction-Hooks werden gestartet …
(1/2) Open key vault to sign EFI UKI later
gpg: verschlüsselt mit 512-Bit ECDH Schlüssel, ID 7DAE27C8C4A956A3, erzeugt 2021-10-23
      "Ruslan Marchenko (ruff) "
(2/2) Removing linux initcpios...
:: Paketänderungen werden verarbeitet …
...
==> Image generation successful
( 8/15) Regenerate and sign EFI UKI
warning: data remaining[19145216 vs 19155444]: gaps between PE/COFF sections?
warning: data remaining[19145216 vs 19155448]: gaps between PE/COFF sections?
Signing Unsigned original image
( 9/15) Warn about old perl modules
...

$ ls -la /esp/EFI/Linux/arch*
-rwxr-xr-x 1 root root 19157744 20. Dez 17:37 /esp/EFI/Linux/arch.efi
-rwxr-xr-x 1 root root 19155444 20. Dez 17:37 /esp/EFI/Linux/arch.uefi
$ sudo cryptsetup status Vault
/dev/mapper/Vault is inactive.

Ok, now it is better! Now you can forget your root password and demolish grub. Actually we need to prove failure to open vault in pre-transaction is properly handled.

Mon Dec 20 18:21:12 2021
 
 
© ruff 2011