Python: Verifizierbare Abhängigkeitsbeschreibungen und Build-Umgebungsdetails als separate Teile eines reproduzierbaren Release-Vertrags speichern
Runtime-Abhängigkeiten in [project.dependencies] und Build-Anforderungen in [build-system.requires] der pyproject.toml speichern und exakte Versionen mit sha256-Hashes in einer separaten Requirements-Datei fixieren, um einen auf Byte-Ebene verifizierbaren Release-Vertrag zu erstellen.
Auf dieser Seite
Kerngedanke
Definieren Sie Runtime-Abhängigkeiten in [project.dependencies] und Build-Zeit-Anforderungen in [build-system.requires] innerhalb der pyproject.toml. Fixieren Sie exakte Versionen und fügen Sie --hash=sha256-Einträge in einer separaten Requirements-Datei hinzu, um einen verifizierbaren, auf Byte-Ebene reproduzierbaren Release-Vertrag zu erstellen.
Trennung von Runtime-Abhängigkeiten und Build-System-Anforderungen in pyproject.toml
Die pyproject.toml-Spezifikation definiert zwei unterschiedliche Abhängigkeitsschichten. Die Tabelle [build-system] erklärt die Abhängigkeiten auf Python-Ebene, die benötigt werden, um das Build-System des Projekts, wie etwa setuptools oder flit, auszuführen. Der Schlüssel requires ist für diese Tabelle obligatorisch und listet die Pakete für die Build-Zeit auf. Die Tabelle [project], die durch PEP 621 eingeführt wurde, enthält den Schlüssel dependencies, der die Pakete auflistet, die Benutzer zum Zeitpunkt der Installation benötigen (Runtime-Abhängigkeiten). Diese werden in die Requires-Dist-Einträge in den Paketmetadaten übertragen.
Die Trennung dieser Schichten ist für einen reproduzierbaren Release-Vertrag unerlässlich. Die Build-Umgebung muss reproduzierbar sein, um die gleichen Distributions-Artefakte zu erzeugen, während die Runtime-Umgebung reproduzierbar sein muss, um ein konsistentes Verhalten nach der Installation zu gewährleisten. Durch die Deklaration beider Schichten in der pyproject.toml machen Sie den Vertrag explizit und verifizierbar: Jeder kann prüfen, welche Tools für den Build und welche Bibliotheken für den Betrieb benötigt werden. Die Spezifikation weist darauf hin, dass Tools nicht voraussetzen sollten, dass die Tabelle [build-system] existiert; falls sie fehlt, werden Standardwerte verwendet, die variieren können. Daher entfernt die explizite Angabe beider Schichten jegliche Mehrdeutigkeit.
Warum Version-Pinning und Hash-Prüfung unterschiedliche Vertrauensgrenzen adressieren
Das Fixieren von Paketversionen mit == in einer Requirements-Datei schützt vor Versionsänderungen upstream, die Bugs oder Inkompatibilitäten einführen könnten. Dennoch vertraut man weiterhin dem Paketindex (wie PyPI) und der TLS-Zertifikatskette. Ein Angreifer, der den Index kompromittiert oder einen Man-in-the-Middle-Angriff durchführt, könnte ein bösartiges Paket mit derselben Versionsnummer ausliefern.
Der Hash-Prüfmodus, aktiviert durch --require-hashes und --hash=sha256:... Einträge, verifiziert das heruntergeladene Artefakt Byte für Byte gegen einen bekannten Digest. Dies schützt vor Index-Kompromittierungen, Angriffen auf die Zertifikatskette und stillen Re-Uploads eines Pakets ohne Versionssprung (bei Indizes, die dies erlauben). Die pip-Dokumentation gibt an, dass die Hash-Prüfung eine arbeitssparende Alternative zum Betrieb eines privaten Index-Servers ist: Sie macht das Hochladen von Paketen, die Pflege von ACLs und die Führung eines Audit-Trails überflüssig, während ein VCS diesen Trail für die Requirements-Datei bereitstellt. Der Preis dafür sind die Wartungskosten: Jedes Mal, wenn sich eine Abhängigkeitsversion ändert, müssen die Hashes neu generiert und überprüft werden. Dieser Ansatz eignet sich besonders für automatisierte Server-Deployments, bei denen die Umgebung kontrolliert ist.
Konkretes Beispiel: Ein Release-Vertrag mit beiden Schichten
Eine minimale pyproject.toml deklariert build-system.requires (z. B. setuptools und wheel) sowie project.dependencies (z. B. requests und pydantic). Eine begleitende Datei requirements-pinned.txt, generiert durch pip-compile oder pip freeze, fixiert jede transitive Abhängigkeit mit exakten Versionen und sha256-Hashes. Die beiden Dateien bilden zusammen den verifizierbaren Vertrag.
Zur Installation führen Sie pip install --require-hashes -r requirements-pinned.txt aus. Dies erzwingt, dass jedes heruntergeladene Paket mit dem aufgezeichneten Hash übereinstimmt. Die Anforderungen an das Build-System werden verwendet, wenn das Projekt aus dem Quellcode gebaut wird; wenn nur ein vorgefertigtes Wheel installiert wird, sind sie zur Installationszeit nicht erforderlich, bleiben aber Teil des Vertrags für jeden, der den Build reproduzieren muss.
Die Liste build-system.requires stellt sicher, dass die Build-Tooling reproduzierbar ist; project.dependencies erklärt, was die installierte Anwendung benötigt. Die begleitende Requirements-Datei fixiert exakte Versionen und liefert Hashes für alle transitiven Abhängigkeiten, was in Kombination mit pip install --require-hashes -r requirements-pinned.txt den verifizierbaren Release-Vertrag bildet. Die gezeigten Hashes sind gekürzte Platzhalter; echte Hashes müssen mit pip-compile oder pip freeze generiert und dann manuell verifiziert oder in der Versionsverwaltung gespeichert werden. Die Trennung der Zuständigkeiten bedeutet, dass die Aktualisierung einer Runtime-Abhängigkeit keine Änderung der Build-System-Spezifikation erfordert und umgekehrt. Die pyproject.toml allein erzwingt keine Reproduzierbarkeit; erst die Kombination mit der fixierten, gehashten Requirements-Datei schafft den Vertrag. Die Tabelle [build-system] ist für die Reproduzierbarkeit obligatorisch, da sie die Build-Tools fixiert; ihr Fehlen würde dazu führen, dass man sich auf toolspezifische Standardwerte verlassen muss, die zwischen Umgebungen variieren und so den Vertrag brechen.
# pyproject.toml
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "myapp"
version = "1.0.0"
dependencies = ["requests>=2.31", "pydantic>=2.5"]
# requirements-pinned.txt (generated by pip-compile or pip freeze)
requests==2.31.0 --hash=sha256:58cd2187c8b806c9970b3f8a8e7c1d2e4f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1
pydantic==2.5.0 --hash=sha256:7f4f1b1b0d8d4e8c9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2
urllib3==2.1.0 --hash=sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
annotated-types==0.6.0 --hash=sha256:b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3
pydantic-core==2.14.1 --hash=sha256:c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4
typing-extensions==4.9.0 --hash=sha256:d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5
# Install command enforcing hash verification:
pip install --require-hashes -r requirements-pinned.txtGrenzen dieses Ansatzes
Wheelhouse-Bundles, die kompilierte Wheels enthalten, sind OS- und architekturspezifisch und somit nicht portabel zwischen verschiedenen Maschinen. Die Hash-Prüfung garantiert nicht die Verfügbarkeit von Paketen, wie es ein privater Index oder eine vendored Library tun würde; wenn ein Paket aus dem Index entfernt wird, schlägt die Installation fehl, selbst wenn der Hash bekannt ist. Die Tabelle [build-system] ist optional; wenn sie fehlt, greifen Tools auf Standardwerte zurück, die je nach Umgebung variieren und die Reproduzierbarkeit beeinträchtigen können. Das Fixieren mit Hashes vertraut immer noch der initialen Auflösung und schützt nicht vor bösartigem Code innerhalb der fixierten Version selbst. Schließlich verursacht die Pflege von Hashes einen Mehraufwand: Jedes Update einer Abhängigkeit erfordert die Neugenerierung und Überprüfung der Hash-Einträge.
Anwendungsbedingungen
- Verwendet Ihr Projekt eine pyproject.toml mit sowohl [build-system]- als auch [project]-Tabellen?
- Sind Sie bereit, Hash-Einträge jedes Mal neu zu generieren, wenn sich eine Abhängigkeitsversion ändert?
- Ist die Zielumgebung homogen genug, dass kompilierte Wheels (falls vorhanden) portabel sind, oder können Sie diese auf jeder Maschine neu bauen?
- Haben Sie einen Prozess zur Generierung und Überprüfung der fixierten Requirements-Datei (z. B. pip-compile oder pip freeze)?
- Können Sie den Wartungsaufwand für die Aktualisierung von Hashes bei Sicherheits-Patches und Upgrades akzeptieren?
- Ist das Fehlen eines privaten Index oder einer vendored Library für Ihre Verfügbarkeitsanforderungen akzeptabel?
Geltungsbereich
Wheelhouse-Bundles mit kompilierten Wheels sind OS- und architekturspezifisch und daher nicht portabel. Die Hash-Prüfung garantiert nicht die Paketverfügbarkeit, wie es ein privater Index oder eine vendored Library tun würde. Die Tabelle [build-system] ist optional; fehlt sie, nutzen Tools Standardwerte, die je nach Umgebung variieren können. Pinning mit Hashes vertraut der initialen Auflösung und schützt nicht vor bösartigem Code in der fixierten Version selbst.