@@ -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 :
0 commit comments