Skip to content

ProvenanceChain re-verification is signer-only — the same subject-blindness class persist just closed on four planes #252

Description

@emooreatx

Found while auditing CIRISPersist for v31.0.0 (CIRISAI/CIRISPersist#660). persist's
own path is closed; the verify-side re-derivation is not, and it has the same
shape.

The class

Every authority gate we audited answered "who signed?". Almost none answered
"who is this ABOUT?". A quorum verifies m-of-n over JCS(envelope) and
nothing else — so if the value naming the subject is not inside those bytes,
the same valid signature applies to any subject.

That single question found four planes in persist. The worst, verified against
the strict gate (withdrawal-wins, the #513 ≥3-FIPS floor, real quorum crypto):

PROBE  key_id = canon-probe-ATTACKER
       envelope = {"key_id": "canon-probe-victim"}
RESULT Ok(())

An accord co-scrub conferred canonical on any key_id. That is the only
door canonical, infra:attest, the co-steward roles and every
AUTHORITY_CONFERRING_IDENTITY_TYPES member enter through. #513 prices a trust
root at three non-virtualizable humans; unbound, that price bought an unbounded
number of conferrals.

Where CIRISVerify is exposed

ProvenanceChain is consumed here and in CIRISEdge, and each link's signature is
re-verified against the next link's pubkey. That is signer-only verification:
the same shape. A hostile peer serving a fabricated chain could lift a signature
exactly as persist's gate allowed, because nothing in the verified bytes pins
which link the signature is about.

persist's in-repo rows are all bound at the write gate now, so rows originating
here are safe. The re-derivation on the verify side is CIRISVerify's to bind,
and it runs against chains persist did not author.

The fix shape that worked, and two refinements that cost us time

persist's remedy is admission::subject_binding — one projection, called by
both the producing and the checking side:

  1. Bind the identity, not just the name. Binding key_id alone still loses
    to a race: on a node that has not yet replicated the victim's row, an attacker
    registers the victim's key_id with their own pubkeys and the conferral
    succeeds. Bind both hybrid legs, with absence asserted as JSON null, so
    substituting a PQC key the quorum never covered is refused exactly as a
    differing string is.
  2. Have the checking side ITERATE the projection, rather than check named
    fields. Then adding a member extends the check with no second edit —
    exhaustive by construction rather than by anyone remembering. This is the
    single detail that made the fix safe to reuse across four planes unchanged.
  3. Fail CLOSED on absence. An envelope not carrying the binding is REFUSED,
    never tolerated. A tolerated absence is how our first attempt got left
    half-done — "an optional check is skippable by omission, which is the whole
    attack."
  4. Check the binding FIRST, before roster or custody resolution, so the
    refusal is deterministic regardless of that state.

One trap worth inheriting: "this record confers nothing today" is not a
reason to skip binding it. In persist a plain KeyRecord becomes a conferral
subject through two separate elevation paths, one of which skips the
registration gate entirely for exactly the role-gated records.

Ask

  1. Audit ProvenanceChain's re-verification for subject-blindness and bind it.
  2. Sweep the same question across any other verify-side quorum or signature
    walk: does anything in the verified bytes pin who this is about?
  3. Tell us if binding it changes a wire shape — persist is mid-mesh-reset and
    this is the cheap window. After 31.1.0 bakes the new trust root, a preimage
    change costs a second ceremony against a live federation.

Reference: CIRISAI/CIRISPersist#660, and issue CIRISAI/CIRISPersist#659 for the
full write-up and probe.

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