Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
431 changes: 431 additions & 0 deletions .github/workflows/test-sbom.yml

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ language.settings.xml
/**/build
/**/build-**

# Exception: vendored wolfGlass SBOM integration sources.
# This directory contains tracked Make/CMake fragments, not generated output.
!/tools/sbom/build/
!/tools/sbom/build/**

# User config
# See cmake/config_defaults_user.cmake.sample
/**/cmake/config_defaults_user.cmake
Expand Down Expand Up @@ -440,3 +445,13 @@ aarch64_efi-stage/
tools/qemu-esp/
# UEFI Secure Boot keys/certs generated by tools/scripts/sign-efi-secureboot.sh
tools/efi-secureboot-keys/

# Generated SBOM artifacts (CycloneDX 1.6 + SPDX 2.3)
wolfboot-*.cdx.json
wolfboot-*.spdx.json
wolfboot-*.spdx
wolfboot-sbom-srcs.txt

# Python cache files
__pycache__/
*.py[cod]
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1550,4 +1550,9 @@ if(HOST_IS_MSVC) # Some VS2022 helpers
"${CMAKE_CURRENT_BINARY_DIR}")
endif() # HOST_IS_MSVC VS2022 helpers

#---------------------------------------------------------------------------------------------
# SBOM generation (CycloneDX 1.6 + SPDX 2.3), shares the engine used by `make sbom`
#---------------------------------------------------------------------------------------------
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/sbom.cmake)

message(STATUS "End [WOLFBOOT_ROOT]/CmakeLists.txt")
166 changes: 112 additions & 54 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -867,71 +867,129 @@ pico-sdk-info: FORCE
# recipe echoes the effective target/sign so a default build is visible; pass
# them explicitly to get an SBOM that reflects your actual configuration.
#
# Extracts the configuration-specific source list from OBJS (which is fully
# assembled by this point — core wolfBoot + wolfcrypt + HAL sources are all
# included), captures the build's -D configuration macros via $(HOSTCC) -dM -E
# on the host, and calls gen-sbom to emit CycloneDX and SPDX output files.
# This is the plain-Make / arch.mk entry point. It also covers every build
# that is really the Makefile with a vendor SDK bolted on via source/include
# paths (MCUXpresso, STM32Cube, PSoC6, Freedom-E-SDK, Vorago) and the IDE
# targets that also have an arch.mk path (TI Hercules, Renesas RX, Zynq). It
# extracts the configuration-specific source list from OBJS (fully assembled by
# this point: core wolfBoot + wolfcrypt + HAL) and passes it, together with the
# build CFLAGS, to the vendored wolfGlass driver under tools/sbom/.
#
# wolfcrypt sources are compiled directly into the wolfBoot image and are
# therefore listed as wolfBoot's own sources, not as a separate component.
# wolfcrypt sources are compiled directly into the wolfBoot image. They stay
# in the source-set hash, and are also declared as a wolfcrypt component so a
# CPE-driven scan can match the registered NVD product wolfssl:wolfcrypt.
#
# Optional make variables:
# HOSTCC Host C compiler for macro capture (default: cc)
# GEN_SBOM Path to wolfssl scripts/gen-sbom
# (default: $(WOLFBOOT_LIB_WOLFSSL)/scripts/gen-sbom)
# SBOM_GEN Path to gen-sbom
# (default: tools/sbom/gen-sbom via driver discovery)
# CRA_PYTHON Python interpreter (default: python3)

HOSTCC?=cc
WOLFBOOT_VERSION:=$(shell sed -n \
's/.*LIBWOLFBOOT_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
include/wolfboot/version.h)
GEN_SBOM?=$(WOLFBOOT_LIB_WOLFSSL)/scripts/gen-sbom
SBOM_CDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).cdx.json
SBOM_SPDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).spdx.json
SBOM_PYTHON?=$(or $(CRA_PYTHON),python3)

sbom:
@if [ -z "$(WOLFBOOT_VERSION)" ]; then \
echo "ERROR: could not read LIBWOLFBOOT_VERSION_STRING from include/wolfboot/version.h" >&2; \
echo " (check the file exists and its version format is intact)." >&2; \
exit 1; \
SBOM_ROOT:=$(WOLFBOOT_ROOT)
SBOM_NAME:=wolfboot
# src/keystore.c carries the public keys that authorise a firmware update. The
# build generates it from the signing key, so a fresh tree does not hold it yet.
# It is still a compiled source, and a bootloader SBOM that omits its own trust
# anchor describes the wrong image, so name it and depend on it below.
SBOM_GENERATED_SRCS=$(filter ./src/keystore.c src/keystore.c,\
$(patsubst %.o,%.c,$(OBJS)))
# $(wildcard) cannot build this list. GNU make 3.81 caches directory contents,
# so a source generated during the same run stays invisible for the rest of it;
# $(shell) re-reads the filesystem instead. The generated sources go in
# unconditionally because sbom.mk expands this list through $(call) while
# reading the makefile, which is before any recipe can create them. The driver
# opens the file afterwards, by which time the guard has made it exist.
SBOM_SRCS=$(sort $(SBOM_GENERATED_SRCS) $(shell for o in $(OBJS); do \
for e in c S; do s="$${o%.o}.$$e"; [ -f "$$s" ] && printf '%s ' "$$s"; done; \
done))
# A source that is absent must not be dropped quietly. Filtering the list to
# what happens to be on disk shrinks the document with no diagnostic anywhere:
# a sim-tpm build without lib/wolfTPM checked out loses all eight tpm2*.c
# sources, emits nothing, and still validates. An object that maps to no
# source is the signal, so compare the two lists and stop.
SBOM_SRCS_MISSING=$(filter-out $(basename $(SBOM_SRCS)),$(basename $(OBJS)))
SBOM_CFLAGS=$(CFLAGS)
# wolfBoot's wolfCrypt configuration is derived, not literal: include/user_settings.h
# turns WOLFBOOT_SIGN_ECC256 into HAVE_ECC, HAVE_ECC256, ECC_TIMING_RESISTANT and
# the rest. Capturing CFLAGS alone would record the -D set and none of what it
# selects, so the SBOM would report a bootloader with no signature algorithm.
SBOM_SETTINGS_H:=$(WOLFBOOT_LIB_WOLFSSL)/wolfssl/wolfcrypt/settings.h
SBOM_INCLUDE_DIRS:=$(WOLFBOOT_ROOT)/include $(WOLFBOOT_LIB_WOLFSSL)
# user_settings.h includes the generated target.h, so it must exist before the
# capture runs. It also carries the flash layout the SBOM records.
SBOM_PREREQS:=include/target.h sbom-check-sources
# Coat: wolfssl (TLS/library CPE) + wolfcrypt (crypto CPE). Sources remain in
# the merkle hash; the components give scanners resolvable identifiers.
SBOM_DEP_WOLFSSL?=yes
SBOM_DEP_WOLFCRYPT?=yes
SBOM_WOLFSSL_VERSION?=$(shell sed -n \
's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
$(WOLFBOOT_LIB_WOLFSSL)/wolfssl/version.h 2>/dev/null | head -1)
SBOM_VERSION=$(WOLFBOOT_VERSION)
SBOM_LICENSE_FILE=$(WOLFBOOT_ROOT)/LICENSE
# wolfBoot is a bootloader flashed as an image, not a library linked into one.
SBOM_COMPONENT_TYPE?=firmware
# LICENSE is the verbatim GPLv3, which says nothing about how wolfBoot licenses
# under it, so inference falls back to GPL-3.0-only and understates the grant.
# Every GPL-headered source says "either version 3 ... or (at your option) any
# later version".
SBOM_LICENSE_OVERRIDE?=GPL-3.0-or-later
# One version of wolfBoot has about 100 configurations, and each one is a
# different image with a different source set. Name the document after the
# configuration so a second target does not overwrite the first. gen-sbom still
# derives serialNumber and the SPDX documentNamespace from name and version
# alone, which collides inside a scanner as well; that part is wolfGlass's to
# fix, and this does not paper over it.
SBOM_CONFIG_TAG:=$(TARGET)$(if $(SIGN),-$(SIGN))$(if $(HASH),-$(HASH))
SBOM_CDX_OUT:=wolfboot-$(SBOM_CONFIG_TAG)-$(WOLFBOOT_VERSION).cdx.json
SBOM_SPDX_OUT:=wolfboot-$(SBOM_CONFIG_TAG)-$(WOLFBOOT_VERSION).spdx.json
SBOM_GEN?=

# Guards both SBOM targets: see SBOM_SRCS_MISSING above. SBOM_ALLOW_MISSING=1
# accepts the partial document, and still lists what is absent.
sbom-check-sources: $(SBOM_GENERATED_SRCS)
$(Q)if [ -n "$(strip $(SBOM_SRCS_MISSING))" ]; then \
echo "sbom: $(words $(SBOM_SRCS_MISSING)) object(s) map to no source on disk:" >&2; \
for o in $(SBOM_SRCS_MISSING); do echo " $$o.c (or .S)" >&2; done; \
if [ "$(SBOM_ALLOW_MISSING)" = "1" ]; then \
echo "sbom: SBOM_ALLOW_MISSING=1 set; the SBOM will describe fewer" >&2; \
echo "sbom: sources than the image really holds." >&2; \
else \
echo "sbom: the SBOM would under-report the image. A submodule is" >&2; \
echo "sbom: absent (git submodule update --init), a vendor SDK lives" >&2; \
echo "sbom: outside the tree, or the build generates that source and" >&2; \
echo "sbom: has not run yet. Set SBOM_ALLOW_MISSING=1 to accept it." >&2; \
exit 1; \
fi; \
fi
@if [ ! -f "$(GEN_SBOM)" ]; then \
echo "ERROR: gen-sbom not found at '$(GEN_SBOM)'." >&2; \
echo " Initialize the submodule: git submodule update --init lib/wolfssl" >&2; \
echo " or point GEN_SBOM at a wolfssl tree: make sbom GEN_SBOM=/path/to/wolfssl/scripts/gen-sbom" >&2; \
exit 1; \
fi
@echo "wolfBoot SBOM: version=$(WOLFBOOT_VERSION) target=$(TARGET) sign=$(SIGN)"
@echo " Outputs: $(SBOM_CDX_OUT) $(SBOM_SPDX_OUT)"
$(eval _SBOM_SRCS := $(wildcard $(patsubst %.o,%.c,$(OBJS))) $(wildcard $(patsubst %.o,%.S,$(OBJS))))
@if [ -z "$(_SBOM_SRCS)" ]; then \
echo "ERROR: no source files found in OBJS — check that TARGET and SIGN are correct." >&2; \
exit 1; \
fi
@set -e; \
_dh=$$(mktemp /tmp/wolfboot-sbom-defines.XXXXXX); \
_sf=$$(mktemp /tmp/wolfboot-sbom-srcs.XXXXXX); \
trap 'rm -f "$$_dh" "$$_sf"' EXIT; \
_defs=""; \
for _t in $(CFLAGS); do \
case "$$_t" in -D*) _defs="$$_defs $$_t" ;; esac; \
done; \
$(HOSTCC) -dM -E -DWOLFSSL_USER_SETTINGS $$_defs \
-x c /dev/null >"$$_dh" 2>/dev/null || \
{ echo "ERROR: '$(HOSTCC) -dM -E' failed; install a host C compiler or set HOSTCC." >&2; exit 1; }; \
printf '%s\n' $(_SBOM_SRCS) >"$$_sf"; \
$(SBOM_PYTHON) "$(GEN_SBOM)" \
--name wolfboot \
--version "$(WOLFBOOT_VERSION)" \
--supplier "wolfSSL Inc." \
--license-file "$(WOLFBOOT_ROOT)/LICENSE" \
--options-h "$$_dh" \
--srcs-file "$$_sf" \
--cdx-out "$(SBOM_CDX_OUT)" \
--spdx-out "$(SBOM_SPDX_OUT)"
@echo "SBOM written: $(SBOM_CDX_OUT) $(SBOM_SPDX_OUT)"

include tools/sbom/build/sbom.mk

## Per-HAL SBOM
# Emits a standalone SBOM whose component is the HAL layer for the selected
# TARGET (hal/hal.c, hal/$(TARGET).c, and any target flash/uart/board drivers),
# separate from the full bootloader SBOM. Uses the same build config (CFLAGS)
# so the captured macros match the real build. Run once per TARGET.
SBOM_HAL_NAME:=wolfboot-hal-$(TARGET)
SBOM_HAL_SRCS=$(filter hal/%,$(patsubst ./%,%,$(wildcard $(patsubst %.o,%.c,$(OBJS)) $(patsubst %.o,%.S,$(OBJS)))))
SBOM_HAL_CFLAGS=$(CFLAGS)
SBOM_HAL_SETTINGS_H:=$(SBOM_SETTINGS_H)
SBOM_HAL_INCLUDE_DIRS:=$(SBOM_INCLUDE_DIRS)
SBOM_HAL_PREREQS:=$(SBOM_PREREQS)
SBOM_HAL_VERSION:=$(WOLFBOOT_VERSION)
SBOM_HAL_LICENSE_FILE:=$(WOLFBOOT_ROOT)/LICENSE
# Same sources, same grant: without this the HAL SBOM would infer
# GPL-3.0-only and contradict the bootloader SBOM built from the same tree.
SBOM_HAL_LICENSE_OVERRIDE:=$(SBOM_LICENSE_OVERRIDE)
SBOM_HAL_CDX_OUT:=wolfboot-hal-$(TARGET)-$(WOLFBOOT_VERSION).cdx.json
SBOM_HAL_SPDX_OUT:=wolfboot-hal-$(TARGET)-$(WOLFBOOT_VERSION).spdx.json
SBOM_HAL_GEN:=$(SBOM_GEN)
$(eval $(call wolfglass_sbom_rule,sbom-hal,SBOM_HAL_))

FORCE:

.PHONY: FORCE clean keytool_check squashelf_check sbom
.PHONY: FORCE clean keytool_check squashelf_check sbom sbom-hal sbom-check-sources
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,31 @@ make sbom TARGET=<target> SIGN=<alg> HASH=<alg>

`TARGET`, `SIGN`, and `HASH` must match your wolfBoot build configuration (same
as a normal `make` invocation), because the SBOM's source set and artifact hash
are configuration-specific. `gen-sbom` lives in the `lib/wolfssl` submodule and
is used automatically; override with `GEN_SBOM=/path/to/wolfssl/scripts/gen-sbom`
if you keep wolfssl elsewhere.
are configuration-specific. `gen-sbom` is part of wolfSSL. The build uses the
copy in the `lib/wolfssl` submodule. If the pinned revision does not include it,
give the path with `GEN_SBOM=/path/to/wolfssl/scripts/gen-sbom`.

The same SBOM engine is available from every wolfBoot build system, so you get
an identical CycloneDX 1.6 / SPDX 2.3 document however you build:

| Build system / artifact | How to generate the SBOM |
| --- | --- |
| Make / arch.mk / vendor SDKs | `make sbom TARGET=<target> SIGN=<alg>` |
| CMake (and Pico SDK) | `cmake --build <dir> --target sbom` |
| IAR Embedded Workbench | `tools/scripts/ide-sbom/iar_sbom.py IDE/IAR/wolfboot.ewp` |
| TI CCS / MPLAB X / Renesas / Xilinx | `tools/scripts/ide-sbom/route_through_sbom.sh --config <cfg> ...` |
| Any IDE with a compilation database | `tools/scripts/ide-sbom/compdb_sbom.py compile_commands.json` |
| Per-HAL component | `make sbom-hal TARGET=<target>` |
| Zephyr TEE/PSA module | `tools/scripts/ide-sbom/zephyr_sbom.py` |

Output files are written to the build directory as
`wolfboot-<version>.cdx.json` (CycloneDX 1.6) and `wolfboot-<version>.spdx.json`
(SPDX 2.3 JSON), where `<version>` is read from `include/wolfboot/version.h`.
`wolfboot-<target>-<sign>-<hash>-<version>.cdx.json` (CycloneDX 1.6) and
`wolfboot-<target>-<sign>-<hash>-<version>.spdx.json` (SPDX 2.3 JSON), where
`<version>` is read from `include/wolfboot/version.h`. Each configuration is a
different image, so each one gets its own document.

For CRA guidance and worked SBOM examples, see the
See [docs/SBOM.md](./docs/SBOM.md) for the full per-build-system guide. For CRA
guidance and worked SBOM examples, see the
[wolfSSL CRA Kit](https://github.com/wolfSSL/wolfssl-examples/tree/master/cra-kit).

## Troubleshooting
Expand Down
116 changes: 116 additions & 0 deletions cmake/sbom.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# cmake/sbom.cmake - wolfBoot wrapper around the vendored wolfGlass CMake helper.

if(NOT DEFINED WOLFBOOT_ROOT)
set(WOLFBOOT_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
endif()

include(${WOLFBOOT_ROOT}/tools/sbom/build/sbom.cmake)

file(STRINGS ${WOLFBOOT_ROOT}/include/wolfboot/version.h _wolfboot_ver_line
REGEX "LIBWOLFBOOT_VERSION_STRING")
string(REGEX REPLACE ".*LIBWOLFBOOT_VERSION_STRING[ \t]+\"([^\"]*)\".*"
"\\1" _wolfboot_sbom_version "${_wolfboot_ver_line}")
if(_wolfboot_sbom_version STREQUAL "")
message(FATAL_ERROR "sbom: could not read LIBWOLFBOOT_VERSION_STRING")
endif()

set(_sbom_targets wolfboot wolfboothal)
if(TARGET public_key)
list(APPEND _sbom_targets public_key)
endif()
if(DEFINED WOLFSSL_TGT AND TARGET ${WOLFSSL_TGT})
list(APPEND _sbom_targets ${WOLFSSL_TGT})
endif()

set(_sbom_defs ${WOLFBOOT_DEFS} ${WOLFBOOT_DEFS_PUBLIC} ${USER_SETTINGS} ${SIGN_OPTIONS})

# wolfBoot's wolfCrypt configuration is derived, not literal: user_settings.h
# turns WOLFBOOT_SIGN_ECC256 into HAVE_ECC and the rest, and gates
# WOLFCRYPT_ONLY. Capturing the -D set alone would record none of it, which is
# how this route used to describe a different configuration than the Makefile
# for the same bootloader. Mirrors SBOM_SETTINGS_H / SBOM_INCLUDE_DIRS there.
set(_sbom_settings_h ${WOLFBOOT_ROOT}/lib/wolfssl/wolfssl/wolfcrypt/settings.h)
set(_sbom_include_dirs ${WOLFBOOT_ROOT}/include ${WOLFBOOT_ROOT}/lib/wolfssl)

# The product description below must stay in step with the SBOM_* block of the
# Makefile. Both routes describe the same bootloader, so a customer must not
# get a different document depending on which build system they generated from.

# Coat: wolfCrypt sources stay in the source-set hash, and wolfcrypt is
# declared as a component nested inside wolfssl, which is the release it ships
# in and the only one of the pair NVD maps advisories to.
if(NOT DEFINED SBOM_DEP_WOLFSSL)
set(SBOM_DEP_WOLFSSL yes)
endif()
if(NOT DEFINED SBOM_DEP_WOLFCRYPT)
set(SBOM_DEP_WOLFCRYPT yes)
endif()

# A cross build has no pkg-config for the submodule, so the version has to come
# from the header. Without it the dependency component carries no version, and
# therefore no PURL and no CPE for a scanner to match.
if(NOT DEFINED SBOM_WOLFSSL_VERSION OR SBOM_WOLFSSL_VERSION STREQUAL "")
set(_wolfssl_ver_header ${WOLFBOOT_ROOT}/lib/wolfssl/wolfssl/version.h)
if(EXISTS ${_wolfssl_ver_header})
file(STRINGS ${_wolfssl_ver_header} _wolfssl_ver_line
REGEX "LIBWOLFSSL_VERSION_STRING")
list(GET _wolfssl_ver_line 0 _wolfssl_ver_line)
string(REGEX REPLACE ".*LIBWOLFSSL_VERSION_STRING[ \t]+\"([^\"]*)\".*"
"\\1" SBOM_WOLFSSL_VERSION "${_wolfssl_ver_line}")
endif()
endif()

# Mirrors SBOM_CONFIG_TAG in the Makefile: one version of wolfBoot covers about
# 100 configurations, and each is a different image, so the document is named
# after the configuration rather than the version alone.
if(DEFINED WOLFBOOT_TARGET AND NOT WOLFBOOT_TARGET STREQUAL "")
set(_sbom_config_tag "${WOLFBOOT_TARGET}")
else()
set(_sbom_config_tag "notarget")
endif()
if(SIGN)
string(APPEND _sbom_config_tag "-${SIGN}")
endif()
if(HASH)
string(APPEND _sbom_config_tag "-${HASH}")
endif()

set(_sbom_args
NAME wolfboot
VERSION_FILE ${WOLFBOOT_ROOT}/include/wolfboot/version.h
VERSION_MACRO LIBWOLFBOOT_VERSION_STRING
TARGETS ${_sbom_targets}
DEFS ${_sbom_defs}
SETTINGS_H ${_sbom_settings_h}
INCLUDE_DIRS ${_sbom_include_dirs}
LICENSE ${WOLFBOOT_ROOT}/LICENSE
ROOT ${WOLFBOOT_ROOT}
# wolfBoot is a bootloader flashed as an image, not a library linked into one.
COMPONENT_TYPE firmware
# LICENSE is the verbatim GPLv3, which says nothing about how wolfBoot
# licenses under it, so inference falls back to GPL-3.0-only and understates
# the grant. Every GPL-headered source says "either version 3 ... or (at your
# option) any later version".
LICENSE_OVERRIDE GPL-3.0-or-later
DEP_WOLFSSL ${SBOM_DEP_WOLFSSL}
DEP_WOLFCRYPT ${SBOM_DEP_WOLFCRYPT}
CDX_OUT ${CMAKE_CURRENT_BINARY_DIR}/wolfboot-${_sbom_config_tag}-${_wolfboot_sbom_version}.cdx.json
SPDX_OUT ${CMAKE_CURRENT_BINARY_DIR}/wolfboot-${_sbom_config_tag}-${_wolfboot_sbom_version}.spdx.json
)

if(SBOM_WOLFSSL_VERSION AND NOT SBOM_WOLFSSL_VERSION STREQUAL "")
list(APPEND _sbom_args DEP_VERSION
wolfssl=${SBOM_WOLFSSL_VERSION}
wolfcrypt=${SBOM_WOLFSSL_VERSION})
endif()

if(DEFINED SBOM_GEN AND NOT SBOM_GEN STREQUAL "")
list(APPEND _sbom_args SBOM_GEN ${SBOM_GEN})
elseif(DEFINED GEN_SBOM AND NOT GEN_SBOM STREQUAL "")
list(APPEND _sbom_args SBOM_GEN ${GEN_SBOM})
endif()
if(DEFINED HOSTCC AND NOT HOSTCC STREQUAL "")
list(APPEND _sbom_args HOSTCC ${HOSTCC})
endif()

wolfglass_add_sbom(${_sbom_args})
Loading
Loading