fix(gen-sbom): unique serialNumber/documentNamespace per build config - #15
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the shared share/gen-sbom generator so CycloneDX serialNumber and SPDX documentNamespace become unique per build configuration, not just per (name, version), by folding a stable config_identity digest into the UUID seed. It also adds unit + end-to-end tests and wires them into CI.
Changes:
- Add
config_identity()to hash build-distinguishing inputs (artifact hash, license ID, build macros, enabled deps + resolved versions). - Include
config_identityin the derived UUID seed forserialNumberanddocumentNamespace. - Add
tests/test_sbom_identity.pyand run it in.github/workflows/selftest.yml.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
share/gen-sbom |
Adds config_identity() and folds it into UUID seeding for serial/document namespace uniqueness per config. |
tests/test_sbom_identity.py |
New unit + end-to-end coverage for identity reproducibility and per-config uniqueness. |
.github/workflows/selftest.yml |
Adds syntax-check and execution of the new SBOM identity tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import json | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| from importlib.machinery import SourceFileLoader | ||
|
|
||
| REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| GEN_SBOM = os.path.join(REPO, "share", "gen-sbom") | ||
|
|
||
| # gen-sbom has no .py extension, so load it by path with an explicit loader. | ||
| gs = SourceFileLoader("gen_sbom", GEN_SBOM).load_module() | ||
|
|
3ab77f9 to
9bdf5b7
Compare
71bb13b to
be02e71
Compare
|
Reviewed and ran the suite on top of current master: rebases clean, all 210
Smaller:
One design call to make explicit: folding FYI the "byte-identical to wolfssl#10343" note is stale. The copies already |
be02e71 to
ed9cf4f
Compare
|
Approving the content. Rebase and I will merge. The block is mechanical. This went dirty after you pushed, and master is now Verified independently of your table: --supplier, --component-type, Nit, not blocking: setUpClass raises on missing pcpp where the rest of the repo Two follow-ups, not for this PR. The "byte-identical to wolfssl#10343" note is |
serial and doc_ns_uuid were derived only from (name, version), so two SBOMs for one release but different build configuration shared an identical CycloneDX serialNumber and SPDX documentNamespace despite differing content: an SPDX 2.3 section 6.5 uniqueness violation, and a CycloneDX version:1 with a shared serial over differing content is self-contradictory. Fold a config_identity digest into the serial/documentNamespace seed. Every CLI input that can change the document body reaches it: artifact hash and how it was obtained, licence identifier and text, supplier, component type, build macros, dependencies and resolved versions, the artefact/source filenames the document names, and the crypto-only subset claim and its basis. bom-refs and the UUID namespace are unchanged. Identifiers rotate once. Values published before this were colliding across configurations, and nothing downstream pins a literal serial. tests/test_sbom_identity.py in three layers: unit tests per digest field, an end-to-end table with one row per identity-relevant option under a fixed SOURCE_DATE_EPOCH that asserts the body changed before demanding the identity moved, and an ast canary over gen-sbom's own add_argument calls against a declared identity-relevant/exempt split so a new option cannot land unclassified.
ed9cf4f to
25ef915
Compare
serialanddoc_ns_uuidwere derived only from(name, version), so twoSBOMs for the same release but different build configuration (FIPS vs
non-FIPS, a different
--srcsset, a different feature-flag/dependency set)shared an identical CycloneDX
serialNumberand SPDXdocumentNamespacedespite differing content: an SPDX 2.3 §6.5 uniqueness violation, and a
CycloneDX
version: 1with a shared serial over differing content isself-contradictory.
Folds a
config_identitydigest into the serial/documentNamespace seed.Different configs now get distinct identifiers; an identical config still
reproduces byte-for-byte.
bom-refs and the UUID namespace are unchanged, andboth claims are now asserted.
What reaches the digest
The rule the generator holds to: every CLI input that can change the emitted
document body reaches the identity digest.
lib_hash) and how it was obtained (hash_kind,hash_source)Tests
tests/test_sbom_identity.py, in three layers:config_identity: each folded field is load-bearing, fields donot alias, ordering is stable, the positional five-argument call still
works.
script under a fixed
SOURCE_DATE_EPOCHand comparing real documents. Eachrow asserts the body changed first, then that the identity moved, so a row
that stops exercising anything fails instead of passing vacuously.
astwalk over gen-sbom's ownadd_argumentcalls, checked against a declared identity-relevant/exempt split. A new
option cannot land without being classified, and every exemption carries its
reason.
Reverting the generator change and re-running the table fails on
--supplier,--component-type,--crypto-only,--lib(renamed, identical bytes), and--license-text.--no-artifact-hashcorrectly does not fail: it moveslib_hashto the sentinel, which was already folded.Wired into
selftest.yml. Full suite green: 23 identity tests, 210 generatortests, self-test.
Identifier rotation
Identifiers rotate once. SBOMs published before this carry the old values.
They were previously wrong (colliding across configurations), and nothing
downstream pins a literal
serialNumber. Documented inREADME.mdunderDocument identity.
Design call, stated explicitly
Folding
lib_hashin means the identity follows the built binary on the--libpath, so recompiling identical sources with a different toolchain movesthe
serialNumbereven though no configuration input changed. That isdeliberate: an SBOM generated from
--libdescribes one specific binary, andtwo binaries with different hashes are two artefacts. The
--srcsand--no-artifact-hashpaths do not have this property, because neither hashes acompiler output.
Backport
This touches the shared
gen-sbomengine. The wolfSSL copy(wolfssl#10343) is not byte-identical any more: the two diverged by roughly
125 lines when four commits landed after the original rebase here, and
wolfssl#11120 is adding ~460 more. The backport still matters, because wolfBoot
vendors the wolfSSL copy and gets none of this until it happens. The
doc/SBOM.mdrotation note belongs to that backport:doc/SBOM.mdlives inwolfssl, not in wolfGlass, so the note is in
README.mdhere.Addresses SBOM-gpex.6.