Skip to content

Execution lineage is hash-linked but unsigned, so a different history can be presented around a valid chain #168

Closed

Description

@imran-siddique

Stated on #150 while answering @ngallo and not tracked anywhere since, so it exists only inside a discussion thread.

At d3b7eb618084c4dc2f1270c2050d443f5bab5a3d:

  • DelegationRecord (src/ca2a_runtime/provenance.py:36) carries ten fields and no signature. The word does not appear in the class.
  • verify_dag accepts one ordered root-to-leaf path, checking that a child's parent_record_hash equals the recomputed hash of the immediately preceding record.
  • cross_check_chain(records, chain) reconciles that path against the credential chain.

So execution lineage is a hash-linked path cross-checked against the authenticated object rather than part of it. The credential chain is signed; the history around it is not.

The consequence, in the words I used on #150: an attacker who cannot forge a credential can still present a different execution history around the same valid chain, and nothing in the signed material contradicts them.

@ngallo confirmed independently that this substitution is accepted by cA2A today and refused by PIC Profile 0.2, where the workload-signed Transition COSE carries predecessor.hash over the predecessor's exact signed bytes, making the binding state-to-state with no separate history object to cross-check.

The remedy is not decided here. This issue records the defect and its exact extent. Anything that closes it touches the signed material, so it is a wire-format decision rather than a check to add.

One constraint on any conformance vector for this. The substitution case must be carried as an expected refusal referencing this issue, never as an accepted result. A fixture that records current behaviour as expected defines the defect as correct, and the next implementer builds to the fixture.

Activity

  1. Knapp-Kevin commented on Sep 28, 2026

    @Knapp-Kevin

    One thing from related work I have been doing in Agent Memory may be useful here.

    We ended up making an explicit distinction between proving a local predecessor relation and proving a complete or canonical history. In our temporal commitment work, a predecessor chain can establish the declared relationship between two exact objects, but that does not by itself establish that the history is complete, unique, or canonical. Two sibling successors can both be cryptographically valid without either one automatically becoming the authoritative branch.

    That seems relevant to the eventual fix here. Authenticating the lineage or transition closes the substitution problem, but I would be careful not to let that stronger binding imply more than it proves.

    A few cases that were useful for us to keep separate were: valid predecessor binding, substituted predecessor, reordered history, missing predecessor evidence, and two independently valid children of the same predecessor. The last case is especially useful because it tests whether the verifier accidentally equates "valid chain" with "only possible chain."

    For reference, the open source Agent Memory ADR that formalized this separation is here:
    https://github.com/MythologIQ-Labs-LLC/agent-memory/blob/main/docs/adr/ADR-031-temporal-claims-require-deterministic-content-commitments.md

    Freely offered as a design data point. The issue already has the right core diagnosis, this is just one boundary that may be worth preserving when the wire format decision is made.

  2. imran-siddique commented on Sep 29, 2026

    @imran-siddique
    MemberAuthor

    @Knapp-Kevin agreed, and #198 already draws that line: verify-lineage authenticates the signed declarations and says nothing about completeness or uniqueness, which is why this stays open. Two independently valid children of one predecessor is the case #198's suite does not have; it goes into the vector set for the wire-format decision, as an expected finding that authenticity alone cannot pick a branch.

  3. imran-siddique commented on Oct 15, 2026

    @imran-siddique
    MemberAuthor

    Closing. #198 shipped verify-lineage, which authenticates the signed lineage and refuses substituted histories. Binding authority to a live request continues in #222, and the two-valid-children vector from @Knapp-Kevin's comment moves there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions