quote-tool.bin has a valid signature. Its provenance names the expected
source revision, dependency lock, workflow digest, and builder. The signature
verifies against the consumer’s trusted fixture key.
The program is still malicious.
In the SEC-01 fixture, the authorized builder changes the artifact after the tests pass and signs the resulting digest. Signature verification succeeds because the statement is authentic. Consumer policy rejects the artifact because its digest does not match the independently reproduced output.
That difference is the useful starting point for software supply-chain security. A control can prove exactly what it claims and still leave another route to the artifact open.
A lockfile constrains dependency resolution. A full commit hash selects build instructions. Provenance binds an artifact to claims about its origin. A signature authenticates those claims. None of them proves by itself that the consumed bytes are the reviewed program.
Begin with authority instead. Trace every identity and system which can change the artifact between review and execution. Then decide what evidence the next boundary must require.
The artifact has more than one author
Source code is only the first mutable input. The fixture retains 12 authority edges across seven stages:
| Stage | Mutable object | Authority which can change it | Consumption evidence |
|---|---|---|---|
| Source | Revision and review record | Contributor, reviewers, repository administrator | Approved revision, ancestry, and independent approvals |
| Dependencies | Lockfile and package bytes | Lockfile updater, package publisher, registry | Expected lock digest and resolved integrity |
| Instructions | Workflow and referenced actions | Workflow author and action publisher | Expected workflow and action digests |
| Build | Artifact bytes | Build platform and job | Builder identity, declared inputs, and artifact digest |
| Provenance | Claims about the build | Provenance issuer or signing identity | Trusted identity, valid signature, and expected fields |
| Publication | Registry object and mutable name | Artifact publisher and registry | Immutable artifact digest and scoped namespace |
| Consumption | Selected bytes | Resolver, verifier, and deployer | Policy approval before extraction or execution |
This is not a list of products to buy. It is a change path. Each row needs an expected identity or digest, a verification point, a failure action, a recovery action, and a residual risk.
The final column matters because evidence must cross boundaries. Verifying an artifact in CI does not protect a consumer which later downloads a mutable tag. Reviewing a workflow does not protect a job which resolves a third-party action from a tag its publisher can move. Protecting a branch does not remove an administrator’s ability to weaken or bypass the rule.
The threat model therefore asks a concrete question at every transition: who can change the object which the next system is about to trust?
A commit identifies content, not how it was approved
The fixture creates a reviewed source revision, two independent approval identities, and an unauthorized revision outside the approved ancestry. The source verifier rejects the unauthorized revision and rejects a review record which reuses one identity for both required approvals.
The commit hash alone cannot make either decision. It identifies content and parents. It does not contain the repository’s protected-branch policy, reviewer independence, administrator authority, or evidence that those rules were enforced when the revision was accepted.
The SLSA 1.2 Source Track separates source roles and explicitly models administrators and two-party review. That model is useful here without claiming that this synthetic repository satisfies a SLSA level. The fixture does not reproduce a hosted source-control service or its administrative boundary.
This distinction changes what provenance must carry. As A Pull Request Is a Review Narrative argues, the revision and the evidence which made it acceptable are different artifacts. Consumer policy must know which repository and revision are allowed. If approval evidence matters, preserve it as evidence about the revision rather than pretending it lives inside the hash.
Frozen dependencies can reproduce a malicious decision
The clean build resolves dependency version 1.0.0. Making 1.1.0 available
does not change that result while the lockfile remains unchanged. This is the
property a frozen install should provide.
The negative control changes the reviewed lockfile to select a malicious
1.1.0. Installation remains repeatable. The same unwanted package is chosen
each time.
npm documents that npm ci uses an existing lockfile and exits when it does
not agree with package.json. Its lockfile documentation
describes the exact dependency tree, resolved locations, and integrity data.
Those are valuable controls. They do not establish whether a publisher was
compromised, whether the package deserves its name, or whether the lockfile
change received meaningful review.
The honest conclusion is narrower: a lockfile can make resolution repeatable and expose an unexpected dependency-graph change. It can also preserve a bad decision perfectly. Publisher trust, update authorization, install-time code, and runtime behavior remain separate questions.
An immutable workflow reference fixes bytes, not intent
Build instructions are executable dependencies. A movable action tag in the fixture resolves to clean code for one build and malicious code for the next. The workflow text has not changed, but the program granted build authority has.
Replacing the tag with an immutable digest closes that particular route. It proves which action bytes were selected. It does not prove those bytes are benign or deserve credentials.
GitHub’s Actions hardening guidance recommends pinning third-party actions to full commit SHAs and granting minimum permissions. Those are two controls because they answer different questions. The pin constrains selection. The permission boundary constrains what selected code can do.
The fixture’s build job can read source and write its own artifact. It receives no unnecessary secret and runs without network access. Those choices reduce the inputs it can fetch and the credentials it can leak. They do not prove that a hosted runner, image publisher, or authorized build program is safe.
An immutable malicious action remains malicious. A reviewed action can become dangerous when a later workflow grants it a publication token, signing identity, secret, or unrestricted egress. Record the selected bytes and the authority they receive.
Provenance answers origin questions, not safety questions
The fixture’s signed provenance binds the artifact name and SHA-256 digest to the exact source revision, workflow digest, dependency-lock digest, builder identity, invocation parameters, and declared environment.
Consumer verification rejects altered artifact bytes, altered provenance, an untrusted key, the wrong source revision, wrong workflow, wrong artifact name, missing provenance, and a revoked builder. These are useful failures. They turn “this came from CI” into fields a verifier can compare against policy.
SLSA provenance describes verifiable information about where, when, and how an artifact was produced. Sigstore’s verification documentation likewise requires the verifier to constrain the expected identity and bind verification to the artifact digest. A valid signature without an expected identity is only half a decision.
The malicious-authorized-builder case preserves the other half of the boundary. Its signature is valid. Its source and workflow claims are well-formed. Cryptography cannot tell that authorized build logic changed the output after tests. A separate expected-output policy rejects the different digest.
Reproducible output is useful evidence in this controlled example because the same declared inputs produce the same tiny binary. It is not universal proof that a remote builder was uncompromised. Many real builds are not reproducible, and two builds can share a compromised input. The independent comparison is a detective control with stated assumptions, not a verdict about intent or vulnerability.
GitHub makes the limitation unusually plain in its artifact-attestation documentation: generating an attestation has no security benefit until it is verified, and an attestation does not guarantee that an artifact is secure. Provenance improves the question from “do we trust CI?” to “do these bytes satisfy the origin and process claims this consumer requires?”
Publication and consumption need separate gates
The fixture publishes the approved artifact under both its immutable digest and a mutable name. CI verifies the approved digest. The mutable name is then moved to a substituted artifact.
A consumer which downloads by name and trusts the earlier CI result accepts a different object from the one CI checked. The fixed consumer selects or receives the artifact, calculates the digest, verifies that digest against the provenance subject, checks the expected source, workflow, and builder, and does so before extraction or execution.
Ordering is part of the control. Verifying an archive after an install hook, package script, or executable payload has run turns prevention into forensic evidence.
The publication token has a different boundary. A wildcard token can overwrite
an unrelated namespace. A token scoped to quote-tool cannot. The narrow token
can still replace artifacts inside quote-tool; least privilege reduces blast
radius rather than preventing compromise.
The same precision belongs in recovery:
- reject substituted bytes and retain the received digest;
- revoke the publisher or builder identity which crossed its boundary;
- restore the approved immutable object;
- rebuild every artifact which may share the compromised input; and
- identify consumers which downloaded before the rejection policy was fixed.
“Rotate credentials” is incomplete unless the affected authority, namespace, artifacts, and consumers are known.
Keep the controls separate enough to disagree
A supply-chain maturity score would hide the most important result in the fixture: several controls pass while the artifact remains unacceptable.
The lockfile is repeatable while selecting malicious bytes. The signature is valid while authenticating malicious output. The immutable action reference is stable while pointing at code which should not receive build authority. The scoped token protects unrelated namespaces while leaving its own namespace at risk.
For each attack path, retain the facts rather than averaging them:
- compromised identity and mutable object;
- precondition and observable change;
- preventive and detective controls;
- verification point and rejection behavior;
- recovery, revocation, or rotation action;
- reduced blast radius; and
- residual risk and reversal condition.
The control should be reconsidered when its authority, credential scope, build platform, registry, consumer population, or recovery objective changes. That is a more useful trigger than crossing an arbitrary aggregate score.
Hosted provenance is not always the cheapest answer
The complete path can be expensive: isolated builders, identity-bound provenance, immutable registry objects, policy engines, revocation, retained evidence, and consumer verification all require ownership.
A small manually built release can be a credible alternative when releases are rare, distribution is narrow, the consumer set is known, and the threat model does not justify hosted provenance infrastructure. Manual does not mean uncontrolled. It still needs a reviewed source revision, named trusted builder, artifact checksum, signing-key custody, publication boundary, and verification before use.
The fixture cannot choose that trade-off for a production system. It contains no real repository policy, hosted builder boundary, dependency ecosystem, release frequency, registry retention, consumer population, or recovery objective. Its keys, repository, registry, and artifact are synthetic.
What it does retain is inspectable: 10 consumer verification cases, 12
authority edges, seven attack paths, two publication permission boundaries, 24
rejected semantic counterfactuals, 11 primary-source responses, and a clean
export of commit 4f2851b9042d9e386c7c8c070babde48efa85776.
Before adding another scanner or badge, draw the path of one artifact which a consumer actually executes. Name who can change it at every seam. Name what the next boundary verifies, what happens on failure, and which risk survives.
The unanswered edge, not the number of installed controls, is where the next supply-chain decision belongs.