diff --git a/docs/Signing.md b/docs/Signing.md index 7c195211..19030e4f 100644 --- a/docs/Signing.md +++ b/docs/Signing.md @@ -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 diff --git a/docs/Targets.md b/docs/Targets.md index c389be23..71dea72c 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -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 diff --git a/hal/aarch64_efi.c b/hal/aarch64_efi.c index 2d591b92..c12d0746 100644 --- a/hal/aarch64_efi.c +++ b/hal/aarch64_efi.c @@ -46,6 +46,10 @@ #include #include +/* 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. */ diff --git a/hal/x86_64_efi.c b/hal/x86_64_efi.c index 49f93c9f..fa3cdf57 100644 --- a/hal/x86_64_efi.c +++ b/hal/x86_64_efi.c @@ -32,6 +32,10 @@ #include #include +/* 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); diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index fb1b0beb..19c45666 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -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 */ diff --git a/include/wolfboot_efi.h b/include/wolfboot_efi.h new file mode 100644 index 00000000..61236856 --- /dev/null +++ b/include/wolfboot_efi.h @@ -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 (, + * ) 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 */ diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 4f722b56..eb10176a 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -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 "": 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");