If you build your own kernels and need to use Secure Boot, this post may be of interest. Otherwise, carry on with your post-holiday recuperation.

Per the Debian wiki, UEFI Secure Boot is “a verification mechanism for ensuring that code launched by a computer’s UEFI firmware is trusted. It is designed to protect a system against malicious code being loaded and executed early in the boot process, before the operating system has been loaded.” I have habitually disabled Secure Boot because I don’t deal with malicious boot loaders on my personal devices and Secure Boot support on Linux has been uneven at times. That said, a recent firmware upgrade forced me to enable Secure Boot to enable ReBAR and OS-controlled ASPM (thanks MSI!). Here’s what I learned.

In practice, Secure Boot verifies the authenticity of a boot loader by verifying the loader’s signature against a public key that’s enrolled in the firmware. Pretty standard PKI stuff. Microsoft controls the root Certificate Authority (CA) keys, but also signs a few bootloaders that can enroll custom keys, thereby extending the chain of trust to distro maintainers and users. Debian and its derivatives use shim.

This mechanism is invisible to most users because Debian, Ubuntu, et. al release signed boot loaders and kernel images, but folks like me who build kernels from source have some additional work to do. Fortunately shim allows users and sysadmins to define Machine-only Keys (MOKs), which are managed by the user on a per-machine basis. From the Debian wiki:

A key part of the shim design is to allow users to control their own systems. The distro CA key is built in to the shim binary itself, but there is also an extra database of keys that can be managed by the user, the so-called Machine Owner Key (MOK for short).

Keys can be added and removed in the MOK list by the user, entirely separate from the distro CA key. The mokutil utility can be used to help manage the keys here from Linux userland, but changes to the MOK keys may only be confirmed directly from the console at boot time. This removes the risk of userland malware potentially enrolling new keys and therefore bypassing the entire point of SB.

The Debian wiki provides detailed instructions for enrolling a MOK that signs kernel modules, but these instruction do not work for the signing of whole kernels, at least on Ubuntu. Specifically, per Stack Exchange the default MOK public keys generated by Ubuntu are generated with the MODSIGN (1.3.6.1.4.1.2312.16.1.2) X.509 Extended Key Usage, so the signature of a whole kernel will be rejected. We must build our own infrastructure.

With a Self-signed Certificate

Self-signing lacks flexibility, but works well for one machine.

  1. In a secure location, generate a private key
  2. Generate a certificate template for the machine:
    # X.509 Certificate options
    #
    # DN options
    
    # The organization of the subject.
    organization = "Example.com"
    
    # The organizational unit of the subject.
    #unit = "sleeping dept."
    
    # The state of the certificate owner.
    state = "Alabama"
    
    # The country of the subject. Two letter code.
    country = US
    
    # The common name of the certificate owner.
    cn = "hostname"
    
    # The serial number of the certificate
    serial = 001
    
    # In how many days, counting from today, this certificate will expire.
    expiration_days = 1460 # 4 years
    
    # X.509 v3 extensions
    signing_key
    code_signing_key
    
  3. Generate a self-signed certificate
    certtool --generate-self-signed --load-privkey hostname.example.com.key --template <templatename> --outder --outfile hostname.example.com.der
    
  4. Enroll the self-signed certificate in the Machine-only Key store:
    mokutil --import hostname.example.com.der
    
    You will be prompted for a password. This password will be input upon reboot.
  5. Reboot. Input the password you specified, then enroll the key.
  6. Sign the kernel with the machine key + certificate
    export KERNEL_VERSION=6.12.0-061200-generic
    sbsign --key hostname.example.com.key --cert hostname.example.com.cert "/boot/vmlinuz-$KERNEL_VERSION" --output /boot/vmlinuz-$KERNEL_VERSION.tmp
    update-grub
    
  7. Reboot and verify the .tmp kernel image loads cleanly
  8. Remove the temporary kernel image:
    export KERNEL_VERSION=6.12.0-061200-generic
    mv /boot/vmlinuz-$KERNEL_VERSION.tmp /boot/vmlinuz-$KERNEL_VERSION
    update-grub
    

As a CA

More setup, easier maintenance across multiple hosts.

  1. In a secure location, generate a private key
  2. Generate a CA certificate template. I specified a long expiration date here for convenience; revise as needed. I used the following template:
    # X.509 Certificate options
    #
    # DN options
    
    # The organization of the subject.
    organization = "Example.com"
    
    # The state of the certificate owner.
    state = "Alabama"
    
    # The country of the subject. Two letter code.
    country = US
    
    # The common name of the certificate owner.
    cn = "MOK CA"
    
    # The serial number of the certificate. Should be incremented each time a new certificate is generated.
    serial = 001
    
    # In how many days, counting from today, this certificate will expire.
    expiration_days = 3650 # ten years
    
    # Whether this is a CA certificate or not
    ca
    
    # Whether this key will be used to sign other certificates.
    cert_signing_key
    
    # Whether this key will be used to sign CRLs.
    crl_signing_key
    
  3. Generate a CA certificate:
    certtool --generate-self-signed --load-privkey mok-ca.example.com.key --template mok-ca.template --outfile mok-ca.example.com.cert
    
  4. Convert CA to DER format for compatibility with the shim keystore:
    certtool --certificate-info --outder --infile mok-ca.example.com.cert --outfile mok-ca.example.com.der
    
  5. Enroll the CA certificate in the Machine-only Key store:
    mokutil --import mok-ca.example.com.der
    
    You will be prompted for a password. This password will be input upon reboot.
  6. Reboot. Input the password you specified, then enroll the key.
  7. Generate a private key for the machine.
  8. Generate a certificate template for the machine:
    # X.509 Certificate options
    #
    # DN options
    
    # The organization of the subject.
    organization = "Example.com"
    
    # The organizational unit of the subject.
    #unit = "sleeping dept."
    
    # The state of the certificate owner.
    state = "Alabama"
    
    # The country of the subject. Two letter code.
    country = US
    
    # The common name of the certificate owner.
    cn = "hostname"
    
    # The serial number of the certificate
    serial = 001
    
    # In how many days, counting from today, this certificate will expire.
    expiration_days = 1460 # 4 years
    
    # X.509 v3 extensions
    signing_key
    code_signing_key
    
  9. Generate a CSR for the machine:
    certtool --generate-request --load-privkey hostname.example.com.key --outfile hostname.example.com.csr --template hostname.template
    
  10. Sign the CSR with the CA key + certificate:
    certtool --generate-certificate --load-request hostname.example.com.csr \
      --load-ca-certificate mok-ca.example.com.cert --load-ca-privkey mok-ca.example.com.key \
      --template hostname.template --outfile hostname.example.com.cert
    
  11. Sign the kernel with the machine key + certificate
    export KERNEL_VERSION=6.12.0-061200-generic
    sbsign --key hostname.example.com.key --cert hostname.example.com.cert "/boot/vmlinuz-$KERNEL_VERSION" --output /boot/vmlinuz-$KERNEL_VERSION.tmp
    update-grub
    
  12. Reboot and verify the .tmp kernel image loads cleanly
  13. Remove the temporary kernel image:
    export KERNEL_VERSION=6.12.0-061200-generic
    mv /boot/vmlinuz-$KERNEL_VERSION.tmp /boot/vmlinuz-$KERNEL_VERSION
    update-grub
    

References