From CMake Build to SBOM: Trusted Firmware-M on STM32H5

| Ritesh Noronha

Interlynk SBOM Breakdown

We recently generated CycloneDX SBOMs for a real Trusted Firmware-M build on an STM32H5 board using lynkctl. The build makes a common embedded firmware problem concrete: most SBOMs describe what is present in a repository, not what actually ships in the image.

Built is not shipped

In an embedded project, the source tree can contain vendor SDKs, board support packages, RTOS ports, HAL drivers, crypto libraries, network stacks, test utilities, generated files, sample applications, and old compatibility code. A CMake target references only a slice of that. The compiler builds less than the tree contains. The linker keeps less than the compiler produced.

For a CMake-based embedded project, I think about four layers:

  1. Code present in the repository.

  2. Code referenced by the selected CMake configuration.

  3. Code compiled into objects and archives.

  4. Code that actually contributes to the final firmware artifact.

Most false positives come from collapsing those layers into one answer.

If a vulnerability scanner flags a product because a vulnerable component exists in a shared third_party folder, the security team now has work to do. Someone has to prove whether that code is reachable, compiled, linked, or entirely unused for that device. At fleet scale, those false positives get expensive.

The inverse happens too. A build can pull in a static archive, a toolchain runtime, a generated source file, or a vendor-supplied object that never appears in a package manifest. If the SBOM only scans manifests or obvious directories, it can miss things that really do ship.

For embedded work, accuracy is not just more components. Accuracy is knowing which evidence supports each component.

Why source scanning is not enough for a CMake build

Source-tree scanning is the common starting point. Tools walk the repository, find package manifests and vendored source, and emit CycloneDX or SPDX. The weakness is not that scanning is bad. Scanning sees presence, not shipment. If third_party/mbedtls exists in the tree, that is useful to know. It does not mean mbedTLS was compiled for this board, linked into this firmware, or retained after dead-code elimination.

For a CMake project, the build already knows more than the filesystem does: the selected configuration, the target graph, compile flags, include paths, link inputs, and artifacts. The open source cmake-sbom project takes that seriously. It integrates with CMake and generates SPDX from install artifacts and explicit sbom_add declarations. The tradeoff is that it depends on what the project explicitly declares, and TF-M does not declare itself that way. It inherits vendor CMake, STMicro board scripts, generated files, and static archives fetched into the build tree, none of it designed for SBOM generation.

That is the case lynkctl is built for.

The build: Trusted Firmware-M on STM32H573I-DK

Trusted Firmware-M, usually written TF-M, is the reference implementation of the Secure Processing Environment for Arm Cortex-M devices. It is built around the Armv8-M security model, including TrustZone where the hardware supports it.

TF-M provides:

  • Secure boot for authenticating firmware images.

  • A secure runtime core that manages isolation and communication.

  • Secure services such as crypto, internal trusted storage, protected storage, firmware update, and initial attestation.

  • PSA Functional API surfaces for non-secure applications to call into secure services.

It is not a single flat application. It is a configured firmware platform with bootloader code, secure partitions, vendor platform support, generated files, third-party libraries, and linker outputs.

The target for this run was the STM32H573I-DK development kit:

TFM_PLATFORM=stm/stm32h573i_dk
TFM_PLATFORM=stm/stm32h573i_dk
TFM_PLATFORM=stm/stm32h573i_dk

In TF-M's platform metadata it is configured as:

TFM_SYSTEM_PROCESSOR=cortex-m33
TFM_SYSTEM_ARCHITECTURE=armv8-m.main
STM32H573xx
CRYPTO_HW_ACCELERATOR_TYPE=stm
TFM_SYSTEM_PROCESSOR=cortex-m33
TFM_SYSTEM_ARCHITECTURE=armv8-m.main
STM32H573xx
CRYPTO_HW_ACCELERATOR_TYPE=stm
TFM_SYSTEM_PROCESSOR=cortex-m33
TFM_SYSTEM_ARCHITECTURE=armv8-m.main
STM32H573xx
CRYPTO_HW_ACCELERATOR_TYPE=stm

The platform enables TrustZone and uses MCUboot as the bootloader path. It also enables the secure services that matter for a representative image: Crypto, Internal Trusted Storage, Protected Storage, Initial Attestation, the Platform partition, and Firmware Update.

The build produces more than one firmware artifact: a bootloader image and a secure TF-M image, each with its own linker map:

build/stm32h5-gnu-brew-newlib/bin/bl2.axf
build/stm32h5-gnu-brew-newlib/bin/tfm_s.axf
build/stm32h5-gnu-brew-newlib/bin/bl2.map
build/stm32h5-gnu-brew-newlib/bin/tfm_s.map
build/stm32h5-gnu-brew-newlib/bin/bl2.axf
build/stm32h5-gnu-brew-newlib/bin/tfm_s.axf
build/stm32h5-gnu-brew-newlib/bin/bl2.map
build/stm32h5-gnu-brew-newlib/bin/tfm_s.map
build/stm32h5-gnu-brew-newlib/bin/bl2.axf
build/stm32h5-gnu-brew-newlib/bin/tfm_s.axf
build/stm32h5-gnu-brew-newlib/bin/bl2.map
build/stm32h5-gnu-brew-newlib/bin/tfm_s.map

That distinction matters for SBOM generation. A map file is image-specific. If we want dead-code reconciliation and linked-image accuracy, the bootloader and secure firmware are two SBOM subjects, not one.

Build the firmware first, then generate

The SBOM run happens after configuring and building the firmware. That gives lynkctl real build metadata instead of asking it to infer from source alone.

We used the GNU Arm embedded toolchain:

TFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake
CROSS_COMPILE=arm-none-eabi
TFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake
CROSS_COMPILE=arm-none-eabi
TFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake
CROSS_COMPILE=arm-none-eabi
cmake -S . -B build/stm32h5-gnu-brew-newlib -GNinja \
  -DTFM_PLATFORM=stm/stm32h573i_dk \
  -DTFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake \
  -DCROSS_COMPILE=arm-none-eabi

ninja -C

cmake -S . -B build/stm32h5-gnu-brew-newlib -GNinja \
  -DTFM_PLATFORM=stm/stm32h573i_dk \
  -DTFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake \
  -DCROSS_COMPILE=arm-none-eabi

ninja -C

cmake -S . -B build/stm32h5-gnu-brew-newlib -GNinja \
  -DTFM_PLATFORM=stm/stm32h573i_dk \
  -DTFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake \
  -DCROSS_COMPILE=arm-none-eabi

ninja -C

Generating two SBOMs

Each linked image has its own map file, so we generated two SBOMs:

build/sboms/stm32h5-bl2.cdx.json
build/sboms/stm32h5-tfm_s.cdx.json
build/sboms/stm32h5-bl2.cdx.json
build/sboms/stm32h5-tfm_s.cdx.json
build/sboms/stm32h5-bl2.cdx.json
build/sboms/stm32h5-tfm_s.cdx.json

This keeps the SBOM subject aligned with the binary subject. The bootloader and secure runtime are not identical: they share some build environment and dependency evidence, but each image has its own target graph, linked contents, and runtime role. Combining them into one map-driven SBOM would blur that boundary.

These commands were run from the TF-M source root, with --project-root set to keep source paths anchored to the TF-M root. We used the CMake provider explicitly, passed the configured build directory, and passed explicit --vendored-root values. That matters here because several dependencies are fetched into the build tree under build/stm32h5-gnu-brew-newlib/lib/ext/*-src. Those roots are the package boundaries for MCUboot, TF-PSA-Crypto, CMSIS_6, QCBOR, and t_cose.

For the bootloader:

lynkctl generate . \
  --provider cmake \
  --cmake-build-dir build/stm32h5-gnu-brew-newlib \
  --cmake-target bl2 \
  --map-file build/stm32h5-gnu-brew-newlib/bin/bl2.map \
  --project-root /src/trusted-firmware-m \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/mcuboot-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/tf-psa-crypto-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/cmsis-src \
  --evidence \
  -o build/sboms/stm32h5-bl2.cdx.json \
  -v
lynkctl generate . \
  --provider cmake \
  --cmake-build-dir build/stm32h5-gnu-brew-newlib \
  --cmake-target bl2 \
  --map-file build/stm32h5-gnu-brew-newlib/bin/bl2.map \
  --project-root /src/trusted-firmware-m \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/mcuboot-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/tf-psa-crypto-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/cmsis-src \
  --evidence \
  -o build/sboms/stm32h5-bl2.cdx.json \
  -v
lynkctl generate . \
  --provider cmake \
  --cmake-build-dir build/stm32h5-gnu-brew-newlib \
  --cmake-target bl2 \
  --map-file build/stm32h5-gnu-brew-newlib/bin/bl2.map \
  --project-root /src/trusted-firmware-m \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/mcuboot-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/tf-psa-crypto-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/cmsis-src \
  --evidence \
  -o build/sboms/stm32h5-bl2.cdx.json \
  -v

For the secure firmware image:

lynkctl generate . \
  --provider cmake \
  --cmake-build-dir build/stm32h5-gnu-brew-newlib \
  --cmake-target tfm_s \
  --map-file build/stm32h5-gnu-brew-newlib/bin/tfm_s.map \
  --project-root /src/trusted-firmware-m \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/mcuboot-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/tf-psa-crypto-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/qcbor-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/t_cose-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/cmsis-src \
  --evidence \
  -o build/sboms/stm32h5-tfm_s.cdx.json \
  -v
lynkctl generate . \
  --provider cmake \
  --cmake-build-dir build/stm32h5-gnu-brew-newlib \
  --cmake-target tfm_s \
  --map-file build/stm32h5-gnu-brew-newlib/bin/tfm_s.map \
  --project-root /src/trusted-firmware-m \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/mcuboot-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/tf-psa-crypto-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/qcbor-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/t_cose-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/cmsis-src \
  --evidence \
  -o build/sboms/stm32h5-tfm_s.cdx.json \
  -v
lynkctl generate . \
  --provider cmake \
  --cmake-build-dir build/stm32h5-gnu-brew-newlib \
  --cmake-target tfm_s \
  --map-file build/stm32h5-gnu-brew-newlib/bin/tfm_s.map \
  --project-root /src/trusted-firmware-m \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/mcuboot-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/tf-psa-crypto-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/qcbor-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/t_cose-src \
  --vendored-root build/stm32h5-gnu-brew-newlib/lib/ext/cmsis-src \
  --evidence \
  -o build/sboms/stm32h5-tfm_s.cdx.json \
  -v


Results

The bootloader SBOM completed with 10 components:

SBOM generated: 10 components, 5 warnings, 0 errors
selected CMake configuration "MinSizeRel"
selected CMake target "bl2"
SBOM generated: 10 components, 5 warnings, 0 errors
selected CMake configuration "MinSizeRel"
selected CMake target "bl2"
SBOM generated: 10 components, 5 warnings, 0 errors
selected CMake configuration "MinSizeRel"
selected CMake target "bl2"

The secure firmware SBOM completed with 20 components:

SBOM generated: 20 components, 5 warnings, 0 errors
selected CMake configuration "MinSizeRel"
selected CMake target "tfm_s"
SBOM generated: 20 components, 5 warnings, 0 errors
selected CMake configuration "MinSizeRel"
selected CMake target "tfm_s"
SBOM generated: 20 components, 5 warnings, 0 errors
selected CMake configuration "MinSizeRel"
selected CMake target "tfm_s"

Both are CycloneDX 1.6 JSON documents. The warnings are OSS-index boundary warnings for content-range or alias matches such as tinycrypt, mbedtls, tf-psa-crypto, mbedtls-framework, and cddl_gen. The component identities were still improved by the explicit vendored roots and git checkout enrichment. This is a diagnostic-cleanup area, not a blocker for the SBOM content.

The component inventory

After generating the SBOMs, we inspected the component list from each CycloneDX document. The full SBOMs carry the complete inventory and evidence annotations; for the article, the important entries are the open source and runtime components that affect review and triage.

Component

Seen in

Why it matters

TF-PSA-Crypto

bl2, tfm_s

Crypto implementation used by the bootloader and secure image.

MCUboot

bl2, tfm_s

Bootloader code and firmware update path.

CMSIS_6

bl2, tfm_s

Arm Cortex-M support layer used by the platform build.

STM32H5 HAL driver

bl2, tfm_s

STMicro platform driver code for the selected board family.

CMSIS Device H5

bl2, tfm_s

STM32H5 device support behind the platform targets.

QCBOR

tfm_s

CBOR encoding used by attestation-related secure firmware code.

t_cose

tfm_s

COSE support used by attestation-related secure firmware code.

GCC runtime support (libgcc.a)

bl2, tfm_s

Runtime archive linked into the firmware image, distinct from the compiler executable.

The full inventories also include first-party TF-M targets such as tfm_spm, tfm_sprt, and the secure partitions. Those rows are useful in the SBOM because they preserve target-level build evidence, but they do not need to be reproduced in full here. Build tools such as cmake, ninja, and the compiler front ends are marked excluded rather than counted as linked runtime firmware content.

What this build tells us

The bootloader and the secure runtime are different subjects. bl2 links MCUboot's bootutil, a crypto slice as bl2_crypto, and the STM32H5 platform as platform_bl2. tfm_s links a wider set: the secure partitions (tfm_psa_rot_partition_*), the SPM and SPRT runtime, QCBOR and t_cose for attestation encoding, and a different crypto service target. A single merged SBOM would have hidden that difference.

Build tools are labeled excluded. cmake, ninja, and the two arm-none-eabi compiler front ends were detected, but they are not linked firmware content, so they are marked out of scope rather than counted as shipped. The one toolchain entry that does ship, gcc as required, is there because libgcc.a is linked into the image. That is the distinction that matters during triage: a scanner that treats "found the compiler" the same as "shipped the compiler" produces noise.

The vendored roots gave real package identity. TF-PSA-Crypto, MCUboot, CMSIS_6, QCBOR, and t_cose resolved to GitHub purls with commit-level versions, because we told the enrichment pipeline where their package boundaries were in the fetched build tree. Without that, the same code shows up as anonymous vendored source.

The STMicro platform shows up as real content. This board sets CRYPTO_HW_ACCELERATOR_TYPE=stm, and that choice is visible in tfm_s as the crypto_service_crypto_hw target: the secure image is wired to STM32H5 hardware crypto, not a pure software path. The rest of the STMicro layer resolves to versioned STMicro repos, the stm32h5xx_hal_driver and the cmsis_device_h5 device support behind platform_bl2 and platform_s. On an STM32 build, that is exactly the code a customer or auditor will ask about, so it should carry a real identity rather than sit inside an unnamed board directory.

Generated code is hard. Vendor blobs are hard. Patched vendored trees are hard. Proprietary toolchain libraries are hard. LTO can obscure boundaries. Stale build directories can lie. Linker maps differ across toolchains.

That is why diagnostics matter. A useful SBOM generator should tell you when evidence is missing or weak, and distinguish what it confirmed from what it inferred. The OSS-index warnings in this run are an example of that honesty: the tool flagged where a content match was fuzzy instead of asserting a clean identity it did not have. A quiet, overconfident SBOM is worse than a noisy but honest one.

The goal is defensible evidence. For CMake-based embedded firmware, the best SBOM starts with the build, is corrected by linker evidence, and is enriched only after that foundation is solid.

Want the actual SBOMs?

The two CycloneDX 1.6 documents from this run, stm32h5-bl2.cdx.json and stm32h5-tfm_s.cdx.json, are available on request. To inspect the real component lists, evidence annotations, and scope decisions, request the SBOMs here or email us at hello@interlynk.io.

Vertraut von Sicherheits- und Compliance-Teams in 100+ regulierten Unternehmen

Sehen Sie sich Ihr richtig erstelltes SBOM an

Interlynk automatisiert SBOMs, verwaltet Open-Source-Risiken, überwacht Lieferanten und bereitet Sie auf die Post-Quanten-Ära vor – alles auf einer vertrauenswürdigen Plattform.

Vertraut von Sicherheits- und Compliance-Teams in 100+ regulierten Unternehmen

Interlynk automatisiert SBOMs, verwaltet Open-Source-Risiken, überwacht Lieferanten und bereitet Sie auf das Post-Quanten-Zeitalter vor – alles auf einer vertrauenswürdigen Plattform.

Sehen Sie Ihr SBOM richtig gemacht

Vertraut von Sicherheits- und Compliance-Teams in 100+ regulierten Unternehmen

Interlynk automatisiert SBOMs, verwaltet Open-Source-Risiken, überwacht Lieferanten und bereitet Sie auf das Post-Quanten-Zeitalter vor – alles auf einer vertrauenswürdigen Plattform.

Sehen Sie Ihr SBOM richtig gemacht