diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 00000000..8a08e91d --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,175 @@ +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' + # TODO: switch back to 'master' once wolfSSL/wolfssl#10343 merges. + default: 'refs/pull/10343/head' + +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: wolfTPM SBOM generation (linux) + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout wolftpm + uses: actions/checkout@v4 + with: + path: wolftpm + + # wolfTPM links wolfSSL/wolfCrypt (RNG, parameter encryption, PK + # callbacks), so its SBOM records wolfSSL as a dependency. wolfSSL is + # built + installed here so wolfTPM 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. gen-sbom is not yet on wolfssl master, + # so default to the open PR head that carries it (wolfSSL/wolfssl#10343) + # so CI actually exercises `make sbom` instead of silently skipping. + # TODO: switch the fallback back to 'master' once #10343 merges. + - name: Checkout wolfssl (gen-sbom + library source) + uses: actions/checkout@v4 + with: + repository: wolfSSL/wolfssl + ref: ${{ github.event.inputs.wolfssl_ref || 'refs/pull/10343/head' }} + 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-wolftpm --enable-pkcallbacks \ + --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 wolftpm + if: steps.gate.outputs.have == 'yes' + working-directory: wolftpm + run: | + autoreconf -ivf + ./configure --enable-swtpm --disable-fwtpm --disable-examples \ + --with-wolfcrypt="$GITHUB_WORKSPACE/wolfssl-install" + make -j"$(nproc)" + + - name: Generate SBOM + if: steps.gate.outputs.have == 'yes' + working-directory: wolftpm + run: make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + + - name: Outputs exist and SPDX validates + if: steps.gate.outputs.have == 'yes' + working-directory: wolftpm + run: | + ls wolftpm-*.cdx.json wolftpm-*.spdx.json wolftpm-*.spdx + pyspdxtools --infile wolftpm-*.spdx.json + + - name: CycloneDX identity, licence, and captured options + if: steps.gate.outputs.have == 'yes' + working-directory: wolftpm + run: | + python3 - <<'PY' + import glob, json + cdx = json.load(open(glob.glob('wolftpm-*.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'] == 'wolftpm', m['name'] + assert m['purl'].startswith('pkg:github/wolfSSL/wolftpm@'), 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: wolfTPM records its feature + # macros in wolftpm/options.h, 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: wolftpm + run: | + rm -f wolftpm-*.cdx.json wolftpm-*.spdx.json wolftpm-*.spdx + SOURCE_DATE_EPOCH=1700000000 make sbom \ + WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + sha256sum wolftpm-*.cdx.json wolftpm-*.spdx.json > /tmp/a.sums + rm -f wolftpm-*.cdx.json wolftpm-*.spdx.json wolftpm-*.spdx + SOURCE_DATE_EPOCH=1700000000 make sbom \ + WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + sha256sum wolftpm-*.cdx.json wolftpm-*.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: wolftpm + run: | + python3 - <<'PY' + import glob, json + d = json.load(open(glob.glob('wolftpm-*.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-wolftpm', '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: wolftpm-sbom-${{ github.sha }} + path: | + wolftpm/wolftpm-*.cdx.json + wolftpm/wolftpm-*.spdx.json + wolftpm/wolftpm-*.spdx + if-no-files-found: warn + retention-days: 90 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0add0f84..2a144813 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -770,3 +770,163 @@ install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/wolftpm/ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/wolftpm/ DESTINATION include/wolftpm FILES_MATCHING PATTERN "*.h") + + +#################################################### +# 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 that contains +# scripts/gen-sbom (available on the feat/sbom-embedded branch). +# +# Outputs written to the build directory: +# wolftpm-.cdx.json (CycloneDX) +# wolftpm-.spdx.json (SPDX JSON) +# wolftpm-.spdx (SPDX tag-value, pyspdxtools validation output) + +if(BUILD_WOLFTPM_LIB) + set(WOLFSSL_DIR "" CACHE PATH + "Path to wolfssl source tree with scripts/gen-sbom") + + # wolfTPM 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 so the SBOM is correct + # regardless of the gen-sbom version's licence detection; commercial + # licensees can override it (e.g. LicenseRef-wolfSSL-Commercial). This + # mirrors SBOM_LICENSE_OVERRIDE in the autotools build. + set(SBOM_LICENSE_OVERRIDE "GPL-3.0-or-later" CACHE STRING + "SPDX licence expression recorded in the SBOM") + + # Derive the version from wolftpm/version.h, NOT from PROJECT_VERSION. + # autotools `make sbom` uses PACKAGE_VERSION, which is sourced from + # version.h. Reading the same header here keeps the cmake and autotools + # SBOMs bit-identical in the version field even if the project() call in + # this file drifts out of sync with the header. + file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/wolftpm/version.h" + _sbom_version_line + REGEX "^#define[ ]+LIBWOLFTPM_VERSION_STRING[ ]+\"") + string(REGEX REPLACE + "^#define[ ]+LIBWOLFTPM_VERSION_STRING[ ]+\"([^\"]+)\".*" "\\1" + SBOM_VERSION "${_sbom_version_line}") + if(NOT SBOM_VERSION) + message(FATAL_ERROR + "sbom: could not parse LIBWOLFTPM_VERSION_STRING from " + "wolftpm/version.h") + endif() + + # Validate the SBOM prerequisites at configure time so the user learns + # what is missing immediately, instead of after a full library build. + # + # These checks must NOT abort configuration of a normal build: someone who + # just wants `cmake -B build && cmake --build build` should never be forced + # to set WOLFSSL_DIR. So when a prerequisite is missing we still define a + # `sbom` target, but one that prints the reason and fails at build time. + # Only when the user explicitly opts in via -DWOLFSSL_DIR=... and that path + # turns out to be wrong do we hard-error at configure time, because at that + # point the user clearly intends to build SBOMs and a typo'd path is a + # mistake worth surfacing right away. + find_program(PYTHON3_CMD python3) + find_program(PYSPDXTOOLS_CMD pyspdxtools) + + set(_sbom_error "") + if(WOLFSSL_DIR STREQUAL "") + set(_sbom_error + "WOLFSSL_DIR is not set. Re-run cmake with -DWOLFSSL_DIR=/path/to/wolfssl") + elseif(NOT EXISTS "${WOLFSSL_DIR}/scripts/gen-sbom") + # User opted in with a bad path -> fail configure now, not at build. + message(FATAL_ERROR + "sbom: ${WOLFSSL_DIR}/scripts/gen-sbom not found.\n" + " Check that WOLFSSL_DIR points to a wolfSSL tree with " + "SBOM support.") + elseif(NOT PYTHON3_CMD) + set(_sbom_error "'python3' not found in PATH. Cannot generate SBOM.") + elseif(NOT PYSPDXTOOLS_CMD) + set(_sbom_error + "'pyspdxtools' not found in PATH (install: pip install spdx-tools)") + endif() + + if(NOT _sbom_error STREQUAL "") + # Prerequisite missing: keep configuration working, but make the sbom + # target fail loudly if someone actually invokes it. + add_custom_target(sbom + VERBATIM + COMMAND ${CMAKE_COMMAND} -E echo "sbom: ${_sbom_error}" + COMMAND ${CMAKE_COMMAND} -E false + COMMENT "SBOM prerequisites missing") + return() + endif() + + set(SBOM_CDX "${CMAKE_BINARY_DIR}/wolftpm-${SBOM_VERSION}.cdx.json") + set(SBOM_SPDX "${CMAKE_BINARY_DIR}/wolftpm-${SBOM_VERSION}.spdx.json") + set(SBOM_SPDX_TV "${CMAKE_BINARY_DIR}/wolftpm-${SBOM_VERSION}.spdx") + set(SBOM_STAGING "${CMAKE_BINARY_DIR}/_sbom_staging") + + # Staged install path. Derive the installed artifact name from the target + # itself so it tracks the real output - a versioned .so/.dylib, a static + # .a/.lib, or the Windows DLL - instead of a hardcoded + # libwolftpm${CMAKE_SHARED_LIBRARY_SUFFIX} that only matches a Unix shared + # build. install(TARGETS) sends the Windows DLL (RUNTIME) to bin/ and + # everything else (LIBRARY/ARCHIVE) to lib/. + if(WIN32 AND BUILD_SHARED_LIBS) + set(SBOM_LIB "${SBOM_STAGING}/bin/$") + else() + set(SBOM_LIB "${SBOM_STAGING}/lib/$") + endif() + + # wolfTPM links wolfSSL/wolfCrypt, so record wolfSSL as a dependency in the + # SBOM (matches the autotools SBOM_DEP_WOLFSSL=yes). Recording it needs the + # gen-sbom from wolfSSL/wolfssl#10343; older gen-sbom rejects unknown flags, + # so probe --help and only pass --dep-wolfssl when supported. Against an + # older gen-sbom the SBOM is still valid but omits the wolfSSL entry. + set(_sbom_dep_wolfssl "") + execute_process( + COMMAND ${PYTHON3_CMD} ${WOLFSSL_DIR}/scripts/gen-sbom --help + OUTPUT_VARIABLE _sbom_help + ERROR_QUIET) + if(_sbom_help MATCHES "--dep-wolfssl") + set(_sbom_dep_wolfssl --dep-wolfssl yes) + else() + message(WARNING + "sbom: ${WOLFSSL_DIR}/scripts/gen-sbom has no --dep-wolfssl; " + "the SBOM will omit the wolfSSL dependency entry. Use the gen-sbom " + "from wolfSSL/wolfssl#10343 (or master once merged) to record it.") + endif() + + # ${OPTION_FILE} is the generated wolftpm/options.h, the same compile-time + # option fingerprint autotools `make sbom` feeds to gen-sbom via + # --options-h. Using it (rather than a raw `cc -dM` dump, which would only + # capture compiler builtins) keeps the cmake SBOM consistent with autotools. + add_custom_target(sbom + VERBATIM + # Stage a clean install so gen-sbom inspects the same artifact layout + # the user would actually ship. + COMMAND ${CMAKE_COMMAND} -E remove_directory ${SBOM_STAGING} + COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} + --prefix ${SBOM_STAGING} --config $ + COMMAND ${PYTHON3_CMD} ${WOLFSSL_DIR}/scripts/gen-sbom + --name wolftpm + --version ${SBOM_VERSION} + --supplier "wolfSSL Inc." + --license-file ${CMAKE_SOURCE_DIR}/LICENSE + --options-h ${OPTION_FILE} + --lib ${SBOM_LIB} + --license-override ${SBOM_LICENSE_OVERRIDE} + ${_sbom_dep_wolfssl} + --cdx-out ${SBOM_CDX} + --spdx-out ${SBOM_SPDX} + # Validate the SPDX JSON and emit the tag-value rendering as a side + # effect; a malformed SBOM makes pyspdxtools exit non-zero and fails + # the target. + COMMAND ${PYSPDXTOOLS_CMD} --infile ${SBOM_SPDX} + --outfile ${SBOM_SPDX_TV} + COMMAND ${CMAKE_COMMAND} -E remove_directory ${SBOM_STAGING} + COMMENT "Generating SBOM for wolfTPM ${SBOM_VERSION}") + + # gen-sbom reads the staged library, so the library must build first. + add_dependencies(sbom wolftpm) +endif() diff --git a/Makefile.am b/Makefile.am index ce79c5e5..e6e0db95 100644 --- a/Makefile.am +++ b/Makefile.am @@ -132,3 +132,26 @@ cppcheck: --suppress=invalidPrintfArgType_sint \ --error-exitcode=89 --std=c89 \ -I wolftpm src/ hal/ examples + +# SBOM generation (CRA compliance). The recipe is shared across the wolfSSL +# stack's autotools products in scripts/sbom.am; wolfTPM just declares what it +# is (a libwolftpm library that links wolfSSL/wolfCrypt) and includes it. +# WOLFSSL_DIR must point to a wolfssl source tree containing scripts/gen-sbom. +SBOM_PKGNAME = wolftpm +SBOM_LICENSE_FILE = $(srcdir)/LICENSE +SBOM_DEP_WOLFSSL = yes + +# wolfTPM records its feature macros in its own generated options header, so +# point gen-sbom at that header rather than at the compiler-derived defaults. +SBOM_OPTIONS_H = $(abs_builddir)/wolftpm/options.h + +# wolfTPM 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 diff --git a/README.md b/README.md index bbe164bc..cad2ae52 100644 --- a/README.md +++ b/README.md @@ -1122,6 +1122,50 @@ See `./examples/endorsement/get_ek_certs`. * Inner wrap support for SensitiveToPrivate. * Add support for IRQ (interrupt line) +## SBOM / EU CRA Compliance + +wolfTPM 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 `wolftpm/options.h`), +hashes the built `libwolftpm` 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 wolfTPM +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: `wolftpm-.cdx.json`, `wolftpm-.spdx.json`, `wolftpm-.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/wolftpm/ +make uninstall-sbom +``` + +Note: recording wolfSSL as a dependency and emitting wolfTPM-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). + ## Support Email us at [support@wolfssl.com](mailto:support@wolfssl.com). diff --git a/configure.ac b/configure.ac index 2a56f5b3..f703bfc0 100644 --- a/configure.ac +++ b/configure.ac @@ -991,6 +991,16 @@ AC_SUBST([AM_CFLAGS]) AC_SUBST([AM_LDFLAGS]) AC_SUBST([CPPCHECK]) +# 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([wolftpm/version.h]) diff --git a/scripts/sbom.am b/scripts/sbom.am new file mode 100644 index 00000000..bbc4942a --- /dev/null +++ b/scripts/sbom.am @@ -0,0 +1,206 @@ +# 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. +# +# The canonical copy lives in the wolfSSL repository (scripts/sbom.am); keep +# product copies in sync. 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. wolfSSH uses 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. +# +# 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. +# --------------------------------------------------------------------------- + +SBOM_ARTIFACT ?= lib +SBOM_LIB_STEM ?= lib$(SBOM_PKGNAME) +SBOM_BIN_NAME ?= $(SBOM_PKGNAME) +SBOM_DEP_WOLFSSL ?= no +SBOM_DEP_OPENSSL ?= no + +SBOM_CDX = $(SBOM_PKGNAME)-$(PACKAGE_VERSION).cdx.json +SBOM_SPDX = $(SBOM_PKGNAME)-$(PACKAGE_VERSION).spdx.json +SBOM_SPDX_TV = $(SBOM_PKGNAME)-$(PACKAGE_VERSION).spdx +sbomdir = $(datadir)/doc/$(PACKAGE) + +# 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 (AM_CPPFLAGS + config.h), 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) \ + $(if $(wildcard $(abs_builddir)/config.h),-include $(abs_builddir)/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[ \t]*"\([^"]*\)".*/\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) + +uninstall-hook: uninstall-sbom