Skip to content

feat(schema): declare build_provenance verification depth (#50) - #173

Merged
imran-siddique merged 10 commits into
mainfrom
spec/provenance-depth-50
Aug 16, 2026
Merged

feat(schema): declare build_provenance verification depth (#50)#173
imran-siddique merged 10 commits into
mainfrom
spec/provenance-depth-50

Conversation

@imran-siddique

@imran-siddique imran-siddique commented Aug 14, 2026

Copy link
Copy Markdown
Member

Implements Option B selected in #50 and incorporates the community verification refinements from #177.

What lands

Field On Meaning
provenance_depth build_provenance how far down the supply chain the issuer claims to have walked
provenance_depth_verified appraisal how far the verifier actually walked

Both fields are optional and use the wire vocabulary surface | builder | transitive. Omitting provenance_depth means surface, preserving the meaning and validity of records issued before the field existed.

Resolved design decisions

  • The wire vocabulary remains surface | builder | transitive; the longer names in the informative guide are descriptive aliases only.
  • Supply-chain depth remains separate from the action-receipt axis because the two do not nest.
  • Verifier requirements now live in normative specification section 3.3.1; the verification guide is explanatory.
  • Missing or unreachable evidence permits an explicit downgrade that names the unresolved evidence. Evidence that resolves and contradicts the record fails appraisal and cannot be hidden by downgrading.
  • transitive is an effort floor until dependency discovery and evidence resolution are standardized. A portable coverage URI remains follow-up work.

Community integration

Includes #177's distinction between unresolvable and contradictory evidence, wire-value vector names, and the poisoned-dependency ordering regression. Integration fixes preserve rule effects, use current wire values, and place RFC 2119 requirements in the normative specification.

Verification

  • python -m pytest -q: 472 passed, 1 skipped
  • git diff --check: clean
  • Hosted CI and CodeQL are the independent merge gate.

Additive and backward-compatible.

Closes #50.

piiiico and others added 4 commits August 12, 2026 20:23
…not assure

Section 3.3 step 7 is one sentence and three stopping points satisfy it. Naming
what each one checks hides the decision: Surface is the weakest and the cheapest
to implement, so a deployment picks it by default and never records that "trusted
builder" is a claim nobody checked. This page states the unknowns each depth
accepts instead.

builder is optional in schema/trace-claim.json and in the reference model, so a
schema-valid record can carry a digest and a level and nothing else. The
build_provenance table in docs/schema.md marked it required; corrected here, and
tests/test_build_provenance_depth_doc.py pins that table against the schema so the
same drift fails CI next time.

Informative, and the test asserts that too: no uppercase RFC 2119 keyword. Nothing
here anticipates the outcome of #50, which is take-internal.

Signed-off-by: piiiico <pico@amdal.dev>
Section 3.3 step 7 requires that "SLSA provenance resolves to a trusted
builder" without saying how far a verifier walks, leaving three stopping
points equally conformant. Two conformant verifiers could reach opposite
conclusions on the same record with no way to say why, which breaks the
federation promise in section 1 at exactly the seam it claims to close.

Implements Option B from #50: name the depth, let deployment profiles pick
the floor.

- build_provenance.provenance_depth: what the issuer claims it walked
- appraisal.provenance_depth_verified: what the verifier actually ran

The pair is deliberate. A verifier that cannot reach the claimed depth
downgrades and records the lower value rather than failing the record, and
records a depth no higher than it executed.

Both fields are optional and a record omitting provenance_depth is read as
surface, so every record issued before this field existed keeps its meaning.

Signed-off-by: Imran Siddique <imran.siddique@opaque.co>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@piiiico

piiiico commented Aug 14, 2026

Copy link
Copy Markdown
Contributor
  1. Keep the enum. The note's names were always labelled descriptive. What the note doesn't cover: examples/build-provenance-depth/*.json carries expected.surface / expected.builder_chain / expected.dependency_chain as machine-read keys, and DEPTHS at tests/test_build_provenance_depth_vectors.py:48 hard-codes the same three. Merge this and the repo holds three vocabularies, only one of them prose. That directory's README pre-committed to renaming once build_provenance: define verification depth (Surface / Builder-chain / Dependency-chain) before v1.0 #50 resolved. It has. I wrote that set, so say the word and the keys move onto the wire values.

  2. Prose, but the two rules aren't symmetric. "Never record a higher depth than executed" can't be expressed in JSON Schema at all: the document is byte-identical whether the verifier walked the chain or lied about it. "Record the verified depth whenever build_provenance is appraised" can be an if-then, and that's the reason not to make it one. It would invalidate every existing record carrying an appraisal without the new field, which is the retroactive rejection the rest of the PR avoids.

  3. Out of scope, agreed. Worth recording before it's lost: until resolution is specified, two verifiers can both honestly record transitive over different material sets. builder has no such gap, because provenance_uri names its own evidence. So transitive is a floor on effort, not yet a comparable claim.

One thing outside the three. The downgrade rule collides with the vectors merged in #166. attestation_unresolvable is a builder_chain rule producing reject (control mutation with an unresolvable provenance_uri), and provenance_uri_missing has the same shape. docs/verification.md now says that condition may downgrade to surface and MUST then be recorded as the lower depth rather than a failure of the higher one. A conformance runner can't do both. The line I'd draw: evidence that doesn't resolve downgrades, evidence that resolves and contradicts fails.

That also tightens the transitive row. "Transitive coverage is unavailable for any input" currently reads over 05-dependency-publisher-untrusted: an attestation that exists but is signed under an untrusted issuer would licence a downgrade to builder and an accept. That's the poisoned-input case your "Why depth is recorded rather than assumed" paragraph says transitive rejects.

— Pico, an AI agent (github.com/piiiico), working under Håkon Åmdal

@imran-siddique

Copy link
Copy Markdown
Member Author

Review disposition for the three draft decisions and Pico's follow-up:\n\n1. Keep the wire enum surface | builder | transitive. Rename the machine-readable vector keys and DEPTHS copies to those wire values in this PR so the repository does not ship three vocabularies after #50 resolves.\n2. Keep the verifier rules in prose for backward compatibility; do not make provenance_depth_verified conditionally required in JSON Schema.\n3. Keep the transitive coverage URI out of scope, but state that ransitive is currently a floor on verification effort, not a comparable claim across verifiers until evidence resolution is standardized.\n\nPico's downgrade distinction is blocking before ready-for-review: unavailable/unresolvable evidence may downgrade to the depth actually verified; evidence that resolves and contradicts the claim (including an untrusted dependency publisher) fails. Update docs/verification.md and the #166 vectors/tests so those two outcomes cannot conflict. Keep this draft until that lands.

Review disposition on #173 asked for three things and blocked on a fourth.

Wire vocabulary (1). `examples/build-provenance-depth/*.json` carried
`expected.surface` / `expected.builder_chain` / `expected.dependency_chain` as
machine-read keys and `DEPTHS` hard-coded the same three, so merging #173 would
have left the repository holding three vocabularies for one enum. Both copies now
use the wire values `surface` | `builder` | `transitive`.

Verifier rules stay in prose (2). Nothing here makes
`provenance_depth_verified` conditionally required in JSON Schema.

Transitive is a floor on effort (3). Recorded in verification.md: until evidence
resolution is standardized, two verifiers can both honestly record `transitive`
over different material sets, so the value is not comparable across verifiers.

The blocking issue. The downgrade rule as drafted collided with the vectors
merged in #166: `attestation_unresolvable` and `provenance_uri_missing` are
`builder` rules producing `reject`, while the docs said that condition may
downgrade to `surface` and be recorded as the lower depth. A conformance runner
cannot do both. The line drawn here is the one from the review:

- Evidence that does not resolve leaves a check unrun. The verifier caps
  `provenance_depth_verified` at the depth below and names the missing evidence.
  Not a finding against the record.
- Evidence that resolves and contradicts the record fails the appraisal, and a
  verifier may not downgrade to escape it. Downgrading there would record a
  narrower claim that is true while suppressing a wider one that is false.

That reclassifies `dependency_publisher_untrusted` as a failure, which closes the
second half of the collision: `05-dependency-publisher-untrusted` is the poisoned
input the "Why depth is recorded rather than assumed" paragraph says transitive
rejects, and "transitive coverage is unavailable for any input" had been licensing
a downgrade-and-accept over exactly it.

Vectors 04 and 06 move the other way — an unattested input and an attestation
declaring no inputs are both unresolved evidence — so they now separate the
builder/transitive boundary without rejecting anything: the deeper verifier
records `builder` and says what it could not fetch, the shallower one records
`builder` with nothing to report. Separation is therefore defined over everything
the verifier reports, not over rejections alone.

Three invariants added, all bidirectionally checked: a verifier never records a
depth deeper than it attempted, a downgrade always names the evidence that caused
it, and the floor depth has no downgrading rule. The first is the rule
verification.md has to state in prose because a record is byte-identical whether
the verifier walked the chain or merely says it did — no JSON Schema can hold it,
but a conformance runner can hold it against its own output.

Verified: 410 passed, 1 skipped; ruff check src tests scripts clean. Reverting
each classification turns the suite red — untrusted-publisher as a downgrade
fails 2, unresolvable-attestation as a failure fails 1, and a verified_depth that
ignores the cap fails 6.

Signed-off-by: piiiico <pico@amdal.dev>
The filenames and each fixture's own `name` field still said
`builder-chain-accepts`, which is the third copy of the vocabulary the previous
commit collapsed — the two that are read by code are done, and this is the one a
reader sees first.

Separate commit because it is the only part of this branch that moves paths.
Drop it if the stable filenames from #166 are worth more than the last copy of
the old naming; nothing in the previous commit depends on it.

Signed-off-by: piiiico <pico@amdal.dev>
This branch files `dependency_attestation_missing` under DOWNGRADES, which takes
04 out of the set of vectors that reject at the builder -> transitive boundary.
05 is then the only vector left carrying it, and 05 still placed its defective
input last: `telemetry@0.4.7`, whose attestation is signed under an issuer
outside `trusted_publisher_issuers`, sat at index 2 of three.

A verifier that walks `resolvedDependencies` and stops after the first entry
therefore accepted 05 while still rejecting 06, whose list is absent — and so
presented as a `transitive` verifier having read one dependency of three. That
is the defect the boundary exists to catch, and separating the effects is what
removed the second vector guarding it.

#169 moved 04's unattested dependency to the front for exactly this reason. The
same move on 05 restores the property for the vector that now carries the
boundary alone. Nothing else changes: every rule quantifies over the whole list,
so every rule fires on the same vectors and every expected verdict is unchanged.

Found by LouieLuNZ in review of #177, on a merge of this branch into main that
this branch's own base does not yet contain.

Signed-off-by: piiiico <pico@amdal.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build_provenance: define verification depth (Surface / Builder-chain / Dependency-chain) before v1.0

2 participants