Skip to content

Commit 4f857d5

Browse files
committed
Sign EFI binaries at flash time for UEFI Secure Boot
The secureboot PR (#1713) enrolls PK/KEK/db keys into the Jetson Orin firmware, but nothing was signing the UKI or systemd-boot. Once keys are enrolled and the UEFI leaves Setup Mode, it rejects unsigned binaries with 'Access denied', bricking the device. Move ESP image construction from a Nix derivation into the flash script so we can sign EFI binaries with sbsign just before writing them to the FAT partition. The private key is read at flash time from SECURE_BOOT_SIGNING_KEY_DIR (or the signingKeyDir option), keeping it out of the Nix store. Add self-signed development keys under modules/secureboot/dev-keys/ for testing. These are explicitly not secret and must not be used in production. Tested on Jetson AGX Orin: device boots with Secure Boot enabled (user mode), unsigned UKI is rejected with 'Access denied'. Signed-off-by: Jörg Thalheim <joerg@thalheim.io>
1 parent 79b8cac commit 4f857d5

File tree

18 files changed

+424
-40
lines changed

18 files changed

+424
-40
lines changed

docs/src/content/docs/ghaf/overview/arch/secureboot.mdx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,132 @@ efi-readvar -v PK
4343
efi-readvar -v KEK
4444
efi-readvar -v db
4545
```
46+
47+
## Jetson Orin Secure Boot
48+
49+
On Jetson Orin targets with verity boot, Secure Boot is handled differently from
50+
the x86 laptop flow. The NVIDIA UEFI firmware supports standard UEFI Secure Boot
51+
key enrollment via a device tree overlay, and EFI binaries (systemd-boot and UKIs)
52+
are signed at flash time rather than build time.
53+
54+
This keeps private signing keys out of the Nix store while still producing a
55+
fully signed boot chain.
56+
57+
### How it works
58+
59+
1. **Key enrollment**: During flash, a DTB overlay (`UefiDefaultSecurityKeys.dtbo`)
60+
embeds PK, KEK, and db certificates into the firmware. On first boot, the UEFI
61+
enrolls these keys and transitions from Setup Mode to User Mode.
62+
63+
2. **EFI signing**: The flash script builds the ESP image at flash time. Before
64+
copying EFI binaries into the FAT partition, it signs each `.efi` file with
65+
`sbsign` using the db private key. This signs both systemd-boot (`BOOTAA64.efi`)
66+
and the UKI.
67+
68+
3. **Enforcement**: Once in User Mode (SetupMode=0, SecureBoot=1), the UEFI
69+
firmware rejects any unsigned or incorrectly signed EFI binary with
70+
"Access denied".
71+
72+
### Key material
73+
74+
The repository contains two sets of keys:
75+
76+
- **`modules/secureboot/keys/`** — Production certificates generated from a
77+
NetHSM. Contains only `.crt` and `.auth` files (public material).
78+
79+
- **`modules/secureboot/dev-keys/`** — Self-signed development keys for testing.
80+
Contains both `.crt` (public) and `.key` (private) files. These keys are
81+
checked into the repository for convenience — they are explicitly **not secret**
82+
and must not be used in production.
83+
84+
### Configuration
85+
86+
Secure Boot is enabled by default for Orin targets via `jetson-orin.nix`:
87+
88+
```nix
89+
ghaf.hardware.nvidia.orin.secureboot.enable = lib.mkDefault true;
90+
```
91+
92+
The Orin profile overrides the enrollment certificates to use dev keys:
93+
94+
```nix
95+
ghaf.hardware.nvidia.orin.secureboot.keysSource = lib.mkDefault ../secureboot/dev-keys;
96+
```
97+
98+
Available options:
99+
100+
| Option | Description |
101+
|--------|-------------|
102+
| `ghaf.hardware.nvidia.orin.secureboot.enable` | Enable/disable UEFI Secure Boot key enrollment |
103+
| `ghaf.hardware.nvidia.orin.secureboot.keysSource` | Directory with `PK.crt`, `KEK.crt`, `db.crt` for enrollment |
104+
| `ghaf.hardware.nvidia.orin.secureboot.signingKeyDir` | Directory with `db.key` and `db.crt` for flash-time signing |
105+
106+
### Flashing with Secure Boot
107+
108+
The flash script reads the signing key from `SECURE_BOOT_SIGNING_KEY_DIR` (or
109+
falls back to the `signingKeyDir` option). Since private keys are not copied into
110+
the Nix store, you must point the environment variable to the working tree:
111+
112+
```sh
113+
# Build the flash script
114+
nix build .#nvidia-jetson-orin-agx-verity-debug-from-x86_64-flash-script
115+
116+
# Flash with dev keys (device must be in recovery mode)
117+
sudo SECURE_BOOT_SIGNING_KEY_DIR=$PWD/modules/secureboot/dev-keys \
118+
./result/bin/flash-ghaf-host
119+
```
120+
121+
The flash script will:
122+
1. Sign `BOOTAA64.efi` and the UKI with the db key
123+
2. Build the ESP FAT image with the signed binaries
124+
3. Flash QSPI firmware (with key enrollment DTB overlay) and eMMC partitions
125+
126+
### Production key workflow
127+
128+
For production builds, replace the key material:
129+
130+
1. Generate production PK/KEK/db certificates (e.g., from a Hardware Security
131+
Module).
132+
2. Set `keysSource` to the directory containing the production `.crt` files.
133+
3. At flash time, set `SECURE_BOOT_SIGNING_KEY_DIR` to a directory containing
134+
the production `db.key` and `db.crt` (e.g., from an HSM-backed PKCS#11
135+
token or a secure build machine).
136+
137+
```nix
138+
ghaf.hardware.nvidia.orin.secureboot = {
139+
keysSource = /path/to/production/certs; # PK.crt, KEK.crt, db.crt
140+
signingKeyDir = "/secure/signing/keys"; # db.key, db.crt (runtime path)
141+
};
142+
```
143+
144+
### Verification
145+
146+
After flashing and booting, verify Secure Boot is active:
147+
148+
```sh
149+
# Check UEFI variables
150+
efi-readvar
151+
152+
# Check boot status
153+
bootctl status | head -10
154+
# Should show: Secure Boot: enabled (user)
155+
156+
# Check SetupMode is off (enforcing)
157+
hexdump -C /sys/firmware/efi/efivars/SetupMode-8be4df61-93ca-11d2-aa0d-00e098032b8c
158+
# Last byte should be 00 (not in setup mode)
159+
```
160+
161+
### Generating new dev keys
162+
163+
To regenerate the development keys:
164+
165+
```sh
166+
cd modules/secureboot/dev-keys
167+
for name in PK KEK db; do
168+
openssl req -new -x509 -newkey rsa:2048 -nodes \
169+
-keyout "$name.key" -out "$name.crt" \
170+
-subj "/CN=Ghaf Dev Secure Boot $name/" -days 3650
171+
done
172+
```
173+
174+
After regenerating, you must reflash the device for the new keys to take effect.

modules/profiles/orin.nix

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ in
216216
givc.enable = true;
217217
global-config.givc.enable = true;
218218

219+
# Use development keys for both enrollment and signing.
220+
# Production builds should override keysSource with the real
221+
# certs and set SECURE_BOOT_SIGNING_KEY_DIR to the HSM-backed
222+
# key directory at flash time.
223+
hardware.nvidia.orin.secureboot.keysSource = lib.mkDefault ../secureboot/dev-keys;
224+
219225
host.networking = {
220226
enable = true;
221227
};

modules/reference/hardware/jetpack/nvidia-jetson-orin/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
imports = [
77
./partition-template.nix
88
./jetson-orin.nix
9+
./secureboot.nix
910
./pci-passthrough-common.nix
1011
./virtualization
1112
./optee/optee.nix

modules/reference/hardware/jetpack/nvidia-jetson-orin/partition-template-verity.nix

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,58 @@ let
126126
# prevents flash.sh from rebuilding esp.img via create_espimage.
127127
export NO_ESP_IMG=0
128128
129-
echo "Decompressing pre-built ESP image..."
130-
"${pkgs.pkgsBuildBuild.zstd}/bin/zstd" -f -d "${verityImages}/esp.img.zst" -o "$WORKDIR/bootloader/esp.img"
129+
echo "Building ESP image..."
130+
_esp="$WORKDIR/bootloader/esp.img"
131+
_uki_name=$(cat "${verityImages}/esp-files/uki-filename")
132+
_uki_src="${verityImages}/esp-files/uki.efi"
133+
_boot_src="${verityImages}/esp-files/systemd-bootaa64.efi"
134+
135+
# Copy to writable tmp so we can sign in-place
136+
_sign_dir=$(mktemp -d)
137+
cp "$_boot_src" "$_sign_dir/BOOTAA64.efi"
138+
cp "$_uki_src" "$_sign_dir/$_uki_name"
139+
140+
# Sign EFI binaries if secure boot keys are available
141+
_sb_key_dir="''${SECURE_BOOT_SIGNING_KEY_DIR:-${
142+
if config.ghaf.hardware.nvidia.orin.secureboot.enable then
143+
config.ghaf.hardware.nvidia.orin.secureboot.signingKeyDir
144+
else
145+
""
146+
}}"
147+
if [ -n "$_sb_key_dir" ] && [ -f "$_sb_key_dir/db.key" ] && [ -f "$_sb_key_dir/db.crt" ]; then
148+
echo "Signing EFI binaries with $_sb_key_dir/db.crt ..."
149+
for _efi in "$_sign_dir"/*.efi; do
150+
echo " Signing: $(basename "$_efi")"
151+
"${pkgs.pkgsBuildBuild.sbsigntool}/bin/sbsign" \
152+
--key "$_sb_key_dir/db.key" --cert "$_sb_key_dir/db.crt" \
153+
--output "$_efi" "$_efi"
154+
done
155+
${
156+
if config.ghaf.hardware.nvidia.orin.secureboot.enable then
157+
''
158+
else
159+
echo "ERROR: Secure Boot is enabled but no signing keys found." >&2
160+
echo " Set SECURE_BOOT_SIGNING_KEY_DIR or place db.key + db.crt in:" >&2
161+
echo " $_sb_key_dir" >&2
162+
exit 1
163+
''
164+
else
165+
''
166+
else
167+
echo "Secure Boot signing skipped (no keys found)."
168+
''
169+
}
170+
fi
171+
172+
# Create 512M FAT32 ESP image
173+
"${pkgs.pkgsBuildBuild.dosfstools}/bin/mkfs.vfat" -F 32 -n ESP -C "$_esp" $((512 * 1024))
174+
"${pkgs.pkgsBuildBuild.mtools}/bin/mmd" -i "$_esp" ::EFI
175+
"${pkgs.pkgsBuildBuild.mtools}/bin/mmd" -i "$_esp" ::EFI/BOOT
176+
"${pkgs.pkgsBuildBuild.mtools}/bin/mmd" -i "$_esp" ::EFI/Linux
177+
"${pkgs.pkgsBuildBuild.mtools}/bin/mcopy" -i "$_esp" "$_sign_dir/BOOTAA64.efi" ::EFI/BOOT/BOOTAA64.efi
178+
"${pkgs.pkgsBuildBuild.mtools}/bin/mcopy" -i "$_esp" "$_sign_dir/$_uki_name" "::EFI/Linux/$_uki_name"
179+
rm -rf "$_sign_dir"
180+
echo "ESP image built: $_esp"
131181
132182
echo "Decompressing pre-built system (LVM) sparse image..."
133183
"${pkgs.pkgsBuildBuild.zstd}/bin/zstd" -f -d "${verityImages}/system.img.zst" -o "$WORKDIR/bootloader/system.img"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# UEFI Secure Boot for Jetson Orin
5+
#
6+
# Enrolls PK/KEK/db keys into the firmware via a DTB overlay.
7+
# EFI binaries are signed at flash time by partition-template-verity.nix
8+
# using the db private key. Private keys never enter the Nix store.
9+
{
10+
config,
11+
lib,
12+
pkgs,
13+
...
14+
}:
15+
let
16+
cfg = config.ghaf.hardware.nvidia.orin.secureboot;
17+
18+
eslFromCert =
19+
name: cert:
20+
pkgs.runCommand name { nativeBuildInputs = [ pkgs.buildPackages.efitools ]; } ''
21+
${pkgs.buildPackages.efitools}/bin/cert-to-efi-sig-list ${cert} $out
22+
'';
23+
24+
keysDir = cfg.keysSource;
25+
26+
pkEsl = eslFromCert "PK.esl" "${keysDir}/PK.crt";
27+
kekEsl = eslFromCert "KEK.esl" "${keysDir}/KEK.crt";
28+
dbEsl = eslFromCert "db.esl" "${keysDir}/db.crt";
29+
in
30+
{
31+
options.ghaf.hardware.nvidia.orin.secureboot = {
32+
enable = lib.mkEnableOption "UEFI Secure Boot key enrollment for Jetson Orin";
33+
34+
keysSource = lib.mkOption {
35+
type = lib.types.path;
36+
default = ../../../../secureboot/keys;
37+
description = "Directory containing PK.crt, KEK.crt and db.crt used to generate ESLs.";
38+
};
39+
40+
signingKeyDir = lib.mkOption {
41+
type = lib.types.str;
42+
default = toString ../../../../secureboot/dev-keys;
43+
description = ''
44+
Path to directory containing db.key and db.crt for signing EFI
45+
binaries at flash time. This is intentionally a string (not a
46+
path) to avoid copying private keys into the Nix store.
47+
48+
Can be overridden at flash time via the SECURE_BOOT_SIGNING_KEY_DIR
49+
environment variable.
50+
'';
51+
};
52+
};
53+
54+
config = lib.mkIf cfg.enable {
55+
hardware.nvidia-jetpack.firmware.uefi.secureBoot = {
56+
enrollDefaultKeys = true;
57+
defaultPkEslFile = pkEsl;
58+
defaultKekEslFile = kekEsl;
59+
defaultDbEslFile = dbEsl;
60+
};
61+
};
62+
}

modules/reference/hardware/jetpack/nvidia-jetson-orin/verity-image.nix

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,43 +32,26 @@ let
3232
# ghaf_<ver>_<hash>.manifest — JSON manifest
3333
inherit (config.system.build) ghafImage;
3434

35-
espImage =
36-
pkgs.runCommand "esp-image"
37-
{
38-
nativeBuildInputs = with pkgs.buildPackages; [
39-
dosfstools
40-
mtools
41-
zstd
42-
];
43-
}
44-
''
45-
mkdir -p $out
46-
47-
# Find the UKI from ghafImage (built without .dtb, roothash patched)
48-
uki=$(find ${ghafImage} -name '*.efi' | head -1)
49-
if [ -z "$uki" ]; then
50-
echo "ERROR: No UKI (.efi) found in ghafImage output"
51-
ls -la ${ghafImage}/
52-
exit 1
53-
fi
54-
echo "Using UKI: $uki"
55-
56-
# Create 512M FAT32 image
57-
truncate -s 512M esp.img
58-
mkfs.vfat -F 32 -n ESP esp.img
59-
60-
# Install systemd-boot as BOOTAA64.efi
61-
boot_efi="${config.systemd.package}/lib/systemd/boot/efi/systemd-bootaa64.efi"
62-
mmd -i esp.img ::EFI
63-
mmd -i esp.img ::EFI/BOOT
64-
mmd -i esp.img ::EFI/Linux
65-
mcopy -i esp.img "$boot_efi" ::EFI/BOOT/BOOTAA64.efi
66-
67-
# Install UKI — auto-discovered by systemd-boot from EFI/Linux/
68-
mcopy -i esp.img "$uki" "::EFI/Linux/$(basename "$uki")"
69-
70-
zstd --compress esp.img -o $out/esp.img.zst
71-
'';
35+
# The ESP FAT image is built at flash time (not here) so that
36+
# EFI binaries can be signed with a private key that never enters
37+
# the Nix store. We only export the individual files needed.
38+
espFiles = pkgs.runCommand "esp-files" { } ''
39+
mkdir -p $out
40+
41+
# UKI (roothash-patched)
42+
uki=$(find ${ghafImage} -name '*.efi' | head -1)
43+
if [ -z "$uki" ]; then
44+
echo "ERROR: No UKI (.efi) found in ghafImage output"
45+
ls -la ${ghafImage}/
46+
exit 1
47+
fi
48+
ln -s "$uki" "$out/uki.efi"
49+
basename "$uki" > "$out/uki-filename"
50+
51+
# systemd-boot
52+
ln -s "${config.systemd.package}/lib/systemd/boot/efi/systemd-bootaa64.efi" \
53+
"$out/systemd-bootaa64.efi"
54+
'';
7255

7356
# LVM image: must be built in a VM because LVM needs block devices.
7457
# Reads the manifest from ghafImage to determine LV names, then writes
@@ -228,7 +211,7 @@ in
228211
config = lib.mkIf cfg.enable {
229212
system.build.verityImages = pkgs.runCommand "verity-images" { } ''
230213
mkdir -p $out
231-
ln -s ${espImage}/esp.img.zst $out/esp.img.zst
214+
ln -s ${espFiles} $out/esp-files
232215
ln -s ${lvmImage}/system.img.zst $out/system.img.zst
233216
ln -s ${lvmImage}/system.raw_size $out/system.raw_size
234217
'';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDJzCCAg+gAwIBAgIUKgZOKjCeExGRuKBKJUB3Xx3f8fgwDQYJKoZIhvcNAQEL
3+
BQAwIzEhMB8GA1UEAwwYR2hhZiBEZXYgU2VjdXJlIEJvb3QgS0VLMB4XDTI2MDIy
4+
NDE3NTUyMloXDTM2MDIyMjE3NTUyMlowIzEhMB8GA1UEAwwYR2hhZiBEZXYgU2Vj
5+
dXJlIEJvb3QgS0VLMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYiA
6+
bnOFBfPs03ajvbPRLP1KIEi6J8K8SGkA5Q6wIrezrYWRQTfGBcLhMOGjlTbfUyrf
7+
FFzdadMH2iPl3cuu1IfHk1O0S/+UPZrQ1xhiNvXYoxTJsq0rKWvcSPm2KjULUtcH
8+
/ZhWlZlZQKi80E8h/mDOr5BnOfLJ8D8BzxTan50V3iEjUpryxGG0iZzl8vElm4Kw
9+
DLfx8OFTr/VgTB/KvuUjZ98qeUJm0GIoTJN51KAXw8GsE9DIRTQo0oxqK1uVEse4
10+
MvV/IMCVqzVzP9ogfGFGEY3OWZ6XkmdM0EYFUjQQiz1+a0AthaxIPr0tqIcw7BzG
11+
0EVK7qMsSOfQl/xmSwIDAQABo1MwUTAdBgNVHQ4EFgQUDjNPiTHM6bFAfxOLr0hm
12+
8FlDFdswHwYDVR0jBBgwFoAUDjNPiTHM6bFAfxOLr0hm8FlDFdswDwYDVR0TAQH/
13+
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAIhmQkI8Cl+kdEoCBOAHuEqP3/0wA
14+
YtDutO4tUw3tpS9RaFwzmTg7Ckf6IVTJTnxDkbqvbHtMIzLnWGEMT0j0JWQETuT0
15+
FLoE1AULwVxxCvE98EIOXzECtB0x5KLj+b5NTriCVfjUB+qRyp+BmcJKhwSiX690
16+
OzGrPDdAnIFacpUgcadqui/Wrh8MRhrQ80905Gpu2H5prPxP9eHbVQmFgT+B7QcE
17+
s6FsOJyby1jRsaKyp+3sgVaIKHAPp8Cj0ExNXRhSj/Wfv3tViY6ZDtCgk9knxHTI
18+
vfOw5iSoQ+FwLHf0XaW3M83unzVSoa+ybJJ5lBsD29RBmiF7zGK6E+U+vA==
19+
-----END CERTIFICATE-----
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors
2+
SPDX-License-Identifier: Apache-2.0

0 commit comments

Comments
 (0)