wolfssl-examples/cra-kit/scripts/generate-wolfssl-sbom.sh

313 lines
13 KiB
Bash
Executable File

#!/bin/sh
# Generate wolfSSL component SBOMs (autotools make sbom or embedded gen-sbom).
# CRA_SBOM_MODE=autotools|embedded (default: autotools if configure+Makefile exist)
# WOLFSSL_DIR=path/to/wolfssl
# CRA_PYTHON=python3 (optional: interpreter with pcpp for embedded path)
# CRA_LICENSE_OVERRIDE=<SPDX-id> (optional: e.g. LicenseRef-wolfSSL-Commercial)
# CRA_LICENSE_TEXT=<path> (required when CRA_LICENSE_OVERRIDE is a
# LicenseRef-* id: the plain-text licence
# embedded in the SBOM. gen-sbom / make sbom
# hard-fail without it.)
set -eu
SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
KIT_DIR=$(dirname "$SCRIPT_DIR")
# shellcheck disable=SC2015 # `|| true` is a deliberate set -e guard, not if-then-else
WOLFSSL_DIR=${WOLFSSL_DIR:-$(cd "$KIT_DIR/../../wolfssl" 2>/dev/null && pwd || true)}
OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolfssl-component"}
VERSION_FILE="$KIT_DIR/VERSION"
if [ -z "${WOLFSSL_DIR:-}" ] || [ ! -d "$WOLFSSL_DIR" ]; then
echo "ERROR: wolfSSL source not found." >&2
echo " Set WOLFSSL_DIR to your wolfssl checkout (sibling of wolfssl-examples)." >&2
exit 1
fi
# shellcheck disable=SC1090
. "$VERSION_FILE" 2>/dev/null || true
VERSION=${WOLFSSL_VERSION:-5.9.1}
mkdir -p "$OUT_DIR"
CDX_OUT="$OUT_DIR/wolfssl-${VERSION}.cdx.json"
SPDX_OUT="$OUT_DIR/wolfssl-${VERSION}.spdx.json"
echo "wolfSSL tree: $WOLFSSL_DIR"
echo "Outputs: $CDX_OUT"
echo " $SPDX_OUT"
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
echo "License override: $CRA_LICENSE_OVERRIDE"
fi
# A LicenseRef-* override (e.g. the commercial license) requires the actual
# licence text to be embedded in the SBOM (SPDX 2.3 §10.1). Both gen-sbom and
# `make sbom` hard-fail without it, so catch the omission here with an
# actionable message instead of letting the run die deep in the generator.
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
case "$CRA_LICENSE_OVERRIDE" in
LicenseRef-*)
if [ -z "${CRA_LICENSE_TEXT:-}" ]; then
echo "ERROR: CRA_LICENSE_OVERRIDE=$CRA_LICENSE_OVERRIDE is a LicenseRef-* identifier," >&2
echo " but CRA_LICENSE_TEXT is not set. SPDX 2.3 requires the licence text to be" >&2
echo " embedded for any LicenseRef-* used in licenseConcluded/licenseDeclared." >&2
echo " Re-run with CRA_LICENSE_TEXT=/path/to/wolfssl-commercial-license.txt," >&2
echo " or use scripts/make-commercial-sample.sh to derive from the pinned GPL samples." >&2
exit 1
fi
if [ ! -f "$CRA_LICENSE_TEXT" ]; then
echo "ERROR: CRA_LICENSE_TEXT=$CRA_LICENSE_TEXT not found." >&2
exit 1
fi
;;
esac
fi
# Canonicalize CRA_LICENSE_TEXT to an absolute path: the autotools path runs
# `make sbom` inside a `cd "$WOLFSSL_DIR"` subshell, where a relative path would
# otherwise resolve against the wolfSSL tree rather than the caller's CWD.
if [ -n "${CRA_LICENSE_TEXT:-}" ] && [ -f "$CRA_LICENSE_TEXT" ]; then
CRA_LICENSE_TEXT=$(CDPATH='' cd -- "$(dirname -- "$CRA_LICENSE_TEXT")" && pwd)/$(basename -- "$CRA_LICENSE_TEXT")
echo "License text: $CRA_LICENSE_TEXT"
fi
# Pick a Python that can `import pcpp` (pip may target a different python3 than /usr/local/bin).
_python_with_pcpp() {
for py in ${CRA_PYTHON:-} python3 python; do
[ -n "$py" ] || continue
if command -v "$py" >/dev/null 2>&1 && \
"$py" -c "import pcpp" 2>/dev/null; then
echo "$py"
return 0
fi
done
return 1
}
_embedded_srcs() {
# Demo list only — production SBOMs must mirror every wolfSSL .c on your link line.
# Outputs from this list are watermarked wolfssl:sbom:demo=true.
for f in \
"$WOLFSSL_DIR/wolfcrypt/src/aes.c" \
"$WOLFSSL_DIR/wolfcrypt/src/sha.c" \
"$WOLFSSL_DIR/wolfcrypt/src/sha256.c" \
"$WOLFSSL_DIR/wolfcrypt/src/random.c" \
"$WOLFSSL_DIR/wolfcrypt/src/ecc.c" \
"$WOLFSSL_DIR/wolfcrypt/src/wc_port.c" \
"$WOLFSSL_DIR/src/tls.c" \
"$WOLFSSL_DIR/src/tls13.c" \
"$WOLFSSL_DIR/src/keys.c"
do
if [ -f "$f" ]; then
echo "$f"
fi
done
}
_run_embedded() {
echo "==> Embedded path: gen-sbom with CRA Kit user_settings.h"
echo " NOTE: --srcs uses the kit's built-in 9-file DEMO list. Production SBOMs"
echo " must pass every wolfSSL .c file you compile. Output is watermarked"
echo " wolfssl:sbom:demo=true so this can never silently ship."
if [ ! -f "$KIT_DIR/user_settings.h" ]; then
echo "ERROR: $KIT_DIR/user_settings.h missing (demo settings for WOLFSSL_USER_SETTINGS)." >&2
exit 1
fi
GEN="$WOLFSSL_DIR/scripts/gen-sbom"
if [ ! -f "$GEN" ]; then
echo "ERROR: $GEN not found (need wolfSSL with SBOM support)." >&2
exit 1
fi
SETTINGS_H="$WOLFSSL_DIR/wolfssl/wolfcrypt/settings.h"
if [ ! -f "$SETTINGS_H" ]; then
echo "ERROR: $SETTINGS_H not found." >&2
exit 1
fi
# Build the positional list of source files newline-safely so paths that
# contain spaces survive (POSIX sh has no arrays; unquoted command
# substitution would word-split and corrupt such paths).
set --
while IFS= read -r _src; do
[ -n "$_src" ] || continue
set -- "$@" "$_src"
done <<EOF
$(_embedded_srcs)
EOF
# Optional commercial license override (LicenseRef-wolfSSL-Commercial etc).
# A LicenseRef-* override must be accompanied by --license-text (validated
# up front above); a stock SPDX id needs no text.
set -- "$@" --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT"
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
set -- "$@" --license-override "$CRA_LICENSE_OVERRIDE"
if [ -n "${CRA_LICENSE_TEXT:-}" ]; then
set -- "$@" --license-text "$CRA_LICENSE_TEXT"
fi
fi
if _py=$(_python_with_pcpp); then
echo " Using $_py (pcpp) for --user-settings"
"$_py" "$GEN" \
--name wolfssl --version "$VERSION" \
--license-file "$WOLFSSL_DIR/LICENSING" \
--user-settings "$SETTINGS_H" \
--user-settings-include "$WOLFSSL_DIR" \
--user-settings-include "$KIT_DIR" \
--user-settings-define WOLFSSL_USER_SETTINGS \
--srcs "$@"
return 0
fi
echo "NOTE: pcpp not found for python3/python; using compiler -dM -E -> --options-h"
echo " Install pcpp on the same interpreter: python3 -m pip install pcpp"
echo " (conda users: pip install pcpp often targets conda python, not /usr/local/bin/python3)"
echo " Cross builds: set CC=arm-none-eabi-gcc (or your target compiler) so the"
echo " fallback reflects target macros, not the host's."
# Use mktemp so the temp filename is unpredictable: a fixed PID-based name in
# a shared/CI directory could be pre-created or raced by another job.
DEFINES_H=$(mktemp "${TMPDIR:-/tmp}/wolfssl-defines.XXXXXX") || {
echo "ERROR: mktemp failed for the defines temp file." >&2
exit 1
}
# Clean up the temp defines file on every exit path, including a failing
# generator run (it previously leaked the file under `set -e` if the
# final gen-sbom invocation failed before the manual `rm -f`).
trap 'rm -f "$DEFINES_H"' EXIT
CC=${CC:-cc}
if ! "$CC" -dM -E \
-I"$WOLFSSL_DIR" \
-I"$KIT_DIR" \
-DWOLFSSL_USER_SETTINGS \
-include "$SETTINGS_H" \
-x c /dev/null >"$DEFINES_H" 2>/dev/null; then
echo "ERROR: $CC -dM -E failed; install pcpp or set CC to your cross-compiler." >&2
exit 1
fi
PYTHON=python3
command -v python3 >/dev/null 2>&1 || PYTHON=python
"$PYTHON" "$GEN" \
--name wolfssl --version "$VERSION" \
--license-file "$WOLFSSL_DIR/LICENSING" \
--options-h "$DEFINES_H" \
--srcs "$@"
}
_run_autotools() {
echo "==> Autotools path: make sbom"
# `make sbom` names its output after the wolfSSL TREE's version
# (PACKAGE_VERSION), not the kit's pinned VERSION. If they differ, the
# `cp` below would otherwise fail with a cryptic "No such file or
# directory" under `set -eu`. Detect the mismatch early and explain it.
_tree_ver=$(sed -n \
's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
"$WOLFSSL_DIR/wolfssl/version.h" 2>/dev/null || true)
if [ -n "$_tree_ver" ] && [ "$_tree_ver" != "$VERSION" ]; then
echo "ERROR: wolfSSL tree is version $_tree_ver but the kit is pinned to $VERSION." >&2
echo " 'make sbom' emits wolfssl-${_tree_ver}.* while the pinned auditor" >&2
echo " packet references wolfssl-${VERSION}.*. Check out a wolfSSL $VERSION" >&2
echo " tree, or update cra-kit/VERSION (and the pinned sample references)." >&2
exit 1
fi
(cd "$WOLFSSL_DIR" && {
if [ ! -f Makefile ]; then
echo " Running ./configure first..."
./configure
fi
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
if [ -n "${CRA_LICENSE_TEXT:-}" ]; then
make sbom SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" \
SBOM_LICENSE_TEXT="$CRA_LICENSE_TEXT"
else
make sbom SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE"
fi
else
make sbom
fi
cp -f "wolfssl-${VERSION}.cdx.json" "$CDX_OUT"
cp -f "wolfssl-${VERSION}.spdx.json" "$SPDX_OUT"
if [ -f "wolfssl-${VERSION}.spdx" ]; then
cp -f "wolfssl-${VERSION}.spdx" "$OUT_DIR/"
fi
})
}
MODE=${CRA_SBOM_MODE:-}
case "$MODE" in
embedded) _run_embedded ;;
autotools) _run_autotools ;;
"")
if [ -f "$WOLFSSL_DIR/Makefile" ] && [ -f "$WOLFSSL_DIR/configure" ]; then
MODE=autotools
_run_autotools
else
MODE=embedded
_run_embedded
fi
;;
*)
echo "ERROR: CRA_SBOM_MODE must be 'autotools' or 'embedded', not '$MODE'" >&2
exit 1
;;
esac
# ---- Post-process: demo watermarks (+ defensive PURL canonicalization) ----
# Current gen-sbom already emits pkg:github/wolfSSL/wolfssl@vX natively, so the
# PURL rewrite below is a defensive no-op kept only for older wolfSSL trees that
# emitted pkg:generic/wolfssl@X. The substantive step here is the demo
# watermark: embedded outputs from the kit's 9-file demo --srcs list get a
# wolfssl:sbom:demo property so a downstream auditor cannot mistake them for
# production-complete SBOMs.
if ! CDX_OUT="$CDX_OUT" SPDX_OUT="$SPDX_OUT" CRA_SBOM_MODE_FINAL="$MODE" \
python3 <<'PY'
import json, os, pathlib
cdx = pathlib.Path(os.environ["CDX_OUT"])
spdx = pathlib.Path(os.environ["SPDX_OUT"])
demo = os.environ.get("CRA_SBOM_MODE_FINAL") == "embedded"
GENERIC = "pkg:generic/wolfssl@"
GITHUB = "pkg:github/wolfSSL/wolfssl@v"
def canonicalize_purl(s):
if isinstance(s, str) and s.startswith(GENERIC):
return GITHUB + s[len(GENERIC):]
return s
if cdx.exists():
d = json.loads(cdx.read_text())
comp = d.get("metadata", {}).get("component", {})
comp["purl"] = canonicalize_purl(comp.get("purl", ""))
if demo:
props = comp.setdefault("properties", [])
if not any(p.get("name") == "wolfssl:sbom:demo" for p in props):
props.append({
"name": "wolfssl:sbom:demo",
"value": "true (built-in --srcs list, not production-complete)"
})
cdx.write_text(json.dumps(d, indent=2) + "\n")
print(f"Post-processed {cdx.name}" + (": demo watermark added" if demo else ": no changes needed"))
if spdx.exists():
d = json.loads(spdx.read_text())
for pkg in d.get("packages", []):
for ref in pkg.get("externalRefs", []):
if ref.get("referenceType") == "purl":
ref["referenceLocator"] = canonicalize_purl(ref.get("referenceLocator", ""))
if demo:
existing = pkg.get("comment", "")
marker = "DEMO ARTIFACT (built-in --srcs list, not production-complete)."
if marker not in existing:
pkg["comment"] = (marker + " " + existing).strip()
spdx.write_text(json.dumps(d, indent=2) + "\n")
print(f"Post-processed {spdx.name}" + (": demo watermark added" if demo else ": no changes needed"))
PY
then
echo "ERROR: post-process failed (PURL canonicalization/watermarking incomplete)." >&2
echo " The emitted SBOM may carry pkg:generic PURLs or lack demo watermarks; not trusting it." >&2
exit 1
fi
echo "Done."