From 6d83dd00248479122e8656cbd530d5023f0a54ec Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Tue, 28 Apr 2026 13:36:30 -0700 Subject: [PATCH 1/2] feat: add CRA SBOM generation (make sbom) Adds `make sbom` producing CycloneDX 1.6 and SPDX 2.3 SBOMs for EU Cyber Resilience Act compliance. Generation is handled by scripts/gen-sbom (Python 3, stdlib only). The script stages a `make install`, hashes the installed libwolfssl.so, generates both formats, then removes the staging directory. pyspdxtools validates the SPDX JSON and converts it to tag-value (.spdx). Output files (all versioned): wolfssl-.cdx.json CycloneDX 1.6 JSON wolfssl-.spdx.json SPDX 2.3 JSON wolfssl-.spdx SPDX 2.3 tag-value SBOMs include: SHA-256 of the library, CPE, PURL, license detected from the LICENSING file, copyright, and build configuration (options.h defines as CDX properties). Optional external dependencies (liboqs, libxmss, liblms, libz) appear as separate components when enabled. Version detection for deps without pkg-config (libxmss, liblms) uses `git describe --tags --always` on the source tree root. configure.ac changes: - AC_SUBST ENABLED_LIBOQS/LIBXMSS/LIBLMS/LIBZ so the dep flags set during ./configure are visible in the generated Makefile - AC_SUBST LIBLMS_ROOT (XMSS_ROOT was already exported by wolfssl) so gen-sbom can locate the source tree for git describe - AC_PATH_PROG([GIT]) to find git robustly at configure time rather than relying on PATH at make sbom time - Initialize LIBLMS_ROOT="" before the liblms detection block, mirroring how XMSS_ROOT is defaulted in the disabled branch Also adds: doc/SBOM.md, INSTALL section 21, README one-liner, install-sbom / uninstall-sbom targets. --- INSTALL | 36 ++++ Makefile.am | 56 +++++++ README.md | 5 + configure.ac | 12 ++ doc/SBOM.md | 107 ++++++++++++ scripts/gen-sbom | 417 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 633 insertions(+) create mode 100644 doc/SBOM.md create mode 100755 scripts/gen-sbom 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/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() From ff31dbb6bab2f01ebc4232841742a33cd846aeb4 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Mon, 22 Jun 2026 12:28:50 -0700 Subject: [PATCH 2/2] build(cmake): add sbom/install-sbom/uninstall-sbom targets Adds three custom targets equivalent to the autotools make sbom, make install-sbom, and make uninstall-sbom targets added in this branch. gen-sbom already reads wolfssl/options.h which cmake generates via cmake/options.h.in, so no changes to the script are required. Uses $ for the library path instead of a staging install, which is cleaner and avoids the overhead of a full install just to obtain the .so path for SHA-256 hashing. DESTDIR is supported on install-sbom and uninstall-sbom via cmake/install-sbom.cmake and cmake/uninstall-sbom.cmake, which read $ENV{DESTDIR} at build time. This matches the autotools behaviour: DESTDIR=/staging cmake --build --target install-sbom Stub targets with clear error messages are emitted at configure time if python3 or pyspdxtools are not found, so cmake --build --target sbom fails descriptively rather than with a cryptic empty-command error. libz is hardcoded to --dep-libz no: LIBZ is a TODO in CMakeLists.txt and cannot be enabled in cmake builds today. --- CMakeLists.txt | 169 +++++++++++++++++++++++++++++++++++++ cmake/install-sbom.cmake | 38 +++++++++ cmake/uninstall-sbom.cmake | 29 +++++++ 3 files changed, 236 insertions(+) create mode 100644 cmake/install-sbom.cmake create mode 100644 cmake/uninstall-sbom.cmake 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/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}")