fix(13.1.0): bind the SUBJECT, not just the signer — three surfaces (#252) - #253
Open
emooreatx wants to merge 1 commit into
Open
fix(13.1.0): bind the SUBJECT, not just the signer — three surfaces (#252)#253emooreatx wants to merge 1 commit into
emooreatx wants to merge 1 commit into
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Valuealongside decision-driving sibling fields — and found six:operational_admit::MembershipGrantparse()readsuser_id/org_id/roleout of the signed envelope. The right shape all along.self_at_login::SignedEnvelopeceg_outbox::SignedCegObjectprovenance::ProvenanceLinktransport_binding::TransportBindingfederation_self_record::KeyRecord(via manifest co-scrub)The three holes
1.
ProvenanceLink— the reported one. Mallory wraps a victim's genuine, validly-signed registration envelope in a link declaring her ownkey_idand 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_destinationandencryption_pubkeysare siblings, while the producer signs all three inside the occurrence envelope. Mallory keeps the victim'sattesting_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. Theencryption_pubkeysvariant aims the same trick at content encryption.3. Co-scrub blessing — privilege transfer. The identity check read the sibling
key_idwhile 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 withinfra: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:key_idalone loses to a node that hasn't replicated the victim's row, where an attacker registers the victim'skey_idunder their own pubkeys.nullsatisfied by omission, per CEG §0.9 omit-vs-materialize, and only when the carrier claims nothing.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_verifierpassing 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_ed25519forpubkey_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