Signing Without Hermeticity Is Theater
A signature on an artifact proves exactly one thing: a particular key blessed a particular pile of bytes. It says nothing about where the bytes came from. If the build that produced them could read the network, pull latest tags, or absorb whatever happened to be installed on the runner, then "signed" means "we vouch for something we cannot describe." That's not supply-chain security. It's a notarized shrug.
The claim everyone actually wants to make is stronger: this artifact was produced from that commit, by this recipe, and nothing else. I spent a while building exactly this for my own projects — a small pipeline that could take any binary I'd published and answer "which commit, which toolchain, prove it" — and the exercise made it very clear which parts of provenance are cryptography and which parts are build discipline. The cryptography is the easy 10%.
What the claim actually decomposes into
"This artifact came from that commit" is really three separate claims, and each one fails independently:
- Identity: the artifact you're holding is the one that was attested. Solved by content addressing — refer to artifacts by digest, never by name or tag. This part is genuinely trivial.
- Binding: some trusted party asserts "I ran build B on commit C and it produced digest D." This is what signing gives you: an attestation, signed by the builder, linking inputs to outputs. Also mostly solved — this is in-toto/SLSA-style provenance, and the tooling is fine.
- Determination: commit C plus recipe B actually determines digest D — the build had no other inputs. This is the hard one, nobody can sell it to you, and without it the other two are decoration.
Determination is just hermeticity wearing a suit. If the build can reach the network, then "commit C" is not the input set — the input set is "commit C, plus the state of every remote endpoint the build touched, at the moment it touched them." Nobody attests to that, because nobody can.
The holes, concretely
Here's a Dockerfile that appears in some form in most repos on earth:
FROM python:3.12 # mutable tag — different bytes next month
RUN apt-get update && \
apt-get install -y libfoo # whatever version the mirror serves today
RUN pip install -r requirements.txt # resolved at build time if unpinned
Build this from the same commit on two different days and you can get materially different artifacts — different base image, different system libraries, different transitive Python dependencies. Sign both and the signatures are equally valid, because signatures don't know the builds diverged. The commit never determined the output; it merely suggested it. Every floating reference is a hole in the provenance story:
# each of these replaces "an input I named" with "an input I pinned"
FROM python:3.12@sha256:aa1b3… # digest, not tag
pip install --require-hashes -r requirements.lock
go mod verify # go.sum is a lockfile with teeth
bazel build --experimental_repository_hash_file=…
The pattern is one rule applied everywhere: a build may only fetch what it can name by hash, and the hashes live in the commit. Do that, and "commit C" finally is the complete input set — the network becomes a content-addressed store instead of an oracle, and it can be cut off entirely after the fetch phase. This is exactly why Bazel splits fetching from building and why sandboxed actions get no network: not paranoia, but making determination true by construction.
Verification is a rebuild, not a lookup
Once determination holds, provenance becomes checkable, and this is the payoff that makes the discipline worth it. Anyone can verify the claim by re-running it:
# the only provenance check that means anything
git checkout $COMMIT
./build.sh
sha256sum out/app.tar | diff - attested-digest.txt
If the build is reproducible, verification requires trusting no one — not the CI vendor, not the signing key, not me. Two independent rebuilders agreeing on a digest is a stronger statement than any signature, because keys get stolen and CI gets compromised, but two colluding compromises that produce the same wrong hash is a much taller order. Reproducible-builds.org has run this play for a decade: independent rebuilders as a distributed check on the official binaries. Bit-for-bit reproducibility is the goal, and it's genuinely achievable for most software (pin toolchains, kill timestamps and path leakage, sort everything you emit). Where it's not yet achievable, hermetic-and-attested is the honest fallback: the builder still vouches, but at least it vouches for a closed input set it can enumerate in the attestation.
The theater version
The theater version of supply-chain security does the steps in the wrong order: adopt signing (visible, easy, demo-friendly), generate SBOMs from whatever the non-hermetic build happened to download (a guest list written after the party), and leave the build itself able to fetch arbitrary bytes from the internet at 2 a.m. The result photographs well and proves nothing, because the thing being signed was never a function of the thing being claimed.
The order that works is the unglamorous one: pin everything to digests, close the build's inputs, make output deterministic, then sign — at which point the signature is finally attached to a claim precise enough to be worth making. Hermeticity isn't a prerequisite you check off before doing provenance. It is the provenance. The signature is just the envelope.