verify_trace_dag reports a schema-valid sha384: parent link as tampering - #119
verify_trace_dag reports a schema-valid sha384: parent link as tampering#119lywinged wants to merge 1 commit into
verify_trace_dag reports a schema-valid sha384: parent link as tampering#119Conversation
…ot tampering verify_trace_dag accepted sha384: as a well-formed parent link and then compared it against a value trace_record_hash only ever computes as sha256:. The mismatch was reported as ProvenanceLinkBroken, whose documented meaning in failure-modes.md is that tampering or reparenting was detected. Nothing was tampered with. The two halves disagreed: _DIGEST_RE admits both digests the TRACE schema permits for delegation.parent_record_hash, and the comparison had not caught up. A conforming, correctly signed chain differing from an accepted baseline only in which permitted digest names its parent was reported as a tampering finding, and an auditor reading the outcome had no way to tell that from a real one. Fail-closed either way: no forged, reparented or tampered DAG was accepted as a result, and every sha256 chain verifies exactly as before. What changes is what the verifier says about a chain it cannot read. TraceDigestUnsupported (TRACE_DIGEST_UNSUPPORTED, 501) is raised instead, before the comparison and per hop, so an unreadable link deep in a chain is not masked by a readable one at the leaf. LINK_DIGEST names the algorithm once and is used both to label and to compute the hash, so the verifier cannot drift from the producer. Tests cover the leaf case, the deep case, and a control that a well-formed link pointing at the wrong record still raises ProvenanceLinkBroken: the fix narrows tamper detection, it does not replace it. Both new tests fail without the guard. Portable vectors for this case are agentrust-io/trace-spec#184, numbers 22 and 23, which assert unverifiable with digest_algorithm_unsupported. Signed-off-by: lywinged <louie.lunz@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
❔ Contributor Check: UNKNOWN
Automated check by AgenTrust Contributor Check. |
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
The red Noting it here because I cannot request a reviewer on this repository, so there is |
verify_trace_dagacceptssha384:as a well-formed parent link, then compares itagainst a value it only ever computes as
sha256:. The mismatch is reported as atampered or reparented record. The record is neither.
This carries the fix, not just the finding. Reproduction first, since the finding is
what justifies the change.
Reproduction
Two DAGs, both correctly signed, differing only in which of the two digests the TRACE
schema permits names the parent. Against
8c49177:Script (runs against main, no test dependencies)
The sha384 digest is correct. A verifier implementing sha384 resolves that link and
verifies the chain.
Where the two halves disagreed
ca2a_verify/dag.py:56-57_DIGEST_REacceptssha256:andsha384:, commented "same as the schema"ca2a_verify/dag.py:78ca2a_runtime/trace_binding.py:207trace_record_hashreturns"sha256:" + ..., alwaysca2a_verify/dag.py:186ca2a_verify/dag.py:189"a tampered or reparented record was detected"The comment on line 56 is accurate.
schema/trace-claim.jsonpermits both digests fordelegation.parent_record_hash, andspec/trace-v0.2.mdstates SHA-384 is required forFIPS-aligned profiles. The validation was right and the comparison had not caught up.
_DIGEST_REhas exactly one use site and nothing exercised its sha384 branch, which iswhy this stayed quiet.
Severity
Not a security defect, and I am not reporting it as one. The behaviour is fail-closed:
no forged, reparented or tampered DAG is accepted as a result, and every sha256 chain
verifies exactly as before. What is lost is a conforming chain that cannot be read.
What is wrong is what the verifier says about it.
docs/spec/failure-modes.mddocumentsthat error as catching tampering and reparenting, so the detail string is the code's
documented meaning rather than incidental wording. A verifier that reports tampering when
it means it does not implement a digest puts a false finding into an audit record, and an
auditor reading the outcome cannot tell it from a real one.
The fix
The alternative was to recompute the parent hash with the algorithm the link names, which
would make a conforming sha384 chain verify. I did not take it, for a reason outside this
repository:
agentrust-io/trace-spec#184vectors 22 and 23 assertunverifiablewithdigest_algorithm_unsupportedfor exactly this input, per your ruling on that case.Accepting here would put the implementation and the published corpus in contradiction. If
you would rather have sha384 support, say so and I will send that instead and correct the
vectors, but the two should be decided together rather than separately.
So:
TraceDigestUnsupported(TRACE_DIGEST_UNSUPPORTED, 501), following the shapeAttestationUnsupportedalready sets in this taxonomy for "no backend for this".masked by a readable one at the leaf. That is vector 23's case: a verifier that inspects
the first link's algorithm and assumes the rest matches reports the chain verified
having never resolved half of it.
LINK_DIGESTnames the algorithm once, and is used both to label and to compute thehash, so the verifier cannot drift from the producer the way these two had.
AttestationUnsupported. Not implemented is what thisis. Trivial to change if you would rather the two match.
error-codes.md, andfailure-modes.mdnow says a hash that was neverrecomputed cannot be evidence of tampering.
Tests
Three cases plus a control, all in
tests/unit/test_trace_binding.py:test_baseline_link_verifiestest_unsupported_link_digest_is_unverifiable_not_brokentest_unsupported_link_is_caught_wherever_it_sitstest_supported_digest_still_detects_a_broken_linkProvenanceLinkBrokenRather than assume these fail for the right reason, I broke each behaviour in turn and
recorded which tests went red:
test_supported_digest_still_detects_a_broken_link,test_resigned_parent_breaks_child_linktest_unsupported_link_is_caught_wherever_it_sitsThe second row is the one worth reviewing: the fix must narrow tamper detection, not
replace it, and that test is red exactly when the old comparison is gone. The third row is
why the both-orders parametrisation is there. With a single ordering that mutation
survived, because a guard applied to the first hop alone is indistinguishable from a
per-hop guard when the interesting hop is first.
504 passed, 2 skipped;ruff check src testsclean;mypyclean on 42 source files.One thing I did not touch
Running the suite rewrites
examples/rejection-with-proof/{chain,dag}.jsonwith freshlygenerated keys, so a clean checkout has a dirty tree after
pytest. Unrelated to thischange and left alone, but it is easy to mistake for something else in a diff.