Skip to main content

Switching on Secure Boot in Debian

Big lock

My main desktop machine is an AMD Ryzen-2400G on an ASUS PRIME B350M-A motherboard. It came with secure boot disabled and as the first action I installed Debian on it. Back in early 2019, Debian 10 (i.e. Buster) was stable and based on an 4.19 kernel. I knew that the integrated Radeon Vega GPU of this SoC required a more recent kernel, so I used Debian testing (to become Bullseye) right from the beginning. It took me a bit of fiddling until two monitors worked correctly, but after that it became my main machine.

I did read about enabling secure boot back then, but it seemed the implementation for GNU/Linux systems had not been defined properly, and so I left it disabled. Now that I understand the process better, I want to use that additional level of security against unwanted software on my main desktop. This blog post will explain not only the basics but also follow through to the nitty-gritty of key handling. This will be in the context of Debian that I run on my machine, but we will also compare this to an Ubuntu laptop to get an idea on how the distributions differ in handling of this topic

Secure Boot Introduction

To understand the implementation, we will take a quick look at the intent of the whole secure boot process to understand the big picture. Equipped with this understanding, we can take a look at how this maps to the tools available in Debian.

The Intent

The idea behind secure boot is to only allow operating system code to execute on your machine originating from a trusted party. Note that secure boot does not extend to user space, i.e. application programs. We believe that operating systems can confine these, so they cannot do bad things. "Bad things" in this context are usually keyboard sniffers, data exfiltrators and other malware. So even with secure boot enabled, we can execute arbitrary programs in GNU/Linux, but we are sure that the operating system is not compromised. If you want to further confine applications, then you will need additional techniques like sand-boxing or SELinux but this is beyond this blog post.

Fundamental for the definition of trust is the transitivity of signature chains. Because we cannot ourselves evaluate every single piece of code for its trustworthiness, we delegate this evaluation to other parties. This trust will then transitively extend to pieces of code trusted by those parties. Every piece of software comes with its own certificate and those trust relationships are expressed by signatures on these certificates. So to understand the secure boot flow, we will have to understand not only the software but also the set of certificates and signatures at a given time. While secure boot is functional, this set really is state of the process, so it can and will change over time and requires its own attention. The origin of this transitive process is called the root of trust and of course plays a special role. The chain of trust can potentially be compromised at any location, but if the root of trust is compromised, the whole secure boot flow breaks down.

Boot Flow

The root of trust for a regular x86 PC is the Unified Extensible Firmware Interface (UEFI) flashed on the motherboard in a flash chip. The UEFI image and the keys in its tables are considered as trusted. UEFI implements a secure boot chain where every chain member is required to be trusted before it is executed. Before passing control to the next component, the integrity of this next component needs to be ensured, before passing transfer to it.

Note that there is also the measured boot mode of execution. In this mode, the chain links will be "measured" against known good values and execution stops if the measurements derive from those values. In practice this is implemented by a TPM and a set of Configuration Registers, but this is not the topic of this post.

So let's look at the boot flow and the key tables involved:

secureboot-flow.svg

In order to understand this flow, we need to consider the key tables involved and look at their contents on this machine. The mokutil binary (from the mokutil package) allows us to look at this data. We will look closer at what a MOK (Machine Owner Key) is later in this document.

PK

The Platform Keys (PK) represent the manufacturer of the platform:

dzu@krikkit:~$ sudo mokutil --pk | grep '\(^\[key\|CN\)'
[key 1]
        Issuer: CN=ASUSTeK MotherBoard PK Certificate
        Subject: CN=ASUSTeK MotherBoard PK Certificate
dzu@krikkit:~$

KEK

Only owners of Key Exchange Keys (KEK) are allowed to modify the key database:

dzu@krikkit:~$ sudo mokutil --kek | grep '\(^\[key\|CN\)'
[key 1]
        Issuer: CN=ASUSTeK MotherBoard KEK Certificate
        Subject: CN=ASUSTeK MotherBoard KEK Certificate
[key 2]
        Issuer: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation Third Party Marketplace Root
        Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation KEK CA 2011
[key 3]
        Issuer: C=GB, ST=Isle of Man, L=Douglas, O=Canonical Ltd., CN=Canonical Ltd. Master Certificate Authority
        Subject: C=GB, ST=Isle of Man, L=Douglas, O=Canonical Ltd., CN=Canonical Ltd. Master Certificate Authority
dzu@krikkit:~$

Again we see a key by the motherboard manufacturer, a Microsoft key and a Canonical key. I am pretty sure that the first two keys were there already when purchasing the machine, but I am not too sure where the Canonical key comes from. It may have been added to the database by booting Ubuntu a long time ago. Those three parties are therefore able to change the keys for secure boot without you noticing.

DB

The Database (DB) of keys (and hashes) contains keys to validate later stages in the boot process:

dzu@krikkit:~$ sudo mokutil --db | grep '\(^[ \t]*\[\|CN\)'
[key 1]
        Issuer: CN=ASUSTeK MotherBoard SW Key Certificate
        Subject: CN=ASUSTeK MotherBoard SW Key Certificate
[key 2]
        Issuer: CN=ASUSTeK Notebook SW Key Certificate
        Subject: CN=ASUSTeK Notebook SW Key Certificate
[key 3]
        Issuer: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation Third Party Marketplace Root
        Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation UEFI CA 2011
[key 4]
        Issuer: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Root Certificate Authority 2010
        Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Windows Production PCA 2011
[key 5]
        Issuer: C=GB, ST=Isle of Man, L=Douglas, O=Canonical Ltd., CN=Canonical Ltd. Master Certificate Authority
        Subject: C=GB, ST=Isle of Man, L=Douglas, O=Canonical Ltd., CN=Canonical Ltd. Master Certificate Authority
[key 6]
  [SHA-256]
dzu@krikkit:~$

The Microsoft Corporation Third Party Marketplace Root certificate is especially noteworthy. It is included in practically all commercial UEFI releases and the GNU/Linux ecosystem decided to use a signature with this key to bootstrap its own key management. As it is impractical for Microsoft to sign every bootloader update (e.g. GRUB), it was decided to introduce a very small, easily audited, piece of software into the regular boot flow. This piece of software is the shim depicted in the above flow chart.

DBX

The Revocation List (DBX) of keys (and hashes) contains entries that are explicitly revoked because of security problems. Usually this list consists of hashes to exclude single problematic versions of programs rather than whole signature keys that have been revoked.

So when a security problem becomes known, securing machines is done by pushing the hash of the affected software into the DBX tables. Of course this should only happen after a new (secured) version of the software has been installed. Otherwise, the boot chain will break, and the system will become unbootable. This seems to be a theoretical problem but the BlackLotus malware has shown that this is also a practical problem. Microsoft acknowledged in 2023 that a proper fix for this malware takes at least a year because they are afraid to render systems unbootable if they did not receive software updates before pushing out the hashes to revoke the problematic software.

Here are some entries to show how they look like:

dzu@krikkit:~$ sudo mokutil --dbx | head -5
[key 1]
  [SHA-256]
  80b4d96931bf0d02fd91a61e19d14f1da452e66db2408ca8604d411f92659f0a
  f52f83a3fa9cfbd6920f722824dbe4034534d25b8507246b3b957dac6e1bce7a
  c5d9d8a186e2c82d09afaa2a6f7f2e73870d3e64f72c4e08ef67796a840f0fbd
dzu@krikkit:~$ 

State of Secure Boot

To check, enable or disable secure boot, again mokutil can be used. Here we see that it is enabled on my machine:

dzu@krikkit:~$ sudo mokutil --sb-state
SecureBoot enabled
dzu@krikkit:~$

Theoretical Summary

Data Structures (state)

This is a summary of the relevant data structures involved in the secure boot flow.

Table 1: Key Overview
Key Verifies Update verified by Note
PK New PK, New KEK, New db/dbx PK Platform key
KEK New db/dbx PK Key Exchange Key
db UEFI Image PK/KEK Authorized Image Database
dbx UEFI Image PK/KEK Forbidden Image Database

Control Flow

Loading an UEFI Image (in our case Shim), follows this procedure:

ob-image-verification.png

MOK

With the keys and certificates we looked at until now, our system would actually not trust a kernel that we have compiled ourselves. Not even kernel modules that we compile locally will be loaded as long as the Linux kernel is configured in lockdown mode and this will be the case by default when secure boot is enabled in UEFI. So kernel modules compiled by dkms will not be loaded on the machine. In practice this usually means that proprietary Nvidia drivers distributed in "source" form, compiled by dmks, will not load without further steps.

Creating a unique MOK (Machine Owner Key), enrolling it in DB and signing kernel modules with it solves this problem. A MOK can have a passphrase to protect its usage. Specifying an empty pass phrase and relying on file permissions to only allow root means that any malware running with root rights will be able to sign malware and execute it on your machine. I will thus use a passphrase, but this means that it will not be possible for automatic updates to install and successfully sign kernel modules. Instead, a manual step is required where I need to provide the passphrase manually.

Creating And Enrolling A MOK

Creating a signing key is pretty standard with the OpenSSL tool set, so I will not describe it in great detail. Here is a transcript to show how it works to create the key below /var/lib/shim-signed/mok. The location is not really important, but it seems that many examples use this place, so I'll simply stick with it.

dzu@krikkit:~$ sudo -i
root@krikkit:~# mkdir -p /var/lib/shim-signed/mok
root@krikkit:~# cd /var/lib/shim-signed/mok
root@krikkit:/var/lib/shim-signed/mok# openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -outform DER -out MOK.der -days 36500 -subj "/CN=Detlev Zundel/"
....+...........+.+..+.+..+...+....+.................+...............+......+............+...+...+.+.........+..+....+.....+.+........+......+...+...+.......+...+.........+......+...+.........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+....+..+....+.....+...+.+.....+....+...+..+..........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
..+..+.............+......+.....+....+..+...+..........+..+...+....+..+.+...+.....+......+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*..+...+......+..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+......+.+...............+........+................+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
root@krikkit:/var/lib/shim-signed/mok# openssl x509 -inform der -in MOK.der -out MOK.pem
root@krikkit:/var/lib/shim-signed/mok# mokutil --import MOK.der
input password:
input password again:
root@krikkit:/var/lib/shim-signed/mok# mokutil --list-new
[key 1]
SHA1 Fingerprint: 01:a0:f6:64:31:b6:b8:6e:78:61:29:27:57:c5:af:1f:c8:1f:e7:d4
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            3c:65:a3:6f:71:a4:cd:8c:6d:04:49:03:cb:11:84:5a:96:b7:5b:0a
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=Detlev Zundel
        Validity
            Not Before: Aug 19 22:15:05 2022 GMT
            Not After : Jul 26 22:15:05 2122 GMT
        Subject: CN=Detlev Zundel
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:a9:19:d6:90:c6:63:b8:4a:b2:4b:2a:bc:66:e1:
                    54:fc:0e:df:3e:19:aa:57:2c:e7:52:aa:64:ad:ae:
                    48:9d:5e:76:40:f9:98:05:2d:fb:2a:22:7c:59:65:
                    15:3a:da:4e:24:90:24:8c:32:fe:c7:47:d3:53:2f:
                    a0:db:5d:b8:02:63:cf:18:e5:c6:c0:82:f1:cf:d4:
                    dc:c1:84:d9:a9:a2:1b:d6:65:2c:cd:58:44:a6:e4:
                    f0:37:86:64:1d:9f:ad:7a:49:29:ec:29:20:54:8d:
                    e7:b8:b2:e9:34:d5:f7:5f:06:8e:be:08:a2:6c:6d:
                    f6:bf:0c:81:dc:97:e9:f6:b8:8d:77:96:c1:5a:16:
                    fb:38:a6:cc:6c:c7:69:67:36:6a:af:5f:27:c5:fc:
                    aa:9b:16:75:8f:ff:d4:55:e6:d7:1d:79:f1:b2:15:
                    2b:53:a5:03:f3:16:45:3d:78:3f:20:89:09:b3:db:
                    34:4d:00:c2:e6:8f:fd:8c:00:5a:20:1c:13:aa:ba:
                    7f:69:ab:18:30:3b:8e:58:17:2d:9a:8d:5d:da:5c:
                    89:61:ef:03:46:78:6e:a6:3f:4a:50:85:a6:3f:a7:
                    a4:c8:55:a3:60:d5:28:d7:92:b7:ca:94:a1:82:9b:
                    d8:c9:9f:7e:11:4c:e4:86:31:d7:78:3f:76:d6:dc:
                    3a:f7
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                EC:8A:FD:29:F8:74:D7:41:C5:ED:3C:C9:41:62:B6:FD:C1:B7:58:55
            X509v3 Authority Key Identifier:
                EC:8A:FD:29:F8:74:D7:41:C5:ED:3C:C9:41:62:B6:FD:C1:B7:58:55
            X509v3 Basic Constraints: critical
                CA:TRUE
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        a1:07:ea:50:a6:16:06:d8:1f:0c:c2:d7:18:a6:b3:4a:7c:e2:
        e3:82:1b:7f:c6:22:c2:75:0f:15:1e:9a:94:2b:3f:3d:0e:8f:
        fd:ab:70:b8:13:fd:c2:44:20:59:28:39:b3:ab:37:31:0b:88:
        f7:82:43:27:07:6e:a5:a5:85:0a:24:d5:63:d0:7e:ee:a0:ec:
        e3:38:af:a1:83:ee:ce:18:ec:0e:18:77:38:f9:2b:07:f7:ed:
        f6:fe:fe:69:fe:ba:09:fd:b6:65:f3:ac:a2:39:e7:32:50:96:
        5f:79:bd:ed:7e:f1:36:c2:75:6e:0b:a9:f3:11:45:8c:10:4b:
        42:4b:e7:5e:4e:f5:90:a4:1d:ed:e4:f1:50:53:b1:65:15:d8:
        6b:a7:6a:46:b7:69:0f:8a:b4:7f:d0:d3:2e:47:88:af:4e:1c:
        e4:a7:99:0c:22:f5:27:7f:a3:57:8d:0b:cb:f2:85:a4:a5:d9:
        49:50:94:20:2e:70:41:e1:8f:1a:ec:da:5a:a2:68:fc:aa:05:
        85:09:89:a0:b3:80:f0:4e:f8:3e:08:a2:c6:4e:16:b4:81:b2:
        0f:24:d5:89:fb:7a:c9:25:97:d2:86:69:12:6b:9b:5c:1b:a4:
        5d:fb:eb:fa:0e:b0:35:b5:51:29:0c:7c:36:70:1b:83:d7:78:
        66:38:2a:b6
root@krikkit:/var/lib/shim-signed/mok#

The actual enrollment is taking place on the next reboot, when UEFI will start the MOKManager UEFI application. This will prompt you for the password you defined in the mokutil --import call. The idea behind this is that only a real person can enroll a key. Even if some malware called mokutil --import, the key is not yet enrolled but only prepared to be enrolled by the MOKManager. Hopefully the user will notice on the next reboot that there is something fishy going on if the machine boots into MOKManager and can then simply decline the new enrollment.

Checking New MOK Key

Having done the actual enrollment on a subsequent boot, we can finally make sure that our key is now present:

dzu@krikkit:~$ sudo mokutil --list-enrolled | grep '\(^\[key\|CN\)'
[key 1]
        Issuer: CN=Debian Secure Boot CA
        Subject: CN=Debian Secure Boot CA
[key 2]
        Issuer: CN=Detlev Zundel
        Subject: CN=Detlev Zundel
dzu@krikkit:~$ 

More On Kernel Modules

The Linux documentation has a section Kernel module signing facility explaining the possible handling of module signatures at run time. Here are the relevant settings for my current Debian kernel:

dzu@krikkit:~$ grep CONFIG_MODULE_SIG /boot/config-`uname -r`
CONFIG_MODULE_SIG_FORMAT=y
CONFIG_MODULE_SIG=y
# CONFIG_MODULE_SIG_FORCE is not set
# CONFIG_MODULE_SIG_SHA1 is not set
# CONFIG_MODULE_SIG_SHA224 is not set
CONFIG_MODULE_SIG_SHA256=y
# CONFIG_MODULE_SIG_SHA384 is not set
# CONFIG_MODULE_SIG_SHA512 is not set
CONFIG_MODULE_SIG_HASH="sha256"
CONFIG_MODULE_SIG_KEY_TYPE_RSA=y
# CONFIG_MODULE_SIG_KEY_TYPE_ECDSA is not set
dzu@krikkit:~$ 

The standard modinfo command will show us signatures on kernel modules. Here is a sample call showing a kernel module signed by my MOK key:

dzu@krikkit:/lib/modules/5.19.0-1-amd64/updates/dkms$ /sbin/modinfo v4l2loopback
filename:       /lib/modules/5.19.0-1-amd64/updates/dkms/v4l2loopback.ko
license:        GPL
author:         Vasily Levin, IOhannes m zmoelnig <zmoelnig@iem.at>,Stefan Diewald,Anton Novikovet al.
description:    V4L2 loopback video device
depends:        videodev
retpoline:      Y
name:           v4l2loopback
vermagic:       5.19.0-1-amd64 SMP preempt mod_unload modversions
sig_id:         PKCS#7
signer:         Detlev Zundel
sig_key:        3C:65:A3:6F:71:A4:CD:8C:6D:04:49:03:CB:11:84:5A:96:B7:5B:0A
sig_hashalgo:   sha512
signature:      22:95:5A:13:C8:60:6F:EB:1A:C9:93:00:9B:1F:B7:E4:FD:7F:9F:86:
                6B:DE:99:98:12:77:39:54:9F:34:C4:3D:8B:6C:1F:D2:F3:66:CA:52:
                95:B4:66:5E:14:89:A7:5F:DC:A9:27:63:FE:19:8A:EB:F1:4D:67:2B:
                2C:21:0D:88:7E:70:A6:51:76:E7:D3:1E:9D:C6:FC:83:77:11:EA:9A:
                8F:B7:62:6E:FE:06:22:8C:44:57:4E:A8:43:20:2D:AE:5D:A6:24:4D:
                2B:B7:08:40:B5:E6:43:13:58:ED:3A:4B:53:96:F4:61:93:0F:10:03:
                DD:8A:C8:DA:AB:C7:D2:D4:62:43:95:1A:DB:9C:2A:ED:B7:00:42:F1:
                22:08:B7:33:4F:F1:1C:78:59:1A:8A:6F:E6:8F:29:06:DD:D4:A7:11:
                58:E0:88:05:E5:43:24:49:39:2F:BC:C1:70:DB:5D:7B:F7:6B:CE:4C:
                80:BD:05:88:2C:6A:13:F3:EC:09:2A:11:EF:5C:56:D6:33:9E:EE:FB:
                04:C5:C1:63:92:86:95:88:28:5E:3F:A5:A5:1E:8D:5A:51:E2:A0:34:
                51:6F:C7:23:71:78:E6:21:35:24:D1:66:70:C5:F3:0E:DD:A9:DD:08:
                21:3E:00:51:C6:8A:9D:E9:AC:9A:52:B5:8D:A5:BE:5B
parm:           debug:debugging level (higher values == more verbose) (int)
parm:           max_buffers:how many buffers should be allocated (int)
parm:           max_openers:how many users can open loopback device (int)
parm:           devices:how many devices should be created (int)
parm:           video_nr:video device numbers (-1=auto, 0=/dev/video0, etc.) (array of int)
parm:           card_label:card labels for every device (array of charp)
parm:           exclusive_caps:whether to announce OUTPUT/CAPTURE capabilities exclusively or not (array of bool)
parm:           max_width:maximum frame width (int)
parm:           max_height:maximum frame height (int)
dzu@krikkit:/lib/modules/5.19.0-1-amd64/updates/dkms$

Signing Modules

The kernel documentation shows us how to use the kernel provided script sign-file (compiled from scripts/sign-file.c in any Linux source tree) to sign modules. Probably for historical reasons, Ubuntu is distributing this binary renamed to kmodsign and so Ubuntu documentation gives this basic template to sign a kernel-module:

KBUILD_SIGN_PIN="xxxx" kmodsign sha512 MOK.priv MOK.der module.ko

In practice, we usually have a few modules to sign, so it makes sense to batch the calls in a simple shell loop:

root@zarniwoop:~# cd /var/lib/shim-signed/mok/
root@zarniwoop:/var/lib/shim-signed/mok# for f in /lib/modules/5.4.0-126-generic/updates/dkms/*.ko ; do \
  KBUILD_SIGN_PIN="xxxx" kmodsign sha512 MOK.priv MOK.der $f ; done
root@zarniwoop:/var/lib/shim-signed/mok#

Of course, we can take this approach somewhat further and package it up in a script that will do all of this and even some more:

#!/bin/bash
#
# sign-kernel-modules: Sign previously unsigned kernel modules for
# secure boot.  Only the dkms modules need to be handled as the
# upstream modules are signed correctly.
#
set -e
shopt -s nullglob

usage() {
    echo "usage: $(basename "$0") [-h] [-r <kver>]" >&2
    echo "        Signs all previously unsigned kernel modules below" >&2
    echo "        /lib/modules/<rev>/updates/dkms" >&2
    echo "        If no revision is specified with -r, then all installed versions" >&2
    echo "        will be processed." 2>&2
    exit 1
}

HELP=no
KVER='*'
while getopts hr: OPTION
do
    case $OPTION in
        r) KVER=$OPTARG
           ;;
        h) HELP=yes
           ;;
        *) echo "Unknown option: $OPTION"
           ;;
    esac
done
shift $(( OPTIND - 1 ))

if [ "yes" = "$HELP" ]; then
    usage
fi

MOKDIR=/var/lib/shim-signed/mok/

KMODSIGN=/usr/bin/kmodsign
if [ ! -x $KMODSIGN ]; then
    KMODSIGN=/usr/src/linux-headers-$(uname -r)/scripts/sign-file
    if [ ! -x "$KMODSIGN" ];    then
        echo "No usable 'kmodsign' binary found - aborting" >&2
        echo "You should try installing a kernel headers package," >&2
        echo "like 'linux-headers-amd64'" >&2
        exit 1
    fi
    echo "Seems to be a Debian system, i.e. using KMODSIGN=$KMODSIGN"
fi
MODINFO=/sbin/modinfo

get_x509_subject() {
    openssl x509 -in $1 -subject -noout | sed 's/subject=CN = //'
}

get_ko_signer() {
    $MODINFO $1 | grep signer: | sed 's/^signer:[ ]\+//'
}

SUBJECT=$(get_x509_subject $MOKDIR/MOK.der)

cd /lib/modules || exit
for rev in $KVER
do
    if [ ! -d "$rev" ]; then
        echo "Revision $rev not found. Exiting." >&2
        exit 1
    fi
    echo Processing "$rev"
    for module in "${rev}"/updates/dkms/*.ko "${rev}"/updates/dkms/*.ko.xz
    do
        # Check if module is signed by correct key already
        SIGNER=$(get_ko_signer $module)
        if [ "$SIGNER" = "$SUBJECT" ]; then
            echo "$module" already signed with correct certificate
            continue
        fi
        if [ -n "$SIGNER" ]; then
            echo Module signed by $SIGNER - will be overwritten
        fi

        if [ -z "$KBUILD_SIGN_PIN" ]
        then
            read -rs -p "Enter MOK passphrase: " KBUILD_SIGN_PIN
            export KBUILD_SIGN_PIN
            echo
        fi
        if [[ "$module" =~ .xz$ ]]
        then
            sudo unxz "$module"
            sudo --preserve-env=KBUILD_SIGN_PIN \
                 $KMODSIGN sha512 $MOKDIR/MOK.priv $MOKDIR/MOK.der "${module%%.xz}"
            sudo xz "${module%%.xz}"
        else
            sudo --preserve-env=KBUILD_SIGN_PIN \
                 $KMODSIGN sha512 $MOKDIR/MOK.priv $MOKDIR/MOK.der "$module"
        fi
        echo Successfully signed module "$module"
    done
done

This script can be run any time and will make sure that newly compiled kernel modules are signed by the key located at /var/lib/shim-signed/mok/MOK.der.

As this script serves me well now for some time, I put it onto Codeberg for your convenience.

Kernel signatures

Now that we know how to sign kernel modules, we also need to understand how the kernel itself is signed to be loaded the GRUB. If not already installed, we will need the sbsigntool package for this to work:

dzu@krikkit:~$ sudo apt install sbsigntool
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  sbsigntool
0 upgraded, 1 newly installed, 0 to remove and 43 not upgraded.
Need to get 68.4 kB of archives.
After this operation, 429 kB of additional disk space will be used.
Get:1 http://deb.debian.org/debian bookworm/main amd64 sbsigntool amd64 0.9.4-3.1 [68.4 kB]
Fetched 68.4 kB in 0s (806 kB/s)
Selecting previously unselected package sbsigntool.
(Reading database ... 603575 files and directories currently installed.)
Preparing to unpack .../sbsigntool_0.9.4-3.1_amd64.deb ...
Unpacking sbsigntool (0.9.4-3.1) ...
Setting up sbsigntool (0.9.4-3.1) ...
Processing triggers for man-db (2.10.2-3) ...
dzu@krikkit:~$

The sbverify tool gives us an easy tool to show the signatures on EFI applications. As the shim, GRUB and the kernel are of this format, we can use it to show the signatures:

dzu@krikkit:~$ sudo sbverify --list /boot/efi/EFI/debian/shimx64.efi
warning: data remaining[809336 vs 934240]: gaps between PE/COFF sections?
signature 1
image signature issuers:
 - /C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Corporation UEFI CA 2011
image signature certificates:
 - subject: /C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Windows UEFI Driver Publisher
   issuer:  /C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Corporation UEFI CA 2011
 - subject: /C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Corporation UEFI CA 2011
   issuer:  /C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Corporation Third Party Marketplace Root
dzu@krikkit:~$ 
dzu@krikkit:~$ sudo sbverify --list /boot/efi/EFI/debian/grubx64.efi
signature 1
image signature issuers:
 - /CN=Debian Secure Boot CA
image signature certificates:
 - subject: /CN=Debian Secure Boot Signer 2022 - grub2
   issuer:  /CN=Debian Secure Boot CA
dzu@krikkit:~$ 
dzu@krikkit:~$ sbverify --list /boot/vmlinuz-6.1.0-1-amd64
signature 1
image signature issuers:
 - /CN=Debian Secure Boot CA
image signature certificates:
 - subject: /CN=Debian Secure Boot Signer 2022 - linux
   issuer:  /CN=Debian Secure Boot CA
dzu@krikkit:~$ 

Ubuntu

In comparison to the examples above, let's look at a Ubuntu installation on an HP laptop:

dzu@zarniwoop:~$ sudo mokutil --pk | grep '\(\[key\|CN=\)'
[sudo] Passwort für dzu: 
[key 1]
        Issuer: C=US, O=HP Inc., CN=HP Inc. PK 2016 CA
        Subject: CN=HP UEFI Secure Boot PK 2017, OU=CODE-SIGN, C=US, O=HP Inc.
dzu@zarniwoop:~$ sudo mokutil --kek | grep '\(\[key\|CN=\)'
[key 1]
        Issuer: C=US, O=HP Inc., CN=HP Inc. KEK 2016 CA
        Subject: CN=HP UEFI Secure Boot KEK 2017, OU=CODE-SIGN, C=US, O=HP Inc.
[key 2]
        Issuer: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation Third Party Marketplace Root
        Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation KEK CA 2011
dzu@zarniwoop:~$ sudo mokutil --db | grep '\(\[key\|CN=\)'
[key 1]
        Issuer: C=US, O=HP Inc., CN=HP Inc. DB Key 2016 CA
        Subject: CN=HP UEFI Secure Boot DB 2017, OU=CODE-SIGN, C=US, O=HP Inc.
[key 2]
        Issuer: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Root Certificate Authority 2010
        Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Windows Production PCA 2011
[key 3]
        Issuer: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation Third Party Marketplace Root
        Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation UEFI CA 2011
dzu@zarniwoop:~$ sudo mokutil --dbx | grep '\(\[key\|CN=\|\[SHA\)'
[key 1]
  [SHA-256]
[key 2]
  [SHA-256]
[key 3]
  [SHA-256]
[key 4]
  [SHA-256]
dzu@zarniwoop:~$ 

It should be obvious now, which keys are the same, and which ones are different.

New Debian Behavior

In the meantime, Debian changed its secure boot implementation to automatically generate a passphrase-less MOK key at /var/lib/dkms/mok.key, enroll that and from then use it automatically in kernel upgrades. As I pointed out previously, this opens the possibility for malware executing with root privileges to install malware in the system, so I did not enroll this key at all. For your information here is a transcript from such an upgrade process:

linux-image-6.0.0-2-amd64 (6.0.3-1) wird eingerichtet ...
I: /vmlinuz.old is now a symlink to boot/vmlinuz-5.19.0-2-amd64
I: /initrd.img.old is now a symlink to boot/initrd.img-5.19.0-2-amd64
I: /vmlinuz is now a symlink to boot/vmlinuz-6.0.0-2-amd64
I: /initrd.img is now a symlink to boot/initrd.img-6.0.0-2-amd64
/etc/kernel/postinst.d/dkms:
dkms: running auto installation service for kernel 6.0.0-2-amd64:Sign command: /usr/lib/linux-kbuild-6.0/scripts/sign
-file
Signing key: /var/lib/dkms/mok.key
Public certificate (MOK): /var/lib/dkms/mok.pub
Certificate or key are missing, generating self signed certificate for MOK...

Building module:
Cleaning build area...
make -j8 KERNELRELEASE=6.0.0-2-amd64 KERNEL_DIR=/lib/modules/6.0.0-2-amd64/build all...
Signing module /var/lib/dkms/v4l2loopback/0.12.7/build/v4l2loopback.ko
Cleaning build area...

v4l2loopback.ko:
Running module version sanity check.
 - Original module
   - No original module exists within this kernel
 - Installation
   - Installing to /lib/modules/6.0.0-2-amd64/updates/dkms/
depmod...
.
/etc/kernel/postinst.d/initramfs-tools:

The script from above handles this situation also just fine as can be seen from this transcript:

dzu@krikkit:~$ sign-kernel-modules
Seems to be a Debian system, i.e. using KMODSIGN=/usr/src/linux-headers-6.0.0-6-amd64/scripts/sign-file
Processing 5.10.0-2-amd64
Processing 5.19.0-1-amd64
5.19.0-1-amd64/updates/dkms/v4l2loopback.ko already signed with correct certificate
Processing 5.19.0-2-amd64
5.19.0-2-amd64/updates/dkms/v4l2loopback.ko already signed with correct certificate
Processing 6.0.0-2-amd64
6.0.0-2-amd64/updates/dkms/v4l2loopback.ko already signed with correct certificate
Processing 6.0.0-3-amd64
6.0.0-3-amd64/updates/dkms/v4l2loopback.ko already signed with correct certificate
Processing 6.0.0-4-amd64
6.0.0-4-amd64/updates/dkms/v4l2loopback.ko already signed with correct certificate
Processing 6.0.0-5-amd64
6.0.0-5-amd64/updates/dkms/v4l2loopback.ko already signed with correct certificate
Processing 6.0.0-6-amd64
6.0.0-6-amd64/updates/dkms/v4l2loopback.ko already signed with correct certificate
Processing 6.1.0-1-amd64
Module signed by DKMS module signing key - will be overwritten
Enter MOK passphrase: 
[sudo] Passwort für dzu: 
Successfully signed module 6.1.0-1-amd64/updates/dkms/v4l2loopback.ko
dzu@krikkit:~$

Revoking Previously Trusted Binaries

In section DBX we saw that binaries can be revoked by adding their signatures to the DBX list. In GNU/Linux, this is maintained by the Linux Vendor Firmware Service (fwupd) project. I have to admit that the command line options and outputs of the fwupdmgr and fwupdtool tools seem very erratic to me, so I settled on this invocation to show the feed for the DBX table:

dzu@krikkit:~$ fwupdmgr get-devices --json | jq '.Devices | map(select(.Name == "UEFI dbx"))'
[
  {
    "Name": "UEFI dbx",
    "DeviceId": "362301da643102b9f38477387e2193e57abaa590",
    "ParentDeviceId": "a45df35ac0e948ee180fe216a5f703f32dda163f",
    "CompositeId": "a45df35ac0e948ee180fe216a5f703f32dda163f",
    "InstanceIds": [
      "UEFI\\CRT_D7F66BE77CEF858C174BF4338A99263C8795B74E02026411F5F532F716AE3263",
      "UEFI\\CRT_D7F66BE77CEF858C174BF4338A99263C8795B74E02026411F5F532F716AE3263&ARCH_X64",
      "UEFI\\CRT_A1117F516A32CEFCBA3F2D1ACE10A87972FD6BBE8FE0D0B996E09E65D802A503",
      "UEFI\\CRT_A1117F516A32CEFCBA3F2D1ACE10A87972FD6BBE8FE0D0B996E09E65D802A503&ARCH_X64",
      "UEFI\\CRT_ED1FE72CB9CA31C9AF5B757AFCD733323D675825032E6CED7FE1AE9EB767998C",
      "UEFI\\CRT_ED1FE72CB9CA31C9AF5B757AFCD733323D675825032E6CED7FE1AE9EB767998C&ARCH_X64"
    ],
    "Guid": [
      "fda6234b-adcb-5105-8515-9af647d29775",
      "f8ff0d50-c757-5dc3-951a-39d86e16f419",
      "c6682ade-b5ec-57c4-b687-676351208742",
      "f8ba2887-9411-5c36-9cee-88995bb39731",
      "7d5759e5-9aa0-5f0c-abd6-7439bb11b9f6",
      "0c7691e1-b6f2-5d71-bc9c-aabee364c916"
    ],
    "Summary": "UEFI revocation database",
    "Plugin": "uefi_dbx",
    "Protocol": "org.uefi.dbx",
    "Flags": [
      "internal",
      "updatable",
      "registered",
      "needs-reboot",
      "usable-during-update",
      "only-version-upgrade",
      "signed-payload"
    ],
    "VendorId": "UEFI:Linux Foundation",
    "Version": "77",
    "VersionLowest": "77",
    "VersionFormat": "number",
    "Icons": [
      "computer"
    ],
    "InstallDuration": 1,
    "Created": 1700174583
  }
]
dzu@krikkit:~$ 

As we can see, the Linux Foundation hosts services to push firmware updates for non-Windows systems to GNU/Linux systems. In Debian you will get this functionality out of the box with the help of the fwupd Systemd service:

dzu@krikkit:~$ systemctl status fwupd.service 
 fwupd.service - Firmware update daemon
     Loaded: loaded (/lib/systemd/system/fwupd.service; static)
     Active: active (running) since Thu 2023-11-16 23:43:06 CET; 48min ago
       Docs: https://fwupd.org/
   Main PID: 1046574 (fwupd)
      Tasks: 5 (limit: 55169)
     Memory: 36.1M
        CPU: 3.066s
     CGroup: /system.slice/fwupd.service
             └─1046574 /usr/libexec/fwupd/fwupd

Nov 16 23:43:02 krikkit systemd[1]: Starting fwupd.service - Firmware update daemon...
Nov 16 23:43:06 krikkit systemd[1]: Started fwupd.service - Firmware update daemon.
dzu@krikkit:~$ 

So once this is running, the daemon will check periodically for updates and install them if possible. In this way the system will automatically receive revocations through the Linux Foundation. Of course this firmware upgrade ecosystem is desirable for many more things than only the UEFI certificates, so I will revisit this topic in another blog post, hopefully soon.

Beware of BIOS upgrades

After upgrading the BIOS on my machine, Linux was working just as well as ever, so it took me a while to find out there was something wrong. But when I tried some nerdy stuff with gstreamer and v4l2-loopback I noticed the v4l2-loopback device was missing, so probably the module did not load correctly. Checking the kernel logs quickly showed the problem:

dzu@krikkit:~$ journalctl -b-6 -t kernel -g v4l2loopback
Okt 14 11:02:34 krikkit kernel: v4l2loopback: loading out-of-tree module taints kernel.
Okt 14 11:02:34 krikkit kernel: v4l2loopback: module verification failed: signature and/or required key missing - tainting kernel
Okt 14 11:02:34 krikkit kernel: v4l2loopback driver version 0.12.7 loaded
dzu@krikkit:~$

It seemed my MOK has been wiped by the BIOS upgrade or the subsequent Load Optimized Defaults that I executed from the setup. With the commands from section Checking New MOK Key, I was able to see that this was really the case. So I repeated the enrollment procedure once more and then v4l2-loopback was loaded again by the kernel.

Summary

It is my hope that this article is able to convey basic information on secure boot for GNU/Linux systems. You should be able to understand how things are supposed to work and hopefully fix problems if this is not the case. I can tell you that it took me a very long time to write this article and "fix" my own shortcomings in understanding the topic 🤓

Update 2023-11-16

Added information on handling DBX in Linux like a firmware and about consequences of a BIOS upgrade / reset.

Update 2024-02-15

The script to sign kernel modules is now hosted on Codeberg and was extended to also handle Debian Trixie. Beginning with Linux 6.6 the kernel modules are now compressed with xz by default so the script needs also to look for .ko.xz files.

Update 2024-02-22

Fix the handling of .ko.xz files. Although modinfo understand the compressed module format, the signing file silently corrupts the file on disk. Be sure to uncompress, sign and compress the file in this new scenario.

Comments

Comments powered by Disqus