Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions .github/workflows/sbom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: SBOM Test

on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
workflow_dispatch:
inputs:
wolfssl_ref:
description: 'wolfssl git ref that provides scripts/gen-sbom'
# TODO: switch back to 'master' once wolfSSL/wolfssl#10343 merges.
default: 'refs/pull/10343/head'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# This workflow only reads the repo and uploads artefacts; no API writes.
permissions:
contents: read

jobs:
sbom:
name: wolfTPM SBOM generation (linux)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout wolftpm
uses: actions/checkout@v4
with:
path: wolftpm

# wolfTPM links wolfSSL/wolfCrypt (RNG, parameter encryption, PK
# callbacks), so its SBOM records wolfSSL as a dependency. wolfSSL is
# built + installed here so wolfTPM has a library to link, and the same
# source tree (scripts/gen-sbom + wolfssl/version.h) is passed to
# `make sbom` via WOLFSSL_DIR -- so the recorded wolfSSL dependency
# version matches the linked one. gen-sbom is not yet on wolfssl master,
# so default to the open PR head that carries it (wolfSSL/wolfssl#10343)
# so CI actually exercises `make sbom` instead of silently skipping.
# TODO: switch the fallback back to 'master' once #10343 merges.
- name: Checkout wolfssl (gen-sbom + library source)
uses: actions/checkout@v4
with:
repository: wolfSSL/wolfssl
ref: ${{ github.event.inputs.wolfssl_ref || 'refs/pull/10343/head' }}
path: wolfssl

- name: Install build tooling and SBOM validator (pyspdxtools)
run: |
sudo apt-get update
sudo apt-get install -y build-essential autoconf automake libtool \
pkg-config
python3 -m pip install --user 'spdx-tools==0.8.*'
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Build and install wolfssl
working-directory: wolfssl
run: |
autoreconf -ivf
./configure --enable-wolftpm --enable-pkcallbacks \
--prefix="$GITHUB_WORKSPACE/wolfssl-install"
make -j"$(nproc)"
make install

# gen-sbom lives in wolfssl and may not be on the checked-out ref yet (the
# wolfSSL SBOM change can land separately). Gate on its presence and on
# --dep-wolfssl so this workflow is safe against a gen-sbom that predates
# it.
- name: Detect gen-sbom availability and capabilities
id: gate
run: |
GS="$GITHUB_WORKSPACE/wolfssl/scripts/gen-sbom"
if [ ! -f "$GS" ]; then
echo "have=no" >> "$GITHUB_OUTPUT"
echo "::notice::wolfssl scripts/gen-sbom not present on this ref; skipping SBOM generation."
exit 0
fi
echo "have=yes" >> "$GITHUB_OUTPUT"
if python3 "$GS" --help 2>/dev/null | grep -q -- '--dep-wolfssl'; then
echo "dep_wolfssl=yes" >> "$GITHUB_OUTPUT"
else
echo "dep_wolfssl=no" >> "$GITHUB_OUTPUT"
echo "::notice::gen-sbom on this ref has no --dep-wolfssl; the wolfssl dependency + name-derived identity assertions will be skipped."
fi

- name: Configure and build wolftpm
if: steps.gate.outputs.have == 'yes'
working-directory: wolftpm
run: |
autoreconf -ivf
./configure --enable-swtpm --disable-fwtpm --disable-examples \
--with-wolfcrypt="$GITHUB_WORKSPACE/wolfssl-install"
make -j"$(nproc)"

- name: Generate SBOM
if: steps.gate.outputs.have == 'yes'
working-directory: wolftpm
run: make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"

- name: Outputs exist and SPDX validates
if: steps.gate.outputs.have == 'yes'
working-directory: wolftpm
run: |
ls wolftpm-*.cdx.json wolftpm-*.spdx.json wolftpm-*.spdx
pyspdxtools --infile wolftpm-*.spdx.json

- name: CycloneDX identity, licence, and captured options
if: steps.gate.outputs.have == 'yes'
working-directory: wolftpm
run: |
python3 - <<'PY'
import glob, json
cdx = json.load(open(glob.glob('wolftpm-*.cdx.json')[0]))
assert cdx['bomFormat'] == 'CycloneDX', cdx.get('bomFormat')
assert cdx['specVersion'] == '1.6', cdx.get('specVersion')
m = cdx['metadata']['component']
assert m['name'] == 'wolftpm', m['name']
assert m['purl'].startswith('pkg:github/wolfSSL/wolftpm@'), m['purl']
# Default override must land as GPL-3.0-or-later (matches source headers).
ids = [l.get('license', {}).get('id') for l in m.get('licenses', [])]
assert 'GPL-3.0-or-later' in ids, ids
# Identity is the hashed library artifact.
assert {h['alg'] for h in m.get('hashes', [])}, 'no component hash'
# SBOM_OPTIONS_H must have been parsed: wolfTPM records its feature
# macros in wolftpm/options.h, so the SBOM's build properties must be
# populated.
props = [p for p in m.get('properties', [])
if p.get('name', '').startswith('wolfssl:build:')]
assert props, 'no wolfssl:build:* properties captured from options.h'
print('CDX ok:', m['name'], m['purl'], ids, f'{len(props)} build props')
PY

- name: Reproducible across two runs (SOURCE_DATE_EPOCH)
if: steps.gate.outputs.have == 'yes'
working-directory: wolftpm
run: |
rm -f wolftpm-*.cdx.json wolftpm-*.spdx.json wolftpm-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
sha256sum wolftpm-*.cdx.json wolftpm-*.spdx.json > /tmp/a.sums
rm -f wolftpm-*.cdx.json wolftpm-*.spdx.json wolftpm-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
sha256sum wolftpm-*.cdx.json wolftpm-*.spdx.json > /tmp/b.sums
diff /tmp/a.sums /tmp/b.sums

- name: wolfssl recorded as a dependency
if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_wolfssl == 'yes'
working-directory: wolftpm
run: |
python3 - <<'PY'
import glob, json
d = json.load(open(glob.glob('wolftpm-*.spdx.json')[0]))
assert 'wolfssl' in {p['name'] for p in d['packages']}, \
[p['name'] for p in d['packages']]
rels = [(r['spdxElementId'], r['relationshipType'],
r['relatedSpdxElement']) for r in d['relationships']]
assert ('SPDXRef-Package-wolftpm', 'DEPENDS_ON',
'SPDXRef-Package-wolfssl') in rels, rels
print('wolfssl dependency ok')
PY

- name: Upload SBOM artefacts
if: always() && steps.gate.outputs.have == 'yes'
uses: actions/upload-artifact@v4
with:
name: wolftpm-sbom-${{ github.sha }}
path: |
wolftpm/wolftpm-*.cdx.json
wolftpm/wolftpm-*.spdx.json
wolftpm/wolftpm-*.spdx
if-no-files-found: warn
retention-days: 90
160 changes: 160 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -770,3 +770,163 @@ install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/wolftpm/
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/wolftpm/
DESTINATION include/wolftpm
FILES_MATCHING PATTERN "*.h")


####################################################
# SBOM generation target
####################################################
#
# Usage:
# cmake -B build -DWOLFSSL_DIR=/path/to/wolfssl/source .
# cmake --build build
# cmake --build build --target sbom
#
# WOLFSSL_DIR must point to a wolfssl source tree that contains
# scripts/gen-sbom (available on the feat/sbom-embedded branch).
#
# Outputs written to the build directory:
# wolftpm-<version>.cdx.json (CycloneDX)
# wolftpm-<version>.spdx.json (SPDX JSON)
# wolftpm-<version>.spdx (SPDX tag-value, pyspdxtools validation output)

if(BUILD_WOLFTPM_LIB)
set(WOLFSSL_DIR "" CACHE PATH
"Path to wolfssl source tree with scripts/gen-sbom")

# wolfTPM is GPLv3-or-later (per the per-file source headers: "either
# version 3 of the License, or (at your option) any later version") or
# commercial. Pin the header-accurate SPDX id so the SBOM is correct
# regardless of the gen-sbom version's licence detection; commercial
# licensees can override it (e.g. LicenseRef-wolfSSL-Commercial). This
# mirrors SBOM_LICENSE_OVERRIDE in the autotools build.
set(SBOM_LICENSE_OVERRIDE "GPL-3.0-or-later" CACHE STRING
"SPDX licence expression recorded in the SBOM")

# Derive the version from wolftpm/version.h, NOT from PROJECT_VERSION.
# autotools `make sbom` uses PACKAGE_VERSION, which is sourced from
# version.h. Reading the same header here keeps the cmake and autotools
# SBOMs bit-identical in the version field even if the project() call in
# this file drifts out of sync with the header.
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/wolftpm/version.h"
_sbom_version_line
REGEX "^#define[ ]+LIBWOLFTPM_VERSION_STRING[ ]+\"")
string(REGEX REPLACE
"^#define[ ]+LIBWOLFTPM_VERSION_STRING[ ]+\"([^\"]+)\".*" "\\1"
SBOM_VERSION "${_sbom_version_line}")
if(NOT SBOM_VERSION)
message(FATAL_ERROR
"sbom: could not parse LIBWOLFTPM_VERSION_STRING from "
"wolftpm/version.h")
endif()

# Validate the SBOM prerequisites at configure time so the user learns
# what is missing immediately, instead of after a full library build.
#
# These checks must NOT abort configuration of a normal build: someone who
# just wants `cmake -B build && cmake --build build` should never be forced
# to set WOLFSSL_DIR. So when a prerequisite is missing we still define a
# `sbom` target, but one that prints the reason and fails at build time.
# Only when the user explicitly opts in via -DWOLFSSL_DIR=... and that path
# turns out to be wrong do we hard-error at configure time, because at that
# point the user clearly intends to build SBOMs and a typo'd path is a
# mistake worth surfacing right away.
find_program(PYTHON3_CMD python3)
find_program(PYSPDXTOOLS_CMD pyspdxtools)

set(_sbom_error "")
if(WOLFSSL_DIR STREQUAL "")
set(_sbom_error
"WOLFSSL_DIR is not set. Re-run cmake with -DWOLFSSL_DIR=/path/to/wolfssl")
elseif(NOT EXISTS "${WOLFSSL_DIR}/scripts/gen-sbom")
# User opted in with a bad path -> fail configure now, not at build.
message(FATAL_ERROR
"sbom: ${WOLFSSL_DIR}/scripts/gen-sbom not found.\n"
" Check that WOLFSSL_DIR points to a wolfSSL tree with "
"SBOM support.")
elseif(NOT PYTHON3_CMD)
set(_sbom_error "'python3' not found in PATH. Cannot generate SBOM.")
elseif(NOT PYSPDXTOOLS_CMD)
set(_sbom_error
"'pyspdxtools' not found in PATH (install: pip install spdx-tools)")
endif()

if(NOT _sbom_error STREQUAL "")
Comment thread
dgarske marked this conversation as resolved.
# Prerequisite missing: keep configuration working, but make the sbom
# target fail loudly if someone actually invokes it.
add_custom_target(sbom
VERBATIM
COMMAND ${CMAKE_COMMAND} -E echo "sbom: ${_sbom_error}"
COMMAND ${CMAKE_COMMAND} -E false
COMMENT "SBOM prerequisites missing")
return()
endif()

set(SBOM_CDX "${CMAKE_BINARY_DIR}/wolftpm-${SBOM_VERSION}.cdx.json")
set(SBOM_SPDX "${CMAKE_BINARY_DIR}/wolftpm-${SBOM_VERSION}.spdx.json")
set(SBOM_SPDX_TV "${CMAKE_BINARY_DIR}/wolftpm-${SBOM_VERSION}.spdx")
set(SBOM_STAGING "${CMAKE_BINARY_DIR}/_sbom_staging")

# Staged install path. Derive the installed artifact name from the target
# itself so it tracks the real output - a versioned .so/.dylib, a static
# .a/.lib, or the Windows DLL - instead of a hardcoded
# libwolftpm${CMAKE_SHARED_LIBRARY_SUFFIX} that only matches a Unix shared
# build. install(TARGETS) sends the Windows DLL (RUNTIME) to bin/ and
# everything else (LIBRARY/ARCHIVE) to lib/.
if(WIN32 AND BUILD_SHARED_LIBS)
set(SBOM_LIB "${SBOM_STAGING}/bin/$<TARGET_FILE_NAME:wolftpm>")
else()
set(SBOM_LIB "${SBOM_STAGING}/lib/$<TARGET_FILE_NAME:wolftpm>")
endif()

# wolfTPM links wolfSSL/wolfCrypt, so record wolfSSL as a dependency in the
# SBOM (matches the autotools SBOM_DEP_WOLFSSL=yes). Recording it needs the
# gen-sbom from wolfSSL/wolfssl#10343; older gen-sbom rejects unknown flags,
# so probe --help and only pass --dep-wolfssl when supported. Against an
# older gen-sbom the SBOM is still valid but omits the wolfSSL entry.
set(_sbom_dep_wolfssl "")
execute_process(
COMMAND ${PYTHON3_CMD} ${WOLFSSL_DIR}/scripts/gen-sbom --help
OUTPUT_VARIABLE _sbom_help
ERROR_QUIET)
if(_sbom_help MATCHES "--dep-wolfssl")
set(_sbom_dep_wolfssl --dep-wolfssl yes)
else()
message(WARNING
"sbom: ${WOLFSSL_DIR}/scripts/gen-sbom has no --dep-wolfssl; "
"the SBOM will omit the wolfSSL dependency entry. Use the gen-sbom "
"from wolfSSL/wolfssl#10343 (or master once merged) to record it.")
endif()

# ${OPTION_FILE} is the generated wolftpm/options.h, the same compile-time
# option fingerprint autotools `make sbom` feeds to gen-sbom via
# --options-h. Using it (rather than a raw `cc -dM` dump, which would only
# capture compiler builtins) keeps the cmake SBOM consistent with autotools.
add_custom_target(sbom
VERBATIM
# Stage a clean install so gen-sbom inspects the same artifact layout
# the user would actually ship.
COMMAND ${CMAKE_COMMAND} -E remove_directory ${SBOM_STAGING}
COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR}
--prefix ${SBOM_STAGING} --config $<CONFIG>
COMMAND ${PYTHON3_CMD} ${WOLFSSL_DIR}/scripts/gen-sbom
--name wolftpm
--version ${SBOM_VERSION}
--supplier "wolfSSL Inc."
--license-file ${CMAKE_SOURCE_DIR}/LICENSE
--options-h ${OPTION_FILE}
--lib ${SBOM_LIB}
--license-override ${SBOM_LICENSE_OVERRIDE}
${_sbom_dep_wolfssl}
--cdx-out ${SBOM_CDX}
--spdx-out ${SBOM_SPDX}
# Validate the SPDX JSON and emit the tag-value rendering as a side
# effect; a malformed SBOM makes pyspdxtools exit non-zero and fails
# the target.
COMMAND ${PYSPDXTOOLS_CMD} --infile ${SBOM_SPDX}
--outfile ${SBOM_SPDX_TV}
COMMAND ${CMAKE_COMMAND} -E remove_directory ${SBOM_STAGING}
COMMENT "Generating SBOM for wolfTPM ${SBOM_VERSION}")

# gen-sbom reads the staged library, so the library must build first.
add_dependencies(sbom wolftpm)
endif()
23 changes: 23 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,26 @@ cppcheck:
--suppress=invalidPrintfArgType_sint \
--error-exitcode=89 --std=c89 \
-I wolftpm src/ hal/ examples

# SBOM generation (CRA compliance). The recipe is shared across the wolfSSL
# stack's autotools products in scripts/sbom.am; wolfTPM just declares what it
# is (a libwolftpm library that links wolfSSL/wolfCrypt) and includes it.
# WOLFSSL_DIR must point to a wolfssl source tree containing scripts/gen-sbom.
SBOM_PKGNAME = wolftpm
SBOM_LICENSE_FILE = $(srcdir)/LICENSE
SBOM_DEP_WOLFSSL = yes

# wolfTPM records its feature macros in its own generated options header, so
# point gen-sbom at that header rather than at the compiler-derived defaults.
SBOM_OPTIONS_H = $(abs_builddir)/wolftpm/options.h

# wolfTPM is GPLv3-or-later (per the per-file source headers: "either version 3
# of the License, or (at your option) any later version") or commercial. Pin
# the header-accurate SPDX id here so the SBOM is correct regardless of the
# gen-sbom version's licence detection; commercial licensees can override it
# (e.g. LicenseRef-wolfSSL-Commercial).
SBOM_LICENSE_OVERRIDE ?= GPL-3.0-or-later

EXTRA_DIST += scripts/sbom.am

include scripts/sbom.am
Loading
Loading