sbom: record the real build configuration

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>
pull/824/head
Sameeh Jubran 2026-07-30 15:07:50 +03:00 committed by Daniele Lacamera
parent c0c8cf2e4f
commit f87bc0f465
7 changed files with 298 additions and 47 deletions

View File

@ -901,8 +901,31 @@ SBOM_ROOT:=$(WOLFBOOT_ROOT)
SBOM_NAME:=wolfboot
SBOM_SRCS=$(wildcard $(patsubst %.o,%.c,$(OBJS))) $(wildcard $(patsubst %.o,%.S,$(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
# wolfCrypt sources are compiled into the image rather than linked, so they are
# listed as wolfBoot's own sources. Declaring wolfssl as a dependency component
# as well is what lets a scanner match wolfSSL advisories against this firmware.
SBOM_DEP_WOLFSSL?=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
SBOM_CDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).cdx.json
SBOM_SPDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).spdx.json
SBOM_GEN?=
@ -916,9 +939,15 @@ include tools/sbom/build/sbom.mk
# 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_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)

View File

@ -99,7 +99,32 @@ the source set and artifact hash are configuration-specific.
Useful overrides: `HOSTCC`, `SBOM_GEN`, `CRA_PYTHON`.
wolfcrypt sources are compiled directly into the wolfBoot image, so they are
listed as wolfBoot's own sources rather than as a separate component.
listed as wolfBoot's own sources rather than as a separate component. wolfSSL
is *also* recorded as a dependency component (`SBOM_DEP_WOLFSSL`, on by
default) with the submodule's version, which is what lets a scanner match
wolfSSL advisories against this firmware.
### How the configuration is captured
wolfBoot's 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. So the capture preprocesses the real
settings header with the real `-D` set, via `SBOM_SETTINGS_H` and
`SBOM_INCLUDE_DIRS`:
```
cc -dM -E $(CFLAGS -D and -I tokens) -include wolfssl/wolfcrypt/settings.h
```
Capturing only the `-D` tokens (against an empty translation unit) records
nothing that the header derives, and capturing only the header (with no `-D`
set) evaluates every `#if` against an empty configuration. Either omission
produces a well-formed SBOM describing a build nobody made — a signing
bootloader with no signature algorithm, in wolfBoot's case. Both halves are
required, and the capture aborts rather than emitting a partial dump.
`include/target.h` is generated and `#include`d by `user_settings.h`, so the
`sbom` target builds it first (`SBOM_PREREQS`).
## Route 2 — CMake (methods 56)

View File

@ -1 +1 @@
9bdf5b7133c603659ea34b9a552c0bf2a962a8c0
a54cde180e09b4158634cc3de99e1adef1ffbc9d

View File

@ -58,6 +58,11 @@ A product does not copy logic. It describes itself:
(`SBOM_VERSION`, or `SBOM_VERSION_FILE` + `SBOM_VERSION_MACRO`), then
`include tools/sbom/build/sbom.mk`. For a second target, instantiate
`$(eval $(call wolfglass_sbom_rule,<target>,<prefix>))`.
If the product's configuration lives in a `user_settings.h`, also set
`SBOM_SETTINGS_H` (and `SBOM_INCLUDE_DIRS` if that header needs paths the
CFLAGS do not already carry). `SBOM_CFLAGS` alone records the literal `-D`
set and nothing it derives, which for a gated header means the SBOM
describes a configuration nobody built.
- CMake: `include(tools/sbom/build/sbom.cmake)` and call `wolfglass_add_sbom()`
with `NAME`, `VERSION_FILE`, `VERSION_MACRO`, `TARGETS`, `DEFS`, `LICENSE`.
`SBOM_GEN` is the canonical generator override; `GEN_SBOM` remains a legacy

View File

@ -15,16 +15,25 @@
#
# Config - set one:
# SBOM_CFLAGS Build CFLAGS whose -D tokens describe the config.
# NOTE: the driver keeps ONLY -D tokens from SBOM_CFLAGS.
# -I / -include / other flags are dropped. Products whose
# config comes from -include'ing a settings header (e.g.
# wolfHSM's wh_settings.h) MUST capture with
# `$(HOSTCC) -dM -E ... -include ...` themselves and pass
# the dump via SBOM_OPTIONS_H, not SBOM_CFLAGS.
# The driver keeps the -D tokens and forwards -I/-isystem;
# all other flags are dropped.
# SBOM_OPTIONS_H A pre-expanded flat #define header (verbatim, scrubbed).
# SBOM_USER_SETTINGS A user_settings.h.
# SBOM_SOURCE_ONLY = 1 Source-inventory SBOM with no build-config macros.
#
# Config, strongly recommended alongside SBOM_CFLAGS:
# SBOM_SETTINGS_H Settings header -include'd during the capture, so the
# macros it derives from the -D set are recorded too.
# Any product whose configuration lives in a
# user_settings.h MUST set this. Without it the capture
# reads an empty translation unit and records only the
# literal -D list, which for a gated header (wolfBoot's
# user_settings.h derives HAVE_ECC from
# WOLFBOOT_SIGN_ECC256) silently omits the entire
# derived configuration.
# SBOM_INCLUDE_DIRS Extra -I paths for the capture, space-separated, so
# SBOM_SETTINGS_H can resolve the headers it pulls in.
#
# Version - set one:
# SBOM_VERSION Literal version string, OR
# SBOM_VERSION_FILE + Header to read and the macro to read from it, e.g.:
@ -32,8 +41,26 @@
# SBOM_VERSION_MACRO = LIBWOLFBOOT_VERSION_STRING
#
# Optional (defaults shown):
# SBOM_PREREQS Targets to build before the SBOM runs. Use this for
# generated headers the capture needs: wolfBoot's
# include/target.h carries the flash layout and is
# #include'd by its user_settings.h, so the capture
# cannot preprocess without it.
# SBOM_ROOT Product root. Default: current directory.
# SBOM_LICENSE_FILE License file. Default: $(SBOM_ROOT)/LICENSE.
# SBOM_COMPONENT_TYPE What kind of artifact this is: CycloneDX
# component.type, mirrored to SPDX
# primaryPackagePurpose. Default: library. A
# bootloader or a flashed image should say firmware.
# SBOM_LICENSE_OVERRIDE SPDX expression recorded instead of the one inferred
# from SBOM_LICENSE_FILE. Needed whenever that file is
# the *full* licence text rather than a licensing
# statement: the GPL text never states whether the
# project grants "or any later version", so inference
# cannot distinguish GPL-3.0-only from
# GPL-3.0-or-later and defaults to -only.
# SBOM_LICENSE_TEXT Plain-text licence embedded for a LicenseRef-* used
# in SBOM_LICENSE_OVERRIDE (required by SPDX 2.3).
# SBOM_GEN Path to gen-sbom. Default: driver auto-discovery.
# GEN_SBOM Legacy alias for SBOM_GEN.
# SBOM_NO_ARTIFACT_HASH = 1 As-built FIPS/kernel: do not re-hash.
@ -62,6 +89,15 @@
# $$name becomes $n + ame (empty single-letter Make var) after that double
# expansion; $$$$name survives as $name for the shell.
#
# That same double expansion is why SBOM_CFLAGS is referenced as
# $$($(2)CFLAGS) rather than $($(2)CFLAGS). A single $ interpolates the CFLAGS
# *value* into the rule text at eval time, and the recipe expansion then
# re-scans it. Any `$` surviving in that value is reinterpreted as a Make
# reference: wolfBoot passes -DBOOTLOADER_PARTITION_SIZE=$$(( A - B )), whose
# `$((` was read as a variable named `( A - B )`, expanded to nothing, and
# left the SBOM recording the literal value `)`. Deferring with $$( ) hands
# the arithmetic to the shell, which is what the compile recipes already do.
#
# To instantiate a second target, set another variable prefix and call:
# $(eval $(call wolfglass_sbom_rule,sbom-hal,SBOM_HAL_))
# using SBOM_HAL_NAME, SBOM_HAL_SRCS, SBOM_HAL_CFLAGS, and so on.
@ -75,7 +111,7 @@ HOSTCC ?= cc
define wolfglass_sbom_rule
.PHONY: $(1)
$(1):
$(1): $($(2)PREREQS)
@test -n "$($(2)NAME)" || { echo "ERROR: set $(2)NAME"; exit 1; }
@test -n "$(strip $($(2)SRCS))$($(2)LIB)" || \
{ echo "ERROR: set $(2)SRCS or $(2)LIB"; exit 1; }
@ -111,9 +147,14 @@ $(1):
$(if $($(2)LIB),--lib "$($(2)LIB)") \
$(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),--no-artifact-hash) \
$(if $(filter 1,$($(2)SOURCE_ONLY)),--source-only) \
$(if $($(2)CFLAGS),--cflags="$($(2)CFLAGS)") \
$(if $($(2)CFLAGS),--cflags="$$($(2)CFLAGS)") \
$(if $($(2)SETTINGS_H),--settings-h "$($(2)SETTINGS_H)") \
$(foreach d,$($(2)INCLUDE_DIRS),--include-dir "$(d)") \
$(if $($(2)OPTIONS_H),--options-h "$($(2)OPTIONS_H)") \
$(if $($(2)USER_SETTINGS),--user-settings "$($(2)USER_SETTINGS)") \
$(if $($(2)COMPONENT_TYPE),--component-type "$($(2)COMPONENT_TYPE)") \
$(if $($(2)LICENSE_OVERRIDE),--license-override '$($(2)LICENSE_OVERRIDE)') \
$(if $($(2)LICENSE_TEXT),--license-text "$($(2)LICENSE_TEXT)") \
$(if $($(2)VERSION),--version "$($(2)VERSION)") \
$(if $($(2)VERSION_FILE),--version-file "$($(2)VERSION_FILE)") \
$(if $($(2)VERSION_MACRO),--version-macro "$($(2)VERSION_MACRO)") \

View File

@ -20,8 +20,17 @@ from datetime import datetime, timezone
# `metadata.tools.components[].version` and SPDX `creationInfo.creators`
# fields. Reproducibility CI keys on byte-equal SBOMs across re-runs,
# so this constant must change in lockstep with the output it produces.
#
# The CLI counts as auditor-visible: '1.2' shipped in two incompatible
# shapes because dropping --dep-liboqs did not bump it, leaving vendored
# copies indistinguishable by the only identifier the SBOM records.
#
# 1.3 Valueless '#define X' records an empty value instead of '1'.
# Warns when the licence file is the full GPL text, where the
# -only/-or-later distinction cannot be inferred.
# 1.2 Dropped --dep-liboqs (unversioned; see above).
GEN_SBOM_TOOL_NAME = 'wolfssl-sbom-gen'
GEN_SBOM_VERSION = '1.2'
GEN_SBOM_VERSION = '1.3'
# Placeholder recorded in the component checksum fields when the operator
# passes --no-artifact-hash: a build (ROM image, HSM firmware, binary-only
@ -122,19 +131,6 @@ DEP_META = {
'pkgconfig': 'wolfssl',
'purl': lambda v: f'pkg:github/wolfSSL/wolfssl@v{v}',
},
# liboqs is the only PQ external dependency wolfSSL still links against
# after upstream PR #10293 collapsed the rest of the PQ surface into
# native wolfCrypt. Today, --enable-falcon strictly implies --with-liboqs
# (configure.ac enforces both directions), so a build that links liboqs
# is precisely a build that exposed Falcon.
'liboqs': {
'name': 'liboqs',
'supplier': 'Open Quantum Safe',
'license': 'MIT',
'download': 'https://github.com/open-quantum-safe/liboqs',
'pkgconfig': 'liboqs',
'purl': lambda v: f'pkg:github/open-quantum-safe/liboqs@{v}',
},
'libz': {
'name': 'zlib',
'supplier': 'Jean-loup Gailly and Mark Adler',
@ -252,6 +248,22 @@ def cdx_license_block(license_expr, license_text):
return [{'expression': license_expr}]
# Section headings that appear only in the verbatim GNU licence text, never
# in a short per-project licensing statement such as wolfSSL's LICENSING.
_FULL_LICENSE_MARKERS = (
'terms and conditions for copying, distribution and modification',
'terms and conditions',
'how to apply these terms to your new programs',
)
def _is_full_license_text(text):
"""True when the licence file is the verbatim GNU licence rather than a
statement about how the project licenses under it."""
low = text.lower()
return sum(marker in low for marker in _FULL_LICENSE_MARKERS) >= 2
def detect_license(license_file):
"""Parse LICENSING file and return an SPDX license ID.
@ -302,6 +314,24 @@ def detect_license(license_file):
if re.search(r'or\s+(?:[^,.;\n]*?\s+)?(?:any\s+)?later',
excerpt, re.IGNORECASE):
return f'GPL-{version}.0-or-later'
if _is_full_license_text(text):
# The verbatim GPL is the licence itself, not a statement about how
# this project licenses under it. Whether the project grants "or any
# later version" appears only in the per-file headers, so -only here
# is a guess that silently narrows the grant. wolfBoot ships the full
# GPLv3 as LICENSE while every source header says "either version 3
# ... or (at your option) any later version", i.e. GPL-3.0-or-later.
print(
f"WARNING: {license_file} is the full GPL text, not a licensing "
f"statement.\n"
f" It cannot say whether this project grants "
f"'or any later version', so\n"
f" GPL-{version}.0-only is assumed and may understate the "
f"grant. Confirm against\n"
f" your source headers and pass --license-override "
f"GPL-{version}.0-or-later if so\n"
f" (Make: SBOM_LICENSE_OVERRIDE).",
file=sys.stderr)
return f'GPL-{version}.0-only'
@ -799,10 +829,26 @@ def spdx_dep_package(key, dep_version_overrides=None):
return spdx_id, pkg
# CycloneDX 1.6 component.type -> SPDX 2.3 primaryPackagePurpose, so the two
# documents agree on what kind of artifact this is. A bootloader described as
# a 'library' misfiles the product for anyone triaging by artifact class, which
# under IEC 62443 is the difference between a component and the firmware it
# boots. Only the values a wolfSSL-stack product can legitimately be.
SPDX_PACKAGE_PURPOSE = {
'library': 'LIBRARY',
'firmware': 'FIRMWARE',
'application': 'APPLICATION',
'framework': 'FRAMEWORK',
'device': 'DEVICE',
'file': 'FILE',
}
def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
timestamp, year, serial, enabled_deps, build_props,
dep_version_overrides=None, hash_kind='library-binary',
hash_source='lib', srcs_basenames=None, file_entries=None):
hash_source='lib', srcs_basenames=None, file_entries=None,
component_type='library'):
bom_ref = derived_uuid(name, version, 'package')
urls = project_urls(name)
@ -813,8 +859,14 @@ def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
dep_bom_refs.append(ref)
components.append(comp)
# A valueless `#define X` is recorded with an empty value, not '1'.
# Coercing to '1' made the two indistinguishable and produced actively
# misleading entries where the macro names a quantity: wolfBoot's
# target.h emits `#define WOLFBOOT_LOAD_ADDRESS` with nothing after it
# when the target does not set a load address, which an auditor then read
# as the address literally being 1.
properties = [
{'name': f'wolfssl:build:{k}', 'value': v if v else '1'}
{'name': f'wolfssl:build:{k}', 'value': v}
for k, v in build_props
]
# Document what the SHA-256 in `hashes` represents, on every entry
@ -845,7 +897,7 @@ def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
main_component = {
'bom-ref': bom_ref,
'type': 'library',
'type': component_type,
'supplier': {'name': supplier},
'name': name,
'version': version,
@ -916,7 +968,8 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
timestamp, year, doc_ns_uuid, enabled_deps, build_props,
dep_version_overrides=None, hash_kind='library-binary',
hash_source='lib', srcs_basenames=None,
document_namespace=None, file_entries=None):
document_namespace=None, file_entries=None,
component_type='library'):
build_defines = ', '.join(k for k, _ in build_props)
# Hash-kind / source-set / bomsh-traced-binary information used to
# be stuffed into the package `comment` as `key=value` slugs, which
@ -965,6 +1018,8 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
'licenseConcluded': license_id,
'licenseDeclared': license_id,
'copyrightText': f'Copyright (C) 2006-{year} wolfSSL Inc.',
'primaryPackagePurpose': SPDX_PACKAGE_PURPOSE.get(
component_type, 'LIBRARY'),
'comment': f'Build configuration defines: {build_defines}',
'annotations': annotations,
'externalRefs': [
@ -1078,7 +1133,7 @@ def _resolve_dep_versions(enabled_deps, overrides):
value instead of each re-invoking pkg-config. Caching the result
(including None) means a later dep_version() lookup short-circuits on the
membership check rather than re-shelling to `pkg-config --modversion`, so
a default --with-libz --with-liboqs build calls pkg-config once per dep
a default --with-libz build calls pkg-config once per dep
(not once per dep per output format) and the two documents can never
disagree if pkg-config output were ever non-deterministic."""
for key in enabled_deps:
@ -1101,6 +1156,12 @@ def main():
parser.add_argument('--version', required=True, help='Package version')
parser.add_argument('--supplier', default='wolfSSL Inc.',
help='Supplier name (default: wolfSSL Inc.)')
parser.add_argument('--component-type', default='library',
choices=sorted(SPDX_PACKAGE_PURPOSE),
help='What kind of artifact this is: CycloneDX '
'component.type, mirrored to SPDX '
'primaryPackagePurpose. Use firmware for a '
'bootloader such as wolfBoot (default: library)')
parser.add_argument('--license-file', required=True,
help='Path to LICENSING file for SPDX ID detection')
parser.add_argument('--license-override', default='',
@ -1185,10 +1246,6 @@ def main():
'openssl.pc.')
parser.add_argument('--dep-libz', default='no',
help='yes if built with --with-libz')
parser.add_argument('--dep-liboqs', default='no',
help='yes if built with --with-liboqs (the package '
'wolfSSL links against; --enable-falcon implies '
'this in any legal configuration)')
parser.add_argument('--dep-version', action='append', default=[],
metavar='KEY=VERSION',
help='Override pkg-config version detection for a '
@ -1257,7 +1314,6 @@ def main():
('wolfssl', args.dep_wolfssl),
('openssl', args.dep_openssl),
('libz', args.dep_libz),
('liboqs', args.dep_liboqs),
]
if flag.lower() == 'yes'
]
@ -1380,6 +1436,7 @@ def main():
hash_kind=hash_kind, hash_source=hash_source,
srcs_basenames=srcs_basenames,
file_entries=file_entries,
component_type=args.component_type,
)
spdx = generate_spdx(
args.name, args.version, args.supplier,
@ -1390,6 +1447,7 @@ def main():
srcs_basenames=srcs_basenames,
document_namespace=(args.document_namespace or None),
file_entries=file_entries,
component_type=args.component_type,
)
try:

View File

@ -18,7 +18,10 @@ The driver covers every product tier:
Config (choose one):
--cflags "..." Raw CFLAGS. -D tokens are expanded through the host
compiler and scrubbed of absolute paths.
compiler and scrubbed of absolute paths. Pair with
--settings-h so the capture also sees the header those
-D tokens are interpreted by; without it the dump is the
-D list alone and every derived macro is missing.
--options-h PATH A pre-expanded flat #define header, used verbatim (scrubbed).
--user-settings P A user_settings.h. Passed to the generator, which captures it.
--source-only No build-config macros (e.g. a Kconfig-driven build).
@ -119,22 +122,81 @@ def gen_sbom_supports(python, gen_sbom, flag):
return flag in (res.stdout + res.stderr)
def capture_macros(hostcc, cflags):
"""Expand the -D tokens of CFLAGS through the host compiler's -dM -E.
def split_cflags(cflags):
"""Split a CFLAGS string on whitespace.
Only tokens that start with ``-D`` are kept. ``-I``, ``-include``, and
every other flag are dropped. Products whose configuration is expressed
by ``-include``'ing a settings header must capture that header themselves
(``hostcc -dM -E ... -include ...``) and pass the dump via ``--options-h``.
Deliberately a plain split rather than shlex: --cflags arrives from a Make
recipe, so the shell has already removed one layer of quoting. Re-lexing it
as shell words would strip a second layer, and shlex's POSIX escape
handling would eat the backslashes out of Windows paths --
-DPICO_SDK_PATH=C:\\Users\\ci\\sdk becomes C:Userscisdk, which is both
wrong and no longer matched by the absolute-path scrub, so a mangled host
path would leak into the SBOM.
"""
defs = [t for t in cflags.split() if t.startswith("-D")]
cmd = [hostcc, "-dM", "-E", "-DWOLFSSL_USER_SETTINGS", *defs,
"-x", "c", os.devnull]
return cflags.split()
def capture_macros(hostcc, cflags, settings_h="", include_dirs=()):
"""Expand a build configuration through the host compiler's -dM -E.
The capture must see BOTH inputs that determine the configuration:
* the ``-D`` tokens from CFLAGS, which select features
(``-DWOLFBOOT_SIGN_ECC256``), and
* the settings header those tokens are interpreted by, reached via
``-include``.
Feeding only one of them yields a confident, well-formed, wrong document.
With no ``-include``, the compiler reads an empty translation unit and the
dump is just the ``-D`` list plus compiler built-ins, so every macro the
settings header derives is missing. With no ``-D`` tokens, every ``#if``
in that header is evaluated against an empty configuration and the dump
describes a product nobody built -- wolfBoot's user_settings.h gates
HAVE_ECC on WOLFBOOT_SIGN_ECC256, so a capture missing the latter silently
reports a secure bootloader with no signature algorithm.
``-I`` and ``-isystem`` from CFLAGS are forwarded so the ``-include``
header can resolve the product headers it pulls in (user_settings.h,
target.h). Every other flag is dropped: they describe code generation,
not configuration, and cross-toolchain flags would break the host cc.
"""
toks = split_cflags(cflags)
defs, incs = [], []
i = 0
while i < len(toks):
t = toks[i]
if t.startswith("-D"):
defs.append(t)
elif t in ("-I", "-isystem", "-include") and i + 1 < len(toks):
# Separated form: keep -I/-isystem, but never a second -include
# (the settings header below is the only one we want).
if t != "-include":
incs += [t, toks[i + 1]]
i += 1
elif t.startswith("-I"):
incs.append(t)
i += 1
for d in include_dirs:
incs += ["-I", d]
cmd = [hostcc, "-dM", "-E", "-DWOLFSSL_USER_SETTINGS", *incs, *defs]
if settings_h:
cmd += ["-include", settings_h]
cmd += ["-x", "c", os.devnull]
try:
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
except (OSError, subprocess.CalledProcessError):
except OSError:
sys.exit(f"ERROR: '{hostcc} -dM -E' failed; install a host C compiler "
f"or set --hostcc.")
except subprocess.CalledProcessError as e:
detail = (e.stderr or "").strip()
hint = ""
if settings_h:
hint = (f"\n The capture includes '{settings_h}'. Check that "
f"--include-dir covers the headers it pulls in.")
sys.exit(f"ERROR: '{hostcc} -dM -E' failed while capturing the build "
f"configuration.{hint}\n{detail}")
return res.stdout
@ -176,6 +238,16 @@ def main():
grp = ap.add_mutually_exclusive_group()
grp.add_argument("--cflags", default="",
help="Build CFLAGS; -D tokens expanded through the host cc.")
ap.add_argument("--settings-h", default="",
help="Settings header -include'd during the --cflags "
"capture, so macros the header derives from the -D "
"set are recorded. Required for any product whose "
"configuration lives in a user_settings.h.")
ap.add_argument("--include-dir", action="append", default=[],
metavar="DIR",
help="Extra -I path for the --cflags capture (repeatable). "
"-I/-isystem already present in CFLAGS are forwarded "
"automatically.")
grp.add_argument("--options-h", default="",
help="A pre-expanded flat #define header, used verbatim.")
grp.add_argument("--user-settings", default="",
@ -189,6 +261,11 @@ def main():
ap.add_argument("--version-file", default="")
ap.add_argument("--version-macro", default="")
ap.add_argument("--supplier", default="wolfSSL Inc.")
ap.add_argument("--component-type", default="",
help="CycloneDX component.type, mirrored to SPDX "
"primaryPackagePurpose. Use firmware for a "
"bootloader. Passed through only if the generator "
"supports it.")
ap.add_argument("--license-file", default="")
ap.add_argument("--license-override", default="")
ap.add_argument("--license-text", default="")
@ -263,6 +340,12 @@ def main():
cmd += ["--no-artifact-hash"]
# Config.
if args.settings_h and not args.cflags:
# --settings-h only feeds the --cflags capture. Ignoring it in
# silence is how a product ends up believing it recorded a
# configuration it never captured.
print("WARNING: --settings-h is only used with --cflags; "
"ignoring it here.", file=sys.stderr)
if args.user_settings:
if not os.path.isfile(args.user_settings):
sys.exit(f"ERROR: --user-settings '{args.user_settings}' "
@ -277,7 +360,13 @@ def main():
with open(args.options_h, encoding="utf-8", errors="replace") as f:
defines_text = f.read()
elif args.cflags:
defines_text = capture_macros(args.hostcc, args.cflags)
if args.settings_h and not os.path.isfile(args.settings_h):
sys.exit(f"ERROR: --settings-h '{args.settings_h}' "
f"does not exist.")
defines_text = capture_macros(
args.hostcc, args.cflags,
settings_h=args.settings_h,
include_dirs=args.include_dir)
else:
sys.exit("ERROR: pass one of --cflags, --options-h, "
"--user-settings, or --source-only.")
@ -296,6 +385,10 @@ def main():
if args.license_text:
cmd += ["--license-text", args.license_text]
if args.component_type and gen_sbom_supports(
args.python, gen_sbom, "--component-type"):
cmd += ["--component-type", args.component_type]
# Dependencies, only if the generator supports them.
if args.dep_wolfssl and gen_sbom_supports(args.python, gen_sbom, "--dep-wolfssl"):
cmd += ["--dep-wolfssl", args.dep_wolfssl]