Skip to content

Commit b3aab11

Browse files
committed
ci(sbom): verify bomsh provenance end-to-end; pyspdxtools-validate enriched SPDX
CI previously asserted only that a gitoid externalRef exists. Add `scripts/bomsh_verify.py` (with 8 synthetic-fixture unit tests) verifying every gitoid resolves, blobs round-trip their sha1, and the wolfSSL gitoid matches the built `libwolfssl.*`; pyspdxtools schema-validates the enriched SPDX and `make bomsh` records the traced artefact in `_bomsh.artefact`.
1 parent ca07536 commit b3aab11

5 files changed

Lines changed: 611 additions & 6 deletions

File tree

.github/workflows/sbom.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,57 @@ jobs:
981981
print(f'bomsh enrichment ok: {len(gitoid_refs)} gitoid refs')
982982
PY
983983
984+
- name: Bomsh-enriched SPDX validates per pyspdxtools
985+
# Schema gate. `make sbom` already runs pyspdxtools on the
986+
# un-enriched SPDX; the enriched document was previously
987+
# ungated. A bomsh_sbom.py change that emits a malformed
988+
# SPDX 2.3 document (e.g. wrong shape on the gitoid externalRef
989+
# block, missing required field on a new package) would
990+
# otherwise ship in the artefact bundle below. Complementary
991+
# to the next step which validates *semantic* correctness
992+
# (does the gitoid actually point at anything real); this
993+
# step pins *schema* correctness only.
994+
run: |
995+
set -e
996+
# `[ -f "$f" ] || continue` makes the loop robust if the glob
997+
# has no match (defensive only; the previous step already
998+
# `ls`-fails the job in that case, but this decouples the
999+
# two if a future maintainer reorders).
1000+
for f in omnibor.wolfssl-*.spdx.json; do
1001+
[ -f "$f" ] || continue
1002+
echo "Validating $f"
1003+
pyspdxtools --infile "$f"
1004+
done
1005+
1006+
- name: Bomsh provenance is end-to-end verifiable
1007+
# Three independent self-consistency checks on the bomsh
1008+
# provenance bundle. The PERSISTENT-ID assertion above only
1009+
# proves the gitoid externalRef *exists*; none of these
1010+
# follow-up properties are guaranteed by it:
1011+
#
1012+
# (A) every gitoid in the SPDX externalRefs resolves to a
1013+
# blob present in omnibor/objects/<aa>/<rest>
1014+
# (B) every blob in omnibor/objects/ round-trips through
1015+
# sha1(b"blob <len>\0" + content) so the object store
1016+
# is internally self-consistent (no bit-rot, no
1017+
# truncation, no stray non-blob file under objects/)
1018+
# (C) the gitoid recorded against the wolfSSL package equals
1019+
# the git-blob hash of the actual library artefact that
1020+
# `make bomsh` traced (the SBOM ties to the binary that
1021+
# would actually ship)
1022+
#
1023+
# Without this, a future bomsh_sbom.py change that emits a
1024+
# plausibly-shaped but fictional gitoid (one that does not
1025+
# resolve in the ADG, or resolves but to the wrong artefact)
1026+
# would pass the existing PERSISTENT-ID assertion and ship a
1027+
# provenance bundle whose externalRef is a lie.
1028+
#
1029+
# The verifier logic lives in scripts/bomsh_verify.py so it can
1030+
# be unit-tested with synthetic fixtures (see the
1031+
# TestBomshProvenanceVerify class in scripts/test_gen_sbom.py)
1032+
# rather than only running here against a real bomsh trace.
1033+
run: python3 scripts/bomsh_verify.py
1034+
9841035
# The full provenance bundle - the high-value artefact of the whole
9851036
# PR, the one a CRA reviewer or downstream packager wants to download.
9861037
# MUST be uploaded BEFORE the `make clean` step below, which deletes
@@ -1029,3 +1080,5 @@ jobs:
10291080
exit 1
10301081
fi
10311082
test ! -d omnibor || (echo "omnibor/ not cleaned"; exit 1)
1083+
test ! -f _bomsh.artefact \
1084+
|| (echo "_bomsh.artefact not cleaned"; exit 1)

Makefile.am

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -469,11 +469,17 @@ uninstall-sbom:
469469
CLEANFILES += $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV)
470470

471471
# Bomsh (OmniBOR build artifact tracing + SBOM enrichment)
472-
BOMSH_RAWLOG_BASE = $(abs_builddir)/bomsh_raw_logfile
473-
BOMSH_RAWLOG = $(BOMSH_RAWLOG_BASE).sha1
474-
BOMSH_CONF = $(abs_builddir)/_bomsh.conf
475-
BOMSH_OMNIBORDIR = $(abs_builddir)/omnibor
476-
BOMSH_SPDX_OUT = omnibor.wolfssl-$(PACKAGE_VERSION).spdx.json
472+
BOMSH_RAWLOG_BASE = $(abs_builddir)/bomsh_raw_logfile
473+
BOMSH_RAWLOG = $(BOMSH_RAWLOG_BASE).sha1
474+
BOMSH_CONF = $(abs_builddir)/_bomsh.conf
475+
BOMSH_OMNIBORDIR = $(abs_builddir)/omnibor
476+
BOMSH_SPDX_OUT = omnibor.wolfssl-$(PACKAGE_VERSION).spdx.json
477+
# Single-source-of-truth manifest of the library artefact bomtrace3
478+
# actually traced. Written by the bomsh: recipe so downstream
479+
# verification (CI: `Bomsh provenance is end-to-end verifiable`) doesn't
480+
# have to re-derive the same WOLFSSL_LIB_DSO_BASENAMES priority order
481+
# in parallel and risk drift.
482+
BOMSH_ARTEFACT_MANIFEST = $(abs_builddir)/_bomsh.artefact
477483
bomshdir = $(datadir)/doc/$(PACKAGE)
478484

479485
.PHONY: bomsh install-bomsh uninstall-bomsh
@@ -520,6 +526,7 @@ bomsh:
520526
echo " OmniBOR graph produced; SPDX enrichment skipped."; \
521527
exit 0; \
522528
fi; \
529+
printf '%s\n' "$$bomsh_artifact" > '$(BOMSH_ARTEFACT_MANIFEST)'; \
523530
echo "Enriching SPDX with OmniBOR ExternalRefs (artifact: $$bomsh_artifact)..."; \
524531
$(BOMSH_SBOM) \
525532
-b '$(BOMSH_OMNIBORDIR)' \
@@ -541,7 +548,7 @@ uninstall-bomsh:
541548
-rm -rf '$(DESTDIR)$(bomshdir)/omnibor'
542549
-rm -f '$(DESTDIR)$(bomshdir)/$(BOMSH_SPDX_OUT)'
543550

544-
CLEANFILES += $(BOMSH_RAWLOG) $(BOMSH_RAWLOG_BASE).sha256 $(BOMSH_CONF) $(BOMSH_SPDX_OUT)
551+
CLEANFILES += $(BOMSH_RAWLOG) $(BOMSH_RAWLOG_BASE).sha256 $(BOMSH_CONF) $(BOMSH_SPDX_OUT) $(BOMSH_ARTEFACT_MANIFEST)
545552

546553
# Hook SBOM/Bomsh cleanup into `make uninstall` so packagers don't leave
547554
# stale artefacts behind after install-sbom/install-bomsh. uninstall-sbom

doc/SBOM.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,35 @@ The raw logfile (`bomsh_raw_logfile.sha1`) and conf file (`_bomsh.conf`)
632632
are written to the build directory and removed by `make clean`. The
633633
`omnibor/` tree is also removed by `make clean`.
634634

635+
#### CI verifiability gates
636+
637+
The bomsh CI job enforces three independent self-consistency properties
638+
on every PR, in addition to schema validation of the enriched SPDX
639+
through `pyspdxtools`:
640+
641+
1. **Resolvability** — every `gitoid` listed in the SPDX `externalRefs`
642+
resolves to a blob present at `omnibor/objects/<aa>/<rest>`.
643+
2. **Object-store integrity** — every blob in `omnibor/objects/`
644+
round-trips through `sha1(b"blob <len>\0" + content)`, so a corrupt or
645+
truncated object store is caught at PR time, not by a downstream
646+
verifier weeks later.
647+
3. **Artefact correspondence** — the `gitoid` recorded against the
648+
`wolfssl` SPDX package equals the git-blob hash of the actual
649+
`libwolfssl.{so,dylib,a}` that `make bomsh` traced. This is what
650+
makes the SBOM a true attestation of the binary that would ship,
651+
rather than a plausible-looking but fictional reference.
652+
653+
If any of these fail, the PR fails — the bomsh provenance bundle that a
654+
CRA reviewer would download is never published with a broken bridge.
655+
656+
The verifier itself lives at `scripts/bomsh_verify.py` (importable, with
657+
synthetic-fixture unit tests in `scripts/test_gen_sbom.py`). Run it
658+
against any local `make bomsh` output with:
659+
660+
```sh
661+
python3 scripts/bomsh_verify.py
662+
```
663+
635664
---
636665

637666
## 4. Combined workflow

0 commit comments

Comments
 (0)