ADR-1305: Hash-locked Python dependency installs and OpenSSF supply-chain hardening¶
- Status: Accepted (decision 8 superseded by ADR-1356)
- Date: 2026-09-23
- Deciders: Lusoris
- Tags:
security,dependencies,supply-chain,python,ci
Amendment (2026-09-29, ADR-1356): The
slsa-github-generatortag-pin exception (Context item 5, decision 8) no longer exists. The organisation'ssha_pinning_requiredpolicy rejects the generator's own tag-referenced sub-actions, so release provenance now comes from the SHA-pinnedactions/attest-build-provenanceand every workflow action is SHA-pinned.
Context¶
Python dependencies across the repository were previously installed via ad-hoc pip install commands across GitHub Actions workflows, multi-stage Dockerfiles, workstation setup scripts (scripts/setup/), and Makefile targets. While top-level manifests specified version bounds, dependencies were not cryptographically pinned with artifact hashes (--require-hashes).
This left several critical supply-chain vulnerabilities and portability gaps:
- Supply-chain exposure: Unhashed installs allowed PyPI to serve mutated wheels or compromised dependencies at run time, failing OpenSSF Scorecard
Pinned-Dependenciesrequirements and HISS-11 hermetic supply chain invariants. - PEP 517 build-isolation failures with hashed sdists: Documentation build tools (e.g.
mkdocs-minify-plugin) depend on pure source distributions (csscompressor,htmlmin2,jsmin). Under--require-hashes, pip's default build isolation creates an isolated temporary environment and attempts to fetch unhashed build backends (setuptools,wheel) from PyPI, aborting the build. - Python 3.12 portability: Running
dev-linters.incompilation locked against Python 3.14 omitted environment markers and packages required on supported workstations running Python 3.12 (such astomli). - Makefile drift: Local developer targets (
$(VENV_PIP),$(MESON),$(NINJA),lint-tools,cythonize-deps) executed unhashed pip installs, allowing local development environments to diverge from CI. - SLSA GitHub generator constraints: OpenSSF Scorecard recommends commit SHA pinning for GitHub Actions; however,
slsa-framework/slsa-github-generatorexplicitly requires an exact@vX.Y.Ztag for its trusted builder verification (slsa-verifier#12; ADR-1128). Any mechanical conversion to SHAs breaks provenance verification.
Decision¶
We enforce hermetic, cryptographically verified Python installations across the entire repository using manifest-driven hash locks.
- Manifest-driven lock authority (
requirements/locks/manifest.json): - All lock files are generated deterministically using a reviewed, pinned
uvbinary (uv_version: 0.12.18). - Every lock file includes header metadata containing the generator version, the inputs list, and a SHA-256 digest of input file contents and compiler arguments.
- Manifest output, input, and alias-consumer paths are local repository-relative paths under both POSIX and Windows semantics; absolute, traversal, remote, and whitespace-padded values are invalid.
- Offline verification is performed via:
scripts/ci/check_python_dependency_locks.py check. -
Explicit network refresh is executed via:
scripts/ci/check_python_dependency_locks.py write(exposed asmake python-locks-write). -
Strict install command policy across all surfaces:
- All executable pip invocations in
.github/workflows/,Dockerfile*,scripts/setup/*.sh, andMakefilemust specify--require-hashes -r <lockfile>. - Local wheels must explicitly pass
--no-deps. - Local editable and source tree installs must explicitly pass both
--no-depsand--no-build-isolation. - The checker scans all shell scripts, workflows, Dockerfiles, Makefiles, and literal
session.install(...)calls innoxfile.pyfor non-compliant invocations. Plain, annotated, and literalgetattraliases do not escape the Nox scan. Dynamic Nox install arguments fail closed. -
A requirement target is accepted only when it exactly matches a manifest output or an explicit
install_aliasesentry. Basename and suffix matching are forbidden, so/tmp/untrusted/requirements/locks/build.txtcannot impersonate the reviewed lock. -
PEP 517 build-isolation dependencies and
--no-build-isolation: docs/requirements.txtexplicitly includessetuptools>=77.0.1andwheel>=0.45.1so they are hashed intodocs/requirements-lock.txt.requirements/locks/package-build.in/.txtprovides the complete hash-locked backend set (build,hatchling,editables,setuptools,wheel,packaging,Cython,numpy,scipy) required for offline, isolated-build-free editable installs and sdist builds (such aslibsvm-official).-
Hashed sdist consumers (
docs.ymlandlint-and-format.yml) invoke:pip install --no-build-isolation --require-hashes -r docs/requirements-lock.txt. -
Python 3.12 portability for developer linters:
-
requirements/locks/manifest.jsonconfiguresdev-linters.txtto compile with--universal --python-version 3.12 --generate-hashes, resolving markers and fallbacks for both Python 3.12 and 3.14. -
Hash-locked Nox orchestration:
- Nox itself is pinned in
requirements/locks/nox.inand installed from the generatedrequirements/locks/nox.txtwith--require-hashes. - Every fork-local package session installs a manifest-owned development lock, then installs only its local source with
--no-deps --no-build-isolation. Each development lock includes the package's PEP 517 backend inputs. -
The ROI-score and ensemble-kit sessions request Python 3.12 because their package metadata excludes Python 3.14. Nox may obtain the supported standalone interpreter when it is absent locally.
-
Executable Makefile pip installs under checker:
- Makefile venv bootstrap targets (
$(VENV_PIP),$(MESON),$(NINJA)) install fromrequirements/locks/build.txt. lint-toolsinstalls fromrequirements/locks/dev-linters.txt.cythonize-depsinstalls fromrequirements/locks/cythonize.txt.- Phony Make targets
python-locks-checkandpython-locks-writeprovide local entrypoints. -
python-locks-checkis integrated intomake lint. -
Tooling and CI impact integration:
- Pre-commit registers
check-python-dependency-locksandtest-python-dependency-locks. - CI impact planner (
.github/ci-impact.json) adds"requirements/"prefix and routes requirements changes to thepythontesting lane. - Dependency PR classifier (
scripts/ci/classify-dependency-pr.sh) allows the explicit dependency surface, includingrequirements/*andrequirements*.in, for automated bot PRs. Generic basename-wide*.inandmanifest.jsonexemptions are forbidden outside their owned subtree. -
Renovate (
renovate.json) monitorsrequirements/locks/*.in,docs/requirements.txt, and ignores compiled*.txtand*-lock.txt. -
Preservation of SLSA tag requirement:
-
Workflows calling
slsa-framework/slsa-github-generatorretain exact semantic tags (@v...), which are exempted from SHA-only rules to preserve SLSA cryptographic attestation. -
Installer tooling exclusion from bootstrap build locks:
requirements/locks/build.inpins build dependencies (meson,ninja) only.-
Installer tooling (
pip) is excluded from build locks to prevent pip from attempting to uninstall runner- or Debian-managed pip packages that lack RECORD metadata. -
Truthful package-wide license review for
text-unidecode:actions/dependency-review-actionevaluates SPDX license expressions underdeny-licenses: GPL-3.0, AGPL-3.0.python-slugifybrings intext-unidecode, dual-licensed underArtistic-1.0-Perl OR GPL-1.0-only OR GPL-2.0-or-later, consumed underArtistic-1.0-Perl.- GitHub Dependency Review matches PURLs package-wide (ignoring versions);
allow-dependencies-licenses: pkg:pypi/text-unidecodeexplicitly and truthfully allows the package using exact package-wide purl syntax.
-
Container Python virtual environment isolation:
- Root
Dockerfileinstalls locked Python packages into an isolated virtual environment (/opt/vmaf-venv), avoiding conflicts with Debian system packages (such aspython3-packaging) without--break-system-packages.
- Root
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
pip-compile (pip-tools) | Standard tooling | Slow resolution; platform-specific wheel hashes unless run on every OS | uv pip compile generates multi-platform universal hashes deterministically and orders of magnitude faster. |
| Poetry / Pipenv locks | Rich metadata | Non-standard format; requires third-party tool at install time rather than native pip --require-hashes | Incompatible with minimal Docker containers and air-gapped CI environments. |
| Basename or suffix recognition for copied locks | No manifest aliases to maintain | Any attacker-controlled path ending in a known lock name is misclassified as trusted | Exact manifest-owned aliases bound to explicit consumer paths and context preserve copied-container paths without weakening lock identity. |
| One repository-wide Nox development lock | Fewer generated files | Co-resolves unrelated heavy packages and ignores incompatible requires-python ranges | Per-session locks preserve package isolation and use the interpreter version each package supports. |
| Blanket SHA pinning for all Actions | 100% Scorecard check | Breaks SLSA builder attestation (slsa-verifier rejects SHA refs) | SLSA builder contract requires tag refs; overriding tag breaks release provenance. |
Consequences¶
- Positive:
- Full cryptographic tamper-evidence on every Python package installed in CI, containers, and developer environments.
- Nox bootstrap and per-package sessions obey the same manifest authority as CI and container installs.
- Offline lint and pre-commit checks prevent unhashed or unpinned dependencies from entering the repository.
- Reproducible builds across Python 3.12, 3.13, and 3.14.
- Scorecard supply-chain hardening without breaking SLSA attestation.
- Negative:
- Updating a Python dependency requires running
make python-locks-writeto update lock files and input digests. - Neutral / follow-ups:
- Pinned
uvgenerator version inmanifest.jsonshould be bumped periodically in lockstep with toolchain updates.
References¶
- req: "finish the pre-RC1 Python dependency hash-lock and OpenSSF/CII hardening in this isolated worktree" — direct operator mandate to enforce hermetic, cryptographically verified Python dependency installs repository-wide.
- OpenSSF Best Practices Badge Project 14549: VMAFx.
- ADR-1126 — Single import-sorting canon under Ruff (no standalone isort).
- ADR-1128 — SLSA Provenance and Builder Verification.
- ADR-1152 — Dependency PR classification.
- OpenSSF Scorecard Pinned-Dependencies Documentation.
- slsa-github-generator Issue #12: Workflow reference by tag.
- PEP 517 — A build-system independent format for source trees.
- PEP 660 — Editable installs for pyproject.toml.