Picks up per-configuration serialNumber and documentNamespace, so two
configurations of one wolfBoot version no longer share an identifier
inside a scanner. Also brings CPE/PURL build-metadata stripping,
--dep-* value validation, wolfcrypt version inheritance, and the
sbom.am host-cc macro capture.
The Makefile comment that recorded this as an open wolfGlass gap is
no longer true, so it goes.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
src/keystore.c holds the public keys that authorise a firmware update. The
build generates it from the signing key, so a fresh checkout does not hold it,
and the missing-source guard read that absence as an absent submodule. Every
Make-path SBOM job failed on it.
Depend on it, so it exists before the guard runs, and name it in the source
list unconditionally: sbom.mk expands SBOM_SRCS through $(call) while reading
the makefile, so a file that appears later can never reach the document.
Without that second half the guard passed while the document was still one
source short, which is the silent under-report the guard exists to prevent.
Build the list with $(shell) rather than $(wildcard). GNU make 3.81 caches
directory contents, so it never sees a source generated during the same run.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
wolfGlass is public, so actions/checkout resolves the pinned revision with
the default token. Requiring WOLFGLASS_TOKEN made the job skip itself
whenever the secret was absent, which is every fork pull request, so the
one guard that holds tools/sbom/ to its pin was off for exactly the
contributors whose vendored copy nobody has reviewed.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Four exclusions were implicit, and a reader had no way to see them from
the document itself:
* the compiler runtime (libgcc, newlib) is linked into every image and
appears nowhere;
* of the six submodules only wolfSSL becomes a component, so a wolfTPM,
wolfHSM or wolfPKCS11 advisory cannot match, even though the captured
configuration records that the stack is compiled in;
* the document records the source set rather than each file, so SPDX
reports filesAnalyzed: false and the Merkle hash cannot be taken apart;
* WOLFCRYPT_ONLY is set for nearly every configuration, so a scanner
reports wolfSSL TLS advisories against an image with no TLS, and
wolfBoot ships no VEX statement to answer them with.
The first is inherent to a source-set SBOM. The other three are open
work, and the wolfGlass component table and a VEX target are where they
get closed.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Both output files were wolfboot-<version>.{cdx,spdx}.json, so every
configuration at a given version wrote to the same two paths. wolfBoot has
on the order of 100 target configurations and each is a different image
with a different source set, so building a second one overwrote the first
with no warning.
Put TARGET, SIGN and HASH in the name, on the Make and the CMake route
alike, so the two routes stay in step.
This does not fix the identity of the documents themselves. gen-sbom
derives both the CycloneDX serialNumber and the SPDX documentNamespace
from name and version only, so sim and stm32h7 at 2.9.0 still share
urn:uuid:6dc51759-ade2-5967-b392-2f23ed0eb4df, which a tool that keys on
serialNumber treats as one project. That is a wolfGlass change, and it
is not worked around here.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
SBOM_SRCS runs the object list through $(wildcard), so a source that is
not on disk disappears before the driver sees it. The driver's own
--skip-missing warning then has nothing left to report, which is why it
never fires on the Make path that every embedded user takes.
The result is a document that under-reports the image with no diagnostic
anywhere. A sim-tpm build without lib/wolfTPM checked out silently loses
all eight tpm2*.c sources, and validate_sbom.py accepts the result even
though the captured configuration still records WOLFBOOT_TPM=1.
Compare the two lists instead. An object that maps to neither a .c nor a
.S on disk means a submodule or a vendor SDK is absent, so stop and name
the objects on stderr. SBOM_ALLOW_MISSING=1 accepts the partial document
for the cases where the SDK genuinely cannot be present, and still lists
what is missing; imx_rt is one, because it compiles three MCUXpresso
drivers from outside the source tree.
Every other example config either passes or names only submodule paths
that CI checks out, so no job needs the escape hatch.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
The cross_targets matrix lists config file names, and the step passed the
same string as TARGET. For four of the seven entries that is not the
target the config selects: nrf52840.config sets TARGET=nrf52,
imx-rt1060.config sets imx_rt, nxp-t1040.config sets nxp_t1040, and
renesas-rx65n.config sets rx65n.
TARGET on the command line beats the config, so the build asked for
hal/nrf52840.o, whose source does not exist. SBOM_SRCS filters the source
list through $(wildcard), which dropped it, and the job stayed green
while publishing an SBOM for a bootloader with no target HAL at all:
29 sources, hal.c present, nrf52.c absent.
Drop the TARGET override and let the config decide, which is what the
documented invocation already does. nrf52840 now records 30 sources with
hal/nrf52.c among them, and the document is named for the real target.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
tools/sbom/.wolfglass-rev named 1bfcf4f1a293ba09f0ff6d67904dca09ee8eb6d7,
which exists in no repository. The vendored content is wolfGlass
d34a906638444b6990218a49927bcebafc5a539b: tools/wolfglass-sync --check
against that revision reports every file identical and only the pin
itself as drift.
A bare SHA with nothing checking it is also how a 1803-line vendored
generator drifts from the copy wolfSSL controls, or carries a local
patch, without anything noticing. Add a job that checks wolfGlass out at
the pinned revision and runs its own tools/wolfglass-sync --check, which
compares every vendored file against share/ and the pin against HEAD. An
unresolvable revision now fails that job rather than sitting in the tree.
A fork PR has no token to read wolfGlass with, so the job reports a
notice and passes there; the run on wolfSSL/wolfBoot is the gate.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Re-vendor wolfGlass tip where PRODUCT_CPE marks wolfboot registered
(NVD dictionary entries created 2026-08-10). Docs drop the "no CPE yet"
wording. The main package now carries cpe:2.3🅰️wolfssl:wolfboot:<ver>.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
The bootloader is built WOLFCRYPT_ONLY: include/user_settings.h defines
it for every configuration except a wolfHSM server with certificate-chain
verification, so the image carries the crypto subset of wolfSSL and no
TLS. The SBOM said nothing about that, leaving an integrator to triage
TLS advisories against a bootloader that cannot run TLS. The re-vendored
generator reads the macro out of the captured configuration and records
the subset; the wolfssl component stays, because it is the identifier NVD
maps wolfSSL advisories to and the wolfcrypt one has no CVEs mapped to it
at all. wolfcrypt is now nested inside wolfssl rather than beside it,
which is where it ships from.
wolfBoot also stopped emitting a CPE. NVD has no wolfssl:wolfboot entry,
and an unlisted CPE is indistinguishable to a scanner from a listed one
with no advisories; the submitted identifier is recorded as
wolfssl:sbom:cpe-requested until the dictionary request is published.
The CMake route passed its -D set with no settings header, so it recorded
none of the configuration user_settings.h derives from that set -- a
different document than `make sbom` produced from the same tree, and one
that could not see WOLFCRYPT_ONLY. It now passes the same SETTINGS_H and
INCLUDE_DIRS the Makefile does.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Re-vendor the wolfGlass tip that gives every dependency component a CPE
alongside its PURL, and that emits pkg:github identifiers a consumer can
actually resolve. The wolfSSL entry in a wolfBoot SBOM previously carried
a PURL only, and that PURL pointed at a git tag which does not exist
(wolfSSL releases are tagged `-stable`), so an integrator monitoring NVD
could not match wolfSSL advisories against this firmware.
The CMake route also described a different product than the Make route:
it never declared the wolfSSL dependency, so its `components` array was
empty, and it recorded wolfBoot as a GPL-3.0-only library rather than a
GPL-3.0-or-later firmware image. Both routes now emit the same component
identity for the same configuration.
wolfCrypt stays inventoried as wolfBoot's own sources. It is not a
separate NVD product; its advisories are published against
wolfssl:wolfssl, so the wolfSSL identifiers cover that code.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Re-vendor wolfGlass a54cde1 and declare what the capture needs.
wolfBoot's wolfCrypt configuration is derived rather than literal:
include/user_settings.h turns WOLFBOOT_SIGN_ECC256 into HAVE_ECC,
HAVE_ECC256, ECC_TIMING_RESISTANT and the rest. Capturing CFLAGS alone
recorded the -D set and none of what it selects, so the SBOM described a
signing bootloader with no signature algorithm. Point SBOM_SETTINGS_H at
the wolfCrypt settings header, give it SBOM_INCLUDE_DIRS, and make
include/target.h a prerequisite since user_settings.h includes it and it
carries the flash layout.
For an stm32u5 ECC256 build the configuration record now holds 151
defines, including WOLFBOOT_SIGN_ECC256, HAVE_ECC, HAVE_ECC256,
WOLFBOOT_HASH_SHA256 and IMAGE_HEADER_SIZE=256, none of which appeared
before. BOOTLOADER_PARTITION_SIZE is 65536 rather than the literal `)`
that Make's double expansion had left in its place.
Declare wolfSSL as a dependency component by default, pinned to the
submodule's version. wolfCrypt is compiled into the image rather than
linked, so its sources are listed as wolfBoot's own; recording the
dependency as well is what lets a scanner match wolfSSL advisories
against this firmware.
Record the product as firmware rather than a library, in CycloneDX
component.type and SPDX primaryPackagePurpose alike. Override the
licence to GPL-3.0-or-later: LICENSE is the verbatim GPLv3 and says
nothing about how wolfBoot licenses under it, while all 252
GPL-headered sources say "either version 3 ... or (at your option) any
later version". The per-HAL SBOM inherits the override so the two
documents cannot contradict each other.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Sync tools/sbom to wolfGlass 9bdf5b7: document --cflags -D-only
behavior, add WOLFSSL_DIR/version.h --dep-version fallback with correct
$$$$ expansion, and teach validate_sbom.py --min-properties. CI Make
path checks now require a non-empty property set so empty captures
cannot pass.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
The SBOM canary now guards the properties customers rely on, not only
that each route runs.
sbom_canary job adds three checks:
* Toolchain neutrality - build the same sim config with gcc and with
clang and require a byte-identical CycloneDX and SPDX result. The
driver captures configuration with the host compiler and a source
list, so the cross-toolchain that builds the firmware does not change
the SBOM. clang, LLVM and vendor compilers need no separate front end.
* Reproducibility - build the same config from a second absolute path
and require an identical SBOM, so no build path leaks into the output.
* Path scrub in a real build - build rp2350 with an absolute
PICO_SDK_PATH and assert the path is redacted while the macro key is
kept. This exercises the scrub through arch.mk, not a synthetic line.
New cross_targets job runs make sbom for a spread of architectures with
no IDE and no cross-toolchain installed: stm32h7, nrf52840, imx-rt1060
and sama5d3 (Arm), nxp-t1040 (PowerPC), renesas-rx65n (Renesas RX) and
hifive1 (RISC-V). The driver never calls the cross compiler, so each
target produces a valid SBOM on a plain runner. This proves the "any
target, any toolchain, no hardware" guarantee.
Every produced document is checked with validate_sbom.py and uploaded as
a build artifact.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
wolfBoot ships as source. Users build it in many ways. Before this
change, only the plain Make build could make an SBOM. So a user could not
make an SBOM for the build that the user runs.
This change adds one shared engine (tools/scripts/wolfboot-sbom.sh, which
calls wolfSSL gen-sbom) and a front end for each build system. Every
build makes a CycloneDX 1.6 and SPDX 2.3 document. The engine captures
the configuration with the host compiler, so the SBOM is the same for
GCC, Clang, LLVM, IAR, armcl, CCRX, and XC32.
Routes:
- Make, arch.mk, and vendor SDKs: make sbom TARGET=<t> SIGN=<a>
- CMake and the Pico SDK: cmake --build <dir> --target sbom
- IAR Embedded Workbench: ide-sbom/iar_sbom.py
- Any IDE with a compilation database: ide-sbom/compdb_sbom.py
- TI CCS, MPLAB X, Renesas, Xilinx: ide-sbom/route_through_sbom.sh
- Per-HAL component: make sbom-hal TARGET=<t>
- Zephyr module: ide-sbom/zephyr_sbom.py
Make the SBOM reproducible. The captured macros can hold an absolute host
path. For example, arch.mk passes -DPICO_SDK_PATH=$(PICO_SDK_PATH). The
driver now redacts each absolute path but keeps the macro name, so the
configuration record stays complete. Add --no-scrub for debug.
Add a validator (ide-sbom/validate_sbom.py) and a CI canary
(.github/workflows/test-sbom.yml) that runs and validates every route.
The canary also checks that no host path leaks into the SBOM.
Add docs/SBOM.md. The tools are product-neutral by design, so they can be
shared across wolfSSL products later without logic changes.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
The test file headers ran to 20-40 lines of prose before the licence,
restating the whole finding and the harness design. Cut them to a short
paragraph on what broke and a short one on how the test reaches the
code. Same for the long inline blocks in the HAL and libwolfboot
changes.
Comments only; no functional change.
test-configs.yml already builds this config; what nothing covers is the
keytools half of the documented procedure -- signing and encrypting an
AArch64 payload and assembling the boot image. That is where the wrong
key size in the docs hid, so build it in CI.
Builds with DEBUG_UART=1, the mode the docs tell users to run and one
that did not compile until now. It stops short of booting: the target
emits nothing on the console under QEMU, so there is nothing to assert.
DEBUG_UART=1 did not compile: uart_init() declared an unused local and
-Werror rejected it, so the target's console output could never be
enabled. Drop the local.
The documented key file is 44 bytes, but AES256-CTR wants a 32-byte key
plus a 16-byte IV; sign fails with "Error reading IV" as written. Use
48 bytes, matching the key the HAL used to carry.
Drop the internal report numbers from source, test and Makefile
comments: they mean nothing outside the tracker and do not belong in
the tree. Condense the long inline comment blocks the review flagged,
and note in the raspi3 encrypted example and its docs that CI builds
but does not boot it, so the end-to-end path has no automated
coverage.
Comments and docs only; no functional change.
Widening the misc.c include guard with NVM_FLASH_WRITEONCE made this
file include <wolfssl/wolfcrypt/types.h> and <wolfcrypt/src/misc.c> in
every such build, including the two that cannot resolve them:
tools/check_config has no wolfSSL include path, and the STM32Cube
test-app has no stm32wbxx_hal_conf.h. Both fail to compile.
Scrub with a local volatile byte loop instead and put the guard back.
That also suits the RAMFUNCTION callers better, since ForceZero() lives
in flash and must not be called while flash is being programmed.
The wait loop increments timeout past FLASH_TIMEOUT_TRIES on its way
out, so "timeout == FLASH_TIMEOUT_TRIES" never matched and a hung
command returned success. Test the completion flag instead.
That made the failure path reachable for the first time, and it did not
exit: the break left the inner block loop only, so the outer loop
respun the same failing command forever with pos unchanged.
Removing the hardcoded test key is right, but nothing on this target
calls wolfBoot_set_encrypt_key() any more, so the shipped example and
the docs walk-through (whose example key was that same test key) no
longer boot an encrypted image. Say so in both places.
NXP's qoriq-fman3 dtsi numbers the 10G memacs 0x8/0x9, and phydevs
holds the 10G port at FM1_10GEC1. Bounding the raw cell-index against
the table traded an out-of-bounds read for a silently dropped MAC
fixup on exactly the DTBs that motivated the guard. Translate instead,
and skip only indexes with no slot.
sdhci_wait_busy() runs at the top of every transfer and can now spin
for the full 30 s budget. Neither loop petted the watchdog, so on a
platform with one a stuck card produced a reset instead of the clean
I/O error the timeout exists to give. The shared deadline between the
two loops is deliberate and now says so.
The weak sdhci_platform_wdt_pet() default moves out of the
SDHCI_BLOCK_VIA_PDMA guard, since the busy waits call it on every
build. The test drops its SDHCI_WAIT_BUSY_TIMEOUT_MS=50 override and
steps its timer 1 ms per read instead, so the shipped default is what
is tested, and it counts watchdog services.
The NOR-extent test ran after the microcode table had already been
dereferenced, so a container near the top of the bank read past the
window the guard exists to enforce -- a machine check on hardware. Bound
the fixed part before the first dereference and the table before the
walk. The accept/reject outcome is unchanged; the out-of-bank read is
what goes away.
Use the file's own QE_MAX_RISC for the count bound instead of a
hand-written 2, matching the t10xx and p1021 siblings; the shared length
formula already handles count > 1.
The versal host copy deleted every asm statement, including the three
with output operands, leaving reads of uninitialised automatics -- at
-O0 an infinite loop in hal_delay_us(). Rewrite those three into
assignments from a host counter and only delete asm with an empty
output field; a new rule guard fails the build if any asm survives,
so a future one cannot be silently dropped.
clean now removes the 17 generated sources and the conditionally-built
tests, and .gitignore covers all 125 test binaries plus the generated
sources -- a full build left 30 untracked files before. The gnu-efi
skip announces itself like the adjacent multilib probe.
open_kernel_image() left *sz set on the read-failure path though it
documents both outputs as 0 on failure. The p1021 FCM helpers used
32-bit accesses from flash_idx, which starts at the page column, so an
odd column made every access misaligned -- an alignment interrupt on
the guarded eLBC window on e500.
The exit_lock scrub was guarded to the case where ENCRYPT_CACHE is a
stack local, skipping both static ones -- NVM_FLASH_WRITEONCE aliases
it to NVM_CACHE, WOLFBOOT_SMALL_STACK gives it its own array. Those
are exactly the buffers where the plaintext key and nonce would stay
resident for the rest of the boot.
sdhci_uhs_recover_rollback() restored 3.3V but left g_uhs_recovered
set, leaving the host in a state the base code could not reach: back
at 3.3V with the recovery spent, so a genuinely UHS-I card could never
be retried for the rest of the boot.
The extract rules append with >> across several commands. Without
.DELETE_ON_ERROR: a failure partway through leaves a truncated header
with a fresh mtime that the next make treats as up to date.
tfm.c never tests WC_NO_HARDEN, so dropping it enabled nothing; it only
un-silenced the advisory #warning in settings.h, which -Werror turns
into a build failure for any USE_FAST_MATH + WOLFCRYPT_TZ_PSA build.
Define TFM_TIMING_RESISTANT, which is what makes tfm.c constant time.
XSPI_IPCMDDONE only says the controller finished driving the sequence
on the bus. The device keeps WIP set for the ~ms the cycle takes and
ignores Write Enable while it is set, so the per-page WREN this PR
added was dropped for pages 2..N and their Page Programs with it.
The erase loops polled the same bit with inverted polarity: they
exited as soon as WIP read *set*, i.e. once the erase had started, and
would spin forever if they missed the busy window. FLASH_READY_MSK is
renamed FLASH_SR_WIP_MSK so the sense is not guessable, and both paths
now share xspi_wait_ready().
The test device models WIP, so a missing wait shows up as a rejected
Write Enable.
The block-crossing short write issued two programming commands
back-to-back. The F021 FSM rejects a command while busy, and the
read-modify-write that stages the next block reads the array while the
first program is in flight, so it stages undefined data and programs
it. The multi-block branch already waits after every command.
The test FSM now stays busy for a few polls and holds the programmed
data back until it drains, so the missing wait is observable.
The bad-block check reads flash_buf[bad_marker], which sits in the
spare region. A BC != 0 transfer loads only the requested main-area
bytes, so the marker byte is leftover FCM RAM: a stale non-0xFF value
makes ext_flash_read() declare the block bad, skip to the next erase
block and return data from the wrong address while still returning
len. BC = 0 is also the only setting that checks ECC.
Latent until the FBCR shift fix made BC take effect. read_size still
bounds the copy out of the buffer, so the F-7976 sizing fix stands.
P1021RM 12.3.30 numbers BC as bits 20-31 MSB-first, i.e. unshifted --
same convention every FPAR field macro in this file already follows
(FPAR_SP_PI at RM 17-21 is << 10, CI at RM 23-31 is unshifted). The
<< 20 put the count in reserved bits 0-19 and left BC at 0, which the
RM defines as "whole page + spare, FPAR[MS]/[CI] treated as zero": a
partial program filled the FCM buffer at offset col but then wrote the
entire page back, stale bytes included.
The test emulator decoded FBCR the same wrong way, so it could not see
this; it now decodes BC as the hardware does, and fails on the shifted
macro while still catching the original F-7976 bug.
The poll loops divided a 64-bit elapsed count by 1000 on every
iteration, which is a libgcc helper call on 32-bit targets. Compute
the timeout in microseconds once instead.
The partial-tail handling skipped the words that hold no requested
byte. Neither part can do that: U5 only starts the 128-bit program on
the fourth word (a partial quad-word leaves FLASH_SR_WDW set and
hal_flash_wait_complete() never returns), and L5 has no 32-bit program
mode, so splitting the double word into two PG cycles programs
nothing and faults the second on alignment.
Build the full unit first, taking bytes outside [i, len) from the
current flash content as hal/stm32h5.c does, then store every word.
That drops the over-read F-9763/F-9764 targeted without ever issuing
a partial unit. L5 TZ claim widened to the 8-byte boundary to match.