Commit Graph

3428 Commits (a79bace7729bc3eeffaef9aa606b5a829828dab8)

Author SHA1 Message Date
David Garske a79bace772 Add CONTRIBUTING.md covering the contributor agreement and PR process 2026-08-13 07:48:05 +02:00
Josh Holtrop 45ff674a87 Build coding.o unconditionally with asn.o 2026-08-13 07:46:42 +02:00
David Garske b1c2db191a
Merge pull request #851 from danielinux/fenrir-fixes-2026-08-11
Fenrir fixes 2026-08 -11 + build regressions fixes
2026-08-12 09:28:16 -07:00
Daniele Lacamera e4fd913acc gitignore: add two missing unit-test binaries
unit-flash-write-nrf52 and unit-tpm-mfgid-eh-zeroize were added to the
unit-tests Makefile without the matching ignore entries.
2026-08-12 13:21:08 +02:00
Daniele Lacamera 6201ba4a38 sign: scrub the primary key when the key load fails
main() exit(1)'d on a key load failure. For the hybrid secondary key that
happens with the primary raw buffer live and the primary key object
initialized, so neither zero_and_free(kbuf) nor free_key() ran -- the case
F-8006 set out to fix. Jump to the tail cleanup instead; the exit status is
unchanged.

Also document why free_key() tolerates an uninitialized or already-freed
object, since load_key() has paths that produce both.
2026-08-12 13:21:08 +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 a2706b98d5 mcxw: extend the erase length when rounding the start down
hal_flash_erase() rounded an unaligned address down to the sector boundary
but left len at the caller's value, so a request ending in a later sector
erased only the first one. Grow len by the same amount.

test_erase_zero_runtime_sector_falls_back covered a request that ends
0x10 into the second sector, so its one-erase expectation encoded the
under-erase; it now expects both.
2026-08-12 13:21:08 +02:00
Daniele Lacamera a69b3384d7 nrf52/nrf5340/stm32l0: fix the 32-bit fast path in hal_flash_write
F-6757 fixed the byte-wise path but left the fast path above it indexing
dst[i >> 2]/src[i >> 2] off the call-time base. The guard only proves that
"address + i" and "data + i" are word aligned, so when the destination and
source share a non-zero misalignment the byte path advances i to the next
word boundary and the fast path then copies the wrong word, through an
unaligned 32-bit access that faults on the Cortex-M0+ of stm32l0.

Index both pointers by i directly, and cover the case the existing tests
deliberately avoided.
2026-08-12 13:20:56 +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
David Garske a8cf1c9ba8
Merge pull request #850 from bigbrett/squashelf-windows-fix
tool bugfix: remove windows CRLFs from squashelf output
2026-08-11 15:38:28 -07: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 31a9adc6d2 test: bump footprint limits
The error handling added by the fixes in this branch costs 40 bytes of
common code, so every stm32f407-discovery configuration grew by that
amount. Raise each limit by 40, keeping the previous headroom.
2026-08-11 18:31:22 +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
Brett Nicholas 1251823d04 add O_BINARY flag for msys2/windows builds 2026-08-11 08:35:12 -06:00
Daniele Lacamera db8e768bbc aarch64: link the ARM ChaCha port when ChaCha is selected
Under WOLFSSL_ARMASM, chacha.c calls wc_chacha_crypt_bytes(), which
arch.mk never adds for AArch64 -- it only pulls in the aes/sha ports.
Any AArch64 build using ChaCha failed to link. Add the object in
options.mk, where ChaCha is selected.

Also add a ChaCha variant of the zynqmp_sdcard ENCRYPT build to CI.
2026-08-11 13:59:06 +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 f7551256d0 F-6757: fix partial-word hal_flash_write on nrf52, nrf5340 and stm32l0
The byte-wise branch of hal_flash_write() derived the containing word from
the call-time "address" instead of the current position "address + i":

    int off = (address + i) - (((address + i) >> 2) << 2);
    dst = (uint32_t *)(address - off);
    val = dst[i >> 2];

so "dst[i >> 2]" addressed physical byte "address - off + (i & ~3)". Any
iteration with "i" not a multiple of 4 modified the wrong byte, and with
off != 0 it did so through a misaligned 32-bit flash access (a HardFault on
the Cortex-M0+ of stm32l0). A word-aligned 6-byte write, for instance, put
data[5] at "address + 4" and left "address + 5" erased.

Use the form already applied to hal/samr21.c and hal/same51.c: base the
word on "address + i - off", read it with a single aligned access, and fill
it byte by byte up to the next word boundary.

Add unit-flash-write-nrf52, covering the aligned-with-tail, mismatched
alignment and single-word cases against hal/nrf52.c.
2026-08-11 13:04:10 +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 ab7b79cd16 F-7383: use one consistent sector size in mcxw hal_flash_erase
hal_flash_erase() in hal/mcxw.c rounded the start address down with the
runtime pflash_sector_size (queried from FLASH_GetProperty() in hal_init())
but stepped address and len by the compile-time WOLFBOOT_SECTOR_SIZE. When
the two differ, a larger WOLFBOOT_SECTOR_SIZE steps over hardware sectors
inside the requested range and leaves them unerased, while a smaller one
issues erase commands at non-sector-aligned addresses. A zero size reported
by the driver would also divide by zero.

Take a local sector_size, fall back to WOLFBOOT_SECTOR_SIZE when the driver
reports zero and use it for the alignment and both loop steps, as
hal/mcxn.c already does.

Add unit-flash-erase-mcxw, using the existing WOLFBOOT_UNIT_TEST_FLASH_ERASE
guard convention to compile hal_flash_erase() in isolation without the NXP
MCUXpresso SDK headers.
2026-08-11 12:52:20 +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 f9957da523 F-8003: separate decoded key objects for hybrid signers
The sign tool kept a single file-static struct for the decoded private
key, so a hybrid run that picks two algorithms sharing one member (e.g.
ECC521 primary + ECC256 secondary, or RSA2048 + RSAPSS2048) had the
secondary load_key() re-init and overwrite the still-live primary key
before either signature was produced. The primary signature was then
made with the secondary key, and the final cleanup in main() dispatched
only on CMD.sign, so the secondary key never reached its algorithm
specific zeroizing free.

Give the primary and the secondary signer their own storage, select it
with key_obj(secondary) in load_key()/load_key_ecc()/load_key_rsa()/
sign_digest()/set_signature_sizes(), and free both keys at exit through
the new free_key() helper.
2026-08-11 09:25:54 +02:00
Daniele Lacamera 748fa8a19a F-8006: return errors from delta base-hash validation in sign tool
make_header_ex() validated the delta base image digest with direct exit(1)
calls. Those are reachable in normal use: base_diff() looks up the base
digest for the selected hash algorithm, and when the base image was signed
with a different algorithm the lookup yields NULL, yet make_header_delta()
is still called. Aborting there skips base_diff()'s cleanup (the temporary
patch file is left in /tmp) and, more importantly, main()'s
zero_and_free(kbuf, key_buffer_sz) and algorithm-specific key free, so the
raw and decoded private signing key are never scrubbed.

Use the function's existing 'failure:' path instead, which returns -1 and
propagates through base_diff() to main()'s unified cleanup.

Reaching 'failure:' from there uncovered a latent double fclose(): the
image-size probe closes 'f' without clearing it, so the cleanup block
closed the same stream again. Clear the pointer after the fclose().

Add unit-sign-delta-basehash-cleanup.py, which signs a SHA256 base image,
requests a SHA384 delta against it, and asserts the run fails with the
temporary patch file removed.
2026-08-11 09:13:31 +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 f505f88bd3
Merge pull request #847 from bigbrett/wolfhsm-expose-max-verify-roots
wolfHSM: expose max verify roots in options.mk
2026-08-10 12:57:19 -07:00
David Garske 485b446e9e
Merge pull request #848 from bigbrett/tc3-fix-uart-gates
tc3: fix debug uart macro protection
2026-08-10 12:31:31 -07:00
David Garske 2b3b7034a5 aarch64_efi: address PR818 review (measure cmdline, DTB GUID, size/page/desc fixes) 2026-08-10 21:03:50 +02:00
David Garske 18b525f53c efi: sign the x86_64_efi reference kernel with an authenticated HDR_CMDLINE 2026-08-10 21:03:50 +02:00
David Garske 4a16dce4f8 efi: authenticate the kernel command line via a signed HDR_CMDLINE manifest TLV 2026-08-10 21:03:50 +02:00
David Garske 9d50c1a6de aarch64_efi: fix open_kernel_image error return; expand kernel cmdline security docs 2026-08-10 21:03:50 +02:00
David Garske 811c9d049a aarch64_efi: measure kernel into firmware TPM via EFI_TCG2 2026-08-10 21:03:50 +02:00
David Garske 34941079e1 Add aarch64_efi target: wolfBoot as an AArch64 UEFI application 2026-08-10 21:03:50 +02:00
Brett Nicholas 5eaf0b0a55 tc3: fix debug uart macro protection 2026-08-10 10:38:49 -06:00
Brett Nicholas f9419ad495 add documentation for exposed wolfHSM config values, plus some small doc fixes 2026-08-07 11:20:03 -06:00
Brett Nicholas 64e6a3aeb0 wolfHSM: expose max verify roots in options.mk 2026-08-06 11:27:33 -06:00
Marco Oliverio 0dfad2dbc1 hal: pic32cz: uart_deinit out from clock_restore block, nop asm volatile 2026-08-06 15:31:25 +02:00
Marco Oliverio 955b07cc0b mc: pic32cz: better state reset before boot 2026-08-06 15:31:25 +02:00
Marco Oliverio c46c3278a4 arch.mk: add default HSM microchip folder (../) 2026-08-06 15:31:25 +02:00
Aidan Garske db0801b9b4 Add wolfCOSE DICE attestation 2026-08-06 09:44:52 +02:00
David Garske 9232a95033
Merge pull request #844 from danielinux/armored-hardening
[ARMORED] Improve hardening against multi-glitch corner cases
2026-08-05 12:16:14 -07:00
Daniele Lacamera 31f0168afc ARMORED: harden digest comparison against instruction skips 2026-08-05 20:13:44 +02:00
Mattia Moffa 817289ed2c
Merge pull request #842 from danielinux/fenrir-fixes-2026-08-04
Fenrir fixes 2026 08 04
2026-08-04 17:29:40 +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 dfdcf7eeb5 F-7053: sign: propagate make_header() failure to the exit status
main() called make_header()/make_hybrid_header() and discarded their
return value. Both are wrappers around make_header_ex(), which returns
-1 on every "goto failure" path (image file not openable, header malloc
failure, firmware version out of range, certificate chain errors,
signing and output write errors). Since ret is initialized to 0 and is
only reassigned by the optional base_diff() delta step, a signing run
that produced no output image still terminated with status 0, so
Makefile recipes and CI treated the failure as success and moved on
with a missing or stale *_v<ver>_signed.bin. This was also asymmetric
with the key loading path just above, which exits on failure.

Capture the return value of both header helpers, skip the delta step
when header generation failed, and let main() return it.

Add tools/unit-tests/unit-sign-header-failure, covering the exit status
of both the plain and the hybrid signing path when the input image
cannot be opened.
2026-08-04 08:16:53 +02:00