Merge bddf787b01 into e9f0f31d27
commit
c50568c528
@ -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.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example event</summary>
|
||||||
|
|
||||||
|
```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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
- 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<br/>is the cause-of-membership event?"}}
|
||||||
|
|
||||||
|
start ==>|<code>join</code> event|join_rrk{"Does the event<br/>contain the correct RRK<br/>and the user's MSK?"}
|
||||||
|
join_rrk -->|No|reject
|
||||||
|
join_rrk ==>|Yes|join_sig{"Does the event<br/>have a valid signature<br/>by the user's RSK?"}
|
||||||
|
join_sig -->|No|reject
|
||||||
|
join_sig ==>|Yes|join_rsk_msk{"Does the user's RSK<br/>have a valid signature<br/>by their MSK?"}
|
||||||
|
join_rsk_msk -->|No|reject
|
||||||
|
join_rsk_msk ==>|Yes|why_join{{"Look up<br/>the <code>join</code> event's<br/>parent event ID"}}
|
||||||
|
|
||||||
|
why_join ==>|<code>invite</code> event|invite_kind{{"How was<br/>the <code>invite</code> event<br/>created?"}}
|
||||||
|
invite_kind ==>|Sent directly<br/>by a user|invite_rrk{"Does the event<br/>contain the correct RRK<br/>and the user's MSK?"}
|
||||||
|
invite_rrk ---->|No|reject
|
||||||
|
invite_rrk =====>|Yes|check_sender{"Does the event<br/>have a valid signature<br/>by the sender's RSK?"}
|
||||||
|
check_sender -->|No|reject
|
||||||
|
check_sender ==>|Yes|sender_rsk_msk{"Does the sender's RSK<br/>have a valid signature<br/>by their MSK?"}
|
||||||
|
sender_rsk_msk ---->|No|reject
|
||||||
|
sender_rsk_msk ==>|Yes|lookup_sender_cause["Lookup this event's<br/>parent event ID -<br/>i.e., the sender's<br/>cause-of-membership event"]
|
||||||
|
lookup_sender_cause==>sender_recurse{"Does the sender's<br/>cause-of-membership event<br/>pass verification?"}
|
||||||
|
sender_recurse -.-> start
|
||||||
|
sender_recurse -->|No|reject
|
||||||
|
sender_recurse =====>|Yes|accept
|
||||||
|
invite_kind ==>|Created by the homeserver<br/>as a successor of an<br/><code>m.room.third_party_invite</code> event|idserver_sig{"Does the signed<br/>third-party-invite data<br/>have a valid signature<br/>from an identity server?"}
|
||||||
|
idserver_sig -->|No|reject
|
||||||
|
idserver_sig ==>|Yes|threepid_signed_msk{"Does the signed data<br/>contain the user's MSK?"}
|
||||||
|
threepid_signed_msk -->|No|reject
|
||||||
|
threepid_signed_msk ==>|Yes|lookup_threepid["Look up the parent<br/><code>m.room.third_party_invite</code> event"]
|
||||||
|
lookup_threepid ==> threepid_token{"Does the event<br/>have a matching token,<br/>and include the<br/>identity server public key<br/>that made the signature?"}
|
||||||
|
threepid_token -->|No|reject
|
||||||
|
threepid_token ==>|Yes|check_sender
|
||||||
|
|
||||||
|
why_join ==>|<code>m.room.join_rules</code> event|join_rule_type{"What kind of<br/>join rule?"}
|
||||||
|
join_rule_type==>|Public|check_sender
|
||||||
|
join_rule_type ==>|Restricted|any_old_rooms{"Does the restricted<br/>join rule include any rooms<br/>whose IDs are not RRKs?"}
|
||||||
|
any_old_rooms==>|Yes|check_sender
|
||||||
|
any_old_rooms==>|No|join_state{"Based on<br/>the list of state events<br/>provided in the <code>join</code> event,<br/>does the user's cause-of-membership event<br/>at the start of the list<br/>pass verification?"}
|
||||||
|
join_state -.-> start
|
||||||
|
join_state ==>|Yes|joinrule_match{"Does the join rule event<br/>include a room ID that<br/>matches the room ID from<br/>the provided state?"}
|
||||||
|
join_state ------>|No|reject
|
||||||
|
joinrule_match ===>|Yes|check_sender
|
||||||
|
joinrule_match -->|No|reject
|
||||||
|
|
||||||
|
start ==>|<code>m.room.create</code> event|create_rrk{"Does the event<br/>contain the correct RRK<br/>and the user's MSK?"}
|
||||||
|
create_rrk -->|No|reject(["The user's cause-of-membership event<br/>does NOT pass verification"])
|
||||||
|
create_rrk ==>|Yes|create_sig{"Does the event<br/>have a valid signature<br/>by the RRK?"}
|
||||||
|
create_sig ------------->|No|reject
|
||||||
|
create_sig ======>|Yes|accept(["The user's cause-of-membership<br/>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{{"<code>m.room.create</code><br/>(Alice creates the room)"}}
|
||||||
|
member0{{"<code>m.room.member</code><br/>(Alice invites Bee)"}}==>create
|
||||||
|
message0["<code>m.room.message</code><br/><b>Alice:</b> “Hi!”"]-->member0
|
||||||
|
message1["<code>m.room.message</code><br/><b>Bee:</b> “hi Alice :)”"]--->member0
|
||||||
|
powerlevel0{{"<code>m.room.power_levels</code><br/>(Alice makes Bee an admin)"}}====>member0
|
||||||
|
member1{{"<code>m.room.member</code><br/>(Bee invites Caleb)"}}==>powerlevel0
|
||||||
|
message2["<code>m.room.message</code><br/><b>Alice:</b> “Giving you admin,<br/>just please dont invite Caleb”"]-->powerlevel0
|
||||||
|
powerlevel1{{"<code>m.room.power_levels</code><br/>(Bee makes Caleb moderator)"}}==>member1
|
||||||
|
member2{{"<code>m.room.member</code><br/>(Alice bans Caleb)"}}==>member1
|
||||||
|
member3{{"<code>m.room.member</code><br/>(Caleb invites Darrell)"}}==>powerlevel1
|
||||||
|
message5["<code>m.room.message</code><br/><b>Darrell:</b> “hi guys”"]-->member3
|
||||||
|
message3["<code>m.room.message</code><br/><b>Bee:</b> “agh sorry, I forgot<br/>you and Caleb dont get along!”"]-->member2
|
||||||
|
message3-->powerlevel1
|
||||||
|
member4{{"<code>m.room.member</code><br/>(Alice invites Eloise)"}}====>member3
|
||||||
|
message4["<code>m.room.message</code><br/><b>Alice:</b> “It's okay, just please try<br/>to remember next time.”"]--->member2
|
||||||
|
message4-->member3
|
||||||
|
member4 =====>member2
|
||||||
|
message6["<code>m.room.message</code><br/><b>Darrell:</b> “Wait am I still here?”"]-->member4
|
||||||
|
message7["<code>m.room.message</code><br/><b>Bee:</b> “i have no idea,<br/>Eloise can you explain<br/>how state resolution<br/>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.
|
||||||
Loading…
Reference in New Issue