What
Three smaller findings from #1346's security and crypto-privacy passes, each deliberately left out of that diff because the fix lands outside the arms it wired.
1. The idempotent-ack short-circuit does not cross-check the bookmark's sharer
crates/engine/src/grants/accept.rs, the SequenceNotNewer branch: the stored bookmark is found by scope_root_name alone and its permission returned, without asserting stored.sharer_identity_pk == contact.identity_pk().to_sec1().
Not exploitable today — the path is reached only after the blinded-tag self-location and the owner-committed-tag check have both passed under this contact's ECDH — but the equality is cheap defense in depth on a path that acks and returns.
2. EngineError::TrustViolation and Seam are carrying verdicts their docs do not describe
EngineError::TrustViolation documents "a fail-closed trust violation on the read path", and Seam documents "a host seam failed (durable op-queue I/O)". The rotation classifier added in #1346 now routes through them:
WriteRotateError::{ResumedSeedNotAtItsRoot, ResumedWaveAtAnotherEpoch, WriteEpochNotAdvancing, IdentityRepoint} and RotationPublishError::Rejected → TrustViolation. These are local encoder/resume verdicts on the owner's own material, not verdicts about a peer.
ResolveFailure::ConflictingChildLabel → Seam, which is a cross-parent index disagreement, not host I/O.
Both are on the correct retryable-vs-trust axis and fail closed, so this is a labelling problem, not a safety one — but a host cannot distinguish "your own vault is mid-rotation" from "someone forged a record". Wants either a distinct variant or widened variant docs.
3. An over-cap head block on the accept path reads as retryable availability
Engine::accept_share maps a None from fanout_get_verify to ContentUnavailable, and map_read_error (crates/engine/src/net/adopter.rs) maps an over-cap head block to GateError::Seam → ContentUnavailable. A hostile contact naming a scope root that serves an oversize head therefore looks permanently retryable. Pre-existing convention rather than new behaviour; the item stays un-acked either way.
Acceptance
- The ack short-circuit asserts the stored bookmark's sharer, with a test that a bookmark under a different sharer is not silently re-acked.
- The rotation verdicts a host acts on are distinguishable from a peer trust verdict — by variant or by documented widening, with the classifier's tests pinned to whichever is chosen.
- A size refusal on a gated read is its own verdict rather than availability.
Part of #635
What
Three smaller findings from #1346's security and crypto-privacy passes, each deliberately left out of that diff because the fix lands outside the arms it wired.
1. The idempotent-ack short-circuit does not cross-check the bookmark's sharer
crates/engine/src/grants/accept.rs, theSequenceNotNewerbranch: the stored bookmark is found byscope_root_namealone and itspermissionreturned, without assertingstored.sharer_identity_pk == contact.identity_pk().to_sec1().Not exploitable today — the path is reached only after the blinded-tag self-location and the owner-committed-tag check have both passed under this contact's ECDH — but the equality is cheap defense in depth on a path that acks and returns.
2.
EngineError::TrustViolationandSeamare carrying verdicts their docs do not describeEngineError::TrustViolationdocuments "a fail-closed trust violation on the read path", andSeamdocuments "a host seam failed (durable op-queue I/O)". The rotation classifier added in #1346 now routes through them:WriteRotateError::{ResumedSeedNotAtItsRoot, ResumedWaveAtAnotherEpoch, WriteEpochNotAdvancing, IdentityRepoint}andRotationPublishError::Rejected→TrustViolation. These are local encoder/resume verdicts on the owner's own material, not verdicts about a peer.ResolveFailure::ConflictingChildLabel→Seam, which is a cross-parent index disagreement, not host I/O.Both are on the correct retryable-vs-trust axis and fail closed, so this is a labelling problem, not a safety one — but a host cannot distinguish "your own vault is mid-rotation" from "someone forged a record". Wants either a distinct variant or widened variant docs.
3. An over-cap head block on the accept path reads as retryable availability
Engine::accept_sharemaps aNonefromfanout_get_verifytoContentUnavailable, andmap_read_error(crates/engine/src/net/adopter.rs) maps an over-cap head block toGateError::Seam→ContentUnavailable. A hostile contact naming a scope root that serves an oversize head therefore looks permanently retryable. Pre-existing convention rather than new behaviour; the item stays un-acked either way.Acceptance
Part of #635