diff --git a/proposals/3917-cryptographic-membership.md b/proposals/3917-cryptographic-membership.md new file mode 100644 index 000000000..8bdf29699 --- /dev/null +++ b/proposals/3917-cryptographic-membership.md @@ -0,0 +1,559 @@ +# MSC3917: Cryptographically Constrained Room Membership + +In the current Matrix protocol, room membership events are not +cryptographically signed, except by homeservers during +federation. This means that a malicious homeserver can easily insert +additional members into an end-to-end encrypted room. The falsified +members will not receive keys for past messages, since those are only +shared by existing members when they invite new members, but the +falsified members will still be provided with keys for all new +messages. Although the new member joining the room will be visible to +all of the existing members, making it more difficult to perform such +an attack undetected, it would still be preferable to have a means for +clients to independently verify that a member actually belongs in a +room. + +This proposal provides a method for clients to sign room membership +events such that the room memberships form a tree of signatures rooted +in the creation of the room, ensuring that every member belongs to a +chain of invitations that ultimately leads back to the room's +creator. This establishes a cryptographically verifiable bounding set +of possible members of a room, significantly raising the barrier for +homeservers to inject unauthorized members into the room. + +In particular, this proposal provides the following security property: +**A user will only be verified as a possible room member if they +created the room, or if they joined the room with permission (explicit +or implicit) from another verified possible room member. As long as +state events are transmitted successfully, all such users will be +verified as possible room members.** In this context, a user is +defined as a Master Signing Key, and a set of devices with +Self-Signing Key signatures rooted in that MSK. A room is defined as a +Room Root Key, and a set of membership events with cryptographic +signatures rooted in that RRK. + +## Proposal + +### Rooms as Keys + +This proposal associates each room with a public signing key, which +will be the root of a tree of signed state events related to user +memberships. This key is called the Room Root Key, and is generated by +the room's creator at creation. + +In this proposal, the RRK will essentially become the cryptographic +identity of a room - being a member of the room, and being able to +verify others' membership in the room, requires knowing the RRK. Users +who disagree about a room's RRK are, for all intents and purposes, not +actually members of the same room. For this reason, we propose that +the RRK should *be* the room ID. Similar to +[MSC1228](https://github.com/matrix-org/matrix-spec-proposals/pull/1228), +the new format of a room ID will be `![unpadded urlsafe-base64ed +ed25519 public key]`, +e.g. `!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI`. However, note +that unlike MSC1228, in this proposal the key is generated +*client-side* by the room's creator, and not shared with the server. + +### Membership Event Signature Tree + +In what follows, we define a *cause-of-membership event* to be a +`join` event or `m.room.create` event that made a user a purported +member of a room. + +This proposal has each user generate a new cryptographic signing key +called the Room Signing Key, or RSK. The RSK is used for signing +certain types of room state events that the user sends (specifically, +invitations, joins, and join rule changes), so that other room members +can verify that the events were genuinely sent by that user. The RSK +should be signed by the Master Signing Key, and stored and retrieved +alongside the user's other signing keys. This key will be identified +by the string `m.cross_signing.room_signing`, and will be published to +the `/keys/device_signing/upload` endpoint using the new optional +field `room_signing_key`, with usage `["room_signing"]`. + +The proposal also adds additional fields to several room state events, +holding cryptographic signatures and related metadata: + +- The `m.room.create` event should have the following new content + fields: + + `room_root_key`: An Ed25519 public key generated for this specific + room by the room creator, henceforth called the Room Root Key + (RRK), that will serve as the root of the room membership + signature tree. + + `creator_key`: The public part of the room creator's Master + Signing Key. + + `signatures`: A signature of the event's `content` by the Room + Root Key, generated using the normal process for signing JSON + objects. For this purpose, the entity performing the signature is + the room ID, and the key identifier is `"rrk"`. + + Rooting room memberships in a Room Root Key, rather than directly in + the creator's MSK, means that each room has a unique root key, and + that as long as clients agree on the RRK, they will agree on the + validity of the entire signature tree. Essentially, the RRK *is* the + cryptographic identity of the room, just as an MSK is the + cryptographic identity of a user. + +
+Example event + +```json +{ + "type": "m.room.create", + "sender": "@alice:example.com", + "content": { + "room_version": "9", + "creator": "@alice:example.com", + "room_root_key" : "/ZK6paR+wBkKcazPx2xijn/0g+m2KCRqdCUZ6agzaaE", + "creator_key" : "D67j2Q4RixFBAikBWXb7NjokkRgTDVyeHyEHjl8Ib9", + "signatures" : { + "@alice:example.com" : { + "ed25519:rrk" : "iI98hykGBn0MuLopSysQYY/6bSaxuSZL05yRI+20P51RtfL3mwEHxSm7x6B3TMvAauxXX5hwohk8rqiWBDBWCQ" + } + } + }, + "state_key": "", + ... + "event_id": "$OSorlEHbz-xyfIaoy200IxyJAI2oTdOYFubheGwNr7c", + "room_id": "!_ZK6paR-wBkKcazPx2xijn_0g-m2KCRqdCUZ6agzaaE" +} +``` + +
+ +- The `m.room.member` event should have the following new content + fields when the membership state is `invite` or `join`, *unless* it + is an `invite` event created by the homeserver as a successor of an + `m.room.third_party_invite` event: + + `sender_key`: The sender's public Room Signing Key, signed by + their Master Signing Key, in the same `CrossSigningKey` format + used by the [`/keys/device_signing/upload` + endpoint](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3keysdevice_signingupload). + This field is provided in order to simplify the process of + connecting the sender's MSK to their RSK, particularly in cases + where the sender may no longer be in the room or may have even + deactivated their account. + + `parent_event_id`: + * If this is an `invite` event sent directly by a user, the + parent event is the inviter's cause-of-membership event. + * If this is a `join` event, the parent event is the `invite` + event or `m.room.join_rules` event that allowed this user to + join. + + `user_key`: The public MSK of the user whose membership is being + affected. + + `room_root_key`: The public RRK. + + `signatures`: A signature of this event's content by the sender's + RSK, generated using the normal process for signing JSON objects. + + `unsigned`: If this is a `join` event for a restricted room based + on membership in another room, and that other room has an RRK, + then the unsigned data must include the following field: + * `membership_events`: An array holding a chain of stripped state + events proving the user's possible membership in the room + specified in the join rule, starting with the + cause-of-membership event, and following parent events back to + the specified room's `m.room.create` event. + + The RRK is included in the signed data as a way of ensuring that + every new member agrees with the existing members on the true + RRK. If a member does not know the true RRK, they may later be + tricked into falsely believing that another user is a member of the + room, and share keys for their own messages with that user. + + If the `m.room.member` event *is* an `invite` event created as a + successor of a third-party invite, it must instead include the + following additional fields: + + `parent_event_id`: The ID of the `m.room.third_party_invite` + event. + + `third_party_invite`: + * `signed`: + - `user_key`: The public MSK of the user being invited. + +- The `m.room.join_rules` event should have the following new content + fields when the join rule is `public` or `restricted`: + + `sender_key`: The sender's public Room Signing Key, signed by + their Master Signing Key, in the same `CrossSigningKey` format + used by the [`/keys/device_signing/upload` + endpoint](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3keysdevice_signingupload). + This field is provided in order to simplify the process of + connecting the sender's MSK to their RSK, particularly in cases + where the sender may no longer be in the room or may have even + deactivated their account. + + `parent_event_id`: The ID of the sender's cause-of-membership + event. + + `signatures`: A signature of this event by the sender's RSK, + generated using the normal process for signing JSON objects. + +- The `m.room.third_party_invite` event should have the following new + content fields: + + `sender_key`: The sender's public Room Signing Key, signed by + their Master Signing Key, in the same `CrossSigningKey` format + used by the [`/keys/device_signing/upload` + endpoint](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3keysdevice_signingupload). + This field is provided in order to simplify the process of + connecting the sender's MSK to their RSK, particularly in cases + where the sender may no longer be in the room or may have even + deactivated their account. + + `parent_event_id`: The ID of the sender's cause-of-membership + event. + + `signatures`: A signature of this event's content by the sender's + RSK, generated using the normal process for signing JSON objects. + +Note that for these state events' content to be signed by clients, the +relevant client-server API endpoints will need to be updated so that +clients can submit complete signed event contents, rather than having +the homeserver generate the events from scratch: + +- The `creation_content` request field for the `/createRoom` endpoint + will now hold all signed content fields of the `m.room.create` + event. This includes the public part of the Room Root Key, and the + server *must* use the RRK as the room ID as described above. The + server may not modify any of the content fields, and may not add any + additional content fields except for data under `unsigned`. If the + event content provided is unacceptable for any reason, the server + should reject the request with a suitable error. + +- The body of a `/rooms/{roomId}/invite` request or of a `/join` + request will now hold all signed content fields of the + `m.room.member` event or `m.room.third_party_invite` event. + +For third-party invitations in particular, the client must now be the +one to communicate directly with the identity server and receive an +MXID to invite directly, or a token to publish in the +`m.room.third_party_invite` event. Furthermore, when the identity +server creates an identity mapping, it must learn the invited user's +public MSK, and include that in its signed data alongside the MXID and +token. + +With these fields in place, a user's cause-of-membership event can be +cryptographically verified via the following procedure: + +```mermaid +flowchart TD + start{{"What type of event
is the cause-of-membership event?"}} + + start ==>|join event|join_rrk{"Does the event
contain the correct RRK
and the user's MSK?"} + join_rrk -->|No|reject + join_rrk ==>|Yes|join_sig{"Does the event
have a valid signature
by the user's RSK?"} + join_sig -->|No|reject + join_sig ==>|Yes|join_rsk_msk{"Does the user's RSK
have a valid signature
by their MSK?"} + join_rsk_msk -->|No|reject + join_rsk_msk ==>|Yes|why_join{{"Look up
the join event's
parent event ID"}} + + why_join ==>|invite event|invite_kind{{"How was
the invite event
created?"}} + invite_kind ==>|Sent directly
by a user|invite_rrk{"Does the event
contain the correct RRK
and the user's MSK?"} + invite_rrk ---->|No|reject + invite_rrk =====>|Yes|check_sender{"Does the event
have a valid signature
by the sender's RSK?"} + check_sender -->|No|reject + check_sender ==>|Yes|sender_rsk_msk{"Does the sender's RSK
have a valid signature
by their MSK?"} + sender_rsk_msk ---->|No|reject + sender_rsk_msk ==>|Yes|lookup_sender_cause["Lookup this event's
parent event ID -
i.e., the sender's
cause-of-membership event"] + lookup_sender_cause==>sender_recurse{"Does the sender's
cause-of-membership event
pass verification?"} + sender_recurse -.-> start + sender_recurse -->|No|reject + sender_recurse =====>|Yes|accept + invite_kind ==>|Created by the homeserver
as a successor of an
m.room.third_party_invite event|idserver_sig{"Does the signed
third-party-invite data
have a valid signature
from an identity server?"} + idserver_sig -->|No|reject + idserver_sig ==>|Yes|threepid_signed_msk{"Does the signed data
contain the user's MSK?"} + threepid_signed_msk -->|No|reject + threepid_signed_msk ==>|Yes|lookup_threepid["Look up the parent
m.room.third_party_invite event"] + lookup_threepid ==> threepid_token{"Does the event
have a matching token,
and include the
identity server public key
that made the signature?"} + threepid_token -->|No|reject + threepid_token ==>|Yes|check_sender + + why_join ==>|m.room.join_rules event|join_rule_type{"What kind of
join rule?"} + join_rule_type==>|Public|check_sender + join_rule_type ==>|Restricted|any_old_rooms{"Does the restricted
join rule include any rooms
whose IDs are not RRKs?"} + any_old_rooms==>|Yes|check_sender + any_old_rooms==>|No|join_state{"Based on
the list of state events
provided in the join event,
does the user's cause-of-membership event
at the start of the list
pass verification?"} + join_state -.-> start + join_state ==>|Yes|joinrule_match{"Does the join rule event
include a room ID that
matches the room ID from
the provided state?"} + join_state ------>|No|reject + joinrule_match ===>|Yes|check_sender + joinrule_match -->|No|reject + + start ==>|m.room.create event|create_rrk{"Does the event
contain the correct RRK
and the user's MSK?"} + create_rrk -->|No|reject(["The user's cause-of-membership event
does NOT pass verification"]) + create_rrk ==>|Yes|create_sig{"Does the event
have a valid signature
by the RRK?"} + create_sig ------------->|No|reject + create_sig ======>|Yes|accept(["The user's cause-of-membership
event passes verification"]) + + style accept fill:#5f5 + style reject fill:#f66 +``` + +Note that this procedure specifically verifies that **a particular +MSK** may legitimately belong in the room. Devices that claim to +belong to a user, but are not signed by a Self-Signing Key signed by +that particular MSK, must not be treated as belonging in the room. + +If clients are unable to verify a user's cause-of-membership event for +a room, they may refuse to share cryptographic material in that room +with that user. + +As verification of the entire membership event chain *requires* +knowing the correct RRK for a room, it is critical that when joining a +new room, clients receive its RRK in a way that cannot easily be +falsified by the homeserver. In the case where a user is being +directly invited by an existing contact, they will receive an +`m.room.membership` invite event which contains the RRK and is signed +by the inviter; therefore, as long as they have the correct MSK for +their contact, they will have the correct RRK for the room. Simply +put, the authenticity of the room is exactly as strong as the +authenticity of communications with the inviter, which is an inherent +limit on any chat system. + +To address situations where users join a room without being directly +invited, we make the following additional changes: + +- The `m.space.child` event should have the following new content + fields: + + `sender_key`: The sender's public Room Signing Key, signed by + their Master Signing Key, in the same `CrossSigningKey` format + used by the [`/keys/device_signing/upload` + endpoint](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3keysdevice_signingupload). + This field is provided in order to simplify the process of + connecting the sender's MSK to their RSK, particularly in cases + where the sender may no longer be in the room or may have even + deactivated their account. + + `parent_event_id`: The ID of the sender's cause-of-membership + event. + + `room_root_key`: The RRK of the child room. This is already + provided by the child room's ID, which is the state key of the + event; however, it is duplicated here so that it will be included + in the signed event content. + + `signatures`: A signature of this event's content by the sender's + RSK, generated using the normal process for signing JSON objects. + + When checking the list of child rooms of a space, clients should + verify that `m.space.child` events are properly signed, and that the + senders' cause-of-membership events pass validation. + +- `matrix:` URIs and `matrix.to` URIs for room aliases should have an + additional query parameter `room_root_key`, holding the + base64-encoded RRK of the room. The authenticity of RRKs obtained + from these URIs is thus as strong as the authenticity of whatever + communications channel the URIs were sent through, which is again a + fundamental limit. + +## Potential issues + +The protocol outlined here is necessary, but not sufficient, to +determine definitively whether a user is a room member. It provides a +bounding set of cryptographically verifiable possible room members, +defined by a tree of signed state events in which users authorize +other users to join. It *does not* cryptographically verify +that the senders of those state events had the required power levels +at the time they issued the events, or that they didn't leave or get +banned between joining and issuing the events. + +Essentially, the primary goal of this proposal is to ensure that +attackers (including malicious homeservers) lose their ability to +*unilaterally* insert members into a room. In order to falsify room +membership, a malicious homeserver must collude with or compromise a +user who has been a legitimate member of the room at some point in +time; or tamper with communications between some existing room member +and a new member who they are inviting, and deceive the existing +member about the new member's MSK before they are invited. In a world +with ubiquitous TOFU via +[MSC3834](https://github.com/matrix-org/matrix-spec-proposals/pull/3834), +this deception must take place at the moment of first contact between +the existing member and the future new member, and they must never +attempt to verify each other out-of-band. + +If a user resets their MSK for any reason, their membership in the +room can no longer be verified. They will need to submit a new join +event, possibly being re-invited or re-joining a room from a +restricted join rule, before they can continue to be trusted as a room +member. Again, this is a fundamental limitation: in a scenario where +homeservers are untrusted, a user's MSK *must* be the root of their +entire identity, and any change to the MSK requires manually +re-establishing any and all trust that has been extended to that +user. Ideally, this work should be coupled with work to provide users +with more easy and reliable backup and recovery options, making MSK +resets as rare as possible. + +In order to participate in rooms belonging to the new room version in +this proposal, it is a hard requirement that clients support this +proposal at least well enough to add signatures and other required +fields to the state events they send, even if they are not interested +in verifying room membership themselves. It might be possible to allow +older clients to participate, but forbid them from inviting new users +or setting join rules; regardless, this presents an obstacle for +smoothly rolling out this proposal in Matrix's ecosystem. + +## Alternatives + +To obtain stronger cryptographic guarantees regarding room membership, +we could additionally mandate that *all* state events be signed +client-side, and that clients be given all the information they need +to perform their own state resolution without relying on the +server. This would allow clients to confirm that invitations were +issued by users with the correct power levels, among other +things. However, in order to prevent users who have left (or been +banned from) the room from colluding with the server to re-enter the +room or invite others, a more complex strategy is required. Even with +signatures on all state events, it is still easy for a server to +simply delete the membership event in which a user leaves or is +banned; likewise, the server can also delete state events where power +levels are changed to reduce a user's capabilities. + +To prevent the server from hiding state events, one option would be to +not only sign state events, but to sign their causal relationships to +other state events. Essentially, state events are a *directed acyclic +graph*, with edges leading from a state event to various other state +events that are causally prior to it. Every time a new state event is +sent, it must be signed client-side by its sender (using RSKs in the +same manner as above), and must include the full list of event IDs of +*source* nodes (nodes with no incoming edges) in the currently-known +state DAG - i.e., a minimal list of prior state event IDs sufficient to +establish that the entire known state DAG is causally prior to the new +state event. + +Now, in order for the server to discard a state event, it must also +discard *all* state events that are causally later than the discarded +state event, in perpetuity; at a bare minimum, all state events sent +by the sender of the discarded state event will need to be +discarded. However, if state events are only sent infrequently, it may +still be possible to perform this attack stealthily, without users +noticing that one of their members has stopped sending any state +events. For this reason, it would also be best to have all non-state +events include the list of source node event IDs in the +currently-known state DAG; in this case, rather than being signed by +RSKs, these lists would be protected by the authenticity guarantees of +the room's Megolm encryption. Now, in order to hide a single state +event, the server must permanently *silence* the sender entirely +within that room, as well as silencing any other users who have +received the state event. Note that non-state events do not become +*part of* the state DAG, and do not need to be linked-to in this way +by later events. + +Here is an example of the event history of a room with this +alternative expanded proposal. In this diagram, every thick arrow +represents a backreference from a new state event to an old state +event, within the state event content signed by an RSK; every thin +arrow represents a backreference from a non-state event to a state +event, protected by Megolm's authenticity properties. + +```mermaid +flowchart BT + create{{"m.room.create
(Alice creates the room)"}} + member0{{"m.room.member
(Alice invites Bee)"}}==>create + message0["m.room.message
Alice: “Hi!”"]-->member0 + message1["m.room.message
Bee: “hi Alice :)”"]--->member0 + powerlevel0{{"m.room.power_levels
(Alice makes Bee an admin)"}}====>member0 + member1{{"m.room.member
(Bee invites Caleb)"}}==>powerlevel0 + message2["m.room.message
Alice: “Giving you admin,
just please dont invite Caleb”"]-->powerlevel0 + powerlevel1{{"m.room.power_levels
(Bee makes Caleb moderator)"}}==>member1 + member2{{"m.room.member
(Alice bans Caleb)"}}==>member1 + member3{{"m.room.member
(Caleb invites Darrell)"}}==>powerlevel1 + message5["m.room.message
Darrell: “hi guys”"]-->member3 + message3["m.room.message
Bee: “agh sorry, I forgot
you and Caleb dont get along!”"]-->member2 + message3-->powerlevel1 + member4{{"m.room.member
(Alice invites Eloise)"}}====>member3 + message4["m.room.message
Alice: “It's okay, just please try
to remember next time.”"]--->member2 + message4-->member3 + member4 =====>member2 + message6["m.room.message
Darrell: “Wait am I still here?”"]-->member4 + message7["m.room.message
Bee: “i have no idea,
Eloise can you explain
how state resolution
works again?”"]--->member4 + + classDef ban fill:#ff6666; + classDef causal fill:#ffbbbb + class member2 ban; + class message3,message4,member4,message6,message7 causal; +``` + +Now, in order for a server to delete the event in which Alice bans +Caleb (highlighted in red), it would also need to delete *all* events +with edges leading to that event (highlighted in pink), or else +clients would easily be able to tell that room history had been +tampered with. + +In order to fully make use of this information, clients would also +need to be provided with all the information required to perform their +own state resolution independently of homeservers. Mainly, this would +be the `auth_events` and `origin_server_ts` fields of events in the +Server-Server API. Ideally, these would be signed client-side as well; +this is easy enough to do for `auth_events`, but ensuring that +`origin_server_ts` cannot be falsified (by clients or by servers) in +ways that would impact state resolution is a more complex problem. In +general, this alternative proposal has the potential to dovetail or +conflict with existing P2P Matrix work in complex ways, and would need +to be considered carefully in that light. + +## Threat modeling + +Here, we present a list of possible attacks, and how well these are +mitigated by this baseline proposal (of a membership signature tree); +by the expanded alternative version, with a full signed state DAG and +client-side state resolution; and by [Signal's system for managing +group memberships](https://eprint.iacr.org/2019/1416.pdf). + +- ✅: Mitigated +- 🚧: Partially mitigated +- ❌: Not mitigated + +| Attack | Membership tree | State DAG | Signal | +|---|---|---|---| +| Homeserver admin inserts a new member | ✅ | ✅ | ✅ | +| Homeserver admin re-inserts a banned member | ❌ | ✅ | ❌ | +| Banned member colludes with homeserver to insert a new member | ❌ | 🚧(1) | ❌ | +| Unprivileged member colludes with homeserver to insert a new member | ❌ | ✅(2) | ❌ | + + +1. Dependent on whether timestamps can be falsified to manipulate the + precedence of causally-unconnected events during state resolution. +2. If the user had invitation privileges at some point in the past, + this is only partially mitigated, as in the case of a banned member. + +## Security considerations + +If a room's join rule is ever set to `public`, or to `restricted` +based on a room with an older version that does not implement this +proposal, then all alleged room memberships will be accepted as +legitimate as long as the member signs their own join event and knows +the correct RRK. In the case of public rooms, this is not particularly +an issue, as verifying the membership of a public room is largely +meaningless to begin with. Allowing restricted rooms based on older +rooms is necessary for backwards compatibility, but since the +membership of these older rooms cannot be verified, there is no longer +a means of verifying the membership of a new room. Therefore, +particularly in the case of migrating from an old version to a new +version, room administrators will need to manually re-invite all +members they believe to be legitimate, rather than setting up a +restricted join rule. + +Third-party invites still inherently require trusting an identity +server to sign identity mappings correctly. This proposal, by +requiring that the inviter sign the `m.room.third_party_invite` event +content, does provide the added protection that a legitimate room +member must designate a list of trusted identity server public keys, +rather than leaving the choice up to the homeserver. Short of manually +verifying a user's third-party identity out-of-band and directly +issuing an invite to their MXID, there is no real way to add further +authenticity guarantees to this process. + +## Unstable prefix + +Newly-added field names will be prefixed with +`org.matrix.msc3917.v1`. Test implementations can identify the RSK as +`org.matrix.msc3917.v1.cross_signing.room_signing`, use the optional +field `org.matrix.msc3917.v1.room_signing_key` for the +`/keys/device_signing/upload` endpoint, and use the newly-added state +event content fields (other than `signatures` and `unsigned`) prefixed +by `org.matrix.msc3917.v1`. + +The updated endpoints for creating, inviting, and joining rooms will +all be prefixed with `/unstable/org.matrix.msc3917.v1/`. + +## Dependencies + +This MSC does not fundamentally require +[MSC3834](https://github.com/matrix-org/matrix-spec-proposals/pull/3834/) +(not yet accepted at the time of writing) in order to function, but +the security guarantees offered by this proposal are much stronger +when it is used in concert with TOFU. + +The rooms-as-keys aspect of this proposal is modeled after +[MSC1228](https://github.com/matrix-org/matrix-spec-proposals/pull/1228). However, +this proposal conflicts with MSC1228 in that we require Room Root Keys +to be generated client-side by room creators.