diff --git a/CMakeLists.txt b/CMakeLists.txt index 663c3cf2cc9..b86534da0b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3249,3 +3249,172 @@ if(WOLFSSL_INSTALL) DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wolfssl ) endif() + +#################################################### +# SBOM generation +#################################################### +# +# Targets: +# sbom -- Generate CycloneDX 1.6 + SPDX 2.3 artifacts in CMAKE_BINARY_DIR +# install-sbom -- Copy artifacts to CMAKE_INSTALL_DOCDIR (triggers sbom first) +# uninstall-sbom -- Remove installed artifacts +# +# All three targets are always defined. If required tools were not found at +# configure time the targets fail with a clear error; re-run cmake after +# installing the tools to re-detect them. +# +# Output filenames match the autotools convention exactly so that CRA Kit +# scripts can locate them with the same pattern: +# wolfssl-.cdx.json CycloneDX 1.6 +# wolfssl-.spdx.json SPDX 2.3 JSON (input to pyspdxtools) +# wolfssl-.spdx SPDX 2.3 tag-value (pyspdxtools output) +# +# Equivalent autotools targets: make sbom / make install-sbom / make uninstall-sbom + +find_program(WOLFSSL_SBOM_PYTHON3 + NAMES python3 + DOC "Python 3 interpreter for SBOM generation (scripts/gen-sbom)") +find_program(WOLFSSL_SBOM_PYSPDXTOOLS + NAMES pyspdxtools + DOC "pyspdxtools SPDX validator for SBOM generation (pip install spdx-tools)") +find_program(WOLFSSL_SBOM_GIT + NAMES git + DOC "git binary for version detection by gen-sbom (optional; empty string if absent)") + +# git is optional: gen-sbom accepts --git "" and falls back to other version sources. +# find_program sets the variable to -NOTFOUND when the program is absent; +# normalise that to an empty string so VERBATIM does not pass the literal +# "WOLFSSL_SBOM_GIT-NOTFOUND" string to gen-sbom. +if(NOT WOLFSSL_SBOM_GIT) + set(WOLFSSL_SBOM_GIT "") +endif() + +# SBOM output file paths (build directory, matching autotools naming). +set(WOLFSSL_SBOM_CDX "${CMAKE_BINARY_DIR}/wolfssl-${PROJECT_VERSION}.cdx.json") +set(WOLFSSL_SBOM_SPDX "${CMAKE_BINARY_DIR}/wolfssl-${PROJECT_VERSION}.spdx.json") +set(WOLFSSL_SBOM_TV "${CMAKE_BINARY_DIR}/wolfssl-${PROJECT_VERSION}.spdx") + +# Map CMake feature options to gen-sbom --dep-* flags. +# +# libz: not yet implemented in the CMake build; see "TODO: - LIBZ" comment +# earlier in this file. Hardcoded "no" until a WOLFSSL_LIBZ option is +# added and wired up here. +# +# liblms / libxmss: being removed in wolfssl PR #10292. Mapped here so the +# SBOM remains accurate for builds that still enable them. The +# autotools --dep-libxmss-root and --dep-liblms-root arguments are +# omitted because CMakeLists.txt has no XMSS_ROOT / LIBLMS_ROOT +# equivalents. +set(_wolfssl_sbom_dep_liboqs "$,yes,no>") +set(_wolfssl_sbom_dep_libxmss "$,yes,no>") +set(_wolfssl_sbom_dep_liblms "$,yes,no>") +set(_wolfssl_sbom_dep_libz "no") + +if(NOT WOLFSSL_SBOM_PYTHON3 OR NOT WOLFSSL_SBOM_PYSPDXTOOLS) + # Stub targets: always defined so that cmake --build --target sbom produces a + # clear diagnostic rather than "No rule to make target 'sbom'". + # Re-run cmake after installing the missing tools to enable the real targets. + set(_wolfssl_sbom_missing "") + if(NOT WOLFSSL_SBOM_PYTHON3) + message(STATUS "SBOM: python3 not found -- 'sbom' target disabled") + string(APPEND _wolfssl_sbom_missing " python3") + endif() + if(NOT WOLFSSL_SBOM_PYSPDXTOOLS) + message(STATUS + "SBOM: pyspdxtools not found -- 'sbom' target disabled (pip install spdx-tools)") + string(APPEND _wolfssl_sbom_missing " pyspdxtools") + endif() + + foreach(_wolfssl_sbom_stub IN ITEMS sbom install-sbom uninstall-sbom) + add_custom_target(${_wolfssl_sbom_stub} + COMMAND ${CMAKE_COMMAND} -E echo + "ERROR: Required SBOM tools not found at configure time:${_wolfssl_sbom_missing}" + COMMAND ${CMAKE_COMMAND} -E echo + " Install the missing tools, re-run cmake, then retry." + COMMAND ${CMAKE_COMMAND} -E false + COMMENT "SBOM prerequisites missing -- re-run cmake to re-detect" + ) + endforeach() + unset(_wolfssl_sbom_stub) + unset(_wolfssl_sbom_missing) +else() + # gen-sbom reads wolfssl/options.h for build-configuration #defines. + # That file is produced by configure_file(cmake/options.h.in ...) during + # the cmake configure step, so it is always present before the sbom target + # runs and does not require a separate build step. + # + # --lib uses $ (the built library in the build tree) + # rather than a staging-install path. The autotools target stages an + # install solely to obtain the versioned .so path for SHA-256 hashing; + # the build-tree file is byte-for-byte identical and avoids that overhead. + add_custom_target(sbom + COMMAND "${WOLFSSL_SBOM_PYTHON3}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/gen-sbom" + --name wolfssl + --version "${PROJECT_VERSION}" + --license-file "${CMAKE_CURRENT_SOURCE_DIR}/LICENSING" + --options-h "${WOLFSSL_OUTPUT_BASE}/wolfssl/options.h" + --lib "$" + --dep-liboqs "${_wolfssl_sbom_dep_liboqs}" + --dep-libxmss "${_wolfssl_sbom_dep_libxmss}" + --dep-liblms "${_wolfssl_sbom_dep_liblms}" + --dep-libz "${_wolfssl_sbom_dep_libz}" + --git "${WOLFSSL_SBOM_GIT}" + --cdx-out "${WOLFSSL_SBOM_CDX}" + --spdx-out "${WOLFSSL_SBOM_SPDX}" + COMMAND "${WOLFSSL_SBOM_PYSPDXTOOLS}" + --infile "${WOLFSSL_SBOM_SPDX}" + --outfile "${WOLFSSL_SBOM_TV}" + COMMENT "Generating wolfSSL SBOM (CycloneDX 1.6 + SPDX 2.3)" + VERBATIM + ) + # wolfssl must be built before gen-sbom can hash $. + add_dependencies(sbom wolfssl) + + # install-sbom: copy SBOM artifacts to the documentation install directory. + # Mirrors autotools: sbomdir = $(datadir)/doc/$(PACKAGE). + # + # Delegates to cmake/install-sbom.cmake so that DESTDIR is read from the + # environment at build time rather than configure time. This matches the + # autotools behaviour: DESTDIR=/staging make install-sbom. + # CMake equivalent: DESTDIR=/staging cmake --build --target install-sbom + add_custom_target(install-sbom + COMMAND ${CMAKE_COMMAND} + "-DWOLFSSL_SBOM_CDX=${WOLFSSL_SBOM_CDX}" + "-DWOLFSSL_SBOM_SPDX=${WOLFSSL_SBOM_SPDX}" + "-DWOLFSSL_SBOM_TV=${WOLFSSL_SBOM_TV}" + "-DWOLFSSL_VERSION=${PROJECT_VERSION}" + "-DWOLFSSL_INSTALL_DOCDIR=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/install-sbom.cmake" + COMMENT "Installing wolfSSL SBOM to ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}" + VERBATIM + ) + # sbom must complete before install-sbom copies its outputs. + add_dependencies(install-sbom sbom) + + # uninstall-sbom: remove installed artifacts. + # Delegates to cmake/uninstall-sbom.cmake for the same DESTDIR reason. + # file(REMOVE ...) in that script is a no-op for absent files, matching + # autotools `rm -f`. + add_custom_target(uninstall-sbom + COMMAND ${CMAKE_COMMAND} + "-DWOLFSSL_VERSION=${PROJECT_VERSION}" + "-DWOLFSSL_INSTALL_DOCDIR=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/uninstall-sbom.cmake" + COMMENT + "Uninstalling wolfSSL SBOM from ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}" + VERBATIM + ) + + # Register SBOM outputs with cmake's clean target so they are removed by + # cmake --build . --target clean alongside compiled artifacts. + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY + ADDITIONAL_CLEAN_FILES + "${WOLFSSL_SBOM_CDX}" + "${WOLFSSL_SBOM_SPDX}" + "${WOLFSSL_SBOM_TV}") +endif() + +unset(_wolfssl_sbom_dep_liboqs) +unset(_wolfssl_sbom_dep_libxmss) +unset(_wolfssl_sbom_dep_liblms) +unset(_wolfssl_sbom_dep_libz) diff --git a/INSTALL b/INSTALL index dc6e2908c1a..f02dd44da3a 100644 --- a/INSTALL +++ b/INSTALL @@ -322,3 +322,39 @@ We also have vcpkg ports for wolftpm, wolfmqtt and curl. Deprecated. wolfSSL now has its own XMMS/XMSS^MT implementation in wolfCrypt. + +21. Generating an SBOM (Software Bill of Materials) + + wolfSSL can generate a Software Bill of Materials for EU Cyber Resilience + Act (CRA) compliance after a normal build and install. + + Prerequisites: + - python3 (detected automatically by configure) + - pyspdxtools (pip install spdx-tools) + + Usage: + + $ ./configure + $ make + $ make sbom + + This produces three files in the build directory: + + wolfssl-.cdx.json CycloneDX 1.6 JSON + wolfssl-.spdx.json SPDX 2.3 JSON + wolfssl-.spdx SPDX 2.3 tag-value (validated by pyspdxtools) + + The SPDX JSON is validated by pyspdxtools before the tag-value file is + written; make sbom fails if validation fails. + + To install the SBOM files to $(datadir)/doc/wolfssl/: + + $ make install-sbom + + To remove installed SBOM files: + + $ make uninstall-sbom + + The generated files are removed by make clean. + + For details on the SBOM contents and CRA context, see doc/SBOM.md. diff --git a/Makefile.am b/Makefile.am index fce812babf5..ea2a04db6b4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -350,3 +350,59 @@ merge-clean: .cu.lo: $(LIBTOOL) --tag=CC --mode=compile $(COMPILE) --compile -o $@ $< -static + +# SBOM generation (CRA compliance) +SBOM_CDX = wolfssl-$(PACKAGE_VERSION).cdx.json +SBOM_SPDX = wolfssl-$(PACKAGE_VERSION).spdx.json +SBOM_SPDX_TV = wolfssl-$(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 + rm -rf $(abs_builddir)/_sbom_staging + $(MAKE) install DESTDIR=$(abs_builddir)/_sbom_staging + $(PYTHON3) $(srcdir)/scripts/gen-sbom \ + --name $(PACKAGE) \ + --version $(PACKAGE_VERSION) \ + --license-file $(srcdir)/LICENSING \ + --options-h $(abs_builddir)/wolfssl/options.h \ + --lib $(abs_builddir)/_sbom_staging$(libdir)/libwolfssl.so.$(WOLFSSL_LIBRARY_VERSION_FIRST).$(WOLFSSL_LIBRARY_VERSION_SECOND).$(WOLFSSL_LIBRARY_VERSION_THIRD) \ + --dep-liboqs $(ENABLED_LIBOQS) \ + --dep-libxmss $(ENABLED_LIBXMSS) \ + --dep-libxmss-root '$(XMSS_ROOT)' \ + --dep-liblms $(ENABLED_LIBLMS) \ + --dep-liblms-root '$(LIBLMS_ROOT)' \ + --dep-libz $(ENABLED_LIBZ) \ + --git '$(GIT)' \ + --cdx-out $(abs_builddir)/$(SBOM_CDX) \ + --spdx-out $(abs_builddir)/$(SBOM_SPDX) + rm -rf $(abs_builddir)/_sbom_staging + $(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/README.md b/README.md index 4c800d85a40..4a3190eacec 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ applications which have previously used the OpenSSL package. For a complete feature list, see [Chapter 4](https://www.wolfssl.com/docs/wolfssl-manual/ch4/) of the wolfSSL manual. +## SBOM / CRA Compliance + +wolfSSL provides a Software Bill of Materials (SBOM) for EU Cyber Resilience +Act (CRA) compliance via `make sbom`. See `doc/SBOM.md` for details. + ## Notes, Please Read ### Note 1 diff --git a/cmake/install-sbom.cmake b/cmake/install-sbom.cmake new file mode 100644 index 00000000000..7d1dcc75354 --- /dev/null +++ b/cmake/install-sbom.cmake @@ -0,0 +1,38 @@ +# cmake -P script: install wolfSSL SBOM artifacts. +# Invoked by the install-sbom custom target. Reads DESTDIR from the +# environment at script-execution time (build time), so staging installs +# work correctly: +# +# DESTDIR=/staging cmake --build --target install-sbom +# +# Required -D arguments (passed by add_custom_target in CMakeLists.txt): +# WOLFSSL_SBOM_CDX path to generated CycloneDX JSON +# WOLFSSL_SBOM_SPDX path to generated SPDX JSON +# WOLFSSL_SBOM_TV path to generated SPDX tag-value +# WOLFSSL_VERSION project version string +# WOLFSSL_INSTALL_DOCDIR resolved install doc directory (absolute) + +foreach(_var WOLFSSL_SBOM_CDX WOLFSSL_SBOM_SPDX WOLFSSL_SBOM_TV + WOLFSSL_VERSION WOLFSSL_INSTALL_DOCDIR) + if(NOT DEFINED ${_var}) + message(FATAL_ERROR "install-sbom.cmake: required variable ${_var} not set") + endif() +endforeach() + +# DESTDIR is read from the environment at script-execution time so that +# `DESTDIR=/staging cmake --build . --target install-sbom` works the same +# way as `make install-sbom DESTDIR=/staging` with autotools. +if(DEFINED ENV{DESTDIR}) + set(_destdir "$ENV{DESTDIR}") +else() + set(_destdir "") +endif() + +set(_dest "${_destdir}${WOLFSSL_INSTALL_DOCDIR}") + +file(MAKE_DIRECTORY "${_dest}") +file(COPY "${WOLFSSL_SBOM_CDX}" DESTINATION "${_dest}") +file(COPY "${WOLFSSL_SBOM_SPDX}" DESTINATION "${_dest}") +file(COPY "${WOLFSSL_SBOM_TV}" DESTINATION "${_dest}") + +message(STATUS "Installed wolfSSL SBOM to ${_dest}") diff --git a/cmake/uninstall-sbom.cmake b/cmake/uninstall-sbom.cmake new file mode 100644 index 00000000000..3d6604c34f3 --- /dev/null +++ b/cmake/uninstall-sbom.cmake @@ -0,0 +1,29 @@ +# cmake -P script: uninstall wolfSSL SBOM artifacts. +# Invoked by the uninstall-sbom custom target. Reads DESTDIR from the +# environment at script-execution time (build time). +# +# Required -D arguments (passed by add_custom_target in CMakeLists.txt): +# WOLFSSL_VERSION project version string +# WOLFSSL_INSTALL_DOCDIR resolved install doc directory (absolute) + +foreach(_var WOLFSSL_VERSION WOLFSSL_INSTALL_DOCDIR) + if(NOT DEFINED ${_var}) + message(FATAL_ERROR "uninstall-sbom.cmake: required variable ${_var} not set") + endif() +endforeach() + +if(DEFINED ENV{DESTDIR}) + set(_destdir "$ENV{DESTDIR}") +else() + set(_destdir "") +endif() + +set(_dest "${_destdir}${WOLFSSL_INSTALL_DOCDIR}") + +# file(REMOVE ...) is a no-op for absent files, matching autotools `rm -f`. +file(REMOVE + "${_dest}/wolfssl-${WOLFSSL_VERSION}.cdx.json" + "${_dest}/wolfssl-${WOLFSSL_VERSION}.spdx.json" + "${_dest}/wolfssl-${WOLFSSL_VERSION}.spdx") + +message(STATUS "Uninstalled wolfSSL SBOM from ${_dest}") diff --git a/configure.ac b/configure.ac index 69f8501f04d..e0583b81580 100644 --- a/configure.ac +++ b/configure.ac @@ -1989,6 +1989,7 @@ done # liblms # Get the path to the hash-sigs LMS HSS lib. ENABLED_LIBLMS="no" +LIBLMS_ROOT="" tryliblmsdir="" AC_ARG_WITH([liblms], [AS_HELP_STRING([--with-liblms=PATH],[PATH to hash-sigs LMS/HSS install (default /usr/local) (requires --enable-experimental)!])], @@ -2051,6 +2052,7 @@ AC_ARG_WITH([liblms], AM_CFLAGS="$AM_CFLAGS -DHAVE_LIBLMS" ENABLED_LIBLMS="yes" + LIBLMS_ROOT=$tryliblmsdir ] ) @@ -11756,6 +11758,16 @@ AC_SUBST([WOLFSSL_PREFIX_ABS]) AC_SUBST([WOLFSSL_LIBDIR_ABS]) AC_SUBST([WOLFSSL_INCLUDEDIR_ABS]) +# SBOM generation +AC_PATH_PROG([PYTHON3], [python3]) +AC_PATH_PROG([PYSPDXTOOLS], [pyspdxtools]) +AC_PATH_PROG([GIT], [git]) +AC_SUBST([ENABLED_LIBOQS]) +AC_SUBST([ENABLED_LIBXMSS]) +AC_SUBST([ENABLED_LIBLMS]) +AC_SUBST([ENABLED_LIBZ]) +AC_SUBST([LIBLMS_ROOT]) + # FINAL AC_CONFIG_FILES([stamp-h], [echo timestamp > stamp-h]) AC_CONFIG_FILES([Makefile diff --git a/doc/SBOM.md b/doc/SBOM.md new file mode 100644 index 00000000000..0f201af053b --- /dev/null +++ b/doc/SBOM.md @@ -0,0 +1,107 @@ +# wolfSSL SBOM Generation + +wolfSSL generates a Software Bill of Materials (SBOM) to support compliance +with the EU Cyber Resilience Act (CRA), which requires software products +placed on the EU market to provide a machine-readable SBOM identifying all +software components. + +## Quick Start + +```sh +./configure +make +make sbom +``` + +This requires `python3` and `pyspdxtools` (`pip install spdx-tools`). +Both are detected by `configure`; `make sbom` fails with a clear error +message if either is missing. + +## Output Files + +`make sbom` produces three files in the build directory: + +| File | Format | Standard | Primary use | +|---|---|---|---| +| `wolfssl-.cdx.json` | JSON | CycloneDX 1.6 | Supply-chain tooling, VEX | +| `wolfssl-.spdx.json` | JSON | SPDX 2.3 | Machine processing | +| `wolfssl-.spdx` | Tag-value | SPDX 2.3 | Human review, archival | + +The `.spdx` tag-value file is produced by `pyspdxtools` converting the +`.spdx.json`. If the JSON fails SPDX validation, `make sbom` stops with +a non-zero exit and the tag-value file is not written. + +## Installing the SBOM + +```sh +make install-sbom # installs to $(datadir)/doc/wolfssl/ +make uninstall-sbom # removes the installed files +``` + +The generated files are removed by `make clean`. + +## SBOM Contents + +Both formats contain the same information: + +| Field | Value | +|---|---| +| Name | `wolfssl` | +| Version | from `configure.ac` (`PACKAGE_VERSION`) | +| Type | library | +| Supplier | wolfSSL Inc. | +| License | detected from `LICENSING` file (currently `GPL-3.0-only`) | +| Copyright | `Copyright (C) 2006- wolfSSL Inc.` | +| SHA-256 | hash of the installed `libwolfssl.so.X.Y.Z` | +| CPE | `cpe:2.3:a:wolfssl:wolfssl::*:*:*:*:*:*:*` | +| PURL | `pkg:generic/wolfssl@` | +| Download location | `https://github.com/wolfSSL/wolfssl` | +| Third-party deps | none (wolfssl has no runtime dependencies in a default build) | + +### License detection + +The license SPDX identifier is parsed from the `LICENSING` file at SBOM +generation time, not hardcoded. If the `LICENSING` file cannot be parsed, +`make sbom` warns and uses `NOASSERTION` rather than silently emitting a +wrong value. + +### Dual licensing + +wolfSSL is available under `GPL-3.0-only` for open-source use, with a +commercial license for proprietary products. The SBOM reflects the +open-source license. Commercial licensees should update the `licenseConcluded` +field to `LicenseRef-wolfSSL-Commercial` or their applicable SPDX expression +when distributing under a commercial agreement. + +## Validating the SBOM Manually + +```sh +# Validate SPDX JSON +pyspdxtools --infile wolfssl-.spdx.json + +# Convert to another format (e.g. RDF) +pyspdxtools --infile wolfssl-.spdx.json \ + --outfile wolfssl-.spdx.rdf +``` + +### External dependency version detection + +For dependencies with pkg-config support (`liboqs`, `libz`), the version is +queried via `pkg-config --modversion` at generation time. + +For dependencies without pkg-config (`libxmss`, `liblms`), wolfSSL is typically +built against a source checkout rather than an installed package. The generator +falls back to `git describe --tags --always` on the source tree root (passed via +`configure` as `XMSS_ROOT` / `LIBLMS_ROOT`). If the source tree has no tags, +`git describe` returns the short commit hash, which is recorded as-is. If the +source tree is unavailable or `git` is not found, version is recorded as +`NOASSERTION`. + +## Implementation Notes + +SBOM generation is implemented in `scripts/gen-sbom` (Python 3, stdlib only) +and hooked into the autotools build via `Makefile.am` and `configure.ac`. +The script stages a `make install` into a temporary directory, hashes the +installed library, generates both SBOM formats, then removes the staging +directory. The `pyspdxtools` validation and conversion step runs after +generation and gates the build on SPDX conformance. diff --git a/scripts/gen-sbom b/scripts/gen-sbom new file mode 100755 index 00000000000..ad893e2b6e6 --- /dev/null +++ b/scripts/gen-sbom @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +"""Generate CycloneDX 1.6 and SPDX 2.3 SBOMs for wolfssl.""" + +import argparse +import hashlib +import json +import re +import subprocess +import sys +import uuid +from datetime import datetime, timezone + + +# Known metadata for optional external dependencies. +# Version is detected at runtime via pkg-config; falls back to None. +DEP_META = { + 'liboqs': { + 'name': 'liboqs', + 'supplier': 'Open Quantum Safe', + 'license': 'MIT', + 'download': 'https://github.com/open-quantum-safe/liboqs', + 'pkgconfig': 'liboqs', + 'purl': lambda v: f'pkg:github/open-quantum-safe/liboqs@{v}', + }, + 'libxmss': { + 'name': 'xmss-reference', + 'supplier': 'XMSS reference implementation authors', + 'license': 'CC0-1.0', + 'download': 'https://github.com/XMSS/xmss-reference', + 'pkgconfig': None, + 'purl': lambda v: f'pkg:github/XMSS/xmss-reference@{v}', + }, + 'liblms': { + 'name': 'hash-sigs', + 'supplier': 'Cisco Systems', + 'license': 'MIT', + 'download': 'https://github.com/cisco/hash-sigs', + 'pkgconfig': None, + 'purl': lambda v: f'pkg:github/cisco/hash-sigs@{v}', + }, + 'libz': { + 'name': 'zlib', + 'supplier': 'Jean-loup Gailly and Mark Adler', + 'license': 'Zlib', + 'download': 'https://github.com/madler/zlib', + 'pkgconfig': 'zlib', + 'purl': lambda v: f'pkg:generic/zlib@{v}', + }, +} + + +def detect_license(license_file): + """Parse LICENSING file and return an SPDX license ID. + + Looks for 'GNU General Public License version N' and whether + 'or later' / 'or any later version' follows. Returns None and + prints a warning if the file cannot be parsed. + """ + try: + text = open(license_file).read() + except OSError as e: + print(f"WARNING: cannot read license file {license_file}: {e}", + file=sys.stderr) + return None + + m = re.search( + r'gnu general public license\s+version\s+(\d+)', + text, re.IGNORECASE + ) + if not m: + print(f"WARNING: no GPL version found in {license_file}", + file=sys.stderr) + return None + + version = m.group(1) + excerpt = text[m.end():m.end() + 100] + if re.search(r'or\s+(any\s+)?later', excerpt, re.IGNORECASE): + return f'GPL-{version}.0-or-later' + return f'GPL-{version}.0-only' + + +def sha256_file(path): + h = hashlib.sha256() + try: + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(65536), b''): + h.update(chunk) + except OSError as e: + sys.exit(f"ERROR: cannot read library for hashing: {e}") + return h.hexdigest() + + +GIT_BIN = None + + +def pkgconfig_version(pkgname): + """Return version string from pkg-config, or None if unavailable.""" + try: + r = subprocess.run( + ['pkg-config', '--modversion', pkgname], + capture_output=True, text=True + ) + if r.returncode == 0: + return r.stdout.strip() + except FileNotFoundError: + pass + return None + + +def git_describe_version(root, git_bin): + """Return version from git describe --tags --always, or None.""" + if not root or not git_bin: + return None + try: + r = subprocess.run( + [git_bin, '-C', root, 'describe', '--tags', '--always'], + capture_output=True, text=True + ) + if r.returncode == 0: + return r.stdout.strip() + except FileNotFoundError: + pass + return None + + +def dep_version(key): + pkgname = DEP_META[key]['pkgconfig'] + if pkgname: + return pkgconfig_version(pkgname) + git_root = DEP_META[key].get('git_root') + if git_root: + return git_describe_version(git_root, GIT_BIN) + return None + + +def parse_options_h(path): + """Parse wolfssl/options.h and return sorted deduplicated list of + (name, value) pairs for every #define found.""" + try: + text = open(path).read() + except OSError as e: + print(f"WARNING: cannot read options.h {path}: {e}", file=sys.stderr) + return [] + + defines = {} + for m in re.finditer(r'^#define[ \t]+(\w+)(?:[ \t]+(.+))?$', text, re.MULTILINE): + defines[m.group(1)] = (m.group(2) or '').strip() + return sorted(defines.items()) + + +def cdx_dep_component(key): + """Return (bom_ref, component_dict) for a CDX dependency component.""" + meta = DEP_META[key] + version = dep_version(key) + bom_ref = str(uuid.uuid4()) + comp = { + 'bom-ref': bom_ref, + 'type': 'library', + 'supplier': {'name': meta['supplier']}, + 'name': meta['name'], + 'licenses': [{'license': {'id': meta['license']}}], + 'externalReferences': [{'type': 'vcs', 'url': meta['download']}], + } + if version: + comp['version'] = version + comp['purl'] = meta['purl'](version) + else: + print(f"WARNING: version unknown for {meta['name']}; " + "omitting version and purl", file=sys.stderr) + return bom_ref, comp + + +def spdx_dep_package(key): + """Return (spdx_id, package_dict) for an SPDX dependency package.""" + meta = DEP_META[key] + version = dep_version(key) + spdx_id = 'SPDXRef-Package-' + re.sub(r'[^A-Za-z0-9.]', '', meta['name']) + pkg = { + 'SPDXID': spdx_id, + 'name': meta['name'], + 'versionInfo': version if version else 'NOASSERTION', + 'supplier': f"Organization: {meta['supplier']}", + 'downloadLocation': meta['download'], + 'filesAnalyzed': False, + 'licenseConcluded': meta['license'], + 'licenseDeclared': meta['license'], + 'copyrightText': 'NOASSERTION', + } + if version: + pkg['externalRefs'] = [{ + 'referenceCategory': 'PACKAGE-MANAGER', + 'referenceType': 'purl', + 'referenceLocator': meta['purl'](version), + }] + return spdx_id, pkg + + +def generate_cdx(name, version, supplier, license_id, lib_hash, + timestamp, serial, enabled_deps, build_props): + year = datetime.now(timezone.utc).year + bom_ref = str(uuid.uuid4()) + + dep_bom_refs = [] + components = [] + for key in enabled_deps: + ref, comp = cdx_dep_component(key) + dep_bom_refs.append(ref) + components.append(comp) + + properties = [ + {'name': f'wolfssl:build:{k}', 'value': v if v else '1'} + for k, v in build_props + ] + + return { + '$schema': 'http://cyclonedx.org/schema/bom-1.6.schema.json', + 'bomFormat': 'CycloneDX', + 'specVersion': '1.6', + 'serialNumber': f'urn:uuid:{serial}', + 'version': 1, + 'metadata': { + 'timestamp': timestamp, + 'tools': { + 'components': [{ + 'type': 'application', + 'author': 'wolfSSL Inc.', + 'name': 'wolfssl-sbom-gen', + 'version': '1.0' + }] + }, + 'component': { + 'bom-ref': bom_ref, + 'type': 'library', + 'supplier': {'name': supplier}, + 'name': name, + 'version': version, + 'licenses': [{'license': {'id': license_id}}], + 'copyright': f'Copyright (C) 2006-{year} wolfSSL Inc.', + 'cpe': f'cpe:2.3:a:wolfssl:{name}:{version}:*:*:*:*:*:*:*', + 'purl': f'pkg:generic/{name}@{version}', + 'hashes': [{'alg': 'SHA-256', 'content': lib_hash}], + 'externalReferences': [{ + 'type': 'vcs', + 'url': 'https://github.com/wolfSSL/wolfssl' + }], + 'properties': properties, + } + }, + 'components': components, + 'dependencies': [ + {'ref': bom_ref, 'dependsOn': dep_bom_refs}, + *[{'ref': r, 'dependsOn': []} for r in dep_bom_refs], + ], + } + + +def generate_spdx(name, version, supplier, license_id, lib_hash, + timestamp, doc_ns_uuid, enabled_deps, build_props): + year = datetime.now(timezone.utc).year + + build_defines = ', '.join(k for k, _ in build_props) + wolfssl_pkg = { + 'SPDXID': 'SPDXRef-Package-wolfssl', + 'name': name, + 'versionInfo': version, + 'supplier': f'Organization: {supplier}', + 'downloadLocation': 'https://github.com/wolfSSL/wolfssl', + 'filesAnalyzed': False, + 'checksums': [{'algorithm': 'SHA256', 'checksumValue': lib_hash}], + 'licenseConcluded': license_id, + 'licenseDeclared': license_id, + 'copyrightText': f'Copyright (C) 2006-{year} wolfSSL Inc.', + 'comment': f'Build configuration defines: {build_defines}', + 'externalRefs': [ + { + 'referenceCategory': 'SECURITY', + 'referenceType': 'cpe23Type', + 'referenceLocator': ( + f'cpe:2.3:a:wolfssl:{name}:{version}:*:*:*:*:*:*:*' + ) + }, + { + 'referenceCategory': 'PACKAGE-MANAGER', + 'referenceType': 'purl', + 'referenceLocator': f'pkg:generic/{name}@{version}' + } + ], + } + + packages = [wolfssl_pkg] + relationships = [{ + 'spdxElementId': 'SPDXRef-DOCUMENT', + 'relatedSpdxElement': 'SPDXRef-Package-wolfssl', + 'relationshipType': 'DESCRIBES', + }] + + for key in enabled_deps: + spdx_id, pkg = spdx_dep_package(key) + packages.append(pkg) + relationships.append({ + 'spdxElementId': 'SPDXRef-Package-wolfssl', + 'relatedSpdxElement': spdx_id, + 'relationshipType': 'DEPENDS_ON', + }) + + return { + 'spdxVersion': 'SPDX-2.3', + 'dataLicense': 'CC0-1.0', + 'SPDXID': 'SPDXRef-DOCUMENT', + 'name': f'{name}-{version}', + 'documentNamespace': ( + f'https://wolfssl.com/sbom/{name}-{version}-{doc_ns_uuid}' + ), + 'creationInfo': { + 'licenseListVersion': '3.28', + 'creators': [ + f'Organization: {supplier}', + 'Tool: wolfssl-sbom-gen-1.0' + ], + 'created': timestamp, + }, + 'packages': packages, + 'relationships': relationships, + } + + +def main(): + parser = argparse.ArgumentParser( + description='Generate CycloneDX and SPDX SBOMs for wolfssl' + ) + parser.add_argument('--name', required=True, help='Package name') + parser.add_argument('--version', required=True, help='Package version') + parser.add_argument('--supplier', default='wolfSSL Inc.', + help='Supplier name (default: wolfSSL Inc.)') + parser.add_argument('--lib', required=True, + help='Path to libwolfssl.so.X.Y.Z for SHA-256 hashing') + parser.add_argument('--license-file', required=True, + help='Path to LICENSING file for SPDX ID detection') + parser.add_argument('--options-h', required=True, + help='Path to wolfssl/options.h for build config') + parser.add_argument('--dep-liboqs', default='no', + help='yes if built with --with-liboqs') + parser.add_argument('--dep-libxmss', default='no', + help='yes if built with --with-libxmss') + parser.add_argument('--dep-libxmss-root', default='', + help='Path to xmss-reference source tree root') + parser.add_argument('--dep-liblms', default='no', + help='yes if built with --with-liblms') + parser.add_argument('--dep-liblms-root', default='', + help='Path to hash-sigs source tree root') + parser.add_argument('--dep-libz', default='no', + help='yes if built with --with-libz') + parser.add_argument('--git', default='', + help='Path to git binary for version detection') + parser.add_argument('--cdx-out', required=True, + help='Output path for CycloneDX JSON') + parser.add_argument('--spdx-out', required=True, + help='Output path for SPDX JSON') + args = parser.parse_args() + + global GIT_BIN + GIT_BIN = args.git or None + + if args.dep_libxmss_root: + DEP_META['libxmss']['git_root'] = args.dep_libxmss_root + if args.dep_liblms_root: + DEP_META['liblms']['git_root'] = args.dep_liblms_root + + enabled_deps = [ + key for key, flag in [ + ('liboqs', args.dep_liboqs), + ('libxmss', args.dep_libxmss), + ('liblms', args.dep_liblms), + ('libz', args.dep_libz), + ] + if flag.lower() == 'yes' + ] + + license_id = detect_license(args.license_file) + if license_id is None: + print("WARNING: license could not be determined; using NOASSERTION", + file=sys.stderr) + license_id = 'NOASSERTION' + + build_props = parse_options_h(args.options_h) + lib_hash = sha256_file(args.lib) + timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + serial = str(uuid.uuid4()) + doc_ns_uuid = str(uuid.uuid4()) + + cdx = generate_cdx( + args.name, args.version, args.supplier, + license_id, lib_hash, timestamp, serial, + enabled_deps, build_props, + ) + spdx = generate_spdx( + args.name, args.version, args.supplier, + license_id, lib_hash, timestamp, doc_ns_uuid, + enabled_deps, build_props, + ) + + try: + with open(args.cdx_out, 'w') as f: + json.dump(cdx, f, indent=2) + f.write('\n') + with open(args.spdx_out, 'w') as f: + json.dump(spdx, f, indent=2) + f.write('\n') + except OSError as e: + sys.exit(f"ERROR: cannot write SBOM output: {e}") + + print(f"Generated: {args.cdx_out}") + print(f"Generated: {args.spdx_out}") + + +if __name__ == '__main__': + main()