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

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

- name: Install SBOM validator (pyspdxtools)
run: |
python3 -m pip install --user 'spdx-tools==0.8.*'
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

# --enable-all gives wolfCLU the OpenSSL-compat / cert-req APIs it links.
- name: Build and install wolfssl
working-directory: wolfssl
run: |
autoreconf -ivf
./configure --enable-all --prefix="$GITHUB_WORKSPACE/wolfssl-install"
make -j"$(nproc)"
make install

# gen-sbom lives in wolfssl and may not be on the checked-out ref yet (the
# wolfSSL SBOM change can land separately). Gate on its presence so this
# workflow is safe even if the ref is changed to one without 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 wolfclu
if: steps.gate.outputs.have == 'yes'
working-directory: wolfclu
run: |
autoreconf -ivf
./configure --with-wolfssl="$GITHUB_WORKSPACE/wolfssl-install"
make -j"$(nproc)"

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

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

- name: CycloneDX identity and licence
if: steps.gate.outputs.have == 'yes'
working-directory: wolfclu
run: |
python3 - <<'PY'
import glob, json
cdx = json.load(open(glob.glob('wolfclu-*.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'] == 'wolfclu', m['name']
assert m['purl'].startswith('pkg:github/wolfSSL/wolfclu@'), 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
# Embedded identity is the hashed binary 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: wolfclu
run: |
rm -f wolfclu-*.cdx.json wolfclu-*.spdx.json wolfclu-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
sha256sum wolfclu-*.cdx.json wolfclu-*.spdx.json > /tmp/a.sums
rm -f wolfclu-*.cdx.json wolfclu-*.spdx.json wolfclu-*.spdx
SOURCE_DATE_EPOCH=1700000000 make sbom \
WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl"
sha256sum wolfclu-*.cdx.json wolfclu-*.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: wolfclu
run: |
python3 - <<'PY'
import glob, json
d = json.load(open(glob.glob('wolfclu-*.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-wolfclu', '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: wolfclu-sbom-${{ github.sha }}
path: |
wolfclu/wolfclu-*.cdx.json
wolfclu/wolfclu-*.spdx.json
wolfclu/wolfclu-*.spdx
if-no-files-found: warn
retention-days: 90
23 changes: 22 additions & 1 deletion Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ if ENABLE_MANPAGES
# -n keeps the output byte-reproducible (no embedded filename/mtime).
MAN_GZ = $(man_MANS:.1=.1.gz)

.PHONY: manpages-gz
manpages-gz: $(MAN_GZ)

manpages/%.1.gz: $(srcdir)/manpages/%.1
Expand All @@ -163,6 +162,28 @@ endif
test: check
#DISTCLEANFILES+= wolfssl-config

# manpages-gz is only a target under ENABLE_MANPAGES, but declare it .PHONY
# unconditionally so it merges with the unconditional .PHONY in scripts/sbom.am
# (a conditional + unconditional .PHONY pair trips automake -Werror).
.PHONY: manpages-gz

# SBOM generation (CRA compliance). The recipe is shared across the wolfSSL
# stack's autotools products in scripts/sbom.am; wolfCLU just declares what it
# is (a CLI program that links wolfSSL) and includes it. WOLFSSL_DIR must point
# to a wolfssl source tree containing scripts/gen-sbom.
SBOM_PKGNAME = wolfclu
SBOM_LICENSE_FILE = $(srcdir)/COPYING
SBOM_ARTIFACT = bin
SBOM_BIN_NAME = wolfssl
SBOM_DEP_WOLFSSL = yes

# wolfCLU is GPLv3-or-later (see source headers) or commercial. Pin the SPDX id
# so the SBOM is correct regardless of the gen-sbom version's licence detection;
# commercial licensees can override (e.g. LicenseRef-wolfSSL-Commercial).
SBOM_LICENSE_OVERRIDE ?= GPL-3.0-or-later

include scripts/sbom.am


maintainer-clean-local:
-rm Makefile.in
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,51 @@ wolfssl ocsp \

The `-index` file uses OpenSSL's CA index format.

## SBOM / EU CRA Compliance

wolfCLU 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 `wolfssl`
command-line binary (ELF, Mach-O, or PE), and (with a sufficiently new
`gen-sbom`) lists wolfSSL as a dependency so vulnerability scanners can
associate wolfSSL advisories with a wolfCLU 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).

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

Optional overrides:

- `SBOM_LICENSE_OVERRIDE` - SPDX expression to use instead of the licence
parsed from `COPYING` (defaults to `GPL-3.0-or-later`; e.g.
`LicenseRef-wolfSSL-Commercial` for commercial licensees).
- `SBOM_LICENSE_TEXT` - path to the licence text for any `LicenseRef-*` used in
`SBOM_LICENSE_OVERRIDE` (required by SPDX 2.3).
- `SBOM_WOLFSSL_VERSION` - version recorded for the wolfSSL dependency;
auto-detected from `WOLFSSL_DIR/wolfssl/version.h` when unset.

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

The SBOM recipe is the shared `scripts/sbom.am` fragment used across the
wolfSSL stack's autotools products; wolfCLU only declares that it is a CLI
program linking wolfSSL. Recording wolfSSL as a dependency and emitting
wolfCLU-specific project URLs require the `gen-sbom` from wolfSSL/wolfssl#10343;
against an older `gen-sbom`, `make sbom` still succeeds and produces a valid
SBOM, but omits the wolfSSL dependency entry and inherits wolfSSL's project URLs.

For further CRA guidance see [wolfssl/doc/CRA.md](https://github.com/wolfSSL/wolfssl/blob/master/doc/CRA.md).

## Contacts

Please contact support@wolfssl.com with any questions or comments.
Expand Down
7 changes: 7 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ AM_PATH_PYTHON([3.6],, [:])
AC_SUBST([PYTHON])
AM_CONDITIONAL([HAVE_PYTHON], [test "$PYTHON" != ":"])

# 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
# 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])

# Checks for headers/libraries
AC_CHECK_HEADERS([sys/time.h string.h termios.h unistd.h])
AC_CHECK_SIZEOF(long long, 8)
Expand Down
Loading
Loading