Skip to content

-02 review: the Verdict binds to a Delivery's identity, not to the evaluation — and constraints is committed but discharged by nothing #12

Description

@msaleme

You closed PR #10 with "a reviewer who is not its author is the next thing it needs," so
here is one. I read -02 in full and diffed it against -01 rather than working from
Appendix B. Three findings, ranked, plus two smaller ones. All line numbers are from
draft-laxsharma-pact-02.txt.

Thank you also for the acknowledgement on p.79 — I did not expect it, and the place it
ended up (§17.2, and the first sentence of §9.1) is a better home for the observation
than the thread it came from.

First, credit where it is due, because it changes what is worth raising. Three things in
-02 are better built than the field:

  • §2's object digest covering "the canonical form of the whole object, including every
    signature member it carries"
    is the rule that makes the whole chain non-malleable. A
    great many receipt designs hash the payload and leave the signature outside the
    commitment.
  • §9.1 making independence a relation the Facilitator derives rather than a field a
    record declares is the correct fix to the self-attestation class, and stating the
    ceiling honestly — "An independence claim reaches exactly as far as the record's own
    commitments"
    — is rarer than the rule itself.
  • typ carrying the full media type, so a signature over one object type cannot stand
    for another.

Everything below sits on top of that, and none of it is a -02 regression. The Verdict's
member set is unchanged from -01 apart from challenge_hash. This is the one layer -02
did not reach.


1. The Verdict commits to the instrument and to the results, but never to the pairing

§7.2 already takes the mirror of this seriously:

Without instrument_hash a Verifier could run something other than the committed
instrument and the contract would have no way to tell; that is the substitution attack
of Section 17.4, arriving from the verification side.

That closes the instrument half. The artefact half is open. A Verifier can run the
committed instrument against something that is not work_hash, emit PASS, set
instrument_hash to criteria_hash, and every check in §7.2 passes — because §7.2's
checks are field comparisons, not evidence of execution.

The reason nothing catches it is that results_hash is never constrained. It occurs six
times in the document: lines 685, 708 and 734 (data dictionary) and 1430, 1529 and 1641
(figures). It appears in no normative sentence. Nothing relates it to work_hash, to
input_hash, or to the Delivery's evidence.results_hash. Figures 4 and 5 print them
equal with no rule requiring it.

work_hash is the same: lines 666, 774, 1421, 2251 — dictionary and figures only.
input_hash occurs at 680, 1389 and 1424, and §6 checks only its presence. The Verdict
has no input_hash member at all, so a re-execution Verdict never states what it
re-executed over.

Suggested fix, and it is cheap: require the Verdict to carry work_hash, and
input_hash for tiers that re-execute, copied from the Delivery and compared by the
Facilitator against the delivered entry — the same one-line comparison already applied
to instrument_hash. Challenge.proof has the identical shape and needs the identical
change, otherwise a proof of nonconformance is unbound to the artefact it was computed
over, and the detection mechanism inherits the defect it exists to detect.

2. constraints names a scope restriction that no record discharges

§3.3, line 654:

constraints: object, optional. Tool prohibitions, confidentiality and compliance
conditions, in a form this document does not define.

As a TaskSpec member it occurs twice in 4,424 lines — that dictionary entry and the §2
extension carve-out. No Facilitator check reads it. No Verdict member reports on it. No
conformance vector touches it.

The Verdict's outcome is PASS/FAIL against acceptance, which is a sibling of
constraints in the same TaskSpec. So a contract can prohibit a tool, the Seller can use
it, the acceptance instrument can pass on output shape and thresholds, the Verdict reads
PASS, and the Outcome Record reads FINAL — with every digest verifying.

This is the shape I care about most, and it is general beyond PACT: a scope committed
by hash and discharged by nothing is a commitment to having had an intention.
Either
§9's list of what a verification profile states should gain a sixth item covering whether
and how it evaluates constraints and what a Verdict reports about it, or §3.3 should
say plainly that this protocol does not evaluate constraints, so that nobody reads a
PASS as covering tool prohibitions. Silence is the one option that misleads.

3. The Outcome Record carries no verification scope, and outcome.state is lossy

§12 is explicit about the stakes: "It is the input to any reputation system built on
PACT."

Its members (§3.8) do not include task.spec_hash, verification.criteria_hash,
verification.tier, verification.profile, or flow. The verdict trace entry carries
signer but no instrument_hash. So from an Outcome Record alone a stranger learns who
passed the work and not what instrument they ran. Recovering the criteria means fetching
the contract; recovering the TaskSpec means fetching bytes of which §5.2 says "It is
not transmitted over the endpoints of this document"
, whose spec_uri is optional, and
which §17.12 restricts by default. An Outcome Record can therefore be permanently unresolvable to what was
asked for, by construction.

Separately, Table 2 terminates "FINAL otherwise." Under verdict-first a
verdict-lapsed opens the window with no Verdict, the window closes, no Verdict stands
— FINAL. Under no-window no Verdict is ever accepted — FINAL. So the summary member a
reputation system will key on collapses "verified pass," "nobody checked," and "the
contract forbade checking" into one value. The trace distinguishes them, but the field
named outcome.state does not.

One more in the same family: §3.8 says work_hash "Binds the record to what was
produced"
— but §3 opens by stating that no sentence in it requires anything of any
party, and no §12 rule constrains it. Compare terms_result, which gets two arithmetic
invariants and an explicit MUST NOT sign. Of the 26 vectors in Table 3, V-24 covers an
overdrawn transfer and none covers Outcome work_hash. The money is checked; the
artefact is not.

4. Smaller: the Delivery's own evidence.instrument_hash is never compared to criteria_hash

§7.2 requires it of the Verdict. §6's refusal list requires only conformance to the
profile, which §6 defines as "a check on shape, not on substance." So a Seller can ship
evidence naming a different instrument and the Facilitator still records delivered. A
one-line asymmetry — worth closing in §6 or documenting as deliberate.

5. Smaller: §9.1's two normalization rules pull against each other

"the normalization MUST fold toward identifying the same party" versus
"Percent-encoding MUST NOT be decoded." The second defeats the first for any
percent-encoded variant. Where parties.verifier is absent, §14.1.1's kid-attribution
rule does not constrain the signer, because the signer is defined by its own kid. So
a party can hold a key under a percent-encoded variant of its own identifier and satisfy
the independence check. I think the no-decoding argument is right on attack-surface
grounds; the fix is probably to reject an identifier whose decoded form would collide
with a party, or to state the residual risk.


On §9.1 more broadly: the rule establishes non-identity of identifiers, and a did:web
host can publish as many DID documents as it likes, so a Seller can register a sibling
identifier and self-verify while passing every check. Your Table 4 wording is careful
about this; §9.1's framing ("Independence is a relation between the party that signs a
Verdict and the parties to the contract") invites the stronger reading. One sentence
saying that independence of control is out of scope would match the honesty of §17.1's
Facilitator row.

Finally — the four defects you found in your own implementation on the second pass
(delivery_hash bypassable, Verdict.instrument_hash and profile never compared,
signing input rebuilt from the parsed header rather than the transmitted bytes,
kid_covers using startswith) are all this same class, and -02 closed each at the
specification level. The three above are the ones that technique will not find, because
they are places where the specification requires no check to exist. An implementation can
conform perfectly and still not detect them.

Happy to be wrong on any of this — particularly on 1, where I may be missing a rule that
binds results_hash somewhere I did not look.

Activity

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