From 21b855842e93b4f13c6ebf6f63e89b219d8a1bae Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Mon, 22 Jun 2026 18:39:12 -0700 Subject: [PATCH 01/15] feat: add make sbom target Adds sbom, install-sbom, and uninstall-sbom targets. Runs gen-sbom to produce CDX and SPDX outputs. Requires WOLFSSL_DIR pointing to a wolfssl tree with the feat/sbom-embedded branch (includes gen-sbom). --- Makefile.am | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 22 +++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index d5db195d5..aadfedb66 100644 --- a/Makefile.am +++ b/Makefile.am @@ -79,3 +79,74 @@ merge-clean: @find ./ | $(GREP) \.OTHER | xargs rm -f @find ./ | $(GREP) \.BASE | xargs rm -f @find ./ | $(GREP) \~$$ | xargs rm -f + +# SBOM generation (CRA compliance) +SBOM_CDX = wolfssh-$(PACKAGE_VERSION).cdx.json +SBOM_SPDX = wolfssh-$(PACKAGE_VERSION).spdx.json +SBOM_SPDX_TV = wolfssh-$(PACKAGE_VERSION).spdx +sbomdir = $(datadir)/doc/$(PACKAGE) + +.PHONY: sbom install-sbom uninstall-sbom + +sbom: + @if test -z "$(PYTHON3)"; then \ + echo ""; \ + echo "ERROR: 'python3' not found in PATH. Cannot generate SBOM."; \ + echo ""; \ + exit 1; \ + fi + @if test -z "$(PYSPDXTOOLS)"; then \ + echo ""; \ + echo "ERROR: 'pyspdxtools' not found in PATH. Cannot validate SBOM."; \ + echo " Install: pip install spdx-tools"; \ + echo ""; \ + exit 1; \ + fi + @if test -z "$(WOLFSSL_DIR)"; then \ + echo ""; \ + echo "ERROR: WOLFSSL_DIR is not set. Cannot locate gen-sbom."; \ + echo " Re-run: make sbom WOLFSSL_DIR=/path/to/wolfssl"; \ + echo ""; \ + exit 1; \ + fi + @if test ! -f "$(WOLFSSL_DIR)/scripts/gen-sbom"; then \ + echo ""; \ + echo "ERROR: $(WOLFSSL_DIR)/scripts/gen-sbom not found."; \ + echo " Use a wolfSSL tree that includes SBOM support."; \ + echo ""; \ + exit 1; \ + fi + rm -rf $(abs_builddir)/_sbom_staging $(abs_builddir)/_sbom_defines.h + $(MAKE) install DESTDIR=$(abs_builddir)/_sbom_staging + $(CC) -dM -E -I$(srcdir) $(CPPFLAGS) -x c /dev/null \ + > $(abs_builddir)/_sbom_defines.h + @set -e; \ + _so=$$(ls $(abs_builddir)/_sbom_staging$(libdir)/libwolfssh.so.[0-9]*.[0-9]*.[0-9]* 2>/dev/null | head -1); \ + test -n "$$_so" || { echo "ERROR: libwolfssh.so not found in staging dir" >&2; exit 1; }; \ + $(PYTHON3) $(WOLFSSL_DIR)/scripts/gen-sbom \ + --name wolfssh \ + --version $(PACKAGE_VERSION) \ + --supplier "wolfSSL Inc." \ + --license-file $(srcdir)/LICENSING \ + --options-h $(abs_builddir)/_sbom_defines.h \ + --lib "$$_so" \ + $(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) + rm -rf $(abs_builddir)/_sbom_staging $(abs_builddir)/_sbom_defines.h + $(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) + +CLEANFILES = $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV) diff --git a/configure.ac b/configure.ac index 4aab10756..8810aac3f 100644 --- a/configure.ac +++ b/configure.ac @@ -18,7 +18,21 @@ AC_ARG_PROGRAM AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_HEADERS([config.h]) -WOLFSSH_LIBRARY_VERSION=20:0:2 +# The three numbers in the libwolfssh.so.*.*.* file name. + +# increment if interfaces have been removed or changed +WOLFSSH_LIBRARY_VERSION_FIRST=20 + +# increment if interfaces have been added +# set to zero if WOLFSSH_LIBRARY_VERSION_FIRST is incremented +WOLFSSH_LIBRARY_VERSION_SECOND=0 + +# increment if source code has changed +# set to zero if WOLFSSH_LIBRARY_VERSION_FIRST is incremented or +# WOLFSSH_LIBRARY_VERSION_SECOND is incremented +WOLFSSH_LIBRARY_VERSION_THIRD=2 + +WOLFSSH_LIBRARY_VERSION=${WOLFSSH_LIBRARY_VERSION_FIRST}:${WOLFSSH_LIBRARY_VERSION_SECOND}:${WOLFSSH_LIBRARY_VERSION_THIRD} # | | | # +-----+ | +----+ # | | | @@ -32,6 +46,9 @@ WOLFSSH_LIBRARY_VERSION=20:0:2 # +- increment if interfaces have been added, removed # or changed AC_SUBST([WOLFSSH_LIBRARY_VERSION]) +AC_SUBST([WOLFSSH_LIBRARY_VERSION_FIRST]) +AC_SUBST([WOLFSSH_LIBRARY_VERSION_SECOND]) +AC_SUBST([WOLFSSH_LIBRARY_VERSION_THIRD]) LT_PREREQ([2.4.3]) LT_INIT([disable-static win32-dll]) @@ -309,6 +326,9 @@ AC_SUBST([AM_CPPFLAGS]) AC_SUBST([AM_CFLAGS]) AC_SUBST([AM_LDFLAGS]) +AC_PATH_PROG([PYTHON3], [python3]) +AC_PATH_PROG([PYSPDXTOOLS], [pyspdxtools]) + # FINAL AC_CONFIG_FILES([Makefile wolfssh/version.h]) From 3af6d629b857277514ccddf7eb2a4d5b38471b40 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Tue, 23 Jun 2026 17:42:01 -0700 Subject: [PATCH 02/15] docs: add SBOM/EU CRA Compliance section to README and build docs --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 554f1298f..972dffb84 100644 --- a/README.md +++ b/README.md @@ -627,3 +627,25 @@ WOLFSSH APPLICATIONS wolfSSH comes with a server daemon and a command line shell tool. Check out the apps directory for more information. + +## SBOM / EU CRA Compliance + +wolfSSH 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). + +```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: `wolfssh-.cdx.json`, `wolfssh-.spdx.json`, `wolfssh-.spdx` + +```sh +make install-sbom # installs to $(datadir)/doc/wolfssh/ +make uninstall-sbom +``` + +For further CRA guidance see [wolfssl/doc/CRA.md](https://github.com/wolfSSL/wolfssl/blob/master/doc/CRA.md). From 69cb55fe4f163346b5f92503b4af0ac76a739572 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Tue, 7 Jul 2026 13:28:38 +0300 Subject: [PATCH 03/15] fix: make SBOM reflect real config and cover all build types - Include AM_CPPFLAGS and config.h in the options snapshot so the configured feature set (WOLFSSH_SCP/SFTP/NO_WOLFSSH_SERVER/...) appears in the SBOM instead of being empty. - Discover static (.a), Mach-O (.dylib) and Windows (.dll/.lib) artifacts, not just a versioned .so. - Set SOURCE_DATE_EPOCH from the last git commit for reproducible output. - Clean staging tree and temp defines file via trap even on failure. - Record wolfssl as an SBOM dependency (--dep-wolfssl), version auto-detected from WOLFSSL_DIR/wolfssl/version.h. - configure.ac: detect git; drop unused version-info split + comment. Signed-off-by: Sameeh Jubran --- Makefile.am | 95 +++++++++++++++++++++++++++++++++++++++++++++------- configure.ac | 23 +++---------- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/Makefile.am b/Makefile.am index aadfedb66..389de269e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -86,8 +86,37 @@ SBOM_SPDX = wolfssh-$(PACKAGE_VERSION).spdx.json SBOM_SPDX_TV = wolfssh-$(PACKAGE_VERSION).spdx sbomdir = $(datadir)/doc/$(PACKAGE) +# Shared-library / Mach-O basenames in priority order (versioned first) so +# `make sbom` finds the built artifact on ELF, Mach-O, and PE targets. Static +# (.a) and Windows (.dll/.lib) variants are appended at the call-site because +# their prefixes differ. +WOLFSSH_LIB_DSO_BASENAMES = \ + libwolfssh.so.[0-9]* \ + libwolfssh.so \ + libwolfssh.[0-9]*.dylib \ + libwolfssh.dylib + .PHONY: sbom install-sbom uninstall-sbom +# Stage a `make install` into a private tree, discover the installed library +# artifact (shared or static, ELF/Mach-O/PE), hash it, capture the configured +# build macros, 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. Honors SOURCE_DATE_EPOCH for +# reproducible builds (set to the last git commit time when unset and a git +# tree is available), matching wolfssl's `make sbom`. +# +# User-overridable variables: +# WOLFSSL_DIR Path to a wolfssl source tree containing +# scripts/gen-sbom (required). +# SBOM_LICENSE_OVERRIDE SPDX expression to use instead of the licence ID +# parsed from LICENSING (e.g. commercial licensees: +# LicenseRef-wolfSSL-Commercial). +# 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 string recorded for the wolfssl dependency. +# Auto-detected from WOLFSSL_DIR/wolfssl/version.h or +# pkg-config when unset. sbom: @if test -z "$(PYTHON3)"; then \ echo ""; \ @@ -116,25 +145,67 @@ sbom: echo ""; \ exit 1; \ fi - rm -rf $(abs_builddir)/_sbom_staging $(abs_builddir)/_sbom_defines.h - $(MAKE) install DESTDIR=$(abs_builddir)/_sbom_staging - $(CC) -dM -E -I$(srcdir) $(CPPFLAGS) -x c /dev/null \ - > $(abs_builddir)/_sbom_defines.h + @rm -rf $(abs_builddir)/_sbom_staging @set -e; \ - _so=$$(ls $(abs_builddir)/_sbom_staging$(libdir)/libwolfssh.so.[0-9]*.[0-9]*.[0-9]* 2>/dev/null | head -1); \ - test -n "$$_so" || { echo "ERROR: libwolfssh.so not found in staging dir" >&2; exit 1; }; \ + _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_lib=""; \ + for lib in \ + $(addprefix "$(abs_builddir)/_sbom_staging$(libdir)"/,$(WOLFSSH_LIB_DSO_BASENAMES)) \ + "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.dll \ + "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.dll.a \ + "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.lib \ + "$(abs_builddir)/_sbom_staging$(libdir)"/wolfssh.lib \ + "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.a; do \ + if test -f "$$lib"; then sbom_lib="$$lib"; break; fi; \ + done; \ + if test -z "$$sbom_lib"; then \ + echo ""; \ + echo "ERROR: No installed wolfSSH library artifact found for SBOM."; \ + echo " Searched in $(abs_builddir)/_sbom_staging$(libdir)"; \ + echo " (configure with --enable-shared or --enable-static)"; \ + echo ""; \ + exit 1; \ + fi; \ + echo "SBOM: hashing $$sbom_lib"; \ + $(CC) -dM -E $(DEFAULT_INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ + $(if $(wildcard $(abs_builddir)/config.h),-include $(abs_builddir)/config.h) \ + -x c /dev/null > "$$_defines"; \ + wolfssl_ver="$(SBOM_WOLFSSL_VERSION)"; \ + if test -z "$$wolfssl_ver" && test -f "$(WOLFSSL_DIR)/wolfssl/version.h"; then \ + wolfssl_ver=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[ \t]*"\([^"]*\)".*/\1/p' \ + "$(WOLFSSL_DIR)/wolfssl/version.h"`; \ + 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 $(PYTHON3) $(WOLFSSL_DIR)/scripts/gen-sbom --help 2>/dev/null \ + | $(GREP) -q -- '--dep-wolfssl'; then \ + dep_args="--dep-wolfssl yes"; \ + if test -n "$$wolfssl_ver"; then \ + dep_args="$$dep_args --dep-version wolfssl=$$wolfssl_ver"; \ + fi; \ + else \ + echo "NOTE: this gen-sbom has no --dep-wolfssl; the SBOM will not list"; \ + echo " wolfssl as a dependency component. Point WOLFSSL_DIR at a"; \ + echo " wolfSSL tree whose scripts/gen-sbom supports it to enable it."; \ + fi; \ $(PYTHON3) $(WOLFSSL_DIR)/scripts/gen-sbom \ --name wolfssh \ --version $(PACKAGE_VERSION) \ --supplier "wolfSSL Inc." \ --license-file $(srcdir)/LICENSING \ - --options-h $(abs_builddir)/_sbom_defines.h \ - --lib "$$_so" \ - $(if $(SBOM_LICENSE_OVERRIDE),--license-override $(SBOM_LICENSE_OVERRIDE)) \ - $(if $(SBOM_LICENSE_TEXT),--license-text $(SBOM_LICENSE_TEXT)) \ + --options-h "$$_defines" \ + --lib "$$sbom_lib" \ + $$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) - rm -rf $(abs_builddir)/_sbom_staging $(abs_builddir)/_sbom_defines.h + --spdx-out $(abs_builddir)/$(SBOM_SPDX); \ $(PYSPDXTOOLS) --infile $(abs_builddir)/$(SBOM_SPDX) \ --outfile $(abs_builddir)/$(SBOM_SPDX_TV) diff --git a/configure.ac b/configure.ac index 8810aac3f..5a2342220 100644 --- a/configure.ac +++ b/configure.ac @@ -18,21 +18,7 @@ AC_ARG_PROGRAM AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_HEADERS([config.h]) -# The three numbers in the libwolfssh.so.*.*.* file name. - -# increment if interfaces have been removed or changed -WOLFSSH_LIBRARY_VERSION_FIRST=20 - -# increment if interfaces have been added -# set to zero if WOLFSSH_LIBRARY_VERSION_FIRST is incremented -WOLFSSH_LIBRARY_VERSION_SECOND=0 - -# increment if source code has changed -# set to zero if WOLFSSH_LIBRARY_VERSION_FIRST is incremented or -# WOLFSSH_LIBRARY_VERSION_SECOND is incremented -WOLFSSH_LIBRARY_VERSION_THIRD=2 - -WOLFSSH_LIBRARY_VERSION=${WOLFSSH_LIBRARY_VERSION_FIRST}:${WOLFSSH_LIBRARY_VERSION_SECOND}:${WOLFSSH_LIBRARY_VERSION_THIRD} +WOLFSSH_LIBRARY_VERSION=20:0:2 # | | | # +-----+ | +----+ # | | | @@ -46,9 +32,6 @@ WOLFSSH_LIBRARY_VERSION=${WOLFSSH_LIBRARY_VERSION_FIRST}:${WOLFSSH_LIBRARY_VERSI # +- increment if interfaces have been added, removed # or changed AC_SUBST([WOLFSSH_LIBRARY_VERSION]) -AC_SUBST([WOLFSSH_LIBRARY_VERSION_FIRST]) -AC_SUBST([WOLFSSH_LIBRARY_VERSION_SECOND]) -AC_SUBST([WOLFSSH_LIBRARY_VERSION_THIRD]) LT_PREREQ([2.4.3]) LT_INIT([disable-static win32-dll]) @@ -326,8 +309,12 @@ AC_SUBST([AM_CPPFLAGS]) AC_SUBST([AM_CFLAGS]) AC_SUBST([AM_LDFLAGS]) +# Tools used by the SBOM targets (see Makefile.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]) # FINAL AC_CONFIG_FILES([Makefile wolfssh/version.h]) From f1268dfba801ab0b74fe3f32c9b3f0e87f044753 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Wed, 8 Jul 2026 15:07:55 +0300 Subject: [PATCH 04/15] sbom: fix license detection and clarify wolfssl dependency note Default SBOM_LICENSE_OVERRIDE to GPL-3.0-only so the SBOM records the correct license instead of NOASSERTION, and point the --dep-wolfssl note at wolfSSL/wolfssl#10343. Signed-off-by: Sameeh Jubran --- Makefile.am | 23 ++++++++++++++++++----- README.md | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/Makefile.am b/Makefile.am index 389de269e..edf173a25 100644 --- a/Makefile.am +++ b/Makefile.am @@ -86,6 +86,16 @@ SBOM_SPDX = wolfssh-$(PACKAGE_VERSION).spdx.json SBOM_SPDX_TV = wolfssh-$(PACKAGE_VERSION).spdx sbomdir = $(datadir)/doc/$(PACKAGE) +# wolfSSH is licensed GPLv3-or-commercial. Its LICENSING file uses the +# abbreviation "GPLv3", which older gen-sbom detect_license does not recognise +# (it matches only the long form "GNU General Public License version 3") and so +# falls back to NOASSERTION. Default the override to the correct SPDX id so the +# SBOM is right regardless of gen-sbom version; commercial licensees can still +# override it (e.g. SBOM_LICENSE_OVERRIDE=LicenseRef-wolfSSL-Commercial). Once +# the detect_license GPLv-abbreviation fix lands in gen-sbom (wolfSSL/wolfssl#10343) +# this default is redundant-but-harmless (GPL-3.0-only is what detection returns). +SBOM_LICENSE_OVERRIDE ?= GPL-3.0-only + # Shared-library / Mach-O basenames in priority order (versioned first) so # `make sbom` finds the built artifact on ELF, Mach-O, and PE targets. Static # (.a) and Windows (.dll/.lib) variants are appended at the call-site because @@ -109,8 +119,9 @@ WOLFSSH_LIB_DSO_BASENAMES = \ # User-overridable variables: # WOLFSSL_DIR Path to a wolfssl source tree containing # scripts/gen-sbom (required). -# SBOM_LICENSE_OVERRIDE SPDX expression to use instead of the licence ID -# parsed from LICENSING (e.g. commercial licensees: +# SBOM_LICENSE_OVERRIDE SPDX expression to record as the licence. Defaults +# to GPL-3.0-only (wolfSSH's licence); commercial +# licensees override it (e.g. # LicenseRef-wolfSSL-Commercial). # SBOM_LICENSE_TEXT Path to the licence text for any LicenseRef-* used # in SBOM_LICENSE_OVERRIDE (required by SPDX 2.3). @@ -190,9 +201,11 @@ sbom: dep_args="$$dep_args --dep-version wolfssl=$$wolfssl_ver"; \ fi; \ else \ - echo "NOTE: this gen-sbom has no --dep-wolfssl; the SBOM will not list"; \ - echo " wolfssl as a dependency component. Point WOLFSSL_DIR at a"; \ - echo " wolfSSL tree whose scripts/gen-sbom supports it to enable it."; \ + 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 to"; \ + echo " enable it. The generated SBOM is valid either way."; \ fi; \ $(PYTHON3) $(WOLFSSL_DIR)/scripts/gen-sbom \ --name wolfssh \ diff --git a/README.md b/README.md index 972dffb84..d9bd8eb3c 100644 --- a/README.md +++ b/README.md @@ -632,6 +632,12 @@ the apps directory for more information. wolfSSH 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, hashes the built library +artifact (shared or static; ELF, Mach-O, or PE), and lists wolfSSL as a +dependency so vulnerability scanners can associate wolfSSL advisories with a +wolfSSH 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 @@ -643,9 +649,25 @@ must point to a wolfssl source tree containing `scripts/gen-sbom` (branch Output: `wolfssh-.cdx.json`, `wolfssh-.spdx.json`, `wolfssh-.spdx` +Optional overrides: + +- `SBOM_LICENSE_OVERRIDE` - SPDX expression to use instead of the licence + parsed from `LICENSING` (e.g. `LicenseRef-wolfSSL-Commercial` for commercial + licensees). +- `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/wolfssh/ make uninstall-sbom ``` +Note: recording wolfSSL as a dependency and emitting wolfSSH-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). From 312b23e23c854c6d4056038dc9232c0f6c89f926 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Wed, 8 Jul 2026 19:48:34 +0300 Subject: [PATCH 05/15] sbom: extract shared scripts/sbom.am recipe and add CI Move the make sbom / install-sbom / uninstall-sbom recipe out of Makefile.am into a reusable scripts/sbom.am fragment; Makefile.am now just declares the product facts (name, LICENSING, wolfssl dependency, GPL-3.0-only default) and includes it. Add a GitHub Actions workflow covering both SBOM paths: - autotools: build wolfSSL + wolfSSH, run make sbom, assert SPDX validity, CycloneDX identity, reproducibility, and the wolfssl dependency edge. - embedded: run gen-sbom against user_settings.h (via pcpp) + the wolfSSH source set, asserting the same identity/validity/reproducibility plus that user_settings.h options and the wolfssl dependency are captured. Both gracefully skip on a gen-sbom that predates the wolfSSL SBOM change. Signed-off-by: Sameeh Jubran --- .github/workflows/sbom.yml | 255 +++++++++++++++++++++++++++++++++++++ Makefile.am | 168 +++--------------------- scripts/sbom.am | 190 +++++++++++++++++++++++++++ 3 files changed, 461 insertions(+), 152 deletions(-) create mode 100644 .github/workflows/sbom.yml create mode 100644 scripts/sbom.am diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 000000000..1d60f26c9 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,255 @@ +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: wolfSSH SBOM generation (linux) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout wolfssh + uses: actions/checkout@v4 + with: + path: wolfssh + + # wolfssl is checked out once and used for two things: built + installed + # so wolfssh has a library to link, and its source tree (scripts/gen-sbom + # + wolfssl/version.h) is passed to `make sbom` via WOLFSSL_DIR. Default + # ref is master; until the wolfSSL SBOM change is on master, use the + # "Run workflow" button with wolfssl_ref set to the SBOM branch. + - 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 SBOM validator (pyspdxtools) and pcpp + run: | + # spdx-tools -> pyspdxtools (validation); pcpp -> the embedded + # (--user-settings) path's C preprocessor for walking user_settings.h. + python3 -m pip install --user 'spdx-tools==0.8.*' pcpp + 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 so + # this workflow is safe to merge before that: it stays green and simply + # skips SBOM generation until a ref that carries the script is used. + - 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. Re-run via 'Run workflow' with wolfssl_ref set to a branch that has it until it merges to wolfssl master." + 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 (wolfSSL SBOM change not merged yet)." + fi + + - name: Configure and build wolfssh + if: steps.gate.outputs.have == 'yes' + working-directory: wolfssh + run: | + autoreconf -ivf + ./configure --with-wolfssl="$GITHUB_WORKSPACE/wolfssl-install" + make -j"$(nproc)" + + - name: Generate SBOM + if: steps.gate.outputs.have == 'yes' + working-directory: wolfssh + run: make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + + - name: Outputs exist and SPDX validates + if: steps.gate.outputs.have == 'yes' + working-directory: wolfssh + run: | + ls wolfssh-*.cdx.json wolfssh-*.spdx.json wolfssh-*.spdx + pyspdxtools --infile wolfssh-*.spdx.json + + - name: CycloneDX is valid JSON with expected identity + if: steps.gate.outputs.have == 'yes' + working-directory: wolfssh + run: | + python3 - <<'PY' + import glob, json + cdx = json.load(open(glob.glob('wolfssh-*.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'] == 'wolfssh', m['name'] + assert m['purl'].startswith('pkg:github/wolfSSL/wolfssh@'), m['purl'] + props = {p['name'] for p in m.get('properties', [])} + # The AM_CPPFLAGS/config.h snapshot must capture real build config; + # PACKAGE_VERSION always lands from config.h, so its absence means + # the options snapshot regressed to empty. + assert any(n.startswith('wolfssl:build:') for n in props), \ + 'no wolfssl:build:* properties - options snapshot is empty' + print('CDX identity ok:', m['name'], m['purl']) + PY + + - name: Reproducible across two runs (SOURCE_DATE_EPOCH) + if: steps.gate.outputs.have == 'yes' + working-directory: wolfssh + run: | + rm -f wolfssh-*.cdx.json wolfssh-*.spdx.json wolfssh-*.spdx + SOURCE_DATE_EPOCH=1700000000 make sbom \ + WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + sha256sum wolfssh-*.cdx.json wolfssh-*.spdx.json > /tmp/a.sums + rm -f wolfssh-*.cdx.json wolfssh-*.spdx.json wolfssh-*.spdx + SOURCE_DATE_EPOCH=1700000000 make sbom \ + WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + sha256sum wolfssh-*.cdx.json wolfssh-*.spdx.json > /tmp/b.sums + diff /tmp/a.sums /tmp/b.sums + + - name: wolfssl recorded as a dependency + wolfssh identity + if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_wolfssl == 'yes' + working-directory: wolfssh + run: | + python3 - <<'PY' + import glob, json + d = json.load(open(glob.glob('wolfssh-*.spdx.json')[0])) + pkgs = {p['name']: p for p in d['packages']} + assert 'wolfssl' in pkgs, list(pkgs) + main = pkgs['wolfssh'] + assert main['SPDXID'] == 'SPDXRef-Package-wolfssh', main['SPDXID'] + assert 'github.com/wolfSSL/wolfssh' in main['downloadLocation'], \ + main['downloadLocation'] + rels = [(r['spdxElementId'], r['relationshipType'], + r['relatedSpdxElement']) for r in d['relationships']] + assert ('SPDXRef-Package-wolfssh', 'DEPENDS_ON', + 'SPDXRef-Package-wolfssl') in rels, rels + print('wolfssl dependency + wolfssh identity ok') + PY + + # ---- Embedded / IDE path (no autotools) ---------------------------- + # Firmware customers don't run ./configure: there's no options.h and no + # installed libwolfssh to hash. gen-sbom reads config from user_settings.h + # (via pcpp) and hashes the wolfSSH source set instead. This exercises + # that path the same way the docs tell customers to invoke it. + - name: Generate embedded SBOM (user_settings.h + source set) + if: steps.gate.outputs.have == 'yes' + working-directory: wolfssh + run: | + VER=$(sed -n 's/^AC_INIT(\[wolfssh\],\[\([^]]*\)\].*/\1/p' configure.ac) + VER=${VER:-0.0.0} + mkdir -p sbom-embedded/cfg sbom-embedded-2 + # A minimal embedded-style config; the assertions below prove these + # #defines survive pcpp and land as build properties in the SBOM. + { + echo '#ifndef USER_SETTINGS_H' + echo '#define USER_SETTINGS_H' + echo '#define WOLFSSH_TERM' + echo '#define WOLFSSH_SFTP' + echo '#define WOLFSSH_SCP' + echo '#endif' + } > sbom-embedded/cfg/user_settings.h + DEP=() + if [ "${{ steps.gate.outputs.dep_wolfssl }}" = "yes" ]; then + DEP+=(--dep-wolfssl yes) + fi + gen() { + SOURCE_DATE_EPOCH=1700000000 python3 \ + "$GITHUB_WORKSPACE/wolfssl/scripts/gen-sbom" \ + --name wolfssh --version "$VER" \ + --license-file LICENSING \ + --user-settings wolfssh/settings.h \ + --user-settings-include . \ + --user-settings-include "$GITHUB_WORKSPACE/wolfssl" \ + --user-settings-include sbom-embedded/cfg \ + --user-settings-define WOLFSSL_USER_SETTINGS \ + --srcs src/*.c \ + "${DEP[@]}" \ + --cdx-out "$1/wolfssh-embedded.cdx.json" \ + --spdx-out "$1/wolfssh-embedded.spdx.json" + } + gen sbom-embedded + gen sbom-embedded-2 + # Reproducible: identical bytes across runs (SOURCE_DATE_EPOCH fixed, + # namespace is uuid5(name,version), source-set hash is path-independent). + a=$(sha256sum < sbom-embedded/wolfssh-embedded.cdx.json) + b=$(sha256sum < sbom-embedded-2/wolfssh-embedded.cdx.json) + test "$a" = "$b" || { echo "embedded SBOM not reproducible"; exit 1; } + + - name: Embedded SBOM validates + reflects user_settings.h + source hash + if: steps.gate.outputs.have == 'yes' + working-directory: wolfssh + run: | + pyspdxtools --infile sbom-embedded/wolfssh-embedded.spdx.json + python3 - <<'PY' + import json + cdx = json.load(open('sbom-embedded/wolfssh-embedded.cdx.json')) + m = cdx['metadata']['component'] + assert m['name'] == 'wolfssh', m['name'] + assert m['purl'].startswith('pkg:github/wolfSSL/wolfssh@'), m['purl'] + # Embedded identity is the source-set hash (no library artifact exists). + algs = {h['alg'] for h in m.get('hashes', [])} + assert algs, 'no component hash - source-set (--srcs) hash missing' + # Config must come from user_settings.h through pcpp, not be empty. + props = {p['name'] for p in m.get('properties', [])} + assert any(n.endswith('WOLFSSH_SFTP') for n in props), \ + 'user_settings.h option WOLFSSH_SFTP not captured: %r' % sorted(props) + print('embedded ok:', m['name'], m['purl'], + '| user_settings props:', + sorted(n for n in props if 'WOLFSSH' in n)) + PY + + - name: Embedded SBOM records wolfssl dependency + if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_wolfssl == 'yes' + working-directory: wolfssh + run: | + python3 - <<'PY' + import json + d = json.load(open('sbom-embedded/wolfssh-embedded.spdx.json')) + 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-wolfssh', 'DEPENDS_ON', + 'SPDXRef-Package-wolfssl') in rels, rels + print('embedded wolfssl dependency ok') + PY + + - name: Upload SBOM artefacts + if: always() && steps.gate.outputs.have == 'yes' + uses: actions/upload-artifact@v4 + with: + name: wolfssh-sbom-${{ github.sha }} + path: | + wolfssh/wolfssh-*.cdx.json + wolfssh/wolfssh-*.spdx.json + wolfssh/wolfssh-*.spdx + wolfssh/sbom-embedded/wolfssh-embedded.* + if-no-files-found: warn + retention-days: 90 diff --git a/Makefile.am b/Makefile.am index edf173a25..7ee6c359b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -7,6 +7,7 @@ noinst_PROGRAMS = nobase_include_HEADERS = check_PROGRAMS = dist_noinst_SCRIPTS = +CLEANFILES = #includes additional rules from aminclude.am @INC_AMINCLUDE@ @@ -80,157 +81,20 @@ merge-clean: @find ./ | $(GREP) \.BASE | xargs rm -f @find ./ | $(GREP) \~$$ | xargs rm -f -# SBOM generation (CRA compliance) -SBOM_CDX = wolfssh-$(PACKAGE_VERSION).cdx.json -SBOM_SPDX = wolfssh-$(PACKAGE_VERSION).spdx.json -SBOM_SPDX_TV = wolfssh-$(PACKAGE_VERSION).spdx -sbomdir = $(datadir)/doc/$(PACKAGE) - -# wolfSSH is licensed GPLv3-or-commercial. Its LICENSING file uses the -# abbreviation "GPLv3", which older gen-sbom detect_license does not recognise -# (it matches only the long form "GNU General Public License version 3") and so -# falls back to NOASSERTION. Default the override to the correct SPDX id so the -# SBOM is right regardless of gen-sbom version; commercial licensees can still -# override it (e.g. SBOM_LICENSE_OVERRIDE=LicenseRef-wolfSSL-Commercial). Once -# the detect_license GPLv-abbreviation fix lands in gen-sbom (wolfSSL/wolfssl#10343) -# this default is redundant-but-harmless (GPL-3.0-only is what detection returns). +# SBOM generation (CRA compliance). The recipe is shared across the wolfSSL +# stack's autotools products in scripts/sbom.am; wolfSSH just declares what it +# is (a library that links wolfSSL) and includes it. WOLFSSL_DIR must point to +# a wolfssl source tree containing scripts/gen-sbom. +SBOM_PKGNAME = wolfssh +SBOM_LICENSE_FILE = $(srcdir)/LICENSING +SBOM_DEP_WOLFSSL = yes + +# wolfSSH is GPLv3-or-commercial; its LICENSING uses the "GPLv3" abbreviation +# that older gen-sbom cannot parse (falls back to NOASSERTION). Default to the +# correct SPDX id so the SBOM is right regardless of gen-sbom version; commercial +# licensees can still override it (e.g. LicenseRef-wolfSSL-Commercial). +# Redundant-but-harmless once the detect_license fix lands in gen-sbom +# (wolfSSL/wolfssl#10343). SBOM_LICENSE_OVERRIDE ?= GPL-3.0-only -# Shared-library / Mach-O basenames in priority order (versioned first) so -# `make sbom` finds the built artifact on ELF, Mach-O, and PE targets. Static -# (.a) and Windows (.dll/.lib) variants are appended at the call-site because -# their prefixes differ. -WOLFSSH_LIB_DSO_BASENAMES = \ - libwolfssh.so.[0-9]* \ - libwolfssh.so \ - libwolfssh.[0-9]*.dylib \ - libwolfssh.dylib - -.PHONY: sbom install-sbom uninstall-sbom - -# Stage a `make install` into a private tree, discover the installed library -# artifact (shared or static, ELF/Mach-O/PE), hash it, capture the configured -# build macros, 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. Honors SOURCE_DATE_EPOCH for -# reproducible builds (set to the last git commit time when unset and a git -# tree is available), matching wolfssl's `make sbom`. -# -# User-overridable variables: -# WOLFSSL_DIR Path to a wolfssl source tree containing -# scripts/gen-sbom (required). -# SBOM_LICENSE_OVERRIDE SPDX expression to record as the licence. Defaults -# to GPL-3.0-only (wolfSSH's licence); commercial -# licensees override it (e.g. -# LicenseRef-wolfSSL-Commercial). -# 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 string recorded for the wolfssl dependency. -# Auto-detected from WOLFSSL_DIR/wolfssl/version.h or -# pkg-config when unset. -sbom: - @if test -z "$(PYTHON3)"; then \ - echo ""; \ - echo "ERROR: 'python3' not found in PATH. Cannot generate SBOM."; \ - echo ""; \ - exit 1; \ - fi - @if test -z "$(PYSPDXTOOLS)"; then \ - echo ""; \ - echo "ERROR: 'pyspdxtools' not found in PATH. Cannot validate SBOM."; \ - echo " Install: pip install spdx-tools"; \ - echo ""; \ - exit 1; \ - fi - @if test -z "$(WOLFSSL_DIR)"; then \ - echo ""; \ - echo "ERROR: WOLFSSL_DIR is not set. Cannot locate gen-sbom."; \ - echo " Re-run: make sbom WOLFSSL_DIR=/path/to/wolfssl"; \ - echo ""; \ - exit 1; \ - fi - @if test ! -f "$(WOLFSSL_DIR)/scripts/gen-sbom"; then \ - echo ""; \ - echo "ERROR: $(WOLFSSL_DIR)/scripts/gen-sbom not found."; \ - echo " Use a wolfSSL tree that includes SBOM support."; \ - echo ""; \ - exit 1; \ - fi - @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_lib=""; \ - for lib in \ - $(addprefix "$(abs_builddir)/_sbom_staging$(libdir)"/,$(WOLFSSH_LIB_DSO_BASENAMES)) \ - "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.dll \ - "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.dll.a \ - "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.lib \ - "$(abs_builddir)/_sbom_staging$(libdir)"/wolfssh.lib \ - "$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssh.a; do \ - if test -f "$$lib"; then sbom_lib="$$lib"; break; fi; \ - done; \ - if test -z "$$sbom_lib"; then \ - echo ""; \ - echo "ERROR: No installed wolfSSH library artifact found for SBOM."; \ - echo " Searched in $(abs_builddir)/_sbom_staging$(libdir)"; \ - echo " (configure with --enable-shared or --enable-static)"; \ - echo ""; \ - exit 1; \ - fi; \ - echo "SBOM: hashing $$sbom_lib"; \ - $(CC) -dM -E $(DEFAULT_INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ - $(if $(wildcard $(abs_builddir)/config.h),-include $(abs_builddir)/config.h) \ - -x c /dev/null > "$$_defines"; \ - wolfssl_ver="$(SBOM_WOLFSSL_VERSION)"; \ - if test -z "$$wolfssl_ver" && test -f "$(WOLFSSL_DIR)/wolfssl/version.h"; then \ - wolfssl_ver=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[ \t]*"\([^"]*\)".*/\1/p' \ - "$(WOLFSSL_DIR)/wolfssl/version.h"`; \ - 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 $(PYTHON3) $(WOLFSSL_DIR)/scripts/gen-sbom --help 2>/dev/null \ - | $(GREP) -q -- '--dep-wolfssl'; then \ - dep_args="--dep-wolfssl yes"; \ - if test -n "$$wolfssl_ver"; then \ - dep_args="$$dep_args --dep-version wolfssl=$$wolfssl_ver"; \ - 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 to"; \ - echo " enable it. The generated SBOM is valid either way."; \ - fi; \ - $(PYTHON3) $(WOLFSSL_DIR)/scripts/gen-sbom \ - --name wolfssh \ - --version $(PACKAGE_VERSION) \ - --supplier "wolfSSL Inc." \ - --license-file $(srcdir)/LICENSING \ - --options-h "$$_defines" \ - --lib "$$sbom_lib" \ - $$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) - -CLEANFILES = $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV) +include scripts/sbom.am diff --git a/scripts/sbom.am b/scripts/sbom.am new file mode 100644 index 000000000..313ac5d17 --- /dev/null +++ b/scripts/sbom.am @@ -0,0 +1,190 @@ +# 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); every +# product vendors a byte-identical copy so `make sbom` works from an unpacked +# release tarball, offline. Keep the copies in sync. +# +# --------------------------------------------------------------------------- +# 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_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. +SBOM_GEN = $(firstword $(wildcard $(srcdir)/scripts/gen-sbom) \ + $(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"; \ + $(CC) -dM -E $(DEFAULT_INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ + $(if $(wildcard $(abs_builddir)/config.h),-include $(abs_builddir)/config.h) \ + -x c /dev/null > "$$_defines"; \ + 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 "$$_defines" \ + --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 From fcd369cb63140b4d5d0662caa814f8be7140421d Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Thu, 9 Jul 2026 07:30:16 +0300 Subject: [PATCH 06/15] sbom: harden gen-sbom path resolution and align docs Guard the SBOM_GEN wolfSSL fallback with $(if $(WOLFSSL_DIR),...) so an unset WOLFSSL_DIR leaves it empty (yielding the intended "set WOLFSSL_DIR" error) instead of resolving to an absolute /scripts/gen-sbom that could run an unrelated host script. Reword the fragment header to describe the actual gen-sbom resolution (vendored copy if present, else WOLFSSL_DIR) rather than claiming offline tarball builds that aren't wired up yet. Condition the README's wolfSSL-dependency statement on a sufficiently new gen-sbom to match the existing degradation note. Signed-off-by: Sameeh Jubran --- README.md | 7 ++++--- scripts/sbom.am | 14 ++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d9bd8eb3c..c76f910a3 100644 --- a/README.md +++ b/README.md @@ -633,9 +633,10 @@ the apps directory for more information. wolfSSH 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, hashes the built library -artifact (shared or static; ELF, Mach-O, or PE), and lists wolfSSL as a -dependency so vulnerability scanners can associate wolfSSL advisories with a -wolfSSH deployment. Output is reproducible: set `SOURCE_DATE_EPOCH` (or build +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 wolfSSH 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. diff --git a/scripts/sbom.am b/scripts/sbom.am index 313ac5d17..c3844fd23 100644 --- a/scripts/sbom.am +++ b/scripts/sbom.am @@ -5,9 +5,11 @@ # 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); every -# product vendors a byte-identical copy so `make sbom` works from an unpacked -# release tarball, offline. Keep the copies in sync. +# 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`: @@ -53,8 +55,12 @@ 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) \ - $(WOLFSSL_DIR)/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. From ccbaeccfdf6c23b40cc1cbf14f094d246386e6ad Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Thu, 9 Jul 2026 10:34:06 +0300 Subject: [PATCH 07/15] sbom: default license to GPL-3.0-or-later and pin CI to #10343 ref Signed-off-by: Sameeh Jubran --- .github/workflows/sbom.yml | 13 ++++++++----- Makefile.am | 14 +++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 1d60f26c9..f8bf571bb 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -9,7 +9,8 @@ on: inputs: wolfssl_ref: description: 'wolfssl git ref that provides scripts/gen-sbom' - default: 'master' + # TODO: switch back to 'master' once wolfSSL/wolfssl#10343 merges. + default: 'refs/pull/10343/head' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -32,14 +33,16 @@ jobs: # wolfssl is checked out once and used for two things: built + installed # so wolfssh has a library to link, and its source tree (scripts/gen-sbom - # + wolfssl/version.h) is passed to `make sbom` via WOLFSSL_DIR. Default - # ref is master; until the wolfSSL SBOM change is on master, use the - # "Run workflow" button with wolfssl_ref set to the SBOM branch. + # + wolfssl/version.h) is passed to `make sbom` via WOLFSSL_DIR. 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 || 'master' }} + ref: ${{ github.event.inputs.wolfssl_ref || 'refs/pull/10343/head' }} path: wolfssl - name: Install SBOM validator (pyspdxtools) and pcpp diff --git a/Makefile.am b/Makefile.am index 7ee6c359b..b04ea3555 100644 --- a/Makefile.am +++ b/Makefile.am @@ -89,12 +89,12 @@ SBOM_PKGNAME = wolfssh SBOM_LICENSE_FILE = $(srcdir)/LICENSING SBOM_DEP_WOLFSSL = yes -# wolfSSH is GPLv3-or-commercial; its LICENSING uses the "GPLv3" abbreviation -# that older gen-sbom cannot parse (falls back to NOASSERTION). Default to the -# correct SPDX id so the SBOM is right regardless of gen-sbom version; commercial -# licensees can still override it (e.g. LicenseRef-wolfSSL-Commercial). -# Redundant-but-harmless once the detect_license fix lands in gen-sbom -# (wolfSSL/wolfssl#10343). -SBOM_LICENSE_OVERRIDE ?= GPL-3.0-only +# wolfSSH 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. Its +# LICENSING summary uses the "GPLv3" abbreviation, which gen-sbom's +# detect_license maps to GPL-3.0-only -- contradicting the headers -- so pin the +# header-accurate SPDX id here. Commercial licensees can still override it +# (e.g. LicenseRef-wolfSSL-Commercial). +SBOM_LICENSE_OVERRIDE ?= GPL-3.0-or-later include scripts/sbom.am From f610a9a63879df84bc555dcba82d115d47c10e89 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Mon, 13 Jul 2026 18:17:33 +0300 Subject: [PATCH 08/15] scripts/sbom.am: capture AM_CFLAGS/CFLAGS and configurable config.h Sync the vendored fragment with the canonical wolfSSL copy: add $(AM_CFLAGS) $(CFLAGS) to the -dM -E build-options capture and make the config header path overridable via SBOM_CONFIG_H (default config.h) so the SBOM records the configured feature macros. Signed-off-by: Sameeh Jubran --- scripts/sbom.am | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/sbom.am b/scripts/sbom.am index c3844fd23..0dcfcd927 100644 --- a/scripts/sbom.am +++ b/scripts/sbom.am @@ -33,6 +33,13 @@ # 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) @@ -48,6 +55,7 @@ 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 @@ -84,7 +92,9 @@ CLEANFILES += $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV) # 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 +# configured build macros (AM_CPPFLAGS/AM_CFLAGS/CFLAGS + config.h; some +# products carry their feature -D flags in AM_CFLAGS rather than AM_CPPFLAGS), +# 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). @@ -127,7 +137,8 @@ sbom: fi; \ echo "SBOM: hashing $$sbom_art"; \ $(CC) -dM -E $(DEFAULT_INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ - $(if $(wildcard $(abs_builddir)/config.h),-include $(abs_builddir)/config.h) \ + $(AM_CFLAGS) $(CFLAGS) \ + $(if $(wildcard $(SBOM_CONFIG_H)),-include $(SBOM_CONFIG_H)) \ -x c /dev/null > "$$_defines"; \ if test -z "$${SOURCE_DATE_EPOCH:-}" && test -n "$(GIT)" && \ $(GIT) -C "$(srcdir)" rev-parse --git-dir >/dev/null 2>&1; then \ From 3f6fb8fb4d6cca765a3d708b5a47bb3b89b44f3d Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Tue, 14 Jul 2026 13:26:48 +0300 Subject: [PATCH 09/15] sbom: sync scripts/sbom.am with canonical copy and widen CI filter Re-sync scripts/sbom.am with the canonical wolfSSL copy ($(docdir) sbomdir, [[:space:]] version parse, GNU-make and uninstall-sbom docs) and widen the SBOM workflow pull_request filter to '**'. Signed-off-by: Sameeh Jubran --- .github/workflows/sbom.yml | 2 +- scripts/sbom.am | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index f8bf571bb..976410390 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -4,7 +4,7 @@ on: push: branches: [ 'master', 'main', 'release/**' ] pull_request: - branches: [ '*' ] + branches: [ '**' ] workflow_dispatch: inputs: wolfssl_ref: diff --git a/scripts/sbom.am b/scripts/sbom.am index 0dcfcd927..606e1fd59 100644 --- a/scripts/sbom.am +++ b/scripts/sbom.am @@ -5,11 +5,12 @@ # 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. +# 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`: @@ -48,6 +49,10 @@ # 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 @@ -60,7 +65,9 @@ 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 -sbomdir = $(datadir)/doc/$(PACKAGE) +# 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 @@ -152,7 +159,7 @@ sbom: 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' \ + wv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ "$(WOLFSSL_DIR)/wolfssl/version.h"`; \ fi; \ if test -n "$$wv"; then \ @@ -204,4 +211,8 @@ uninstall-sbom: -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 From 10d1a4629f92b9c0734742c36ae98a5c07c1f19c Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Thu, 16 Jul 2026 17:01:58 +0300 Subject: [PATCH 10/15] sbom: re-vendor unified canonical scripts/sbom.am Sync scripts/sbom.am with the unified canonical copy in wolfSSL (scripts/sbom.am), which adds the SBOM_OPTIONS_H override so one fragment supports both options.h- and config.h/AM_CFLAGS-based macro capture. wolfssh uses the default config.h path and does not set SBOM_OPTIONS_H, so behavior is unchanged. Keeps the vendored fragment byte-identical to the canonical. Signed-off-by: Sameeh Jubran --- scripts/sbom.am | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/scripts/sbom.am b/scripts/sbom.am index 606e1fd59..509735985 100644 --- a/scripts/sbom.am +++ b/scripts/sbom.am @@ -20,6 +20,12 @@ # (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). @@ -99,8 +105,9 @@ CLEANFILES += $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV) # 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/AM_CFLAGS/CFLAGS + config.h; some -# products carry their feature -D flags in AM_CFLAGS rather than AM_CPPFLAGS), +# 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 @@ -143,10 +150,14 @@ sbom: exit 1; \ fi; \ echo "SBOM: hashing $$sbom_art"; \ - $(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"; \ + 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`; \ @@ -190,7 +201,7 @@ sbom: --version $(PACKAGE_VERSION) \ --supplier "wolfSSL Inc." \ --license-file $(SBOM_LICENSE_FILE) \ - --options-h "$$_defines" \ + --options-h "$$opts_h" \ --lib "$$sbom_art" \ $$dep_args \ $(if $(SBOM_LICENSE_OVERRIDE),--license-override '$(SBOM_LICENSE_OVERRIDE)') \ From 7e22d45a1aeb547361a904af0f591e4244180739 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Tue, 25 Aug 2026 18:06:24 -0700 Subject: [PATCH 11/15] fix: unpin SBOM CI from merged wolfssl PR ref wolfSSL/wolfssl#10343 merged on 2026-07-27, but the SBOM workflow still checked out wolfssl at refs/pull/10343/head. GitHub keeps PR refs alive after merge, so CI stayed green while validating a pre-merge snapshot rather than master. Point wolfssl_ref and its fallback at master. Distinguish the two gen-sbom lookup failures. A set-but-wrong WOLFSSL_DIR previously printed the "re-run with WOLFSSL_DIR=..." advice to someone who had already set it; it now names the path that was probed and states that WOLFSSL_DIR is a source tree, not an install prefix. Refresh the README and in-recipe notes that still told users to track the #10343 branch until it merged. --- .github/workflows/sbom.yml | 21 ++++++++------------- README.md | 11 ++++++----- scripts/sbom.am | 16 +++++++++++----- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 976410390..ea8665cef 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -9,8 +9,7 @@ on: 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' + default: 'master' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -34,15 +33,12 @@ jobs: # wolfssl is checked out once and used for two things: built + installed # so wolfssh has a library to link, and its source tree (scripts/gen-sbom # + wolfssl/version.h) is passed to `make sbom` via WOLFSSL_DIR. 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. + # landed on wolfssl master in wolfSSL/wolfssl#10343, so CI tracks master. - 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' }} + ref: ${{ github.event.inputs.wolfssl_ref || 'master' }} path: wolfssl - name: Install SBOM validator (pyspdxtools) and pcpp @@ -60,17 +56,16 @@ jobs: 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 so - # this workflow is safe to merge before that: it stays green and simply - # skips SBOM generation until a ref that carries the script is used. + # gen-sbom is on wolfssl master (wolfSSL/wolfssl#10343), but an older + # wolfssl_ref may predate it. Gate on its presence so a deliberately + # pinned older ref skips SBOM generation rather than failing. - 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. Re-run via 'Run workflow' with wolfssl_ref set to a branch that has it until it merges to wolfssl master." + echo "::notice::wolfssl scripts/gen-sbom not present on this ref; skipping SBOM generation. It is on wolfssl master; re-run via 'Run workflow' with wolfssl_ref=master." exit 0 fi echo "have=yes" >> "$GITHUB_OUTPUT" @@ -78,7 +73,7 @@ jobs: 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 (wolfSSL SBOM change not merged yet)." + echo "::notice::gen-sbom on this ref has no --dep-wolfssl; the wolfssl dependency + name-derived identity assertions will be skipped (ref predates wolfSSL/wolfssl#10343)." fi - name: Configure and build wolfssh diff --git a/README.md b/README.md index c76f910a3..6e38a4d45 100644 --- a/README.md +++ b/README.md @@ -645,8 +645,8 @@ 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). +must point to a wolfssl source tree containing `scripts/gen-sbom` (on `master` +since wolfSSL/wolfssl#10343). This is a source checkout, not an install prefix. Output: `wolfssh-.cdx.json`, `wolfssh-.spdx.json`, `wolfssh-.spdx` @@ -667,8 +667,9 @@ make uninstall-sbom ``` Note: recording wolfSSL as a dependency and emitting wolfSSH-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. +URLs require the `gen-sbom` added in wolfSSL/wolfssl#10343 and present on +wolfssl master. 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). diff --git a/scripts/sbom.am b/scripts/sbom.am index 509735985..92b3bdca9 100644 --- a/scripts/sbom.am +++ b/scripts/sbom.am @@ -120,8 +120,14 @@ sbom: 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"; \ + if test -n "$(WOLFSSL_DIR)"; then \ + echo "ERROR: no gen-sbom at $(WOLFSSL_DIR)/scripts/gen-sbom"; \ + echo " WOLFSSL_DIR must name a wolfSSL *source tree*, not an"; \ + echo " install prefix such as /usr/local or /opt/wolfssl."; \ + else \ + echo "ERROR: gen-sbom not found. Vendor scripts/gen-sbom, or re-run:"; \ + echo " make sbom WOLFSSL_DIR=/path/to/wolfssl"; \ + fi; \ exit 1; } @rm -rf $(abs_builddir)/_sbom_staging @set -e; \ @@ -179,9 +185,9 @@ sbom: 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."; \ + echo " support landed on wolfssl master in wolfSSL/wolfssl#10343;"; \ + echo " point WOLFSSL_DIR at a wolfssl checkout that includes it."; \ + echo " The generated SBOM is valid either way."; \ fi; \ fi; \ if test "$(SBOM_DEP_OPENSSL)" = yes; then \ From feaf6929dbdc1fe388dfbb2ee890fd4f59b31e9d Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Tue, 25 Aug 2026 18:25:19 -0700 Subject: [PATCH 12/15] fix: fail SBOM CI when gen-sbom missing on master The gate skipped SBOM generation and exited 0 whenever wolfssl's scripts/gen-sbom was absent, so the job could report success having never exercised `make sbom`. That was deliberate while the script was unmerged, but it landed on master in wolfSSL/wolfssl#10343. On master, a missing gen-sbom or one too old for --dep-wolfssl is a regression, so fail instead of skipping: a green run now always means the target was exercised and the wolfssl-dependency assertions ran. Only a deliberately pinned older wolfssl_ref still degrades to a skip. The ref is passed through env rather than interpolated into the shell script, to keep the workflow expression out of the command line. --- .github/workflows/sbom.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index ea8665cef..828d39e61 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -56,24 +56,36 @@ jobs: make -j"$(nproc)" make install - # gen-sbom is on wolfssl master (wolfSSL/wolfssl#10343), but an older - # wolfssl_ref may predate it. Gate on its presence so a deliberately - # pinned older ref skips SBOM generation rather than failing. + # gen-sbom is on wolfssl master (wolfSSL/wolfssl#10343). On master its + # absence, or a gen-sbom too old to support --dep-wolfssl, is a real + # regression: fail rather than skip, so a green run always means + # `make sbom` was actually exercised. Only a deliberately pinned older + # wolfssl_ref, which may predate the script, degrades to a skip. - name: Detect gen-sbom availability and capabilities id: gate + env: + WOLFSSL_REF: ${{ github.event.inputs.wolfssl_ref || 'master' }} run: | GS="$GITHUB_WORKSPACE/wolfssl/scripts/gen-sbom" if [ ! -f "$GS" ]; then + if [ "$WOLFSSL_REF" = master ]; then + echo "::error::wolfssl scripts/gen-sbom is missing on master; it landed in wolfSSL/wolfssl#10343, so its absence is a regression. Failing instead of skipping SBOM generation." + exit 1 + fi echo "have=no" >> "$GITHUB_OUTPUT" - echo "::notice::wolfssl scripts/gen-sbom not present on this ref; skipping SBOM generation. It is on wolfssl master; re-run via 'Run workflow' with wolfssl_ref=master." + echo "::notice::wolfssl scripts/gen-sbom not present on pinned ref '$WOLFSSL_REF'; skipping SBOM generation. Re-run with wolfssl_ref=master to exercise it." 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 + if [ "$WOLFSSL_REF" = master ]; then + echo "::error::gen-sbom on master has no --dep-wolfssl support; that is part of wolfSSL/wolfssl#10343. Failing instead of passing with the wolfssl-dependency assertions skipped." + exit 1 + fi 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 (ref predates wolfSSL/wolfssl#10343)." + echo "::notice::gen-sbom on pinned ref '$WOLFSSL_REF' has no --dep-wolfssl; the wolfssl dependency + name-derived identity assertions will be skipped (ref predates wolfSSL/wolfssl#10343)." fi - name: Configure and build wolfssh From 51408ba512ff85ab547ce191a30a9be0ef3000d2 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Wed, 26 Aug 2026 07:51:25 -0700 Subject: [PATCH 13/15] fix: read wolfSSL version from WOLFSSL_DIR only The wolfSSL dependency version came solely from WOLFSSL_DIR/wolfssl/version.h. That header is generated by configure as well as tracked, so `make distclean` in the wolfSSL tree removes it. With it gone the --dep-version override was never passed and gen-sbom fell back to `pkg-config --modversion wolfssl`, which reports the *installed* wolfSSL. On a host whose installed build differs from WOLFSSL_DIR the SBOM recorded that unrelated version (9.9.9 in the report) and still exited 0, attesting a component wolfSSH was not built against. Fall back to AC_INIT in WOLFSSL_DIR/configure.ac, which survives distclean, and fail when neither source is readable rather than letting an installed copy answer for the tree. SBOM_WOLFSSL_VERSION still overrides both. --- README.md | 11 ++++++++--- scripts/sbom.am | 27 +++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6e38a4d45..52cac6809 100644 --- a/README.md +++ b/README.md @@ -657,9 +657,14 @@ Optional overrides: licensees). - `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. +- `SBOM_WOLFSSL_VERSION` - version recorded for the wolfSSL dependency. When + unset it is read from `WOLFSSL_DIR/wolfssl/version.h`, falling back to + `AC_INIT` in `WOLFSSL_DIR/configure.ac`. Note `wolfssl/version.h` is generated + by configure, so `make distclean` in the wolfSSL tree removes it until you + re-run configure; the `configure.ac` fallback covers that case. If neither is + readable, `make sbom` fails rather than falling back to `pkg-config`, which + reports the *installed* wolfSSL and may describe a different build than + `WOLFSSL_DIR`. ```sh make install-sbom # installs to $(datadir)/doc/wolfssh/ diff --git a/scripts/sbom.am b/scripts/sbom.am index 92b3bdca9..36437f9ca 100644 --- a/scripts/sbom.am +++ b/scripts/sbom.am @@ -36,8 +36,13 @@ # 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_WOLFSSL_VERSION Version recorded for the wolfSSL dependency. When +# unset it is read from WOLFSSL_DIR/wolfssl/version.h, +# else from WOLFSSL_DIR/configure.ac (version.h is +# generated, so 'make distclean' removes it). If neither +# is readable the build fails rather than letting gen-sbom +# fall back to pkg-config, which describes the installed +# wolfSSL rather than the one at WOLFSSL_DIR. # 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 @@ -179,9 +184,23 @@ sbom: 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"; \ + if test -z "$$wv" && test -f "$(WOLFSSL_DIR)/configure.ac"; then \ + wv=`sed -n 's/^AC_INIT(\[[^]]*\],\[\([^]]*\)\].*/\1/p' \ + "$(WOLFSSL_DIR)/configure.ac" | sed -n 1p`; \ fi; \ + if test -z "$$wv"; then \ + echo "ERROR: cannot determine the wolfSSL version from"; \ + echo " $(WOLFSSL_DIR)"; \ + echo " Neither wolfssl/version.h (removed by 'make distclean',"; \ + echo " restored by configure) nor configure.ac was readable."; \ + echo " Refusing to fall back to pkg-config: that reports the"; \ + echo " *installed* wolfSSL, which may be a different build"; \ + echo " than WOLFSSL_DIR, and would record a wrong version in"; \ + echo " the SBOM. Re-run configure in that tree, or pass"; \ + echo " SBOM_WOLFSSL_VERSION=X.Y.Z explicitly."; \ + exit 1; \ + fi; \ + dep_args="$$dep_args --dep-version wolfssl=$$wv"; \ else \ echo "NOTE: this gen-sbom has no --dep-wolfssl support, so the SBOM"; \ echo " will not list wolfssl as a dependency component. That"; \ From 0072eaa4afe640783584230f02e2eb1c250d3616 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Wed, 26 Aug 2026 08:26:50 -0700 Subject: [PATCH 14/15] test: assert SBOM wolfSSL version comes from WOLFSSL_DIR The dependency assertion checked only that wolfssl was present with a DEPENDS_ON edge, never that the recorded version matched WOLFSSL_DIR, so the SBOM could attest a version wolfSSH was not built against and still pass. Compare versionInfo against the tree. That assertion alone would not have caught it: the runner installs wolfssl from the same tree it passes as WOLFSSL_DIR, so pkg-config and wolfssl/version.h always agree and a wrong source is indistinguishable from a right one. Add a step that forces them apart -- a stub pkg-config reports a sentinel version, and a distclean'd copy of the tree has no generated version.h, so the configure.ac fallback is the only route to a correct answer. Reverting the recipe fix makes this step fail with the sentinel, and the fallback path now has CI coverage it lacked. --- .github/workflows/sbom.yml | 72 +++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 828d39e61..cd0a33fc2 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -146,6 +146,8 @@ jobs: - name: wolfssl recorded as a dependency + wolfssh identity if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_wolfssl == 'yes' working-directory: wolfssh + env: + WOLFSSL_DIR: ${{ github.workspace }}/wolfssl run: | python3 - <<'PY' import glob, json @@ -160,7 +162,75 @@ jobs: r['relatedSpdxElement']) for r in d['relationships']] assert ('SPDXRef-Package-wolfssh', 'DEPENDS_ON', 'SPDXRef-Package-wolfssl') in rels, rels - print('wolfssl dependency + wolfssh identity ok') + # The recorded version must be the one from WOLFSSL_DIR. Presence + # alone passed while the version was silently taken from the + # installed wolfSSL instead of the source tree. + import os, re + wd = os.environ['WOLFSSL_DIR'] + want = None + vh = os.path.join(wd, 'wolfssl', 'version.h') + if os.path.exists(vh): + m = re.search(r'LIBWOLFSSL_VERSION_STRING\s+"([^"]+)"', + open(vh).read()) + want = m and m.group(1) + if not want: + m = re.search(r'^AC_INIT\(\[[^]]*\],\[([^]]*)\]', + open(os.path.join(wd, 'configure.ac')).read(), re.M) + want = m and m.group(1) + assert want, 'could not determine expected wolfSSL version' + got = pkgs['wolfssl'].get('versionInfo') + assert got == want, f'SBOM says wolfssl {got!r}, WOLFSSL_DIR says {want!r}' + print(f'wolfssl dependency + wolfssh identity ok (version {got})') + PY + + # The runner installs wolfssl from the very tree it passes as + # WOLFSSL_DIR, so pkg-config and wolfssl/version.h always agree and a + # version taken from the wrong source is indistinguishable from the + # right one. Force them apart: a stub pkg-config reports a sentinel, + # and a distclean'd copy of the tree has no generated version.h, so the + # configure.ac fallback is the only path to a correct answer. This is + # the shape that reached a user -- `make sbom` reported the installed + # wolfSSL's version and exited 0. + - name: Version comes from WOLFSSL_DIR, not the installed wolfSSL + if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_wolfssl == 'yes' + env: + SENTINEL: 0.0.0-sentinel + run: | + set -euo pipefail + cp -a wolfssl wolfssl-distclean + ( cd wolfssl-distclean && make distclean >/dev/null 2>&1 || true ) + if [ -f wolfssl-distclean/wolfssl/version.h ]; then + echo "::error::distclean left wolfssl/version.h in place; this test needs it gone to exercise the configure.ac fallback." + exit 1 + fi + mkdir -p "$RUNNER_TEMP/stubbin" + REAL_PKGCONFIG="$(command -v pkg-config)" + cat > "$RUNNER_TEMP/stubbin/pkg-config" < Date: Wed, 26 Aug 2026 11:31:48 -0700 Subject: [PATCH 15/15] ci: retrigger checks after Actions outage The 2026-08-26 GitHub Actions major outage left this branch's checks in a mixed state: several workflows recorded startup_failure with no jobs created, and wolfSSHd Test could not be re-run because there was nothing to retry. No code change; this empty commit exists only to trigger a clean run of every workflow.