Uh oh!
There was an error while loading. Please reload this page.
sbom: generate SBOMs from every wolfBoot build system - #830
Conversation
e16d1a6 to
df63948Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #830
No scan targets match the changed files in this PR. Review skipped.
23cc8a5 to
b99d976Comparesameehj
commented
Jul 27, 2026
Update: re-vendor wolfGlass tip |
danielinux
commented
Aug 12, 2026
@sameehj please rebase on latest master |
danielinux
left a comment
There was a problem hiding this comment.
Reviewed against head aafafcb4. Built SBOMs locally for sim, stm32h7 and sim-tpm with SOURCE_DATE_EPOCH=1700000000 and compared the outputs.
The architecture is the right one. Generating per build from OBJS rather than shipping a canned release SBOM is correct for wolfBoot, since every integrator's image is a different composition and a static document would be wrong for all of them. Three things here are clear improvements over the previous recipe:
- The config capture is now accurate. The old
make sbomran$(HOSTCC) -dM -Eon the bare-Dlist with no-include, soinclude/user_settings.hwas never evaluated andHAVE_ECC(derived fromWOLFBOOT_SIGN_ECC256) was absent. The SBOM described a secure bootloader with no signature algorithm.SBOM_SETTINGS_Hfixes that: asimbuild now records 230 config macros. - The absolute path scrub keeps
-DPICO_SDK_PATH=<abs path>out of the document while preserving the macro name, and CI asserts both halves on a real rp2350 build rather than only on a synthetic command line. - The
registered/pendingsplit inPRODUCT_CPEis the right discipline. I checked the NVD API:cpe:2.3:a:wolfssl:wolfboot:2.9.0:*:*:*:*:*:*:*is in the dictionary (created 2026-08-10, 33 entries), so the emitted CPE resolves.
Comments inline. Of those, the lib/wolfssl change and the SBOM identity collision are the two I would want resolved before this merges; the $(wildcard) drop and the missing sibling-library components are the two that decide whether the output actually satisfies "top-level dependencies".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| # the integrator of a product that embeds wolfSSL. Each `cpe` value must be | ||
| # the vendor:product pair NVD actually registers for that dependency; never | ||
| # synthesize one. | ||
| DEP_META = { |
There was a problem hiding this comment.
DEP_META covers wolfssl, wolfcrypt, libz and openssl, and the driver exposes only --dep-wolfssl, --dep-wolfcrypt and --dep-openssl. wolfBoot has six submodules: wolfssl, wolfTPM, wolfPKCS11, wolfHSM, wolfPSA and wolfHAL. The five that are not wolfssl can never be emitted as components.
Built config/examples/sim-tpm.config with lib/wolfTPM checked out:
- the captured config records
WOLFBOOT_TPM=1,WOLFTPM_SWTPM=1,WOLFTPM2_MAX_BUFFER=1500, so the document knows perfectly well that a TPM stack is compiled in tpm2.c,tpm2_wrap.c,tpm2_tis.c,tpm2_packet.c,tpm2_param_enc.c,tpm2_swtpm.c,tpm2_util.candtpm2_crypto.care folded into wolfBoot's own source setcomponents[]iswolfssl -> wolfcryptonly. No wolfTPM component, no version, no CPE, no PURL.
A wolfTPM advisory cannot match that document. The same holds for wolfHSM and wolfPKCS11 builds. This is the specific thing CRA Annex I Part II(1) asks for ("covering at the very least the top-level dependencies of the product"), so it is worth closing rather than deferring.
The detection signal is already present. Suggested shape: make DEP_META a table keyed by name with the same fields it has now, add a generic --dep NAME alongside the existing --dep-version NAME=VER, and have sbom.mk declare the component whenever the corresponding macro (WOLFBOOT_TPM, WOLFHSM_CLIENT, WOLFBOOT_PKCS11 and so on) appears in the captured config.
Two related exclusions are worth stating explicitly in the docs/SBOM.md Limitations section rather than leaving implicit: the compiler runtime (libgcc, newlib) is linked into every image and appears nowhere in the document, and vendor SDK sources are absent whenever the SDK lives outside the source tree.
There was a problem hiding this comment.
Agreed, and agreed it should be closed rather than deferred. The generic --dep NAME plus a name-keyed DEP_META is a gen-sbom change, so it is going upstream as a wolfGlass issue rather than a local patch.
The two exclusions you asked to be explicit are in docs/SBOM.md as of a050aee: the compiler runtime (libgcc, newlib) and vendor SDK sources outside the tree. I also listed the sibling submodules there so the gap is stated rather than implicit until the upstream fix lands.
| if srcs_basenames: | ||
| properties.append({ | ||
| 'name': 'wolfssl:sbom:source-set', | ||
| 'value': ','.join(srcs_basenames), | ||
| }) |
There was a problem hiding this comment.
This property is the only file-level record in the document, and it is a comma joined list of basenames: no path, no per file hash, no per file license. SPDX comes out with files: 0 and filesAnalyzed: false.
Two consequences. You cannot answer "which wolfssl file revisions are in this image", which is the question an incident response actually asks. And the basename collapse is ambiguous wherever a name repeats: sha256.c in a wolfBoot SBOM could be lib/wolfssl/wolfcrypt/src/sha256.c or a HAL file. The Merkle root cannot be decomposed to check one file.
The inputs already exist. srcs_merkle_hash computes a gitoid per source, and generate_cdx already has a file_entries path (line 1174) that the --lib route uses. Emitting SPDX files[] with repo-relative fileName plus SHA-256 and CONTAINS relationships, and reusing file_entries for the source route, closes this cheaply.
Related: srcs_merkle_hash calls sys.exit on any duplicate basename, so a target that pairs a vendor flash.c with another flash.c fails SBOM generation outright rather than degrading. Sorting on the repo-relative path instead of the basename would remove both the ambiguity and the hard failure.
There was a problem hiding this comment.
Agreed on all three points, including that srcs_merkle_hash should sort on the repo-relative path so a duplicate basename degrades instead of calling sys.exit. All of it is in vendored gen-sbom, so it goes upstream as a wolfGlass issue. Documented as a limitation meanwhile.
| The `wolfssl` component stays in the document regardless. Dropping it would | ||
| read as more precise and would take the scan from every wolfSSL advisory to | ||
| none. Narrow the TLS-only CVEs with a VEX statement instead, which is the | ||
| mechanism designed to say "present but not exploitable here". |
There was a problem hiding this comment.
This paragraph and the matching comment in gen-sbom (resolve_crypto_only) both direct the reader to narrow TLS-only wolfSSL CVEs with a VEX statement, but nothing in this PR produces one and there is no target that does.
include/user_settings.h sets WOLFCRYPT_ONLY for every configuration except a wolfHSM server with certificate chain verification, so essentially every wolfBoot user's scanner will report wolfSSL TLS advisories against an image that contains no TLS. Recommending the remedy without shipping it leaves that noise in place for everyone.
This looks like the highest value follow-up and a small one: a make vex emitting a CycloneDX VEX with not_affected / code_not_present for the TLS-only CVEs, keyed off the WOLFCRYPT_ONLY the document already captures. Worth filing as a follow-up issue even if it stays out of this PR.
There was a problem hiding this comment.
Agreed, and the noise is as broad as you describe given WOLFCRYPT_ONLY. Filing it as a follow-up rather than growing this PR.
One constraint worth flagging: gen-sbom has no VEX support at all today, only a docstring mention. wolfGlass keeps VEX in gen-advisory, which wolfBoot does not vendor. So make vex needs a decision on whether to extend gen-sbom or vendor gen-advisory too, which is why the issue goes to wolfGlass first.
Uh oh!
There was an error while loading. Please reload this page.
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 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>
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>
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>
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>
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 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:a:wolfssl:wolfboot:<ver>. 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>
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>
… SBOM 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>
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>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>
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>
aafafcb to
d0fb517Comparesrc/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>
Description
wolfBoot ships as source. Users build it in many ways. Each user needs an
SBOM for the build that the user makes. This PR makes wolfBoot produce a
CycloneDX 1.6 and SPDX 2.3 SBOM from every build system, and it makes the
SBOM reproducible.
One engine does the work. Each build system gives the engine two inputs:
the compiled source list and the build configuration. The engine captures
the configuration with the host compiler, so the SBOM is the same for GCC,
Clang, LLVM, IAR, armcl, CCRX, and XC32.
What is new
tools/scripts/wolfboot-sbom.sh.make sbom.cmake --build --target sbom.ide-sbom/iar_sbom.py.ide-sbom/compdb_sbom.py.ide-sbom/route_through_sbom.sh.make sbom-hal.ide-sbom/zephyr_sbom.py.ide-sbom/validate_sbom.py..github/workflows/test-sbom.yml.docs/SBOM.mdanddocs/SBOM-WOLFGLASS.md.Reproducibility fix
The driver captures build macros with the host compiler. Some macros hold
an absolute host path. For example,
arch.mkpasses-DPICO_SDK_PATH=$(PICO_SDK_PATH). Before this change, the path enteredthe SBOM. The SBOM was then different on each machine, and it leaked the
local file system.
The driver now redacts each absolute path in the captured macros. It keeps
the macro name, so the configuration record stays complete. The CI canary
asserts that no path leaks and that the macro name stays. Use
--no-scrubfor debug only.
Toolchain support
The SBOM content does not depend on the cross-compiler. The config uses
the host compiler. The composition is a source list. So Clang, LLVM,
Renesas CCRX, TI armcl, and IAR all give the same SBOM. A new toolchain
needs no work. A new IDE that emits a compilation database needs no work.
Relation to wolfGlass
The tools are product-neutral. They are self-contained in wolfBoot on
purpose. They are the reference implementation of the shared SBOM layer
for every wolfSSL product (wolfSSL, wolfHSM, and others).
docs/ SBOM-WOLFGLASS.mdgives the file-by-file migration map and the list ofchanges that wolfGlass must cover. No wolfBoot logic changes when the
tools move.
Prerequisite
The tools need
gen-sbomfrom wolfSSL (lib/wolfssl/scripts/gen-sbom).If the pinned submodule does not carry it, pass a path with
GEN_SBOM=...or the
--gen-sbomoption. The CI canary fetches it from wolfSSL masteras a fallback.
Testing
make sbom TARGET=simproduces a valid SBOM. The validator passes.make sbom-hal TARGET=simproduces a valid per-HAL SBOM.cmake --build <dir> --target sbomproduces a valid SBOM.the macro name stays.
Notes
sources, not as a separate component.
Git Bash. As an alternative, use
compdb_sbom.py.