From 21b855842e93b4f13c6ebf6f63e89b219d8a1bae Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Mon, 22 Jun 2026 18:39:12 -0700 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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