Skip to content

ADR-0014: Supply chain and build integrity

  • Status: Accepted
  • Date: 2026-04-12
  • Supersedes:
  • Superseded by:

Context

Trails composes third-party Rust and Python crates at the center of the trust boundary: Oxigraph (ADR-0007), Cedar (ADR-0006), biscuit-auth (ADR-0010), PyO3, oxrdflib, rdflib, pyshacl, JOSE / VC verification libraries. A malicious or careless update to any of these flows straight into the kernel — signed wheels don't protect against a tampered dependency in the build chain. The research/03 threat survey and the security review (§M-3 "Supply chain") both flag this: none of the existing ADRs commit to dependency pinning, audit cadence, reproducible builds, or release-signing discipline.

The framework ships wheels that execute in regulated-industry environments (the differentiator pitched in ADR-0013). "We use Rust" is not by itself a supply-chain story.

Options considered:

  1. No policy; rely on Cargo + pip defaults. Status-quo for most OSS projects. Dependabot PRs, community vetting. Acceptable for libraries where consumers own their own pinning. Unacceptable for a framework positioned as a trust substrate.
  2. Vendor everything. Fully vendored vendor/ trees, offline builds. Maximum control. Prohibitive maintenance cost; community contributions become painful.
  3. Pin + audit + sign, no vendoring. Exact-version pins in lockfiles, cargo audit / pip-audit in CI, signed git tags, reproducible wheel builds, per-release audit notes for sensitive deps.
  4. Third-party build service (Sigstore, rules_oci). Strongest provenance. Heavyweight for a solo-maintained framework at M0/M1 scope; revisit at v1.0.

Decision

Option 3: pin + audit + sign, with a per-release audit note for sensitive crates. Concretely:

Rust

  • Every kernel crate pins dependencies to exact minor versions in Cargo.toml (e.g., oxigraph = "=0.4.5", not ^0.4). Patch-level floats only where the upstream semver discipline is trusted.
  • Cargo.lock is committed in every kernel repo (including library crates, contrary to Rust-community default).
  • cargo audit runs on every PR via CI; new advisories block merge until triaged.
  • cargo deny enforces a license allowlist (Apache-2.0, MIT, BSD-⅔, ISC, MPL-2.0) and a crate-source allowlist (crates.io + kernel's git submodules only; no ad-hoc git URLs).

Python

  • uv.lock is committed for every Python package in the repo.
  • pip-audit runs on every PR.
  • License checks via pip-licenses with the same allowlist as Rust.

Toolchains

  • Rust version pinned in rust-toolchain.toml at the repo root (channel, components, targets); current pin is 1.85.0 (bumped from 1.82.0 for pyo3 0.28.3 compatibility). CI fails if the file is absent or modified without a PR that updates the pin deliberately.
  • Python version pinned in .python-version (used by uv, pyenv).
  • maturin version pinned in pyproject.toml build-system requires.

Reproducible wheel builds

  • Wheels built with maturin build --release --locked.
  • Linux wheels built in a pinned manylinux image (e.g., quay.io/pypa/manylinux_2_28_x86_64:2024-01-01-abc123, not :latest). The image digest is recorded in the release notes.
  • macOS and Windows wheel builds use pinned runs-on: images (macos-14, windows-2022); no floating runner tags.

Audit notes

  • Third-party RDF and policy-evaluation crates require a per-release audit note in CHANGELOG.md listing the version delta since the previous release and a one-line risk assessment. Sensitive crates for this purpose: oxigraph, oxrdf, oxrdflib, cedar-policy, biscuit-auth, pyshacl, rdflib, JOSE / COSE libraries, any DID resolver crate.

Release signing

  • Framework releases are tagged with git tag -s using the repo owner's SSH signing key (same pattern as the workspace's id_ed25519_dev). Unsigned tags are rejected by CI's release workflow.
  • Wheel artifacts attached to GitHub releases include a .intoto.jsonl provenance statement (GitHub's built-in attestation) so consumers can verify the build workflow that produced each wheel.
  • Publishing to PyPI or any registry requires explicit human approval via workflow dispatch, not automatic on tag. (Policy, not a technical control — but the workflow fails closed by default.)

Consequences

Positive

  • Supply-chain threat class from research/03 and §Supply-chain of the security review is addressed as specification, not implementation discretion.
  • Deterministic builds: a given tag reproduces byte-identical wheels on Linux (and close-to-identical elsewhere).
  • License risk bounded: a disallowed license cannot land silently.
  • Advisories surface at PR time, before release cadence is disrupted.
  • Release-signing discipline inherits from the workspace's existing SSH signing key — no new key ceremony.

Negative

  • Maintenance cost. Every dependency bump requires an audit-note entry. Mitigated by scripting: scripts/release-audit-notes.sh diffs Cargo.lock + uv.lock between tags and scaffolds the CHANGELOG stub.
  • Slower response to upstream fixes. Exact pinning delays picking up patches until a Trails release. Mitigated by Dependabot + weekly security-only dependency sweep that is allowed to land without a feature release.
  • CI footprint grows. cargo audit, cargo deny, pip-audit, pip-licenses, image-digest verification — each is a CI step. Budget ~2 minutes added per PR.
  • Committed Cargo.lock in library crates is mildly contrarian; some downstream consumers will ignore it. That is fine — the lock exists for our builds, not theirs.

Non-consequences

  • Does not mandate SBOM emission at M1 (deferred to v1.0 alongside Sigstore/rules_oci evaluation).
  • Does not mandate cargo vet audit chains (heavyweight for M1 solo-maintained scope; revisit when the contributor base grows).
  • Does not change kernel/surface split (ADR-0001) or any runtime decision — this is entirely a build-time and release-time policy.

Revisit conditions

  • If Sigstore-style keyless signing or GitHub attestations become the consumer-side default for Python/Rust ecosystems, fold attestation verification into the framework's own install-time checks and remove the manual audit-note burden where automation covers it.
  • If the kernel-crate count grows beyond a solo maintainer's ability to track advisories, introduce cargo vet trust delegations to peer projects (Pelletier / Oxigraph, Cedar maintainers) before scaling the audit-note discipline.
  • If a third-party RDF or policy crate ships a supply-chain incident, promote the weekly sweep to per-release blocking and add mandatory staging-deployment soak before publish.