efi: authenticate the kernel command line via a signed HDR_CMDLINE manifest TLV

pull/850/head
David Garske 2026-07-30 10:59:24 -07:00 committed by Daniele Lacamera
parent 9d50c1a6de
commit 4a16dce4f8
7 changed files with 173 additions and 79 deletions

View File

@ -285,6 +285,14 @@ Provides a value to be set with a custom tag
an application-level public key (e.g. for verifying payloads at runtime, or for key
rotation) in the signed manifest.
* `--cmdline "ascii-string"`: Adds the OS command line as a signature-covered TLV using the
wolfBoot-reserved tag `HDR_CMDLINE` (0x0034). This is a shorthand for
`--custom-tlv-string 0x0034 "ascii-string"` (max 255 bytes). The EFI targets (`aarch64_efi`,
`x86_64_efi`) read this authenticated command line from the verified image and pass it to the
Linux kernel EFI stub via `LoadOptions`, so kernel arguments (e.g. `root=`, `init=`) cannot be
altered without breaking the image signature. Example:
`--cmdline "root=/dev/mmcblk0p2 rw rootwait console=ttyTCU0,115200"`.
The 65524-byte maximum is the largest TLV value the wolfBoot header parser can walk
past when locating the fields that follow it, such as the signature.
@ -293,6 +301,9 @@ Provides a value to be set with a custom tag
must be built with a matching `IMAGE_HEADER_SIZE`, or it will fail to locate the firmware
image at boot.
Note: all options, including `--cmdline` and the `--custom-tlv*` options, must appear **before**
the positional `image key version` arguments.
#### Three-steps signing using external provisioning tools
If the private key is not accessible, while it's possible to sign payloads using

View File

@ -7782,6 +7782,8 @@ Staging kernel at address D630100, size: 6658016
You can `Ctrl-C` or login as `root` and power off qemu with `poweroff`
To pass an authenticated Linux kernel command line, sign the kernel with `--cmdline "..."` (before the positional `image key version` arguments; see [Signing](Signing.md)). wolfBoot reads the `HDR_CMDLINE` TLV from the verified image and applies it to the kernel EFI stub via `LoadOptions`, so it is covered by the image signature.
## NVIDIA Jetson Orin (aarch64_efi)
@ -7844,9 +7846,10 @@ The Jetson UEFI auto-boots removable media via `\EFI\BOOT\BOOTAA64.EFI`. Place w
```
\EFI\BOOT\BOOTAA64.EFI <- wolfboot.efi
\kernel.img <- signed payload (read from the volume root)
\cmdline.txt <- optional Linux kernel command line (see below)
```
The Linux kernel command line is signed into `kernel.img` itself (see "Booting Linux" below), so no separate file is placed on the ESP.
Insert the card and power on; UEFI auto-launches wolfBoot, which verifies and boots the payload. The debug console on the Orin Nano Developer Kit is the J14 button header (not the 40-pin), 115200 8N1. Example output:
```
@ -7866,32 +7869,28 @@ An AArch64 Linux `Image` carries a PE/COFF EFI stub, so wolfBoot boots it with t
Obtain a Tegra234-compatible kernel and root filesystem from the [NVIDIA Jetson Linux (L4T)](https://developer.nvidia.com/embedded/jetson-linux) BSP. The driver package (`Jetson_Linux_R36.x.x_aarch64.tbz2`) contains `Linux_for_Tegra/kernel/Image` and `Linux_for_Tegra/kernel/dtb/tegra234-*.dtb`; the matching `Tegra_Linux_Sample-Root-Filesystem_*.tbz2` provides the rootfs.
Sign the kernel `Image` with the wolfBoot key and name it `kernel.img`:
Sign the kernel `Image` with the wolfBoot key, including the kernel command line as an authenticated TLV (`--cmdline`, before the positional arguments), and name it `kernel.img`:
```
./tools/keytools/sign --ed25519 --sha256 Linux_for_Tegra/kernel/Image \
wolfboot_signing_private_key.der 1
./tools/keytools/sign --ed25519 --sha256 \
--cmdline "root=/dev/mmcblk0p2 rw rootwait console=ttyTCU0,115200" \
Linux_for_Tegra/kernel/Image wolfboot_signing_private_key.der 1
cp Linux_for_Tegra/kernel/Image_v1_signed.bin kernel.img
```
The command line is stored in the signed manifest header (`HDR_CMDLINE` TLV) and is therefore covered by the image signature; wolfBoot reads it from the verified image and passes it to the kernel EFI stub via `LoadOptions`. For a long command line, set `IMAGE_HEADER_SIZE=512` when signing (the default 256-byte ED25519/SHA256 header holds roughly 70 command-line bytes).
Lay out the microSD as a FAT ESP plus a rootfs partition and place:
```
FAT (p1): \EFI\BOOT\BOOTAA64.EFI <- wolfboot.efi (UEFI auto-boots this)
\kernel.img <- signed L4T kernel
\cmdline.txt <- kernel command line (below)
\kernel.img <- signed L4T kernel (command line inside)
ext4 (p2): the L4T sample root filesystem
```
`cmdline.txt` (read by wolfBoot and passed to the kernel via `LoadOptions`):
```
root=/dev/mmcblk0p2 rw rootwait console=ttyTCU0,115200
```
On power-up the Jetson UEFI auto-boots `\EFI\BOOT\BOOTAA64.EFI`; wolfBoot verifies `kernel.img` and hands off to the kernel, which receives the real Tegra234 device tree from the UEFI configuration table (`EFI stub: Using DTB from configuration table`), mounts `mmcblk0p2`, and brings up systemd and the login on the J14 debug console (`ttyTCU0`).
Security note: the plaintext `\cmdline.txt` is convenient for development but is not covered by the wolfBoot image signature (it is, however, measured into the TPM via TCG2, so it remains attestable). For a production trust chain, prefer an authenticated command line. The simplest and most robust option is to bake the command line into the kernel itself: build Linux with `CONFIG_CMDLINE="..."` together with `CONFIG_CMDLINE_FORCE`, which makes the kernel ignore any command line supplied by UEFI/wolfBoot entirely -- this is independent of wolfBoot and cannot be overridden by an attacker who controls the ESP. Alternatively, supply the arguments through the `/chosen` `bootargs` of a signed device tree, or carry them in a signed wolfBoot manifest TLV (a custom TLV added at sign time via `--custom-tlv` and passed by wolfBoot as `LoadOptions`, so it is covered by the image signature -- not currently implemented). An initramfs-based flow (rather than a direct `root=` mount) would additionally need initrd support via the `LINUX_EFI_INITRD_MEDIA_GUID` LoadFile2 protocol, which this target does not currently implement.
Security note: because the command line lives in the signed manifest, kernel arguments (e.g. `root=`, `init=`, security flags) cannot be altered without breaking the image signature -- unlike an unauthenticated file on the ESP. If the image carries no `HDR_CMDLINE` TLV, wolfBoot boots with no command line and the kernel uses its built-in `CONFIG_CMDLINE`. As additional (independent) hardening you may also build Linux with `CONFIG_CMDLINE="..."` plus `CONFIG_CMDLINE_FORCE`, which makes the kernel ignore any externally supplied command line entirely. An initramfs-based flow (rather than a direct `root=` mount) would additionally need initrd support via the `LINUX_EFI_INITRD_MEDIA_GUID` LoadFile2 protocol, which this target does not currently implement.
### Root of trust: enrolling wolfBoot into UEFI Secure Boot

View File

@ -46,6 +46,10 @@
#include <efi/efi.h>
#include <efi/efilib.h>
/* Shared EFI helper: read the authenticated kernel command line (HDR_CMDLINE
* TLV) from the verified manifest. Must follow the gnu-efi headers above. */
#include "wolfboot_efi.h"
#ifdef __WOLFBOOT
void hal_init(void)
{
@ -66,8 +70,9 @@ static EFI_HANDLE gImageHandle;
EFI_PHYSICAL_ADDRESS kernel_addr;
EFI_PHYSICAL_ADDRESS update_addr;
/* Optional Linux kernel command line, read from \cmdline.txt on the ESP and
* handed to the kernel EFI stub via LoadOptions (see aarch64_efi_do_boot). */
/* Linux kernel command line, read from the signed HDR_CMDLINE TLV of the
* verified image and handed to the kernel EFI stub via LoadOptions (see
* aarch64_efi_do_boot). NULL if the image carries no command line. */
static CHAR16 *kernel_cmdline = NULL;
static UINTN kernel_cmdline_bytes = 0;
@ -433,11 +438,13 @@ void RAMFUNCTION aarch64_efi_do_boot(const uint32_t *boot_addr)
panic();
}
/* Pass the kernel command line (from \cmdline.txt) to the loaded image via
* LoadOptions for the Linux EFI stub. For a production trust chain the
* cmdline should be authenticated (in the signed image or DT /chosen), not
* a plaintext file. A direct root= boot needs no initrd; an initramfs flow
* would add it via LINUX_EFI_INITRD_MEDIA_GUID (LoadFile2). */
/* Pass the authenticated kernel command line to the loaded image via
* LoadOptions for the Linux EFI stub. It comes from the HDR_CMDLINE TLV in
* the (already verified) manifest, so it is covered by the image signature.
* NULL if the image carries no command line (kernel uses CONFIG_CMDLINE).
* A direct root= boot needs no initrd; an initramfs flow would add it via
* LINUX_EFI_INITRD_MEDIA_GUID (LoadFile2). */
kernel_cmdline = wolfBoot_efi_get_cmdline(manifest, &kernel_cmdline_bytes);
if (kernel_cmdline != NULL) {
status = uefi_call_wrapper(gSystemTable->BootServices->HandleProtocol, 3,
kernelImageHandle, &lipGuid,
@ -583,64 +590,6 @@ static int open_kernel_image(EFI_FILE_HANDLE vol, CHAR16 *filename,
return 0;
}
/* Read an optional \cmdline.txt (ASCII) from the ESP into a widechar buffer
* for the kernel LoadOptions. No-op (leaves kernel_cmdline NULL) if absent. */
static void read_cmdline(EFI_FILE_HANDLE vol)
{
EFI_FILE_HANDLE file;
EFI_STATUS status;
UINT64 sz;
UINTN readsz, i, n;
uint8_t *ascii = NULL;
CHAR16 *wide = NULL;
file = openFile(L"cmdline.txt", vol);
if (file == NULL)
return; /* optional */
sz = FileSize(file);
if (sz == 0 || sz > 4096) {
uefi_call_wrapper(file->Close, 1, file);
return;
}
status = uefi_call_wrapper(BS->AllocatePool, 3, EfiLoaderData,
(UINTN)sz, (void**)&ascii);
if (status != EFI_SUCCESS || ascii == NULL) {
uefi_call_wrapper(file->Close, 1, file);
return;
}
readsz = (UINTN)sz;
status = uefi_call_wrapper(file->Read, 3, file, &readsz, ascii);
uefi_call_wrapper(file->Close, 1, file); /* done with the file */
if (status != EFI_SUCCESS) {
FreePool(ascii);
return;
}
/* trim trailing CR/LF/whitespace */
n = (UINTN)readsz;
while (n > 0 && (ascii[n-1] == '\n' || ascii[n-1] == '\r' ||
ascii[n-1] == ' ' || ascii[n-1] == '\t'))
n--;
status = uefi_call_wrapper(BS->AllocatePool, 3, EfiLoaderData,
(n + 1) * sizeof(CHAR16), (void**)&wide);
if (status != EFI_SUCCESS || wide == NULL) {
FreePool(ascii);
return;
}
for (i = 0; i < n; i++)
wide[i] = (CHAR16)ascii[i];
wide[n] = 0;
FreePool(ascii);
kernel_cmdline = wide;
kernel_cmdline_bytes = (n + 1) * sizeof(CHAR16);
wolfBoot_printf("Kernel cmdline (%d chars) from cmdline.txt\n", (int)n);
}
EFI_STATUS
EFIAPI
efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
@ -665,7 +614,6 @@ efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
if (status == EFI_SUCCESS)
wolfBoot_printf("Image base: 0x%lx\n", loaded_image->ImageBase);
vol = GetVolume(ImageHandle);
read_cmdline(vol);
/* open_kernel_image() leaves *_addr == 0 on failure (and frees any pages),
* so the "no image" check below is reliable; the sizes are logged inside
* and not needed here. */

View File

@ -32,6 +32,10 @@
#include <efi/efi.h>
#include <efi/efilib.h>
/* Shared EFI helper: read the authenticated kernel command line (HDR_CMDLINE
* TLV) from the verified manifest. Must follow the gnu-efi headers above. */
#include "wolfboot_efi.h"
#ifdef __WOLFBOOT
void hal_init(void)
{
@ -105,9 +109,17 @@ void RAMFUNCTION x86_64_efi_do_boot(uint32_t *boot_addr, uint8_t *dts_address)
MEMMAP_DEVICE_PATH mem_path_device[2];
EFI_HANDLE kernelImageHandle;
EFI_STATUS status;
CHAR16 *kernel_cmdline;
UINTN kernel_cmdline_bytes = 0;
EFI_LOADED_IMAGE *kernel_li = NULL;
EFI_GUID lipGuid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
size = (uint32_t *)(manifest + 4);
/* Authenticated kernel command line from the verified image's HDR_CMDLINE
* TLV (covered by the signature); NULL if the image carries none. */
kernel_cmdline = wolfBoot_efi_get_cmdline(manifest, &kernel_cmdline_bytes);
mem_path_device->Header.Type = EFI_DEVICE_PATH_PROTOCOL_HW_TYPE;
mem_path_device->Header.SubType = EFI_DEVICE_PATH_PROTOCOL_MEM_SUBTYPE;
mem_path_device->MemoryType = EfiLoaderData;
@ -132,6 +144,18 @@ void RAMFUNCTION x86_64_efi_do_boot(uint32_t *boot_addr, uint8_t *dts_address)
panic();
}
/* Hand the authenticated command line to the loaded image via LoadOptions
* for the Linux EFI stub. */
if (kernel_cmdline != NULL) {
status = uefi_call_wrapper(gSystemTable->BootServices->HandleProtocol, 3,
kernelImageHandle, &lipGuid,
(void**)&kernel_li);
if (status == EFI_SUCCESS && kernel_li != NULL) {
kernel_li->LoadOptions = kernel_cmdline;
kernel_li->LoadOptionsSize = (UINT32)kernel_cmdline_bytes;
}
}
status = uefi_call_wrapper(gSystemTable->BootServices->StartImage,
3,
kernelImageHandle, 0, NULL);

View File

@ -235,6 +235,10 @@ extern "C" {
#define HDR_POLICY_SIGNATURE 0x21
#define HDR_SECONDARY_SIGNATURE 0x22
#define HDR_CERT_CHAIN 0x23
/* OS command line (ASCII), a wolfBoot-reserved tag in the custom range
* (0x0030-0xFEFE). Signature-covered, unlike an external cmdline file; consumed
* by the EFI targets and passed to the kernel via LoadOptions. */
#define HDR_CMDLINE 0x0034
#define HDR_PADDING 0xFF
/* Auth Key types */

View File

@ -0,0 +1,76 @@
/* wolfboot_efi.h
*
* Shared helpers for the wolfBoot EFI-application targets (aarch64_efi,
* x86_64_efi).
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot 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.
*
* wolfBoot 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
#ifndef WOLFBOOT_EFI_H
#define WOLFBOOT_EFI_H
/* IMPORTANT: include this AFTER the gnu-efi headers (<efi/efi.h>,
* <efi/efilib.h>) so the EFI types (CHAR16/UINTN/BS/uefi_call_wrapper) are
* visible. wolfBoot declarations (HDR_CMDLINE, IMAGE_HEADER_OFFSET,
* wolfBoot_find_header) come from wolfboot/wolfboot.h, included below. */
#include "wolfboot/wolfboot.h"
/* Return the authenticated OS command line (the HDR_CMDLINE TLV) from a
* VERIFIED wolfBoot manifest header as a NUL-terminated CHAR16 buffer suitable
* for an EFI LoadedImage's LoadOptions. The TLV is covered by the image
* signature, so this must be called only AFTER the image has been verified.
*
* `manifest` points at the wolfBoot header (i.e. boot_addr - IMAGE_HEADER_SIZE).
* Returns NULL and sets *out_bytes = 0 when the image carries no command-line
* TLV (the caller then boots with no command line, so the kernel uses its
* built-in CONFIG_CMDLINE). The returned buffer is allocated with
* BS->AllocatePool (EfiLoaderData) and includes the terminating NUL in
* *out_bytes (a byte count). */
static CHAR16 *wolfBoot_efi_get_cmdline(const uint8_t *manifest, UINTN *out_bytes)
{
uint8_t *val = NULL;
uint16_t len;
UINTN i;
CHAR16 *wide = NULL;
EFI_STATUS status;
*out_bytes = 0;
if (manifest == NULL)
return NULL;
len = wolfBoot_find_header((uint8_t *)(manifest + IMAGE_HEADER_OFFSET),
HDR_CMDLINE, &val);
if (len == 0 || val == NULL)
return NULL; /* no signed command line present */
status = uefi_call_wrapper(BS->AllocatePool, 3, EfiLoaderData,
((UINTN)len + 1) * sizeof(CHAR16), (void **)&wide);
if (status != EFI_SUCCESS || wide == NULL)
return NULL;
/* The TLV value is ASCII and not NUL-terminated; widen exactly len bytes. */
for (i = 0; i < (UINTN)len; i++)
wide[i] = (CHAR16)val[i];
wide[len] = 0;
*out_bytes = ((UINTN)len + 1) * sizeof(CHAR16);
return wide;
}
#endif /* WOLFBOOT_EFI_H */

View File

@ -3572,6 +3572,38 @@ int main(int argc, char** argv)
CMD.custom_tlvs++;
i += 2;
}
else if (strcmp(argv[i], "--cmdline") == 0) {
/* Shorthand for --custom-tlv-string HDR_CMDLINE "<args>": stores the
* OS command line as a signature-covered TLV in the manifest. */
int p = CMD.custom_tlvs;
uint16_t len;
uint32_t j;
if (p >= MAX_CUSTOM_TLVS) {
fprintf(stderr, "Too many custom TLVs.\n");
exit(16);
}
if (argc < (i + 2)) {
fprintf(stderr, "Missing --cmdline argument.\n");
exit(16);
}
len = (uint16_t)strlen(argv[i + 1]);
if (len == 0 || len > 255) {
fprintf(stderr, "cmdline must be 1..255 bytes: %s\n", argv[i + 1]);
exit(16);
}
CMD.custom_tlv[p].tag = HDR_CMDLINE;
CMD.custom_tlv[p].len = len;
CMD.custom_tlv[p].buffer = malloc(len);
if (CMD.custom_tlv[p].buffer == NULL) {
fprintf(stderr, "Error malloc for cmdline tlv buffer %d\n", len);
exit(16);
}
for (j = 0; j < len; j++) {
CMD.custom_tlv[p].buffer[j] = (uint8_t)argv[i + 1][j];
}
CMD.custom_tlvs++;
i += 1;
}
else if (strcmp(argv[i], "--cert-chain") == 0) {
if (argc <= (i + 1)) {
fprintf(stderr, "Missing certificate chain file argument\n");