Skip to content

dweb: per-dwapp derived subidentities — break cross-dwapp linkability of the single mesh DID #320

Description

@NotASithLord

Problem

There is exactly one Ed25519 mesh identity per browser profile (vault DWEB_IDENTITY_SECRET, held by the offscreen base node — offscreen/dweb-base.js: "one identity per browser"). Every surface reuses it: base-mesh presence, every dwapp's room membership (rosters are DIDs), publishing, and A2A. Consequence: any peer who shares two rooms with you can link your activity across every dwapp you run — the union of your dwapp usage is one globally-correlatable identity. The hello() bridge op hands the DID string to every dwapp, so apps themselves can also correlate out-of-band.

This is invisible now (small mesh) and expensive later: fixing it after the mesh has a population means rotating every user's visible identity in every room at once.

Proposal

Domain-separated derived subidentities, consistent with the codebase's existing discipline (the D-8 rule; content/manifest.js's tagged-signature scheme):

  • seed_dwapp = HKDF(master_secret, "peerd/dwapp-identity/v1" || dwapp_id) → per-dwapp Ed25519 keypair → per-dwapp did:key.
  • Room presence, announce, publish/DM, mute all use the derived DID for that dwapp.
  • Publisher identity stays the master DID — publishing is deliberately attributable (dwapp_id = H(publisher‖slug) depends on it); unlinkability is for usage, not authorship.
  • The agent envoy (reserved peerd-agent room, the dweb actor) keeps the master DID or gets its own fixed derivation — it is the user's addressable agent, so stability there is the point.
  • hello() reports the derived DID, shrinking what a dwapp can correlate.

Derivation is deterministic, so no new vault state: the same master secret yields the same per-dwapp identity on every unlock.

Granularity decision

Per-dwapp (not per-room): the bridge already enforces one room per app, and a stable in-app identity is what users expect socially (your name in the commons shouldn't rotate). Per-room is strictly stronger unlinkability but can be a later knob ("peerd/dwapp-identity/v1" || dwapp_id || roomId) — the versioned info-string leaves room.

Design-through list (the real work)

  • DM routing and mute lists key on DID (offscreen/dweb-base.js, room membership) — the base node must hold N subidentities concurrently and route ch=3 traffic per derived key.
  • dweb_block semantics: blocking a peer's publisher DID vs. one of their room subidentities are different acts; decide what the user-facing block means (probably: block the identity you can see; publisher-level ban stays the discovery-plane mechanism).
  • Signaling/DHT: provider records and rendezvous currently assume the one DID; audit which planes may carry a subidentity without breaking Kademlia bookkeeping.
  • Grant keying (bridge.js appKey) is content-hash based and orthogonal — unaffected.
  • Rate caps (background/dweb-inbound-rate-cap.js) are per-did — per-subidentity caps must not multiply an attacker's budget (cap by master on our side, by observed did for inbound).

Prior art

Cloudflare OS scopes identity per-account-per-vendor and deliberately passes gatekeepers an opaque random observer id instead of a real identity precisely to prevent cross-context correlation. Same instinct, P2P setting.

Do this while the mesh is small; it's a wire-visible identity change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions