Summary
fromager downloads sdists and wheels without computing or verifying any cryptographic hash. A compromised upstream can replace artifact bytes between runs and fromager will accept them silently. This is Phase 1 of #1238.
Current state
download_url() streams bytes to disk with no hashlib computation
#sha256= URL fragments from PyPI indexes are carried in URL strings but never verified
pypi_simple.DistributionPackage.digests is available but never read
Candidate, DependencyNode, and build-order entries have no digest field
Proposed changes
- Compute SHA-256 during download — add streaming hash in
download_url(), the single chokepoint for all HTTP downloads; return the digest alongside the file path
- Verify URL fragment hashes — extract
#sha256= from download URLs and hard-fail on mismatch with computed digest
- Read index digests — use
dp.digests from pypi_simple as a cross-check when creating Candidate objects
- Store digests in data structures — add optional
digest field to Candidate, DependencyNode/DependencyNodeDict, and build-order entries (backward-compatible via NotRequired/defaults)
- Write
provenance-index.json — aggregate per-package digests during finalize(), alongside build-order.json
- Re-download verification — when a digest is already recorded, verify new downloads produce identical bytes (trust-on-first-use)
Summary
fromager downloads sdists and wheels without computing or verifying any cryptographic hash. A compromised upstream can replace artifact bytes between runs and fromager will accept them silently. This is Phase 1 of #1238.
Current state
download_url()streams bytes to disk with nohashlibcomputation#sha256=URL fragments from PyPI indexes are carried in URL strings but never verifiedpypi_simple.DistributionPackage.digestsis available but never readCandidate,DependencyNode, and build-order entries have no digest fieldProposed changes
download_url(), the single chokepoint for all HTTP downloads; return the digest alongside the file path#sha256=from download URLs and hard-fail on mismatch with computed digestdp.digestsfrompypi_simpleas a cross-check when creatingCandidateobjectsdigestfield toCandidate,DependencyNode/DependencyNodeDict, and build-order entries (backward-compatible viaNotRequired/defaults)provenance-index.json— aggregate per-package digests duringfinalize(), alongsidebuild-order.json