Commit Graph

1184 Commits (c8faa5adc222c37b07e45e2a18f03890dad19848)

Author SHA1 Message Date
David Garske c8faa5adc2 Authenticate raw device tree via signed HDR_DEVICE_TREE_DIGEST TLV (Fenrir #7998) 2026-08-13 08:39:22 +02:00
Daniele Lacamera acf07799a5 update_ram: track the uImage entry override explicitly
The ih_ep override was skipped when load_address no longer equalled the
recorded ih_load, as a proxy for "a later stage supplied its own entry
point". elf_load_image_mmu() publishes *pentry before validating the
program headers, so a rejected ELF also rewrites load_address and silently
suppressed the override; conversely a stage landing on ih_load would let it
through. Use a flag set in the ELF and FIT success paths instead, and
publish *pentry only after validation.

Also note in the ih_load == 0 branch that ih_ep is ignored there, which the
TODO removed by F-7985 used to record.
2026-08-12 13:21:08 +02:00
Daniele Lacamera cfaf145649 libwolfboot: propagate the head-block write failure
ext_flash_encrypt_write() captured the unaligned head block's write result
but only returned it when the request fit inside that block; otherwise the
remainder loop overwrote it with its own status. On an external encrypted
partition this function is wb_flash_write(), so a failed head program was
reported as success and defeated the swap abort added in F-7987.

Also round the staging size down to a whole number of encryption blocks.
NVM_CACHE_SIZE defaults to WOLFBOOT_SECTOR_SIZE and is always a multiple
today, but an override would write stale cache bytes and desynchronise the
keystream. Assert the invariant at compile time.
2026-08-12 13:20:56 +02:00
Daniele Lacamera fca6bf0fa6 Address PR review on the encrypted write and sector copy paths
ext_flash_encrypt_write() writes whole ENCRYPT_BLOCK_SIZE blocks:

  - A length that is not a multiple of the block size dropped the trailing
    bytes, since the remainder loop rounds down. Merge them into the block
    that already backs them, as the unaligned head is handled.
  - len == 0 fell through to a read-modify-write of the containing block,
    re-encrypting it in place. Return early instead.

wolfBoot_copy_sector() checked the reads added in F-7987 for a negative
return, but ext_flash_read() and ext_flash_check_read() return the number
of bytes read (docs/HAL.md), so a short read was accepted and a partially
filled buffer copied on. Require the full FLASHBUFFER_SIZE.

Add unit-update-flash-enc coverage for the two write cases.
2026-08-11 18:31:29 +02:00
Daniele Lacamera e24fff0a38 libwolfboot: declare ForceZero() in the test-app build
The encrypted key handling calls ForceZero(), but misc.c is only included
inline under __WOLFBOOT or UNIT_TEST. The test-app build of libwolfboot.c
defines neither, so on MMU targets with EXT_ENCRYPTED the call had no
declaration. GCC 14 rejects that; older compilers only warned.

Add EXT_ENCRYPTED to the guard, which keeps ForceZero() static and adds no
link dependency.
2026-08-11 17:20:24 +02:00
Daniele Lacamera 4726dce42b update_disk: use wc_ForceZero() in the DISK_ENCRYPT helpers
ForceZero() is only visible in libwolfboot.c, which pulls in misc.c
inline; update_disk.c called it without a declaration, so any config with
disk encryption failed to build. Use the exported wc_ForceZero() from
memory.o instead, which is always linked.

Add a zynqmp_sdcard ENCRYPT build job to CI, the only one that compiles
these paths.
2026-08-11 13:59:06 +02:00
Daniele Lacamera f9fe1386ac F-6130: clear disk_encrypt_key/nonce on the FIT DTS load failure path
dd0712ec added the disk_decrypted_header_clear()/disk_crypto_clear() pair
to the wolfBoot_start() panic paths that were missing it, but the FIT
flat-device-tree load failure was one more: when wolfBoot_fit_memcpy()
fails to relocate the DTS, wolfBoot_panic() is entered with
disk_encrypt_key/disk_encrypt_nonce still live in BSS, and that call never
returns on a real target.

Add unit-update-disk-fit, which drives wolfBoot_start() through the FIT
branch with DISK_ENCRYPT enabled and snapshots the module statics from the
WOLFBOOT_HOOK_PANIC hook.
2026-08-11 13:59:06 +02:00
Daniele Lacamera 40021b8c81 F-7069: clear the EH authValue from the stack in wolfBoot_tpm2_get_timestamp
wolfBoot_tpm2_get_timestamp() derives (or copies) the endorsement-hierarchy
authValue into the stack-local eh_handle before issuing TPM2_GetTime. The
wolfTPM2_UnsetAuth() calls on the way out only clear the copies wolfTPM keeps
in the device session slots, and the existing TPM2_ForceZero() only clears the
reel master secret, so the derived per-device authValue was left resident in
the Secure stack frame after the non-secure entry veneer returned.

Wipe eh_handle before returning, matching the scrubbing already done for the
master secret. Add unit-tpm-mfgid-eh-zeroize, which captures the handle passed
to wolfTPM2_SetIdentityAuth() and snapshots the dead frame on both the success
and TPM2_GetTime-error paths.
2026-08-11 12:59:01 +02:00
Daniele Lacamera a799a98c70 F-7382: clip QSPI page program transfers at the device page boundary
spi_flash_write() chunked purely by length, issuing up to a full
FLASH_PAGE_SIZE page program at address + page*FLASH_PAGE_SIZE. NOR
flash page program wraps within the device's own page, so a transfer
starting mid-page (e.g. 0x10F0 with 256 bytes) programmed the tail of
the page and then wrapped the rest back over the start of the same
page, corrupting already-programmed data and leaving the intended
range unwritten.

Drive the loop from the running address and clip each transfer to the
bytes remaining in the current page, matching src/spi_flash.c.
2026-08-11 12:54:36 +02:00
Daniele Lacamera bc743ad3d7 F-7987: abort the swap when a sector copy fails
wolfBoot_copy_sector() discarded the return value of every flash
operation it performed and unconditionally returned the number of bytes
processed. Callers therefore treated a partially written sector as a
completed one and advanced the persistent sector flags, which are the
only record used to resume an interrupted swap. A write error while
copying BOOT into UPDATE (the backup step) could leave both the running
image and its backup corrupted with no way to redo the sector.

Check the result of every erase/read/write in wolfBoot_copy_sector() and
return -1 on the first failure. In the interruptible swap loop, the
delta loop and the DISABLE_BACKUP direct copy, stop on a negative return
without advancing the sector flag or confirming the boot partition, so
the swap is retried from the last completed step on the next boot.
2026-08-11 12:48:05 +02:00
Daniele Lacamera f446a4ad9f F-8007: wipe TPM advanced-IO staging buffers in TPM2_IoCb()
With WOLFTPM_ADV_IO the TIS layer hands the raw command payload to the
HAL callback, so TPM2_IoCb() stages it in stack-local txBuf/rxBuf.  Both
were left intact on the normal return and on the wait-state error return,
keeping a TPM command's plaintext authValue (and the response bytes) in
bootloader stack SRAM.  Wipe them like TPM2_TIS_Read()/TPM2_TIS_Write()
already do for their own staging buffers in the non-advanced-IO path.

Adds unit-tpm-advio-zeroize, which drives TPM2_IoCb() through the write,
read, payload-error and wait-state-timeout paths with a mock SPI slave
and inspects the staging buffers afterwards.
2026-08-11 12:23:09 +02:00
Daniele Lacamera 93edc29082 F-7985: enter legacy uImage at ih_ep when it differs from ih_load
wolfBoot_start() parsed both ih_load and ih_ep from the U-Boot legacy
uImage header, relocated the payload to ih_load, then discarded ih_ep and
passed the load address to do_boot(). An image built with the entry point
ahead of the load address (a preamble before the entry, as U-Boot bootm
handles by copying to ih_load and jumping to ih_ep) was staged correctly
but entered at the wrong address.

Keep ih_load as the relocation destination and remember ih_ep as the
entry point when the two differ, then override load_address just before
do_boot(). The override is skipped when a later stage (ELF/FIT) re-derived
the load address, since that stage supplies its own entry point.

Extend unit-update-ram-uboot with a case where ih_ep = ih_load + 0x40:
it asserts the payload lands at ih_load and do_boot() is entered at
ih_ep. Fails before this change (jumps to ih_load).
2026-08-11 12:16:39 +02:00
Daniele Lacamera 5dfdec31b1 F-7992: bound staged ciphertext in ext_flash_encrypt_write()
ext_flash_encrypt_write() encrypted the whole caller-supplied buffer into
ENCRYPT_CACHE, which is only NVM_CACHE_SIZE bytes, without any check that
the request fits. A request longer than the cache (reachable from the
non-secure world through wolfBoot_nsc_write_update(), which only bounds
len against the partition size) overran the staging buffer and made
ext_flash_write() read past its end.

Stage and flush the ciphertext in NVM_CACHE_SIZE chunks instead. The
encryption stream is not restarted between chunks, so the resulting flash
content is unchanged for requests that already fitted.
2026-08-11 09:29:53 +02:00
Daniele Lacamera e200579d36 F-7969: validate boot-side digest before delta base hash compare
wolfBoot_delta_update() compared the boot partition digest against the
delta base hash using base_hash_sz, the length returned by
wolfBoot_find_header() for the boot header's hash TLV, without ever
checking it. When the tag is absent, find_header() sets base_hash to
NULL and returns 0, so wolfBoot_hardened_CT_compare(NULL, ..., 0)
compared zero bytes and reported a match: the base image digest gate
silently succeeded instead of rejecting the patch. A short or oversized
TLV length would likewise truncate the comparison or read past the
delta base hash in the update header.

The gate is reachable because wolfBoot_update() runs before the boot
partition is verified, so the boot header contents are not guaranteed
to carry a well-formed digest TLV at that point.

Reject the patch when the base image has no usable digest, and compare
a fixed WOLFBOOT_SHA_DIGEST_SIZE. The inverse and resume paths are
unaffected, as they do not use this gate.

Add unit-update-flash-delta coverage for a boot header without a
digest TLV.
2026-08-11 09:05:36 +02:00
Daniele Lacamera 7608e333a8 F-7988: clamp unaligned head size in ext_flash_encrypt_write()
When a write starts in the middle of an encryption block, or is shorter
than a full block, the head copy size was computed as
ENCRYPT_BLOCK_SIZE - row_offset without regard for the requested length.
A write shorter than the remainder of the block (e.g. 1 byte at offset 1)
copied up to ENCRYPT_BLOCK_SIZE-1 bytes out of the caller's buffer into
the read-modify-write block, and left sz negative, so the subsequent
step = sz & ~(ENCRYPT_BLOCK_SIZE - 1) was passed to ext_flash_write() as
a negative length. This is reachable from wb_flash_write_verify_word()
(4-byte writes), from wolfBoot_nsc_write_update() and from the delta
patch writer.

Clamp the head size to the bytes actually requested, and return the
result of the head block write when the request fits within that block.

Add a unit test covering short unaligned writes (offsets 1/0/8/blk-1)
that checks the return value, the data read back and that the bytes past
the requested length were not taken from the caller's buffer; the
ext_flash_write() mock now also rejects negative lengths.
2026-08-11 08:14:12 +02:00
Daniele Lacamera e6655e696c F-7989: clamp unaligned head size in ext_flash_decrypt_read()
When a read starts in the middle of an encryption block, the head copy
size was computed as ENCRYPT_BLOCK_SIZE - row_offset without regard for
the requested length. A read shorter than the remainder of the block
(e.g. 1 byte at offset 1) copied up to 15 decrypted bytes into a buffer
sized for fewer, and left read_remaining negative, so the subsequent
flash_read_size = read_remaining & ~(ENCRYPT_BLOCK_SIZE - 1) was passed
to ext_flash_read() as a negative length.

Clamp the head size to the bytes actually requested.

Add a unit test covering short unaligned reads (offsets 1/4/8/15) that
checks the return value, the decrypted contents and that no byte past
the requested length is written; the ext_flash_read() mock now also
rejects negative lengths.
2026-08-11 08:09:10 +02:00
David Garske 34941079e1 Add aarch64_efi target: wolfBoot as an AArch64 UEFI application 2026-08-10 21:03:50 +02:00
Aidan Garske db0801b9b4 Add wolfCOSE DICE attestation 2026-08-06 09:44:52 +02:00
Daniele Lacamera 31f0168afc ARMORED: harden digest comparison against instruction skips 2026-08-05 20:13:44 +02:00
Daniele Lacamera 35a23bf0ef F-6875: pkcs11: zeroize NSC bounce buffers before freeing them
The PKCS#11 non-secure-callable veneers deep-copy every NS attribute
value and every mechanism parameter into secure-world heap. On key
import (C_CreateObject/C_UnwrapKey/C_CopyObject/C_SetAttributeValue
carrying CKA_VALUE or the RSA private components) and on password-based
derivation (CKM_PKCS5_PBKD2 pPassword) those bounce buffers hold
plaintext secrets, but nsc_tmpl_free() and nsc_mech_free() released them
with a bare XFREE(), leaving the material in the freed secure heap block
until something else happens to overwrite it.

Scrub each block with wc_ForceZero() before releasing it. The template
values use the prepare-time snapshot length, since wolfPKCS11 rewrites
work[].ulValueLen on the C_GetAttributeValue path; nsc_alloc() now
records the length of each mechanism allocation for the same reason.

Adds unit-pkcs11-nsc-zeroize, which drives C_CreateObject_nsc_call and
C_DeriveKey_nsc_call over a secure-heap stand-in that is never cleared,
and fails if the imported key or the PBKDF2 password survives the free.
2026-08-04 08:26:17 +02:00
Daniele Lacamera 0e53cac36c F-7065: tpm: snapshot NS length in wolfBoot_tpm2_read_cert veneer
wolfBoot_tpm2_read_cert() is a cmse_nonsecure_entry veneer. It dereferenced
the caller-supplied 'certSz' to bound-check 'cert' with
cmse_check_address_range(), then passed the same non-secure pointer to
wolfTPM2_NVReadCert(), which re-reads '*len' as the destination capacity
before copying the NV data (lib/wolfTPM/src/tpm2_wrap.c:7221). The length was
therefore fetched twice from non-secure memory with no snapshot in between,
so a racing non-secure agent could present a small capacity to pass the CMSE
check and enlarge it before wolfTPM's own check, making the secure world write
the certificate past the validated range and into adjacent Secure SRAM.

Single-fetch the capacity into a secure local before validating, hand wolfTPM
the local, and copy the result back, matching ns_outlen_begin() in
src/pkcs11_callable.c and the rsp_capacity handling in src/wolfhsm_callable.c
and src/fwtpm_callable.c.

Add unit-tpm-nsc-cert, which drives the veneer through a CMSE stub that models
Secure SRAM immediately after the validated non-secure buffer and a wolfTPM
stub that enlarges the non-secure length word in the race window. The
out-of-bounds write test fails before this fix and passes after it.
2026-08-04 08:07:18 +02:00
Daniele Lacamera 307c2e8548 F-7389: arm_tee: reject Secure .base in zero-length PSA iovecs
arm_tee_psa_call() only ran cmse_check_address_range() on descriptors
whose .len was non-zero, so a non-secure caller could pass an outvec of
{Secure address, 0} and skip attribution checking entirely. Several
dispatch handlers write a fixed-size object through out_vec[0].base
without consulting out_vec[0].len (ARM_TEE_PS_GET_SUPPORT,
ARM_TEE_CRYPTO_OPEN_KEY/IMPORT_KEY/GENERATE_KEY and
GET_KEY_ATTRIBUTES), which turned that into an arbitrary write into
Secure memory from the non-secure world.

Check every non-NULL .base with at least one byte regardless of the
declared length, and require the handlers that write a fixed-size object
to be given a large enough output descriptor.

Adds unit tests covering the zero-length Secure outvec and the
PS_GET_SUPPORT length check; the CMSE stub is now a test-provided
function so it can model a Secure region.
2026-08-04 07:02:53 +02:00
David Garske 5f40400e57 PolarFire SoC: address PR review
- wolfBoot_fit_memcpy: return int so a failed PDMA copy propagates instead
  of being swallowed. The weak default (memcpy) returns 0; the MPFS250 PDMA
  override returns -1 if any chunk's mpfs_pdma_memcpy() fails. Callers now
  fail closed: fit_load_image_inner returns NULL (kernel load then panics
  via update_disk.c), the update_disk DTS copy panics, and hal_dts_fixup
  returns an error on a failed L2->DDR copy-back.
- options.mk: gate -DSTACK_SIZE_PER_HART behind RISC-V arch (RISCV/RISCV64).
  The macro is only consumed by the RISC-V startup asm and the mpfs250-m.ld
  sed token, so it is no longer emitted for PPC/ARM/other targets. The
  unconditional default (?= 0) is kept because the linker sed always needs
  a value to substitute.
2026-08-03 12:27:22 +02:00
David Garske 6b3612f4b9 RISC-V: minimal SBI runtime; PolarFire SoC boots 4-CPU SMP Yocto Linux 2026-08-03 12:27:22 +02:00
David Garske e718eed709 PolarFire SoC: LPDDR4 DDR init for MPFS250T Video Kit (M-Mode) 2026-08-03 12:27:22 +02:00
Mattia Moffa 190de90224 Clean up build flag usage, use trng healthtest in test-app 2026-07-31 14:33:56 +02:00
Mattia Moffa f5f54c294e Implementation of test-app crypto test 2026-07-31 14:33:56 +02:00
David Garske b871ec3f62 update_ram: relocate uImage payload to ih_load under RAMBOOT 2026-07-30 15:36:51 +02:00
David Garske d073ae4ddb watchdog: add generic feed hook and RX driver 2026-07-30 15:32:31 +02:00
David Garske eaa39be9e4 renesas-rx: fix build for GCC RX 8.3/14.2 2026-07-30 15:32:31 +02:00
David Garske 87c34064d9 Add NXP LS1028A ENETC wolfIP support 2026-07-30 13:29:09 +02:00
David Garske 1ceaef3eee zynqmp: wolfBoot ZynqMP FSBL with signed FIT Linux boot and EL3 security (eFuse/PUF/AES-CSU) 2026-07-22 11:56:40 +02:00
aidan garske 1ac8452c36 6589 - Use volatile accumulator in whFlashH5_Verify CT compare 2026-07-14 17:44:26 +02:00
aidan garske 5ceb794690 6593 - Bound uncompressed FIT subimage copy by staging buffer size 2026-07-14 17:44:26 +02:00
aidan garske c88ebcc5bc 6568 - Zeroize DICE UEID digest and hash context before return 2026-07-14 17:44:26 +02:00
aidan garske 717421db4e 6561 - Restore upper 64-bit BAR register on pci_program_bar error paths 2026-07-14 17:44:26 +02:00
David Garske e81761e0d9
Merge pull request #816 from danielinux/hardening-fixes
Extend mitigations against glitch injections
2026-07-02 13:56:42 -07:00
Daniele Lacamera 195610647c armored: address review on hardened compare and fw_base check 2026-07-02 20:33:07 +02:00
Daniele Lacamera 6708438024 armored: use hardened digest compare in elf and delta paths 2026-07-02 20:11:46 +02:00
Daniele Lacamera eca9a20b3b armored: harden fw_base against fault injection 2026-07-02 20:09:16 +02:00
Daniele Lacamera 2c1b3f28da Address PR #815 review comments
- Document wolfBoot_erase_partition() lock postcondition in doxygen
- Comment the hwswap anti-rollback guard as defense-in-depth
- Clarify sim-dualbank-rollback-denied.sh purpose via header comment
- Simplify lock guards in unit-nvm partition_magic_write test
2026-07-02 19:20:34 +02:00
Daniele Lacamera 020d126b63 Fix hwswap fallback CI regressions 2026-07-02 18:57:55 +02:00
Daniele Lacamera 0a6eb658f5 Fix STM32U5 fallback erase and rollback handling 2026-07-02 18:55:23 +02:00
David Garske 277cbbe4f8
Merge pull request #814 from danielinux/fenrir-fixes-2026-07-02
Fenrir fixes 2026 07 02
2026-07-02 09:44:58 -07:00
Mattia Moffa ca2c7f6c9f Check that diagnostics sectors fit at least one record 2026-07-02 17:53:12 +02:00
Mattia Moffa ef03cf007f Fix additional findings
- Check diag_erase() return value in wolfBoot_record_failure()
- Dedicated WOLFBOOT_FAILURE_PHASE_SELF_UPDATE
- Mark the diagnostics flash write buffers XALIGNED_STACK(4) for HALs
  that access the source word by word
- Ensure diagnostics, update, or swap partitions don't overlap the
  bootloader
2026-07-02 17:53:12 +02:00
Mattia Moffa 1da9866630 Diagnostics: record additional events
- Failed boot after emergency update (WOLFBOOT_FAILURE_PHASE_RECOVERY)
- Failed verification of self-update image
2026-07-02 17:53:12 +02:00
Mattia Moffa 67f8ed1194 Diagnostics: support hal_flash_write/erase that return size 2026-07-02 17:53:12 +02:00
Mattia Moffa 55295b705c Diagnostics: move to next sector when last record is broken 2026-07-02 17:53:12 +02:00
Mattia Moffa ceaed58782 Allow configuration of diagnostics record size on flash 2026-07-02 17:53:12 +02:00