Skip to content

🐛 fix(sbom): keep the build machine out of the SBOMs - #3311

Merged
gaborbernat merged 1 commit into
pypa:mainfrom
gaborbernat:fix/reproducible-sbom
Sep 23, 2026
Merged

gaborbernat merged 1 commit into
pypa:mainfrom
gaborbernat:fix/reproducible-sbom

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Rebuilding the 21.10.0 tag with the release's SOURCE_DATE_EPOCH reproduces the sdist but not the wheel. The CycloneDX SBOM that hatch_build.py embeds has recorded the build machine since #3270: the interpreter build string, compiler and build date, and a tool:os component with the kernel release and version, platform string, architecture and every /etc/os-release field. A macOS rebuild therefore gets a different SBOM, a different content-derived serialNumber, a different RECORD and a different wheel hash. The zipapp has the same problem, because its SBOM reuses that tool listing and every zip entry took the wall clock time. The GitHub provenance attestation already names the builder, workflow and commit, so the SBOMs do not need to repeat it. 🔒

The SBOMs now keep only facts that hold for any rebuild of the same source with the same toolchain: components, hashes, licences, purls, the source commit, the Python version number, the build backend and its dependencies with their files, and the SOURCE_DATE_EPOCH timestamp. The serial number stays derived from the content, so it is now stable across rebuilds. Two more inputs varied by host once the OS fields were gone. Build tool distributions installed from platform wheels (such as pyyaml and rpds-py in the zipapp build environment) list different .so files per OS and architecture, so they keep name and version but lose their file list. The build frontend writes INSTALLER, REQUESTED and direct_url.json, which the listing now skips. make_zipapp.py writes each entry with the SOURCE_DATE_EPOCH timestamp, mode 0644 and a fixed creator OS. The zipapp tox environment now passes SOURCE_DATE_EPOCH through; before, it dropped the variable and fell back to hatchling's 2020 default. cyclonedx_to_spdx.py drops its now unused operating-system purpose mapping.

✅ I checked this on commit 81fc2ceb with SOURCE_DATE_EPOCH=1790127591, CPython 3.14.7 and hatchling 1.32.4 (build backend dependencies pinned from the first build's SBOM). The wheel built with uv build on macOS arm64, with uv build in a Linux aarch64 container and with python -m build (pip) in a Linux x86_64 container has the same SHA-256 on all three (1c0430d6…90ed), and cmp reports no difference. tox r -e zipapp on macOS and in the Linux container produced a byte-identical virtualenv.pyz and virtualenv.pyz.cdx.json. A byte-for-byte wheel rebuild needs the same source tree (a git checkout, since the SBOM records the commit), SOURCE_DATE_EPOCH, Python patch version, and build backend version along with its dependencies. The build frontend, OS and architecture can differ. The zipapp needs those inputs too. Since #3306 its bundled distributions come from the committed PEP 751 lock, and the tools that build the wheel inside it must match the versions its SBOM lists, since that build resolves its own isolated environment.

The docs from #3305 said the wheel differs from a rebuild in its SBOM and RECORD. They now list the inputs a byte-for-byte rebuild needs, and the how-to shows how to read the Python version and build constraints from the published SBOM and compare the rebuilt wheel with cmp. Wheels up to 21.10.0 keep the old SBOM and still differ on rebuild.

@gaborbernat gaborbernat added bug security Fixes a vulnerability or hardens the supply chain, CI or release labels Sep 23, 2026
The wheel SBOM recorded the interpreter build string, compiler, build date,
kernel, architecture and os-release of the machine that ran the build, so a
rebuild on another host produced a different SBOM, serial number, RECORD and
wheel hash. The provenance attestation already names the builder, so the SBOM
now keeps only facts that hold for any rebuild with the same toolchain.

For the same reason the build tool listing skips the INSTALLER, REQUESTED and
direct_url.json files the build frontend writes, and lists tool distributions
that are not pure Python without their files, since those differ per OS and
architecture. The zipapp SBOM reuses that listing.

The zipapp entries took the wall clock time, and the tox env dropped
SOURCE_DATE_EPOCH; entries now carry that timestamp, fixed permissions and a
fixed creator OS.
@gaborbernat
gaborbernat merged commit 5d9dc58 into pypa:main Sep 23, 2026
65 checks passed
gaborbernat added a commit to gaborbernat/virtualenv that referenced this pull request Sep 26, 2026
The verify-release how-to said the zipapp build does not pin what it
bundles, which stopped being true when pypa#3306 added pylock.zipapp.toml, and
the threat model still said wheels differ between build machines after
pypa#3311. Neither page covered the release attestation GitHub signs for the
immutable releases published since 21.11.0.

Move the examples to 21.12.1, add gh release verify and verify-asset, and
give a zipapp rebuild recipe that constrains the build tools from the SBOMs.
Fix the same claims in the release artifacts reference, the explanation and
the threat model, whose upgrade job note also named a deploy key that the
workflow does not use.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug security Fixes a vulnerability or hardens the supply chain, CI or release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant