13 KiB
MSC1228: Removing MXIDs from events
Background
We would like to be able to break the association between a user's ID (such as
@richvdh:sw1v.org
) and their activity in a room.
The stretch goal is to also remove the association with server names, since for
many users, they are the only user on a server and it is reasonable to be able
to ask for the removal of any history of sw1v.org
's involvement with a room.
The general idea presented here is to use a pseudomym in many places where we
currently use user IDs. The current @user:server
then becomes a user alias;
the mapping between alias and the psuedonumous ID is public but can be removed
in the future.
User IDs currently appear in the following places in a room:
sender
of each eventstate_key
ofm.room.member
eventsusers
list inm.room.power_levels
eventscreatorUserId
in the content ofm.widget
Server names appear in the following places:
origin
of each event- keys in the
signatures
dict of each event - Room IDs
- Room Aliases
state_keys
inm.room.aliases
eventsmatrix.to
permalinks
Proposal
[This is v3 of this proposal, which in summary is: do proposal v1, but also
introduce a requirement for a global user_key
at the same time, which in
future will replace mxids as the user's One True Identity. v1 and v2 are
available for reference at
https://docs.google.com/document/d/1ni4LnC_vafX4h4K4sYNpmccS7QeHEFpAcYcbLS-J21Q#heading=h.y1krynr6itl4.]
-
Each user (currently identified by an mxid) will also have a
user_key
. In time, this will replace the mxid as your One True Identity; however for now they will live in parallel.-
A
user_key
is represented like~1:dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh
, where1:
is a version (to allow other systems to be used in future) and the rest is an (unpadded urlsafe-base64ed) ed25519 public key. -
Homeservers are responsible for making up keys for their users.
-
For now, each homeserver maintains a one-to-one mapping between
user_key
and mxid for each of their users. In future, we will look to break this link to allow portability of accounts.
-
-
Room IDs also become ed25519 public keys.
-
They look like:
!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI
. -
The server which creates the room is responsible for creating the keypair.
-
The
m.room.create
event is signed with the room id to stop people making new rooms which look like old ones. After this point, the private key is never needed again. 1
-
-
Define a
user_room_key
, which is yet another ed25519 public key.-
It looks like
^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw
. -
Homeservers are responsible for making up user keys for their users. They can (and should) use a different key in each room for each user.
-
This
user_room_key
is used where we currently use an mxid in the DAG:sender
,m.room.member
,m.room.power_levels
,creatorUserId
.creator
is removed as per MSC2175. -
Events are signed by the
user_room_key
of the sender instead of the server's key. -
If a user leaves and rejoins a room, they should use the same
user_room_key
(unless a server admin has manually removed the old mapping). This makes ban evasion harder. (It's up to server owners to ensure this rule is followed - servers which don't respect it and allow a serial abuser to evade bans by issuing differentuser_room_keys
are likely to suffer whole-server bans.)
-
-
Invite and join events include:
-
mxid_mapping
: field which gives the user's@user:server
mxid and which must be signed by the server in question, and the signature must be verified before the mapping is considered valid. -
user_mapping
: contains theuser_key
giving your True Identity, and signed by that key. The signature must be verified by receiving homeservers for it to be considered a valid invite/join event for a vNext room.
-
Examples
m.room.create
: signed by both the room key and the user_room_key
of the
sender:
{
"type": "m.room.create",
"state_key": "",
"room_id": "!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI",
"event_id": "$Riw5upWofaD4MNGM7bbZIj3bf+Th3fW/tklH4+6+VOg",
"sender": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
"content": {},
"origin_server_ts": 1459891964497,
"prev_events": [],
"prev_state": [],
"auth_events": [],
"signatures": {
"!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI": "<...>",
"^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw": "<...>"
},
"hashes":{"sha256":"3ASU57dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh/Uo"}
}
m.room.member
, showing mxid_mapping
and user_mapping
:
{
"type": "m.room.member",
"state_key": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
"room_id": "!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI",
"event_id": "$k21EhS3j8lhwqTi5NMTUH04oFyvR/1ujBGSWbW27aDs",
"sender": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
"content": {
"membership": "join",
"avatar_url": "...",
"displayname": "...",
"mxid_mapping": {
"user_room_key": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
"user_id": "@richvdh:matrix.org",
"signatures": {
"matrix.org": { "ed25519:a_zrXW": "<...>" }
}
},
"user_mapping": {
"user_key": "~1:dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh",
"user_room_key": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
"signatures": {
"~1:dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh": "<...>"
}
}
},
"origin_server_ts": 1489597048772,
"prev_events": [
"$Riw5upWofaD4MNGM7bbZIj3bf+Th3fW/tklH4+6+VOg"
],
"prev_state": [],
"auth_events": [
"$Riw5upWofaD4MNGM7bbZIj3bf+Th3fW/tklH4+6+VOg"
],
"signatures": {
"^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw": "<...>"
},
"hashes":{"sha256":"KLx4Alfa0QzOihSmUMZ1WQj5QdWnbMHwmqKxmYO8hJE"}
}
Handling the mxid mapping
When a server joins a room, it will be presented with a bunch of
m.room.member
events which may claim mappings onto mxids. There are three
reasons we need to know whether the mapping is valid:
- For clients, to help track users between rooms and to correlate to presence
- To authorise other servers to do backfill requests, etc.
- For outgoing messages, knowing which servers to send to.
None of these are particularly urgent (they all degrade fairly gracefully in
the case that a mapping is missed). A lot of the slowness in joining rooms
currently comes from having to pull server keys so that event signatures can be
verified during the join process, so it would be nice to be able to consider
the join complete as soon as the signature on the event is verified, and verify
the mxid_mappings
in the background.
Implementation
As a homeserver, we make an attempt to verify the sender before sending events to our clients.
-
When a new invite/join event turns up for a room you are already in (either via federation push, or because we pulled it via
/event/xxx
due to missingauth_events
/prev_events
):We make an attempt to verify its
mxid_mapping
before persisting it into our db. If the sig is wrong, we reject the event at that point.If we can't get the key (with a shortish timeout), we handle it as normal (and schedule a retry for later).
Typically we only care about the most recent2
mxid_mapping
for eachuser_room_key
; when we see another we can cancel any pending verification of any previous mapping3. Any previously-verified mapping should remain in place until another mapping becomes available. -
When we backfill:
We do the same thing, although in many cases we'll already have active mappings for the users in question, so we can ignore any received that way. By only honouring the most recent mapping, we gain the correct semantics for account portability: authorisation for backfill depends on the current location of the user, rather than wherever they were in the past.
-
When we join a new room:
For now we do the same thing (ie, make an attempt to verify the mxid mappings in the join events we receive, and time them out quickly). In future we might optimise this so the the mappings are verified lazily.
This should mean that in the majority of cases, we'll have a verified mxid by the time we send the event to a client.
For each user_room_key
, we therefore have:
- zero or one verified mxid mappings.
- zero or one incomplete mxid mappings.
We extend the CS API to include a verified_sender_mxid
field on any events
sent to a client where we know the user's current verified mxid.
- This is included in the interests of helping simple clients do the right thing most of the time - but it is annoying and dangerous because it will sometimes be missing. Still, we don't want to either (a) hold up all traffic in the room while we wait for a verification which may never succeed; (b) hold back some events while we do a verification for a sender; (c) require that all clients always have to wait for an asynchronous verification and match them up.
We also add a new field to the /sync
response which tells clients about
mxid mappings as they are resolved.
Question: should we remove unverified mxid mappings from join events before serving them to the clients, to stop client developers relying on it and breaking everything?
Sending invites
We have a bootstrapping problem for invites in that, until a user joins a room,
we don't know their user_room_key
.
Also: invite events are supposed to be signed by the invitee, so that other members of the room can be sure that they have actually received a copy of the event.
The current invite dance is:
- inviting server builds a complete invite event, and signs it
- inviting server sends a copy to invited server, along with some (unsigned) state about the room: name, avatar, inviting user's join event
- invited server checks that request came from server of inviting user
- invited server also signs the event, and tells the user about it
- inviting server adds the (double-signed) event to the DAG and sends it to the rest of the federation (including the invited server, if it was already in the room)
We could change this to:
- inviting server builds a partial invite event
- inviting server PUTs to
/_matrix/federation/v3/invite/<event_id>/<target_mxid>
on invited server - invited server checks that request came from server of inviting user
- invited server adds:
user_room_key
in state_key- mxid attestation
- signature
- invited server tells the user about it, and returns the completed event to inviting server
- inviting server adds its signature to the complete event
- inviting server adds the (double-signed) event to the DAG and sends it to the rest of the federation (including the invited server, if it was already in the room)
Problems
How to handle the state keys in room_aliases
events?
How to handle server names in matrix.to
permalinks?
What if somebody does a join with an inappropriate avatar/displayname? If we redact their join, we'll redact their identity assertion too :/
Other things that might fall out nicely
- Fixes broken backfill due to changed signing keys (matrix-org/synapse#3121)
- Case sensitive mxid comparison problems?
- Opens a path to killing off perspectives in favour of just asking servers what their keys are (via TLS, with trust coming from X.509 certificates).
- Helping with the domain reuse problem
- Nicer way of validating redactions by comparing the
user_room_key
of the message and the redaction which addresses the suboptimal solution introduced by MSC1659 - …
Stuff we might want to avoid designing out
-
Ability to migrate users between servers by changing their mapping assertions
-
Ability to support alternative identity mapping assertions rather than being strictly mxid->user_key (e.g. 3pid->user-key too). This could help decentralised identity mappings in general in future, and possibly unify 3pid invites with normal invites? It could also support discovering servers by key rather than DNS (e.g. via DHT), which would be useful for p2p in future.
Key management
If a private user key gets lost, they can just start using a new one and
announce a new mapping; however this may require an update to the
power_levels
to give rights to the new user.
If a private user key is compromised, then again we start using a new one and
announce a new mapping, so that new events from the old key wouldn't look like
they came from that user. Again it may need power_level
s updates to remove
power from the old user_key
, but I think that is fair: you give away the keys
to your privileged account, you have to expect some cleanup. Ideally we would
have a way of revoking the old key properly, but this can be deferred for now.
[1] although we might think about letting its use confer some sort of founder semantics.↩
[2] or more accurately, the one in the current room state.↩
[3] if a user/server spams out mappings so quickly that none of them ever complete, that is their own loss.↩