Commit graph

2 commits

Author SHA1 Message Date
Scott Reimers
dfd3253734 Fix: GroupKeyDistribute admin forgery + cap concurrent port scanners
Two pre-release fixes found during audit.

1) GroupKeyDistribute admin forgery (critical)

   `group_key_distribution::try_apply_distribution_post` trusted the
   `admin` field inside the decrypted payload without verifying it
   matched the post's author. Exploit: any peer who learns a victim's
   posting NodeId (public — appears as a recipient on any DM/group
   post) and observes a target group_id in the wild could craft an
   encrypted distribution post claiming to be from the legitimate
   admin. The victim's storage uses INSERT OR REPLACE on group_keys,
   so a successful forgery would overwrite the victim's legitimate
   group key record and stored seed, breaking future rotations / key
   distributions from the real admin.

   Fix: reject the distribution post when `content.admin != post.author`.
   Added test `forged_admin_is_rejected` that seeds a legitimate
   record, attempts a forgery, and asserts the legitimate record is
   untouched.

2) Cap concurrent port-scan hole punches at 1 (bandwidth)

   `hole_punch_with_scanning` fires ~100 QUIC ClientHellos/sec for up
   to SCAN_MAX_DURATION_SECS (300s), ~1 Mbps per active scanner. With
   no cap, the growth loop / anchor referrals / replication paths
   could spawn several scanners at once and drive sustained multi-Mbps
   upload — particularly pathological on obfuscated VPNs where every
   probe stalls at a proxy timeout, explaining the reported 10 Mbps
   sustained upload after anchor connect.

   Fix: module-level `tokio::sync::Semaphore(1)` guarding entry to the
   scanning loop. Second-and-beyond callers fall back to the cheaper
   `hole_punch_parallel` (standard punching, no 100/sec port walk)
   instead of spawning another scanner. Permit is held for the scanner
   lifetime and released on return. Added unit test
   `scanner_semaphore_caps_concurrent_scans_at_one`.

Both changes leave the successful-call path untouched (single scanner
still runs; legitimate key distributions still apply). 120 / 120 core
tests pass.
2026-04-22 23:32:10 -04:00
Scott Reimers
f88618bb6f Phase 2g: GroupKeyDistribute \u2192 encrypted post
Removes the last persona-signed direct push on the wire. Group/circle
seeds no longer travel via the 0xA0 `GroupKeyDistribute` uni-stream from
admin to member. Instead the admin publishes an encrypted post containing
the seed + metadata; each member is a recipient; the post propagates via
the normal CDN. Members decrypt with their posting secret to recover the
seed.

Eliminates the wire-level coordination signal between an admin endpoint
and each member endpoint when a group is created, a member is added, or
a key is rotated.

Core pieces:
- New `VisibilityIntent::GroupKeyDistribute` variant.
- New `types::GroupKeyDistributionContent` — JSON payload inside the
  encrypted post: group_id, circle_name, epoch, group_public_key, admin,
  canonical_root_post_id, group_seed.
- New `group_key_distribution` module:
  - `build_distribution_post(admin, admin_secret, record, group_seed, members)`
    returns `(PostId, Post, PostVisibility::Encrypted)` — wraps the CEK
    per member using standard `crypto::encrypt_post`.
  - `try_apply_distribution_post(s, post, visibility, our_personas)`
    iterates every posting identity's secret trying to decrypt; on
    success stores `group_key` + `group_seed` and returns true.
  - `process_pending(s, our_personas)` scans stored
    GroupKeyDistribute-intent posts and applies any we can decrypt.

Node API:
- `add_to_circle`: builds a distribution post wrapping the current seed
  to just the new member, stores with intent=GroupKeyDistribute, and
  propagates via `update_neighbor_manifests_as` (no direct push).
- `create_group_key_inner`: at group creation, after wrapping keys for
  every non-self member, builds one distribution post addressed to all
  of them and propagates through the CDN.
- `rotate_group_key`: same pattern at epoch rotation.
- New `Node::process_group_key_distributions` — scans and applies.
  `sync_all` now calls it automatically so seeds take effect right after
  a pull cycle.

Removals (wire-breaking; v0.6.2 already forked):
- MessageType 0xA0 (`GroupKeyDistribute`), its payload struct, the
  handler in connection.rs, and `Network::push_group_key` all deleted.

ConnectionManager's `secret_seed` (network secret) is no longer used for
group-key unwrapping — that shifted to posting secrets in the apply
pass, matching the v0.6.1+ identity split where group keys are wrapped
to posting NodeIds.

Tests: new `member_decrypts_and_applies` covers a recipient decrypting +
storing the seed and a non-recipient failing to apply. Workspace
compiles clean; 118 / 118 core tests pass on a stable run (pre-existing
flaky `relay_cooldown` test with a 1ms timing window is unrelated).
2026-04-22 23:09:19 -04:00