Skip to content

fix(13.1.0): bind the SUBJECT, not just the signer — three surfaces (#252) - #253

Open
emooreatx wants to merge 1 commit into
mainfrom
fix/252-subject-binding
Open

fix(13.1.0): bind the SUBJECT, not just the signer — three surfaces (#252)#253
emooreatx wants to merge 1 commit into
mainfrom
fix/252-subject-binding

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

SECURITY. Closes #252 — and the audit it asked for found two more instances of the same class.

The class

Every authority gate answered "who signed?". None answered "who is this about?" A quorum verifies m-of-n over JCS(envelope) and nothing else — so where the value naming the subject lives outside those bytes and is never compared, the same valid signature applies to any subject.

ELI5: it's a sealed notarized letter. We checked the seal was real, the letter unaltered, the notary trusted — and never opened it to see whose name was inside. The cover sheet said whatever the bearer wanted.

The sweep

I looked for the structural signature — a struct carrying an opaque signed Value alongside decision-driving sibling fields — and found six:

Struct Verdict
operational_admit::MembershipGrant Safe — siblings are signatures only, and parse() reads user_id/org_id/role out of the signed envelope. The right shape all along.
self_at_login::SignedEnvelope Safe — siblings are signatures only
ceg_outbox::SignedCegObject Safe — no verifier keys authority off its siblings
provenance::ProvenanceLink EXPOSED
transport_binding::TransportBinding EXPOSED
federation_self_record::KeyRecord (via manifest co-scrub) EXPOSED

The three holes

1. ProvenanceLink — the reported one. Mallory wraps a victim's genuine, validly-signed registration envelope in a link declaring her own key_id and pubkeys. Content hash matches (it is the victim's envelope), both scrub-signatures verify (really signed by the real parent), linkage passes — and the chain roots the attacker's key. The test asserts the hash and signature still verify before asserting refusal, so it's provably the binding doing the work.

2. TransportBinding — worse, because it's a traffic-redirect primitive. attesting_key_id, transport_destination and encryption_pubkeys are siblings, while the producer signs all three inside the occurrence envelope. Mallory keeps the victim's attesting_key_id — so the signature still verifies — and substitutes her own transport address. Key separation and the destination-hash recompute then both run against her address and agree. The verdict reports the victim's identity as reachable at Mallory's address. The encryption_pubkeys variant aims the same trick at content encryption.

3. Co-scrub blessingprivilege transfer. The identity check read the sibling key_id while the authority evidence (roles_in_envelope() + the ≥2-anchor scrubs) came from the envelope. So a genuinely accord-co-scrubbed record for a key that was blessed with infra:attest, relabelled on the outside only, blesses a key that never was — out of entirely valid signatures.

The fix

New subject_binding, implementing the four rules persist paid for:

  1. Bind the identity, not just the name — both key legs. Binding key_id alone loses to a node that hasn't replicated the victim's row, where an attacker registers the victim's key_id under their own pubkeys.
  2. The checker ITERATES the projection — a member added later is enforced with no second edit.
  3. Fail CLOSED on absence. An optional check is skippable by omission, which is the whole attack. Exactly one tolerated case: an expected null satisfied by omission, per CEG §0.9 omit-vs-materialize, and only when the carrier claims nothing.
  4. Check the binding FIRST, before anchor/roster/custody resolution.

Verifier-only — no wire change. Verify's own producers already put the binding in the signed bytes; nothing had opened the letter. Proven by real_producer_record_roundtrips_through_verifier passing untouched throughout.

The finding under the finding

Five fixtures had to be rebuilt. Each mutated a field outside the signed envelope to simulate tampering — which is now precisely the refused attack — so they were asserting downstream errors (Malformed, TerminusNotSteward, InsufficientAccordScrubs) while handing the verifier incoherent objects. They now tamper coherently, so they test what they claim.

The provenance fixture additionally invented its own member names (pubkey_ed25519 for pubkey_ed25519_base64) — the same producer/fixture divergence that let the v10.4.0 preimage bug ship cargo-green and artifact-broken. That fixture is now pinned to the producer's shape with a comment saying why.

3 attack tests + 8 primitive tests. 954 core green, 1348 workspace, clippy clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FZPhbXWTzykcbotEX9BN2p

…252)

SECURITY. Every authority gate answered "who signed?"; none answered "who
is this ABOUT?". A quorum verifies m-of-n over JCS(envelope) and NOTHING
ELSE, so where the value naming the subject lives outside those bytes and is
never compared, the same valid signature applies to any subject.

Swept for the shape -- a struct carrying an opaque signed Value alongside
decision-driving sibling fields -- and found six. Two were already safe
(siblings are signatures only). One already read its subject from inside the
signed bytes (operational_admit, the right shape all along). Three were
exposed:

1. provenance::ProvenanceLink -- the reported one. Mallory wraps a victim's
   genuine, validly-signed registration envelope in a link declaring her own
   key_id and pubkeys. Content hash matches (it IS the victim's envelope),
   both scrub-signatures verify (really signed by the real parent), linkage
   passes, and the chain roots THE ATTACKER'S KEY.

2. transport_binding::TransportBinding -- worse, because it is a redirect
   primitive. attesting_key_id, transport_destination and encryption_pubkeys
   are siblings while the producer signs all three inside the occurrence
   envelope. Mallory KEEPS the victim's attesting_key_id so the signature
   still verifies, and substitutes her own transport address; key separation
   and the dest-hash recompute then run against her address and agree, and
   the verdict reports the victim's identity as reachable at Mallory's
   address. Same trick on encryption_pubkeys aims at content encryption.

3. manifest_contribution co-scrub blessing -- privilege transfer. The
   identity check read the sibling key_id while the authority evidence
   (roles_in_envelope + the >=2-anchor scrubs) came from the envelope, so a
   GENUINELY co-scrubbed record for a key that WAS blessed with infra:attest,
   relabelled on the outside only, blesses a key that never was.

New subject_binding implements persist's four rules: bind the identity not
just the name (both key legs -- binding key_id alone loses to a node that
has not replicated the victim's row, where an attacker registers the
victim's key_id under their own pubkeys); the checker ITERATES the
projection so a member added later is enforced with no second edit; fail
CLOSED on absence, with exactly one tolerated case (an expected null
satisfied by omission per CEG 0.9 omit-vs-materialize, and only when the
carrier claims nothing); and check the binding FIRST.

Verifier-only, no wire change: verify's own producers already put the
binding in the signed bytes. Nothing had opened the letter. Proven by
real_producer_record_roundtrips_through_verifier passing untouched.

Five fixtures had to be rebuilt, which is the finding under the finding:
each mutated a field OUTSIDE the signed envelope to simulate tampering --
now precisely the refused attack -- so they asserted downstream errors while
handing the verifier incoherent objects. The provenance fixture also
invented its own member names (pubkey_ed25519 for pubkey_ed25519_base64),
the same producer/fixture divergence that let the v10.4.0 preimage bug ship
green.

954 core green, 1348 workspace, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FZPhbXWTzykcbotEX9BN2p
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant