Skip to content

Commit ca07536

Browse files
committed
fix(sbom): emit urn:uuid documentNamespace; validate override; drop unhosted wolfssl.com URLs
The previous output asserted `https://wolfssl.com/sbom/...` URLs the project does not serve. Default `documentNamespace` to a deterministic `urn:uuid` (SPDX 2.3 §6.5 requires uniqueness, not resolvability) and add `--document-namespace` / `SBOM_DOCUMENT_NAMESPACE` with RFC 3986 validation; the uuid5 seed is unchanged so derived UUIDs stay byte-stable.
1 parent e1e47cb commit ca07536

5 files changed

Lines changed: 130 additions & 15 deletions

File tree

Makefile.am

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,13 @@ WOLFSSL_LIB_DSO_BASENAMES = \
387387
# for SPDX 2.3 conformance whenever a custom
388388
# LicenseRef is in use; `make sbom` exits with an
389389
# error if it is missing.
390+
# SBOM_DOCUMENT_NAMESPACE Override the SPDX documentNamespace. Default
391+
# is a deterministic urn:uuid (SPDX 2.3 §6.5
392+
# requires only uniqueness, not resolvability).
393+
# Downstream packagers re-hosting the SBOM under
394+
# their own URL should set this to a URI they
395+
# actually serve (e.g.
396+
# https://example.com/sbom/wolfssl-X.Y.Z.spdx.json).
390397
sbom:
391398
@if test -z "$(PYTHON3)"; then \
392399
echo ""; \
@@ -438,6 +445,7 @@ sbom:
438445
--license-file $(srcdir)/LICENSING \
439446
$(if $(SBOM_LICENSE_OVERRIDE),--license-override '$(SBOM_LICENSE_OVERRIDE)') \
440447
$(if $(SBOM_LICENSE_TEXT),--license-text '$(SBOM_LICENSE_TEXT)') \
448+
$(if $(SBOM_DOCUMENT_NAMESPACE),--document-namespace '$(SBOM_DOCUMENT_NAMESPACE)') \
441449
--options-h $(abs_builddir)/wolfssl/options.h \
442450
--lib "$$sbom_lib" \
443451
--dep-libz $(ENABLED_LIBZ) \

doc/CRA.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,20 @@ Reference wolfSSL's SPDX document from your product's SPDX document using
101101
`externalDocumentRefs`. This keeps the documents separate and lets wolfSSL's
102102
SBOM stand as an independently verifiable artefact.
103103

104+
wolfSSL ships the generated SBOM with the source distribution and does not
105+
currently publish it at a fixed, resolvable URL. In the `spdxDocument`
106+
field below, substitute the URI under which your distribution mirrors
107+
`wolfssl-<version>.spdx.json` (e.g. an artifact server, OCI registry, or
108+
distribution mirror you control). SPDX 2.3 §6.5 only requires the value
109+
be a unique URI; if you do not re-host, the `urn:uuid:<uuid5-derived>`
110+
form that `make sbom` emits by default is acceptable.
111+
104112
```json
105113
{
106114
"externalDocumentRefs": [
107115
{
108116
"externalDocumentId": "DocumentRef-wolfssl",
109-
"spdxDocument": "https://wolfssl.com/sbom/wolfssl-<version>.spdx.json",
117+
"spdxDocument": "<URI where your distribution serves wolfssl-<version>.spdx.json>",
110118
"checksum": {
111119
"algorithm": "SHA256",
112120
"checksumValue": "<sha256-of-wolfssl-spdx.json>"
@@ -136,7 +144,11 @@ directly into your own SPDX document and add the `DYNAMIC_LINK` /
136144
### CycloneDX: component reference
137145

138146
Include wolfSSL as a component in your CycloneDX BOM, referencing the
139-
wolfSSL CycloneDX document via an external reference of type `bom`:
147+
wolfSSL CycloneDX document via an external reference of type `bom`.
148+
149+
As with the SPDX `spdxDocument` URI above, wolfSSL does not currently
150+
publish CycloneDX SBOMs at a fixed, resolvable URL; substitute the URI
151+
under which your distribution mirrors `wolfssl-<version>.cdx.json`.
140152

141153
```json
142154
{
@@ -149,7 +161,7 @@ wolfSSL CycloneDX document via an external reference of type `bom`:
149161
"externalReferences": [
150162
{
151163
"type": "bom",
152-
"url": "https://wolfssl.com/sbom/wolfssl-<version>.cdx.json",
164+
"url": "<URI where your distribution serves wolfssl-<version>.cdx.json>",
153165
"hashes": [
154166
{
155167
"alg": "SHA-256",

doc/SBOM.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Optional flags:
7878
| `--dep-version libz=1.3.1` | Explicit dep version when `pkg-config` is unavailable (typical cross-compile) |
7979
| `--license-override LicenseRef-wolfSSL-Commercial` | If you are a commercial licensee, not GPL |
8080
| `--license-text /path/to/commercial-license.txt` | Required when `--license-override` is a `LicenseRef-*` |
81+
| `--document-namespace https://example.com/sbom/wolfssl-5.9.1.spdx.json` | Override the SPDX `documentNamespace`. Default is a deterministic `urn:uuid:` derived from `--name`/`--version` (SPDX 2.3 §6.5 requires only uniqueness, not resolvability). Set this when **your** distribution re-hosts the SBOM under a stable URL. |
8182

8283
### 1.3 Dependencies
8384

@@ -458,6 +459,23 @@ make sbom \
458459
SBOM_LICENSE_TEXT=/path/to/wolfssl-commercial-license.txt
459460
```
460461

462+
#### Overriding the SPDX `documentNamespace`
463+
464+
By default the SPDX document's `documentNamespace` is a deterministic
465+
`urn:uuid:<uuid5-derived>` value. SPDX 2.3 §6.5 only requires that this
466+
field be a unique URI; it does **not** have to resolve to anything. The
467+
default avoids asserting a hosted URL the wolfSSL project does not serve.
468+
469+
If your distribution re-publishes the SBOM under a stable URL you control,
470+
set `SBOM_DOCUMENT_NAMESPACE` (or `--document-namespace` in the standalone
471+
entry point) so downstream consumers can `externalDocumentRef` your
472+
hosted copy:
473+
474+
```sh
475+
make sbom \
476+
SBOM_DOCUMENT_NAMESPACE=https://example.com/sbom/wolfssl-5.9.1.spdx.json
477+
```
478+
461479
#### External dependency version detection
462480

463481
The optional external dependencies wolfSSL can link against (`libz` and

scripts/gen-sbom

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,17 @@ import uuid
1313
from datetime import datetime, timezone
1414

1515

16-
# Stable namespace for deterministic uuid5 derivation. Anchored under
17-
# wolfssl.com so collisions with other projects' SBOM UUIDs are not a concern.
16+
# Stable namespace for deterministic uuid5 derivation. The seed string is
17+
# an opaque input to uuid5 -- it only needs to be (a) constant across
18+
# releases so the derived UUIDs reproduce byte-for-byte (any consumer
19+
# pinning a wolfSSL SBOM hash would otherwise see a content rotation
20+
# from a seed change alone), and (b) unlikely to collide with another
21+
# project's uuid5 namespace. It is NOT a URL the SBOM resolves to and
22+
# is NOT what we serialize as the SPDX documentNamespace -- that field
23+
# is now `urn:uuid:<derived>` (see generate_spdx). The historical
24+
# string is preserved verbatim to keep derived UUIDs (bom-refs,
25+
# serialNumbers, the documentNamespace UUID component) stable across
26+
# the documentNamespace shape change.
1827
SBOM_UUID_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL, 'https://wolfssl.com/sbom/')
1928

2029

@@ -707,7 +716,7 @@ def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
707716
def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
708717
timestamp, year, doc_ns_uuid, enabled_deps, build_props,
709718
dep_version_overrides=None, hash_kind='library-binary',
710-
srcs_basenames=None):
719+
srcs_basenames=None, document_namespace=None):
711720
build_defines = ', '.join(k for k, _ in build_props)
712721
# Only annotate the comment when running the source-merkle entry
713722
# point. The autotools / library-binary path keeps its existing
@@ -762,14 +771,20 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
762771
'relationshipType': 'DEPENDS_ON',
763772
})
764773

774+
# SPDX 2.3 §6.5: documentNamespace must be a unique URI; it is NOT
775+
# required to resolve to anything. Default to `urn:uuid:<derived>`
776+
# rather than a `https://wolfssl.com/sbom/...` URL the project does
777+
# not actually host -- emitting an unresolvable URL misleads any
778+
# downstream tool that follows it. Downstream packagers who DO host
779+
# a per-version mirror can override via `--document-namespace`
780+
# (Makefile.am: SBOM_DOCUMENT_NAMESPACE).
781+
doc_namespace = document_namespace or f'urn:uuid:{doc_ns_uuid}'
765782
doc = {
766783
'spdxVersion': 'SPDX-2.3',
767784
'dataLicense': 'CC0-1.0',
768785
'SPDXID': 'SPDXRef-DOCUMENT',
769786
'name': f'{name}-{version}',
770-
'documentNamespace': (
771-
f'https://wolfssl.com/sbom/{name}-{version}-{doc_ns_uuid}'
772-
),
787+
'documentNamespace': doc_namespace,
773788
'creationInfo': {
774789
'creators': [
775790
f'Organization: {supplier}',
@@ -884,6 +899,16 @@ def main():
884899
+ ', '.join(sorted(DEP_META)) + '. Required '
885900
'on hosts without pkg-config (typical embedded '
886901
'cross-compile setups).')
902+
parser.add_argument('--document-namespace', default='',
903+
metavar='URI',
904+
help='Override SPDX documentNamespace. Default '
905+
'is a deterministic urn:uuid derived from '
906+
'--name and --version. Set to a URI you '
907+
'actually host (e.g. '
908+
'https://example.com/sbom/wolfssl-X.Y.Z.spdx.json) '
909+
'when re-publishing the SBOM under your own '
910+
'distribution. SPDX 2.3 §6.5 requires only '
911+
'uniqueness, not resolvability.')
887912
parser.add_argument('--cdx-out', required=True,
888913
help='Output path for CycloneDX JSON')
889914
parser.add_argument('--spdx-out', required=True,
@@ -908,6 +933,24 @@ def main():
908933
" --srcs: hash the wolfSSL source files compiled into "
909934
"your firmware (OmniBOR gitoid Merkle hash).")
910935

936+
# SPDX 2.3 §6.5 requires documentNamespace to be a unique absolute URI
937+
# per RFC 3986. `make sbom` runs pyspdxtools afterwards and would
938+
# catch a malformed value, but the standalone entry point has no
939+
# validation gate -- a typo in SBOM_DOCUMENT_NAMESPACE / a packager
940+
# passing a relative path would otherwise land malformed SPDX in
941+
# downstream artefacts. An absolute URI per RFC 3986 §3 has a
942+
# non-empty scheme; urlparse extracts that.
943+
if args.document_namespace:
944+
from urllib.parse import urlparse
945+
scheme = urlparse(args.document_namespace).scheme
946+
if not scheme:
947+
sys.exit(
948+
f"ERROR: --document-namespace {args.document_namespace!r} "
949+
"is not an absolute URI (SPDX 2.3 §6.5 requires RFC 3986 "
950+
"absolute URI form). Expected e.g. "
951+
"https://example.com/sbom/wolfssl-X.Y.Z.spdx.json or "
952+
"urn:uuid:00000000-0000-0000-0000-000000000000.")
953+
911954
enabled_deps = [
912955
key for key, flag in [
913956
('libz', args.dep_libz),
@@ -1004,6 +1047,7 @@ def main():
10041047
enabled_deps, build_props,
10051048
dep_version_overrides=dep_version_overrides,
10061049
hash_kind=hash_kind, srcs_basenames=srcs_basenames,
1050+
document_namespace=(args.document_namespace or None),
10071051
)
10081052

10091053
try:

scripts/test_gen_sbom.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,12 +1720,45 @@ def test_top_level_shape(self):
17201720
self.assertEqual(doc['dataLicense'], 'CC0-1.0')
17211721
self.assertEqual(doc['SPDXID'], 'SPDXRef-DOCUMENT')
17221722
self.assertEqual(doc['name'], 'wolfssl-5.9.1')
1723-
self.assertTrue(
1724-
doc['documentNamespace'].startswith('https://wolfssl.com/sbom/'))
1725-
# documentNamespace must include the doc_ns_uuid so two
1726-
# different versions produce different namespaces (SPDX 2.3
1727-
# §6.5).
1728-
self.assertIn(self.BASE_KW['doc_ns_uuid'], doc['documentNamespace'])
1723+
# SPDX 2.3 §6.5: documentNamespace must be a unique URI; no
1724+
# requirement that it resolve. Default to `urn:uuid:<derived>`
1725+
# rather than a `https://wolfssl.com/sbom/...` URL the project
1726+
# does not host -- emitting an unresolvable URL would mislead
1727+
# any downstream tool that follows it. The doc_ns_uuid keeps
1728+
# the namespace per-version unique without making a hosting
1729+
# claim.
1730+
self.assertEqual(
1731+
doc['documentNamespace'],
1732+
f'urn:uuid:{self.BASE_KW["doc_ns_uuid"]}')
1733+
1734+
def test_document_namespace_override_is_honoured(self):
1735+
# Downstream packagers who legitimately re-host the SBOM under
1736+
# their own URL pass --document-namespace; the override must
1737+
# win over the urn:uuid default. Without this knob a packager
1738+
# would have to fork the script to satisfy SPDX 2.3 §6.5
1739+
# uniqueness against a self-hosted mirror.
1740+
custom = 'https://example.com/sbom/wolfssl-5.9.1.spdx.json'
1741+
doc = gs.generate_spdx(**{
1742+
**self.BASE_KW,
1743+
'document_namespace': custom,
1744+
})
1745+
self.assertEqual(doc['documentNamespace'], custom)
1746+
1747+
def test_document_namespace_default_is_urn_uuid(self):
1748+
# Negative companion to test_document_namespace_override: when
1749+
# no override is supplied (None or empty), the urn:uuid form is
1750+
# used and the previously-emitted https://wolfssl.com/sbom/
1751+
# URL is NOT reintroduced (regression guard for the M1
1752+
# correction).
1753+
for explicit in (None, ''):
1754+
doc = gs.generate_spdx(**{
1755+
**self.BASE_KW,
1756+
'document_namespace': explicit,
1757+
})
1758+
self.assertTrue(
1759+
doc['documentNamespace'].startswith('urn:uuid:'),
1760+
f'{explicit!r} -> {doc["documentNamespace"]!r}')
1761+
self.assertNotIn('wolfssl.com/sbom', doc['documentNamespace'])
17291762

17301763
def test_main_package_fields(self):
17311764
doc = gs.generate_spdx(**self.BASE_KW)

0 commit comments

Comments
 (0)