Problem
Organizations that build Python wheels from source with fromager need to answer: was every package in this tree built from the claimed source, by a known builder, with known tools? Fromager does not record or verify that today.
Existing standards address a narrower scope:
| Mechanism |
What it verifies |
| PyPI publish attestations (PEP 740) |
One published wheel ↔ one source repo |
| SLSA / Sigstore |
One artifact at a time |
Fromager SBOM (sbom.py) |
Package identity in the built wheel, not build-dependency chain |
None of these cover build dependencies — the packages installed into the build environment before a wheel is compiled. A compromised build-time dependency can affect every downstream wheel without any cross-package check.
Fromager already builds the full dependency tree in topological order (bootstrap roots first, then layers that depend on them). Each layer's build deps exist in wheels-repo/ before the next layer builds. That ordering is the right foundation for a provenance chain, but fromager does not use it yet:
- No SHA-256 hashes recorded for built wheels or downloaded sdists
- Sdist download URLs often include
#sha256= fragments; fromager does not verify them (sources.py)
- No structured build-environment snapshot per wheel
- No attestation documents linking a wheel to its build-dep attestations
Related open work: #1200 (CI metadata in SBOM annotations) records which pipeline built a wheel but not what build deps were used.
Proposal
Add an opt-in provenance feature that, for each built wheel:
- Records SHA-256 digests of the sdist input and output wheel
- Captures build environment metadata (Python version, platform, compiler, fromager version, allowlisted env vars)
- Generates an attestation document (in-toto Statement v1 / SLSA Provenance v1 predicate) referencing each build dependency's attestation by digest
- Optionally signs attestations
- Provides
fromager verify to walk the dependency tree and check chain integrity
The key field is attestationDigest in resolvedDependencies — each build dep points to its own attestation, forming a tree verifiable from any top-level wheel down to bootstrap roots (flit_core, setuptools, etc.).
Trust model
Fromager acts as builder and attester for packages it builds from source:
- Source integrity: hash sdists at download; verify PyPI URL
#sha256= fragments; fail on mismatch
- Bootstrap roots: simplest attestations (source in, wheel out, environment recorded)
- Chain construction: before installing a build dep, verify its wheel bytes match the hash in the attestation from the current run; repeat layer by layer
- Output integrity: after each build, hash the wheel and write an attestation referencing build-dep attestation digests
This establishes build integrity (known bytes, known builder, no tampering between layers). It does not establish source trustworthiness (whether upstream code is safe).
Pre-built wheels (not built by fromager) break the chain. Recommended handling: mark as unattested-prebuilt in the attestation; verify warns unless --allow-prebuilt.
Attestation example (abbreviated)
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{ "name": "scipy-....whl", "digest": { "sha256": "..." } }],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://fromager.readthedocs.io/provenance/v1",
"externalParameters": {
"source": { "uri": "...", "digest": { "sha256": "..." } }
},
"resolvedDependencies": [{
"name": "numpy-....whl",
"digest": { "sha256": "..." },
"attestationDigest": { "sha256": "..." }
}]
},
"runDetails": {
"builder": { "id": "..." },
"environment": { "python": "3.12.3", "compiler": "gcc-14.1", "fromager": "0.88.0" }
}
}
}
Storage (no graph.json changes)
Keep graph.json unchanged. Store provenance separately:
work-dir/provenance-index.json # package key → digests + sidecar paths
wheels-repo/downloads/{wheel}.attestation.json
wheels-repo/downloads/{wheel}.attestation.json.sig # when GPG signing enabled
wheels-repo/downloads/{wheel}.attestation.json.sigstore.json # when Sigstore signing enabled
Lightweight integrity/env JSON may be embedded in .dist-info/ (same pattern as SBOM). Full attestations and signatures stay as sidecars to avoid repack-on-sign.
Integration points
| Step |
Module |
Hook |
| Source download |
sources.py |
hash + verify URL fragment |
| Before build-dep install |
bootstrapper.py |
verify build-dep wheel hash + attestation |
| After wheel build |
wheels.py |
embed integrity/env; write attestation sidecar |
| Post-build |
new commands/verify.py |
fromager verify walks topology |
New module: provenance.py (hashing, environment capture, attestation generation, chain verification). Settings gate via provenance.enabled in package settings (mirrors SBOM opt-in pattern).
Suggested phases
| Phase |
Scope |
| 1 |
SHA-256 sdists/wheels; URL fragment verify; provenance-index.json |
| 2 |
Build environment capture |
| 3 |
Attestation generation + fromager verify |
| 4 |
Optional signing + verify --signatures |
Phases 1–3 deliver unsigned attestations and chain verification. Phase 4 adds signature sidecars via a provenance.signing setting:
- GPG — detached
.sig files; works offline; suited to private indexes and air-gapped builders.
- Sigstore — keyless signing via OIDC in CI; produces a
.sigstore.json bundle. The default public Sigstore flow also writes to Rekor, which publishes the signing identity and artifact digest — worth considering when choosing a backend for non-public artifacts.
Each phase is independently mergeable; later phases no-op when provenance.enabled: false.
Optional follow-up (lower priority): verify PyPI PEP 740 sdist attestations on download when available (verify_if_present mode).
Non-goals
- Key management infrastructure (HSM, KMS, rotation policies)
- Replacing PyPI publish attestations (complementary, not competing)
- Reproducible builds verification (provenance ≠ reproducibility)
- Modifying
graph.json schema
Happy to contribute a docs/proposals/ write-up and phased PRs if there is interest.
References
Problem
Organizations that build Python wheels from source with fromager need to answer: was every package in this tree built from the claimed source, by a known builder, with known tools? Fromager does not record or verify that today.
Existing standards address a narrower scope:
sbom.py)None of these cover build dependencies — the packages installed into the build environment before a wheel is compiled. A compromised build-time dependency can affect every downstream wheel without any cross-package check.
Fromager already builds the full dependency tree in topological order (bootstrap roots first, then layers that depend on them). Each layer's build deps exist in
wheels-repo/before the next layer builds. That ordering is the right foundation for a provenance chain, but fromager does not use it yet:#sha256=fragments; fromager does not verify them (sources.py)Related open work: #1200 (CI metadata in SBOM annotations) records which pipeline built a wheel but not what build deps were used.
Proposal
Add an opt-in provenance feature that, for each built wheel:
fromager verifyto walk the dependency tree and check chain integrityThe key field is
attestationDigestinresolvedDependencies— each build dep points to its own attestation, forming a tree verifiable from any top-level wheel down to bootstrap roots (flit_core,setuptools, etc.).Trust model
Fromager acts as builder and attester for packages it builds from source:
#sha256=fragments; fail on mismatchThis establishes build integrity (known bytes, known builder, no tampering between layers). It does not establish source trustworthiness (whether upstream code is safe).
Pre-built wheels (not built by fromager) break the chain. Recommended handling: mark as
unattested-prebuiltin the attestation;verifywarns unless--allow-prebuilt.Attestation example (abbreviated)
{ "_type": "https://in-toto.io/Statement/v1", "subject": [{ "name": "scipy-....whl", "digest": { "sha256": "..." } }], "predicateType": "https://slsa.dev/provenance/v1", "predicate": { "buildDefinition": { "buildType": "https://fromager.readthedocs.io/provenance/v1", "externalParameters": { "source": { "uri": "...", "digest": { "sha256": "..." } } }, "resolvedDependencies": [{ "name": "numpy-....whl", "digest": { "sha256": "..." }, "attestationDigest": { "sha256": "..." } }] }, "runDetails": { "builder": { "id": "..." }, "environment": { "python": "3.12.3", "compiler": "gcc-14.1", "fromager": "0.88.0" } } } }Storage (no
graph.jsonchanges)Keep
graph.jsonunchanged. Store provenance separately:Lightweight integrity/env JSON may be embedded in
.dist-info/(same pattern as SBOM). Full attestations and signatures stay as sidecars to avoid repack-on-sign.Integration points
sources.pybootstrapper.pywheels.pycommands/verify.pyfromager verifywalks topologyNew module:
provenance.py(hashing, environment capture, attestation generation, chain verification). Settings gate viaprovenance.enabledin package settings (mirrors SBOM opt-in pattern).Suggested phases
provenance-index.jsonfromager verifyverify --signaturesPhases 1–3 deliver unsigned attestations and chain verification. Phase 4 adds signature sidecars via a
provenance.signingsetting:.sigfiles; works offline; suited to private indexes and air-gapped builders..sigstore.jsonbundle. The default public Sigstore flow also writes to Rekor, which publishes the signing identity and artifact digest — worth considering when choosing a backend for non-public artifacts.Each phase is independently mergeable; later phases no-op when
provenance.enabled: false.Optional follow-up (lower priority): verify PyPI PEP 740 sdist attestations on download when available (
verify_if_presentmode).Non-goals
graph.jsonschemaHappy to contribute a
docs/proposals/write-up and phased PRs if there is interest.References