mirror of https://github.com/wolfSSL/wolfBoot.git
sbom: record the real build configuration
Re-vendor wolfGlass a54cde1 and declare what the capture needs. wolfBoot's wolfCrypt configuration is derived rather than literal: include/user_settings.h turns WOLFBOOT_SIGN_ECC256 into HAVE_ECC, HAVE_ECC256, ECC_TIMING_RESISTANT and the rest. Capturing CFLAGS alone recorded the -D set and none of what it selects, so the SBOM described a signing bootloader with no signature algorithm. Point SBOM_SETTINGS_H at the wolfCrypt settings header, give it SBOM_INCLUDE_DIRS, and make include/target.h a prerequisite since user_settings.h includes it and it carries the flash layout. For an stm32u5 ECC256 build the configuration record now holds 151 defines, including WOLFBOOT_SIGN_ECC256, HAVE_ECC, HAVE_ECC256, WOLFBOOT_HASH_SHA256 and IMAGE_HEADER_SIZE=256, none of which appeared before. BOOTLOADER_PARTITION_SIZE is 65536 rather than the literal `)` that Make's double expansion had left in its place. Declare wolfSSL as a dependency component by default, pinned to the submodule's version. wolfCrypt is compiled into the image rather than linked, so its sources are listed as wolfBoot's own; recording the dependency as well is what lets a scanner match wolfSSL advisories against this firmware. Record the product as firmware rather than a library, in CycloneDX component.type and SPDX primaryPackagePurpose alike. Override the licence to GPL-3.0-or-later: LICENSE is the verbatim GPLv3 and says nothing about how wolfBoot licenses under it, while all 252 GPL-headered sources say "either version 3 ... or (at your option) any later version". The per-HAL SBOM inherits the override so the two documents cannot contradict each other. Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>pull/824/head
parent
c0c8cf2e4f
commit
f87bc0f465
31
Makefile
31
Makefile
|
|
@ -901,8 +901,31 @@ SBOM_ROOT:=$(WOLFBOOT_ROOT)
|
|||
SBOM_NAME:=wolfboot
|
||||
SBOM_SRCS=$(wildcard $(patsubst %.o,%.c,$(OBJS))) $(wildcard $(patsubst %.o,%.S,$(OBJS)))
|
||||
SBOM_CFLAGS=$(CFLAGS)
|
||||
# wolfBoot's wolfCrypt configuration is derived, not literal: include/user_settings.h
|
||||
# turns WOLFBOOT_SIGN_ECC256 into HAVE_ECC, HAVE_ECC256, ECC_TIMING_RESISTANT and
|
||||
# the rest. Capturing CFLAGS alone would record the -D set and none of what it
|
||||
# selects, so the SBOM would report a bootloader with no signature algorithm.
|
||||
SBOM_SETTINGS_H:=$(WOLFBOOT_LIB_WOLFSSL)/wolfssl/wolfcrypt/settings.h
|
||||
SBOM_INCLUDE_DIRS:=$(WOLFBOOT_ROOT)/include $(WOLFBOOT_LIB_WOLFSSL)
|
||||
# user_settings.h includes the generated target.h, so it must exist before the
|
||||
# capture runs. It also carries the flash layout the SBOM records.
|
||||
SBOM_PREREQS:=include/target.h
|
||||
# wolfCrypt sources are compiled into the image rather than linked, so they are
|
||||
# listed as wolfBoot's own sources. Declaring wolfssl as a dependency component
|
||||
# as well is what lets a scanner match wolfSSL advisories against this firmware.
|
||||
SBOM_DEP_WOLFSSL?=yes
|
||||
SBOM_WOLFSSL_VERSION?=$(shell sed -n \
|
||||
's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
|
||||
$(WOLFBOOT_LIB_WOLFSSL)/wolfssl/version.h 2>/dev/null | head -1)
|
||||
SBOM_VERSION=$(WOLFBOOT_VERSION)
|
||||
SBOM_LICENSE_FILE=$(WOLFBOOT_ROOT)/LICENSE
|
||||
# wolfBoot is a bootloader flashed as an image, not a library linked into one.
|
||||
SBOM_COMPONENT_TYPE?=firmware
|
||||
# LICENSE is the verbatim GPLv3, which says nothing about how wolfBoot licenses
|
||||
# under it, so inference falls back to GPL-3.0-only and understates the grant.
|
||||
# Every GPL-headered source says "either version 3 ... or (at your option) any
|
||||
# later version".
|
||||
SBOM_LICENSE_OVERRIDE?=GPL-3.0-or-later
|
||||
SBOM_CDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).cdx.json
|
||||
SBOM_SPDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).spdx.json
|
||||
SBOM_GEN?=
|
||||
|
|
@ -916,9 +939,15 @@ include tools/sbom/build/sbom.mk
|
|||
# so the captured macros match the real build. Run once per TARGET.
|
||||
SBOM_HAL_NAME:=wolfboot-hal-$(TARGET)
|
||||
SBOM_HAL_SRCS=$(filter hal/%,$(patsubst ./%,%,$(wildcard $(patsubst %.o,%.c,$(OBJS)) $(patsubst %.o,%.S,$(OBJS)))))
|
||||
SBOM_HAL_CFLAGS:=$(CFLAGS)
|
||||
SBOM_HAL_CFLAGS=$(CFLAGS)
|
||||
SBOM_HAL_SETTINGS_H:=$(SBOM_SETTINGS_H)
|
||||
SBOM_HAL_INCLUDE_DIRS:=$(SBOM_INCLUDE_DIRS)
|
||||
SBOM_HAL_PREREQS:=$(SBOM_PREREQS)
|
||||
SBOM_HAL_VERSION:=$(WOLFBOOT_VERSION)
|
||||
SBOM_HAL_LICENSE_FILE:=$(WOLFBOOT_ROOT)/LICENSE
|
||||
# Same sources, same grant: without this the HAL SBOM would infer
|
||||
# GPL-3.0-only and contradict the bootloader SBOM built from the same tree.
|
||||
SBOM_HAL_LICENSE_OVERRIDE:=$(SBOM_LICENSE_OVERRIDE)
|
||||
SBOM_HAL_CDX_OUT:=wolfboot-hal-$(TARGET)-$(WOLFBOOT_VERSION).cdx.json
|
||||
SBOM_HAL_SPDX_OUT:=wolfboot-hal-$(TARGET)-$(WOLFBOOT_VERSION).spdx.json
|
||||
SBOM_HAL_GEN:=$(SBOM_GEN)
|
||||
|
|
|
|||
27
docs/SBOM.md
27
docs/SBOM.md
|
|
@ -99,7 +99,32 @@ the source set and artifact hash are configuration-specific.
|
|||
Useful overrides: `HOSTCC`, `SBOM_GEN`, `CRA_PYTHON`.
|
||||
|
||||
wolfcrypt sources are compiled directly into the wolfBoot image, so they are
|
||||
listed as wolfBoot's own sources rather than as a separate component.
|
||||
listed as wolfBoot's own sources rather than as a separate component. wolfSSL
|
||||
is *also* recorded as a dependency component (`SBOM_DEP_WOLFSSL`, on by
|
||||
default) with the submodule's version, which is what lets a scanner match
|
||||
wolfSSL advisories against this firmware.
|
||||
|
||||
### How the configuration is captured
|
||||
|
||||
wolfBoot's configuration is derived rather than literal: `include/user_settings.h`
|
||||
turns `WOLFBOOT_SIGN_ECC256` into `HAVE_ECC`, `HAVE_ECC256`,
|
||||
`ECC_TIMING_RESISTANT` and the rest. So the capture preprocesses the real
|
||||
settings header with the real `-D` set, via `SBOM_SETTINGS_H` and
|
||||
`SBOM_INCLUDE_DIRS`:
|
||||
|
||||
```
|
||||
cc -dM -E $(CFLAGS -D and -I tokens) -include wolfssl/wolfcrypt/settings.h
|
||||
```
|
||||
|
||||
Capturing only the `-D` tokens (against an empty translation unit) records
|
||||
nothing that the header derives, and capturing only the header (with no `-D`
|
||||
set) evaluates every `#if` against an empty configuration. Either omission
|
||||
produces a well-formed SBOM describing a build nobody made — a signing
|
||||
bootloader with no signature algorithm, in wolfBoot's case. Both halves are
|
||||
required, and the capture aborts rather than emitting a partial dump.
|
||||
|
||||
`include/target.h` is generated and `#include`d by `user_settings.h`, so the
|
||||
`sbom` target builds it first (`SBOM_PREREQS`).
|
||||
|
||||
## Route 2 — CMake (methods 5–6)
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
9bdf5b7133c603659ea34b9a552c0bf2a962a8c0
|
||||
a54cde180e09b4158634cc3de99e1adef1ffbc9d
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ A product does not copy logic. It describes itself:
|
|||
(`SBOM_VERSION`, or `SBOM_VERSION_FILE` + `SBOM_VERSION_MACRO`), then
|
||||
`include tools/sbom/build/sbom.mk`. For a second target, instantiate
|
||||
`$(eval $(call wolfglass_sbom_rule,<target>,<prefix>))`.
|
||||
If the product's configuration lives in a `user_settings.h`, also set
|
||||
`SBOM_SETTINGS_H` (and `SBOM_INCLUDE_DIRS` if that header needs paths the
|
||||
CFLAGS do not already carry). `SBOM_CFLAGS` alone records the literal `-D`
|
||||
set and nothing it derives, which for a gated header means the SBOM
|
||||
describes a configuration nobody built.
|
||||
- CMake: `include(tools/sbom/build/sbom.cmake)` and call `wolfglass_add_sbom()`
|
||||
with `NAME`, `VERSION_FILE`, `VERSION_MACRO`, `TARGETS`, `DEFS`, `LICENSE`.
|
||||
`SBOM_GEN` is the canonical generator override; `GEN_SBOM` remains a legacy
|
||||
|
|
|
|||
|
|
@ -15,16 +15,25 @@
|
|||
#
|
||||
# Config - set one:
|
||||
# SBOM_CFLAGS Build CFLAGS whose -D tokens describe the config.
|
||||
# NOTE: the driver keeps ONLY -D tokens from SBOM_CFLAGS.
|
||||
# -I / -include / other flags are dropped. Products whose
|
||||
# config comes from -include'ing a settings header (e.g.
|
||||
# wolfHSM's wh_settings.h) MUST capture with
|
||||
# `$(HOSTCC) -dM -E ... -include ...` themselves and pass
|
||||
# the dump via SBOM_OPTIONS_H, not SBOM_CFLAGS.
|
||||
# The driver keeps the -D tokens and forwards -I/-isystem;
|
||||
# all other flags are dropped.
|
||||
# SBOM_OPTIONS_H A pre-expanded flat #define header (verbatim, scrubbed).
|
||||
# SBOM_USER_SETTINGS A user_settings.h.
|
||||
# SBOM_SOURCE_ONLY = 1 Source-inventory SBOM with no build-config macros.
|
||||
#
|
||||
# Config, strongly recommended alongside SBOM_CFLAGS:
|
||||
# SBOM_SETTINGS_H Settings header -include'd during the capture, so the
|
||||
# macros it derives from the -D set are recorded too.
|
||||
# Any product whose configuration lives in a
|
||||
# user_settings.h MUST set this. Without it the capture
|
||||
# reads an empty translation unit and records only the
|
||||
# literal -D list, which for a gated header (wolfBoot's
|
||||
# user_settings.h derives HAVE_ECC from
|
||||
# WOLFBOOT_SIGN_ECC256) silently omits the entire
|
||||
# derived configuration.
|
||||
# SBOM_INCLUDE_DIRS Extra -I paths for the capture, space-separated, so
|
||||
# SBOM_SETTINGS_H can resolve the headers it pulls in.
|
||||
#
|
||||
# Version - set one:
|
||||
# SBOM_VERSION Literal version string, OR
|
||||
# SBOM_VERSION_FILE + Header to read and the macro to read from it, e.g.:
|
||||
|
|
@ -32,8 +41,26 @@
|
|||
# SBOM_VERSION_MACRO = LIBWOLFBOOT_VERSION_STRING
|
||||
#
|
||||
# Optional (defaults shown):
|
||||
# SBOM_PREREQS Targets to build before the SBOM runs. Use this for
|
||||
# generated headers the capture needs: wolfBoot's
|
||||
# include/target.h carries the flash layout and is
|
||||
# #include'd by its user_settings.h, so the capture
|
||||
# cannot preprocess without it.
|
||||
# SBOM_ROOT Product root. Default: current directory.
|
||||
# SBOM_LICENSE_FILE License file. Default: $(SBOM_ROOT)/LICENSE.
|
||||
# SBOM_COMPONENT_TYPE What kind of artifact this is: CycloneDX
|
||||
# component.type, mirrored to SPDX
|
||||
# primaryPackagePurpose. Default: library. A
|
||||
# bootloader or a flashed image should say firmware.
|
||||
# SBOM_LICENSE_OVERRIDE SPDX expression recorded instead of the one inferred
|
||||
# from SBOM_LICENSE_FILE. Needed whenever that file is
|
||||
# the *full* licence text rather than a licensing
|
||||
# statement: the GPL text never states whether the
|
||||
# project grants "or any later version", so inference
|
||||
# cannot distinguish GPL-3.0-only from
|
||||
# GPL-3.0-or-later and defaults to -only.
|
||||
# SBOM_LICENSE_TEXT Plain-text licence embedded for a LicenseRef-* used
|
||||
# in SBOM_LICENSE_OVERRIDE (required by SPDX 2.3).
|
||||
# SBOM_GEN Path to gen-sbom. Default: driver auto-discovery.
|
||||
# GEN_SBOM Legacy alias for SBOM_GEN.
|
||||
# SBOM_NO_ARTIFACT_HASH = 1 As-built FIPS/kernel: do not re-hash.
|
||||
|
|
@ -62,6 +89,15 @@
|
|||
# $$name becomes $n + ame (empty single-letter Make var) after that double
|
||||
# expansion; $$$$name survives as $name for the shell.
|
||||
#
|
||||
# That same double expansion is why SBOM_CFLAGS is referenced as
|
||||
# $$($(2)CFLAGS) rather than $($(2)CFLAGS). A single $ interpolates the CFLAGS
|
||||
# *value* into the rule text at eval time, and the recipe expansion then
|
||||
# re-scans it. Any `$` surviving in that value is reinterpreted as a Make
|
||||
# reference: wolfBoot passes -DBOOTLOADER_PARTITION_SIZE=$$(( A - B )), whose
|
||||
# `$((` was read as a variable named `( A - B )`, expanded to nothing, and
|
||||
# left the SBOM recording the literal value `)`. Deferring with $$( ) hands
|
||||
# the arithmetic to the shell, which is what the compile recipes already do.
|
||||
#
|
||||
# To instantiate a second target, set another variable prefix and call:
|
||||
# $(eval $(call wolfglass_sbom_rule,sbom-hal,SBOM_HAL_))
|
||||
# using SBOM_HAL_NAME, SBOM_HAL_SRCS, SBOM_HAL_CFLAGS, and so on.
|
||||
|
|
@ -75,7 +111,7 @@ HOSTCC ?= cc
|
|||
|
||||
define wolfglass_sbom_rule
|
||||
.PHONY: $(1)
|
||||
$(1):
|
||||
$(1): $($(2)PREREQS)
|
||||
@test -n "$($(2)NAME)" || { echo "ERROR: set $(2)NAME"; exit 1; }
|
||||
@test -n "$(strip $($(2)SRCS))$($(2)LIB)" || \
|
||||
{ echo "ERROR: set $(2)SRCS or $(2)LIB"; exit 1; }
|
||||
|
|
@ -111,9 +147,14 @@ $(1):
|
|||
$(if $($(2)LIB),--lib "$($(2)LIB)") \
|
||||
$(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),--no-artifact-hash) \
|
||||
$(if $(filter 1,$($(2)SOURCE_ONLY)),--source-only) \
|
||||
$(if $($(2)CFLAGS),--cflags="$($(2)CFLAGS)") \
|
||||
$(if $($(2)CFLAGS),--cflags="$$($(2)CFLAGS)") \
|
||||
$(if $($(2)SETTINGS_H),--settings-h "$($(2)SETTINGS_H)") \
|
||||
$(foreach d,$($(2)INCLUDE_DIRS),--include-dir "$(d)") \
|
||||
$(if $($(2)OPTIONS_H),--options-h "$($(2)OPTIONS_H)") \
|
||||
$(if $($(2)USER_SETTINGS),--user-settings "$($(2)USER_SETTINGS)") \
|
||||
$(if $($(2)COMPONENT_TYPE),--component-type "$($(2)COMPONENT_TYPE)") \
|
||||
$(if $($(2)LICENSE_OVERRIDE),--license-override '$($(2)LICENSE_OVERRIDE)') \
|
||||
$(if $($(2)LICENSE_TEXT),--license-text "$($(2)LICENSE_TEXT)") \
|
||||
$(if $($(2)VERSION),--version "$($(2)VERSION)") \
|
||||
$(if $($(2)VERSION_FILE),--version-file "$($(2)VERSION_FILE)") \
|
||||
$(if $($(2)VERSION_MACRO),--version-macro "$($(2)VERSION_MACRO)") \
|
||||
|
|
|
|||
|
|
@ -20,8 +20,17 @@ from datetime import datetime, timezone
|
|||
# `metadata.tools.components[].version` and SPDX `creationInfo.creators`
|
||||
# fields. Reproducibility CI keys on byte-equal SBOMs across re-runs,
|
||||
# so this constant must change in lockstep with the output it produces.
|
||||
#
|
||||
# The CLI counts as auditor-visible: '1.2' shipped in two incompatible
|
||||
# shapes because dropping --dep-liboqs did not bump it, leaving vendored
|
||||
# copies indistinguishable by the only identifier the SBOM records.
|
||||
#
|
||||
# 1.3 Valueless '#define X' records an empty value instead of '1'.
|
||||
# Warns when the licence file is the full GPL text, where the
|
||||
# -only/-or-later distinction cannot be inferred.
|
||||
# 1.2 Dropped --dep-liboqs (unversioned; see above).
|
||||
GEN_SBOM_TOOL_NAME = 'wolfssl-sbom-gen'
|
||||
GEN_SBOM_VERSION = '1.2'
|
||||
GEN_SBOM_VERSION = '1.3'
|
||||
|
||||
# Placeholder recorded in the component checksum fields when the operator
|
||||
# passes --no-artifact-hash: a build (ROM image, HSM firmware, binary-only
|
||||
|
|
@ -122,19 +131,6 @@ DEP_META = {
|
|||
'pkgconfig': 'wolfssl',
|
||||
'purl': lambda v: f'pkg:github/wolfSSL/wolfssl@v{v}',
|
||||
},
|
||||
# liboqs is the only PQ external dependency wolfSSL still links against
|
||||
# after upstream PR #10293 collapsed the rest of the PQ surface into
|
||||
# native wolfCrypt. Today, --enable-falcon strictly implies --with-liboqs
|
||||
# (configure.ac enforces both directions), so a build that links liboqs
|
||||
# is precisely a build that exposed Falcon.
|
||||
'liboqs': {
|
||||
'name': 'liboqs',
|
||||
'supplier': 'Open Quantum Safe',
|
||||
'license': 'MIT',
|
||||
'download': 'https://github.com/open-quantum-safe/liboqs',
|
||||
'pkgconfig': 'liboqs',
|
||||
'purl': lambda v: f'pkg:github/open-quantum-safe/liboqs@{v}',
|
||||
},
|
||||
'libz': {
|
||||
'name': 'zlib',
|
||||
'supplier': 'Jean-loup Gailly and Mark Adler',
|
||||
|
|
@ -252,6 +248,22 @@ def cdx_license_block(license_expr, license_text):
|
|||
return [{'expression': license_expr}]
|
||||
|
||||
|
||||
# Section headings that appear only in the verbatim GNU licence text, never
|
||||
# in a short per-project licensing statement such as wolfSSL's LICENSING.
|
||||
_FULL_LICENSE_MARKERS = (
|
||||
'terms and conditions for copying, distribution and modification',
|
||||
'terms and conditions',
|
||||
'how to apply these terms to your new programs',
|
||||
)
|
||||
|
||||
|
||||
def _is_full_license_text(text):
|
||||
"""True when the licence file is the verbatim GNU licence rather than a
|
||||
statement about how the project licenses under it."""
|
||||
low = text.lower()
|
||||
return sum(marker in low for marker in _FULL_LICENSE_MARKERS) >= 2
|
||||
|
||||
|
||||
def detect_license(license_file):
|
||||
"""Parse LICENSING file and return an SPDX license ID.
|
||||
|
||||
|
|
@ -302,6 +314,24 @@ def detect_license(license_file):
|
|||
if re.search(r'or\s+(?:[^,.;\n]*?\s+)?(?:any\s+)?later',
|
||||
excerpt, re.IGNORECASE):
|
||||
return f'GPL-{version}.0-or-later'
|
||||
if _is_full_license_text(text):
|
||||
# The verbatim GPL is the licence itself, not a statement about how
|
||||
# this project licenses under it. Whether the project grants "or any
|
||||
# later version" appears only in the per-file headers, so -only here
|
||||
# is a guess that silently narrows the grant. wolfBoot ships the full
|
||||
# GPLv3 as LICENSE while every source header says "either version 3
|
||||
# ... or (at your option) any later version", i.e. GPL-3.0-or-later.
|
||||
print(
|
||||
f"WARNING: {license_file} is the full GPL text, not a licensing "
|
||||
f"statement.\n"
|
||||
f" It cannot say whether this project grants "
|
||||
f"'or any later version', so\n"
|
||||
f" GPL-{version}.0-only is assumed and may understate the "
|
||||
f"grant. Confirm against\n"
|
||||
f" your source headers and pass --license-override "
|
||||
f"GPL-{version}.0-or-later if so\n"
|
||||
f" (Make: SBOM_LICENSE_OVERRIDE).",
|
||||
file=sys.stderr)
|
||||
return f'GPL-{version}.0-only'
|
||||
|
||||
|
||||
|
|
@ -799,10 +829,26 @@ def spdx_dep_package(key, dep_version_overrides=None):
|
|||
return spdx_id, pkg
|
||||
|
||||
|
||||
# CycloneDX 1.6 component.type -> SPDX 2.3 primaryPackagePurpose, so the two
|
||||
# documents agree on what kind of artifact this is. A bootloader described as
|
||||
# a 'library' misfiles the product for anyone triaging by artifact class, which
|
||||
# under IEC 62443 is the difference between a component and the firmware it
|
||||
# boots. Only the values a wolfSSL-stack product can legitimately be.
|
||||
SPDX_PACKAGE_PURPOSE = {
|
||||
'library': 'LIBRARY',
|
||||
'firmware': 'FIRMWARE',
|
||||
'application': 'APPLICATION',
|
||||
'framework': 'FRAMEWORK',
|
||||
'device': 'DEVICE',
|
||||
'file': 'FILE',
|
||||
}
|
||||
|
||||
|
||||
def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
|
||||
timestamp, year, serial, enabled_deps, build_props,
|
||||
dep_version_overrides=None, hash_kind='library-binary',
|
||||
hash_source='lib', srcs_basenames=None, file_entries=None):
|
||||
hash_source='lib', srcs_basenames=None, file_entries=None,
|
||||
component_type='library'):
|
||||
bom_ref = derived_uuid(name, version, 'package')
|
||||
urls = project_urls(name)
|
||||
|
||||
|
|
@ -813,8 +859,14 @@ def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
|
|||
dep_bom_refs.append(ref)
|
||||
components.append(comp)
|
||||
|
||||
# A valueless `#define X` is recorded with an empty value, not '1'.
|
||||
# Coercing to '1' made the two indistinguishable and produced actively
|
||||
# misleading entries where the macro names a quantity: wolfBoot's
|
||||
# target.h emits `#define WOLFBOOT_LOAD_ADDRESS` with nothing after it
|
||||
# when the target does not set a load address, which an auditor then read
|
||||
# as the address literally being 1.
|
||||
properties = [
|
||||
{'name': f'wolfssl:build:{k}', 'value': v if v else '1'}
|
||||
{'name': f'wolfssl:build:{k}', 'value': v}
|
||||
for k, v in build_props
|
||||
]
|
||||
# Document what the SHA-256 in `hashes` represents, on every entry
|
||||
|
|
@ -845,7 +897,7 @@ def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
|
|||
|
||||
main_component = {
|
||||
'bom-ref': bom_ref,
|
||||
'type': 'library',
|
||||
'type': component_type,
|
||||
'supplier': {'name': supplier},
|
||||
'name': name,
|
||||
'version': version,
|
||||
|
|
@ -916,7 +968,8 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
|
|||
timestamp, year, doc_ns_uuid, enabled_deps, build_props,
|
||||
dep_version_overrides=None, hash_kind='library-binary',
|
||||
hash_source='lib', srcs_basenames=None,
|
||||
document_namespace=None, file_entries=None):
|
||||
document_namespace=None, file_entries=None,
|
||||
component_type='library'):
|
||||
build_defines = ', '.join(k for k, _ in build_props)
|
||||
# Hash-kind / source-set / bomsh-traced-binary information used to
|
||||
# be stuffed into the package `comment` as `key=value` slugs, which
|
||||
|
|
@ -965,6 +1018,8 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
|
|||
'licenseConcluded': license_id,
|
||||
'licenseDeclared': license_id,
|
||||
'copyrightText': f'Copyright (C) 2006-{year} wolfSSL Inc.',
|
||||
'primaryPackagePurpose': SPDX_PACKAGE_PURPOSE.get(
|
||||
component_type, 'LIBRARY'),
|
||||
'comment': f'Build configuration defines: {build_defines}',
|
||||
'annotations': annotations,
|
||||
'externalRefs': [
|
||||
|
|
@ -1078,7 +1133,7 @@ def _resolve_dep_versions(enabled_deps, overrides):
|
|||
value instead of each re-invoking pkg-config. Caching the result
|
||||
(including None) means a later dep_version() lookup short-circuits on the
|
||||
membership check rather than re-shelling to `pkg-config --modversion`, so
|
||||
a default --with-libz --with-liboqs build calls pkg-config once per dep
|
||||
a default --with-libz build calls pkg-config once per dep
|
||||
(not once per dep per output format) and the two documents can never
|
||||
disagree if pkg-config output were ever non-deterministic."""
|
||||
for key in enabled_deps:
|
||||
|
|
@ -1101,6 +1156,12 @@ def main():
|
|||
parser.add_argument('--version', required=True, help='Package version')
|
||||
parser.add_argument('--supplier', default='wolfSSL Inc.',
|
||||
help='Supplier name (default: wolfSSL Inc.)')
|
||||
parser.add_argument('--component-type', default='library',
|
||||
choices=sorted(SPDX_PACKAGE_PURPOSE),
|
||||
help='What kind of artifact this is: CycloneDX '
|
||||
'component.type, mirrored to SPDX '
|
||||
'primaryPackagePurpose. Use firmware for a '
|
||||
'bootloader such as wolfBoot (default: library)')
|
||||
parser.add_argument('--license-file', required=True,
|
||||
help='Path to LICENSING file for SPDX ID detection')
|
||||
parser.add_argument('--license-override', default='',
|
||||
|
|
@ -1185,10 +1246,6 @@ def main():
|
|||
'openssl.pc.')
|
||||
parser.add_argument('--dep-libz', default='no',
|
||||
help='yes if built with --with-libz')
|
||||
parser.add_argument('--dep-liboqs', default='no',
|
||||
help='yes if built with --with-liboqs (the package '
|
||||
'wolfSSL links against; --enable-falcon implies '
|
||||
'this in any legal configuration)')
|
||||
parser.add_argument('--dep-version', action='append', default=[],
|
||||
metavar='KEY=VERSION',
|
||||
help='Override pkg-config version detection for a '
|
||||
|
|
@ -1257,7 +1314,6 @@ def main():
|
|||
('wolfssl', args.dep_wolfssl),
|
||||
('openssl', args.dep_openssl),
|
||||
('libz', args.dep_libz),
|
||||
('liboqs', args.dep_liboqs),
|
||||
]
|
||||
if flag.lower() == 'yes'
|
||||
]
|
||||
|
|
@ -1380,6 +1436,7 @@ def main():
|
|||
hash_kind=hash_kind, hash_source=hash_source,
|
||||
srcs_basenames=srcs_basenames,
|
||||
file_entries=file_entries,
|
||||
component_type=args.component_type,
|
||||
)
|
||||
spdx = generate_spdx(
|
||||
args.name, args.version, args.supplier,
|
||||
|
|
@ -1390,6 +1447,7 @@ def main():
|
|||
srcs_basenames=srcs_basenames,
|
||||
document_namespace=(args.document_namespace or None),
|
||||
file_entries=file_entries,
|
||||
component_type=args.component_type,
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ The driver covers every product tier:
|
|||
|
||||
Config (choose one):
|
||||
--cflags "..." Raw CFLAGS. -D tokens are expanded through the host
|
||||
compiler and scrubbed of absolute paths.
|
||||
compiler and scrubbed of absolute paths. Pair with
|
||||
--settings-h so the capture also sees the header those
|
||||
-D tokens are interpreted by; without it the dump is the
|
||||
-D list alone and every derived macro is missing.
|
||||
--options-h PATH A pre-expanded flat #define header, used verbatim (scrubbed).
|
||||
--user-settings P A user_settings.h. Passed to the generator, which captures it.
|
||||
--source-only No build-config macros (e.g. a Kconfig-driven build).
|
||||
|
|
@ -119,22 +122,81 @@ def gen_sbom_supports(python, gen_sbom, flag):
|
|||
return flag in (res.stdout + res.stderr)
|
||||
|
||||
|
||||
def capture_macros(hostcc, cflags):
|
||||
"""Expand the -D tokens of CFLAGS through the host compiler's -dM -E.
|
||||
def split_cflags(cflags):
|
||||
"""Split a CFLAGS string on whitespace.
|
||||
|
||||
Only tokens that start with ``-D`` are kept. ``-I``, ``-include``, and
|
||||
every other flag are dropped. Products whose configuration is expressed
|
||||
by ``-include``'ing a settings header must capture that header themselves
|
||||
(``hostcc -dM -E ... -include ...``) and pass the dump via ``--options-h``.
|
||||
Deliberately a plain split rather than shlex: --cflags arrives from a Make
|
||||
recipe, so the shell has already removed one layer of quoting. Re-lexing it
|
||||
as shell words would strip a second layer, and shlex's POSIX escape
|
||||
handling would eat the backslashes out of Windows paths --
|
||||
-DPICO_SDK_PATH=C:\\Users\\ci\\sdk becomes C:Userscisdk, which is both
|
||||
wrong and no longer matched by the absolute-path scrub, so a mangled host
|
||||
path would leak into the SBOM.
|
||||
"""
|
||||
defs = [t for t in cflags.split() if t.startswith("-D")]
|
||||
cmd = [hostcc, "-dM", "-E", "-DWOLFSSL_USER_SETTINGS", *defs,
|
||||
"-x", "c", os.devnull]
|
||||
return cflags.split()
|
||||
|
||||
|
||||
def capture_macros(hostcc, cflags, settings_h="", include_dirs=()):
|
||||
"""Expand a build configuration through the host compiler's -dM -E.
|
||||
|
||||
The capture must see BOTH inputs that determine the configuration:
|
||||
|
||||
* the ``-D`` tokens from CFLAGS, which select features
|
||||
(``-DWOLFBOOT_SIGN_ECC256``), and
|
||||
* the settings header those tokens are interpreted by, reached via
|
||||
``-include``.
|
||||
|
||||
Feeding only one of them yields a confident, well-formed, wrong document.
|
||||
With no ``-include``, the compiler reads an empty translation unit and the
|
||||
dump is just the ``-D`` list plus compiler built-ins, so every macro the
|
||||
settings header derives is missing. With no ``-D`` tokens, every ``#if``
|
||||
in that header is evaluated against an empty configuration and the dump
|
||||
describes a product nobody built -- wolfBoot's user_settings.h gates
|
||||
HAVE_ECC on WOLFBOOT_SIGN_ECC256, so a capture missing the latter silently
|
||||
reports a secure bootloader with no signature algorithm.
|
||||
|
||||
``-I`` and ``-isystem`` from CFLAGS are forwarded so the ``-include``
|
||||
header can resolve the product headers it pulls in (user_settings.h,
|
||||
target.h). Every other flag is dropped: they describe code generation,
|
||||
not configuration, and cross-toolchain flags would break the host cc.
|
||||
"""
|
||||
toks = split_cflags(cflags)
|
||||
defs, incs = [], []
|
||||
i = 0
|
||||
while i < len(toks):
|
||||
t = toks[i]
|
||||
if t.startswith("-D"):
|
||||
defs.append(t)
|
||||
elif t in ("-I", "-isystem", "-include") and i + 1 < len(toks):
|
||||
# Separated form: keep -I/-isystem, but never a second -include
|
||||
# (the settings header below is the only one we want).
|
||||
if t != "-include":
|
||||
incs += [t, toks[i + 1]]
|
||||
i += 1
|
||||
elif t.startswith("-I"):
|
||||
incs.append(t)
|
||||
i += 1
|
||||
|
||||
for d in include_dirs:
|
||||
incs += ["-I", d]
|
||||
|
||||
cmd = [hostcc, "-dM", "-E", "-DWOLFSSL_USER_SETTINGS", *incs, *defs]
|
||||
if settings_h:
|
||||
cmd += ["-include", settings_h]
|
||||
cmd += ["-x", "c", os.devnull]
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
except OSError:
|
||||
sys.exit(f"ERROR: '{hostcc} -dM -E' failed; install a host C compiler "
|
||||
f"or set --hostcc.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
detail = (e.stderr or "").strip()
|
||||
hint = ""
|
||||
if settings_h:
|
||||
hint = (f"\n The capture includes '{settings_h}'. Check that "
|
||||
f"--include-dir covers the headers it pulls in.")
|
||||
sys.exit(f"ERROR: '{hostcc} -dM -E' failed while capturing the build "
|
||||
f"configuration.{hint}\n{detail}")
|
||||
return res.stdout
|
||||
|
||||
|
||||
|
|
@ -176,6 +238,16 @@ def main():
|
|||
grp = ap.add_mutually_exclusive_group()
|
||||
grp.add_argument("--cflags", default="",
|
||||
help="Build CFLAGS; -D tokens expanded through the host cc.")
|
||||
ap.add_argument("--settings-h", default="",
|
||||
help="Settings header -include'd during the --cflags "
|
||||
"capture, so macros the header derives from the -D "
|
||||
"set are recorded. Required for any product whose "
|
||||
"configuration lives in a user_settings.h.")
|
||||
ap.add_argument("--include-dir", action="append", default=[],
|
||||
metavar="DIR",
|
||||
help="Extra -I path for the --cflags capture (repeatable). "
|
||||
"-I/-isystem already present in CFLAGS are forwarded "
|
||||
"automatically.")
|
||||
grp.add_argument("--options-h", default="",
|
||||
help="A pre-expanded flat #define header, used verbatim.")
|
||||
grp.add_argument("--user-settings", default="",
|
||||
|
|
@ -189,6 +261,11 @@ def main():
|
|||
ap.add_argument("--version-file", default="")
|
||||
ap.add_argument("--version-macro", default="")
|
||||
ap.add_argument("--supplier", default="wolfSSL Inc.")
|
||||
ap.add_argument("--component-type", default="",
|
||||
help="CycloneDX component.type, mirrored to SPDX "
|
||||
"primaryPackagePurpose. Use firmware for a "
|
||||
"bootloader. Passed through only if the generator "
|
||||
"supports it.")
|
||||
ap.add_argument("--license-file", default="")
|
||||
ap.add_argument("--license-override", default="")
|
||||
ap.add_argument("--license-text", default="")
|
||||
|
|
@ -263,6 +340,12 @@ def main():
|
|||
cmd += ["--no-artifact-hash"]
|
||||
|
||||
# Config.
|
||||
if args.settings_h and not args.cflags:
|
||||
# --settings-h only feeds the --cflags capture. Ignoring it in
|
||||
# silence is how a product ends up believing it recorded a
|
||||
# configuration it never captured.
|
||||
print("WARNING: --settings-h is only used with --cflags; "
|
||||
"ignoring it here.", file=sys.stderr)
|
||||
if args.user_settings:
|
||||
if not os.path.isfile(args.user_settings):
|
||||
sys.exit(f"ERROR: --user-settings '{args.user_settings}' "
|
||||
|
|
@ -277,7 +360,13 @@ def main():
|
|||
with open(args.options_h, encoding="utf-8", errors="replace") as f:
|
||||
defines_text = f.read()
|
||||
elif args.cflags:
|
||||
defines_text = capture_macros(args.hostcc, args.cflags)
|
||||
if args.settings_h and not os.path.isfile(args.settings_h):
|
||||
sys.exit(f"ERROR: --settings-h '{args.settings_h}' "
|
||||
f"does not exist.")
|
||||
defines_text = capture_macros(
|
||||
args.hostcc, args.cflags,
|
||||
settings_h=args.settings_h,
|
||||
include_dirs=args.include_dir)
|
||||
else:
|
||||
sys.exit("ERROR: pass one of --cflags, --options-h, "
|
||||
"--user-settings, or --source-only.")
|
||||
|
|
@ -296,6 +385,10 @@ def main():
|
|||
if args.license_text:
|
||||
cmd += ["--license-text", args.license_text]
|
||||
|
||||
if args.component_type and gen_sbom_supports(
|
||||
args.python, gen_sbom, "--component-type"):
|
||||
cmd += ["--component-type", args.component_type]
|
||||
|
||||
# Dependencies, only if the generator supports them.
|
||||
if args.dep_wolfssl and gen_sbom_supports(args.python, gen_sbom, "--dep-wolfssl"):
|
||||
cmd += ["--dep-wolfssl", args.dep_wolfssl]
|
||||
|
|
|
|||
Loading…
Reference in New Issue