Skip to content
Open
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions proposals/4345-server-key-identity-and-room-membership.md
Copy link
Member

@tulir tulir Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation requirements:

  • Server (preferably multiple)
  • Client (preferably multiple)
  • Complement tests

Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
# MSC4345: Server key identity and room membership
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a fundamental fork-in-the-road choice here, when comparing this MSC to ones like MSC4243 (per-user keys). Both MSCs turn the DAG into a self-verifying data structure which is not reliant on DNS. The key difference is how they do this.

  • This MSC does this at the server level, introducing new state events to effectively represent "is this server key in the room?".
  • MSC4243 does this at the user level, which doesn't need new state events.

Neither MSC verifies the claimed domain for each server key: this is allowed to split brain which results in ugly user IDs appearing on clients (@alice:server-key for this MSC or @user-key:invalid for MSC4243) which need to be patched up when the domain is verified, so both end up looking like @alice:domain.

The fork in the road is ultimately which direction the protocol wants to go:

  • If we want to double down on "Matrix is a federation protocol, users must have some chaperone server when sending events" then this proposal fits that ideal better than MSC4243 because it bakes that "chaperone server" semantics explicitly into the protocol with their own set of keys.
  • If we want to double down on "Matrix is a peer-to-peer protocol, which happens to be currently be between servers but could be directly between users" then MSC4243 fits that ideal better because it promotes user keys to being the only key required to send valid events.

Note that this proposal can support P2P, by adding another layer of keys on top of the server keys (which is what MSC4348 does).

Federation protocols fundamentally need the server to know more information about their users, so it leaks more metadata than peer-to-peer protocols. For example, whilst this proposal can support P2P via MSC4348, it needs to know the PLs of all users on each server to know how to enforce the server participation event (the ambient power level in this MSC). Events also need to expose their sending node information in order to check the signatures on the "chaperone server/node". In contrast, P2P protocols can end up turning servers into store-and-forward nodes for encrypted data and that's it.

Copy link
Contributor Author

@Gnuxie Gnuxie Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to double down on "Matrix is a federation protocol, users must have some chaperone server when sending events" then this proposal fits that ideal better than MSC4243 because it bakes that "chaperone server" semantics explicitly into the protocol with their own set of keys.

This is not true. Please stop misrepresenting this proposal. #4348 shows how P2P matrix would work within this proposal. And it is a cleaner solution that does not tie accounts to any domain or server, MSC4243 does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't need to mislead anyone if i add my own commentary of the differences on MSC4243 in this manner. Because i have taken the time to understand MSC4243 through conversation with you. Please stop.

Copy link
Contributor Author

@Gnuxie Gnuxie Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also consider that you have had months to develop 4243 behind a placeholder in private. With support and consultation from others. I've written this proposal very quickly solo. Without praise or encouragement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still extend an invitation to you to develop this MSC with me collaboratively.


Events in Matrix are signed by the sending server's domain-scoped
sigining key, which is also a rotating key. During signature
verification, a server must obtain the public key used to sign an
event. This presents problems when the origin server is offline or has
been decommissioned. As this forces servers to rely on notaries to
supply historical keys.

There are several issues with this system that lead to inconsistent
views of the DAG:

- Centralisation of trust: Signature verification depends on notaries
being online and honest about historical keys.

- Fragility: Notaries may never have been present in the rooms that a
server is trying to join. This is especially true of matrix.org
which is the notary used by default in synapse. If no notary has the
key history for a given server, none of the events can be verified.

- Complexity and insecurity: Verifying authenticity of events is an
unnecessarily crossed concern with verification of ownership of a
domain.

This MSC is inspired the work of @kegsay in
[MSC4243](https://github.com/matrix-org/matrix-spec-proposals/pull/4243).

## Proposal
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should explain that we allow anyone with invite to set participation to permitted. But only people with ban can modify the participation to denied once a server has set their own participation from permitted to accepted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this so that the proposal works well for private rooms and public rooms.


We propose to tie the server's identity within a room to a long lived
ed25519 public key. This key is explicitly appended to the DAG via an
auth event. This event also sets the terms for routing information and
the server's participation within the room.

Therefore the DAG itself becomes a record of which keys were held by
participants, which eliminates the need for notaries in public key
discovery.

In addition, servers are unable to participate within a room until
their key has been added by an existing participant. This allows for
current participants to verify ownership of a domain before
participation is permitted.

We also introduce server participation, which allows servers to be
denied access to the room at the DAG level, and we also introduce
rules that allow servers to be removed without the need for
soft-failure.

This allows both public and private rooms to benefit from DAG
reproducibility and preemptive access control for servers without the
use of a policy server.

### The `m.server.participation` state event, `state_key: ${origin_server_key}`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to steal more prose from MSC4243 to describe the exact format for the server key and then use that consistently.


#### The `advertised_domain` property

This is a string representing the domain of the server. This is not an
attestation that ownership has been verified by the sender of the
event. This property is protected from redaction.

This property is not required, as it may be desirable to hide the
domain when setting the server's participation to `denied`. Particularly
in the event of attempted impersonation or an abusive domain name.

#### The `participation` property

`participation` can be one of `permitted`, `accepted` or
`denied`. `participation` is protected from redaction.

A denied server must not be sent a `m.server.participation` event
unless the targeted server is already present within the room. This is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're just stating existing behaviour (we don't tell servers who aren't in the room that we're talking about them)? If so, it's really confusing because it makes it seem like this is a really important part of the property, instead of just some edge case handling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware that this was existing behaviour. Even if it is, it seems like it is a really important part of the property right?

to prevent malicious servers being made aware of rooms that they have
not yet discovered.

#### The `reason` property

An optional reason property may be present in order to explain the
reason why a server has been denied or permitted to participate.

### Terminology for authorization

The _considered event extremities_ is the set of events provided by
`prev_events` and `auth_events` of the considered event.

The _considered event's acknowledged events_ is the set of events
connected to the `prev_events` of the considered event.

The _origin server's acknowledged extremities_ is the set of events
that are the tips of all DAG fragments which the origin server has
previously referenced in the `prev_events` of any event that has
already been authorized. This set is empty if the origin server has
not sent any prior events to the room.

The _origin server's acknowledged events_ is the set of events that
are connected to the _origin server's acknowledged extremities_ set,
including the _acknowledged extremities_ themselves.

### Key revocation

We define a _key revocation event_ to be an `m.server.participation`
event with the following properties:

1. The event's signature can be verified with the key found in the `state_key`.
2. The event's `participation` is `denied`.
3. The _considered event's acknowledged events_ is not a subset of the
_origin server's acknowledged events_.
4. If the current `participation` is `denied`:
1. If the current `participation` is not signed with the same key.
2. The _considered event's acknowledged events_ is equal to the
the _origin server's acknowledged events_.

### The `m.server.participation` authorization rule

These rules are to be inserted before rule 4 in [version
12](https://spec.matrix.org/v1.10/rooms/v11/#authorization-rules), the
check for `m.room.member`.

1. If type is `m.server.participation`:
1. If the sender's signature matches the `state_key` of the
considered event:
1. If the `participation` field of the considered event is
`denied`, allow.
1. If the `participation` field of the considered event is not
`accepted`, reject.
1. If the sender is a room owner, allow.
1. If the current participation state for the target is `permitted`
or `accepted`, allow.
1. Otherwise, reject.
2. If the `sender`'s current participation state is not `accepted`, reject.
3. If `participation` is `accepted`, reject[^participation-accept].
4. If there is no current participation state for the target:
2. If `partcipation` is `denied`:
1. If the `sender`'s power level is greater than or equal to the _ban level_,
is greater than or equal to the target server's ambient power level, allow.
2. Otherwise, reject.
3. If `participation` is `permitted`:
1. If the _target server_'s current participation state is `accepted`, reject.
4. If the _target server_'s current participation state is `denied`:
1. If the origin of the current participation state is the target key, reject[^revocation].
2. If the `sender`'s power level is less than the _ban
level_ or is less than the target server's ambient power
level, reject.
5. if the `sender`'s power level is greater than or equal to
the _invite level_, allow. 3. Otherwise, reject.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reusing the invite and ban permission could be inappropriate since this is equivalent to managing server ACL's in Matrix classic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invite is probably ok to reuse since those are equivalent... ban isn't though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know about this actually.. it might not matter. ACL was always more risky because it can have irreversible efects

Copy link
Contributor Author

@Gnuxie Gnuxie Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

denied has now been changed to be equivalent to revocation. So there will be disruptive effects for any room version that doesn't incorporate MSC4348 or something like it. Ie, in a version where users are resident to a server key (rather than serverless), those users will all have to be rejoined to a room.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs reworking anyways, see #4345 (comment)


5. If the `sender`'s current participation state is not `accepted`, reject.

[^participation-accept]:
This rule prevents anyone but the owner of
the key from setting the participation to accept

### The authorization rule for `denied` participation

This rule should be inserted at the beginning of auth rules and noted
in the description of soft failure
https://spec.matrix.org/latest/server-server-api/#soft-failure.

1. If the `sender`'s current participation is `denied`:
1. If the considered event is a _key revocation event_, allow[^revocation].
2. If the the _current participation_ event's _origin server's
acknowledged events_ does not include the considered event, reject.
3. Fall-through.

This rule exists to ensure that a consistent history is provided for
the _denied server_. It removes the avenue for the denied server to
reference stale state to append an infinite number of soft failed
events to the DAG. It also prevents the sender of the deny event from
placing the deny earlier in history to remove the target server's
events. Doing so will have the same effect as using the current
state.

[^revocation]:
This rule enforces that the owner of the key has total
autonomy over its revocation. Room admins cannot steal a key and
override this, and even if server admins set the server to deny,
the key owner can still revoke the key.

### The `/request_participation` endpoint
Copy link
Contributor Author

@Gnuxie Gnuxie Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subtext for this endpoint is that the requested server needs to create the participation event with one of its users. There may be objections to this, but in terms of client UI we're close enough with restricted join already in that the event says "bob joined via alice's server". So i'm not sure that argument alone can stand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs reworking see #4345 (comment)


When a server requests participation, the requested server should
verify that the joining server is claiming ownership of the provided
server key. The request should also be signed using the same server key.

Then, the requested server will emit an `m.server.participation` event
into the room with the key and the `advertised_domain` property filled
for the request origin.

Once this is complete, the requested server will respond with the information
required to begin interacting with the room.

When the joining server gets this response, it should immediately
change its own participation to `accepted` in order to prevent users
from overwriting the `advertised_domain`.

The following endpoint is defined: GET `/_matrix/federation/v1/request_participation/{roomId}/{serverKey}`.

The following query parameters are supported:

- `ver` the room versions the sending server has support for (identical to `make_join`).

- `omit_members` whether to omit members from the response (identical to `send_join`).

The response is identical to `send_join`.

### Changes to `/_matrix/key/v3/query`

`valid_until_ts` is removed. Keys are never time-bounded and
revocation is explicit via DAG state.

### Changes to the user ID format

- User ID _server name_'s are replaced with an ed25519 public key,
called the _server key_[^msc4243-prose].

- The private key for this _server key_ signs event JSON over federation[^msc4243-prose].

[^msc4243-prose]:
This wording is taken directly from MSC4243 and
shaped up a little

### Impositions on Client UI

Homeserver's must verify domain ownership before events are annotated
with `unsigned.server_domain`. Clients then use this to show a user's
server domain user ID rather than a user's server key user ID. Clients
should never use the `m.server.participation` `advertised_domain` to
show the origin of events.

Clients should encode the public key for displaying unverified
servers. Clients may also highlight this by deriving a stable colour
identity from the key.

Please suggest specific algorithms to make this consistent.

## Potential issues
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Server implementers should only allow server admins to revoke keys, and shouldn't let any user of theirs in a room to do it.


## Alternatives

### MSC4243: User ID localparts as Account Keys

This proposal is an alternative to [MSC4243: User ID localparts as
Account
Keys](https://github.com/matrix-org/matrix-spec-proposals/pull/4243)
and borrows several ideas from the same proposal. It is not required
reading. The key difference between these proposals is that this
proposal describes long lived identity for servers as a key pair in
Matrix rooms. Whereas MSC4243 only does so for individual user
accounts.

However, critically this MSC provides traceability to the origin of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually true? Nothing is verified in this proposal, so as per my sleeper server example you still have to play whack-a-mole with any malicious server. The only advantage this proposal provides is the implicit invite to public rooms, so you could feasibly do some investigation to see which server is letting in these other servers. It's probably not safe to actually take action against those servers though because that could be weaponsied (e.g I make a domain and valid server, I join via the victim server, then nuke the domain and spam freely, such that when investigators see who invited me they think the victim server is colluding when it isn't).

The exciting part of this proposal to me was the idea that this would provide better traceability to the origin of users, but sadly I just don't see how this proposal does this.

Copy link
Contributor Author

@Gnuxie Gnuxie Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So let's be specific. The sleeper server would have to have the invite permission. So the rooms where this can happen are going to be ones set to invite only AND where it is a choice to have the invite be the default power level (this is true for rooms created with the private preset I believe). If someone does this, their key can be revoked, and all the keys that they added / others added. So it is a game of wack-o-mole until you change the power level for invite when you figure out you are being attacked. Traceability exists because we can still see which servers added which keys and deny them. This is not possible at all currently for rooms that do not use join-restricted to ensure that each join event is signed by an existing participant. Whether anyone has a valid domain/invalid/attempts impersonation is not strictly relevant because the denies happen with relation to the server keys and not a domain. And a domain isn't going to be shown to anyone unless they themselves have verified ownership.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should break away from the invite power level though and require each participant to be explicitly granted this permission by a room admin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you'd hold the same standard for the invite power level in private rooms though? Since currently this same attack can happen in all matrix rooms where invite is default?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(e.g I make a domain and valid server, I join via the victim server, then nuke the domain and spam freely, such that when investigators see who invited me they think the victim server is colluding when it isn't).

@kegsay I don't understand this bit. Why would investigators conclude that spam events are originating from the victim server? What evidence would they use to conclude that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never said originating, I said colluding. The victim server "let in" the spam server.

usrs, whereas MSC4243 does not unless a policy server is in use to
sign each event.

### MSC4124: Simple Server Authorization

This proposal borrows the principle of constrained server membership
from MSC4124. Specifically changing authorization to stop
unencountered servers from suddenly appending an infinite amount of
data to the DAG.

### MSC4104: Auth Lock: Soft-failure-be-gone!

This proposal encodes a special auth rule for `denied` participation to
avoid soft failure and the problems discussed in MSC4104.

## Security considerations
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please allow keys to be scoped to a number of events. E.g. 1000 before rotation is required. See also #4353

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth requiring that authorization events have a separate limit to normal events.

Copy link
Contributor Author

@Gnuxie Gnuxie Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth doing the hard work of specifying keys as capabilities omg.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the awesome secure Matrix you can have! But you don't!

Copy link
Contributor Author

@Gnuxie Gnuxie Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably scope creep and should be a follow up MSC, see the-draupnir-project/planning#51

Copy link
Contributor Author

@Gnuxie Gnuxie Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a note in alternatives or issues before closing the thread.


See [Impositions on client UI](#impositions-on-client-ui).

## Unstable prefix

`m.server.participation` -> `org.matrix.msc4345.participation`

`_matrix` => `_matrix/msc4345/` or whatever the norm is here.

## Dependencies

None.