Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -937,8 +937,9 @@ pico-sdk-info: FORCE
# build CFLAGS, to the vendored wolfGlass driver under tools/sbom/.
#
# wolfcrypt sources are compiled directly into the wolfBoot image. They stay
# in the source-set hash, and are also declared as a wolfcrypt component so a
# CPE-driven scan can match the registered NVD product wolfssl:wolfcrypt.
# in the source-set hash, and are also declared as a nested wolfcrypt
# component (PURL / provenance). Matching uses the wolfssl CPE. Do not emit
# a wolfcrypt CPE: NVD files crypto CVEs against wolfssl, not wolfcrypt.
#
# Optional make variables:
# HOSTCC Host C compiler for macro capture (default: cc)
Expand Down Expand Up @@ -983,8 +984,8 @@ 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 sbom-check-sources
# Coat: wolfssl (TLS/library CPE) + wolfcrypt (crypto CPE). Sources remain in
# the merkle hash; the components give scanners resolvable identifiers.
# Coat: wolfssl (matching CPE) + nested wolfcrypt (PURL / provenance).
# Sources remain in the merkle hash; the wolfssl CPE is what NVD matches.
Comment on lines +987 to +988
SBOM_DEP_WOLFSSL?=yes
SBOM_DEP_WOLFCRYPT?=yes
SBOM_WOLFSSL_VERSION?=$(shell sed -n \
Expand Down
18 changes: 10 additions & 8 deletions docs/SBOM.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,17 @@ inside the `wolfssl` component that represents the release they ship in
submodule's version). CycloneDX expresses that as a sub-component plus a
`wolfssl → wolfcrypt` dependency edge; SPDX as a `CONTAINS` relationship.

That coat carries machine-resolvable identifiers for both scanner families:
That coat carries machine-resolvable identifiers:

* `cpe:2.3:a:wolfssl:wolfssl:<version>:*:*:*:*:*:*:*` — NVD product for the
wolfSSL library, and the identifier that actually matches: wolfCrypt
advisories are filed against this product, not against the wolfcrypt one.
* `cpe:2.3:a:wolfssl:wolfcrypt:<version>:*:*:*:*:*:*:*` — NVD product for
wolfCrypt. Registered, but no CVE is mapped to it today, so it documents
provenance rather than driving matches.
wolfSSL library, and the identifier that matches: wolfCrypt advisories
are filed against this product, not against wolfcrypt.
* `pkg:github/wolfssl/wolfssl@v<version>-stable` — resolvable PURL for the
wolfssl release (lowercase per purl-spec; `-stable` is the real tag).
* `pkg:github/wolfssl/wolfssl@v<version>-stable#wolfcrypt` — nested
wolfcrypt provenance (supplier, version, PURL). No wolfcrypt CPE: that
product has no mapped CVEs today, and a second CPE on the same sources
is a future double-match risk.
* wolfBoot itself: `cpe:2.3:a:wolfssl:wolfboot:<version>:*:*:*:*:*:*:*` —
registered in the NVD Official CPE Dictionary (published 2026-08-10).

Expand All @@ -163,8 +164,9 @@ wolfssl:sbom:wolfssl-subset-basis=captured

The `wolfssl` component stays in the document regardless. Dropping it would
read as more precise and would take the scan from every wolfSSL advisory to
none. Narrow the TLS-only CVEs with a VEX statement instead, which is the
mechanism designed to say "present but not exploitable here".
none. A crypto-only image versus TLS firmware is a disposition you record
on your product. A VEX feed that states `not_affected` / `code_not_present`
for TLS-only CVEs is separate work.

### How the configuration is captured

Expand Down
2 changes: 1 addition & 1 deletion tools/sbom/.wolfglass-rev
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b44ae8d1d578ccf0b4269ffb395157cfb3fab0dc
48796eab98b7f763f6a083a848ca85c4bf637e95
3 changes: 2 additions & 1 deletion tools/sbom/build/sbom.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
# INCLUDE_DIRS <dir>... Include directories for that capture (typically the
# directory holding user_settings.h).
# DEP_WOLFSSL yes|no Record wolfSSL as a dependency component.
# DEP_WOLFCRYPT yes|no Record wolfCrypt as a component (registered NVD CPE).
# DEP_WOLFCRYPT yes|no Record wolfCrypt as a nested component (PURL;
# matching uses the wolfssl CPE).
# DEP_OPENSSL yes|no Record OpenSSL as a dependency component.
# CRYPTO_ONLY auto|yes|no Whether only the wolfCrypt subset of wolfSSL is
# compiled in (default auto: read from the capture).
Expand Down
3 changes: 2 additions & 1 deletion tools/sbom/build/sbom.mk
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
# GEN_SBOM Legacy alias for SBOM_GEN.
# SBOM_NO_ARTIFACT_HASH = 1 As-built FIPS/kernel: do not re-hash.
# SBOM_DEP_WOLFSSL yes/no - record wolfSSL as a dependency.
# SBOM_DEP_WOLFCRYPT yes/no - record wolfCrypt as a component (NVD CPE).
# SBOM_DEP_WOLFCRYPT yes/no - record wolfCrypt as a nested component
# (PURL; matching uses the wolfssl CPE).
# SBOM_DEP_OPENSSL yes/no - record OpenSSL as a dependency.
# SBOM_CRYPTO_ONLY auto/yes/no - whether only the wolfCrypt subset of
# the wolfSSL release is compiled in. Default auto:
Expand Down
85 changes: 48 additions & 37 deletions tools/sbom/gen-sbom
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ from datetime import datetime, timezone
# shapes because dropping --dep-liboqs did not bump it, leaving vendored
# copies indistinguishable by the only identifier the SBOM records.
#
# 1.9 Nested wolfcrypt keeps name, version, supplier and PURL, but no
# longer emits cpe:2.3:a:wolfssl:wolfcrypt. NVD files crypto CVEs
# against wolfssl, not wolfcrypt; a second CPE on the same sources
# is a future double-match risk. Matching stays on the wolfssl CPE.
# 1.8 CPE and PURL identifiers drop `+` build metadata (OpenSSL
# BUILD_METADATA, PEP 440 local versions). A raw `+` is not legal
# in CPE 2.3 and does not match an upstream git tag. version /
Expand Down Expand Up @@ -56,7 +60,7 @@ from datetime import datetime, timezone
# -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.8'
GEN_SBOM_VERSION = '1.9'

# Placeholder recorded in the component checksum fields when the operator
# passes --no-artifact-hash: a build (ROM image, HSM firmware, binary-only
Expand Down Expand Up @@ -346,14 +350,16 @@ def build_timestamp():
# / Dependency-Track resolve CVEs against the right package). Algorithm
# enablement is captured separately via build_props (HAVE_FALCON, ...).
#
# Every entry carries both machine-resolvable identifiers, because the two
# scanner families do not agree on one. PURL serves the ecosystem scanners
# (OSV, GHSA, Trivy, Dependency-Track); CPE serves NVD, which is what a CRA /
# IEC 62443 vulnerability-monitoring process keys on. A dependency with only
# a PURL is invisible to a CPE-driven scan, so wolfSSL advisories never reach
# the integrator of a product that embeds wolfSSL. Each `cpe` value must be
# the vendor:product pair NVD actually registers for that dependency; never
# synthesize one.
# Every linked-library entry carries both machine-resolvable identifiers,
# because the two scanner families do not agree on one. PURL serves the
# ecosystem scanners (OSV, GHSA, Trivy, Dependency-Track); CPE serves NVD,
# which is what a CRA / IEC 62443 vulnerability-monitoring process keys on.
# Nested wolfcrypt is the exception: it is provenance (PURL only). NVD files
# crypto CVEs against the parent wolfssl CPE, and a second CPE on the same
# sources is a future double-match risk. A linked dependency with only a
# PURL is still invisible to a CPE-driven scan, so wolfssl itself must keep
# its CPE. Each `cpe` value must be the vendor:product pair NVD actually
# registers; never synthesize one. Omit `cpe` from DEP_META to skip it.
DEP_META = {
# wolfssl itself, declared as a dependency by downstream wolfSSL-stack
# products (wolfSSH, wolfMQTT, wolfTPM, ...) that link libwolfssl. Only
Expand All @@ -376,11 +382,12 @@ DEP_META = {
# The CPE NVD registers for the wolfSSL library.
'cpe': lambda v: cpe23_uri('wolfssl', 'wolfssl', v),
},
# wolfCrypt is a separate NVD product (cpe:2.3:a:wolfssl:wolfcrypt).
# Embedders such as wolfBoot compile wolfcrypt sources into the image;
# wolfSSL itself co-ships wolfCrypt. Emitting it as a component lets a
# CPE-driven scan match wolfCrypt advisories, which NVD indexes under
# wolfcrypt rather than only under wolfssl.
# wolfCrypt is co-shipped in the wolfSSL tree. Embedders compile those
# sources into the image. Record it as a nested component so the SBOM
# shows provenance (supplier, version, PURL). Do not emit a wolfcrypt
# CPE: NVD files crypto CVEs against cpe:2.3:a:wolfssl:wolfssl, not
# wolfcrypt (zero CVEs on that product today). A second CPE on the same
# sources is a future double-match risk once a CNA starts filing there.
Comment on lines +385 to +390
'wolfcrypt': {
'name': 'wolfcrypt',
'supplier': 'wolfSSL Inc.',
Expand All @@ -392,10 +399,9 @@ DEP_META = {
'pkgconfig': None,
# wolfcrypt lives in the wolfssl repository. The resolvable PURL is
# the wolfssl release that ships it, with a #wolfcrypt subpath so it
# does not collide with the wolfssl component's own PURL. NVD
# matching keys on the wolfcrypt CPE below.
# does not collide with the wolfssl component's own PURL. Matching
# keys on the wolfssl CPE of the parent component.
'purl': lambda v: wolfssl_project_purl('wolfssl', v) + '#wolfcrypt',
'cpe': lambda v: cpe23_uri('wolfssl', 'wolfcrypt', v),
},
'libz': {
'name': 'zlib',
Expand Down Expand Up @@ -1074,10 +1080,12 @@ def cdx_dep_component(name, pkg_version, key, dep_version_overrides=None):
# version / versionInfo keep the local string (BUILD_METADATA).
comp['version'] = version
comp['purl'] = meta['purl'](version)
# Both identifiers are version-bearing, so neither can be emitted
# without a resolved version: a CPE with an empty version field
# matches every release of the dependency in an NVD scan.
comp['cpe'] = meta['cpe'](version)
# Emit CPE only when DEP_META provides a constructor. Nested
# wolfcrypt has none; matching rides on the parent wolfssl CPE.
# A versionless CPE matches every release, so never emit one
# without a resolved version.
if meta.get('cpe'):
comp['cpe'] = meta['cpe'](version)
else:
print(f"WARNING: version unknown for {meta['name']}; "
"omitting version, purl and cpe", file=sys.stderr)
Expand All @@ -1101,18 +1109,19 @@ def spdx_dep_package(key, dep_version_overrides=None):
'copyrightText': 'NOASSERTION',
}
if version:
pkg['externalRefs'] = [
{
refs = []
if meta.get('cpe'):
refs.append({
'referenceCategory': 'SECURITY',
'referenceType': 'cpe23Type',
'referenceLocator': meta['cpe'](version),
},
{
'referenceCategory': 'PACKAGE-MANAGER',
'referenceType': 'purl',
'referenceLocator': meta['purl'](version),
},
]
})
refs.append({
'referenceCategory': 'PACKAGE-MANAGER',
'referenceType': 'purl',
'referenceLocator': meta['purl'](version),
})
pkg['externalRefs'] = refs
return spdx_id, pkg


Expand Down Expand Up @@ -1520,7 +1529,7 @@ def _resolve_dep_versions(enabled_deps, overrides):


def _inherit_wolfcrypt_version(enabled_deps, overrides, name, version):
"""Give wolfcrypt a version so its CPE and PURL are not dropped.
"""Give wolfcrypt a version so its PURL is not dropped.

wolfcrypt has no pkg-config file of its own, so `dep_version` can never
resolve it. Prefer an explicit `--dep-version wolfcrypt=`, else the
Expand All @@ -1531,8 +1540,8 @@ def _inherit_wolfcrypt_version(enabled_deps, overrides, name, version):
normally arrives from pkg-config rather than from `--dep-version`, so
inheriting before the resolve step only ever saw an explicit override: a
downstream embedder such as wolfBoot that did not pass
`--dep-version wolfssl=` emitted a wolfcrypt component with no version,
no purl and no cpe, and still exited 0.
`--dep-version wolfssl=` emitted a wolfcrypt component with no version
and no purl, and still exited 0.

Tests the value rather than the key, because `_resolve_dep_versions`
caches a None for every dep pkg-config cannot resolve, so by this point
Expand Down Expand Up @@ -1663,10 +1672,12 @@ def main():
'with --dep-version wolfssl=X.Y.Z on hosts '
'without wolfssl.pc.')
parser.add_argument('--dep-wolfcrypt', default='no',
help='yes to record wolfcrypt as a component with its '
'registered NVD CPE (cpe:2.3:a:wolfssl:wolfcrypt). '
'Use for embedders (wolfBoot) and for wolfSSL\'s '
'own SBOM (containment). Combine with '
help='yes to record wolfcrypt as a nested component '
'(supplier, version, PURL). Matching stays on '
'the wolfssl CPE; this flag does not emit '
'cpe:2.3:a:wolfssl:wolfcrypt. Use for embedders '
'(wolfBoot) and for wolfSSL\'s own SBOM '
'(containment). Combine with '
'--dep-version wolfcrypt=X.Y.Z, or inherit the '
'wolfssl / package version when unset.')
parser.add_argument('--crypto-only', default='auto',
Expand Down
17 changes: 15 additions & 2 deletions tools/sbom/validate_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
* at least one component or component property recorded
* optional --min-properties N on metadata.component.properties
* optional --require-dep-version NAME: a components[] entry with that
name must exist and carry a non-empty version and CPE. Nested
sub-components count, since wolfcrypt ships inside wolfssl.
name must exist and carry a non-empty version. Nested sub-components
count, since wolfcrypt ships inside wolfssl. wolfcrypt is provenance
only (PURL, no CPE); matching rides on the parent wolfssl CPE. Every
other required dep must also carry a CPE.

SPDX (*.spdx.json):
* spdxVersion starts with "SPDX-2"
Expand All @@ -34,6 +36,12 @@
import sys


# Nested wolfcrypt is a provenance component. NVD files crypto CVEs against
# wolfssl, not wolfcrypt; requiring a wolfcrypt CPE would fail a correct
# SBOM and would later double-match. The unique id is the PURL.
_CPE_OPTIONAL_DEPS = frozenset({"wolfcrypt"})


def fail(path, msg):
print(f"FAIL [{path}]: {msg}", file=sys.stderr)
sys.exit(1)
Expand Down Expand Up @@ -81,6 +89,11 @@ def validate_cyclonedx(path, d, name_prefix, min_properties, require_deps):
if not matches[0].get("version"):
fail(path, f"dependency component {dep_name!r} has no version "
f"(pass --dep-version or set WOLFSSL_DIR)")
if dep_name in _CPE_OPTIONAL_DEPS:
if not matches[0].get("purl"):
fail(path, f"dependency component {dep_name!r} has no purl "
f"(provenance id; matching uses the wolfssl CPE)")
continue
if not matches[0].get("cpe"):
fail(path, f"dependency component {dep_name!r} has no cpe "
f"(CPE-driven scanners cannot match it)")
Expand Down
Loading