Skip to content

Commit c9c51d3

Browse files
sameehjcursoragent
andcommitted
fix(gen-sbom): drop nested wolfcrypt CPE
NVD files crypto CVEs against wolfssl, not wolfcrypt. Keep the nested component (name, version, PURL) so provenance stays; matching rides on the wolfssl CPE to avoid a future double-match. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b44ae8d commit c9c51d3

6 files changed

Lines changed: 100 additions & 55 deletions

File tree

docs/cpe-requests/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ when the SBOM is otherwise correct.
1717
All products in the table are registered in the NVD Official CPE Dictionary.
1818
`gen-sbom` emits the main-package `cpe` field for each.
1919

20+
The wolfcrypt CPE stays in the dictionary. Nested wolfcrypt components do
21+
**not** emit it: NVD files crypto CVEs against `wolfssl`, not `wolfcrypt`,
22+
and a second CPE on the same sources is a future double-match risk.
23+
Matching rides on `cpe:2.3:a:wolfssl:wolfssl:<version>`. The unique id for
24+
the nested library is the PURL (`pkg:github/wolfssl/wolfssl@v<ver>-stable#wolfcrypt`).
25+
2026
## Pending products (none today)
2127

2228
A product with `status: pending` emits **no `cpe` field**. A scanner cannot

share/build/sbom.cmake

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
# INCLUDE_DIRS <dir>... Include directories for that capture (typically the
3838
# directory holding user_settings.h).
3939
# DEP_WOLFSSL yes|no Record wolfSSL as a dependency component.
40-
# DEP_WOLFCRYPT yes|no Record wolfCrypt as a component (registered NVD CPE).
40+
# DEP_WOLFCRYPT yes|no Record wolfCrypt as a nested component (PURL;
41+
# matching uses the wolfssl CPE).
4142
# DEP_OPENSSL yes|no Record OpenSSL as a dependency component.
4243
# CRYPTO_ONLY auto|yes|no Whether only the wolfCrypt subset of wolfSSL is
4344
# compiled in (default auto: read from the capture).

share/build/sbom.mk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
# GEN_SBOM Legacy alias for SBOM_GEN.
6666
# SBOM_NO_ARTIFACT_HASH = 1 As-built FIPS/kernel: do not re-hash.
6767
# SBOM_DEP_WOLFSSL yes/no - record wolfSSL as a dependency.
68-
# SBOM_DEP_WOLFCRYPT yes/no - record wolfCrypt as a component (NVD CPE).
68+
# SBOM_DEP_WOLFCRYPT yes/no - record wolfCrypt as a nested component
69+
# (PURL; matching uses the wolfssl CPE).
6970
# SBOM_DEP_OPENSSL yes/no - record OpenSSL as a dependency.
7071
# SBOM_CRYPTO_ONLY auto/yes/no - whether only the wolfCrypt subset of
7172
# the wolfSSL release is compiled in. Default auto:

share/gen-sbom

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ from datetime import datetime, timezone
2525
# shapes because dropping --dep-liboqs did not bump it, leaving vendored
2626
# copies indistinguishable by the only identifier the SBOM records.
2727
#
28+
# 1.9 Nested wolfcrypt keeps name, version, supplier and PURL, but no
29+
# longer emits cpe:2.3:a:wolfssl:wolfcrypt. NVD files crypto CVEs
30+
# against wolfssl, not wolfcrypt; a second CPE on the same sources
31+
# is a future double-match risk. Matching stays on the wolfssl CPE.
2832
# 1.8 CPE and PURL identifiers drop `+` build metadata (OpenSSL
2933
# BUILD_METADATA, PEP 440 local versions). A raw `+` is not legal
3034
# in CPE 2.3 and does not match an upstream git tag. version /
@@ -56,7 +60,7 @@ from datetime import datetime, timezone
5660
# -only/-or-later distinction cannot be inferred.
5761
# 1.2 Dropped --dep-liboqs (unversioned; see above).
5862
GEN_SBOM_TOOL_NAME = 'wolfssl-sbom-gen'
59-
GEN_SBOM_VERSION = '1.8'
63+
GEN_SBOM_VERSION = '1.9'
6064

6165
# Placeholder recorded in the component checksum fields when the operator
6266
# passes --no-artifact-hash: a build (ROM image, HSM firmware, binary-only
@@ -376,11 +380,12 @@ DEP_META = {
376380
# The CPE NVD registers for the wolfSSL library.
377381
'cpe': lambda v: cpe23_uri('wolfssl', 'wolfssl', v),
378382
},
379-
# wolfCrypt is a separate NVD product (cpe:2.3:a:wolfssl:wolfcrypt).
380-
# Embedders such as wolfBoot compile wolfcrypt sources into the image;
381-
# wolfSSL itself co-ships wolfCrypt. Emitting it as a component lets a
382-
# CPE-driven scan match wolfCrypt advisories, which NVD indexes under
383-
# wolfcrypt rather than only under wolfssl.
383+
# wolfCrypt is co-shipped in the wolfSSL tree. Embedders compile those
384+
# sources into the image. Record it as a nested component so the SBOM
385+
# shows provenance (supplier, version, PURL). Do not emit a wolfcrypt
386+
# CPE: NVD files crypto CVEs against cpe:2.3:a:wolfssl:wolfssl, not
387+
# wolfcrypt (zero CVEs on that product today). A second CPE on the same
388+
# sources is a future double-match risk once a CNA starts filing there.
384389
'wolfcrypt': {
385390
'name': 'wolfcrypt',
386391
'supplier': 'wolfSSL Inc.',
@@ -392,10 +397,9 @@ DEP_META = {
392397
'pkgconfig': None,
393398
# wolfcrypt lives in the wolfssl repository. The resolvable PURL is
394399
# the wolfssl release that ships it, with a #wolfcrypt subpath so it
395-
# does not collide with the wolfssl component's own PURL. NVD
396-
# matching keys on the wolfcrypt CPE below.
400+
# does not collide with the wolfssl component's own PURL. Matching
401+
# keys on the wolfssl CPE of the parent component.
397402
'purl': lambda v: wolfssl_project_purl('wolfssl', v) + '#wolfcrypt',
398-
'cpe': lambda v: cpe23_uri('wolfssl', 'wolfcrypt', v),
399403
},
400404
'libz': {
401405
'name': 'zlib',
@@ -1074,10 +1078,12 @@ def cdx_dep_component(name, pkg_version, key, dep_version_overrides=None):
10741078
# version / versionInfo keep the local string (BUILD_METADATA).
10751079
comp['version'] = version
10761080
comp['purl'] = meta['purl'](version)
1077-
# Both identifiers are version-bearing, so neither can be emitted
1081+
# Identifiers are version-bearing, so they cannot be emitted
10781082
# without a resolved version: a CPE with an empty version field
10791083
# matches every release of the dependency in an NVD scan.
1080-
comp['cpe'] = meta['cpe'](version)
1084+
# wolfcrypt has no CPE: matching rides on the parent wolfssl CPE.
1085+
if meta.get('cpe'):
1086+
comp['cpe'] = meta['cpe'](version)
10811087
else:
10821088
print(f"WARNING: version unknown for {meta['name']}; "
10831089
"omitting version, purl and cpe", file=sys.stderr)
@@ -1101,18 +1107,19 @@ def spdx_dep_package(key, dep_version_overrides=None):
11011107
'copyrightText': 'NOASSERTION',
11021108
}
11031109
if version:
1104-
pkg['externalRefs'] = [
1105-
{
1110+
refs = []
1111+
if meta.get('cpe'):
1112+
refs.append({
11061113
'referenceCategory': 'SECURITY',
11071114
'referenceType': 'cpe23Type',
11081115
'referenceLocator': meta['cpe'](version),
1109-
},
1110-
{
1111-
'referenceCategory': 'PACKAGE-MANAGER',
1112-
'referenceType': 'purl',
1113-
'referenceLocator': meta['purl'](version),
1114-
},
1115-
]
1116+
})
1117+
refs.append({
1118+
'referenceCategory': 'PACKAGE-MANAGER',
1119+
'referenceType': 'purl',
1120+
'referenceLocator': meta['purl'](version),
1121+
})
1122+
pkg['externalRefs'] = refs
11161123
return spdx_id, pkg
11171124

11181125

@@ -1520,7 +1527,7 @@ def _resolve_dep_versions(enabled_deps, overrides):
15201527

15211528

15221529
def _inherit_wolfcrypt_version(enabled_deps, overrides, name, version):
1523-
"""Give wolfcrypt a version so its CPE and PURL are not dropped.
1530+
"""Give wolfcrypt a version so its PURL is not dropped.
15241531
15251532
wolfcrypt has no pkg-config file of its own, so `dep_version` can never
15261533
resolve it. Prefer an explicit `--dep-version wolfcrypt=`, else the
@@ -1531,8 +1538,8 @@ def _inherit_wolfcrypt_version(enabled_deps, overrides, name, version):
15311538
normally arrives from pkg-config rather than from `--dep-version`, so
15321539
inheriting before the resolve step only ever saw an explicit override: a
15331540
downstream embedder such as wolfBoot that did not pass
1534-
`--dep-version wolfssl=` emitted a wolfcrypt component with no version,
1535-
no purl and no cpe, and still exited 0.
1541+
`--dep-version wolfssl=` emitted a wolfcrypt component with no version
1542+
and no purl, and still exited 0.
15361543
15371544
Tests the value rather than the key, because `_resolve_dep_versions`
15381545
caches a None for every dep pkg-config cannot resolve, so by this point
@@ -1663,10 +1670,12 @@ def main():
16631670
'with --dep-version wolfssl=X.Y.Z on hosts '
16641671
'without wolfssl.pc.')
16651672
parser.add_argument('--dep-wolfcrypt', default='no',
1666-
help='yes to record wolfcrypt as a component with its '
1667-
'registered NVD CPE (cpe:2.3:a:wolfssl:wolfcrypt). '
1668-
'Use for embedders (wolfBoot) and for wolfSSL\'s '
1669-
'own SBOM (containment). Combine with '
1673+
help='yes to record wolfcrypt as a nested component '
1674+
'(supplier, version, PURL). Matching stays on '
1675+
'the wolfssl CPE; this flag does not emit '
1676+
'cpe:2.3:a:wolfssl:wolfcrypt. Use for embedders '
1677+
'(wolfBoot) and for wolfSSL\'s own SBOM '
1678+
'(containment). Combine with '
16701679
'--dep-version wolfcrypt=X.Y.Z, or inherit the '
16711680
'wolfssl / package version when unset.')
16721681
parser.add_argument('--crypto-only', default='auto',

tests/test_gen_sbom.py

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -854,13 +854,14 @@ def test_wolfssl_dep_entry_describes_the_linked_artefact(self):
854854
wolfssl['cpe']('5.7.4'),
855855
'cpe:2.3:a:wolfssl:wolfssl:5.7.4:*:*:*:*:*:*:*')
856856

857-
def test_wolfcrypt_dep_entry_carries_registered_nvd_cpe(self):
857+
def test_wolfcrypt_dep_entry_has_purl_and_no_cpe(self):
858+
# Provenance only: NVD files crypto CVEs against wolfssl, not
859+
# wolfcrypt. A nested CPE would not match today and would
860+
# double-match later.
858861
wc = gs.DEP_META['wolfcrypt']
859862
self.assertEqual(wc['name'], 'wolfcrypt')
860863
self.assertIsNone(wc['pkgconfig'])
861-
self.assertEqual(
862-
wc['cpe']('5.9.1'),
863-
'cpe:2.3:a:wolfssl:wolfcrypt:5.9.1:*:*:*:*:*:*:*')
864+
self.assertIsNone(wc.get('cpe'))
864865
# Resolvable github PURL for the wolfssl release that ships it,
865866
# with a #wolfcrypt subpath so it does not collide with wolfssl.
866867
self.assertEqual(
@@ -907,12 +908,14 @@ def test_dep_constructors_strip_plus_build_metadata(self):
907908
dirty = '1.2.3+local'
908909
for key, meta in gs.DEP_META.items():
909910
with self.subTest(dep=key):
910-
cpe = meta['cpe'](dirty)
911-
self.assertNotIn('+', cpe, cpe)
912-
self.assertIn(':1.2.3:', cpe)
913911
purl = meta['purl'](dirty)
914912
self.assertNotIn('+', purl, purl)
915913
self.assertIn('1.2.3', purl)
914+
if not meta.get('cpe'):
915+
continue
916+
cpe = meta['cpe'](dirty)
917+
self.assertNotIn('+', cpe, cpe)
918+
self.assertIn(':1.2.3:', cpe)
916919

917920
def test_openssl_dep_cpe_and_purl_drop_build_metadata(self):
918921
# wolfProvider patches OpenSSL BUILD_METADATA, so openssl version
@@ -943,16 +946,19 @@ def test_openssl_dep_cpe_and_purl_drop_build_metadata(self):
943946
self.assertEqual(
944947
locators['purl'], 'pkg:github/openssl/openssl@openssl-3.5.4')
945948

946-
def test_every_dep_entry_carries_both_identifiers(self):
947-
# A dep with only one identifier is invisible to half the scanner
948-
# population: PURL serves OSV / Trivy / Dependency-Track, CPE serves
949-
# NVD, which is what a CRA vulnerability-monitoring process keys on.
949+
def test_every_dep_entry_carries_a_purl(self):
950+
# PURL serves OSV / Trivy / Dependency-Track. CPE serves NVD.
951+
# wolfcrypt is the exception: matching rides on the parent
952+
# wolfssl CPE, so a nested wolfcrypt CPE is omitted on purpose.
950953
for key, meta in gs.DEP_META.items():
951954
with self.subTest(dep=key):
952955
purl = meta['purl']('1.2.3')
953-
cpe = meta['cpe']('1.2.3')
954956
self.assertTrue(purl.startswith('pkg:'), purl)
955957
self.assertIn('1.2.3', purl)
958+
if key == 'wolfcrypt':
959+
self.assertIsNone(meta.get('cpe'))
960+
continue
961+
cpe = meta['cpe']('1.2.3')
956962
self.assertTrue(cpe.startswith('cpe:2.3:a:'), cpe)
957963
self.assertEqual(len(cpe.split(':')), 13, cpe)
958964
self.assertIn(':1.2.3:', cpe)
@@ -1955,13 +1961,12 @@ def test_wolfssl_dep_component_carries_nvd_cpe(self):
19551961
self.assertEqual(
19561962
comp['purl'], 'pkg:github/wolfssl/wolfssl@v5.9.1-stable')
19571963

1958-
def test_wolfcrypt_dep_component_carries_nvd_cpe(self):
1964+
def test_wolfcrypt_dep_component_has_purl_and_no_cpe(self):
19591965
_, comp = gs.cdx_dep_component(
19601966
'wolfboot', '2.9.0', 'wolfcrypt', {'wolfcrypt': '5.9.1'})
19611967
self.assertEqual(comp['name'], 'wolfcrypt')
19621968
self.assertEqual(comp['version'], '5.9.1')
1963-
self.assertEqual(
1964-
comp['cpe'], 'cpe:2.3:a:wolfssl:wolfcrypt:5.9.1:*:*:*:*:*:*:*')
1969+
self.assertNotIn('cpe', comp)
19651970
self.assertEqual(
19661971
comp['purl'],
19671972
'pkg:github/wolfssl/wolfssl@v5.9.1-stable#wolfcrypt')
@@ -2067,6 +2072,12 @@ def test_cpe_externalref_present_when_version_known(self):
20672072
cpe_refs[0]['referenceLocator'],
20682073
'cpe:2.3:a:wolfssl:wolfssl:5.9.1:*:*:*:*:*:*:*')
20692074

2075+
def test_wolfcrypt_omits_cpe_externalref(self):
2076+
_, pkg = gs.spdx_dep_package('wolfcrypt', {'wolfcrypt': '5.9.1'})
2077+
types = [r['referenceType'] for r in pkg.get('externalRefs', [])]
2078+
self.assertNotIn('cpe23Type', types)
2079+
self.assertIn('purl', types)
2080+
20702081

20712082
class TestGenerateCdx(unittest.TestCase):
20722083
"""gen-sbom:624 generate_cdx assembles the full CycloneDX 1.6 doc."""
@@ -2996,9 +3007,11 @@ def test_cdx_nests_wolfcrypt_inside_wolfssl(self):
29963007

29973008
nested = {c['name']: c for c in top['wolfssl']['components']}
29983009
self.assertEqual(set(nested), {'wolfcrypt'})
3010+
self.assertEqual(nested['wolfcrypt']['version'], '5.9.1')
29993011
self.assertEqual(
3000-
nested['wolfcrypt']['cpe'],
3001-
'cpe:2.3:a:wolfssl:wolfcrypt:5.9.1:*:*:*:*:*:*:*')
3012+
nested['wolfcrypt']['purl'],
3013+
'pkg:github/wolfssl/wolfssl@v5.9.1-stable#wolfcrypt')
3014+
self.assertNotIn('cpe', nested['wolfcrypt'])
30023015

30033016
def test_cdx_dependency_edges_follow_the_nesting(self):
30043017
doc = gs.generate_cdx(**self.BASE_KW)
@@ -3053,6 +3066,18 @@ def test_spdx_expresses_containment_not_a_second_dependency(self):
30533066
self.assertEqual(
30543067
rels[('SPDXRef-Package-wolfssl', 'SPDXRef-Package-wolfcrypt')],
30553068
'CONTAINS')
3069+
pkgs = {p['name']: p for p in doc['packages']}
3070+
wc_refs = {r['referenceType']: r['referenceLocator']
3071+
for r in pkgs['wolfcrypt']['externalRefs']}
3072+
self.assertNotIn('cpe23Type', wc_refs)
3073+
self.assertEqual(
3074+
wc_refs['purl'],
3075+
'pkg:github/wolfssl/wolfssl@v5.9.1-stable#wolfcrypt')
3076+
ssl_refs = {r['referenceType']: r['referenceLocator']
3077+
for r in pkgs['wolfssl']['externalRefs']}
3078+
self.assertEqual(
3079+
ssl_refs['cpe23Type'],
3080+
'cpe:2.3:a:wolfssl:wolfssl:5.9.1:*:*:*:*:*:*:*')
30563081

30573082
def test_spdx_wolfssl_own_sbom_contains_its_wolfcrypt(self):
30583083
# wolfSSL ships wolfCrypt; it does not depend on it.
@@ -3075,11 +3100,12 @@ def test_wolfssl_own_sbom_keeps_wolfcrypt_top_level(self):
30753100
self.BASE_KW, name='wolfssl', version='5.9.1',
30763101
enabled_deps=['wolfcrypt'], component_type='library'))
30773102
self.assertEqual([c['name'] for c in doc['components']], ['wolfcrypt'])
3103+
self.assertNotIn('cpe', doc['components'][0])
30783104

3079-
def test_tool_version_is_1_8(self):
3105+
def test_tool_version_is_1_9(self):
30803106
doc = gs.generate_cdx(**self.BASE_KW)
30813107
tools = doc['metadata']['tools']['components']
3082-
self.assertEqual(tools[0]['version'], '1.8')
3108+
self.assertEqual(tools[0]['version'], '1.9')
30833109

30843110

30853111
if __name__ == '__main__':
@@ -3092,7 +3118,7 @@ class TestWolfcryptVersionInheritance(unittest.TestCase):
30923118
Regression: the inheritance ran BEFORE _resolve_dep_versions, so it only
30933119
ever saw versions passed explicitly via --dep-version. A downstream
30943120
embedder (wolfBoot) whose wolfssl version came from pkg-config got a
3095-
wolfcrypt component with no version, no purl and no cpe, and exit 0.
3121+
wolfcrypt component with no version and no purl, and exit 0.
30963122
"""
30973123

30983124
def test_inherits_wolfssl_version_resolved_by_pkgconfig(self):
@@ -3121,17 +3147,20 @@ def test_noop_when_wolfcrypt_not_enabled(self):
31213147
['wolfssl'], overrides, 'wolfboot', '2.9.0')
31223148
self.assertNotIn('wolfcrypt', overrides)
31233149

3124-
def test_inherited_version_yields_a_resolvable_cpe(self):
3125-
# End state that matters: the CPE 2.3 formatted string is well formed
3126-
# (13 colon-separated fields) and carries the inherited version.
3150+
def test_inherited_version_yields_a_resolvable_purl_and_no_cpe(self):
3151+
# End state that matters: the nested component is versioned and
3152+
# has a resolvable PURL. Matching stays on the wolfssl CPE.
31273153
overrides = {'wolfssl': '5.9.1', 'wolfcrypt': None}
31283154
gs._inherit_wolfcrypt_version(
31293155
['wolfssl', 'wolfcrypt'], overrides, 'wolfboot', '2.9.0')
31303156
_ref, comp = gs.cdx_dep_component(
31313157
'wolfcrypt', overrides['wolfcrypt'], 'wolfcrypt', overrides)
31323158
self.assertEqual(
3133-
comp['cpe'], 'cpe:2.3:a:wolfssl:wolfcrypt:5.9.1:*:*:*:*:*:*:*')
3134-
self.assertEqual(len(comp['cpe'].split(':')), 13)
3159+
comp['purl'],
3160+
'pkg:github/wolfssl/wolfssl@v5.9.1-stable#wolfcrypt')
3161+
self.assertNotIn('cpe', comp)
3162+
3163+
31353164
class TestEnabledDepsValidation(unittest.TestCase):
31363165
"""--dep-* must be exactly yes or no.
31373166

tests/test_sbom.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ def unit_tests():
176176
"cpe": "cpe:2.3:a:wolfssl:wolfssl:5.9.1:*:*:*:*:*:*:*",
177177
"components": [{
178178
"name": "wolfcrypt", "version": "5.9.1",
179-
"cpe": "cpe:2.3:a:wolfssl:wolfcrypt:5.9.1:*:*:*:*:*:*:*",
180179
}],
181180
}],
182181
}, f)

0 commit comments

Comments
 (0)