Check provenance attestation adversarially (F6-F9) - #154
Conversation
There was a problem hiding this comment.
Sorry @macanderson, you've used your own review budget of 250,000 diff characters for the last 7 days.
You can request another review in 15 hours and 32 minutes by commenting @sourcery-ai review. Upgrade to get a review now.
Reviewer's GuideThis PR moves provenance-attestation verification from implementation self-tests to an adversarial wire-level conformance check, adds five targeted forgery fixtures and tests, carries detached attestations through additive envelope fields, and updates the specification, schema, vectors, and documentation accordingly. Sequence diagram for wire-level provenance attestation verificationsequenceDiagram
participant Provider
participant Connection as RawStdioConnection
participant Probe as AttestationProbe
participant Host
Provider->>Connection: handshake_ack(attester_keys)
Connection->>Probe: handshake provider.name and attester_keys
Probe->>Provider: query(sample_query)
Provider-->>Probe: frames + detached attestations
Probe->>Probe: recompute frame commitment
Probe->>Probe: verify_frame_attestation(provider.name, frame, attestation, key)
alt attestation verifies
Probe-->>Probe: mark frame attested
else attestation fails
Probe->>Host: query_provider(sample_query)
Host-->>Probe: served frames
Probe-->>Probe: retain frame as unattested per F9
end
Flow diagram for adversarial attestation checksflowchart TD
Start[Provider serves frames and attestations] --> Keys{Handshake published attester keys?}
Keys -- No keys and no attestations --> Pass[Pass: nothing to verify]
Keys -- Yes --> Recompute[Recompute commitment from served frame]
Recompute --> Compare{Commitment matches signed commitment?}
Compare -- No --> Mismatch[CommitmentMismatch or MalformedCommitment]
Compare -- Yes --> Signature[Verify signature with published key]
Signature --> Valid{Signature valid?}
Valid -- Yes --> Verified[Frame attested]
Valid -- No --> Bad[BadSignature]
Mismatch --> Retain[Keep frame served as unattested per F9]
Bad --> Retain
Verified --> Complete[Conformance result]
Retain --> Complete
Pass --> Complete
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
2d958f5 to
ad2bb23
Compare
|
SCR-003 DoD check passed — every linked issue's definition of done is fully checked. |
|
DoD ticked on #89 — every item verified, most of them by this PR's own CI run (33292737181):
The one item CI cannot show is the negative direction, since it needs a deliberately broken tree. Deleting |
54bda96 to
532e536
Compare
Rebased onto current
|
SPEC.md §6.5's F6-F9 shipped with their "Verified by" column pointing at `contextgraph_types::attest` — the implementation's own unit tests. Every other guarantee in this protocol earns its credibility from a suite with an adversarial mode behind it, and a guarantee whose only witness is the implementation asserting about itself is the self-attestation §11.1 exists to rule out. The new `attestation` check reads the wire like the §R1, §E1 and §H4 probes do: it takes the attester keys the handshake published, recomputes each served frame's commitment from the frame in hand, and verifies the signature over it in the order §6.5.4 fixes — commitment first, so "the frame moved after signing" is never reported as "the key is wrong". `contextgraph-example-docs` signs what it serves, and grows five `--misbehave` modes, one per forgery the constructions exist to stop: forge-signature wrong key -> BadSignature lift-signature A's signature stapled to B -> CommitmentMismatch truncate-chain a hidden `derivation` link -> CommitmentMismatch swap-content other bytes, signed frame id -> CommitmentMismatch malformed-attestation garbage -> MalformedCommitment `lift-signature` is the one a plausible implementation really does get wrong — sign the bare chain head and every frame citing the same source shares a valid signature. It serves two frames from one backing file so their chain heads and `content_digest`s are equal and only the frame id separates their commitments; `an_attestation_lift_differs_only_in_the_frame_id` asserts that precondition instead of trusting it, because a mode that fails for an unrelated reason proves nothing. Deleting the identity binding from `frame_commitment` makes the mode pass and turns `conformance-red.sh` red, which is the evidence that the check is worth having. `malformed-attestation` also holds F9: the frame stays served, degraded to unattested. The probe asks the reference host that question directly rather than trusting its own bookkeeping, because a host that dropped such frames would hand any peer a denial-of-service primitive. Attestations reach the verifier through two optional envelope members (§6.5.5): `handshake_ack.attester_keys` and `frames.attestations`, detached per F6. Both are additive within contextgraph/1 — a peer that knows nothing about them drops them. §6.5.2 now also pins `provider_id` to the handshake-declared `provider.name`, the only identifier both ends observe. Issue #90 owns the fuller wire treatment (result-set Merkle roots, inclusion proofs); this is the minimum the check cannot run without. A provider that publishes no key and serves no attestation passes: §6.5 makes the construction mandatory and the signing optional, and `conformance-external.sh` treats a skip as a failure. Closes #89
clippy 1.98's `chunks_exact_to_as_chunks` fires on `decode_hex`. The even-length check above the call already rules out a remainder, so `.0` discards nothing, and `&[u8; 2]` indexes without the bounds check a `&[u8]` carries. The sibling fix in `contextgraph-types::attest` landed with #114, so only this site remained. Refs #160
532e536 to
6820cbb
Compare
Not merging this yet — it collides with #138 and #140 in a way that needs a decision, not a rebaseI rebased this branch onto current There are now three types called Separately, this PR adds Full analysis and the proposed resolution: #161. Why I stopped rather than resolved itI could have picked one of the three types and deleted the others, or dropped the envelope field, and got this green. Both are protocol-visible decisions about a wire format, made by hand at the end of a long session, in someone else's design. This repository holds the wire contract to a higher bar than that, and a wrong choice here is expensive to undo once it is on What is actually finished hereThe conformance work — this PR's whole point — is complete and was verified green before the collision surfaced: All five Issue #89's definition of done is written and ticked against that evidence. Suggested pathLand #161's unification first — one Two notes for whoever picks it up. The branch's second commit fixes |
# Conflicts: # CHANGELOG.md
|
CI is now red on this PR — 9 failing checks, all the same failure. Recording that so the count does not read as nine separate problems: Every one of those jobs compiles the workspace, and the workspace does not compile: Reproduced locally with No action wanted on this PR until #161 lands. Rebasing again will not help; the two definitions have to be collapsed to one first. |
… the trust one keeps the root export
Pull request
Summary
SPEC.md§6.5's F6–F9 shipped with their "Verified by" column pointing atcontextgraph_types::attest— the implementation's own unit tests. This adds anattestationconformance check that probes those four guarantees on the wire,plus five adversarial
--misbehavemodes incontextgraph-example-docs, one perforgery the constructions exist to stop.
Closes #89
What changed
attestationcheck (contextgraph-conformance/src/lib.rs). Raw-stdio,like the §R1/§E1/§H4 probes and for the same reason: the attestations ride the
envelope, and
Host::query_providerhands back aContextQueryResultwith theenvelope already discarded. It takes the attester keys the handshake published,
recomputes each served frame's commitment from the frame in hand, and verifies
the signature over it in the order §6.5.4 fixes — commitment first, so "the
frame moved after signing" is never reported as "the key is wrong".
--misbehavemodes incontextgraph-example-docs, each signing onething and serving another.
handshake_ack.attester_keys,frames.attestations) plus their$defsinschema/contextgraph-envelope.schema.json,SPEC.md§6.5.5 with aschema-validated example, and the maximal reference vectors populated.
SPEC.md§6.5.2 pinsprovider_idto the handshake-declaredprovider.name— the only identifier both ends of the wire observe. It waspreviously unstated, which would have made the check unimplementable from the
spec alone.
attestation.contextgraph-conformanceturns theattestationfeature ofcontextgraph-typeson. A suite that could not run the verifier would beasserting §6.5 rather than checking it. Side effect worth naming: nothing in CI
built that feature before, so
contextgraph-types/tests/attestation_vectors.rs— which is
#![cfg(feature = "attestation")]— now actually runs in theworkspace test job.
Evidence
Green path —
.github/scripts/conformance-green.sh, exit 0:The five modes. Each trips
attestationand no other check, with theverdict named in the evidence string, not merely "some failure":
forge-signatureattestationBadSignature(both frames)lift-signatureattestationCommitmentMismatchonfrm_configuration;frm_getting_startedverifiestruncate-chainattestationCommitmentMismatchonfrm_getting_startedswap-contentattestationCommitmentMismatchonfrm_getting_startedmalformed-attestationattestationMalformedCommitmentonfrm_getting_started; the other frame stays attestedlift-signature's full evidence, which shows the lift rather than asserting it —the signed commitment
sha256:36ce21…is exactly the commitmentfrm_getting_startedproduces:The negative direction. Deleting
enc_str(&mut preimage, &frame.id)fromframe_commitment— the frame-identity binding — makeslift-signatureverify:and
conformance-red.shgoes red naming it:Reverted. Separately, corrupting one hex digit of an honest signature turns the
green path's
attestationcheck red withBadSignature.conformance-red.shneeded no edit: it derives the mode list and each mode'sexpected check from the binary's own
--help, so the doc comments' "(tripsattestation)" is what wires them. Confirmed — all 26 modes caught, the five newones by
attestationalone.Key-less providers still pass (
conformance-external.shtreats a skip as afailure, so this had to be a
pass, not askip): ripgrep, treesitter,mcp-bridge over the in-repo MCP fixture, and the Python SDK example provider all
report
All 14 checks passed, withattestation: provider publishes no attester key and serves no attestation.Tests, all scoped per SCR-001:
conformance_suite23/23,attestation_conformance5/5 (new),reference_vectors3/3, plusexamples_roundtrip,stdio_roundtrip,ingest_conformance,usage_report,golden_fixtures,host_conformance_suite,composition_conformance_suite,lifecycle_profile_examples,verify_wire, and a-p-scoped run forcontextgraph-host,-types,-refprov,-mcp-bridge,-mcp-server,-ripgrep,-treesitter,-trace.python3 schema/validate-examples.pyOK.cargo fmt --all --checkandcargo clippy --workspace --all-targets -D warningsboth clean.
Coordination with #90
#90 owns the envelope shape for attestations and had not landed, so this
constructs the minimum the check cannot run without: two optional members on
Envelope, their schema$defs, and the §6.5.5 example. It is deliberately theshape #90's issue text describes ("a place in the
framesenvelope for per-frameattestations"). Not built here, and left to #90: the result-set Merkle root
and inclusion proofs on the wire, and the
examples/transcript. If #90 landsfirst, reconcile by keeping its member names and re-pointing
attestation_stdio_probeandattestations_forat them — nothing else in thisPR depends on the spelling.
The keys ride the handshake, not the answer, on purpose: a key republished
with every response could be swapped by the same forgery that swapped the
signature, and
forge-signaturewould then verify.Checklist
fmt,clippy -D warnings,test(scoped per SCR-001; see Evidence)README.md,docs/, doc comments,--helptext)git commit -s, DCO)CHANGELOG.mdupdated under[Unreleased]if user-visibleWitness tests:
contextgraph-conformance/tests/attestation_conformance.rs(5 new)and 5 new mode tests in
tests/conformance_suite.rs. All fail onmain, whereneither the check nor the modes exist.
an_attestation_lift_differs_only_in_the_frame_idis the one that matters — it asserts the
lift-signaturefixture's preconditionrather than trusting it, because a mode that fails for an unrelated reason is a
test that will sit quietly through a real regression.
No test was deleted.
Registry submission (only if adding a row to
docs/registry.md)contextgraph-inspect ... --jsoninvocation used to produce the listed report is included below (no self-attested listings — a maintainer must be able to re-run it and get the same result)passorskip, nonefailThe row is not new; its report is regenerated because the suite gained a check.
The regenerated report also repairs pre-existing drift the run surfaced: the
committed copy recorded
v0.1.0while the crate is at2.0.0.Protocol-stability impact (if a spec/wire change)
contextgraph/1contextgraph/2; explain belowBoth new members are
#[serde(default, skip_serializing_if = "Vec::is_empty")],so a provider or host that knows nothing about them serializes exactly the bytes
it did before and drops them on receipt (§13 U1). Every existing reference vector
is byte-identical; only the two maximal vectors changed, and only because the
test's contract is that a maximal vector populates every optional field.
SPEC.md§6.5.2'sprovider_idsentence tightens an existing requirement ratherthan adding one — the field was already in the normative preimage with no
statement of what it holds. Per GOVERNANCE.md the witness for both is a
conformance check, which is what this PR is.
License
By submitting this pull request, I agree to dual-license this contribution
under MIT OR Apache-2.0, as certified by my DCO sign-off.
Summary by Sourcery
Add adversarial wire-level provenance attestation verification for F6–F9, including optional envelope support and conformance coverage for honest, forged, and unverifiable attestations.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: