Skip to content

Proposal: build provenance chain with per-wheel attestations #1238

Description

@LalatenduMohanty

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:

  1. Records SHA-256 digests of the sdist input and output wheel
  2. Captures build environment metadata (Python version, platform, compiler, fromager version, allowlisted env vars)
  3. Generates an attestation document (in-toto Statement v1 / SLSA Provenance v1 predicate) referencing each build dependency's attestation by digest
  4. Optionally signs attestations
  5. 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:

  1. Source integrity: hash sdists at download; verify PyPI URL #sha256= fragments; fail on mismatch
  2. Bootstrap roots: simplest attestations (source in, wheel out, environment recorded)
  3. 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
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions