Skip to content

Commit c6ec782

Browse files
committed
Sign EFI binaries at flash time for UEFI Secure Boot
The secureboot PR (tiiuae#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 668f895 commit c6ec782

File tree

22 files changed

+507
-40
lines changed

22 files changed

+507
-40
lines changed

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

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,149 @@ 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+
### OTA update image signing
127+
128+
UKIs in OTA update images must also be signed for Secure Boot to accept them.
129+
130+
In **debug builds**, the `uki-signing-key-dir` option signs UKIs at Nix build
131+
time using the dev keys. This is gated by an assertion — it cannot be used in
132+
release builds because it places private keys in the Nix store:
133+
134+
```nix
135+
ghaf.partitioning.verity-volume.uki-signing-key-dir = ./path/to/dev-keys;
136+
```
137+
138+
In **production**, the OTA update server is responsible for signing UKIs before
139+
distributing them to devices. The device never holds private signing keys.
140+
The exact signing integration depends on the update server implementation
141+
and is not yet implemented.
142+
143+
### Production key workflow
144+
145+
For production builds, replace the key material:
146+
147+
1. Generate production PK/KEK/db certificates (e.g., from a Hardware Security
148+
Module).
149+
2. Set `keysSource` to the directory containing the production `.crt` files.
150+
3. At flash time, set `SECURE_BOOT_SIGNING_KEY_DIR` to a directory containing
151+
the production `db.key` and `db.crt` (e.g., from an HSM-backed PKCS#11
152+
token or a secure build machine).
153+
154+
```nix
155+
ghaf.hardware.nvidia.orin.secureboot = {
156+
keysSource = /path/to/production/certs; # PK.crt, KEK.crt, db.crt
157+
signingKeyDir = "/secure/signing/keys"; # db.key, db.crt (runtime path)
158+
};
159+
```
160+
161+
### Verification
162+
163+
After flashing and booting, verify Secure Boot is active:
164+
165+
```sh
166+
# Check UEFI variables
167+
efi-readvar
168+
169+
# Check boot status
170+
bootctl status | head -10
171+
# Should show: Secure Boot: enabled (user)
172+
173+
# Check SetupMode is off (enforcing)
174+
hexdump -C /sys/firmware/efi/efivars/SetupMode-8be4df61-93ca-11d2-aa0d-00e098032b8c
175+
# Last byte should be 00 (not in setup mode)
176+
```
177+
178+
### Generating new dev keys
179+
180+
To regenerate the development keys:
181+
182+
```sh
183+
cd modules/secureboot/dev-keys
184+
for name in PK KEK db; do
185+
openssl req -new -x509 -newkey rsa:2048 -nodes \
186+
-keyout "$name.key" -out "$name.crt" \
187+
-subj "/CN=Ghaf Dev Secure Boot $name/" -days 3650
188+
done
189+
```
190+
191+
After regenerating, you must reflash the device for the new keys to take effect.

modules/partitioning/verity-volume.nix

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,26 @@ in
1414
{
1515
options.ghaf.partitioning.verity-volume = {
1616
enable = lib.mkEnableOption "the verity (image-based) partitioning scheme";
17+
18+
uki-signing-key-dir = lib.mkOption {
19+
type = lib.types.nullOr lib.types.path;
20+
default = null;
21+
description = ''
22+
Directory containing db.key and db.crt for signing the UKI in
23+
the OTA update image. When set, the UKI is signed at build time
24+
so devices with Secure Boot enabled can boot from OTA-installed
25+
slots. In production, the OTA server signs images instead.
26+
'';
27+
};
1728
};
1829

1930
config = lib.mkIf cfg.enable {
31+
assertions = [
32+
{
33+
assertion = cfg.uki-signing-key-dir == null || debugEnable;
34+
message = "uki-signing-key-dir puts private keys in the Nix store and must only be used in debug builds. In production, the OTA server signs images before distribution.";
35+
}
36+
];
2037
system.build.ghafImage =
2138
let
2239
inherit (config.ghaf) version;
@@ -92,6 +109,13 @@ in
92109
--output="kernel.efi"
93110
# ${kernelImage} don't work for some reasons, so move kernel in place
94111
mv kernel.efi ${kernelImage}
112+
${lib.optionalString (cfg.uki-signing-key-dir != null) ''
113+
echo "Signing UKI with Secure Boot key..."
114+
${pkgs.buildPackages.sbsigntool}/bin/sbsign \
115+
--key ${cfg.uki-signing-key-dir}/db.key \
116+
--cert ${cfg.uki-signing-key-dir}/db.crt \
117+
--output ${kernelImage} ${kernelImage}
118+
''}
95119
96120
# FIXME: move compression into mk-manifest.py and compute unpacked sizes there.
97121
rootUnpackedSize=$(stat -c%s ${fsImage})

modules/profiles/orin.nix

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ in
213213
givc.enable = true;
214214
global-config.givc.enable = true;
215215

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

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
@@ -140,8 +140,58 @@ let
140140
# prevents flash.sh from rebuilding esp.img via create_espimage.
141141
export NO_ESP_IMG=0
142142
143-
echo "Decompressing pre-built ESP image..."
144-
"${pkgs.pkgsBuildBuild.zstd}/bin/zstd" -f -d "${verityImages}/esp.img.zst" -o "$WORKDIR/bootloader/esp.img"
143+
echo "Building ESP image..."
144+
_esp="$WORKDIR/bootloader/esp.img"
145+
_uki_name=$(cat "${verityImages}/esp-files/uki-filename")
146+
_uki_src="${verityImages}/esp-files/uki.efi"
147+
_boot_src="${verityImages}/esp-files/systemd-bootaa64.efi"
148+
149+
# Copy to writable tmp so we can sign in-place
150+
_sign_dir=$(mktemp -d)
151+
cp "$_boot_src" "$_sign_dir/BOOTAA64.efi"
152+
cp "$_uki_src" "$_sign_dir/$_uki_name"
153+
154+
# Sign EFI binaries if secure boot keys are available
155+
_sb_key_dir="''${SECURE_BOOT_SIGNING_KEY_DIR:-${
156+
if config.ghaf.hardware.nvidia.orin.secureboot.enable then
157+
config.ghaf.hardware.nvidia.orin.secureboot.signingKeyDir
158+
else
159+
""
160+
}}"
161+
if [ -n "$_sb_key_dir" ] && [ -f "$_sb_key_dir/db.key" ] && [ -f "$_sb_key_dir/db.crt" ]; then
162+
echo "Signing EFI binaries with $_sb_key_dir/db.crt ..."
163+
for _efi in "$_sign_dir"/*.efi; do
164+
echo " Signing: $(basename "$_efi")"
165+
"${pkgs.pkgsBuildBuild.sbsigntool}/bin/sbsign" \
166+
--key "$_sb_key_dir/db.key" --cert "$_sb_key_dir/db.crt" \
167+
--output "$_efi" "$_efi"
168+
done
169+
${
170+
if config.ghaf.hardware.nvidia.orin.secureboot.enable then
171+
''
172+
else
173+
echo "ERROR: Secure Boot is enabled but no signing keys found." >&2
174+
echo " Set SECURE_BOOT_SIGNING_KEY_DIR or place db.key + db.crt in:" >&2
175+
echo " $_sb_key_dir" >&2
176+
exit 1
177+
''
178+
else
179+
''
180+
else
181+
echo "Secure Boot signing skipped (no keys found)."
182+
''
183+
}
184+
fi
185+
186+
# Create 512M FAT32 ESP image
187+
"${pkgs.pkgsBuildBuild.dosfstools}/bin/mkfs.vfat" -F 32 -n ESP -C "$_esp" $((512 * 1024))
188+
"${pkgs.pkgsBuildBuild.mtools}/bin/mmd" -i "$_esp" ::EFI
189+
"${pkgs.pkgsBuildBuild.mtools}/bin/mmd" -i "$_esp" ::EFI/BOOT
190+
"${pkgs.pkgsBuildBuild.mtools}/bin/mmd" -i "$_esp" ::EFI/Linux
191+
"${pkgs.pkgsBuildBuild.mtools}/bin/mcopy" -i "$_esp" "$_sign_dir/BOOTAA64.efi" ::EFI/BOOT/BOOTAA64.efi
192+
"${pkgs.pkgsBuildBuild.mtools}/bin/mcopy" -i "$_esp" "$_sign_dir/$_uki_name" "::EFI/Linux/$_uki_name"
193+
rm -rf "$_sign_dir"
194+
echo "ESP image built: $_esp"
145195
146196
echo "Decompressing pre-built system (LVM) sparse image..."
147197
"${pkgs.pkgsBuildBuild.zstd}/bin/zstd" -f -d "${verityImages}/system.img.zst" -o "$WORKDIR/bootloader/system.img"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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. OTA update images should be pre-signed by
9+
# the update server before distribution.
10+
{
11+
config,
12+
lib,
13+
pkgs,
14+
...
15+
}:
16+
let
17+
cfg = config.ghaf.hardware.nvidia.orin.secureboot;
18+
19+
eslFromCert =
20+
name: cert:
21+
pkgs.runCommand name { nativeBuildInputs = [ pkgs.buildPackages.efitools ]; } ''
22+
${pkgs.buildPackages.efitools}/bin/cert-to-efi-sig-list ${cert} $out
23+
'';
24+
25+
keysDir = cfg.keysSource;
26+
27+
pkEsl = eslFromCert "PK.esl" "${keysDir}/PK.crt";
28+
kekEsl = eslFromCert "KEK.esl" "${keysDir}/KEK.crt";
29+
dbEsl = eslFromCert "db.esl" "${keysDir}/db.crt";
30+
in
31+
{
32+
options.ghaf.hardware.nvidia.orin.secureboot = {
33+
enable = lib.mkEnableOption "UEFI Secure Boot key enrollment for Jetson Orin";
34+
35+
keysSource = lib.mkOption {
36+
type = lib.types.path;
37+
default = ../../../../secureboot/keys;
38+
description = "Directory containing PK.crt, KEK.crt and db.crt used to generate ESLs.";
39+
};
40+
41+
signingKeyDir = lib.mkOption {
42+
type = lib.types.str;
43+
default = toString ../../../../secureboot/dev-keys;
44+
description = ''
45+
Path to directory containing db.key and db.crt for signing EFI
46+
binaries at flash time (on the build host). This is intentionally
47+
a string (not a path) to avoid copying private keys into the Nix
48+
store.
49+
50+
Can be overridden at flash time via the SECURE_BOOT_SIGNING_KEY_DIR
51+
environment variable.
52+
'';
53+
};
54+
};
55+
56+
config = lib.mkIf cfg.enable {
57+
hardware.nvidia-jetpack.firmware.uefi.secureBoot = {
58+
enrollDefaultKeys = true;
59+
defaultPkEslFile = pkEsl;
60+
defaultKekEslFile = kekEsl;
61+
defaultDbEslFile = dbEsl;
62+
};
63+
};
64+
}

0 commit comments

Comments
 (0)