feat: add SBOM generation for autotools and cmake

Add `sbom`, `install-sbom` and `uninstall-sbom` targets to both build
systems, producing CycloneDX and SPDX output for CRA compliance.

The recipe is the canonical scripts/sbom.am fragment from wolfSSL
(wolfSSL/wolfssl#10343), vendored byte-identical; Makefile.am only
declares the wolfMQTT-specific inputs. Feature macros come from the
generated wolfmqtt/options.h via SBOM_OPTIONS_H rather than config.h,
since wolfMQTT uses no AC_DEFINE.

Both paths stage an install to hash the real installed library, record
wolfSSL as a dependency component, validate the SPDX with pyspdxtools,
and are exercised in CI.

Requires a wolfssl source tree via WOLFSSL_DIR for scripts/gen-sbom,
plus python3 and spdx-tools on the build host.
pull/557/head
Mark Atwood 2026-08-11 12:46:31 -07:00
parent 6a94b741f1
commit 07d08d5995
7 changed files with 755 additions and 0 deletions

267
.github/workflows/sbom.yml vendored 100644
View File

@ -0,0 +1,267 @@
name: SBOM Test
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '**' ]
workflow_dispatch:
inputs:
wolfssl_ref:
description: 'wolfssl git ref that provides scripts/gen-sbom'
default: 'master'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# This workflow only reads the repo and uploads artefacts; no API writes.
permissions:
contents: read
jobs:
sbom:
name: wolfMQTT SBOM generation (linux)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout wolfmqtt
uses: actions/checkout@v4
with:
path: wolfmqtt
# wolfMQTT links wolfSSL for TLS, so its SBOM records wolfSSL as a
# dependency. wolfSSL is built + installed here so wolfMQTT has a library
# to link, and the same source tree (scripts/gen-sbom + wolfssl/version.h)
# is passed to `make sbom` via WOLFSSL_DIR -- so the recorded wolfSSL
# dependency version matches the linked one. scripts/gen-sbom lives on
# wolfssl master (wolfSSL/wolfssl#10343); the wolfssl_ref input overrides
# it for testing against a different ref.
- name: Checkout wolfssl (gen-sbom + library source)
uses: actions/checkout@v4
with:
repository: wolfSSL/wolfssl
ref: ${{ github.event.inputs.wolfssl_ref || 'master' }}
path: wolfssl
- name: Install build tooling and SBOM validator (pyspdxtools)
run: |
sudo apt-get update
sudo apt-get install -y build-essential autoconf automake libtool \
pkg-config
python3 -m pip install --user 'spdx-tools==0.8.*'
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Build and install wolfssl
working-directory: wolfssl
run: |
autoreconf -ivf
./configure --enable-all \
--prefix="$GITHUB_WORKSPACE/wolfssl-install"
make -j"$(nproc)"
make install
# gen-sbom lives in wolfssl and may not be on the checked-out ref yet (the
# wolfSSL SBOM change can land separately). Gate on its presence and on
# --dep-wolfssl so this workflow is safe against a gen-sbom that predates
# it.
- name: Detect gen-sbom availability and capabilities
id: gate
run: |
GS="$GITHUB_WORKSPACE/wolfssl/scripts/gen-sbom"
if [ ! -f "$GS" ]; then
echo "have=no" >> "$GITHUB_OUTPUT"
echo "::notice::wolfssl scripts/gen-sbom not present on this ref; skipping SBOM generation."
exit 0
fi
echo "have=yes" >> "$GITHUB_OUTPUT"
if python3 "$GS" --help 2>/dev/null | grep -q -- '--dep-wolfssl'; then
echo "dep_wolfssl=yes" >> "$GITHUB_OUTPUT"
else
echo "dep_wolfssl=no" >> "$GITHUB_OUTPUT"
echo "::notice::gen-sbom on this ref has no --dep-wolfssl; the wolfssl dependency + name-derived identity assertions will be skipped."
fi
- name: Configure and build wolfmqtt
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt
run: |
autoreconf -ivf
./configure --enable-tls \
--with-libwolfssl-prefix="$GITHUB_WORKSPACE/wolfssl-install"
make -j"$(nproc)"
- name: Generate SBOM
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt
run: make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
- name: Outputs exist and SPDX validates
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt
run: |
ls wolfmqtt-*.cdx.json wolfmqtt-*.spdx.json wolfmqtt-*.spdx
pyspdxtools --infile wolfmqtt-*.spdx.json
- name: CycloneDX identity, licence, and captured options
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt
run: |
python3 - <<'PY'
import glob, json
cdx = json.load(open(glob.glob('wolfmqtt-*.cdx.json')[0]))
assert cdx['bomFormat'] == 'CycloneDX', cdx.get('bomFormat')
assert cdx['specVersion'] == '1.6', cdx.get('specVersion')
m = cdx['metadata']['component']
assert m['name'] == 'wolfmqtt', m['name']
assert m['purl'].startswith('pkg:github/wolfSSL/wolfmqtt@'), m['purl']
# Default override must land as GPL-3.0-or-later (matches source headers).
ids = [l.get('license', {}).get('id') for l in m.get('licenses', [])]
assert 'GPL-3.0-or-later' in ids, ids
# Identity is the hashed library artifact.
assert {h['alg'] for h in m.get('hashes', [])}, 'no component hash'
# SBOM_OPTIONS_H must have been parsed: wolfMQTT stores its feature
# macros in wolfmqtt/options.h (no config.h AC_DEFINE), so the SBOM's
# build properties must be populated.
props = [p for p in m.get('properties', [])
if p.get('name', '').startswith('wolfssl:build:')]
assert props, 'no wolfssl:build:* properties captured from options.h'
print('CDX ok:', m['name'], m['purl'], ids, f'{len(props)} build props')
PY
- name: Reproducible across two runs (SOURCE_DATE_EPOCH)
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt
run: |
rm -f wolfmqtt-*.cdx.json wolfmqtt-*.spdx.json wolfmqtt-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
sha256sum wolfmqtt-*.cdx.json wolfmqtt-*.spdx.json > /tmp/a.sums
rm -f wolfmqtt-*.cdx.json wolfmqtt-*.spdx.json wolfmqtt-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
sha256sum wolfmqtt-*.cdx.json wolfmqtt-*.spdx.json > /tmp/b.sums
diff /tmp/a.sums /tmp/b.sums
- name: wolfssl recorded as a dependency
if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_wolfssl == 'yes'
working-directory: wolfmqtt
run: |
python3 - <<'PY'
import glob, json
d = json.load(open(glob.glob('wolfmqtt-*.spdx.json')[0]))
assert 'wolfssl' in {p['name'] for p in d['packages']}, \
[p['name'] for p in d['packages']]
rels = [(r['spdxElementId'], r['relationshipType'],
r['relatedSpdxElement']) for r in d['relationships']]
assert ('SPDXRef-Package-wolfmqtt', 'DEPENDS_ON',
'SPDXRef-Package-wolfssl') in rels, rels
print('wolfssl dependency ok')
PY
- name: Upload SBOM artefacts
if: always() && steps.gate.outputs.have == 'yes'
uses: actions/upload-artifact@v4
with:
name: wolfmqtt-sbom-${{ github.sha }}
path: |
wolfmqtt/wolfmqtt-*.cdx.json
wolfmqtt/wolfmqtt-*.spdx.json
wolfmqtt/wolfmqtt-*.spdx
if-no-files-found: warn
retention-days: 90
# The CMake `sbom` custom target is a separate implementation from the
# autotools recipe above; build it too so a regression in either path
# fails CI. The reproducibility and CycloneDX assertions live in the
# autotools job; the wolfssl dependency is asserted in both.
sbom-cmake:
name: wolfMQTT SBOM generation (cmake)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout wolfmqtt
uses: actions/checkout@v4
with:
path: wolfmqtt
- name: Checkout wolfssl (gen-sbom + library source)
uses: actions/checkout@v4
with:
repository: wolfSSL/wolfssl
ref: ${{ github.event.inputs.wolfssl_ref || 'master' }}
path: wolfssl
- name: Install build tooling and SBOM validator (pyspdxtools)
run: |
sudo apt-get update
sudo apt-get install -y build-essential autoconf automake libtool \
pkg-config cmake
python3 -m pip install --user 'spdx-tools==0.8.*'
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Build and install wolfssl
working-directory: wolfssl
run: |
autoreconf -ivf
./configure --enable-all \
--prefix="$GITHUB_WORKSPACE/wolfssl-install"
make -j"$(nproc)"
make install
- name: Detect gen-sbom availability
id: gate
run: |
GS="$GITHUB_WORKSPACE/wolfssl/scripts/gen-sbom"
if [ ! -f "$GS" ]; then
echo "have=no" >> "$GITHUB_OUTPUT"
echo "::notice::wolfssl scripts/gen-sbom not present on this ref; skipping CMake SBOM job."
exit 0
fi
echo "have=yes" >> "$GITHUB_OUTPUT"
if python3 "$GS" --help 2>/dev/null | grep -q -- '--dep-wolfssl'; then
echo "dep_wolfssl=yes" >> "$GITHUB_OUTPUT"
else
echo "dep_wolfssl=no" >> "$GITHUB_OUTPUT"
echo "::notice::gen-sbom on this ref has no --dep-wolfssl; the wolfssl dependency assertion will be skipped."
fi
- name: Configure and build wolfmqtt (cmake)
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt
run: |
cmake -B build \
-DWITH_WOLFSSL="$GITHUB_WORKSPACE/wolfssl-install" \
-DWOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
cmake --build build -j"$(nproc)"
- name: Generate SBOM via CMake target
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt
run: cmake --build build --target sbom
- name: Outputs exist and SPDX validates
if: steps.gate.outputs.have == 'yes'
working-directory: wolfmqtt/build
run: |
ls wolfmqtt-*.cdx.json wolfmqtt-*.spdx.json wolfmqtt-*.spdx
pyspdxtools --infile wolfmqtt-*.spdx.json
# The CMake target passes --dep-wolfssl independently of scripts/sbom.am,
# so assert it here too; file existence alone would pass on an SBOM that
# silently dropped the dependency component.
- name: wolfssl recorded as a dependency (cmake)
if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_wolfssl == 'yes'
working-directory: wolfmqtt/build
run: |
python3 - <<'PY'
import glob, json
d = json.load(open(glob.glob('wolfmqtt-*.spdx.json')[0]))
assert 'wolfssl' in {p['name'] for p in d['packages']}, \
[p['name'] for p in d['packages']]
rels = [(r['spdxElementId'], r['relationshipType'],
r['relatedSpdxElement']) for r in d['relationships']]
assert ('SPDXRef-Package-wolfmqtt', 'DEPENDS_ON',
'SPDXRef-Package-wolfssl') in rels, rels
print('cmake wolfssl dependency ok')
PY

5
.gitignore vendored
View File

@ -132,3 +132,8 @@ tests/test_broker_connect
tests/test_mqtt_sn
tests/test_mqtt_sn_client
tests/unit_tests
# SBOM output (make sbom)
wolfmqtt-*.cdx.json
wolfmqtt-*.spdx
wolfmqtt-*.spdx.json

View File

@ -418,3 +418,178 @@ message("\tMultithread: ${ENABLE_MULTITHREAD}")
message("\tCurl: ${ENABLE_CURL}")
message("\tBroker: ${WOLFMQTT_BROKER}")
message("-----------------------------------------------")
# SBOM generation target
#
# Usage:
# cmake -B build -DWOLFSSL_DIR=/path/to/wolfssl/source .
# cmake --build build
# cmake --build build --target sbom
#
# WOLFSSL_DIR must point to a wolfssl source tree containing scripts/gen-sbom
# (feat/sbom-embedded branch). wolfSSL is NOT required to be installed for
# SBOM generation only the source tree is needed to locate gen-sbom.
#
# Outputs in build directory:
# wolfmqtt-<version>.cdx.json
# wolfmqtt-<version>.spdx.json
# wolfmqtt-<version>.spdx
#
# This mirrors the autotools `make sbom` target: same package metadata, licence,
# option fingerprint and wolfSSL dependency component. The hashed artifact is
# whatever that build system actually installs, so the recorded file name and
# checksum legitimately differ between the two (autotools installs the
# SONAME-versioned library, cmake an unversioned one).
# Location of the wolfssl source tree that ships scripts/gen-sbom. A cache PATH
# (not a plain var) so it can be set on the command line with -DWOLFSSL_DIR=...
# and persists across reconfigures.
set(WOLFSSL_DIR "" CACHE PATH
"Path to wolfssl source tree containing scripts/gen-sbom (for the sbom target)")
# Derive the SBOM version from wolfmqtt/version.h, NOT from PROJECT_VERSION.
# version.h is the single source of truth shared with the autotools build
# (PACKAGE_VERSION is generated from it); project(VERSION ...) is hand-maintained
# and can drift, which would make the cmake SBOM disagree with the autotools one.
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/wolfmqtt/version.h" WOLFMQTT_VERSION_LINE
REGEX "^#define[ \t]+LIBWOLFMQTT_VERSION_STRING[ \t]+\"")
string(REGEX REPLACE "^.*\"([^\"]+)\".*$" "\\1"
WOLFMQTT_SBOM_VERSION "${WOLFMQTT_VERSION_LINE}")
set(SBOM_CDX "wolfmqtt-${WOLFMQTT_SBOM_VERSION}.cdx.json")
set(SBOM_SPDX "wolfmqtt-${WOLFMQTT_SBOM_VERSION}.spdx.json")
set(SBOM_SPDX_TV "wolfmqtt-${WOLFMQTT_SBOM_VERSION}.spdx")
# gen-sbom runs under python3; pyspdxtools validates the SPDX tag-value output.
# Both are required fail at configure time so the user fixes their environment
# before building rather than hitting a confusing failure mid-`--target sbom`.
find_program(PYTHON3_EXECUTABLE python3)
find_program(PYSPDXTOOLS_EXECUTABLE pyspdxtools)
# WOLFSSL_DIR (a wolfssl source tree with scripts/gen-sbom) is required only to
# actually build the sbom target. CI and default builds never set it, so checking
# it at configure time would abort every build. Mirror the autotools `make sbom`
# recipe: configure always succeeds; if WOLFSSL_DIR is unset, define a stub sbom
# target that fails clearly at BUILD time. An empty WOLFSSL_DIR is the common
# first-run mistake.
if(WOLFSSL_DIR STREQUAL "")
add_custom_target(sbom
COMMAND ${CMAKE_COMMAND} -E echo
"ERROR: WOLFSSL_DIR is not set. Cannot locate gen-sbom."
COMMAND ${CMAKE_COMMAND} -E echo
" Reconfigure with: cmake -B build -DWOLFSSL_DIR=/path/to/wolfssl ."
COMMAND ${CMAKE_COMMAND} -E false
COMMENT "SBOM target requires -DWOLFSSL_DIR")
return()
endif()
# WOLFSSL_DIR is set: the operator opted into SBOM generation. Validate the rest
# of the environment now so they get immediate, actionable feedback.
if(NOT EXISTS "${WOLFSSL_DIR}/scripts/gen-sbom")
message(FATAL_ERROR
"gen-sbom not found at ${WOLFSSL_DIR}/scripts/gen-sbom.\n"
" WOLFSSL_DIR must point to a wolfssl source tree (feat/sbom-embedded).")
endif()
if(NOT PYTHON3_EXECUTABLE)
message(FATAL_ERROR "python3 not found in PATH. Cannot generate SBOM.")
endif()
if(NOT PYSPDXTOOLS_EXECUTABLE)
message(FATAL_ERROR
"pyspdxtools not found in PATH. Cannot validate SBOM.\n"
" Install with: pip install spdx-tools")
endif()
# Staging dir for `cmake --install` so gen-sbom hashes the as-installed
# library without polluting the system or requiring root.
set(SBOM_STAGING "${CMAKE_BINARY_DIR}/_sbom_staging")
# Subdirectory of the staging prefix that holds the installed library. The
# install(TARGETS) rule above sends LIBRARY/ARCHIVE to lib and RUNTIME to bin,
# and a shared build on the Windows-family toolchains (MSVC, MinGW, Cygwin) is
# the only case whose artifact - the DLL - is a RUNTIME output.
# CMAKE_IMPORT_LIBRARY_SUFFIX is set exactly on those platforms. The file name
# itself comes from $<TARGET_FILE_NAME:wolfmqtt>, so .so/.dylib/.dll/.a/.lib
# are all handled without hardcoding a suffix.
if(BUILD_SHARED_LIBS AND CMAKE_IMPORT_LIBRARY_SUFFIX)
set(SBOM_ARTIFACT_DIR "bin")
else()
set(SBOM_ARTIFACT_DIR "lib")
endif()
# wolfMQTT links wolfSSL for TLS, so the SBOM records it as a dependency
# component -- the same thing SBOM_DEP_WOLFSSL = yes does in Makefile.am.
# gen-sbom only learned --dep-wolfssl in wolfSSL/wolfssl#10343, so probe for it
# the way scripts/sbom.am does: an older gen-sbom still emits a valid SBOM
# (just without the dependency) instead of dying on an unknown flag.
execute_process(
COMMAND ${PYTHON3_EXECUTABLE} ${WOLFSSL_DIR}/scripts/gen-sbom --help
OUTPUT_VARIABLE SBOM_GEN_HELP
ERROR_VARIABLE SBOM_GEN_HELP
RESULT_VARIABLE SBOM_GEN_HELP_RC)
set(SBOM_DEP_ARGS "")
if(SBOM_GEN_HELP_RC EQUAL 0 AND SBOM_GEN_HELP MATCHES "--dep-wolfssl")
list(APPEND SBOM_DEP_ARGS --dep-wolfssl yes)
# Record the version of the wolfSSL that WOLFSSL_DIR points at, matching the
# version scrape in scripts/sbom.am.
if(EXISTS "${WOLFSSL_DIR}/wolfssl/version.h")
file(STRINGS "${WOLFSSL_DIR}/wolfssl/version.h" WOLFSSL_VERSION_LINE
REGEX "^#define[ \t]+LIBWOLFSSL_VERSION_STRING[ \t]+\"")
string(REGEX REPLACE "^.*\"([^\"]+)\".*$" "\\1"
WOLFSSL_SBOM_VERSION "${WOLFSSL_VERSION_LINE}")
if(NOT WOLFSSL_SBOM_VERSION STREQUAL "")
list(APPEND SBOM_DEP_ARGS --dep-version wolfssl=${WOLFSSL_SBOM_VERSION})
endif()
endif()
else()
message(STATUS
"gen-sbom has no --dep-wolfssl support, so the SBOM will not list "
"wolfssl as a dependency component. That support is added by "
"wolfSSL/wolfssl#10343. The generated SBOM is valid either way.")
endif()
add_custom_target(sbom
# Install into the staging dir so the SBOM hashes the real installed
# artifact rather than a build-tree intermediate.
# --config is required for multi-config generators (Visual Studio, Ninja
# Multi-Config): without it the install script falls back to "Release" and
# cannot find a library built in any other configuration. It expands to an
# empty (harmless) argument under single-config generators.
COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR}
--config $<CONFIG>
--prefix ${SBOM_STAGING}/usr/local
# The generated options.h is passed to gen-sbom directly, the same input
# the autotools `make sbom` feeds it, so both build systems record an
# identical compile-time option fingerprint. (No shell redirection here:
# add_custom_target COMMANDs do not run through a shell under VERBATIM.)
COMMAND ${PYTHON3_EXECUTABLE} ${WOLFSSL_DIR}/scripts/gen-sbom
--name wolfmqtt
--version ${WOLFMQTT_SBOM_VERSION}
--supplier "wolfSSL Inc."
--license-file ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE
# Match the autotools default (SBOM_LICENSE_OVERRIDE in
# scripts/sbom.am): source headers say GPLv3 or later, which
# LICENSE-file detection alone records as GPL-3.0-only.
--license-override GPL-3.0-or-later
${SBOM_DEP_ARGS}
--options-h ${WOLFMQTT_OUTPUT_BASE}/wolfmqtt/options.h
--lib ${SBOM_STAGING}/usr/local/${SBOM_ARTIFACT_DIR}/$<TARGET_FILE_NAME:wolfmqtt>
--cdx-out ${CMAKE_BINARY_DIR}/${SBOM_CDX}
--spdx-out ${CMAKE_BINARY_DIR}/${SBOM_SPDX}
# Validate the SPDX output and emit the tag-value (.spdx) rendering.
COMMAND ${PYSPDXTOOLS_EXECUTABLE}
--infile ${CMAKE_BINARY_DIR}/${SBOM_SPDX}
--outfile ${CMAKE_BINARY_DIR}/${SBOM_SPDX_TV}
# Remove the staging tree; keep only the three SBOM files.
# (-E remove_directory, not -E rm: the latter needs CMake >= 3.17 and
# this project's minimum is 3.16.)
COMMAND ${CMAKE_COMMAND} -E remove_directory ${SBOM_STAGING}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating SBOM for wolfMQTT ${WOLFMQTT_SBOM_VERSION}"
VERBATIM)
# The library must exist before we install/hash it.
add_dependencies(sbom wolfmqtt)

View File

@ -74,6 +74,30 @@ test: check
DISTCLEANFILES+= wolfmqtt-config
# SBOM generation (CRA compliance). The recipe is shared across the wolfSSL
# stack's autotools products in scripts/sbom.am; wolfMQTT just declares what it
# is (a libwolfmqtt library that links wolfSSL for TLS) and includes it.
# WOLFSSL_DIR must point to a wolfssl source tree containing scripts/gen-sbom.
SBOM_PKGNAME = wolfmqtt
SBOM_LICENSE_FILE = $(srcdir)/LICENSE
SBOM_DEP_WOLFSSL = yes
# wolfMQTT records its feature macros in its own generated options header (it
# uses no AC_DEFINE / config.h defines), so point gen-sbom at that header rather
# than at the compiler-derived defaults.
SBOM_OPTIONS_H = $(abs_builddir)/wolfmqtt/options.h
# wolfMQTT is GPLv3-or-later (per the per-file source headers: "either version 3
# of the License, or (at your option) any later version") or commercial. Pin
# the header-accurate SPDX id here so the SBOM is correct regardless of the
# gen-sbom version's licence detection; commercial licensees can override it
# (e.g. LicenseRef-wolfSSL-Commercial).
SBOM_LICENSE_OVERRIDE ?= GPL-3.0-or-later
EXTRA_DIST += scripts/sbom.am
include scripts/sbom.am
clean-local:
-rm -rf tests/fuzz/corpus
@if test "$(abs_srcdir)" != "$(abs_builddir)"; then \

View File

@ -571,3 +571,48 @@ You can test the wolfMQTT client against public brokers supporting websockets:
* HiveMQ secure websockets
`./examples/websocket/websocket_client -h broker.hivemq.com -p8884 -t`
## SBOM / EU CRA Compliance
wolfMQTT generates a Software Bill of Materials (SBOM) in CycloneDX 1.6 and
SPDX 2.3 formats to support compliance with the EU Cyber Resilience Act (CRA).
The SBOM records the configured build options (from `wolfmqtt/options.h`),
hashes the built `libwolfmqtt` library artifact (shared or static; ELF, Mach-O,
or PE), and (with a sufficiently new `gen-sbom`) lists wolfSSL as a dependency
so vulnerability scanners can associate wolfSSL advisories with a TLS-enabled
wolfMQTT deployment. Output is reproducible: set `SOURCE_DATE_EPOCH` (or build
from a git checkout, which uses the last commit time) and repeated runs are
byte-identical.
```sh
make sbom WOLFSSL_DIR=/path/to/wolfssl
```
Requires `python3` and `pyspdxtools` (`pip install spdx-tools`). `WOLFSSL_DIR`
must point to a wolfssl source tree containing `scripts/gen-sbom` (branch
`feat/sbom-embedded`, or `master` once wolfSSL/wolfssl#10343 merges).
Output: `wolfmqtt-<version>.cdx.json`, `wolfmqtt-<version>.spdx.json`, `wolfmqtt-<version>.spdx`
Optional overrides:
- `SBOM_LICENSE_OVERRIDE` - SPDX expression to use instead of the licence
parsed from `LICENSE` (e.g. `LicenseRef-wolfSSL-Commercial` for commercial
licensees). Defaults to `GPL-3.0-or-later` (the per-file header licence).
- `SBOM_LICENSE_TEXT` - path to the licence text for any `LicenseRef-*` used in
`SBOM_LICENSE_OVERRIDE` (required by SPDX 2.3).
- `SBOM_WOLFSSL_VERSION` - version recorded for the wolfSSL dependency;
auto-detected from `WOLFSSL_DIR/wolfssl/version.h` (or wolfSSL's `pkg-config`
entry) when unset.
```sh
make install-sbom # installs to $(datadir)/doc/wolfmqtt/
make uninstall-sbom
```
Note: recording wolfSSL as a dependency and emitting wolfMQTT-specific project
URLs require the `gen-sbom` from wolfSSL/wolfssl#10343. Against an older
`gen-sbom`, `make sbom` still succeeds and produces a valid SBOM, but omits the
wolfSSL dependency entry and inherits wolfSSL's project URLs.
For further CRA guidance see [wolfssl/doc/CRA.md](https://github.com/wolfSSL/wolfssl/blob/master/doc/CRA.md).

View File

@ -571,6 +571,16 @@ AC_SUBST([AM_CPPFLAGS])
AC_SUBST([AM_CFLAGS])
AC_SUBST([AM_LDFLAGS])
# Tools used by the SBOM targets (see scripts/sbom.am `make sbom`). GIT is used
# only to derive SOURCE_DATE_EPOCH for reproducible SBOM output; all three are
# optional and the target reports a clear error when a required one is missing.
AC_PATH_PROG([PYTHON3], [python3])
AC_PATH_PROG([PYSPDXTOOLS], [pyspdxtools])
AC_PATH_PROG([GIT], [git])
AC_SUBST([PYTHON3])
AC_SUBST([PYSPDXTOOLS])
AC_SUBST([GIT])
# FINAL
AC_CONFIG_FILES([Makefile])
AC_CONFIG_FILES([wolfmqtt/version.h])

229
scripts/sbom.am 100644
View File

@ -0,0 +1,229 @@
# scripts/sbom.am - shared Automake recipe for CRA-compliant SBOM generation.
#
# One generator (gen-sbom) does the work; each product just describes itself and
# includes this fragment. It is deliberately product-agnostic: a Makefile.am
# sets a few variables (below) and does `include scripts/sbom.am` to get the
# `sbom`, `install-sbom` and `uninstall-sbom` targets.
#
# This is the canonical copy (wolfSSL repository, scripts/sbom.am); product
# repositories vendor a copy of it and must be kept in sync with this file.
# gen-sbom is taken from a vendored scripts/gen-sbom if a product ships one
# (used automatically), otherwise from a wolfSSL source tree via WOLFSSL_DIR.
# Products such as wolfSSH use the WOLFSSL_DIR route; vendoring gen-sbom for
# fully offline tarball builds can be added later with no change here.
#
# ---------------------------------------------------------------------------
# The including Makefile.am MUST set, before `include scripts/sbom.am`:
# SBOM_PKGNAME Product name recorded in the SBOM (e.g. wolfssh). Drives
# the output filenames and gen-sbom --name.
# SBOM_LICENSE_FILE Path to the product's LICENSING file
# (e.g. $(srcdir)/LICENSING).
#
# Optional (defaults shown):
# SBOM_OPTIONS_H Path to a product-generated options header (e.g.
# wolfMQTT's $(builddir)/wolfmqtt/options.h) that records
# the enabled build macros. Set this for products whose
# feature flags are NOT in config.h (no AC_DEFINE); when
# unset the recipe derives the macros from the compiler +
# config.h. Default: unset.
# SBOM_ARTIFACT lib | bin - which build output to hash. Default: lib.
# SBOM_LIB_STEM Library basename w/o extension. Default: lib$(SBOM_PKGNAME).
# SBOM_BIN_NAME Program name when SBOM_ARTIFACT = bin. Default: $(SBOM_PKGNAME).
# SBOM_DEP_WOLFSSL yes | no - record wolfSSL as a dependency. Default: no.
# SBOM_DEP_OPENSSL yes | no - record OpenSSL as a dependency (wolfProvider /
# wolfEngine). Default: no.
# SBOM_LICENSE_OVERRIDE SPDX expression to record instead of the licence
# detected from SBOM_LICENSE_FILE.
# SBOM_LICENSE_TEXT Path to licence text for any LicenseRef-* used in
# SBOM_LICENSE_OVERRIDE (required by SPDX 2.3).
# SBOM_WOLFSSL_VERSION Version recorded for the wolfSSL dependency;
# auto-detected from WOLFSSL_DIR/wolfssl/version.h when unset.
# SBOM_OPENSSL_VERSION Version recorded for the OpenSSL dependency;
# gen-sbom resolves it via pkg-config when unset.
# SBOM_CONFIG_H Path to the configure-generated config header to
# force-include when capturing the configured build
# macros. Products whose AC_CONFIG_HEADERS lives in a
# subdirectory MUST override this so config.h defines are
# captured (e.g. wolfEngine: $(abs_builddir)/include/config.h;
# wolfCLU: $(abs_builddir)/src/config.h).
# Default: $(abs_builddir)/config.h.
#
# The wolfSSL/OpenSSL dependency flags are feature-detected against gen-sbom
# --help, so a product wired for them still produces a valid SBOM (with a NOTE)
# against a gen-sbom that predates the flag.
#
# gen-sbom is located at $(srcdir)/scripts/gen-sbom if vendored, else at
# $(WOLFSSL_DIR)/scripts/gen-sbom. python3, pyspdxtools and git come from
# configure (AC_PATH_PROG); git is used only to derive SOURCE_DATE_EPOCH.
#
# NOTE: this fragment requires GNU make. It uses GNU conditional assignment
# (?=) and the GNU make functions $(wildcard), $(if), $(firstword) and
# $(addprefix); under a non-GNU make the SBOM targets will not work.
# ---------------------------------------------------------------------------
SBOM_ARTIFACT ?= lib
SBOM_LIB_STEM ?= lib$(SBOM_PKGNAME)
SBOM_BIN_NAME ?= $(SBOM_PKGNAME)
SBOM_DEP_WOLFSSL ?= no
SBOM_DEP_OPENSSL ?= no
SBOM_CONFIG_H ?= $(abs_builddir)/config.h
SBOM_CDX = $(SBOM_PKGNAME)-$(PACKAGE_VERSION).cdx.json
SBOM_SPDX = $(SBOM_PKGNAME)-$(PACKAGE_VERSION).spdx.json
SBOM_SPDX_TV = $(SBOM_PKGNAME)-$(PACKAGE_VERSION).spdx
# Use Automake's $(docdir) so a user's --docdir override is honoured (this
# equals $(datadir)/doc/$(PACKAGE) by default).
sbomdir = $(docdir)
# Prefer a vendored gen-sbom; fall back to an external wolfSSL source tree.
# The fallback is $(wildcard)-guarded and only consulted when WOLFSSL_DIR is
# set, so an unset WOLFSSL_DIR leaves SBOM_GEN empty (and the sbom recipe's
# `test -f` prints the "set WOLFSSL_DIR" error) rather than resolving to an
# absolute /scripts/gen-sbom that could run an unrelated host script.
SBOM_GEN = $(firstword $(wildcard $(srcdir)/scripts/gen-sbom) \
$(if $(WOLFSSL_DIR),$(wildcard $(WOLFSSL_DIR)/scripts/gen-sbom)))
# Library artifact search order (versioned first) covering ELF, Mach-O and PE.
# Windows import libs (.lib) come with and without the "lib" prefix.
SBOM_LIB_GLOBS = \
$(SBOM_LIB_STEM).so.[0-9]* \
$(SBOM_LIB_STEM).so \
$(SBOM_LIB_STEM).[0-9]*.dylib \
$(SBOM_LIB_STEM).dylib \
$(SBOM_LIB_STEM).dll \
$(SBOM_LIB_STEM).dll.a \
$(SBOM_LIB_STEM).lib \
$(SBOM_PKGNAME).lib \
$(SBOM_LIB_STEM).a
# Automake requires CLEANFILES to be initialised with `=` before `+=`; the
# including Makefile.am must declare `CLEANFILES =` (typically in its primaries
# init block) before `include scripts/sbom.am`.
CLEANFILES += $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV)
.PHONY: sbom install-sbom uninstall-sbom
# Stage a `make install` into a private tree, discover the installed artifact
# (shared/static library or program; ELF/Mach-O/PE), hash it, capture the
# configured build macros (from SBOM_OPTIONS_H if set, else AM_CPPFLAGS/
# AM_CFLAGS/CFLAGS + config.h; some products carry their feature -D flags in
# AM_CFLAGS rather than AM_CPPFLAGS, and some outside config.h entirely),
# generate SPDX+CDX, validate
# the SPDX, then convert to tag-value. The staging tree and temp defines file
# are removed unconditionally via `trap`, even on failure. SOURCE_DATE_EPOCH is
# honoured for reproducible output (defaults to the last git commit time).
sbom:
@test -n "$(PYTHON3)" || { \
echo "ERROR: 'python3' not found in PATH. Cannot generate SBOM."; \
exit 1; }
@test -n "$(PYSPDXTOOLS)" || { \
echo "ERROR: 'pyspdxtools' not found (pip install spdx-tools)."; \
exit 1; }
@test -f "$(SBOM_GEN)" || { \
echo "ERROR: gen-sbom not found. Vendor scripts/gen-sbom, or re-run:"; \
echo " make sbom WOLFSSL_DIR=/path/to/wolfssl"; \
exit 1; }
@rm -rf $(abs_builddir)/_sbom_staging
@set -e; \
_defines=`mktemp $(abs_builddir)/_sbom_defines.XXXXXX`; \
trap 'rm -rf $(abs_builddir)/_sbom_staging "$$_defines"' EXIT INT TERM HUP; \
$(MAKE) install DESTDIR=$(abs_builddir)/_sbom_staging; \
sbom_art=""; \
if test "$(SBOM_ARTIFACT)" = bin; then \
for art in \
"$(abs_builddir)/_sbom_staging$(bindir)/$(SBOM_BIN_NAME)" \
"$(abs_builddir)/_sbom_staging$(bindir)/$(SBOM_BIN_NAME)".exe; do \
if test -f "$$art"; then sbom_art="$$art"; break; fi; \
done; \
else \
for art in \
$(addprefix "$(abs_builddir)/_sbom_staging$(libdir)"/,$(SBOM_LIB_GLOBS)) \
$(addprefix "$(abs_builddir)/_sbom_staging$(bindir)"/,$(SBOM_LIB_STEM).dll $(SBOM_PKGNAME).dll); do \
if test -f "$$art"; then sbom_art="$$art"; break; fi; \
done; \
fi; \
if test -z "$$sbom_art"; then \
echo ""; \
echo "ERROR: no installed $(SBOM_PKGNAME) artifact found for SBOM."; \
echo " (configure with --enable-shared or --enable-static)"; \
echo ""; \
exit 1; \
fi; \
echo "SBOM: hashing $$sbom_art"; \
opts_h="$(SBOM_OPTIONS_H)"; \
if test -z "$$opts_h"; then \
opts_h="$$_defines"; \
$(CC) -dM -E $(DEFAULT_INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
$(AM_CFLAGS) $(CFLAGS) \
$(if $(wildcard $(SBOM_CONFIG_H)),-include $(SBOM_CONFIG_H)) \
-x c /dev/null > "$$_defines"; \
fi; \
if test -z "$${SOURCE_DATE_EPOCH:-}" && test -n "$(GIT)" && \
$(GIT) -C "$(srcdir)" rev-parse --git-dir >/dev/null 2>&1; then \
sde=`$(GIT) -C "$(srcdir)" log -1 --format=%ct 2>/dev/null`; \
if test -n "$$sde"; then SOURCE_DATE_EPOCH="$$sde"; export SOURCE_DATE_EPOCH; fi; \
fi; \
dep_args=""; \
if test "$(SBOM_DEP_WOLFSSL)" = yes; then \
if $(PYTHON3) "$(SBOM_GEN)" --help 2>/dev/null \
| $(GREP) -q -- '--dep-wolfssl'; then \
dep_args="$$dep_args --dep-wolfssl yes"; \
wv="$(SBOM_WOLFSSL_VERSION)"; \
if test -z "$$wv" && test -f "$(WOLFSSL_DIR)/wolfssl/version.h"; then \
wv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
"$(WOLFSSL_DIR)/wolfssl/version.h"`; \
fi; \
if test -n "$$wv"; then \
dep_args="$$dep_args --dep-version wolfssl=$$wv"; \
fi; \
else \
echo "NOTE: this gen-sbom has no --dep-wolfssl support, so the SBOM"; \
echo " will not list wolfssl as a dependency component. That"; \
echo " support is added by wolfSSL/wolfssl#10343; until it merges"; \
echo " to wolfssl master, point WOLFSSL_DIR at that PR's branch"; \
echo " to enable it. The generated SBOM is valid either way."; \
fi; \
fi; \
if test "$(SBOM_DEP_OPENSSL)" = yes; then \
if $(PYTHON3) "$(SBOM_GEN)" --help 2>/dev/null \
| $(GREP) -q -- '--dep-openssl'; then \
dep_args="$$dep_args --dep-openssl yes"; \
if test -n "$(SBOM_OPENSSL_VERSION)"; then \
dep_args="$$dep_args --dep-version openssl=$(SBOM_OPENSSL_VERSION)"; \
fi; \
else \
echo "NOTE: this gen-sbom has no --dep-openssl support; openssl will"; \
echo " not be listed as a dependency component."; \
fi; \
fi; \
$(PYTHON3) "$(SBOM_GEN)" \
--name $(SBOM_PKGNAME) \
--version $(PACKAGE_VERSION) \
--supplier "wolfSSL Inc." \
--license-file $(SBOM_LICENSE_FILE) \
--options-h "$$opts_h" \
--lib "$$sbom_art" \
$$dep_args \
$(if $(SBOM_LICENSE_OVERRIDE),--license-override '$(SBOM_LICENSE_OVERRIDE)') \
$(if $(SBOM_LICENSE_TEXT),--license-text '$(SBOM_LICENSE_TEXT)') \
--cdx-out $(abs_builddir)/$(SBOM_CDX) \
--spdx-out $(abs_builddir)/$(SBOM_SPDX); \
$(PYSPDXTOOLS) --infile $(abs_builddir)/$(SBOM_SPDX) \
--outfile $(abs_builddir)/$(SBOM_SPDX_TV)
install-sbom: sbom
$(MKDIR_P) $(DESTDIR)$(sbomdir)
$(INSTALL_DATA) $(SBOM_CDX) $(DESTDIR)$(sbomdir)/
$(INSTALL_DATA) $(SBOM_SPDX) $(DESTDIR)$(sbomdir)/
$(INSTALL_DATA) $(SBOM_SPDX_TV) $(DESTDIR)$(sbomdir)/
uninstall-sbom:
-rm -f $(DESTDIR)$(sbomdir)/$(SBOM_CDX)
-rm -f $(DESTDIR)$(sbomdir)/$(SBOM_SPDX)
-rm -f $(DESTDIR)$(sbomdir)/$(SBOM_SPDX_TV)
# SBOM install is intentionally opt-in (`make install-sbom`), so `make install`
# does NOT place SBOM files. uninstall-sbom is still chained into the standard
# `make uninstall` via uninstall-hook so a prior `make install-sbom` is cleaned
# up; it uses `rm -f`, so it is a harmless no-op when no SBOM was installed.
uninstall-hook: uninstall-sbom