diff --git a/cra-kit/CRA-Cheat-Sheet.md b/cra-kit/CRA-Cheat-Sheet.md index 16dcd355..3ccddc7c 100644 --- a/cra-kit/CRA-Cheat-Sheet.md +++ b/cra-kit/CRA-Cheat-Sheet.md @@ -49,7 +49,7 @@ Detail: [CRA-Compliance-Shortlist.md](CRA-Compliance-Shortlist.md) | Question | Term | wolfSSL today | |----------|------|---------------| -| What software is in the product? | **SBOM** | `make sbom` or `gen-sbom` → SPDX + CycloneDX | +| What software is in the product? | **SBOM** | `make sbom`, `cmake --target sbom`, or `gen-sbom` → SPDX + CycloneDX | | What crypto is enabled in *your* build? | **CBOM** (path) | `wolfssl:build:*` in CycloneDX — not full `cryptographic-asset` yet | | How was the library binary built? | **Provenance** | `make bomsh` (**Linux** host, optional) | @@ -57,6 +57,25 @@ Detail: [CRA-Compliance-Shortlist.md](CRA-Compliance-Shortlist.md) --- +## Build system integration quick-reference + +| Build system | How to generate SBOM | Script env var | +|---|---|---| +| **autotools** | `make sbom` | `CRA_SBOM_MODE=autotools` | +| **cmake** | `cmake --build build --target sbom` | `CRA_SBOM_MODE=cmake` + `WOLFSSL_BUILD_DIR=build` | +| **embedded / custom** (source list) | `gen-sbom --user-settings … --srcs *.c` | `CRA_SBOM_MODE=embedded` + `CRA_SBOM_SRCS_FILE=srcs.txt` | +| **embedded** (no hashable artifact) | `gen-sbom --user-settings … --no-artifact-hash` | `CRA_SBOM_MODE=embedded` + `CRA_SBOM_NO_HASH=true` | + +For the embedded path the `generate-wolfssl-sbom.sh` script: +- Tries **pcpp** (pure-Python preprocessor) first — `pip install pcpp` +- Falls back to **`CC -dM -E`** — set `CC=arm-none-eabi-gcc` for cross builds +- Accepts a source file list from `CRA_SBOM_SRCS_FILE` (one path per line, `#` lines ignored) +- Accepts `CRA_SBOM_NO_HASH=true` when no source list is available + +Contact wolfssl@wolfssl.com before shipping a `--no-artifact-hash` SBOM in production. + +--- + ## BOMs at a glance | Name | Owner | wolfSSL today | diff --git a/cra-kit/README.md b/cra-kit/README.md index 5d85d0a4..f93e492a 100644 --- a/cra-kit/README.md +++ b/cra-kit/README.md @@ -206,6 +206,10 @@ Pinned sample version: see [`VERSION`](VERSION) (default **5.9.1**). Production SBOMs must use **your** project's `user_settings.h` and **your** full `--srcs` list (every wolfSSL `.c` you compile). +See **[SRCS-FILE-HOWTO.md](SRCS-FILE-HOWTO.md)** for instructions on extracting +your wolfSSL source list from common embedded build systems (Makefile, CMake, +Zephyr, ESP-IDF, Keil, IAR) and passing it via `CRA_SBOM_SRCS_FILE`. + --- ## Presentation diff --git a/cra-kit/SRCS-FILE-HOWTO.md b/cra-kit/SRCS-FILE-HOWTO.md new file mode 100644 index 00000000..d8d5d10f --- /dev/null +++ b/cra-kit/SRCS-FILE-HOWTO.md @@ -0,0 +1,462 @@ +# Generating a wolfSSL source list for `CRA_SBOM_SRCS_FILE` + +The embedded SBOM path hashes every wolfSSL `.c` file you compile — not the +library binary. That list comes from your build system. This guide shows how +to extract it for the most common embedded build systems. + +Once you have the file, pass it to the kit script: + +```sh +CRA_SBOM_MODE=embedded \ +CRA_SBOM_SRCS_FILE=/path/to/wolfssl-srcs.txt \ +CRA_SBOM_SRCS_ONLY_FROM_FILE=true \ +WOLFSSL_DIR=/path/to/wolfssl \ +./scripts/generate-wolfssl-sbom.sh +``` + +`CRA_SBOM_SRCS_ONLY_FROM_FILE=true` suppresses the demo watermark and uses +only the paths in your file. Omit it to merge your list with the kit's +built-in 9-file demo list (keeps the `wolfssl:sbom:demo=true` watermark). + +Manual extraction is now optional for most build systems. Set the right +environment variable for your build system and the kit script extracts the +source list automatically — no `CRA_SBOM_SRCS_FILE` needed. See the +relevant section below for the variable to set and any tool requirements. + +--- + +## Custom Makefile + +### Option A — add a `print-wolfssl-srcs` target (recommended) + +Add this to your `Makefile`. Replace `$(WOLFSSL_SRCS)` with however your +project names the wolfSSL source variable: + +```makefile +.PHONY: print-wolfssl-srcs +print-wolfssl-srcs: + @printf '%s\n' $(WOLFSSL_SRCS) +``` + +Then extract: + +```sh +make print-wolfssl-srcs > wolfssl-srcs.txt +``` + +This is immune to recursive makes, response files, and multi-rule compilation. + +### Option B — `make -n` dry-run (when you cannot modify the Makefile) + +```sh +make -n 2>/dev/null \ + | grep -oE '[^ ]+wolfssl[^ ]+\.c' \ + | sort -u \ + > wolfssl-srcs.txt +``` + +`make -n` prints compiler command lines without running them, so a missing +cross-compiler is not a problem. The grep pattern matches any token that +contains `wolfssl` and ends in `.c`. + +**Limitation**: fails if sources are passed via response files (`@srcs.rsp`) +or compiled through recursive `$(MAKE) -C` sub-invocations that do not echo +the final compile lines. Use Option A in those cases. + +### Automatic extraction + +Set `CRA_SBOM_MAKEFILE_DIR` to the directory containing your project Makefile, then run the +kit script with no `CRA_SBOM_SRCS_FILE`: + +```sh +CRA_SBOM_MODE=embedded \ +CRA_SBOM_MAKEFILE_DIR=/path/to/your/project \ +WOLFSSL_DIR=/path/to/wolfssl \ +./scripts/generate-wolfssl-sbom.sh +``` + +The script tries the `print-wolfssl-srcs` target first; if that target does not exist, it +falls back to `make -n` dry-run. Either way the extracted list is used automatically — +no manual `CRA_SBOM_SRCS_FILE` needed. + +--- + +## CMake — `compile_commands.json` + +Enable compile commands at configure time: + +```sh +cmake -B build /path/to/your/project \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake --build build +``` + +Extract wolfSSL library sources: + +```sh +WOLFSSL_DIR=/path/to/wolfssl +jq -r '.[].file' build/compile_commands.json \ + | grep "^${WOLFSSL_DIR}/" \ + | grep -E "/(wolfcrypt/src|src)/[^/]+\.c$" \ + | sort -u \ + > wolfssl-srcs.txt +``` + +The `grep -E` step restricts to `src/` and `wolfcrypt/src/` — without it, +`examples/` and `tests/` files are included, which inflates the SBOM with +files you did not ship. + +**Requirements**: `jq` (`apt install jq` / `brew install jq`). + +### Automatic extraction + +Set `WOLFSSL_BUILD_DIR` to your cmake build directory. The kit script detects +`compile_commands.json` automatically and extracts wolfssl sources without manual steps: + +```sh +CRA_SBOM_MODE=embedded \ +WOLFSSL_BUILD_DIR=/path/to/build \ +WOLFSSL_DIR=/path/to/wolfssl \ +./scripts/generate-wolfssl-sbom.sh +``` + +Requires `jq` on the host. + +--- + +## Zephyr RTOS + +Zephyr uses CMake internally. Add `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` at +build time to get `compile_commands.json` in your build directory: + +```sh +west build -b -- -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DZEPHYR_EXTRA_MODULES=/path/to/wolfssl +``` + +`compile_commands.json` is written by CMake at configure time — it exists +even if the compilation itself fails (e.g., missing cross-compiler). + +Extract wolfSSL library sources: + +```sh +WOLFSSL_DIR=/path/to/wolfssl +jq -r '.[].file' build/compile_commands.json \ + | grep "^${WOLFSSL_DIR}/" \ + | grep -E "/(wolfcrypt/src|src)/[^/]+\.c$" \ + | sort -u \ + > wolfssl-srcs.txt +``` + +All paths in `compile_commands.json` are absolute. The `WOLFSSL_DIR` filter +matches entries from the wolfssl module directly; no path translation needed. +A typical wolfssl Zephyr build produces around 89 library sources +(`wolfcrypt/src/` + `src/`). + +**Requirements**: `jq` must be installed on the host running the extraction +(not the target board). + +### Automatic extraction + +Same as CMake — set `WOLFSSL_BUILD_DIR` to the `west build` output directory: + +```sh +CRA_SBOM_MODE=embedded \ +WOLFSSL_BUILD_DIR=/path/to/wolfssl-app/build \ +WOLFSSL_DIR=/path/to/wolfssl \ +./scripts/generate-wolfssl-sbom.sh +``` + +Requires `jq`. The `compile_commands.json` must have been generated at cmake configure time +(pass `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to `west build`). + +--- + +## ESP-IDF + +ESP-IDF uses CMake and writes `compile_commands.json` to the `build/` +subdirectory automatically. Build your project normally: + +```sh +idf.py build +``` + +When wolfssl is added as a **managed component** (via `idf_component.yml` +declaring `wolfssl/wolfssl`), the ESP-IDF component manager downloads it into +`managed_components/wolfssl__wolfssl/` inside your project. The directory name +is `wolfssl__wolfssl` (registry namespace and name joined with double +underscore). + +Extract the wolfssl library sources: + +```sh +PROJECT_DIR=/path/to/your/esp-idf-project +jq -r '.[].file' "${PROJECT_DIR}/build/compile_commands.json" \ + | grep "^${PROJECT_DIR}/managed_components/wolfssl__wolfssl/" \ + | grep -E "/(wolfcrypt/src|src)/[^/]+\.c$" \ + | sort -u \ + > wolfssl-srcs.txt +``` + +All paths in `compile_commands.json` are absolute. The +`grep -E "/(wolfcrypt/src|src)/[^/]+\.c$"` step excludes build-generated +files (e.g., `build/project_elf_src_esp32.c`) that also appear under the +project directory. + +If wolfssl is added as a **local component** (placed manually in +`components/wolfssl/` rather than managed), replace `managed_components/wolfssl__wolfssl` +with `components/wolfssl` in the filter. + +### Automatic extraction + +Set `WOLFSSL_BUILD_DIR` to your project's `build/` directory: + +```sh +CRA_SBOM_MODE=embedded \ +WOLFSSL_BUILD_DIR=/path/to/esp-idf-project/build \ +WOLFSSL_DIR=/path/to/wolfssl \ +./scripts/generate-wolfssl-sbom.sh +``` + +The script detects the ESP-IDF managed-component layout (`managed_components/wolfssl__wolfssl/`) +automatically when `WOLFSSL_DIR` sources are not found under `WOLFSSL_BUILD_DIR` directly. +Requires `jq`. + +--- + +## Keil MDK / uVision (`.uvprojx`) + +> **Note**: Keil MDK is Windows-only and requires a license. This section +> was verified against real wolfSSL Keil project files from +> `wolfssl/IDE/MDK5-ARM/`. The CMSIS Pack note below reflects actual +> wolfSSL project structure. + +Keil projects integrate wolfssl in one of two ways — the extraction method +differs between them. + +### Option A — wolfSSL CMSIS Pack (modern, recommended) + +The official wolfSSL Keil projects (e.g., `wolfSSL-Lib.uvprojx`) use the +**CMSIS Pack RTE (Run-Time Environment)**. In this mode the wolfssl sources +are **not listed in the `.uvprojx` file** — they are resolved at build time +from the installed wolfSSL CMSIS pack. The project XML records which pack +components are selected, not which `.c` files they compile. + +To find the source list, locate the installed pack descriptor: + +``` +# Windows +%LOCALAPPDATA%\Arm\Packs\wolfSSL\wolfSSL\\wolfSSL.pdsc + +# Linux / macOS (Keil Studio / CMSIS-Toolbox) +~/.arm/Packs/wolfSSL/wolfSSL//wolfSSL.pdsc +``` + +The `.pdsc` file is XML. Extract the `.c` sources for your selected +component group (e.g., `wolfCrypt/CORE`): + +```python +#!/usr/bin/env python3 +"""Extract .c sources for a wolfSSL CMSIS Pack component from its .pdsc.""" +import sys, xml.etree.ElementTree as ET + +pdsc = ET.parse(sys.argv[1]) +cgroup = sys.argv[2] if len(sys.argv) > 2 else '' # e.g. "wolfCrypt" + +for comp in pdsc.findall('.//component'): + if cgroup and comp.get('Cgroup', '') != cgroup: + continue + for f in comp.findall('.//file[@category="source"]'): + name = f.get('name', '') + if name.lower().endswith('.c'): + print(name.replace('\\', '/')) +``` + +Usage: + +```sh +python3 extract-pdsc-srcs.py wolfSSL.pdsc wolfCrypt > wolfssl-srcs.txt +``` + +Paths in the `.pdsc` are relative to the pack root directory. Prefix with +the pack install path to make them absolute before passing to `gen-sbom`. + +### Option B — wolfssl sources listed directly in the project + +Older or custom projects may list wolfssl `.c` files explicitly as +`` entries under ``. The Python script +from the CMSIS approach will produce no output for these — use this +instead: + +```python +#!/usr/bin/env python3 +"""Extract explicit .c FilePath entries from a Keil .uvprojx (non-pack).""" +import sys, xml.etree.ElementTree as ET + +proj = ET.parse(sys.argv[1]) +paths = set() +for file_elem in proj.findall('.//File'): + fp = file_elem.find('FilePath') + ft = file_elem.find('FileType') + if fp is None or not fp.text: + continue + ftype = int(ft.text) if ft is not None and ft.text else 0 + if ftype == 1 or fp.text.lower().endswith('.c'): + paths.add(fp.text.replace('\\', '/')) + +for p in sorted(paths): + print(p) +``` + +Usage: + +```sh +python3 extract-keil-srcs.py MyProject.uvprojx > wolfssl-srcs.txt +``` + +Paths are relative to the `.uvprojx` file. Resolve to absolute before +passing to `gen-sbom`. + +**How to tell which option you need**: open the `.uvprojx` in a text editor +and search for ``. If present and `` is +inside it, you are using the CMSIS Pack (Option A). If wolfssl `.c` files +appear under `` directly, use Option B. + +### Automatic extraction + +Set `CRA_SBOM_KEIL_PROJECT` to the path of your `.uvprojx` file: + +```sh +CRA_SBOM_MODE=embedded \ +CRA_SBOM_KEIL_PROJECT=/path/to/MyProject.uvprojx \ +WOLFSSL_DIR=/path/to/wolfssl \ +./scripts/generate-wolfssl-sbom.sh +``` + +The script parses the project file and chooses Option A (CMSIS Pack) or Option B +(explicit FilePath) automatically based on whether a `` is +present in the RTE block. For CMSIS Pack mode the installed `.pdsc` must be present at +`~/.arm/Packs/wolfSSL/wolfSSL//wolfSSL.pdsc`. Requires `python3`. + +--- + +## IAR Embedded Workbench (`.ewp`) + +> **Note**: IAR EW is Windows-only and requires a license. This section +> was verified against real wolfSSL IAR project files from +> `wolfssl/IDE/IAR-EWARM/`. + +IAR stores source files as `` elements with a `$PROJ_DIR$` +path prefix (IAR's built-in variable for the directory containing the `.ewp` +file) and Windows backslash separators. + +**Important**: wolfssl sources live under `wolfcrypt/src/` and `src/` — neither +path segment contains the string `"wolfssl"`. Do **not** filter by `"wolfssl"` +substring; instead filter by path depth or accept all `.c` files from the +project. + +```python +#!/usr/bin/env python3 +r""" +Extract .c source paths from an IAR EWARM .ewp project file. + +Paths are emitted as absolute paths (resolves $PROJ_DIR$ automatically). +Pass --raw to keep the original $PROJ_DIR$ prefix instead. + +Usage: + python3 extract-iar-srcs.py MyProject.ewp [--raw] > wolfssl-srcs.txt +""" +import sys, os, argparse, xml.etree.ElementTree as ET + + +def is_excluded(file_elem): + """True if the file is excluded from at least one build configuration.""" + return file_elem.find('excluded') is not None + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('ewp') + ap.add_argument('--raw', action='store_true', + help='Keep $PROJ_DIR$ prefix instead of resolving') + args = ap.parse_args() + + proj_dir = os.path.dirname(os.path.abspath(args.ewp)) + proj = ET.parse(args.ewp) + paths = set() + + for file_elem in proj.findall('.//file'): + if is_excluded(file_elem): + continue + name = file_elem.find('name') + if name is None or not name.text: + continue + raw = name.text + if not raw.lower().endswith('.c'): + continue + if args.raw: + paths.add(raw.replace('\\', '/')) + else: + resolved = raw.replace('$PROJ_DIR$', proj_dir) + paths.add(os.path.normpath(resolved.replace('\\', '/'))) + + for p in sorted(paths): + print(p) + + +if __name__ == '__main__': + main() +``` + +Usage: + +```sh +# Absolute paths (ready for gen-sbom) +python3 extract-iar-srcs.py wolfSSL-Lib.ewp > wolfssl-srcs.txt + +# Keep $PROJ_DIR$ prefix (for inspection) +python3 extract-iar-srcs.py wolfSSL-Lib.ewp --raw > wolfssl-srcs.txt +``` + +This produces 65 sources (56 under `wolfcrypt/src/`, 9 under `src/`) for the +standard `wolfSSL-Lib.ewp` project. + +**Caveats**: +- The script skips files that appear in `` blocks (per-configuration + exclusions). If you need sources for a specific configuration only, check + `` matches against your target config name. +- Application-specific `.c` files (test runners, benchmark harness) will also + appear; remove them from `wolfssl-srcs.txt` manually if they are not part + of your shipped wolfssl build. + +### Automatic extraction + +Set `CRA_SBOM_IAR_PROJECT` to the path of your `.ewp` file: + +```sh +CRA_SBOM_MODE=embedded \ +CRA_SBOM_IAR_PROJECT=/path/to/wolfSSL-Lib.ewp \ +WOLFSSL_DIR=/path/to/wolfssl \ +./scripts/generate-wolfssl-sbom.sh +``` + +The script resolves `$PROJ_DIR$` automatically. Requires `python3`. + +--- + +## Verifying the output + +After generating `wolfssl-srcs.txt`, sanity-check it: + +```sh +# Count should match your mental model of what you compile +wc -l wolfssl-srcs.txt + +# All paths should exist on disk +while IFS= read -r f; do + [ -f "$f" ] || echo "MISSING: $f" +done < wolfssl-srcs.txt + +# No duplicates (gen-sbom deduplicates, but worth checking) +sort wolfssl-srcs.txt | uniq -d +``` diff --git a/cra-kit/scripts/_cra-sbom-extract.sh b/cra-kit/scripts/_cra-sbom-extract.sh new file mode 100644 index 00000000..a36262dd --- /dev/null +++ b/cra-kit/scripts/_cra-sbom-extract.sh @@ -0,0 +1,271 @@ +#!/bin/sh +# _cra-sbom-extract.sh — shared source-extraction helper for CRA Kit SBOM scripts. +# +# Source this file; do not execute it directly. It provides one function, +# _cra_extract_srcs, that product SBOM scripts (generate-wolfssl-sbom.sh, +# generate-wolfssh-sbom.sh, ...) call to auto-detect the list of .c files that +# went into an embedded build. +# +# _cra_extract_srcs PRODUCT_DIR PRODUCT_NAME OUT_FILE +# +# Tries each extraction method in priority order. On success, writes the +# sorted, de-duplicated list of .c paths to OUT_FILE and returns 0. If a +# method is selected (its env var is set) but yields no sources, prints an +# error to stderr and returns 1. If no extraction env var is set at all, +# returns 2 so the caller can fall back to its default source glob. +# +# Methods, in priority order: +# 1. CRA_SBOM_SRCS_FILE — copy verbatim to OUT_FILE (safety net; callers +# normally handle this themselves before calling) +# 2. CRA_SBOM_KEIL_PROJECT — parse .uvprojx, filter to PRODUCT_DIR +# 3. CRA_SBOM_IAR_PROJECT — parse .ewp, filter to PRODUCT_DIR +# 4. CRA_SBOM_MAKEFILE_DIR — `make -n`, grep for PRODUCT_DIR/...*.c +# 5. CRA_SBOM_BUILD_DIR — jq filter compile_commands.json to PRODUCT_DIR +# none set — return 2 +# +# PRODUCT_DIR: absolute path to the product source tree; used to filter paths. +# PRODUCT_NAME: short name for log messages and the wolfssl CMSIS-Pack special +# case (e.g. "wolfssl", "wolfssh", "wolftpm"). +# OUT_FILE: path the caller has already created (mktemp) for the result. +# +# The function appends any temp files it creates to _cra_auto_tempfiles so the +# caller's EXIT trap can clean them up. CRA_SBOM_NO_HASH is honoured by callers, +# not here: a caller that sees CRA_SBOM_NO_HASH=true should skip hashing (and +# thus skip this function) entirely. + +_cra_extract_srcs() { + _cra_product_dir="$1" + _cra_product_name="$2" + _cra_out_file="$3" + + if [ -z "$_cra_product_dir" ] || [ -z "$_cra_product_name" ] || [ -z "$_cra_out_file" ]; then + echo "ERROR: _cra_extract_srcs requires PRODUCT_DIR PRODUCT_NAME OUT_FILE." >&2 + return 1 + fi + + # Method 1: explicit source list file. Callers usually handle this before + # calling us, but honour it here too so the function is safe to call blindly. + if [ -n "${CRA_SBOM_SRCS_FILE:-}" ]; then + if [ ! -f "$CRA_SBOM_SRCS_FILE" ]; then + echo "ERROR: CRA_SBOM_SRCS_FILE=$CRA_SBOM_SRCS_FILE not found." >&2 + return 1 + fi + sort -u "$CRA_SBOM_SRCS_FILE" > "$_cra_out_file" || { + echo "ERROR: failed to read CRA_SBOM_SRCS_FILE=$CRA_SBOM_SRCS_FILE." >&2 + return 1 + } + if [ ! -s "$_cra_out_file" ]; then + echo "ERROR: CRA_SBOM_SRCS_FILE=$CRA_SBOM_SRCS_FILE is empty." >&2 + return 1 + fi + _cra_n=$(wc -l < "$_cra_out_file" | tr -d ' ') + echo " Using $_cra_n sources from CRA_SBOM_SRCS_FILE" + return 0 + fi + + # Method 2: Keil .uvprojx + if [ -n "${CRA_SBOM_KEIL_PROJECT:-}" ]; then + if [ ! -f "$CRA_SBOM_KEIL_PROJECT" ]; then + echo "ERROR: CRA_SBOM_KEIL_PROJECT=$CRA_SBOM_KEIL_PROJECT not found." >&2 + return 1 + fi + if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: python3 is required to parse a Keil .uvprojx file." >&2 + return 1 + fi + # The CMSIS Pack RTE lookup only applies when PRODUCT_NAME=wolfssl: + # wolfSSL ships a CMSIS Pack whose sources live in the installed pack, + # not in the project. Other products have no CMSIS pack, so the parser + # skips that path and enumerates / entries directly. + python3 - "$CRA_SBOM_KEIL_PROJECT" "$_cra_product_dir" "$_cra_product_name" \ + > "$_cra_out_file" <<'PYEOF' || { +import sys, os, glob, xml.etree.ElementTree as ET + +proj_file = sys.argv[1] +product_dir = sys.argv[2] if len(sys.argv) > 2 else '' +product_name = sys.argv[3] if len(sys.argv) > 3 else '' +proj_dir = os.path.dirname(os.path.abspath(proj_file)) +proj = ET.parse(proj_file) +paths = set() + +rte = proj.find('.//RTE') +# CMSIS Pack RTE special case: wolfSSL only. +if (product_name == 'wolfssl' and rte is not None + and rte.find('.//component[@Cvendor="wolfSSL"]') is not None): + pdsc_candidates = sorted(glob.glob( + os.path.expanduser('~/.arm/Packs/wolfSSL/wolfSSL/*/wolfSSL.pdsc') + )) + if os.name == 'nt': + appdata = os.environ.get('LOCALAPPDATA', '') + pdsc_candidates += sorted(glob.glob( + os.path.join(appdata, 'Arm', 'Packs', 'wolfSSL', 'wolfSSL', '*', 'wolfSSL.pdsc') + )) + if pdsc_candidates: + pdsc_file = pdsc_candidates[-1] + pack_dir = os.path.dirname(pdsc_file) + pdsc = ET.parse(pdsc_file) + for f in pdsc.findall('.//file[@category="source"]'): + name = f.get('name', '') + if name.lower().endswith('.c'): + paths.add(os.path.normpath(os.path.join(pack_dir, name.replace('\\', '/')))) + elif product_dir and os.path.isdir(product_dir): + # Pack not installed locally; enumerate sources from PRODUCT_DIR. + for subdir in ('wolfcrypt/src', 'src'): + d = os.path.join(product_dir, subdir) + if os.path.isdir(d): + for name in os.listdir(d): + if name.endswith('.c'): + paths.add(os.path.join(d, name)) +else: + for file_elem in proj.findall('.//File'): + fp = file_elem.find('FilePath') + ft = file_elem.find('FileType') + if fp is None or not fp.text: + continue + ftype = int(ft.text) if ft is not None and ft.text else 0 + if ftype == 1 or fp.text.lower().endswith('.c'): + abs_path = os.path.normpath( + os.path.join(proj_dir, fp.text.replace('\\', '/')) + ) + paths.add(abs_path) + +for p in sorted(paths): + print(p) +PYEOF + echo "ERROR: failed to parse Keil project $CRA_SBOM_KEIL_PROJECT." >&2 + return 1 + } + # wolfssl's CMSIS-Pack branch legitimately emits paths under the installed + # pack dir (~/.arm/Packs/...), which lie OUTSIDE PRODUCT_DIR; filtering + # would wrongly drop them. The wolfssl parser already constrains its + # output to wolfssl sources, so skip the PRODUCT_DIR filter for wolfssl. + if [ "$_cra_product_name" != "wolfssl" ]; then + _cra_filter_to_product "$_cra_out_file" "$_cra_product_dir" + fi + if [ ! -s "$_cra_out_file" ]; then + echo "ERROR: CRA_SBOM_KEIL_PROJECT is set but no $_cra_product_name sources were extracted from $CRA_SBOM_KEIL_PROJECT." >&2 + return 1 + fi + _cra_n=$(wc -l < "$_cra_out_file" | tr -d ' ') + echo " Auto-extracted $_cra_n $_cra_product_name sources from Keil project" + return 0 + fi + + # Method 3: IAR .ewp + if [ -n "${CRA_SBOM_IAR_PROJECT:-}" ]; then + if [ ! -f "$CRA_SBOM_IAR_PROJECT" ]; then + echo "ERROR: CRA_SBOM_IAR_PROJECT=$CRA_SBOM_IAR_PROJECT not found." >&2 + return 1 + fi + if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: python3 is required to parse an IAR .ewp file." >&2 + return 1 + fi + python3 - "$CRA_SBOM_IAR_PROJECT" > "$_cra_out_file" <<'PYEOF' || { +import sys, os, xml.etree.ElementTree as ET + +def is_excluded(file_elem): + return file_elem.find('excluded') is not None + +proj_file = sys.argv[1] +proj_dir = os.path.dirname(os.path.abspath(proj_file)) +proj = ET.parse(proj_file) +paths = set() + +for file_elem in proj.findall('.//file'): + if is_excluded(file_elem): + continue + name_elem = file_elem.find('name') + if name_elem is None or not name_elem.text: + continue + raw = name_elem.text + if not raw.lower().endswith('.c'): + continue + resolved = raw.replace('$PROJ_DIR$', proj_dir) + paths.add(os.path.normpath(resolved.replace('\\', '/'))) + +for p in sorted(paths): + print(p) +PYEOF + echo "ERROR: failed to parse IAR project $CRA_SBOM_IAR_PROJECT." >&2 + return 1 + } + # Preserve wolfssl's original behaviour (all project .c, unfiltered); other + # products filter to PRODUCT_DIR to drop demo/BSP sources from the project. + if [ "$_cra_product_name" != "wolfssl" ]; then + _cra_filter_to_product "$_cra_out_file" "$_cra_product_dir" + fi + if [ ! -s "$_cra_out_file" ]; then + echo "ERROR: CRA_SBOM_IAR_PROJECT is set but no $_cra_product_name sources were extracted from $CRA_SBOM_IAR_PROJECT." >&2 + return 1 + fi + _cra_n=$(wc -l < "$_cra_out_file" | tr -d ' ') + echo " Auto-extracted $_cra_n $_cra_product_name sources from IAR project" + return 0 + fi + + # Method 4: Makefile dry-run + if [ -n "${CRA_SBOM_MAKEFILE_DIR:-}" ]; then + if [ ! -d "$CRA_SBOM_MAKEFILE_DIR" ]; then + echo "ERROR: CRA_SBOM_MAKEFILE_DIR=$CRA_SBOM_MAKEFILE_DIR is not a directory." >&2 + return 1 + fi + if ! command -v make >/dev/null 2>&1; then + echo "ERROR: make is required to auto-extract sources from CRA_SBOM_MAKEFILE_DIR." >&2 + return 1 + fi + # `make -n` (dry run) emits the compile commands; pull out any .c path + # that lives under PRODUCT_DIR. grep -F on the dir keeps the pattern + # literal (PRODUCT_DIR may contain regex metacharacters). + make -C "$CRA_SBOM_MAKEFILE_DIR" -n 2>/dev/null \ + | grep -oE '[^ ]+\.c' \ + | grep -F "$_cra_product_dir/" \ + | sort -u > "$_cra_out_file" || true + if [ ! -s "$_cra_out_file" ]; then + echo "ERROR: CRA_SBOM_MAKEFILE_DIR is set but make yielded no $_cra_product_name sources." >&2 + echo " Ensure 'make -n' in $CRA_SBOM_MAKEFILE_DIR references .c files under $_cra_product_dir." >&2 + return 1 + fi + _cra_n=$(wc -l < "$_cra_out_file" | tr -d ' ') + echo " Auto-extracted $_cra_n $_cra_product_name sources via Makefile (CRA_SBOM_MAKEFILE_DIR=$CRA_SBOM_MAKEFILE_DIR)" + return 0 + fi + + # Method 5: compile_commands.json (CMake / Zephyr / ESP-IDF) + if [ -n "${CRA_SBOM_BUILD_DIR:-}" ] && [ -f "$CRA_SBOM_BUILD_DIR/compile_commands.json" ]; then + if ! command -v jq >/dev/null 2>&1; then + echo "ERROR: jq is required to auto-extract sources from compile_commands.json." >&2 + echo " Install jq, or set CRA_SBOM_SRCS_FILE manually. See SRCS-FILE-HOWTO.md." >&2 + return 1 + fi + jq -r '.[].file' "$CRA_SBOM_BUILD_DIR/compile_commands.json" \ + | grep -F "$_cra_product_dir/" \ + | grep -E '/(wolfcrypt/src|src)/[^/]+\.c$' \ + | sort -u > "$_cra_out_file" || true + if [ ! -s "$_cra_out_file" ]; then + echo "ERROR: compile_commands.json in $CRA_SBOM_BUILD_DIR yielded no $_cra_product_name sources under $_cra_product_dir." >&2 + return 1 + fi + _cra_n=$(wc -l < "$_cra_out_file" | tr -d ' ') + echo " Auto-extracted $_cra_n $_cra_product_name sources from compile_commands.json" + return 0 + fi + + # No extraction method selected — caller should use its default glob. + return 2 +} + +# _cra_filter_to_product OUT_FILE PRODUCT_DIR +# In-place filter: keep only lines that are .c paths under PRODUCT_DIR. +# grep -F keeps PRODUCT_DIR literal (it may contain regex metacharacters). +_cra_filter_to_product() { + _cra_f="$1" + _cra_dir="$2" + _cra_tmp=$(mktemp "${TMPDIR:-/tmp}/cra-filter.XXXXXX") || { + echo "ERROR: mktemp failed while filtering sources." >&2 + return 1 + } + _cra_auto_tempfiles="${_cra_auto_tempfiles:-} $_cra_tmp" + grep -F "$_cra_dir/" "$_cra_f" | grep -E '\.c$' | sort -u > "$_cra_tmp" || true + mv -f "$_cra_tmp" "$_cra_f" +} diff --git a/cra-kit/scripts/generate-wolfboot-sbom.sh b/cra-kit/scripts/generate-wolfboot-sbom.sh new file mode 100755 index 00000000..1207688f --- /dev/null +++ b/cra-kit/scripts/generate-wolfboot-sbom.sh @@ -0,0 +1,294 @@ +#!/bin/sh +# Generate wolfBoot component SBOMs via gen-sbom. +# +# wolfBoot is build-configuration-specific: its compiled source list depends +# on TARGET, SIGN, HASH, and EXT_FLASH. This script runs `make -n` against +# the wolfBoot tree to extract the exact set of .c files for the requested +# configuration, then calls gen-sbom directly. +# +# wolfcrypt sources are compiled directly into the wolfBoot image (there is +# no separate wolfssl shared library). They appear in OBJS alongside core +# wolfBoot sources and are therefore included as wolfBoot's own component +# sources, not as a separate dependency. +# +# Required variables: +# WOLFBOOT_DIR=path/to/wolfBoot (source tree root) +# WOLFBOOT_TARGET= (e.g. stm32h7, x86_64_efi) +# WOLFBOOT_SIGN= (e.g. ECC256, RSA2048, ED25519) +# +# Optional variables: +# WOLFBOOT_HASH= (default: SHA256) +# WOLFBOOT_EXT_FLASH=<0|1> (default: 0) +# CRA_PYTHON=python3 (Python interpreter with gen-sbom deps) +# CRA_LICENSE_OVERRIDE= (e.g. LicenseRef-wolfBoot-Commercial) +# CRA_LICENSE_TEXT= (required when CRA_LICENSE_OVERRIDE is a +# LicenseRef-* id: plain-text license +# embedded in the SBOM) +# CRA_SBOM_OUT_DIR= (override output directory) +# CRA_SBOM_SRCS_FILE=path explicit .c file list (overrides make -n) +# CRA_SBOM_KEIL_PROJECT=path auto-extract from Keil .uvprojx (overrides make -n) +# CRA_SBOM_IAR_PROJECT=path auto-extract from IAR .ewp (overrides make -n) +# CRA_SBOM_NO_HASH=true emit SBOM without an artifact hash, skipping +# the source list — for NDA customers who cannot +# share source lists; WARNING: not suitable for +# production compliance +set -eu + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +KIT_DIR=$(dirname "$SCRIPT_DIR") + +# Shared source-extraction helper. It appends temp files it creates to +# _cra_auto_tempfiles, so the EXIT trap below cleans both lists. +_auto_tempfiles="" +_cra_auto_tempfiles="" +trap 'rm -f ${_auto_tempfiles:-} ${_cra_auto_tempfiles:-}' EXIT + +# shellcheck source=_cra-sbom-extract.sh disable=SC1091 +# shellcheck disable=SC1091 # sourced helper, resolved at runtime +. "$SCRIPT_DIR/_cra-sbom-extract.sh" + +# Default wolfBoot directory: sibling of the wolfssl-examples checkout. +# shellcheck disable=SC2015 +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFBOOT_DIR=${WOLFBOOT_DIR:-$(cd "$KIT_DIR/../../wolfBoot" 2>/dev/null && pwd || true)} + +if [ -z "${WOLFBOOT_DIR:-}" ] || [ ! -d "$WOLFBOOT_DIR" ]; then + echo "ERROR: wolfBoot source not found." >&2 + echo " Set WOLFBOOT_DIR to your wolfBoot checkout." >&2 + exit 1 +fi + +# CRA_SBOM_NO_HASH skips the make -n source extraction entirely, so TARGET/SIGN +# (which only drive that extraction) are not required in that mode. +if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + _no_hash=1 +else + _no_hash=0 +fi + +if [ "$_no_hash" = "0" ]; then + if [ -z "${WOLFBOOT_TARGET:-}" ]; then + echo "ERROR: WOLFBOOT_TARGET is not set." >&2 + echo " Example: WOLFBOOT_TARGET=stm32h7 $0" >&2 + exit 1 + fi + + if [ -z "${WOLFBOOT_SIGN:-}" ]; then + echo "ERROR: WOLFBOOT_SIGN is not set." >&2 + echo " Example: WOLFBOOT_SIGN=ECC256 $0" >&2 + exit 1 + fi +fi + +WOLFBOOT_HASH=${WOLFBOOT_HASH:-SHA256} +WOLFBOOT_EXT_FLASH=${WOLFBOOT_EXT_FLASH:-0} + +OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolfboot-component"} + +# gen-sbom lives inside the wolfssl submodule under wolfBoot; GEN_SBOM env var overrides. +GEN_SBOM="${GEN_SBOM:-$WOLFBOOT_DIR/lib/wolfssl/scripts/gen-sbom}" +if [ ! -f "$GEN_SBOM" ]; then + echo "ERROR: gen-sbom not found at $GEN_SBOM" >&2 + echo " Ensure the wolfssl submodule is initialized:" >&2 + echo " git -C \"$WOLFBOOT_DIR\" submodule update --init lib/wolfssl" >&2 + exit 1 +fi + +# Extract version from wolfBoot's version header. +VERSION=$(sed -n \ + 's/.*LIBWOLFBOOT_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$WOLFBOOT_DIR/include/wolfboot/version.h") +if [ -z "$VERSION" ]; then + echo "ERROR: could not detect wolfBoot version from $WOLFBOOT_DIR/include/wolfboot/version.h" >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" +CDX_OUT="$OUT_DIR/wolfboot-${VERSION}.cdx.json" +SPDX_OUT="$OUT_DIR/wolfboot-${VERSION}.spdx.json" + +echo "wolfBoot tree: $WOLFBOOT_DIR" +if [ "$_no_hash" = "0" ]; then + echo "Configuration: TARGET=$WOLFBOOT_TARGET SIGN=$WOLFBOOT_SIGN HASH=$WOLFBOOT_HASH EXT_FLASH=$WOLFBOOT_EXT_FLASH" +fi +echo "Version: $VERSION" +echo "Outputs: $CDX_OUT" +echo " $SPDX_OUT" +if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then + echo "License override: $CRA_LICENSE_OVERRIDE" +fi + +# A LicenseRef-* override requires the license text to be embedded in the SBOM +# (SPDX 2.3 §10.1). gen-sbom hard-fails without it; catch the omission here. +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 license text to be" >&2 + echo " embedded for any LicenseRef-* used in licenseConcluded/licenseDeclared." >&2 + echo " Re-run with CRA_LICENSE_TEXT=/path/to/wolfboot-license.txt" >&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. +if [ -n "${CRA_LICENSE_TEXT:-}" ] && [ -f "$CRA_LICENSE_TEXT" ]; then + CRA_LICENSE_TEXT=$(CDPATH='' cd -- "$(dirname -- "$CRA_LICENSE_TEXT")" && pwd)/$(basename -- "$CRA_LICENSE_TEXT") +fi + +# CRA_SBOM_NO_HASH (resolved to $_no_hash above) emits a placeholder checksum +# and skips the source list entirely (NDA customers who cannot share sources). +# Extract the configuration-specific source list. +# +# Priority order: +# 1. CRA_SBOM_SRCS_FILE explicit .c list +# 2. CRA_SBOM_KEIL_PROJECT Keil .uvprojx +# 3. CRA_SBOM_IAR_PROJECT IAR .ewp +# 4. make -n TARGET/SIGN wolfBoot's product-specific default (below) +# +# The shared helper handles 1-3. wolfBoot's Makefile path is product-specific +# (driven by TARGET/SIGN/HASH/EXT_FLASH, not the generic CRA_SBOM_MAKEFILE_DIR / +# compile_commands.json handlers), so we blank those two env vars before calling +# the helper and run our own make -n below when no IDE project is set. +if [ "$_no_hash" = "0" ]; then +_srcs_tmp=$(mktemp "${TMPDIR:-/tmp}/wolfboot-sbom-srcs.XXXXXX") +_auto_tempfiles="${_auto_tempfiles:-} $_srcs_tmp" + +_saved_makefile_dir="${CRA_SBOM_MAKEFILE_DIR:-}" +_saved_build_dir="${CRA_SBOM_BUILD_DIR:-}" +CRA_SBOM_MAKEFILE_DIR="" +CRA_SBOM_BUILD_DIR="" + +_cra_rc=0 +_cra_extract_srcs "$WOLFBOOT_DIR" "wolfboot" "$_srcs_tmp" || _cra_rc=$? + +CRA_SBOM_MAKEFILE_DIR="$_saved_makefile_dir" +CRA_SBOM_BUILD_DIR="$_saved_build_dir" + +if [ "$_cra_rc" -eq 0 ]; then + _n=$(wc -l < "$_srcs_tmp" | tr -d ' ') + echo " Extracted $_n source files (from IDE project / source list)" +elif [ "$_cra_rc" -eq 2 ]; then + # No IDE project set: use wolfBoot's product-specific make -n extraction. + # + # `make -n TARGET=... SIGN=...` prints every command make would run without + # executing them. The compiler invocations include every .c file on the + # wolfBoot link line, including both core wolfBoot sources and wolfcrypt + # files compiled inline. grep -oE pulls out every .c argument; sort -u + # deduplicates. + echo "Extracting source list via make -n TARGET=$WOLFBOOT_TARGET SIGN=$WOLFBOOT_SIGN ..." + make --no-print-directory \ + -C "$WOLFBOOT_DIR" \ + -n \ + TARGET="$WOLFBOOT_TARGET" \ + SIGN="$WOLFBOOT_SIGN" \ + HASH="$WOLFBOOT_HASH" \ + EXT_FLASH="$WOLFBOOT_EXT_FLASH" \ + 2>/dev/null \ + | grep -oE '[^ ]+\.c' \ + | grep -v '\.h' \ + | sort -u > "$_srcs_tmp" || true + + if [ ! -s "$_srcs_tmp" ]; then + echo "ERROR: make -n yielded no .c source files for TARGET=$WOLFBOOT_TARGET SIGN=$WOLFBOOT_SIGN." >&2 + echo " Check that TARGET and SIGN are valid for this wolfBoot tree." >&2 + exit 1 + fi + + _n=$(wc -l < "$_srcs_tmp" | tr -d ' ') + echo " Extracted $_n source files" +else + exit 1 +fi + +# Resolve paths to absolute: make -n emits relative paths; gen-sbom needs to +# open the files to compute gitoid hashes. +_srcs_abs_tmp=$(mktemp "${TMPDIR:-/tmp}/wolfboot-sbom-srcs-abs.XXXXXX") +_auto_tempfiles="${_auto_tempfiles:-} $_srcs_abs_tmp" + +while IFS= read -r _src; do + [ -n "$_src" ] || continue + # Paths from make -n are relative to wolfBoot root. + if [ "${_src#/}" = "$_src" ]; then + _abs="$WOLFBOOT_DIR/$_src" + else + _abs="$_src" + fi + # Skip paths that do not exist on disk (generated files, stubs, etc.). + if [ -f "$_abs" ]; then + echo "$_abs" + fi +done < "$_srcs_tmp" > "$_srcs_abs_tmp" + +if [ ! -s "$_srcs_abs_tmp" ]; then + echo "ERROR: no source files from make -n exist on disk." >&2 + echo " Verify WOLFBOOT_DIR=$WOLFBOOT_DIR and submodules are initialized." >&2 + exit 1 +fi + +_n_abs=$(wc -l < "$_srcs_abs_tmp" | tr -d ' ') +echo " Resolved $_n_abs paths ($(( _n - _n_abs )) non-existent skipped)" +else + echo "==> CRA_SBOM_NO_HASH=true: emitting SBOM without artifact hash." + echo " WARNING: not suitable for production CRA compliance." >&2 +fi + +# Preprocess build settings for gen-sbom --options-h. +# +# wolfBoot has no options.h (it is not an autotools project). We use +# cc -dM -E on the host with wolfBoot's include dirs to produce a flat +# #define file that gen-sbom can parse for algorithm enablement. +_defines_tmp=$(mktemp "${TMPDIR:-/tmp}/wolfboot-sbom-defines.XXXXXX") +_auto_tempfiles="${_auto_tempfiles:-} $_defines_tmp" + +CC=${CC:-cc} +echo " Preprocessing build settings via $CC -dM -E ..." +if ! "$CC" -dM -E \ + -I"$WOLFBOOT_DIR/include" \ + -I"$WOLFBOOT_DIR/lib/wolfssl" \ + -I"$WOLFBOOT_DIR/lib/wolfssl/wolfcrypt/src" \ + -DWOLFSSL_USER_SETTINGS \ + -x c /dev/null > "$_defines_tmp" 2>/dev/null; then + echo "ERROR: $CC -dM -E failed; install a host C compiler or set CC." >&2 + exit 1 +fi + +# Build gen-sbom argument list. +_PYTHON=${CRA_PYTHON:-python3} +command -v "$_PYTHON" >/dev/null 2>&1 || \ + { echo "ERROR: $_PYTHON not found. Set CRA_PYTHON to your Python interpreter." >&2; exit 1; } + +_license_override=${CRA_LICENSE_OVERRIDE:-GPL-3.0-only} +if [ "$_no_hash" = "1" ]; then + set -- --no-artifact-hash +else + set -- --srcs-file "$_srcs_abs_tmp" +fi +set -- "$@" \ + --cdx-out "$CDX_OUT" \ + --spdx-out "$SPDX_OUT" \ + --license-override "$_license_override" + +if [ -n "${CRA_LICENSE_TEXT:-}" ]; then + set -- "$@" --license-text "$CRA_LICENSE_TEXT" +fi + +echo "==> Running gen-sbom ..." +"$_PYTHON" "$GEN_SBOM" \ + --name wolfboot \ + --version "$VERSION" \ + --supplier "wolfSSL Inc." \ + --license-file "$WOLFBOOT_DIR/LICENSE" \ + --options-h "$_defines_tmp" \ + "$@" + +echo "SBOM written:" +echo " $CDX_OUT" +echo " $SPDX_OUT" diff --git a/cra-kit/scripts/generate-wolfhsm-sbom.sh b/cra-kit/scripts/generate-wolfhsm-sbom.sh new file mode 100755 index 00000000..9c44ac05 --- /dev/null +++ b/cra-kit/scripts/generate-wolfhsm-sbom.sh @@ -0,0 +1,231 @@ +#!/bin/sh +# Generate wolfHSM component SBOM (embedded gen-sbom path). +# +# wolfHSM is a Makefile-only library with no autotools configure step. +# This script always uses the embedded gen-sbom path: it enumerates +# wolfHSM sources directly from the source tree and derives compile-time +# defines via CC -dM -E (or pcpp when available). +# +# Required variables: +# WOLFSSL_DIR=path/to/wolfssl (source tree root; must contain scripts/gen-sbom) +# WOLFHSM_DIR=path/to/wolfHSM (source tree root) +# +# Optional variables: +# CC= (default: cc; set for cross builds) +# CRA_PYTHON=python3 (interpreter with pcpp) +# CRA_LICENSE_OVERRIDE= (e.g. LicenseRef-wolfSSL-Commercial) +# CRA_LICENSE_TEXT= (required when CRA_LICENSE_OVERRIDE is LicenseRef-*) +# WOLFHSM_BUILD_DIR=path auto-extract from compile_commands.json +# CRA_SBOM_SRCS_FILE=path explicit .c file list, one per line +# CRA_SBOM_KEIL_PROJECT=path auto-extract from Keil .uvprojx +# CRA_SBOM_IAR_PROJECT=path auto-extract from IAR .ewp +# CRA_SBOM_MAKEFILE_DIR=path auto-extract via make -n dry-run +# CRA_SBOM_NO_HASH=true emit SBOM without an artifact hash (NDA +# customers who cannot share source lists; +# WARNING: not suitable for production compliance) +set -eu + +# shellcheck disable=SC1091 # sourced helper, resolved at runtime +. "$(dirname "$0")/_cra-sbom-extract.sh" + +# Accumulator for temp files; cleaned up on exit. The shared extraction library +# appends to _cra_auto_tempfiles, so trap both. +_auto_tempfiles="" +_cra_auto_tempfiles="" +trap 'rm -f ${_auto_tempfiles:-} ${_cra_auto_tempfiles:-}' EXIT + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +KIT_DIR=$(dirname "$SCRIPT_DIR") + +# Locate WOLFSSL_DIR (default: sibling of wolfssl-examples). +# shellcheck disable=SC2015 +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFSSL_DIR=${WOLFSSL_DIR:-$(cd "$KIT_DIR/../../wolfssl" 2>/dev/null && pwd || true)} +# WOLFHSM_DIR has no sensible default; must be explicit. +WOLFHSM_DIR=${WOLFHSM_DIR:-} + +if [ -z "${WOLFSSL_DIR:-}" ] || [ ! -d "$WOLFSSL_DIR" ]; then + echo "ERROR: wolfSSL source not found." >&2 + echo " Set WOLFSSL_DIR to your wolfssl checkout." >&2 + exit 1 +fi + +if [ -z "${WOLFHSM_DIR:-}" ] || [ ! -d "$WOLFHSM_DIR" ]; then + echo "ERROR: WOLFHSM_DIR is not set or not a directory." >&2 + echo " Set WOLFHSM_DIR to your wolfHSM source tree." >&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 + +# Parse version from ChangeLog.md: first heading matching "wolfHSM Release vX.Y.Z". +# Current ChangeLog.md uses an H1 ("# wolfHSM Release v1.4.0"); tolerate one or +# two leading '#', surrounding whitespace, and an optional 'v' so a changelog +# style change does not silently break the parse. +VERSION=$(sed -n 's/^#\{1,2\}[[:space:]]*wolfHSM Release[[:space:]]*v\{0,1\}\([0-9][0-9.]*\).*/\1/p' \ + "$WOLFHSM_DIR/ChangeLog.md" 2>/dev/null | head -1) +if [ -z "$VERSION" ]; then + echo "ERROR: could not parse version from $WOLFHSM_DIR/ChangeLog.md." >&2 + exit 1 +fi + +OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolfhsm-component"} +mkdir -p "$OUT_DIR" +CDX_OUT="$OUT_DIR/wolfhsm-${VERSION}.cdx.json" +SPDX_OUT="$OUT_DIR/wolfhsm-${VERSION}.spdx.json" + +echo "wolfHSM tree: $WOLFHSM_DIR" +echo "wolfSSL tree: $WOLFSSL_DIR" +echo "Version: $VERSION" +echo "Outputs: $CDX_OUT" +echo " $SPDX_OUT" +if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then + echo "License override: $CRA_LICENSE_OVERRIDE" +fi + +# A LicenseRef-* override requires the licence text to be embedded (SPDX 2.3 §10.1). +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" >&2 + echo " to be embedded. Re-run with CRA_LICENSE_TEXT=/path/to/license.txt." >&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 absolute path (subshells resolve relative paths +# against their own CWD; gen-sbom may be invoked from a different directory). +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`. +_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 +} + +echo "==> Embedded path: gen-sbom with CC -dM -E (no user_settings.h)" + +# CRA_SBOM_NO_HASH emits a placeholder checksum and skips the source list +# entirely (for NDA customers who cannot share source lists). +if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + echo " NOTE: CRA_SBOM_NO_HASH=true: emitting SBOM without artifact hash." + echo " WARNING: not suitable for production CRA compliance." >&2 + _hash_arg="--no-artifact-hash" +else + _srcs_file=$(mktemp "${TMPDIR:-/tmp}/wolfhsm-srcs.XXXXXX") + _auto_tempfiles="${_auto_tempfiles:-} $_srcs_file" + + # Allow WOLFHSM_BUILD_DIR to feed compile_commands.json extraction. The + # shared library reads CRA_SBOM_BUILD_DIR; map our env var onto it. + CRA_SBOM_BUILD_DIR="${CRA_SBOM_BUILD_DIR:-${WOLFHSM_BUILD_DIR:-}}" + + _cra_rc=0 + _cra_extract_srcs "$WOLFHSM_DIR" "wolfhsm" "$_srcs_file" || _cra_rc=$? + + if [ "$_cra_rc" -eq 2 ]; then + # No extraction method active: enumerate all wolfHSM C sources via find. + # find is used (not glob) because wolfHSM sources span subdirectories. + find "$WOLFHSM_DIR/src" -name "*.c" | sort > "$_srcs_file" || { + echo "ERROR: find failed on $WOLFHSM_DIR/src" >&2; exit 1 + } + _n=$(wc -l < "$_srcs_file" | tr -d ' ') + echo " Source list: find $WOLFHSM_DIR/src -name '*.c' ($_n files)" + elif [ "$_cra_rc" -ne 0 ]; then + exit 1 + fi + + if [ ! -s "$_srcs_file" ]; then + echo "ERROR: no wolfHSM sources found in $WOLFHSM_DIR/src" >&2 + exit 1 + fi + _n=$(wc -l < "$_srcs_file" | tr -d ' ') + echo "NOTE: hashed $_n source file(s)" + _hash_arg="--srcs-file $_srcs_file" +fi + +# Build license-override args. +_license_args="" +if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then + _license_args="--license-override $CRA_LICENSE_OVERRIDE" + if [ -n "${CRA_LICENSE_TEXT:-}" ]; then + _license_args="$_license_args --license-text $CRA_LICENSE_TEXT" + fi +fi + +if _py=$(_python_with_pcpp); then + echo " Using $_py (pcpp) for --user-settings" + # wolfHSM has no user_settings.h equivalent; pass settings.h from wolfssl + # so gen-sbom has a preprocessable configuration source. The include path + # covers wolfHSM headers and the wolfssl tree. + SETTINGS_H="$WOLFSSL_DIR/wolfssl/wolfcrypt/settings.h" + if [ ! -f "$SETTINGS_H" ]; then + echo "ERROR: $SETTINGS_H not found." >&2 + exit 1 + fi + # shellcheck disable=SC2086 + "$_py" "$GEN" \ + --name wolfhsm \ + --version "$VERSION" \ + --supplier "wolfSSL Inc." \ + --license-file "$WOLFHSM_DIR/LICENSING" \ + --user-settings "$SETTINGS_H" \ + --user-settings-include "$WOLFHSM_DIR" \ + --user-settings-include "$WOLFSSL_DIR" \ + ${_hash_arg} \ + --cdx-out "$CDX_OUT" \ + --spdx-out "$SPDX_OUT" \ + ${_license_args} +else + echo "NOTE: pcpp not found; using CC -dM -E -> --options-h" + echo " Install pcpp: python3 -m pip install pcpp" + echo " For cross builds: set CC=" + + CC=${CC:-cc} + _defines=$(mktemp "${TMPDIR:-/tmp}/wolfhsm-defines.XXXXXX") + _auto_tempfiles="${_auto_tempfiles:-} $_defines" + if ! "$CC" -dM -E \ + -I"$WOLFHSM_DIR" \ + -I"$WOLFSSL_DIR" \ + -x c /dev/null >"$_defines" 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 + # shellcheck disable=SC2086 + "$_python" "$GEN" \ + --name wolfhsm \ + --version "$VERSION" \ + --supplier "wolfSSL Inc." \ + --license-file "$WOLFHSM_DIR/LICENSING" \ + --options-h "$_defines" \ + ${_hash_arg} \ + --cdx-out "$CDX_OUT" \ + --spdx-out "$SPDX_OUT" \ + ${_license_args} +fi + +echo "Done." diff --git a/cra-kit/scripts/generate-wolfmqtt-sbom.sh b/cra-kit/scripts/generate-wolfmqtt-sbom.sh new file mode 100755 index 00000000..0343aeff --- /dev/null +++ b/cra-kit/scripts/generate-wolfmqtt-sbom.sh @@ -0,0 +1,304 @@ +#!/bin/sh +# Generate wolfMQTT component SBOM (autotools make sbom, or embedded gen-sbom). +# +# Mode selection: +# CRA_SBOM_MODE=autotools|embedded (default: autotools) +# autotools: runs `make sbom` inside the wolfMQTT tree +# embedded: runs gen-sbom directly over the MQTT protocol sources, hashing +# them with an OmniBOR gitoid Merkle hash. Use this when wolfMQTT +# is compiled into firmware (ESP-IDF, Arduino, STM32, bare-metal) +# and there is no .so/.a to hash. +# +# Required variables: +# WOLFSSL_DIR=path/to/wolfssl (source tree root; must contain scripts/gen-sbom) +# WOLFMQTT_DIR=path/to/wolfMQTT (source tree root) +# +# Embedded-mode variables (CRA_SBOM_MODE=embedded): +# CRA_SBOM_SRCS_FILE=path/to/srcs.txt (explicit .c list, one path per line; +# used verbatim — highest priority) +# CRA_SBOM_KEIL_PROJECT=path/to/x.uvprojx (parse Keil project for .c sources) +# CRA_SBOM_IAR_PROJECT=path/to/x.ewp (parse IAR project for .c sources) +# CRA_SBOM_MAKEFILE_DIR=path/to/dir (run `make -n` to extract .c sources) +# CRA_SBOM_BUILD_DIR=path/to/build (CMake/ESP-IDF build dir; sources are +# read from its compile_commands.json) +# CRA_SBOM_NO_HASH=true (emit SBOM without an artifact hash, +# skipping the source list — for NDA +# customers who cannot share source lists; +# WARNING: not suitable for production compliance) +# +# Optional variables: +# CRA_LICENSE_OVERRIDE= (e.g. LicenseRef-wolfSSL-Commercial) +# CRA_LICENSE_TEXT= (required when CRA_LICENSE_OVERRIDE is LicenseRef-*) +# CRA_SBOM_OUT_DIR= (default: /auditor-packet/wolfmqtt-component) +set -eu +# Enable pipefail when the shell supports it (bash/ksh/some dash builds). +# Plain POSIX sh may not; tolerate its absence so the script still runs. +# shellcheck disable=SC3040 +if (set -o pipefail) 2>/dev/null; then set -o pipefail; fi + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +KIT_DIR=$(dirname "$SCRIPT_DIR") + +# Shared source-extraction helper (Keil/IAR/Makefile/compile_commands.json). +# shellcheck disable=SC1091 # sourced helper, resolved at runtime +. "$SCRIPT_DIR/_cra-sbom-extract.sh" + +# shellcheck disable=SC2015 +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFSSL_DIR=${WOLFSSL_DIR:-$(cd "$KIT_DIR/../../wolfssl" 2>/dev/null && pwd || true)} +WOLFMQTT_DIR=${WOLFMQTT_DIR:-} + +if [ -z "${WOLFSSL_DIR:-}" ] || [ ! -d "$WOLFSSL_DIR" ]; then + echo "ERROR: wolfSSL source not found." >&2 + echo " Set WOLFSSL_DIR to your wolfssl checkout (contains scripts/gen-sbom)." >&2 + exit 1 +fi + +if [ -z "${WOLFMQTT_DIR:-}" ] || [ ! -d "$WOLFMQTT_DIR" ]; then + echo "ERROR: WOLFMQTT_DIR is not set or not a directory." >&2 + echo " Set WOLFMQTT_DIR to your wolfMQTT source tree." >&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 + +# Parse version from wolfmqtt/version.h. +VERSION=$(sed -n \ + 's/.*LIBWOLFMQTT_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$WOLFMQTT_DIR/wolfmqtt/version.h" 2>/dev/null || true) +if [ -z "$VERSION" ]; then + echo "ERROR: could not parse version from $WOLFMQTT_DIR/wolfmqtt/version.h." >&2 + exit 1 +fi + +OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolfmqtt-component"} +mkdir -p "$OUT_DIR" +CDX_OUT="$OUT_DIR/wolfmqtt-${VERSION}.cdx.json" +SPDX_OUT="$OUT_DIR/wolfmqtt-${VERSION}.spdx.json" + +echo "wolfMQTT tree: $WOLFMQTT_DIR" +echo "wolfSSL tree: $WOLFSSL_DIR" +echo "Version: $VERSION" +echo "Outputs: $CDX_OUT" +echo " $SPDX_OUT" +if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then + echo "License override: $CRA_LICENSE_OVERRIDE" +fi + +# A LicenseRef-* override requires the licence text to be embedded (SPDX 2.3 §10.1). +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" >&2 + echo " to be embedded. Re-run with CRA_LICENSE_TEXT=/path/to/license.txt." >&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: make sbom runs inside a +# subshell `cd "$WOLFMQTT_DIR"`, where a relative path would resolve against +# the wolfMQTT 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 + +_run_autotools() { + echo "==> Autotools path: make sbom" + + # Detect whether the wolfMQTT tree is already configured; run ./configure first + # if no Makefile is present. + (cd "$WOLFMQTT_DIR" && { + if [ ! -f Makefile ]; then + echo " Running ./configure first (WOLFSSL_DIR=$WOLFSSL_DIR)..." + ./configure --with-wolfssl="$WOLFSSL_DIR" + fi + if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then + if [ -n "${CRA_LICENSE_TEXT:-}" ]; then + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" \ + SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" \ + SBOM_LICENSE_TEXT="$CRA_LICENSE_TEXT" + else + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" \ + SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" + fi + else + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" + fi + # make sbom names artifacts after configure.ac's PACKAGE_VERSION; if + # that ever skews from wolfmqtt/version.h (our $VERSION), fail with an + # explanation instead of a cryptic cp "No such file" under set -eu. + if [ ! -f "wolfmqtt-${VERSION}.cdx.json" ]; then + echo "ERROR: make sbom did not produce wolfmqtt-${VERSION}.cdx.json." >&2 + echo " wolfmqtt/version.h says $VERSION but the autotools" >&2 + echo " PACKAGE_VERSION (which names make sbom outputs) differs:" >&2 + ls wolfmqtt-*.cdx.json >&2 2>/dev/null || true + exit 1 + fi + cp -f "wolfmqtt-${VERSION}.cdx.json" "$CDX_OUT" + cp -f "wolfmqtt-${VERSION}.spdx.json" "$SPDX_OUT" + if [ -f "wolfmqtt-${VERSION}.spdx" ]; then + cp -f "wolfmqtt-${VERSION}.spdx" "$OUT_DIR/" + fi + }) +} + +_run_embedded() { + echo "==> Embedded path: gen-sbom over MQTT protocol sources" + + # wolfMQTT's MQTT protocol implementation lives entirely in src/mqtt_*.c. + # Unlike wolfTPM there are no platform-specific HAL files to exclude and no + # host-only sources, so the mqtt_*.c glob is the correct default source set. + # + # wolfcrypt/wolfSSL crypto sources and wolfSSL TLS sources are deliberately + # NOT hashed here: they are a separate component with their own SBOM, + # produced by generate-wolfssl-sbom.sh. Mixing them in would double-count + # the crypto component and misattribute its provenance to wolfMQTT. + if [ ! -d "$WOLFMQTT_DIR/src" ]; then + echo "ERROR: $WOLFMQTT_DIR/src not found; cannot locate MQTT sources." >&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 + + PY=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true) + if [ -z "$PY" ]; then + echo "ERROR: python3 (or python) not found; required to run gen-sbom." >&2 + exit 1 + fi + + # wolfMQTT ships its own licence file; detect the SPDX id from it (not from + # the wolfSSL tree, which carries a different LICENSING file). + LICENSE_FILE="" + for _lf in "$WOLFMQTT_DIR/LICENSE" "$WOLFMQTT_DIR/COPYING" "$WOLFMQTT_DIR/LICENSING"; do + if [ -f "$_lf" ]; then + LICENSE_FILE="$_lf" + break + fi + done + if [ -z "$LICENSE_FILE" ]; then + echo "ERROR: no LICENSE/COPYING/LICENSING file found in $WOLFMQTT_DIR." >&2 + exit 1 + fi + + SRCS_LIST=$(mktemp "${TMPDIR:-/tmp}/wolfmqtt-srcs.XXXXXX") || { + echo "ERROR: mktemp failed for the source-list temp file." >&2 + exit 1 + } + # gen-sbom requires exactly one of --options-h / --user-settings to source + # its build properties. Those describe wolfSSL's crypto/TLS configuration, + # which belongs to the wolfssl component's SBOM, not wolfMQTT's. Feed an + # empty options file so gen-sbom records no (and thus no misattributed) + # build defines for the wolfMQTT component. + EMPTY_OPTS=$(mktemp "${TMPDIR:-/tmp}/wolfmqtt-opts.XXXXXX") || { + echo "ERROR: mktemp failed for the empty options temp file." >&2 + exit 1 + } + # _cra_auto_tempfiles collects any temp files the shared extractor creates; + # initialise it so the EXIT trap is safe under `set -u` even when the + # extractor adds nothing (e.g. the default-glob path). + _cra_auto_tempfiles="" + trap 'rm -f "$SRCS_LIST" "$EMPTY_OPTS" $_cra_auto_tempfiles' EXIT + + # CRA_SBOM_NO_HASH emits a placeholder checksum and skips the source list + # entirely (for NDA customers who cannot share source lists). + if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + echo " NOTE: CRA_SBOM_NO_HASH=true: emitting SBOM without artifact hash." + echo " WARNING: not suitable for production CRA compliance." >&2 + _count=0 + set -- --no-artifact-hash --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT" + else + # Resolve the source list via the shared extractor (CRA_SBOM_SRCS_FILE, + # Keil/IAR projects, Makefile dry-run, or compile_commands.json). It + # returns 2 when no extraction method is selected, in which case we + # fall back to the default mqtt_*.c glob. + _cra_rc=0 + _cra_extract_srcs "$WOLFMQTT_DIR" "wolfmqtt" "$SRCS_LIST" || _cra_rc=$? + + if [ "$_cra_rc" -eq 2 ]; then + # No extraction method active: use default glob (all src/mqtt_*.c + # sorted). MQTT has no HAL split, so the full source set is the + # right default for bare-metal builds without an extractable list. + echo " Source list: default glob $WOLFMQTT_DIR/src/mqtt_*.c" + for _c in "$WOLFMQTT_DIR"/src/mqtt_*.c; do + [ -f "$_c" ] && echo "$_c" + done | sort > "$SRCS_LIST" + elif [ "$_cra_rc" -ne 0 ]; then + exit 1 + fi + + if [ ! -s "$SRCS_LIST" ]; then + echo "ERROR: no MQTT source files found to hash." >&2 + exit 1 + fi + + # Validate every resolved path exists before handing the file to gen-sbom. + _count=0 + while IFS= read -r _src; do + [ -n "$_src" ] || continue + if [ ! -f "$_src" ]; then + echo "ERROR: listed source not found: $_src" >&2 + exit 1 + fi + _count=$((_count + 1)) + done < "$SRCS_LIST" + + set -- --srcs-file "$SRCS_LIST" --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT" + fi + 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 + + "$PY" "$GEN" \ + --name wolfmqtt --version "$VERSION" \ + --license-file "$LICENSE_FILE" \ + --options-h "$EMPTY_OPTS" \ + "$@" || { + echo "ERROR: gen-sbom failed." >&2 + exit 1 + } + + if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + echo "NOTE: artifact hash omitted (CRA_SBOM_NO_HASH)" + else + echo "NOTE: hashed ${_count} source file(s)" + fi +} + +case "${CRA_SBOM_MODE:-autotools}" in + autotools) _run_autotools ;; + embedded) _run_embedded ;; + *) + echo "ERROR: unknown CRA_SBOM_MODE='${CRA_SBOM_MODE:-}' (expected 'autotools' or 'embedded')" >&2 + exit 1 + ;; +esac + +# Verify the generator actually produced non-empty SBOM files. +for _out in "$CDX_OUT" "$SPDX_OUT"; do + if [ ! -s "$_out" ]; then + echo "ERROR: expected SBOM output missing or empty: $_out" >&2 + exit 1 + fi +done + +echo "Done." diff --git a/cra-kit/scripts/generate-wolfsentry-sbom.sh b/cra-kit/scripts/generate-wolfsentry-sbom.sh new file mode 100755 index 00000000..2a458058 --- /dev/null +++ b/cra-kit/scripts/generate-wolfsentry-sbom.sh @@ -0,0 +1,203 @@ +#!/bin/sh +# Generate wolfSentry component SBOMs via gen-sbom. +# +# Required variables: +# WOLFSENTRY_DIR=path/to/wolfsentry (source tree root) +# +# gen-sbom location (one required): +# WOLFSSL_DIR=path/to/wolfssl (gen-sbom taken from scripts/gen-sbom) +# CRA_GEN_SBOM=path/to/gen-sbom (direct path; overrides WOLFSSL_DIR) +# +# Optional variables: +# CRA_SBOM_OUT_DIR= (default: $KIT_DIR/auditor-packet/wolfsentry-component) +# CC= (default: cc; for -dM -E options dump) +# CRA_WOLFSENTRY_IP_STACK=wolfip|lwip|none +# (default: none; selects which optional IP-stack glue +# to include in the firmware source set) +# CRA_SBOM_NO_HASH=true emit SBOM without an artifact hash, skipping the +# source list — for NDA customers who cannot share +# source lists; WARNING: not suitable for production +# compliance +set -eu + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +KIT_DIR=$(dirname "$SCRIPT_DIR") + +# shellcheck disable=SC2015 +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFSENTRY_DIR=${WOLFSENTRY_DIR:-$(cd "$KIT_DIR/../../wolfsentry" 2>/dev/null && pwd || true)} +OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolfsentry-component"} + +# CRA_WOLFSENTRY_IP_STACK selects which optional IP stack glue is included. +# Values: wolfip, lwip, none (default: none). +# Why default none / exactly one: wolfip/ and lwip/ both contain +# packet_filter_glue.c, and a firmware build compiles exactly one IP stack; +# including both would misrepresent what is actually in the firmware. +# (gen-sbom keys its Merkle hash on relative path, not basename, so the two +# same-named files no longer collide -- but a build still uses only one.) +CRA_WOLFSENTRY_IP_STACK="${CRA_WOLFSENTRY_IP_STACK:-none}" + +case "$CRA_WOLFSENTRY_IP_STACK" in + wolfip|lwip|none) ;; + *) echo "ERROR: CRA_WOLFSENTRY_IP_STACK must be wolfip, lwip, or none (got: $CRA_WOLFSENTRY_IP_STACK)" >&2; exit 1 ;; +esac + +if [ -z "${WOLFSENTRY_DIR:-}" ] || [ ! -d "$WOLFSENTRY_DIR" ]; then + echo "ERROR: wolfSentry source not found." >&2 + echo " Set WOLFSENTRY_DIR to your wolfsentry checkout (sibling of wolfssl-examples)." >&2 + exit 1 +fi + +# Resolve gen-sbom: CRA_GEN_SBOM takes precedence, then WOLFSSL_DIR. +if [ -n "${CRA_GEN_SBOM:-}" ]; then + GEN_SBOM="$CRA_GEN_SBOM" +elif [ -n "${WOLFSSL_DIR:-}" ]; then + GEN_SBOM="$WOLFSSL_DIR/scripts/gen-sbom" +else + echo "ERROR: gen-sbom location not specified." >&2 + echo " Set WOLFSSL_DIR (path to wolfssl repo) or CRA_GEN_SBOM (direct path to gen-sbom)." >&2 + exit 1 +fi + +if [ ! -f "$GEN_SBOM" ]; then + echo "ERROR: gen-sbom not found: $GEN_SBOM" >&2 + exit 1 +fi + +# Extract version from wolfsentry/wolfsentry.h macros. +HEADER="$WOLFSENTRY_DIR/wolfsentry/wolfsentry.h" +if [ ! -f "$HEADER" ]; then + echo "ERROR: version header not found: $HEADER" >&2 + exit 1 +fi +_extract_ver() { + grep -E "^#define[[:space:]]+$1[[:space:]]+[0-9]+" "$HEADER" | awk '{print $3}' +} +_major=$(_extract_ver WOLFSENTRY_VERSION_MAJOR) +_minor=$(_extract_ver WOLFSENTRY_VERSION_MINOR) +_tiny=$(_extract_ver WOLFSENTRY_VERSION_TINY) +VERSION="${_major}.${_minor}.${_tiny}" +case "$VERSION" in + [0-9]*.[0-9]*.[0-9]*) ;; + *) echo "ERROR: could not parse wolfsentry version from $HEADER (got: '$VERSION')" >&2; exit 1 ;; +esac + +mkdir -p "$OUT_DIR" +CDX_OUT="$OUT_DIR/wolfsentry-${VERSION}.cdx.json" +SPDX_OUT="$OUT_DIR/wolfsentry-${VERSION}.spdx.json" + +echo "wolfSentry tree: $WOLFSENTRY_DIR" +echo "Version: $VERSION" +echo "gen-sbom: $GEN_SBOM" +echo "Outputs: $CDX_OUT" +echo " $SPDX_OUT" + +# CRA_SBOM_NO_HASH emits a placeholder checksum and skips the source list +# entirely (for NDA customers who cannot share source lists). +if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + _no_hash=1 + echo " NOTE: CRA_SBOM_NO_HASH=true: emitting SBOM without artifact hash." + echo " WARNING: not suitable for production CRA compliance." >&2 +else + _no_hash=0 + # Core sources: all .c files except the two IP stack subdirs, which are + # selected individually via CRA_WOLFSENTRY_IP_STACK (a build compiles one). + SRCS=$(find "$WOLFSENTRY_DIR/src" -name "*.c" \ + ! -path "*/wolfip/*" \ + ! -path "*/lwip/*" \ + | sort) + if [ -z "$SRCS" ]; then + echo "ERROR: no .c files found under $WOLFSENTRY_DIR/src/" >&2 + exit 1 + fi + + # Optionally add the selected IP stack. + if [ "$CRA_WOLFSENTRY_IP_STACK" != "none" ]; then + SRCS="$SRCS +$(find "$WOLFSENTRY_DIR/src/$CRA_WOLFSENTRY_IP_STACK" -name "*.c" | sort)" + echo " IP stack: $CRA_WOLFSENTRY_IP_STACK" + else + echo " NOTE: CRA_WOLFSENTRY_IP_STACK not set; IP glue excluded from SBOM." + echo " Set CRA_WOLFSENTRY_IP_STACK=wolfip or lwip to include it." + fi + + _n=$(echo "$SRCS" | wc -l | tr -d ' ') + echo "Sources: $_n .c files from $WOLFSENTRY_DIR/src/" +fi + +# Dump compiler defines for --options-h (no user_settings.h; wolfsentry is +# configured via Makefile flags, not a settings header). +CC=${CC:-cc} +_defines_h=$(mktemp "${TMPDIR:-/tmp}/wolfsentry-defines.XXXXXX") +_srcs_file=$(mktemp "${TMPDIR:-/tmp}/wolfsentry-srcs.XXXXXX") +trap 'rm -f "$_defines_h" "$_srcs_file"' EXIT +if ! "$CC" -dM -E -I"$WOLFSENTRY_DIR" -x c /dev/null >"$_defines_h" 2>/dev/null; then + echo "ERROR: $CC -dM -E failed; set CC to an available compiler." >&2 + exit 1 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: python3 not found in PATH." >&2 + exit 1 +fi + +# Build the component-checksum argument: either a placeholder (NO_HASH) or the +# enumerated source list written one path per line for gen-sbom's --srcs-file. +if [ "$_no_hash" = "1" ]; then + set -- --no-artifact-hash +else + printf '%s\n' "$SRCS" > "$_srcs_file" + set -- --srcs-file "$_srcs_file" +fi + +python3 "$GEN_SBOM" \ + --name wolfsentry \ + --version "$VERSION" \ + --supplier "wolfSSL Inc." \ + --license-file "$WOLFSENTRY_DIR/LICENSING" \ + --options-h "$_defines_h" \ + "$@" \ + --cdx-out "$CDX_OUT" \ + --spdx-out "$SPDX_OUT" + +# Post-process: rewrite pkg:generic/wolfsentry@X -> pkg:github/wolfSSL/wolfsentry@vX +# gen-sbom emits pkg:generic/{name}@{version} for non-wolfssl names; the canonical +# PURL for wolfsentry is the GitHub package form. +if ! CDX_OUT="$CDX_OUT" SPDX_OUT="$SPDX_OUT" VERSION="$VERSION" \ +python3 <<'PY' +import json, os, pathlib + +cdx = pathlib.Path(os.environ["CDX_OUT"]) +spdx = pathlib.Path(os.environ["SPDX_OUT"]) +ver = os.environ["VERSION"] + +GENERIC = "pkg:generic/wolfsentry@" +GITHUB = "pkg:github/wolfSSL/wolfsentry@v" + +def fix(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"] = fix(comp.get("purl", "")) + cdx.write_text(json.dumps(d, indent=2) + "\n") + print(f"Post-processed {cdx.name}: PURL -> {comp['purl']}") + +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"] = fix(ref.get("referenceLocator", "")) + spdx.write_text(json.dumps(d, indent=2) + "\n") + print(f"Post-processed {spdx.name}: PURL canonicalized") +PY +then + echo "ERROR: PURL post-processing failed; SBOM may carry pkg:generic PURLs." >&2 + exit 1 +fi + +echo "Done." diff --git a/cra-kit/scripts/generate-wolfssh-sbom.sh b/cra-kit/scripts/generate-wolfssh-sbom.sh new file mode 100755 index 00000000..c05b6a03 --- /dev/null +++ b/cra-kit/scripts/generate-wolfssh-sbom.sh @@ -0,0 +1,370 @@ +#!/bin/sh +# Generate wolfSSH component SBOM (autotools make sbom, or embedded gen-sbom). +# +# Mode selection: +# CRA_SBOM_MODE=autotools|embedded (default: autotools) +# autotools: builds libwolfssh.so and runs `make sbom` +# embedded: hashes wolfSSH source files directly (no .so is produced when +# wolfSSH is compiled into firmware), via wolfSSL's gen-sbom. +# +# Required variables: +# WOLFSSL_DIR=path/to/wolfssl (source tree root; provides gen-sbom) +# WOLFSSH_DIR=path/to/wolfssh (source tree root) +# +# Embedded-mode variables: +# CRA_SBOM_BUILD_DIR=path/to/build (embedded: dir containing +# compile_commands.json for CMake / ESP-IDF +# / Zephyr builds, used to extract the exact +# wolfSSH .c files on the link line) +# CRA_SBOM_SRCS_FILE=path/to/srcs.txt (embedded: explicit list of wolfSSH .c +# paths, one per line; takes priority over +# every other source-resolution method) +# CRA_SBOM_KEIL_PROJECT=path (embedded: auto-extract from Keil .uvprojx) +# CRA_SBOM_IAR_PROJECT=path (embedded: auto-extract from IAR .ewp) +# CRA_SBOM_MAKEFILE_DIR=path (embedded: auto-extract via make -n dry-run) +# CRA_SBOM_NO_HASH=true (embedded: emit SBOM without an artifact +# hash, skipping the source list — for NDA +# customers who cannot share source lists; +# WARNING: not suitable for production compliance) +# +# Optional variables: +# CRA_SBOM_OUT_DIR= (output directory; default auditor-packet) +# CRA_LICENSE_OVERRIDE= (e.g. LicenseRef-wolfSSH-Commercial) +# CRA_LICENSE_TEXT= (required when CRA_LICENSE_OVERRIDE is a +# LicenseRef-* id: plain-text licence embedded +# in the SBOM; make sbom / gen-sbom hard-fail +# without it.) +# POSIX sh (script is #!/bin/sh and run via `sh`); dash has no `set -o pipefail`, +# so we use `set -eu` like the rest of the kit. Pipelines that must not mask a +# failed first stage are checked explicitly instead. +set -eu + +# Accumulator for temp files (embedded source extraction); cleaned up on exit. +# Why: mktemp temp files must not leak if any later command fails under set -e; +# a single EXIT trap removes them on every exit path including errors. +_auto_tempfiles="" +_cra_auto_tempfiles="" +# _cra_auto_tempfiles is populated by _cra-sbom-extract.sh's helpers; clean both. +trap 'rm -f ${_auto_tempfiles:-} ${_cra_auto_tempfiles:-}' EXIT + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +KIT_DIR=$(dirname "$SCRIPT_DIR") + +# shared extraction methods (Keil, IAR, Makefile, compile_commands.json) +# shellcheck disable=SC1091 # sourced helper, resolved at runtime +. "$SCRIPT_DIR/_cra-sbom-extract.sh" +# shellcheck disable=SC2015 # `|| true` is a deliberate set -e guard, not if-then-else +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFSSL_DIR=${WOLFSSL_DIR:-$(cd "$KIT_DIR/../../wolfssl" 2>/dev/null && pwd || true)} +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFSSH_DIR=${WOLFSSH_DIR:-$(cd "$KIT_DIR/../../wolfSSH" 2>/dev/null && pwd || true)} +OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolfssh-component"} + +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 + +if [ -z "${WOLFSSH_DIR:-}" ] || [ ! -d "$WOLFSSH_DIR" ]; then + echo "ERROR: wolfSSH source not found." >&2 + echo " Set WOLFSSH_DIR to your wolfSSH checkout (sibling of wolfssl-examples)." >&2 + exit 1 +fi + +VERSION=$(sed -n \ + 's/.*LIBWOLFSSH_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$WOLFSSH_DIR/wolfssh/version.h" 2>/dev/null || true) +if [ -z "$VERSION" ]; then + echo "ERROR: could not extract LIBWOLFSSH_VERSION_STRING from $WOLFSSH_DIR/wolfssh/version.h" >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" +CDX_OUT="$OUT_DIR/wolfssh-${VERSION}.cdx.json" +SPDX_OUT="$OUT_DIR/wolfssh-${VERSION}.spdx.json" + +echo "wolfSSL tree: $WOLFSSL_DIR" +echo "wolfSSH tree: $WOLFSSH_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/wolfssh-commercial-license.txt." >&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: make sbom runs inside a +# `cd "$WOLFSSH_DIR"` subshell, where a relative path would resolve against the +# wolfSSH 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 +# the one first on PATH). pcpp lets gen-sbom walk settings.h without a compiler. +_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 +} + +# Resolve the wolfSSH source list for embedded mode into the file named by $1. +# +# Source set rule: ONLY wolfSSH protocol sources (${WOLFSSH_DIR}/src/*.c) are +# product-owned and belong in this component. The wolfcrypt / wolfSSL sources +# that wolfSSH links against are NOT included here — they are a separate +# component covered by generate-wolfssl-sbom.sh and referenced as a dependency. +# Mixing them in would double-count the crypto library across two SBOMs. +# +# Priority order (first match wins), all but the last handled by +# _cra_extract_srcs from _cra-sbom-extract.sh: +# 1. CRA_SBOM_SRCS_FILE — explicit user list beats anything inferred, because +# the user knows their exact link line. +# 2. CRA_SBOM_KEIL_PROJECT — parse Keil .uvprojx, filter to WOLFSSH_DIR. +# 3. CRA_SBOM_IAR_PROJECT — parse IAR .ewp, filter to WOLFSSH_DIR. +# 4. CRA_SBOM_MAKEFILE_DIR — `make -n` dry-run, filter to WOLFSSH_DIR. +# 5. compile_commands.json at CRA_SBOM_BUILD_DIR — per-build extraction for +# CMake / ESP-IDF / Zephyr. +# 6. Default: all ${WOLFSSH_DIR}/src/*.c sorted — the "all sources" fallback +# for toolchains where we cannot infer the exact +# subset compiled. Listing all sources is the safe +# over-approximation: it never omits a file that +# shipped. +_resolve_wolfssh_srcs() { + _out="$1" + + # Try every env-var-driven extraction method (SRCS_FILE, Keil, IAR, + # Makefile, compile_commands.json). rc=2 means none was set: fall back to + # the wolfSSH default glob below. + _cra_rc=0 + _cra_extract_srcs "$WOLFSSH_DIR" "wolfssh" "$_out" || _cra_rc=$? + if [ "$_cra_rc" -eq 0 ]; then + return 0 + elif [ "$_cra_rc" -ne 2 ]; then + exit 1 # _cra_extract_srcs already printed the error + fi + + # No extraction env var set: every wolfSSH src/*.c. A POSIX glob expands in + # sorted order; guard the no-match case where the pattern stays literal. + : > "$_out" + for _c in "$WOLFSSH_DIR"/src/*.c; do + [ -f "$_c" ] || continue + printf '%s\n' "$_c" >> "$_out" + done + if [ ! -s "$_out" ]; then + echo "ERROR: no wolfSSH sources found in $WOLFSSH_DIR/src/*.c." >&2 + exit 1 + fi + _n=$(wc -l < "$_out" | tr -d ' ') + echo " Using all $_n wolfSSH sources from $WOLFSSH_DIR/src/*.c (default)" + return 0 +} + +_run_embedded() { + echo "==> Embedded path: gen-sbom hashing wolfSSH source files" + + GEN="$WOLFSSL_DIR/scripts/gen-sbom" + if [ ! -f "$GEN" ]; then + echo "ERROR: $GEN not found (need a wolfSSL tree with SBOM support)." >&2 + exit 1 + fi + + # gen-sbom's embedded entry point walks wolfSSL's settings.h to resolve the + # build config. wolfSSH headers pull in wolfSSL headers, so we reuse the same + # settings.h + kit user_settings.h the wolfssl embedded path uses. + SETTINGS_H="$WOLFSSL_DIR/wolfssl/wolfcrypt/settings.h" + if [ ! -f "$SETTINGS_H" ]; then + echo "ERROR: $SETTINGS_H not found." >&2 + exit 1 + fi + if [ ! -f "$KIT_DIR/user_settings.h" ]; then + echo "ERROR: $KIT_DIR/user_settings.h missing (demo WOLFSSL_USER_SETTINGS)." >&2 + exit 1 + fi + + if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: python3 not found in PATH (required by gen-sbom)." >&2 + exit 1 + fi + + # CRA_SBOM_NO_HASH emits a placeholder checksum and skips the source list + # entirely (for NDA customers who cannot share source lists). + if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + echo " NOTE: CRA_SBOM_NO_HASH=true: emitting SBOM without artifact hash." + echo " WARNING: not suitable for production CRA compliance." >&2 + set -- --no-artifact-hash --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT" + else + # Resolve the source list into a temp file (cleaned up by the EXIT trap). + _srcs=$(mktemp "${TMPDIR:-/tmp}/wolfssh-srcs.XXXXXX") || { + echo "ERROR: mktemp failed for the source-list temp file." >&2 + exit 1 + } + _auto_tempfiles="${_auto_tempfiles:-} $_srcs" + _resolve_wolfssh_srcs "$_srcs" + + # Validate every resolved path exists before handing the file to gen-sbom. + while IFS= read -r _src; do + [ -n "$_src" ] || continue + if [ ! -f "$_src" ]; then + echo "ERROR: source file does not exist: $_src" >&2 + exit 1 + fi + done < "$_srcs" + if [ ! -s "$_srcs" ]; then + echo "ERROR: resolved source list is empty." >&2 + exit 1 + fi + set -- --srcs-file "$_srcs" --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT" + fi + 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" + else + echo "ERROR: no python3/python with pcpp installed (required for embedded mode)." >&2 + echo " Install it on the same interpreter: python3 -m pip install pcpp" >&2 + exit 1 + fi + + "$_py" "$GEN" \ + --name wolfssh --version "$VERSION" \ + --license-file "$WOLFSSH_DIR/LICENSING" \ + --user-settings "$SETTINGS_H" \ + --user-settings-include "$WOLFSSL_DIR" \ + --user-settings-include "$KIT_DIR" \ + --user-settings-define WOLFSSL_USER_SETTINGS \ + "$@" + + # Verify gen-sbom actually produced both outputs and they are non-empty. + for _f in "$CDX_OUT" "$SPDX_OUT"; do + if [ ! -s "$_f" ]; then + echo "ERROR: expected output missing or empty: $_f" >&2 + exit 1 + fi + done +} + +_run_autotools() { + echo "==> Autotools path: make sbom" + # `make sbom` names its output after the wolfSSH TREE's version + # (PACKAGE_VERSION). Detect mismatches early so the cp below doesn't fail + # with a cryptic "No such file or directory" under `set -eu`. + _tree_ver=$(sed -n \ + 's/.*LIBWOLFSSH_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$WOLFSSH_DIR/wolfssh/version.h" 2>/dev/null || true) + if [ -n "$_tree_ver" ] && [ "$_tree_ver" != "$VERSION" ]; then + echo "ERROR: wolfSSH tree is version $_tree_ver but expected $VERSION." >&2 + exit 1 + fi + (cd "$WOLFSSH_DIR" && { + if [ ! -f Makefile ]; then + echo " Running ./configure first..." + ./configure --with-wolfssl="$WOLFSSL_DIR" + fi + if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then + if [ -n "${CRA_LICENSE_TEXT:-}" ]; then + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" \ + SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" \ + SBOM_LICENSE_TEXT="$CRA_LICENSE_TEXT" + else + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" \ + SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" + fi + else + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" + fi + cp -f "wolfssh-${VERSION}.cdx.json" "$CDX_OUT" + cp -f "wolfssh-${VERSION}.spdx.json" "$SPDX_OUT" + if [ -f "wolfssh-${VERSION}.spdx" ]; then + cp -f "wolfssh-${VERSION}.spdx" "$OUT_DIR/" + fi + }) +} + +MODE=${CRA_SBOM_MODE:-autotools} +case "$MODE" in + embedded) _run_embedded ;; + autotools) _run_autotools ;; + *) + echo "ERROR: CRA_SBOM_MODE must be 'autotools' or 'embedded', not '$MODE'" >&2 + exit 1 + ;; +esac + +# ---- Post-process: defensive PURL canonicalization ---- +# Current gen-sbom already emits pkg:github/wolfSSL/wolfSSH@vX natively for +# known component names; the rewrite below is a defensive no-op kept only for +# older generator versions that may emit pkg:generic/wolfssh@X. +if ! CDX_OUT="$CDX_OUT" SPDX_OUT="$SPDX_OUT" \ + python3 <<'PY' +import json, os, pathlib + +cdx = pathlib.Path(os.environ["CDX_OUT"]) +spdx = pathlib.Path(os.environ["SPDX_OUT"]) + +GENERIC = "pkg:generic/wolfssh@" +GITHUB = "pkg:github/wolfSSL/wolfSSH@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", "")) + cdx.write_text(json.dumps(d, indent=2) + "\n") + print(f"Post-processed {cdx.name}") + +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", "")) + spdx.write_text(json.dumps(d, indent=2) + "\n") + print(f"Post-processed {spdx.name}") +PY +then + echo "ERROR: post-process failed (PURL canonicalization incomplete)." >&2 + echo " The emitted SBOM may carry pkg:generic PURLs; not trusting it." >&2 + exit 1 +fi + +echo "Done. SBOM outputs:" +echo " $CDX_OUT" +echo " $SPDX_OUT" diff --git a/cra-kit/scripts/generate-wolfssl-sbom.sh b/cra-kit/scripts/generate-wolfssl-sbom.sh index 799d7209..293cd92b 100755 --- a/cra-kit/scripts/generate-wolfssl-sbom.sh +++ b/cra-kit/scripts/generate-wolfssl-sbom.sh @@ -1,18 +1,53 @@ #!/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= (optional: e.g. LicenseRef-wolfSSL-Commercial) -# CRA_LICENSE_TEXT= (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.) +# Generate wolfSSL component SBOMs (autotools make sbom, cmake sbom, or embedded gen-sbom). +# +# Mode selection: +# CRA_SBOM_MODE=autotools|cmake|embedded +# autotools (default when configure+Makefile exist): runs `make sbom` +# cmake: runs `cmake --build $WOLFSSL_BUILD_DIR --target sbom` +# embedded: runs gen-sbom directly with source files and user_settings.h +# +# Required variables: +# WOLFSSL_DIR=path/to/wolfssl (source tree root) +# +# Mode-specific variables: +# WOLFSSL_BUILD_DIR=path/to/build (cmake mode: path to cmake build directory; +# embedded: also triggers compile_commands.json +# auto-extraction when present) +# CRA_SBOM_SRCS_FILE=path/to/srcs.txt (embedded: file listing .c paths, one per line; +# combined with the built-in demo list unless +# CRA_SBOM_SRCS_ONLY_FROM_FILE=true) +# CRA_SBOM_SRCS_ONLY_FROM_FILE=true (embedded: skip the built-in demo list and +# use only paths from CRA_SBOM_SRCS_FILE) +# CRA_SBOM_NO_HASH=true (embedded: emit SBOM without a real artifact +# hash; use when no source list is available) +# CRA_SBOM_MAKEFILE_DIR= (embedded: auto-extract srcs via make -n) +# CRA_SBOM_KEIL_PROJECT= (embedded: auto-extract srcs from .uvprojx) +# CRA_SBOM_IAR_PROJECT= (embedded: auto-extract srcs from .ewp) +# +# Optional variables: +# CRA_PYTHON=python3 (interpreter with pcpp; for embedded path) +# CRA_LICENSE_OVERRIDE= (e.g. LicenseRef-wolfSSL-Commercial) +# CRA_LICENSE_TEXT= (required when CRA_LICENSE_OVERRIDE is a +# LicenseRef-* id: plain-text licence embedded +# in the SBOM; gen-sbom / make sbom hard-fail +# without it.) set -eu +# Accumulator for temp files created by _auto_extract_srcs / the shared +# extraction library; cleaned up on exit. The library appends to +# _cra_auto_tempfiles, so trap both. +_auto_tempfiles="" +_cra_auto_tempfiles="" +trap 'rm -f ${_auto_tempfiles:-} ${_cra_auto_tempfiles:-}' EXIT + SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +# shellcheck source=_cra-sbom-extract.sh disable=SC1091 +# shellcheck disable=SC1091 # sourced helper, resolved at runtime +. "$SCRIPT_DIR/_cra-sbom-extract.sh" KIT_DIR=$(dirname "$SCRIPT_DIR") # shellcheck disable=SC2015 # `|| true` is a deliberate set -e guard, not if-then-else +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional 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" @@ -102,11 +137,39 @@ _embedded_srcs() { done } +_auto_extract_srcs() { + # Delegate to the shared extraction library. The library reads + # CRA_SBOM_BUILD_DIR for compile_commands.json; map WOLFSSL_BUILD_DIR onto it + # so the embedded cmake/Zephyr/ESP-IDF auto-extraction keeps working. + if [ -n "${WOLFSSL_BUILD_DIR:-}" ]; then + CRA_SBOM_BUILD_DIR=${CRA_SBOM_BUILD_DIR:-$WOLFSSL_BUILD_DIR} + fi + _auto=$(mktemp "${TMPDIR:-/tmp}/wolfssl-auto-srcs.XXXXXX") || { + echo "ERROR: mktemp failed for the auto-extract source list." >&2 + exit 1 + } + _auto_tempfiles="${_auto_tempfiles:-} $_auto" + # `|| _rc=$?` keeps `set -e` from aborting on the library's non-zero returns + # (1 = error, 2 = no method) so we can dispatch on the code below. + _rc=0 + _cra_extract_srcs "$WOLFSSL_DIR" "wolfssl" "$_auto" || _rc=$? + case "$_rc" in + 0) + CRA_SBOM_SRCS_FILE="$_auto" + CRA_SBOM_SRCS_ONLY_FROM_FILE=true + ;; + 2) + # No extraction method selected; fall back to the built-in demo list. + ;; + *) + # Library already printed an actionable error to stderr. + exit 1 + ;; + esac +} + _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 @@ -123,20 +186,88 @@ _run_embedded() { 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). + # --no-artifact-hash: skip all source-file logic and emit a placeholder hash. + # Use when no compiled library AND no source file list is accessible. + if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + if [ -n "${CRA_SBOM_SRCS_FILE:-}" ] || [ -n "${CRA_SBOM_SRCS_ONLY_FROM_FILE:-}" ]; then + echo "ERROR: CRA_SBOM_NO_HASH cannot be combined with CRA_SBOM_SRCS_FILE." >&2 + exit 1 + fi + echo " NOTE: CRA_SBOM_NO_HASH=true: emitting SBOM with placeholder hash." + echo " Contact wolfssl@wolfssl.com to discuss integrity verification" + echo " options before using this in production." + set -- --no-artifact-hash --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 + _py=$(command -v python3 2>/dev/null || command -v python) + [ -n "$_py" ] || { echo "ERROR: python3 not found." >&2; exit 1; } + "$_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 \ + "$@" + return 0 + fi + + # Auto-extract if caller didn't supply CRA_SBOM_SRCS_FILE + [ -z "${CRA_SBOM_SRCS_FILE:-}" ] && [ "${CRA_SBOM_NO_HASH:-}" != "true" ] && \ + _auto_extract_srcs + + # Build the source file list. + # + # Priority: + # CRA_SBOM_SRCS_ONLY_FROM_FILE=true — use only the caller-supplied file + # CRA_SBOM_SRCS_FILE (without ONLY) — merge file with built-in demo list + # (neither) — use built-in demo list + # + # The built-in 9-file demo list is for kit demonstration only. Production + # SBOMs MUST list every wolfSSL .c file on your link line. The post- + # processing step below watermarks demo outputs with wolfssl:sbom:demo=true. set -- - while IFS= read -r _src; do - [ -n "$_src" ] || continue - set -- "$@" "$_src" - done <&2 + exit 1 + fi + echo " Using source list from $CRA_SBOM_SRCS_FILE (CRA_SBOM_SRCS_ONLY_FROM_FILE=true)" + else + echo " NOTE: --srcs uses the kit's built-in 9-file DEMO list. Production SBOMs" + echo " must list every wolfSSL .c file you compile. Set CRA_SBOM_SRCS_FILE" + echo " to your link-time source list to replace the demo list." + echo " Output is watermarked wolfssl:sbom:demo=true." + while IFS= read -r _src; do + [ -n "$_src" ] || continue + set -- "$@" "$_src" + done <&2 + exit 1 + fi + _srcs_file_args="--srcs-file $CRA_SBOM_SRCS_FILE" + fi # 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. + # Append the --srcs positional args last; argparse stops --srcs consumption + # at the next -- option, so --cdx-out / --spdx-out end the list cleanly. + # Capture whether positional srcs exist before output flags are appended. + _srcs_flag="" + [ $# -gt 0 ] && _srcs_flag="--srcs" set -- "$@" --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT" if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then set -- "$@" --license-override "$CRA_LICENSE_OVERRIDE" @@ -147,6 +278,7 @@ EOF if _py=$(_python_with_pcpp); then echo " Using $_py (pcpp) for --user-settings" + # shellcheck disable=SC2086 "$_py" "$GEN" \ --name wolfssl --version "$VERSION" \ --license-file "$WOLFSSL_DIR/LICENSING" \ @@ -154,7 +286,8 @@ EOF --user-settings-include "$WOLFSSL_DIR" \ --user-settings-include "$KIT_DIR" \ --user-settings-define WOLFSSL_USER_SETTINGS \ - --srcs "$@" + ${_srcs_file_args} \ + ${_srcs_flag} "$@" return 0 fi @@ -173,7 +306,7 @@ EOF # 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 + _auto_tempfiles="${_auto_tempfiles:-} $DEFINES_H" CC=${CC:-cc} if ! "$CC" -dM -E \ -I"$WOLFSSL_DIR" \ @@ -187,11 +320,64 @@ EOF PYTHON=python3 command -v python3 >/dev/null 2>&1 || PYTHON=python + # shellcheck disable=SC2086 "$PYTHON" "$GEN" \ --name wolfssl --version "$VERSION" \ --license-file "$WOLFSSL_DIR/LICENSING" \ --options-h "$DEFINES_H" \ - --srcs "$@" + ${_srcs_file_args} \ + ${_srcs_flag} "$@" +} + +_run_cmake() { + echo "==> cmake path: cmake --build --target sbom" + if [ -z "${WOLFSSL_BUILD_DIR:-}" ]; then + echo "ERROR: WOLFSSL_BUILD_DIR is not set." >&2 + echo " Set it to your cmake out-of-source build directory." >&2 + echo " Example: cmake -B build && WOLFSSL_BUILD_DIR=\$PWD/build $0" >&2 + exit 1 + fi + if [ ! -d "$WOLFSSL_BUILD_DIR" ]; then + echo "ERROR: WOLFSSL_BUILD_DIR=$WOLFSSL_BUILD_DIR is not a directory." >&2 + exit 1 + fi + if ! command -v cmake >/dev/null 2>&1; then + echo "ERROR: cmake not found in PATH." >&2 + exit 1 + fi + + # Detect version from cmake cache so we can find the output files. + # cmake -L/-LA both omit :STATIC (internal) entries; grep the cache file directly. + _cmake_ver=$(grep -m1 '^CMAKE_PROJECT_VERSION:STATIC=' \ + "$WOLFSSL_BUILD_DIR/CMakeCache.txt" 2>/dev/null | cut -d= -f2) + if [ -z "$_cmake_ver" ]; then + echo "WARNING: could not detect PROJECT_VERSION from cmake cache; using kit VERSION=$VERSION" >&2 + _cmake_ver="$VERSION" + fi + if [ "$_cmake_ver" != "$VERSION" ]; then + echo "ERROR: cmake build has wolfSSL $_cmake_ver but the kit is pinned to $VERSION." >&2 + echo " Update cra-kit/VERSION or reconfigure cmake against wolfSSL $VERSION." >&2 + exit 1 + fi + + cmake --build "$WOLFSSL_BUILD_DIR" --target sbom + + _cdx_src="$WOLFSSL_BUILD_DIR/wolfssl-${VERSION}.cdx.json" + _spdx_src="$WOLFSSL_BUILD_DIR/wolfssl-${VERSION}.spdx.json" + _tv_src="$WOLFSSL_BUILD_DIR/wolfssl-${VERSION}.spdx" + for _f in "$_cdx_src" "$_spdx_src"; do + if [ ! -f "$_f" ]; then + echo "ERROR: expected cmake sbom output not found: $_f" >&2 + echo " The sbom target may have failed; check cmake build output above." >&2 + exit 1 + fi + done + + cp -f "$_cdx_src" "$CDX_OUT" + cp -f "$_spdx_src" "$SPDX_OUT" + if [ -f "$_tv_src" ]; then + cp -f "$_tv_src" "$OUT_DIR/" + fi } _run_autotools() { @@ -237,8 +423,12 @@ MODE=${CRA_SBOM_MODE:-} case "$MODE" in embedded) _run_embedded ;; autotools) _run_autotools ;; + cmake) _run_cmake ;; "") - if [ -f "$WOLFSSL_DIR/Makefile" ] && [ -f "$WOLFSSL_DIR/configure" ]; then + if [ -n "${WOLFSSL_BUILD_DIR:-}" ] && [ -d "${WOLFSSL_BUILD_DIR}" ]; then + MODE=cmake + _run_cmake + elif [ -f "$WOLFSSL_DIR/Makefile" ] && [ -f "$WOLFSSL_DIR/configure" ]; then MODE=autotools _run_autotools else @@ -247,7 +437,7 @@ case "$MODE" in fi ;; *) - echo "ERROR: CRA_SBOM_MODE must be 'autotools' or 'embedded', not '$MODE'" >&2 + echo "ERROR: CRA_SBOM_MODE must be 'autotools', 'cmake', or 'embedded', not '$MODE'" >&2 exit 1 ;; esac @@ -260,12 +450,14 @@ esac # 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" \ + CRA_SBOM_SRCS_ONLY_FROM_FILE="${CRA_SBOM_SRCS_ONLY_FROM_FILE:-}" \ 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" +demo = os.environ.get("CRA_SBOM_MODE_FINAL") == "embedded" and \ + os.environ.get("CRA_SBOM_SRCS_ONLY_FROM_FILE") != "true" GENERIC = "pkg:generic/wolfssl@" GITHUB = "pkg:github/wolfSSL/wolfssl@v" diff --git a/cra-kit/scripts/generate-wolftpm-sbom.sh b/cra-kit/scripts/generate-wolftpm-sbom.sh new file mode 100755 index 00000000..9f85960f --- /dev/null +++ b/cra-kit/scripts/generate-wolftpm-sbom.sh @@ -0,0 +1,502 @@ +#!/bin/sh +# Generate wolfTPM component SBOMs (autotools make sbom, cmake sbom, or direct gen-sbom). +# +# Mode selection: +# CRA_SBOM_MODE=autotools|cmake|embedded +# autotools (default when configure+Makefile exist): runs `make sbom` +# cmake: auto-extracts sources from compile_commands.json and runs gen-sbom directly +# embedded: hashes the wolfTPM core sources + one platform HAL file directly +# (for firmware builds that compile wolfTPM in, with no .so to hash) +# +# Required variables: +# WOLFSSL_DIR=path/to/wolfssl (source tree root; provides gen-sbom) +# WOLFTPM_DIR=path/to/wolftpm (source tree root) +# +# Mode-specific variables: +# WOLFTPM_BUILD_DIR=path/to/build (cmake mode: path to cmake build directory; +# triggers compile_commands.json auto-extraction) +# CRA_TPM_HAL=st|espressif|microchip|atmel|zephyr|xilinx|uboot|barebox|qnx|mmio|infineon +# (embedded mode: selects the single platform HAL +# file hal/tpm_io_${CRA_TPM_HAL}.c. Required for a +# complete embedded SBOM; if unset, no HAL file is +# hashed and a warning is emitted.) +# CRA_SBOM_SRCS_FILE=path/to/srcs.txt (embedded mode: explicit source list, one .c path +# per line; takes priority over all auto-detection. +# The caller owns correctness of this list.) +# CRA_SBOM_KEIL_PROJECT= (embedded mode: auto-extract srcs from a Keil .uvprojx) +# CRA_SBOM_IAR_PROJECT= (embedded mode: auto-extract srcs from an IAR .ewp) +# CRA_SBOM_MAKEFILE_DIR= (embedded mode: auto-extract srcs via `make -n`) +# CRA_SBOM_NO_HASH=true (embedded mode: emit SBOM without a real artifact +# hash, skipping the source list — for NDA customers +# who cannot share source lists; WARNING: not +# suitable for production compliance) +# CRA_TPM_OPTIONS_H=path/to/options.h (embedded/cmake mode: flat #define build-config header +# for feature enumeration. Embedded mode defaults to +# $WOLFTPM_DIR/wolftpm/options.h; cmake mode prefers the +# cmake-generated $WOLFTPM_BUILD_DIR/wolftpm/options.h, +# then falls back to the source-tree copy.) +# +# Optional variables: +# CRA_LICENSE_OVERRIDE= (e.g. LicenseRef-wolfTPM-Commercial) +# CRA_LICENSE_TEXT= (required when CRA_LICENSE_OVERRIDE is a +# LicenseRef-* id: plain-text licence embedded +# in the SBOM; gen-sbom / make sbom hard-fail +# without it.) +set -eu + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +# shellcheck source=_cra-sbom-extract.sh disable=SC1091 +# shellcheck disable=SC1091 # sourced helper, resolved at runtime +. "$SCRIPT_DIR/_cra-sbom-extract.sh" +KIT_DIR=$(dirname "$SCRIPT_DIR") +# shellcheck disable=SC2015 # `|| true` is a deliberate set -e guard, not if-then-else +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFTPM_DIR=${WOLFTPM_DIR:-$(cd "$KIT_DIR/../../wolftpm" 2>/dev/null && pwd || true)} +# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional +WOLFSSL_DIR=${WOLFSSL_DIR:-$(cd "$KIT_DIR/../../wolfssl" 2>/dev/null && pwd || true)} +OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolftpm-component"} + +if [ -z "${WOLFTPM_DIR:-}" ] || [ ! -d "$WOLFTPM_DIR" ]; then + echo "ERROR: wolfTPM source not found." >&2 + echo " Set WOLFTPM_DIR to your wolftpm checkout." >&2 + exit 1 +fi + +if [ -z "${WOLFSSL_DIR:-}" ] || [ ! -d "$WOLFSSL_DIR" ]; then + echo "ERROR: wolfSSL source not found (needed for gen-sbom)." >&2 + echo " Set WOLFSSL_DIR to your wolfssl checkout." >&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 + +VERSION=$(sed -n \ + 's/.*LIBWOLFTPM_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$WOLFTPM_DIR/wolftpm/version.h" 2>/dev/null || true) +if [ -z "$VERSION" ]; then + echo "ERROR: could not extract version from $WOLFTPM_DIR/wolftpm/version.h" >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" +CDX_OUT="$OUT_DIR/wolftpm-${VERSION}.cdx.json" +SPDX_OUT="$OUT_DIR/wolftpm-${VERSION}.spdx.json" + +echo "wolfTPM tree: $WOLFTPM_DIR" +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 requires the actual licence text to be embedded +# in the SBOM (SPDX 2.3 §10.1). Catch the omission early. +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/wolftpm-commercial-license.txt" >&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 "$WOLFTPM_DIR"` subshell, where a relative path would +# otherwise resolve against the wolfTPM 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 + +# Accumulators for temp files; cleaned up on exit. The shared extraction library +# appends to _cra_auto_tempfiles, so trap both. +_auto_tempfiles="" +_cra_auto_tempfiles="" +trap 'rm -f ${_auto_tempfiles:-} ${_cra_auto_tempfiles:-}' EXIT + +_auto_extract_srcs() { + # Extract wolftpm sources from compile_commands.json (CMake build). + if [ -n "${WOLFTPM_BUILD_DIR:-}" ] && [ -f "$WOLFTPM_BUILD_DIR/compile_commands.json" ]; then + _ccdb="$WOLFTPM_BUILD_DIR/compile_commands.json" + if ! command -v jq >/dev/null 2>&1; then + echo "ERROR: jq is required to auto-extract sources from compile_commands.json." >&2 + echo " Install jq, or set CRA_SBOM_SRCS_FILE manually." >&2 + exit 1 + fi + _auto=$(mktemp "${TMPDIR:-/tmp}/wolftpm-auto-srcs.XXXXXX") + _auto_tempfiles="${_auto_tempfiles:-} $_auto" + jq -r '.[].file' "$_ccdb" \ + | grep "^${WOLFTPM_DIR}/" \ + | grep -E '/src/[^/]+\.c$' \ + | sort -u > "$_auto" + if [ -s "$_auto" ]; then + _n=$(wc -l < "$_auto" | tr -d ' ') + echo " Auto-extracted $_n wolftpm sources from compile_commands.json" + CRA_SBOM_SRCS_FILE="$_auto" + return 0 + fi + echo " WARNING: compile_commands.json found but yielded no wolftpm sources." >&2 + fi +} + +_run_autotools() { + echo "==> Autotools path: make sbom" + _tree_ver=$(sed -n \ + 's/.*LIBWOLFTPM_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$WOLFTPM_DIR/wolftpm/version.h" 2>/dev/null || true) + if [ -n "$_tree_ver" ] && [ "$_tree_ver" != "$VERSION" ]; then + echo "ERROR: wolfTPM tree is version $_tree_ver but detected version is $VERSION." >&2 + exit 1 + fi + (cd "$WOLFTPM_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 WOLFSSL_DIR="$WOLFSSL_DIR" \ + SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" \ + SBOM_LICENSE_TEXT="$CRA_LICENSE_TEXT" + else + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" \ + SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" + fi + else + make sbom WOLFSSL_DIR="$WOLFSSL_DIR" + fi + cp -f "wolftpm-${VERSION}.cdx.json" "$CDX_OUT" + cp -f "wolftpm-${VERSION}.spdx.json" "$SPDX_OUT" + if [ -f "wolftpm-${VERSION}.spdx" ]; then + cp -f "wolftpm-${VERSION}.spdx" "$OUT_DIR/" + fi + }) +} + +_run_cmake() { + echo "==> cmake path: gen-sbom with compile_commands.json source extraction" + if [ -z "${WOLFTPM_BUILD_DIR:-}" ]; then + echo "ERROR: WOLFTPM_BUILD_DIR is not set." >&2 + echo " Set it to your cmake out-of-source build directory." >&2 + echo " Example: cmake -B build && WOLFTPM_BUILD_DIR=\$PWD/build $0" >&2 + exit 1 + fi + if [ ! -d "$WOLFTPM_BUILD_DIR" ]; then + echo "ERROR: WOLFTPM_BUILD_DIR=$WOLFTPM_BUILD_DIR is not a directory." >&2 + exit 1 + fi + + CRA_SBOM_SRCS_FILE="" + _auto_extract_srcs + + if [ -z "${CRA_SBOM_SRCS_FILE:-}" ]; then + echo "ERROR: could not extract wolftpm sources from compile_commands.json." >&2 + echo " Reconfigure cmake with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON, or" >&2 + echo " switch to autotools mode (CRA_SBOM_MODE=autotools)." >&2 + exit 1 + fi + + PYTHON3=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true) + if [ -z "$PYTHON3" ]; then + echo "ERROR: python3 not found in PATH." >&2 + exit 1 + fi + + # gen-sbom requires exactly one of --options-h / --user-settings to enumerate + # enabled features. Prefer the cmake-generated header (reflects the actual + # build config) over the source-tree template; CRA_TPM_OPTIONS_H overrides both. + _options_h="${CRA_TPM_OPTIONS_H:-}" + if [ -z "$_options_h" ] && [ -n "${WOLFTPM_BUILD_DIR:-}" ] && \ + [ -f "$WOLFTPM_BUILD_DIR/wolftpm/options.h" ]; then + _options_h="$WOLFTPM_BUILD_DIR/wolftpm/options.h" + fi + if [ -z "$_options_h" ] && [ -f "$WOLFTPM_DIR/wolftpm/options.h" ]; then + _options_h="$WOLFTPM_DIR/wolftpm/options.h" + fi + if [ -z "$_options_h" ]; then + echo "ERROR: no wolftpm/options.h found." >&2 + echo " Run cmake to generate it, or set CRA_TPM_OPTIONS_H." >&2 + exit 1 + fi + + set -- \ + --name wolftpm \ + --version "$VERSION" \ + --supplier "wolfSSL Inc." \ + --license-file "$WOLFTPM_DIR/LICENSE" \ + --options-h "$_options_h" \ + --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 + set -- "$@" --srcs-file "$CRA_SBOM_SRCS_FILE" + "$PYTHON3" "$GEN" "$@" +} + +_run_embedded() { + echo "==> Embedded path: hash wolfTPM core sources + one platform HAL" + + GEN_PY=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true) + if [ -z "$GEN_PY" ]; then + echo "ERROR: python3 not found in PATH (needed to run gen-sbom)." >&2 + exit 1 + fi + + if [ ! -d "$WOLFTPM_DIR/src" ]; then + echo "ERROR: $WOLFTPM_DIR/src not found; not a wolfTPM source tree?" >&2 + exit 1 + fi + + # gen-sbom needs a build-config header to enumerate enabled features for the + # SBOM (it requires exactly one of --options-h / --user-settings). For wolfTPM + # the committed wolftpm/options.h is a flat #define file in the same shape the + # autotools `make sbom` path feeds via --options-h, so reuse it; callers whose + # firmware uses a different config can point CRA_TPM_OPTIONS_H at their header. + OPTIONS_H=${CRA_TPM_OPTIONS_H:-"$WOLFTPM_DIR/wolftpm/options.h"} + if [ ! -f "$OPTIONS_H" ]; then + echo "ERROR: build-config header not found: $OPTIONS_H" >&2 + echo " Run ./configure in WOLFTPM_DIR to generate wolftpm/options.h," >&2 + echo " or set CRA_TPM_OPTIONS_H to your firmware's flat #define header." >&2 + exit 1 + fi + + # CRA_SBOM_NO_HASH emits a placeholder checksum and skips the source list + # entirely (for NDA customers who cannot share source lists). OPTIONS_H is + # still required so the SBOM records the enabled-feature build properties. + if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then + echo " NOTE: CRA_SBOM_NO_HASH=true: emitting SBOM without artifact hash." + echo " WARNING: not suitable for production CRA compliance." >&2 + set -- \ + --name wolftpm \ + --version "$VERSION" \ + --supplier "wolfSSL Inc." \ + --license-file "$WOLFTPM_DIR/LICENSE" \ + --options-h "$OPTIONS_H" \ + --no-artifact-hash \ + --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 + "$GEN_PY" "$GEN" "$@" || { + echo "ERROR: gen-sbom failed in embedded mode." >&2 + exit 1 + } + for _out in "$CDX_OUT" "$SPDX_OUT"; do + if [ ! -s "$_out" ]; then + echo "ERROR: expected output $_out is missing or empty." >&2 + exit 1 + fi + done + return 0 + fi + + # Source list, one .c path per line. + _srcs=$(mktemp "${TMPDIR:-/tmp}/wolftpm-embedded-srcs.XXXXXX") || { + echo "ERROR: mktemp failed for the embedded source list." >&2 + exit 1 + } + _auto_tempfiles="${_auto_tempfiles:-} $_srcs" + + # Resolve the source list. The shared library handles CRA_SBOM_SRCS_FILE, + # Keil/IAR/Makefile, and compile_commands.json extraction; it returns: + # 0 = a build-system method produced the list (trust it verbatim) + # 2 = no method active (fall back to the default glob + HAL selection) + # 1 = a method was selected but failed (library already explained why) + # `|| _cra_rc=$?` keeps `set -e` from aborting on the non-zero returns. + _cra_rc=0 + _cra_extract_srcs "$WOLFTPM_DIR" "wolftpm" "$_srcs" || _cra_rc=$? + + if [ "$_cra_rc" -eq 0 ]; then + # A build-system method (Keil/IAR/Makefile/compile_commands) produced the + # list. Trust it: the build system already selected the correct single HAL + # and excluded the host-only transports (tpm2_linux.c, tpm2_winapi.c, + # tpm2_swtpm.c). Do NOT apply CRA_TPM_HAL on top — that would double-count + # the HAL or, if CRA_TPM_HAL disagrees with the build, conflict with it. + _n=$(wc -l < "$_srcs" | tr -d ' ') + echo "NOTE: hashed $_n source file(s) (from build system)" + elif [ "$_cra_rc" -eq 2 ]; then + # No extraction method active: build the default source list ourselves, + # selecting the single platform HAL via CRA_TPM_HAL. + # + # Core sources: every src/tpm2*.c EXCEPT the host-only transports below. + # The excluded files (Linux /dev/tpm0, Windows TBS, swtpm simulator) target + # a full OS and will not compile or link on a bare-metal/RTOS firmware build, + # so including them would misrepresent what is actually in the firmware. + for _f in "$WOLFTPM_DIR"/src/tpm2*.c; do + [ -f "$_f" ] || continue + case "$(basename "$_f")" in + tpm2_linux.c|tpm2_winapi.c|tpm2_swtpm.c) continue ;; + esac + echo "$_f" >> "$_srcs" || { + echo "ERROR: failed writing core source to list." >&2 + exit 1 + } + done + + # Dispatcher: always part of the HAL layer. + if [ -f "$WOLFTPM_DIR/hal/tpm_io.c" ]; then + echo "$WOLFTPM_DIR/hal/tpm_io.c" >> "$_srcs" || { + echo "ERROR: failed writing dispatcher to list." >&2 + exit 1 + } + fi + + # Platform HAL: exactly one tpm_io_.c belongs in a given firmware. + # Which one is the caller's responsibility — only they know the target board. + # Picking the wrong HAL (or all of them) would produce an SBOM that does not + # match the shipped firmware, so we hash exactly the one named by CRA_TPM_HAL + # and refuse to guess: an unset CRA_TPM_HAL yields a warning and no HAL file. + if [ -n "${CRA_TPM_HAL:-}" ]; then + _hal="$WOLFTPM_DIR/hal/tpm_io_${CRA_TPM_HAL}.c" + if [ ! -f "$_hal" ]; then + echo "ERROR: CRA_TPM_HAL=$CRA_TPM_HAL but $_hal does not exist." >&2 + echo " Available HALs:" >&2 + for _h in "$WOLFTPM_DIR"/hal/tpm_io_*.c; do + [ -f "$_h" ] || continue + _b=$(basename "$_h"); _b=${_b#tpm_io_}; _b=${_b%.c} + echo " $_b" >&2 + done + exit 1 + fi + echo "$_hal" >> "$_srcs" || { + echo "ERROR: failed writing HAL source to list." >&2 + exit 1 + } + echo " HAL: tpm_io_${CRA_TPM_HAL}.c" + else + echo "WARNING: CRA_TPM_HAL not set; HAL source excluded from SBOM. Set CRA_TPM_HAL=st|espressif|..." >&2 + fi + + if [ ! -s "$_srcs" ]; then + echo "ERROR: no source files collected for the embedded SBOM." >&2 + exit 1 + fi + + _n=$(wc -l < "$_srcs" | tr -d ' ') + echo "NOTE: hashed $_n source file(s)" + else + # Library selected a method but it failed; it already printed the reason. + exit 1 + fi + + # wolfcrypt/wolfssl sources are intentionally NOT hashed here: they are a + # separate component covered by generate-wolfssl-sbom.sh (embedded mode), and + # the wolfSSL SBOM is referenced as a dependency rather than duplicated. + + set -- \ + --name wolftpm \ + --version "$VERSION" \ + --supplier "wolfSSL Inc." \ + --license-file "$WOLFTPM_DIR/LICENSE" \ + --options-h "$OPTIONS_H" \ + --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 + set -- "$@" --srcs-file "$_srcs" + "$GEN_PY" "$GEN" "$@" || { + echo "ERROR: gen-sbom failed in embedded mode." >&2 + exit 1 + } + + for _out in "$CDX_OUT" "$SPDX_OUT"; do + if [ ! -s "$_out" ]; then + echo "ERROR: expected output $_out is missing or empty." >&2 + exit 1 + fi + done +} + +MODE=${CRA_SBOM_MODE:-} +case "$MODE" in + autotools) _run_autotools ;; + cmake) _run_cmake ;; + embedded) _run_embedded ;; + "") + if [ -n "${WOLFTPM_BUILD_DIR:-}" ] && [ -d "${WOLFTPM_BUILD_DIR}" ]; then + MODE=cmake + _run_cmake + elif [ -f "$WOLFTPM_DIR/Makefile" ] && [ -f "$WOLFTPM_DIR/configure" ]; then + MODE=autotools + _run_autotools + else + echo "ERROR: could not detect build mode." >&2 + echo " Set CRA_SBOM_MODE=autotools or cmake, and ensure the build" >&2 + echo " directory exists (WOLFTPM_BUILD_DIR) or configure has been run" >&2 + echo " in WOLFTPM_DIR." >&2 + exit 1 + fi + ;; + *) + echo "ERROR: CRA_SBOM_MODE must be 'autotools', 'cmake', or 'embedded', not '$MODE'" >&2 + exit 1 + ;; +esac + +# ---- Post-process: PURL canonicalization ---- +# gen-sbom emits pkg:generic/wolftpm@X by default; rewrite to the canonical +# pkg:github/wolfSSL/wolfTPM@vX form expected in the auditor packet. +if ! CDX_OUT="$CDX_OUT" SPDX_OUT="$SPDX_OUT" \ + python3 <<'PY' +import json, os, pathlib + +cdx = pathlib.Path(os.environ["CDX_OUT"]) +spdx = pathlib.Path(os.environ["SPDX_OUT"]) + +GENERIC = "pkg:generic/wolftpm@" +GITHUB = "pkg:github/wolfSSL/wolfTPM@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", "")) + cdx.write_text(json.dumps(d, indent=2) + "\n") + print(f"Post-processed {cdx.name}") + +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", "")) + spdx.write_text(json.dumps(d, indent=2) + "\n") + print(f"Post-processed {spdx.name}") +PY +then + echo "ERROR: post-process failed (PURL canonicalization incomplete)." >&2 + echo " The emitted SBOM may carry pkg:generic PURLs; not trusting it." >&2 + exit 1 +fi + +echo "Done." diff --git a/cra-kit/user_settings.h b/cra-kit/user_settings.h index 94ebd570..3df3d779 100644 --- a/cra-kit/user_settings.h +++ b/cra-kit/user_settings.h @@ -1,4 +1,5 @@ /* Demo user_settings.h for CRA Kit embedded SBOM generation. + * Illustrative only, NOT a security-hardened production configuration. * Production: replace with your project's user_settings.h (or point gen-sbom at it). */ #ifndef CRA_KIT_USER_SETTINGS_H #define CRA_KIT_USER_SETTINGS_H