From 1a76ee21ce5119d182464d35aef21c815df7f5bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lars=20Sj=C3=B6strom?= <lars@radicore.se>
Date: Thu, 23 Jan 2025 12:11:57 +0100
Subject: [PATCH] feat: initial secure boot

---
 .woodpecker/ci.yaml       |  13 +++-
 flake.nix                 |   3 +
 keys/DB.auth              | Bin 0 -> 2092 bytes
 keys/KEK.auth             | Bin 0 -> 2091 bytes
 keys/PK.auth              | Bin 0 -> 2089 bytes
 modules/image/builder.nix |   1 +
 scripts/sbkeys            | 154 ++++++++++++++++++++++++++++++++++++++
 scripts/sign-release.sh   |  19 +++++
 8 files changed, 189 insertions(+), 1 deletion(-)
 create mode 100644 keys/DB.auth
 create mode 100644 keys/KEK.auth
 create mode 100644 keys/PK.auth
 create mode 100755 scripts/sbkeys
 create mode 100755 scripts/sign-release.sh

diff --git a/.woodpecker/ci.yaml b/.woodpecker/ci.yaml
index 3099d84..606a477 100644
--- a/.woodpecker/ci.yaml
+++ b/.woodpecker/ci.yaml
@@ -6,6 +6,17 @@ when:
 
 steps:
   check:
-    image: docker.io/nixpkgs/nix-flakes:nixos-24.05
+    image: docker.io/nixpkgs/nix-flakes:nixos-25.05
     commands:
       - nix flake check
+
+  sign:
+    image: docker.io/nixpkgs/nix-flakes:nixos-25.05
+    environment:
+        DB_KEY:
+          from_secret: secure_boot_key
+        DB_CRT:
+          from_secret: secure_boot_crt
+    commands:
+      - ./scripts/sign-release.sh
+
diff --git a/flake.nix b/flake.nix
index 7648b8b..e5f4787 100644
--- a/flake.nix
+++ b/flake.nix
@@ -80,8 +80,11 @@
 
       devShells.${system}.default = pkgs.mkShell {
         buildInputs = with pkgs; [
+          efitools
           erofs-utils
           just
+          openssl
+          sbsigntool
           self.packages.${system}.qemu-uefi-tpm
           squashfs-tools-ng
         ];
diff --git a/keys/DB.auth b/keys/DB.auth
new file mode 100644
index 0000000000000000000000000000000000000000..d8ce304ab5dcd9eccabe35c4093e7498daa916b0
GIT binary patch
literal 2092
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

literal 0
HcmV?d00001

diff --git a/keys/KEK.auth b/keys/KEK.auth
new file mode 100644
index 0000000000000000000000000000000000000000..1e01cd38364e5af65e068ae38f9f71bc8975cc28
GIT binary patch
literal 2091
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

literal 0
HcmV?d00001

diff --git a/keys/PK.auth b/keys/PK.auth
new file mode 100644
index 0000000000000000000000000000000000000000..77ce10ffeb80f9ff109269b36782368b8064f20d
GIT binary patch
literal 2089
zcmaFK&M3yuWXb>or&t&m7??it&AqhV>wd;N&#sjwwdP@|%MF@X)-f?M8uA<PvT-J~
zc`&9jvoJBTG8k~PacZ@Bw0-AgWM^E^#H?)4#4K;n#KgXUnTe5!NhI~;iPure=W7ze
z#U~xse{#6Z;PYmX5l}sh+^h@+@`kbo(rnD3EX+J20f{C4!3x2t$)!c93Qqa?B?<xF
z26E!OMy3XaMn;Av21cevQR2MDAg%$FOW1%WMkQoR8Ce;an;7{S44N3Zn3@<F8P?U^
zF&9uul+4)J6u+T&TfocklM5~DVotN3UGs|RcX4Lp_5&B6M@dI++0hXzb?F4>#j9(#
zABx;^WX_aXrx<OXMyy)0rqb_I<+ZCS-ySZyd?HK1^32|sEFv>?&bIAadEn!B-@d*u
z`8YB8Jdv14d3C3k7Hfph$D9vYUmp3>qW`dDmtTgh-Kre|i!Q&Ew_wgFkBR;lbJa9F
zO<JOCNzt{`mOhaw{h{Yw;+GVCSe4HFV8W3V`|K`!s#?!6>C@|9b)Rl@AMjys77I}n
z;@R+vr|!kitqEJKE0+8}xNGrIcejVjPcX)n>)7Y!KaGDEFhy%!o%@l0DmnWs+Z7dJ
zi_*DU4`$~wF*7nSE)F&bG>~Ou4wdC&5n~b2*l=<4O!<3rcQGvzo+;fladD{sZUcFc
zv@(l?fmj1}1^gfd!i<dnSy&C285#d02Rk=7*cln-y|~6Q^WgWsbtju%yyEloI2Zf1
zP&@f^r7ZIfRi2`W?zRHP@?S3boVI^=MRmr*<3^|V&z-ZRJI`R<buV=T##=gPl264Y
zu3%MqyIB6zgY8et3fJc^TDK&<OyJAM2_Nl$f1Yh;dM|&@l<#FW7tSc1?|8km-;UAv
zZvE~0xwAb|vy%Qsm$q%a^}?}!cg0clioors?`ZPQDJ#BvboZT`ch20{6Yl6@@j2(y
zj2Y#84_xo>v?&$4zbs8o;npfnqqcd`9}Kt7l#!S#{-fQ*pJD2p3f4Efu00B9o4Dq0
z&z!7>?|<bB6c>Hy^I6mTMJ2}Pu0Y2=!NNOVSMhbSu9zQLGdrxNrccqZi80Bbi7}3e
zk<q}MVB*D{hmf-&FFYGEGO{wTG%+$T-P*d;TKrI$+R{&NJ+kh+wPxY}Z!T=prOcxK
zvRU(X1LMu?t%ufy1PD#IbM0Zz$I_1qhhp=(B$VrI@>a8{E{)gJVTrvc#x_qj{G6!;
z|Dx8XQ(p=kTRHdNu}I@hT&gEN8YsJ`nroaCc(F>zI8&u~rDA*Rl<)_u#hUDVR>?Tn
zM{-KCtUuFv`2JJ1{}G$~Zz<guY+A7S{sD&E=|A51%k2M?aXqqOBX9rSeTHGFmz}+q
zZ#e%T{I+F8(`+LJS*@8zQ#84~G^(xD@18#SR!KVYs=y`JxL@HOKkhs2;|g5w;#TQg
zV4#?u(RAF&{KJ<y;ct)J6KJ<7;1j$ptnc2kQMNo_VdR0OPo^yQYTvqAG)DVFkv}t}
lj5B6tU=aKrx-8ks&v|*i68E7;Dea?$)@Y$MT4>=dv;bYNUCjUh

literal 0
HcmV?d00001

diff --git a/modules/image/builder.nix b/modules/image/builder.nix
index f510fe7..65dc08a 100644
--- a/modules/image/builder.nix
+++ b/modules/image/builder.nix
@@ -76,6 +76,7 @@ let
       contents = {
         "/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source = "${pkgs.systemdUkify}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi";
         "/EFI/Linux/${config.system.boot.loader.ukiFile}".source = "${config.system.build.uki}/${config.system.boot.loader.ukiFile}";
+        "/EFI/loader/keys/patos".source = ../../keys;
         "/EFI/memtest86/memtest86.efi".source = "${pkgs.memtest86plus}/memtest.efi";
         "/loader/entries/patos-factory-reset.conf".source = pkgs.writeText "patos-factory-reset.conf" ''
             title Patos Factory Reset
diff --git a/scripts/sbkeys b/scripts/sbkeys
new file mode 100755
index 0000000..a24e215
--- /dev/null
+++ b/scripts/sbkeys
@@ -0,0 +1,154 @@
+#!/usr/bin/env bash
+# Copyright (c) 2015 by Roderick W. Smith
+# Copyright (c) 2020 Corey Hinshaw
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+[ -n "${DEBUG}" ] && set -x
+set -e
+
+usage() {
+  cat <<EOF
+Usage: sbkeys [OPTION]...
+Generate secure boot keys
+
+Options:
+  -h      Print this help text
+  -m      Generate signature database entries for Microsoft certificates
+EOF
+}
+
+generate_keys() {
+  # Do not create new keys if key files already exist
+  KEYS=(
+    PK.key PK.crt PK.cer PK.esl PK.auth
+    KEK.key KEK.crt KEK.cer KEK.esl KEK.auth
+    DB.key DB.crt DB.cer DB.esl DB.auth
+    noPK.esl noPK.auth
+    myGUID.txt
+  )
+  for file in ${KEYS[@]}; do
+    if [ -f ${file} ]; then
+      echo "Skipping key generation: keys already exist in $(pwd)"
+      return
+    fi
+  done
+
+  echo -n "Enter a Common Name to embed in the keys: "
+  read NAME
+
+  # Platform key
+  openssl req -new -x509 \
+      -subj "/CN=${NAME} PK/" -days 3650 -nodes \
+      -newkey rsa:2048 -sha256 \
+      -keyout PK.key -out PK.crt
+  openssl x509 -in PK.crt -out PK.cer -outform DER
+
+  # Key exchange key
+  openssl req -new -x509 \
+      -subj "/CN=${NAME} KEK/" -days 3650 -nodes \
+      -newkey rsa:2048 -sha256 \
+      -keyout KEK.key -out KEK.crt
+  openssl x509 -in KEK.crt -out KEK.cer -outform DER
+
+  # Signature database
+  openssl req -new -x509 \
+      -subj "/CN=${NAME} DB/" -days 3650 -nodes \
+      -newkey rsa:2048 -sha256 \
+      -keyout DB.key -out DB.crt
+  openssl x509 -in DB.crt -out DB.cer -outform DER
+
+  GUID="$(uuidgen -r)"
+  echo ${GUID} > myGUID.txt
+
+  cert-to-efi-sig-list -g ${GUID} PK.crt PK.esl
+  cert-to-efi-sig-list -g ${GUID} KEK.crt KEK.esl
+  cert-to-efi-sig-list -g ${GUID} DB.crt DB.esl
+  rm -f noPK.esl
+  touch noPK.esl
+
+  sign-efi-sig-list \
+      -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
+      -k PK.key -c PK.crt \
+      PK PK.esl PK.auth
+  sign-efi-sig-list \
+      -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
+      -k PK.key -c PK.crt \
+      PK noPK.esl noPK.auth
+  sign-efi-sig-list \
+      -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
+      -k PK.key -c PK.crt \
+      KEK KEK.esl KEK.auth
+  sign-efi-sig-list \
+      -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
+      -k KEK.key -c KEK.crt \
+      DB DB.esl DB.auth
+
+  chmod 0600 *.key
+}
+
+generate_ms_db() {
+  msguid=77fa9abd-0359-4d32-bd60-28f4e78f784b
+
+  msdb="MS_db.esl add_MS_db.auth"
+  for file in $msdb; do
+    if [ -f $file ]; then
+      echo "Microsoft signature lists already exist in $(pwd)"
+      return
+    fi
+  done
+
+  wget --user-agent="Mozilla" https://www.microsoft.com/pkiops/certs/MicWinProPCA2011_2011-10-19.crt
+  wget --user-agent="Mozilla" https://www.microsoft.com/pkiops/certs/MicCorUEFCA2011_2011-06-27.crt
+
+  sbsiglist --owner "$msguid" --type x509 --output MS_Win_db.esl MicWinProPCA2011_2011-10-19.crt
+  sbsiglist --owner "$msguid" --type x509 --output MS_UEFI_db.esl MicCorUEFCA2011_2011-06-27.crt
+  cat MS_Win_db.esl MS_UEFI_db.esl > MS_db.esl
+  sign-efi-sig-list -a -g "$msguid" -k KEK.key -c KEK.crt DB MS_db.esl add_MS_db.auth
+
+  rm MS_Win_db.esl MS_UEFI_db.esl MicWinProPCA2011_2011-10-19.crt MicCorUEFCA2011_2011-06-27.crt
+}
+
+mskeys=0
+
+while getopts ":hm" opt; do
+  case $opt in
+    h)
+      usage
+      cat <<EOF
+
+For use with KeyTool, copy the *.auth and *.esl files to a FAT USB
+flash drive or to your EFI System Partition (ESP).
+For use with most UEFIs' built-in key managers, copy the *.cer files.
+
+To add Microsoft's certificates use KeyTool or UEFI to append
+add_MS_db.auth to the signature database.
+EOF
+      exit 0
+      ;;
+    m)
+      mskeys=1
+      ;;
+    \?)
+      echo "Invalid option: -$OPTARG" >&2
+      usage >&2
+      exit 1
+      ;;
+   esac
+done
+
+generate_keys
+if [ $mskeys -eq 1 ]; then
+  generate_ms_db
+fi
diff --git a/scripts/sign-release.sh b/scripts/sign-release.sh
new file mode 100755
index 0000000..0de9aed
--- /dev/null
+++ b/scripts/sign-release.sh
@@ -0,0 +1,19 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p efitools
+
+set -eux
+
+mkdir signed
+cp -L result/* signed/
+
+loopdev=$(sudo losetup -f)
+sudo losetup -P "$loopdev" signed/*.img
+sudo mount "${loopdev}p1" /mnt -t vfat
+
+sudo find signed/ /mnt/ -name "*.efi" -type f -exec sbsign --key <(echo "$DB_KEY") --cert <(echo "$DB_CRT") --output {} {} \;
+
+sudo mkdir -p /mnt/loader/keys/patos
+sudo cp keys/*.auth /mnt/loader/keys/patos/
+
+sudo umount /mnt
+sudo losetup -d "$loopdev"