Skip to content
Open
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
211 changes: 211 additions & 0 deletions .github/workflows/sbom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
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: wolfProvider SBOM generation (linux)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout wolfprovider
uses: actions/checkout@v4
with:
path: wolfprovider

# wolfProvider links both wolfSSL and OpenSSL, so its SBOM records both as
# dependencies. gen-sbom lives in wolfssl and is not yet on master, so
# default to the open PR head that carries it (wolfSSL/wolfssl#10343) so CI
# actually exercises `make sbom` (with --dep-wolfssl / --dep-openssl)
# instead of silently skipping. TODO: switch the fallback back to
# 'master' once #10343 merges. Note: this checkout only supplies gen-sbom
# (and its wolfssl version.h); the wolfSSL that wolfProvider actually links
# is the one built from source by build-wolfprovider.sh below.
- name: Checkout wolfssl (gen-sbom source)
uses: actions/checkout@v4
with:
repository: wolfSSL/wolfssl
ref: ${{ github.event.inputs.wolfssl_ref || 'refs/pull/10343/head' }}
path: wolfssl-gensbom

- 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"

# Build the full stack (OpenSSL + wolfSSL + wolfProvider) from source using
# the project's maintained script. WOLFPROV_SKIP_TEST keeps it a
# build-only run (the SBOM only needs the built libwolfprov artifact).
# This also runs autogen + ./configure in-tree, so `make sbom` picks up the
# AC_PATH_PROG(PYTHON3/PYSPDXTOOLS/GIT) substitutions.
- name: Build wolfProvider stack (openssl + wolfssl + wolfprovider)
working-directory: wolfprovider
run: WOLFPROV_SKIP_TEST=1 ./scripts/build-wolfprovider.sh

# 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
# the --dep-wolfssl / --dep-openssl capabilities so this workflow is safe
# even against a gen-sbom that predates them.
- name: Detect gen-sbom availability and capabilities
id: gate
run: |
GS="$GITHUB_WORKSPACE/wolfssl-gensbom/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"
HELP="$(python3 "$GS" --help 2>/dev/null)"
if echo "$HELP" | 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
if echo "$HELP" | grep -q -- '--dep-openssl'; then
echo "dep_openssl=yes" >> "$GITHUB_OUTPUT"
else
echo "dep_openssl=no" >> "$GITHUB_OUTPUT"
echo "::notice::gen-sbom on this ref has no --dep-openssl; the openssl dependency assertion will be skipped."
fi

# Record the versions of the wolfSSL that was actually linked (built from
# source by the script, not the gen-sbom checkout) and of the built
# OpenSSL, so the SBOM's dependency versions match the deployed binaries.
- name: Resolve linked wolfSSL / OpenSSL versions
if: steps.gate.outputs.have == 'yes'
id: vers
working-directory: wolfprovider
run: |
wv=""
if [ -f wolfssl-source/wolfssl/version.h ]; then
wv=$(sed -n 's/.*LIBWOLFSSL_VERSION_STRING[ \t]*"\([^"]*\)".*/\1/p' \
wolfssl-source/wolfssl/version.h)
fi
ov=""
if [ -x openssl-install/bin/openssl ]; then
ov=$(openssl-install/bin/openssl version | awk '{print $2}')
fi
echo "wolfssl=$wv" >> "$GITHUB_OUTPUT"
echo "openssl=$ov" >> "$GITHUB_OUTPUT"
echo "linked wolfSSL=$wv openssl=$ov"

- name: Generate SBOM
if: steps.gate.outputs.have == 'yes'
working-directory: wolfprovider
run: |
make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl-gensbom" \
SBOM_WOLFSSL_VERSION="${{ steps.vers.outputs.wolfssl }}" \
SBOM_OPENSSL_VERSION="${{ steps.vers.outputs.openssl }}"

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

- name: CycloneDX identity and licence
if: steps.gate.outputs.have == 'yes'
working-directory: wolfprovider
run: |
python3 - <<'PY'
import glob, json
cdx = json.load(open(glob.glob('wolfprovider-*.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'] == 'wolfprovider', m['name']
assert m['purl'].startswith('pkg:github/wolfSSL/wolfprovider@'), 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'
print('CDX ok:', m['name'], m['purl'], ids)
PY

- name: Reproducible across two runs (SOURCE_DATE_EPOCH)
if: steps.gate.outputs.have == 'yes'
working-directory: wolfprovider
run: |
rm -f wolfprovider-*.cdx.json wolfprovider-*.spdx.json wolfprovider-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl-gensbom" \
SBOM_WOLFSSL_VERSION="${{ steps.vers.outputs.wolfssl }}" \
SBOM_OPENSSL_VERSION="${{ steps.vers.outputs.openssl }}"
sha256sum wolfprovider-*.cdx.json wolfprovider-*.spdx.json > /tmp/a.sums
rm -f wolfprovider-*.cdx.json wolfprovider-*.spdx.json wolfprovider-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl-gensbom" \
SBOM_WOLFSSL_VERSION="${{ steps.vers.outputs.wolfssl }}" \
SBOM_OPENSSL_VERSION="${{ steps.vers.outputs.openssl }}"
sha256sum wolfprovider-*.cdx.json wolfprovider-*.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: wolfprovider
run: |
python3 - <<'PY'
import glob, json
d = json.load(open(glob.glob('wolfprovider-*.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-wolfprovider', 'DEPENDS_ON',
'SPDXRef-Package-wolfssl') in rels, rels
print('wolfssl dependency ok')
PY

- name: openssl recorded as a dependency
if: steps.gate.outputs.have == 'yes' && steps.gate.outputs.dep_openssl == 'yes'
working-directory: wolfprovider
run: |
python3 - <<'PY'
import glob, json
d = json.load(open(glob.glob('wolfprovider-*.spdx.json')[0]))
assert 'openssl' 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-wolfprovider', 'DEPENDS_ON',
'SPDXRef-Package-openssl') in rels, rels
print('openssl dependency ok')
PY

- name: Upload SBOM artefacts
if: always() && steps.gate.outputs.have == 'yes'
uses: actions/upload-artifact@v4
with:
name: wolfprovider-sbom-${{ github.sha }}
path: |
wolfprovider/wolfprovider-*.cdx.json
wolfprovider/wolfprovider-*.spdx.json
wolfprovider/wolfprovider-*.spdx
if-no-files-found: warn
retention-days: 90
25 changes: 25 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ noinst_HEADERS =
check_PROGRAMS =
dist_noinst_SCRIPTS =
DISTCLEANFILES =
CLEANFILES =
pkginclude_HEADERS =
EXTRA_DIST =

Expand Down Expand Up @@ -50,3 +51,27 @@ test: check
# The '--with-wolfssl' doesn't get propagated during a distcheck either, but it
# is necessary when they are installed somewhere other than /usr/local.
AM_DISTCHECK_CONFIGURE_FLAGS=CPPFLAGS="-I@abs_top_srcdir@/include" --with-openssl=@OPENSSL_INSTALL_DIR@ --with-wolfssl=@WOLFSSL_INSTALL_DIR@

# SBOM generation (CRA compliance). The recipe is shared across the wolfSSL
# stack's autotools products in scripts/sbom.am; wolfProvider just declares what
# it is (a shared library that links both wolfSSL and OpenSSL) and includes it.
# WOLFSSL_DIR must point to a wolfssl source tree containing scripts/gen-sbom;
# it defaults to the --with-wolfssl location, which is an install prefix, so for
# `make sbom` pass a source tree: make sbom WOLFSSL_DIR=/path/to/wolfssl.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [High] SBOM version lookup breaks clean out-of-tree builds and overrides Automake VERSION
🚫 BLOCK bug

The PR defines VERSION by grepping $(srcdir)/include/wolfprovider/version.h, but this header is generated by configure.ac and is gitignored; a clean source tree only contains include/wolfprovider/version.h.in. In a normal VPATH/out-of-tree build, the generated header lives in the build tree, so this grep returns empty. Because VERSION is also Automake's standard package-version variable, this can produce SBOM paths like wolfprov-.spdx and can also affect other Automake rules that rely on $(VERSION). The PR introduces this by adding a new top-level VERSION = ... assignment for SBOM generation.

Recommendation: Replace the custom VERSION assignment with a separate SBOM variable based on $(PACKAGE_VERSION) or @VERSION@ before merge. For example: SBOM_VERSION = $(PACKAGE_VERSION) and reference $(SBOM_VERSION) in the SBOM paths and options.

SBOM_PKGNAME = wolfprovider
SBOM_LICENSE_FILE = $(srcdir)/COPYING
SBOM_LIB_STEM = libwolfprov
SBOM_DEP_WOLFSSL = yes
SBOM_DEP_OPENSSL = yes

# wolfProvider 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

50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,56 @@ Information on how to configure, build, and test wolfProvider can be found here:
* Ed25519, Ed448 (signatures)


## SBOM / EU CRA Compliance

wolfProvider 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 `libwolfprov`
library artifact (shared or static; ELF, Mach-O, or PE), and (with a
sufficiently new `gen-sbom`) lists both wolfSSL and OpenSSL as dependencies so
vulnerability scanners can associate wolfSSL and OpenSSL advisories with a
wolfProvider 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
```

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); note that
`--with-wolfssl` normally points at an install prefix, which does not ship
`gen-sbom`, so pass a source tree here.

Output: `wolfprovider-<version>.cdx.json`, `wolfprovider-<version>.spdx.json`, `wolfprovider-<version>.spdx`

Optional overrides:

- `SBOM_LICENSE_OVERRIDE` - SPDX expression to use instead of the licence
parsed from `COPYING` (e.g. `LicenseRef-wolfSSL-Commercial` for commercial
licensees). Defaults to `GPL-3.0-or-later` (the per-file header licence).
- `SBOM_LICENSE_TEXT` - path to the licence text for any `LicenseRef-*` used in
`SBOM_LICENSE_OVERRIDE` (required by SPDX 2.3).
- `SBOM_WOLFSSL_VERSION` - version recorded for the wolfSSL dependency;
auto-detected from `WOLFSSL_DIR/wolfssl/version.h` (or wolfSSL's `pkg-config`
entry) when unset.
- `SBOM_OPENSSL_VERSION` - version recorded for the OpenSSL dependency;
resolved via OpenSSL's `pkg-config` entry when unset.

```sh
make install-sbom # installs to $(datadir)/doc/wolfprov/
make uninstall-sbom
```

Note: recording wolfSSL and OpenSSL as dependencies and emitting
wolfProvider-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 dependency entries 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).

## Support

- [GitHub Issues](https://github.com/wolfssl/wolfProvider/issues)
Expand Down
10 changes: 10 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ USER_CFLAGS="$CFLAGS"
AC_PROG_CC
AC_LANG(C)

# Tools used by the SBOM targets (see scripts/sbom.am `make sbom`). GIT is used
# only to derive SOURCE_DATE_EPOCH for reproducible SBOM output; all three are

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Medium] pyspdxtools probe is never enforced by the sbom target
💡 SUGGEST bug

The PR adds AC_CHECK_PROG([PYSPDXTOOLS], [pyspdxtools], [pyspdxtools]) and the README says pyspdxtools is required, but make sbom only checks $(PYTHON3) before invoking gen-sbom. If pyspdxtools is missing, configure still succeeds and the target proceeds until the generator fails later, likely with a less actionable error. This weakens the dependency check introduced by the PR.

Recommendation: Use the PYSPDXTOOLS configure result in the sbom recipe so the documented dependency is checked before running the generator, e.g. add an @if test -z "$(PYSPDXTOOLS)"; then echo "ERROR: pyspdxtools not found in PATH. Install with: pip install spdx-tools"; exit 1; fi guard.

# 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])
AC_SUBST([PYTHON3])
AC_SUBST([PYSPDXTOOLS])
AC_SUBST([GIT])

# wolfSSL - check first so its -I/-L paths take precedence over OpenSSL prefix
# which may contain stale wolfSSL headers from a different version
AX_CHECK_WOLFSSL(
Expand Down
Loading
Loading