swicg / swicg/activitypub-e2ee

Limiting group membership metadata exposure

Open
#83 17 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

draft blocker
Dominant language
HTML
Stars
78
Forks
2
PR merge metrics
No merged PRs in 30d

Description

(Discussion thread: https://activitypub.space/topic/421/limiting-group-membership-metadata-exposure)

This issue is intended as a brainstorming discussions based on several comments by people such as @ThisIsMissEm from the Trust and Safety task force such as "E2EE protocol should not allow a server to know who is in a conversation". I don't pretend to have a good answer, but its worth trying to think about some options...

Problem

The current spec sends group messages with all recipients in the to field:

{
  "type": "PrivateMessage",
  "to": ["https://server-a/users/alice", "https://server-b/users/bob", "https://server-c/users/charlie"],
  ...
}

Every server that receives this message sees the full member list. For a group with members across N servers, every server learns the identities of all members on all other servers, a significant metadata leak independent of the E2E encryption of message content.

Some possible approaches

Option A: Use bto instead of to

ActivityPub already defines bto (blind carbon copy) semantics: the origin server strips bto recipients before delivery, and each destination server receives only the recipients relevant to it.

Bonfire's AP server already implements this: with in the AP publisher code the function preserve_privacy_of_outgoing groups recipients by destination server and filters bto/bcc to only that server's actors before each S2S HTTP POST.

Proposed change: Send group messages with recipients in bto rather than to:

{
  "type": "PrivateMessage",
  "actor": "https://server-a/users/alice",
  "bto": ["https://server-a/users/alice", "https://server-b/users/bob", "https://server-c/users/charlie"],
  ...
}

Each server receives the activity with bto filtered to only its own local members. No server other than the sender's sees the full member list.

What each server learns:

Server Learns
Alice's server (origin) Full bto list (needed to do the fanout)
Bob's server Only that bob received this message
Charlie's server Only that charlie received this message

No new infrastructure needed. This is simple to do with existing AP semantics.

Option B: bto with shared inbox URLs

Applying bto semantics (Option A) to shared inbox addresses rather than individual actors, with a single Create activity addressed to each participating server's shared inbox:

{
  "type": "Create",
  "actor": "https://server-a/users/alice",
  "bto": ["https://server-b/shared_inbox", "https://server-c/shared_inbox"],
  "object": {
    "type": "PrivateMessage",
    "mediaType": "message/mls",
    "encoding": "base64",
    "content": "<mls ciphertext>"
  }
}

What each server learns:

Server Learns
Alice's server (origin) Which servers have at least one group member (shared inbox URLs only)
Bob's server A PrivateMessage arrived; with bto stripped, sees only itself as a recipient
Charlie's server Same

Local delivery gap: the receiving server has no individual recipient named, so it needs a way to know which local users to deliver to. This option is not workable as decribed.

Option C: Central group actor as address target

A Group actor (group@server-a, hosted on Alice's server) acts as the routing hub. Alice, Bob (bob@server-b), and Charlie (charlie@server-c) all follow it as members.

When Bob sends a message to the group:

  • Bob submits Create { PrivateMessage, to: [group@server-a] } via C2S
  • Bob's server delivers to exactly one destination: server-a's inbox
  • Bob's server never sees the follower list
  • Alice's server (hosting the group actor) looks up followers and fans out to server-b and server-c

What each server learns:

Server Learns
Alice's server (group host) Full follower list (since it does the fanout)
Bob's server Bob sent to group@server-a
Charlie's server Charlie received a message from the group

The privacy gain is for senders on other servers, their servers only learn the group actor's address. The group host (Alice's server here) is the single point that knows all members.

This requires servers to support a Group actor type, follower management, and a trusted server to host it (eg. the group creator's server). That server becomes the single point holding the full member list.

Option D1: Hub-and-spoke satellite groups

An extension of Option C. Rather than individual users following the origin group actor directly, each participating server also hosts a local satellite group actor. The origin group actor's followers are these per-server satellite actors, rather than individual users. Each satellite actor is in turn followed by that server's local members.

This would use only standard C2S and FEP-1b12 Group actor behaviour, just nesting it with groups following groups.

Structure:

  • group@server-a: origin group actor (Alice's server, acts as hub)
  • group-satellite@server-b: satellite group actor on Bob's server, followed by Bob
  • group-satellite@server-c: satellite group actor on Charlie's server, followed by Charlie

Provisioning via C2S:

  1. Alice's client via C2S Create { type: "Group" } → hub group@server-a created on server-a
  2. Alice invites Bob to the group (method TBD)
  3. Bob's client via C2S Create { type: "Group", context: "https://server-a/group" } on server-b → satellite group-satellite@server-b created (context indicates group affiliation, exact field TBD)
  4. Bob's client via C2S Follow { actor: "group-satellite@server-b", object: "group@server-a" } → server sends S2S Follow on behalf of the satellite; hub accepts (server should accept Bob acting on behalf of the group here, as its creator)
  5. Bob's client via C2S Follow { object: "group-satellite@server-b" } → Bob subscribes locally for inbound delivery
  6. Charlie's client follows the same steps on server-c

Message delivery:

All members (including Bob and Charlie) send directly to the hub:

  • Bob submits Create { PrivateMessage, to: [group@server-a] } via C2S; his server delivers S2S to server-a
  • The hub Announces to its followers: group-satellite@server-b and group-satellite@server-c
  • Each satellite Announces to its local followers: Bob and Charlie respectively

Satellites are delivery-only nodes: each satellite follows the hub so the hub can fan out to them, the hub does not follow the satellites and satellites do not follow each other.

What each server learns:

Server Learns
Alice's server (hub) Which servers have a satellite (server-level only, no individual identities)
Bob's server Bob is a member; group@server-a is the hub
Charlie's server Charlie received a message from the group

Privacy gain over Option C: The hub never learns individual member identities on other servers, only which servers have a satellite. Alice's server remains structurally central as the hub.

Option D2: Fully decentralised peer group actors

No origin or hub. Each server hosts a peer group actor; all peer group actors follow each other (mesh topology). Members address their local peer group actor.

Structure:

  • group@server-a: peer group actor, followed by Alice; follows group@server-b and group@server-c
  • group@server-b: peer group actor, followed by Bob; follows group@server-a and group@server-c
  • group@server-c: peer group actor, followed by Charlie; follows group@server-a and group@server-b

Provisioning via C2S:

  1. Alice C2S Create { type: "Group" }group@server-a created; Alice C2S Follows it locally
  2. Alice invites Bob to the group (method TBD)
  3. Bob C2S Create { type: "Group", context: "https://server-a/group" } on server-b → group@server-b created (context indicates peer affiliation, exact field TBD)
  4. Bob C2S Follow { actor: "group@server-b", object: "group@server-a" } → server sends S2S Follow; group@server-a accepts and sends a reciprocal S2S Follow back (mutual follow for mesh)
  5. Bob C2S Follow { object: "group@server-b" } → Bob subscribes locally
  6. When Charlie joins, the same steps repeat against all existing peers (O(n) explicit C2S Follow activities at join time)

Message delivery:

When Bob sends a message:

  • Bob submits Create { PrivateMessage, to: [group@server-b] } via C2S
  • group@server-b Announces to its followers: Bob gets a local copy, fans out to group@server-a and group@server-c
  • Each peer delivers to its local followers: Alice and Charlie respectively

What each server learns:

Server Learns
Alice's server Alice is a member; peer group actors exist on server-b and server-c
Bob's server Bob is a member; peer group actors exist on server-a and server-c
Charlie's server Charlie is a member; peer group actors exist on server-a and server-b

Privacy gain: No server is privileged. Each server knows only its own members and the existence of peer group actors on other server, not who those peers serve individually.

Additional requirement over D1: Loop prevention, so that when group@server-b fans out to group@server-a, server-a must not Announce it again to group@server-b (creating an infinite loop). Servers must deduplicate by activity ID: if a group actor has already processed a given activity ID, it silently drops it rather than re-announcing.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked discussion thread and the ActivityPub recipient-delivery semantics described here. Inspect the publisher path around preserve_privacy_of_outgoing, then compare Options A-D2 for what each server learns and how delivery works. The issue is not done until one approach is selected and its protocol requirements are specified.

Written by the indexing model from the issue text.

Assessment

Domain
backend-api-design, distributed-systems, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.