Skip to content

Commit 9e6cdb3

Browse files
committed
feat(sbom): standalone gen-sbom for embedded / RTOS builds
Adds --user-settings (pcpp), --srcs (Merkle/OmniBOR), --dep-version, noise filter with NO_/USE_ carve-out, and pcpp #error fail-fast so embedded customers can produce a CRA SBOM without autoconf or .a. Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
1 parent 2fff508 commit 9e6cdb3

7 files changed

Lines changed: 1877 additions & 59 deletions

File tree

.github/workflows/sbom.yml

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,308 @@ jobs:
3030
- name: Unit tests
3131
run: python3 -W error::ResourceWarning -m unittest scripts/test_gen_sbom.py -v
3232

33+
# Tier 2 (standalone) - the embedded entry point: gen-sbom invoked
34+
# directly without autotools, against a real wolfSSL user_settings.h
35+
# plus a representative source set. Mirrors how a Keil / IAR /
36+
# STM32CubeIDE / ESP-IDF / Zephyr customer would call it from a
37+
# post-build step. Without this job the standalone path would only
38+
# be exercised by hand and a regression in --user-settings,
39+
# --srcs, or --dep-version handling would silently land.
40+
standalone:
41+
name: SBOM standalone (no autotools)
42+
if: github.repository_owner == 'wolfssl'
43+
runs-on: ubuntu-24.04
44+
needs: unit
45+
timeout-minutes: 10
46+
steps:
47+
- uses: actions/checkout@v4
48+
49+
- name: Install standalone-path deps
50+
# pcpp is the in-Python C preprocessor that lets gen-sbom walk
51+
# settings.h + user_settings.h with no compiler invocation.
52+
# spdx-tools is for the post-generation validation step.
53+
run: |
54+
python3 -m pip install --user --upgrade pip
55+
python3 -m pip install --user pcpp 'spdx-tools==0.8.*'
56+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
57+
58+
- name: Generate SBOM via standalone Python entry point
59+
# Uses IDE/GCC-ARM/Header/user_settings.h as the fixture - it
60+
# is a real, comprehensive embedded user_settings.h shipping in
61+
# the tree, so any CI failure here represents a regression a
62+
# real customer would hit. Source set is a small but
63+
# representative slice of wolfcrypt that does not depend on
64+
# any pre-build code generation.
65+
#
66+
# The two `NO_*_H` predefines exercise the noise-filter
67+
# `_CONFIG_H_TOKENS` carve-out end-to-end: a NETOS / Telit /
68+
# similar RTOS profile sets these in user_settings.h to disable
69+
# stdlib header inclusion, and an over-aggressive header-guard
70+
# filter would silently drop them from the SBOM (see the
71+
# corresponding row in `required` below).
72+
run: |
73+
mkdir -p /tmp/standalone
74+
SOURCE_DATE_EPOCH=1700000000 \
75+
python3 scripts/gen-sbom \
76+
--name wolfssl --version 5.9.1 \
77+
--license-file LICENSING \
78+
--user-settings wolfssl/wolfcrypt/settings.h \
79+
--user-settings-include . \
80+
--user-settings-include IDE/GCC-ARM/Header \
81+
--user-settings-define WOLFSSL_USER_SETTINGS \
82+
--user-settings-define NO_STDINT_H \
83+
--user-settings-define WOLFSSL_NO_ASSERT_H \
84+
--srcs wolfcrypt/src/aes.c \
85+
wolfcrypt/src/sha.c \
86+
wolfcrypt/src/sha256.c \
87+
wolfcrypt/src/dh.c \
88+
--cdx-out /tmp/standalone/wolfssl.cdx.json \
89+
--spdx-out /tmp/standalone/wolfssl.spdx.json
90+
91+
- name: Standalone SPDX validates per pyspdxtools
92+
# Same validator the autotools `make sbom` recipe runs. If
93+
# the embedded path produces an SBOM autotools' validator
94+
# rejects, our portability claim is false.
95+
run: pyspdxtools --infile /tmp/standalone/wolfssl.spdx.json
96+
97+
- name: Standalone SBOM advertises source-merkle hash semantics
98+
# The auditor-facing contract: the standalone SBOM must say
99+
# "this checksum is over a source set, not a library binary",
100+
# and must list which sources fed the hash. Without these
101+
# properties the SHA-256 in `hashes` is ambiguous to anyone
102+
# reviewing the SBOM.
103+
#
104+
# The build-property assertion is a pinned set rather than a
105+
# `len > N` smoke for two reasons:
106+
# 1. A noise-filter regression that drops 80% of the wolfSSL
107+
# config flags but keeps 21 unrelated names would still
108+
# pass a length check, and silently ship an SBOM that
109+
# misrepresents the build to a CRA reviewer.
110+
# 2. The pinned set covers three distinct filter paths:
111+
# - regular config (no `_H` suffix): SINGLE_THREADED,
112+
# WOLFSSL_USER_SETTINGS, USE_FAST_MATH, NO_FILESYSTEM,
113+
# WOLFSSL_SMALL_STACK, NO_DEV_RANDOM
114+
# - `_H`-suffix carve-out via `--user-settings-define`:
115+
# NO_STDINT_H, WOLFSSL_NO_ASSERT_H. These are the
116+
# regression sentinels for the bug fixed in this PR
117+
# (header-guard filter dropping NETOS/Telit-style
118+
# stdlib disablement flags); a regression of
119+
# `_CONFIG_H_TOKENS` in gen-sbom would surface here.
120+
run: |
121+
python3 - <<'PY'
122+
import json
123+
with open('/tmp/standalone/wolfssl.cdx.json') as f:
124+
cdx = json.load(f)
125+
props = {p['name']: p['value']
126+
for p in cdx['metadata']['component']['properties']}
127+
assert props.get('wolfssl:sbom:hash-kind') == 'source-merkle-omnibor', \
128+
props
129+
srcs = props['wolfssl:sbom:source-set'].split(',')
130+
assert sorted(srcs) == ['aes.c', 'dh.c', 'sha.c', 'sha256.c'], srcs
131+
132+
build_prop_names = {
133+
k.split(':', 2)[-1]
134+
for k in props if k.startswith('wolfssl:build:')
135+
}
136+
# Regular wolfSSL config flags (no `_H` suffix). Each is set
137+
# by the GCC-ARM/Header user_settings.h fixture or by the
138+
# --user-settings-define WOLFSSL_USER_SETTINGS predefine.
139+
required_regular = {
140+
'WOLFSSL_USER_SETTINGS', # the gate the customer set in CFLAGS
141+
'SINGLE_THREADED', # IDE/GCC-ARM/Header/user_settings.h
142+
'USE_FAST_MATH', # IDE/GCC-ARM/Header/user_settings.h
143+
'NO_FILESYSTEM', # IDE/GCC-ARM/Header/user_settings.h
144+
'WOLFSSL_SMALL_STACK', # IDE/GCC-ARM/Header/user_settings.h
145+
'NO_DEV_RANDOM', # IDE/GCC-ARM/Header/user_settings.h
146+
}
147+
# `_H`-suffix carve-out sentinels - injected via
148+
# --user-settings-define above so a regression of
149+
# `_CONFIG_H_TOKENS` in gen-sbom (i.e. the bug that this PR
150+
# fixes) blows up CI rather than only the unit tests.
151+
required_h_carveout = {
152+
'NO_STDINT_H', # NETOS / Telit stdlib disablement
153+
'WOLFSSL_NO_ASSERT_H', # gates types.h:2132
154+
}
155+
required = required_regular | required_h_carveout
156+
missing = required - build_prop_names
157+
assert not missing, (
158+
f'pinned wolfSSL config flags missing from SBOM '
159+
f'(pcpp + noise filter regression?): {missing}\n'
160+
f' - regular missing : {required_regular - build_prop_names}\n'
161+
f' - _H carve-out missing: {required_h_carveout - build_prop_names}\n'
162+
f'present subset: {build_prop_names & required}'
163+
)
164+
# No host-leak / Apple TargetConditionals / __* internals.
165+
import re
166+
forbidden = [n for n in build_prop_names
167+
if re.match(r'(?:__|_[A-Z]|TARGET_OS_|TARGET_IPHONE_)',
168+
n)]
169+
assert not forbidden, (
170+
f'host-leak macros present in SBOM (noise filter '
171+
f'regression?): {forbidden[:10]}'
172+
)
173+
print(f'standalone SBOM ok: {len(build_prop_names)} build props '
174+
f'(all {len(required_regular)} regular + '
175+
f'{len(required_h_carveout)} carve-out flags present, '
176+
f'no host-leak names), {len(srcs)} source files')
177+
PY
178+
179+
- name: Reproducibility - two standalone runs are byte-identical
180+
# The deterministic UUID + SOURCE_DATE_EPOCH machinery applies
181+
# to both entry points; this guards against a future change
182+
# accidentally introducing wallclock or random data into the
183+
# standalone path. Predefines must match the generate step
184+
# exactly; any drift here would diff against the original run.
185+
run: |
186+
mkdir -p /tmp/standalone-r2
187+
SOURCE_DATE_EPOCH=1700000000 \
188+
python3 scripts/gen-sbom \
189+
--name wolfssl --version 5.9.1 \
190+
--license-file LICENSING \
191+
--user-settings wolfssl/wolfcrypt/settings.h \
192+
--user-settings-include . \
193+
--user-settings-include IDE/GCC-ARM/Header \
194+
--user-settings-define WOLFSSL_USER_SETTINGS \
195+
--user-settings-define NO_STDINT_H \
196+
--user-settings-define WOLFSSL_NO_ASSERT_H \
197+
--srcs wolfcrypt/src/aes.c \
198+
wolfcrypt/src/sha.c \
199+
wolfcrypt/src/sha256.c \
200+
wolfcrypt/src/dh.c \
201+
--cdx-out /tmp/standalone-r2/wolfssl.cdx.json \
202+
--spdx-out /tmp/standalone-r2/wolfssl.spdx.json
203+
diff /tmp/standalone/wolfssl.cdx.json \
204+
/tmp/standalone-r2/wolfssl.cdx.json
205+
diff /tmp/standalone/wolfssl.spdx.json \
206+
/tmp/standalone-r2/wolfssl.spdx.json
207+
208+
- name: --dep-version override (no pkg-config needed)
209+
# The whole point of --dep-version is to let cross-compile /
210+
# baremetal hosts emit a dep version when pkg-config is
211+
# unavailable for the target. Asserts the value lands in the
212+
# SBOM dep entry instead of NOASSERTION.
213+
run: |
214+
mkdir -p /tmp/standalone-deps
215+
SOURCE_DATE_EPOCH=1700000000 \
216+
python3 scripts/gen-sbom \
217+
--name wolfssl --version 5.9.1 \
218+
--license-file LICENSING \
219+
--user-settings wolfssl/wolfcrypt/settings.h \
220+
--user-settings-include . \
221+
--user-settings-include IDE/GCC-ARM/Header \
222+
--user-settings-define WOLFSSL_USER_SETTINGS \
223+
--srcs wolfcrypt/src/aes.c wolfcrypt/src/sha.c \
224+
--dep-libz yes \
225+
--dep-version libz=1.3.1 \
226+
--cdx-out /tmp/standalone-deps/wolfssl.cdx.json \
227+
--spdx-out /tmp/standalone-deps/wolfssl.spdx.json
228+
python3 - <<'PY'
229+
import json
230+
with open('/tmp/standalone-deps/wolfssl.spdx.json') as f:
231+
d = json.load(f)
232+
deps = {p['name']: p for p in d['packages'] if p['name'] != 'wolfssl'}
233+
assert 'zlib' in deps, list(deps)
234+
assert deps['zlib']['versionInfo'] == '1.3.1', deps['zlib']
235+
print('--dep-version override ok: zlib@1.3.1')
236+
PY
237+
238+
- name: --options-h escape hatch ($CC -dM -E, no pcpp)
239+
# The doc/SBOM.md § 1.5 escape hatch for toolchains that cannot
240+
# install pcpp (older Keil / IAR sites with restricted pip
241+
# access): pre-process settings.h with the system compiler's
242+
# `-dM -E` macro-dump mode and feed the resulting flat #define
243+
# list to gen-sbom via --options-h. This step proves the path
244+
# actually works end-to-end and that the noise filter scrubs
245+
# the host-leak macros (__VERSION__, __SSE2__, TARGET_OS_*,
246+
# ...) that `gcc -dM -E` always emits alongside the wolfSSL
247+
# config.
248+
run: |
249+
mkdir -p /tmp/standalone-dme
250+
# Same effective build the pcpp step covered above; the only
251+
# difference is the macro-extraction mechanism. The two
252+
# `-D NO_*_H` predefines mirror the pcpp step and pin the
253+
# `_CONFIG_H_TOKENS` carve-out on the no-pcpp path too.
254+
gcc -dM -E \
255+
-I . -I IDE/GCC-ARM/Header \
256+
-DWOLFSSL_USER_SETTINGS \
257+
-DNO_STDINT_H \
258+
-DWOLFSSL_NO_ASSERT_H \
259+
-include wolfssl/wolfcrypt/settings.h \
260+
-x c /dev/null > /tmp/standalone-dme/options.h
261+
262+
# Defensive: the value of this whole step is that the noise
263+
# filter scrubs `gcc -dM -E`'s host-leak macros. If a future
264+
# GCC / runner image happened to emit no `__*` defines, the
265+
# `forbidden` assertion below would pass vacuously even with
266+
# the noise filter disabled. Confirm the raw dump actually
267+
# contains plenty of host-leak names, otherwise this step
268+
# is not actually testing what it claims to test.
269+
raw_underscores=$(grep -cE '^#define[[:space:]]+(__|_[A-Z])' \
270+
/tmp/standalone-dme/options.h || true)
271+
echo "raw -dM -E dump has $raw_underscores compiler-reserved defines"
272+
test "$raw_underscores" -ge 50 || {
273+
echo "ERROR: --options-h CI step is not exercising the noise"
274+
echo " filter (raw dump has only $raw_underscores"
275+
echo " compiler-reserved defines; expected >= 50)."
276+
exit 1
277+
}
278+
279+
SOURCE_DATE_EPOCH=1700000000 \
280+
python3 scripts/gen-sbom \
281+
--name wolfssl --version 5.9.1 \
282+
--license-file LICENSING \
283+
--options-h /tmp/standalone-dme/options.h \
284+
--srcs wolfcrypt/src/aes.c \
285+
wolfcrypt/src/sha.c \
286+
wolfcrypt/src/sha256.c \
287+
wolfcrypt/src/dh.c \
288+
--cdx-out /tmp/standalone-dme/wolfssl.cdx.json \
289+
--spdx-out /tmp/standalone-dme/wolfssl.spdx.json
290+
291+
# Validate + assert the same wolfSSL config flags reach the
292+
# SBOM via the no-pcpp path that the pcpp path produced
293+
# above. If the noise filter regresses, this step is what
294+
# surfaces it (the raw `gcc -dM -E` dump contains hundreds
295+
# of host-leak macros and only a handful of wolfSSL ones).
296+
pyspdxtools --infile /tmp/standalone-dme/wolfssl.spdx.json
297+
python3 - <<'PY'
298+
import json, re
299+
with open('/tmp/standalone-dme/wolfssl.cdx.json') as f:
300+
cdx = json.load(f)
301+
props = {p['name']: p['value']
302+
for p in cdx['metadata']['component']['properties']}
303+
build_prop_names = {
304+
k.split(':', 2)[-1]
305+
for k in props if k.startswith('wolfssl:build:')
306+
}
307+
required_regular = {
308+
'WOLFSSL_USER_SETTINGS', 'SINGLE_THREADED', 'USE_FAST_MATH',
309+
'NO_FILESYSTEM', 'WOLFSSL_SMALL_STACK', 'NO_DEV_RANDOM',
310+
}
311+
required_h_carveout = {
312+
'NO_STDINT_H', 'WOLFSSL_NO_ASSERT_H',
313+
}
314+
required = required_regular | required_h_carveout
315+
missing = required - build_prop_names
316+
assert not missing, (
317+
f'--options-h path lost wolfSSL config flags: {missing}\n'
318+
f' - regular missing : {required_regular - build_prop_names}\n'
319+
f' - _H carve-out missing: {required_h_carveout - build_prop_names}\n'
320+
f'present subset: {build_prop_names & required}'
321+
)
322+
forbidden = [n for n in build_prop_names
323+
if re.match(r'(?:__|_[A-Z]|TARGET_OS_|TARGET_IPHONE_)',
324+
n)]
325+
assert not forbidden, (
326+
f'host-leak macros from `gcc -dM -E` dump survived the '
327+
f'noise filter: {forbidden[:10]}'
328+
)
329+
print(f'--options-h path ok: {len(build_prop_names)} build '
330+
f'props (all {len(required_regular)} regular + '
331+
f'{len(required_h_carveout)} carve-out flags present, '
332+
f'host-leak macros filtered)')
333+
PY
334+
33335
# Tier 2 - integration: build wolfSSL, generate the SBOMs, and assert
34336
# everything an external auditor or vulnerability scanner relies on.
35337
integration:

INSTALL

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,45 @@ We also have vcpkg ports for wolftpm, wolfmqtt and curl.
317317
19. Generating an SBOM (Software Bill of Materials)
318318

319319
wolfSSL can generate a Software Bill of Materials for EU Cyber Resilience
320-
Act (CRA) compliance after a normal build and install.
320+
Act (CRA) compliance. Two entry points are supported, depending on how
321+
you build wolfSSL.
322+
323+
--- 19a. Embedded / RTOS / IDE-based builds (no autotools) ----------
324+
325+
For customers building wolfSSL from a hand-edited user_settings.h with
326+
their own Makefile, Keil MDK, IAR EWARM, STM32CubeIDE, ESP-IDF,
327+
Zephyr, or plain CMake, invoke scripts/gen-sbom directly. No
328+
./configure, no autotools.
329+
330+
Prerequisites:
331+
- python3
332+
- pcpp (pip install pcpp) # required for --user-settings
333+
- spdx-tools (pip install spdx-tools) # optional; for SPDX validation
334+
335+
Usage:
336+
337+
$ python3 wolfssl/scripts/gen-sbom \
338+
--name wolfssl --version 5.9.1 \
339+
--license-file wolfssl/LICENSING \
340+
--user-settings wolfssl/wolfssl/wolfcrypt/settings.h \
341+
--user-settings-include wolfssl \
342+
--user-settings-include path/to/your/user_settings_dir \
343+
--user-settings-define WOLFSSL_USER_SETTINGS \
344+
--srcs wolfssl/wolfcrypt/src/aes.c [...your wolfssl source list] \
345+
--cdx-out wolfssl-5.9.1.cdx.json \
346+
--spdx-out wolfssl-5.9.1.spdx.json
347+
348+
The component checksum is a deterministic OmniBOR-compatible Merkle
349+
hash over the source files you compile into your firmware, so you do
350+
not need to synthesize a separate libwolfssl.a just for SBOM purposes.
351+
352+
See doc/SBOM.md section 1 for per-toolchain recipes (Keil, IAR,
353+
STM32CubeIDE, ESP-IDF, Zephyr, CMake) and the full flag reference.
354+
355+
--- 19b. Linux / autotools builds ----------------------------------
356+
357+
For Debian, RPM, Yocto, FIPS-Ready, and other builds that already use
358+
./configure && make:
321359

322360
Prerequisites:
323361
- python3 (detected automatically by configure)
@@ -338,6 +376,10 @@ We also have vcpkg ports for wolftpm, wolfmqtt and curl.
338376
The SPDX JSON is validated by pyspdxtools before the tag-value file is
339377
written; make sbom fails if validation fails.
340378

379+
`make sbom` is a thin convenience wrapper around the same
380+
scripts/gen-sbom Python entry point that section 19a uses, with all
381+
paths resolved automatically from the autotools build tree.
382+
341383
To install the SBOM files to $(datadir)/doc/wolfssl/:
342384

343385
$ make install-sbom

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,17 @@ of the wolfSSL manual.
3737
## SBOM / CRA Compliance
3838

3939
wolfSSL provides a Software Bill of Materials (SBOM) for EU Cyber Resilience
40-
Act (CRA) compliance via `make sbom`. See `doc/SBOM.md` for details.
40+
Act (CRA) compliance via two entry points:
41+
42+
- `python3 scripts/gen-sbom …` for embedded / RTOS / IDE-based builds
43+
(Keil, IAR, STM32CubeIDE, ESP-IDF, Zephyr, plain CMake, custom Makefile)
44+
configured through a hand-edited `user_settings.h`. No autotools required.
45+
- `make sbom` for Linux server / Debian / RPM / Yocto / FIPS-Ready
46+
builds that already use `./configure && make`.
47+
48+
Both produce SPDX 2.3 + CycloneDX 1.6 JSON validated against NTIA
49+
minimum elements. See `doc/SBOM.md` for per-toolchain recipes and the
50+
full flag reference.
4151

4252
## OmniBOR / Bomsh
4353

0 commit comments

Comments
 (0)