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.
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. Thehello()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.dwapp_id = H(publisher‖slug)depends on it); unlinkability is for usage, not authorship.peerd-agentroom, 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)
offscreen/dweb-base.js, room membership) — the base node must hold N subidentities concurrently and route ch=3 traffic per derived key.dweb_blocksemantics: 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).bridge.jsappKey) is content-hash based and orthogonal — unaffected.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.