Skip to content

spec: parent_record_hash's preimage is stated only in a document that binds nothing #245

Description

@chernistry

delegation.parent_record_hash is the field a verifier walks to reconstruct a delegation chain, and nothing normative says what it is a digest of.

At e7e2eca:

  • The schema gives it a format and a sentence: "pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$", described as "SHA-256 or SHA-384 digest of the parent hop's Trust Record."
  • spec/trace-v0.2.md does not mention the field at all (zero occurrences).
  • docs/rfcs/a2a-delegation-profile.md §4.1 does state a preimage, and D-3 (parent_not_found) is defined in terms of it — but that document says "Status: Draft proposal. Binds nothing."

"the parent hop's Trust Record" has at least three natural readings, and they give different digests:

  1. The published file's bytes.
  2. RFC 8785 (JCS) over the complete record, signature included.
  3. RFC 8785 over the record with signature removed — the same body the signature itself covers.

I computed all three against our own delegated-parent record. Readings 1 and 2 agree on it and match the child's link; reading 3 gives a different digest. So two implementers reading only the schema can both act in good faith and produce chains that resolve to dangling links against each other, with no diagnostic on either side saying why.

The complete-record reading is the right one, and it should be normative

§4.1 already takes it, for a reason worth promoting out of a draft: a digest over the signed body alone commits to what the parent said and not to who said it, so any holder of any key can re-sign bytes that satisfy the same link. That is the difference between a chain that binds hops and one that binds statements.

Asking for that sentence to move somewhere that binds — the schema description, or spec/trace-v0.2.md — rather than for a new rule.

One reading is a trap rather than a disagreement

Plain json.dumps(obj, sort_keys=True, separators=(",", ":")) over the complete record produces the same digest as JCS on records like ours, so an implementer who reaches for it interoperates and has no reason to look further.

It stops matching the first time a record carries a non-ASCII character, and the cause is a default rather than a choice: json.dumps escapes non-ASCII to \uXXXX unless ensure_ascii=False is passed. Putting 模型-π in model.model_id and v1-é in policy.version, both of which the schema accepts:

JCS                              91eca6bbe63ebe2ffeba09c0...
json.dumps (default)             fd74a7636b2665d69185150c...   diverges
json.dumps (ensure_ascii=False)  91eca6bbe63ebe2ffeba09c0...   agrees

A silent agreement that ends on a field nobody thought was load-bearing is worse than a mismatch on day one. If the normative text names RFC 8785 explicitly rather than "canonical JSON", that whole class goes away.

Why this surfaced here

We publish a delegated parent/child vector pair whose link is built to the complete-record reading, and the aggregate over them carries member digests under the same convention. When an independent implementer re-derived the link, the preimage had to be inferred from our vectors rather than read from the specification — which works exactly once, for whoever has our vectors in front of them.

Same shape as #232, #240 and #244: a rule an implementer needs, that no binding surface states.

Credit: @lywinged enumerated six encodings against our parent record and isolated both the four-way split and the json.dumps trap.

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